ajustes nos scripts de calibragem e o raw processor core para compatibilidade local
This commit is contained in:
parent
73722a5db1
commit
744065b888
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -33,17 +33,22 @@ IMPORTANTE:
|
||||||
Fluxo:
|
Fluxo:
|
||||||
1) Mostre o ChArUco nas 3 câmeras.
|
1) Mostre o ChArUco nas 3 câmeras.
|
||||||
2) ENTER trava EXP/ISO.
|
2) ENTER trava EXP/ISO.
|
||||||
3) G captura views variadas.
|
3) R seleciona a ROI operacional da câmera escolhida (opcional).
|
||||||
4) Varie X/Y, distância, yaw, pitch e roll.
|
4) Enquadre o ChArUco no quadrilátero-guia.
|
||||||
5) Ideal: 18-25 views.
|
5) G captura e avança; F força captura fora da tolerância do guia.
|
||||||
6) A calibra e executa acceptance.
|
6) O roteiro varia X/Y, distância, yaw, pitch e roll.
|
||||||
7) PASS promove o JSON ativo.
|
7) Ideal: completar as 25 poses guiadas.
|
||||||
|
8) A calibra e executa acceptance.
|
||||||
|
9) PASS promove o JSON ativo.
|
||||||
|
|
||||||
Teclas:
|
Teclas:
|
||||||
G captura view
|
G captura view quando o guia estiver READY
|
||||||
|
F força captura válida fora da tolerância do guia
|
||||||
A calibra/avalia
|
A calibra/avalia
|
||||||
V limpa views
|
V limpa views
|
||||||
S snapshot
|
S snapshot
|
||||||
|
R seleciona ROI operacional da câmera escolhida
|
||||||
|
C restaura frame completo na câmera escolhida
|
||||||
1/2/3 seleciona câmera do preview
|
1/2/3 seleciona câmera do preview
|
||||||
U liga/desliga preview undistorted após avaliação
|
U liga/desliga preview undistorted após avaliação
|
||||||
Q/ESC cancela
|
Q/ESC cancela
|
||||||
|
|
@ -106,9 +111,9 @@ PREVIEW_ORIENTATION_BY_SENSOR = {
|
||||||
|
|
||||||
QA = {
|
QA = {
|
||||||
"min_markers": 4,
|
"min_markers": 4,
|
||||||
"min_corners": 10,
|
"min_corners": 20,
|
||||||
"min_views": 14,
|
"min_views": 14,
|
||||||
"recommended_views": 20,
|
"recommended_views": 25,
|
||||||
"min_retained_views": 12,
|
"min_retained_views": 12,
|
||||||
"min_unique_ids": 24,
|
"min_unique_ids": 24,
|
||||||
|
|
||||||
|
|
@ -168,11 +173,26 @@ QA = {
|
||||||
"iso_tol": 5,
|
"iso_tol": 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GUIDE = {
|
||||||
|
"center_tolerance_norm": 0.085,
|
||||||
|
"area_ratio_min": 0.68,
|
||||||
|
"area_ratio_max": 1.48,
|
||||||
|
"quad_rms_tolerance_norm": 0.135,
|
||||||
|
"live_detection_period_s": 0.25,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def now_str():
|
def now_str():
|
||||||
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
|
||||||
|
def calc_log(message):
|
||||||
|
print(
|
||||||
|
f"[CALC {datetime.now().strftime('%H:%M:%S')}] {message}",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def stamp():
|
def stamp():
|
||||||
return datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
return datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
||||||
|
|
||||||
|
|
@ -269,6 +289,197 @@ def apply_preview_orientation(img: np.ndarray, cfg: dict) -> np.ndarray:
|
||||||
return np.ascontiguousarray(img)
|
return np.ascontiguousarray(img)
|
||||||
|
|
||||||
|
|
||||||
|
def oriented_size(shape_hw, cfg):
|
||||||
|
h, w = shape_hw
|
||||||
|
cfg = normalize_preview_orientation(cfg)
|
||||||
|
return (h, w) if cfg["rotate_deg"] in (90, 270) else (w, h)
|
||||||
|
|
||||||
|
|
||||||
|
def orient_points(points, shape_hw, cfg):
|
||||||
|
"""Converte pontos SENSOR-NATIVE para o preview canônico."""
|
||||||
|
pts = np.asarray(points, dtype=np.float32).reshape(-1, 2).copy()
|
||||||
|
h, w = shape_hw
|
||||||
|
cfg = normalize_preview_orientation(cfg)
|
||||||
|
rot = cfg["rotate_deg"]
|
||||||
|
|
||||||
|
x, y = pts[:, 0].copy(), pts[:, 1].copy()
|
||||||
|
if rot == 90:
|
||||||
|
pts[:, 0], pts[:, 1] = h - 1 - y, x
|
||||||
|
ow, oh = h, w
|
||||||
|
elif rot == 180:
|
||||||
|
pts[:, 0], pts[:, 1] = w - 1 - x, h - 1 - y
|
||||||
|
ow, oh = w, h
|
||||||
|
elif rot == 270:
|
||||||
|
pts[:, 0], pts[:, 1] = y, w - 1 - x
|
||||||
|
ow, oh = h, w
|
||||||
|
else:
|
||||||
|
ow, oh = w, h
|
||||||
|
|
||||||
|
if cfg["flip_horizontal"]:
|
||||||
|
pts[:, 0] = ow - 1 - pts[:, 0]
|
||||||
|
if cfg["flip_vertical"]:
|
||||||
|
pts[:, 1] = oh - 1 - pts[:, 1]
|
||||||
|
return pts
|
||||||
|
|
||||||
|
|
||||||
|
def unorient_points(points, native_shape_hw, cfg):
|
||||||
|
"""Converte pontos do preview canônico de volta ao SENSOR-NATIVE."""
|
||||||
|
pts = np.asarray(points, dtype=np.float32).reshape(-1, 2).copy()
|
||||||
|
h, w = native_shape_hw
|
||||||
|
cfg = normalize_preview_orientation(cfg)
|
||||||
|
ow, oh = oriented_size(native_shape_hw, cfg)
|
||||||
|
|
||||||
|
if cfg["flip_horizontal"]:
|
||||||
|
pts[:, 0] = ow - 1 - pts[:, 0]
|
||||||
|
if cfg["flip_vertical"]:
|
||||||
|
pts[:, 1] = oh - 1 - pts[:, 1]
|
||||||
|
|
||||||
|
x, y = pts[:, 0].copy(), pts[:, 1].copy()
|
||||||
|
rot = cfg["rotate_deg"]
|
||||||
|
if rot == 90:
|
||||||
|
pts[:, 0], pts[:, 1] = y, h - 1 - x
|
||||||
|
elif rot == 180:
|
||||||
|
pts[:, 0], pts[:, 1] = w - 1 - x, h - 1 - y
|
||||||
|
elif rot == 270:
|
||||||
|
pts[:, 0], pts[:, 1] = w - 1 - y, x
|
||||||
|
return pts
|
||||||
|
|
||||||
|
|
||||||
|
def full_roi(spec):
|
||||||
|
return {"x": 0, "y": 0, "width": int(spec.width), "height": int(spec.height)}
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_roi(roi, image_size):
|
||||||
|
w, h = image_size
|
||||||
|
if roi is None:
|
||||||
|
return {"x": 0, "y": 0, "width": int(w), "height": int(h)}
|
||||||
|
x = max(0, min(int(roi["x"]), w - 1))
|
||||||
|
y = max(0, min(int(roi["y"]), h - 1))
|
||||||
|
rw = max(1, min(int(roi["width"]), w - x))
|
||||||
|
rh = max(1, min(int(roi["height"]), h - y))
|
||||||
|
if rw < max(80, int(0.15 * w)) or rh < max(80, int(0.15 * h)):
|
||||||
|
raise ValueError("ROI operacional pequena demais.")
|
||||||
|
return {"x": x, "y": y, "width": rw, "height": rh}
|
||||||
|
|
||||||
|
|
||||||
|
def roi_native_corners(roi):
|
||||||
|
x, y, w, h = roi["x"], roi["y"], roi["width"], roi["height"]
|
||||||
|
return np.asarray([
|
||||||
|
[x, y], [x + w - 1, y],
|
||||||
|
[x + w - 1, y + h - 1], [x, y + h - 1],
|
||||||
|
], dtype=np.float32)
|
||||||
|
|
||||||
|
|
||||||
|
def roi_in_preview(roi, shape_hw, orientation):
|
||||||
|
pts = orient_points(roi_native_corners(roi), shape_hw, orientation)
|
||||||
|
x0, y0 = pts.min(axis=0)
|
||||||
|
x1, y1 = pts.max(axis=0)
|
||||||
|
return {"x": float(x0), "y": float(y0),
|
||||||
|
"width": float(x1 - x0 + 1), "height": float(y1 - y0 + 1)}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_roi_arg(text, spec):
|
||||||
|
if not text:
|
||||||
|
return full_roi(spec)
|
||||||
|
try:
|
||||||
|
values = [int(x.strip()) for x in str(text).split(",")]
|
||||||
|
if len(values) != 4:
|
||||||
|
raise ValueError
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ValueError("ROI deve usar x,y,width,height") from exc
|
||||||
|
return normalize_roi(
|
||||||
|
{"x": values[0], "y": values[1],
|
||||||
|
"width": values[2], "height": values[3]},
|
||||||
|
(spec.width, spec.height),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def select_operational_roi(frame_native, role, orientation):
|
||||||
|
"""Seleção visual canônica; retorno sempre em coordenadas nativas."""
|
||||||
|
shown = apply_preview_orientation(frame_native, orientation)
|
||||||
|
oh, ow = shown.shape[:2]
|
||||||
|
scale = min(1.0, 1200.0 / ow, 760.0 / oh)
|
||||||
|
display = cv2.resize(
|
||||||
|
shown, (int(round(ow * scale)), int(round(oh * scale))),
|
||||||
|
interpolation=cv2.INTER_AREA,
|
||||||
|
)
|
||||||
|
rect = cv2.selectROI(
|
||||||
|
f"ROI operacional {role.upper()} | ENTER confirma | C cancela",
|
||||||
|
display, showCrosshair=True, fromCenter=False,
|
||||||
|
)
|
||||||
|
cv2.destroyWindow(f"ROI operacional {role.upper()} | ENTER confirma | C cancela")
|
||||||
|
x, y, rw, rh = rect
|
||||||
|
if rw <= 0 or rh <= 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
x0, y0 = x / scale, y / scale
|
||||||
|
x1, y1 = (x + rw - 1) / scale, (y + rh - 1) / scale
|
||||||
|
canonical = np.asarray([[x0, y0], [x1, y0], [x1, y1], [x0, y1]],
|
||||||
|
dtype=np.float32)
|
||||||
|
native = unorient_points(canonical, frame_native.shape[:2], orientation)
|
||||||
|
nx0, ny0 = np.floor(native.min(axis=0)).astype(int)
|
||||||
|
nx1, ny1 = np.ceil(native.max(axis=0)).astype(int)
|
||||||
|
return normalize_roi(
|
||||||
|
{"x": nx0, "y": ny0, "width": nx1 - nx0 + 1, "height": ny1 - ny0 + 1},
|
||||||
|
(frame_native.shape[1], frame_native.shape[0]),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _guide_quad(cx, cy, width, height, roll=0.0, yaw=0.0, pitch=0.0):
|
||||||
|
"""Quadrilátero normalizado TL,TR,BR,BL dentro da ROI canônica."""
|
||||||
|
sx_top = 1.0 - pitch
|
||||||
|
sx_bottom = 1.0 + pitch
|
||||||
|
sy_left = 1.0 - yaw
|
||||||
|
sy_right = 1.0 + yaw
|
||||||
|
pts = np.asarray([
|
||||||
|
[-0.5 * width * sx_top, -0.5 * height * sy_left],
|
||||||
|
[ 0.5 * width * sx_top, -0.5 * height * sy_right],
|
||||||
|
[ 0.5 * width * sx_bottom, 0.5 * height * sy_right],
|
||||||
|
[-0.5 * width * sx_bottom, 0.5 * height * sy_left],
|
||||||
|
], dtype=np.float32)
|
||||||
|
a = math.radians(roll)
|
||||||
|
rot = np.asarray([[math.cos(a), -math.sin(a)],
|
||||||
|
[math.sin(a), math.cos(a)]], dtype=np.float32)
|
||||||
|
pts = pts @ rot.T
|
||||||
|
pts += np.asarray([cx, cy], dtype=np.float32)
|
||||||
|
return np.clip(pts, 0.035, 0.965).tolist()
|
||||||
|
|
||||||
|
|
||||||
|
def build_guide_targets():
|
||||||
|
raw = [
|
||||||
|
("CENTRO LONGE", .50, .50, .30, .26, 0, 0, 0),
|
||||||
|
("CENTRO MEDIO", .50, .50, .48, .41, 0, 0, 0),
|
||||||
|
("CENTRO PERTO", .50, .50, .68, .58, 0, 0, 0),
|
||||||
|
("ESQUERDA", .28, .50, .44, .38, 0, 0, 0),
|
||||||
|
("DIREITA", .72, .50, .44, .38, 0, 0, 0),
|
||||||
|
("SUPERIOR", .50, .28, .44, .38, 0, 0, 0),
|
||||||
|
("INFERIOR", .50, .72, .44, .38, 0, 0, 0),
|
||||||
|
("CANTO SUP ESQ", .28, .28, .40, .34, 0, 0, 0),
|
||||||
|
("CANTO SUP DIR", .72, .28, .40, .34, 0, 0, 0),
|
||||||
|
("CANTO INF ESQ", .28, .72, .40, .34, 0, 0, 0),
|
||||||
|
("CANTO INF DIR", .72, .72, .40, .34, 0, 0, 0),
|
||||||
|
("ESQ PERTO / INCLINE", .31, .50, .58, .49, 0, .22, 0),
|
||||||
|
("DIR PERTO / INCLINE", .69, .50, .58, .49, 0, -.22, 0),
|
||||||
|
("SUPERIOR PERTO", .50, .33, .57, .48, 0, 0, .18),
|
||||||
|
("INFERIOR PERTO", .50, .67, .57, .48, 0, 0, -.18),
|
||||||
|
("YAW ESQUERDA", .37, .48, .50, .42, 0, .28, 0),
|
||||||
|
("YAW DIREITA", .63, .52, .50, .42, 0, -.28, 0),
|
||||||
|
("YAW SUP ESQ", .34, .34, .45, .38, 0, -.24, 0),
|
||||||
|
("YAW INF DIR", .66, .66, .45, .38, 0, .24, 0),
|
||||||
|
("PITCH SUPERIOR", .48, .34, .50, .42, 0, 0, .24),
|
||||||
|
("PITCH INFERIOR", .52, .66, .50, .42, 0, 0, -.24),
|
||||||
|
("PITCH SUP DIR", .66, .35, .44, .37, 0, 0, -.22),
|
||||||
|
("PITCH INF ESQ", .34, .65, .44, .37, 0, 0, .22),
|
||||||
|
("DIAGONAL +", .43, .48, .52, .42, 14, .18, .14),
|
||||||
|
("DIAGONAL -", .57, .52, .42, .35, -14, -.18, -.14),
|
||||||
|
]
|
||||||
|
return [
|
||||||
|
{"index": i, "name": x[0],
|
||||||
|
"quad_norm": _guide_quad(*x[1:])}
|
||||||
|
for i, x in enumerate(raw, start=1)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def resolve_preview_orientation(specs, args):
|
def resolve_preview_orientation(specs, args):
|
||||||
"""
|
"""
|
||||||
Resolve orientação visual por sensor, com override opcional por role.
|
Resolve orientação visual por sensor, com override opcional por role.
|
||||||
|
|
@ -889,17 +1100,109 @@ def detect_triplet(triplet, board, dictionary, params):
|
||||||
# Pose/cobertura
|
# Pose/cobertura
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
def pose_signature(points, shape_hw):
|
def order_quad(points):
|
||||||
|
pts = np.asarray(points, dtype=np.float32).reshape(4, 2)
|
||||||
|
s = pts.sum(axis=1)
|
||||||
|
d = np.diff(pts, axis=1).reshape(-1)
|
||||||
|
return np.asarray([
|
||||||
|
pts[np.argmin(s)], pts[np.argmin(d)],
|
||||||
|
pts[np.argmax(s)], pts[np.argmax(d)],
|
||||||
|
], dtype=np.float32)
|
||||||
|
|
||||||
|
|
||||||
|
def detected_board_quad(detection, board_pts):
|
||||||
|
points = detection.get("points", {})
|
||||||
|
ids = sorted(int(x) for x in points.keys())
|
||||||
|
if len(ids) < 4:
|
||||||
|
return None
|
||||||
|
src = np.asarray([board_pts[i][:2] for i in ids], dtype=np.float32)
|
||||||
|
dst = np.asarray([
|
||||||
|
points[str(i)] if str(i) in points else points[i] for i in ids
|
||||||
|
], dtype=np.float32)
|
||||||
|
H, _ = cv2.findHomography(src, dst, method=0)
|
||||||
|
if H is None:
|
||||||
|
return None
|
||||||
|
all_xy = np.asarray(board_pts, dtype=np.float32)[:, :2]
|
||||||
|
x0, y0 = all_xy.min(axis=0)
|
||||||
|
x1, y1 = all_xy.max(axis=0)
|
||||||
|
obj_quad = np.asarray(
|
||||||
|
[[[x0, y0]], [[x1, y0]], [[x1, y1]], [[x0, y1]]],
|
||||||
|
dtype=np.float32,
|
||||||
|
)
|
||||||
|
projected = cv2.perspectiveTransform(obj_quad, H).reshape(4, 2)
|
||||||
|
return order_quad(projected)
|
||||||
|
|
||||||
|
|
||||||
|
def target_quad_preview(target, roi_preview):
|
||||||
|
q = np.asarray(target["quad_norm"], dtype=np.float32)
|
||||||
|
q[:, 0] = roi_preview["x"] + q[:, 0] * roi_preview["width"]
|
||||||
|
q[:, 1] = roi_preview["y"] + q[:, 1] * roi_preview["height"]
|
||||||
|
return order_quad(q)
|
||||||
|
|
||||||
|
|
||||||
|
def assess_guide(detection, board_pts, shape_hw, orientation, roi, target):
|
||||||
|
observed_native = detected_board_quad(detection, board_pts)
|
||||||
|
if observed_native is None:
|
||||||
|
return {"ready": False, "instruction": "MOSTRE O CHARUCO",
|
||||||
|
"observed_preview": None, "target_preview": None}
|
||||||
|
|
||||||
|
observed = order_quad(orient_points(observed_native, shape_hw, orientation))
|
||||||
|
rp = roi_in_preview(roi, shape_hw, orientation)
|
||||||
|
wanted = target_quad_preview(target, rp)
|
||||||
|
diag = max(math.hypot(rp["width"], rp["height"]), 1.0)
|
||||||
|
|
||||||
|
oc = observed.mean(axis=0)
|
||||||
|
tc = wanted.mean(axis=0)
|
||||||
|
delta = (oc - tc) / np.asarray([rp["width"], rp["height"]])
|
||||||
|
center_error = float(np.linalg.norm(delta))
|
||||||
|
observed_area = abs(float(cv2.contourArea(observed)))
|
||||||
|
target_area = max(abs(float(cv2.contourArea(wanted))), 1.0)
|
||||||
|
area_ratio = observed_area / target_area
|
||||||
|
quad_rms = float(np.sqrt(np.mean(np.sum((observed - wanted) ** 2, axis=1))) / diag)
|
||||||
|
|
||||||
|
ready = (
|
||||||
|
center_error <= GUIDE["center_tolerance_norm"]
|
||||||
|
and GUIDE["area_ratio_min"] <= area_ratio <= GUIDE["area_ratio_max"]
|
||||||
|
and quad_rms <= GUIDE["quad_rms_tolerance_norm"]
|
||||||
|
)
|
||||||
|
|
||||||
|
if abs(delta[0]) > GUIDE["center_tolerance_norm"] * 0.70:
|
||||||
|
instruction = "MOVA PARA ESQUERDA" if delta[0] > 0 else "MOVA PARA DIREITA"
|
||||||
|
elif abs(delta[1]) > GUIDE["center_tolerance_norm"] * 0.70:
|
||||||
|
instruction = "MOVA PARA CIMA" if delta[1] > 0 else "MOVA PARA BAIXO"
|
||||||
|
elif area_ratio < GUIDE["area_ratio_min"]:
|
||||||
|
instruction = "APROXIME"
|
||||||
|
elif area_ratio > GUIDE["area_ratio_max"]:
|
||||||
|
instruction = "AFASTE"
|
||||||
|
elif quad_rms > GUIDE["quad_rms_tolerance_norm"]:
|
||||||
|
instruction = "AJUSTE ANGULO/PERSPECTIVA"
|
||||||
|
else:
|
||||||
|
instruction = "READY - PRESSIONE G"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"ready": bool(ready),
|
||||||
|
"instruction": instruction,
|
||||||
|
"center_error_norm": center_error,
|
||||||
|
"area_ratio": float(area_ratio),
|
||||||
|
"quad_rms_norm": quad_rms,
|
||||||
|
"observed_preview": observed.tolist(),
|
||||||
|
"target_preview": wanted.tolist(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def pose_signature(points, shape_hw, roi=None):
|
||||||
pts = np.asarray(list(points.values()), dtype=np.float32)
|
pts = np.asarray(list(points.values()), dtype=np.float32)
|
||||||
h, w = shape_hw
|
h, w = shape_hw
|
||||||
|
roi = normalize_roi(roi, (w, h))
|
||||||
|
|
||||||
x0, x1 = float(pts[:, 0].min()), float(pts[:, 0].max())
|
x0, x1 = float(pts[:, 0].min()), float(pts[:, 0].max())
|
||||||
y0, y1 = float(pts[:, 1].min()), float(pts[:, 1].max())
|
y0, y1 = float(pts[:, 1].min()), float(pts[:, 1].max())
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"cx": ((x0 + x1) * 0.5) / w,
|
"cx": (((x0 + x1) * 0.5) - roi["x"]) / roi["width"],
|
||||||
"cy": ((y0 + y1) * 0.5) / h,
|
"cy": (((y0 + y1) * 0.5) - roi["y"]) / roi["height"],
|
||||||
"area": ((x1 - x0) * (y1 - y0)) / float(w * h),
|
"area": ((x1 - x0) * (y1 - y0)) /
|
||||||
|
float(roi["width"] * roi["height"]),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -928,19 +1231,26 @@ def duplicate_pose(sig, views):
|
||||||
return repeated == 3
|
return repeated == 3
|
||||||
|
|
||||||
|
|
||||||
def coverage(points, image_size):
|
def coverage(points, image_size, roi=None):
|
||||||
pts = np.asarray(points, dtype=np.float32)
|
pts = np.asarray(points, dtype=np.float32)
|
||||||
w, h = image_size
|
w, h = image_size
|
||||||
|
roi = normalize_roi(roi, image_size)
|
||||||
|
|
||||||
if len(pts) < 3:
|
if len(pts) < 3:
|
||||||
return {"span_x": 0.0, "span_y": 0.0, "hull": 0.0}
|
return {"span_x": 0.0, "span_y": 0.0, "hull": 0.0,
|
||||||
|
"operational_roi_native": roi}
|
||||||
|
|
||||||
hull = cv2.convexHull(pts.reshape(-1, 1, 2))
|
clipped = pts.copy()
|
||||||
|
clipped[:, 0] = np.clip(clipped[:, 0], roi["x"], roi["x"] + roi["width"] - 1)
|
||||||
|
clipped[:, 1] = np.clip(clipped[:, 1], roi["y"], roi["y"] + roi["height"] - 1)
|
||||||
|
hull = cv2.convexHull(clipped.reshape(-1, 1, 2))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"span_x": float((pts[:, 0].max() - pts[:, 0].min()) / w),
|
"span_x": float((clipped[:, 0].max() - clipped[:, 0].min()) / roi["width"]),
|
||||||
"span_y": float((pts[:, 1].max() - pts[:, 1].min()) / h),
|
"span_y": float((clipped[:, 1].max() - clipped[:, 1].min()) / roi["height"]),
|
||||||
"hull": float(cv2.contourArea(hull) / (w * h)),
|
"hull": float(cv2.contourArea(hull) /
|
||||||
|
(roi["width"] * roi["height"])),
|
||||||
|
"operational_roi_native": roi,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1107,6 +1417,11 @@ def iterative_reject(views, role, board_pts, image_size, model_name):
|
||||||
rounds = []
|
rounds = []
|
||||||
|
|
||||||
for round_idx in range(QA["max_reject_rounds"] + 1):
|
for round_idx in range(QA["max_reject_rounds"] + 1):
|
||||||
|
calc_log(
|
||||||
|
f"{role.upper()}/{model_name}: ajuste robusto "
|
||||||
|
f"{round_idx + 1}/{QA['max_reject_rounds'] + 1} "
|
||||||
|
f"| views={len(retained)}"
|
||||||
|
)
|
||||||
ds = build_dataset(views, role, board_pts, retained)
|
ds = build_dataset(views, role, board_pts, retained)
|
||||||
|
|
||||||
if len(ds["objects"]) < QA["min_retained_views"]:
|
if len(ds["objects"]) < QA["min_retained_views"]:
|
||||||
|
|
@ -1156,7 +1471,15 @@ def leave_one_out(views, role, board_pts, image_size, model_name, retained):
|
||||||
if len(retained) < 6:
|
if len(retained) < 6:
|
||||||
return {"valid": False, "views": [], "overall": {}}
|
return {"valid": False, "views": [], "overall": {}}
|
||||||
|
|
||||||
for held in retained:
|
total = len(retained)
|
||||||
|
progress_step = max(1, math.ceil(total / 5))
|
||||||
|
|
||||||
|
for held_idx, held in enumerate(retained, start=1):
|
||||||
|
if held_idx == 1 or held_idx == total or held_idx % progress_step == 0:
|
||||||
|
calc_log(
|
||||||
|
f"{role.upper()}/{model_name}: validação cruzada "
|
||||||
|
f"{held_idx}/{total}"
|
||||||
|
)
|
||||||
train_ids = [x for x in retained if x != held]
|
train_ids = [x for x in retained if x != held]
|
||||||
train = build_dataset(views, role, board_pts, train_ids)
|
train = build_dataset(views, role, board_pts, train_ids)
|
||||||
test = build_dataset(views, role, board_pts, [held])
|
test = build_dataset(views, role, board_pts, [held])
|
||||||
|
|
@ -1208,10 +1531,11 @@ def leave_one_out(views, role, board_pts, image_size, model_name, retained):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def distortion_shift(K, D, image_size):
|
def distortion_shift(K, D, image_size, roi=None):
|
||||||
w, h = image_size
|
w, h = image_size
|
||||||
xs = np.linspace(0, w - 1, 21)
|
roi = normalize_roi(roi, image_size)
|
||||||
ys = np.linspace(0, h - 1, 15)
|
xs = np.linspace(roi["x"], roi["x"] + roi["width"] - 1, 21)
|
||||||
|
ys = np.linspace(roi["y"], roi["y"] + roi["height"] - 1, 15)
|
||||||
|
|
||||||
pts = np.asarray([[x, y] for y in ys for x in xs],
|
pts = np.asarray([[x, y] for y in ys for x in xs],
|
||||||
dtype=np.float32).reshape(-1, 1, 2)
|
dtype=np.float32).reshape(-1, 1, 2)
|
||||||
|
|
@ -1221,10 +1545,10 @@ def distortion_shift(K, D, image_size):
|
||||||
shift = np.linalg.norm(und - orig, axis=1)
|
shift = np.linalg.norm(und - orig, axis=1)
|
||||||
|
|
||||||
edge = (
|
edge = (
|
||||||
(orig[:, 0] < 0.15 * w)
|
(orig[:, 0] < roi["x"] + 0.15 * roi["width"])
|
||||||
| (orig[:, 0] > 0.85 * w)
|
| (orig[:, 0] > roi["x"] + 0.85 * roi["width"])
|
||||||
| (orig[:, 1] < 0.15 * h)
|
| (orig[:, 1] < roi["y"] + 0.15 * roi["height"])
|
||||||
| (orig[:, 1] > 0.85 * h)
|
| (orig[:, 1] > roi["y"] + 0.85 * roi["height"])
|
||||||
)
|
)
|
||||||
|
|
||||||
e = shift[edge]
|
e = shift[edge]
|
||||||
|
|
@ -1236,6 +1560,7 @@ def distortion_shift(K, D, image_size):
|
||||||
"edge_median_px": float(np.median(e)),
|
"edge_median_px": float(np.median(e)),
|
||||||
"edge_p95_px": float(np.percentile(e, 95)),
|
"edge_p95_px": float(np.percentile(e, 95)),
|
||||||
"edge_max_px": float(e.max()),
|
"edge_max_px": float(e.max()),
|
||||||
|
"operational_roi_native": roi,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1304,7 +1629,8 @@ def sanity(model, image_size):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def evaluate_model(views, role, board_pts, image_size, model_name):
|
def evaluate_model(views, role, board_pts, image_size, model_name,
|
||||||
|
operational_roi=None):
|
||||||
rej = iterative_reject(
|
rej = iterative_reject(
|
||||||
views, role, board_pts, image_size, model_name
|
views, role, board_pts, image_size, model_name
|
||||||
)
|
)
|
||||||
|
|
@ -1330,11 +1656,12 @@ def evaluate_model(views, role, board_pts, image_size, model_name):
|
||||||
for im in ds["images"]:
|
for im in ds["images"]:
|
||||||
all_points.extend(np.asarray(im).reshape(-1, 2).tolist())
|
all_points.extend(np.asarray(im).reshape(-1, 2).tolist())
|
||||||
|
|
||||||
cov = coverage(all_points, image_size)
|
cov = coverage(all_points, image_size, operational_roi)
|
||||||
retained_views = [v for v in views if int(v["view_id"]) in set(rej["retained"])]
|
retained_views = [v for v in views if int(v["view_id"]) in set(rej["retained"])]
|
||||||
div = diversity(retained_views, role)
|
div = diversity(retained_views, role)
|
||||||
sane = sanity(model, image_size)
|
sane = sanity(model, image_size)
|
||||||
shift = distortion_shift(model["K"], model["D"], image_size)
|
shift = distortion_shift(model["K"], model["D"], image_size, operational_roi)
|
||||||
|
shift_full_frame = distortion_shift(model["K"], model["D"], image_size)
|
||||||
und = undistort_products(model["K"], model["D"], image_size)
|
und = undistort_products(model["K"], model["D"], image_size)
|
||||||
|
|
||||||
status = sane["status"]
|
status = sane["status"]
|
||||||
|
|
@ -1420,6 +1747,8 @@ def evaluate_model(views, role, board_pts, image_size, model_name):
|
||||||
"unique_charuco_ids": len(unique_ids),
|
"unique_charuco_ids": len(unique_ids),
|
||||||
"sanity": sane,
|
"sanity": sane,
|
||||||
"distortion_shift": shift,
|
"distortion_shift": shift,
|
||||||
|
"distortion_shift_full_frame": shift_full_frame,
|
||||||
|
"operational_roi_native": normalize_roi(operational_roi, image_size),
|
||||||
"undistort_products": und,
|
"undistort_products": und,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1457,24 +1786,55 @@ def choose_model(brown, rational, requested):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def evaluate_all(views, board_pts, specs, requested):
|
def evaluate_all(views, board_pts, specs, requested, operational_rois=None):
|
||||||
cameras = {}
|
cameras = {}
|
||||||
overall = "good"
|
overall = "good"
|
||||||
reasons = []
|
reasons = []
|
||||||
|
operational_rois = operational_rois or {
|
||||||
|
role: full_roi(specs[role]) for role in ROLES
|
||||||
|
}
|
||||||
|
|
||||||
for role in ROLES:
|
for camera_idx, role in enumerate(ROLES, start=1):
|
||||||
size = (specs[role].width, specs[role].height)
|
size = (specs[role].width, specs[role].height)
|
||||||
|
roi = normalize_roi(operational_rois.get(role), size)
|
||||||
|
|
||||||
brown = evaluate_model(
|
calc_log(
|
||||||
views, role, board_pts, size, "brown5"
|
f"Câmera {camera_idx}/{len(ROLES)}: {role.upper()} "
|
||||||
|
f"{size[0]}x{size[1]} | ROI="
|
||||||
|
f"{roi['x']},{roi['y']},{roi['width']},{roi['height']}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
started = time.perf_counter()
|
||||||
|
calc_log(f"{role.upper()}: iniciando modelo Brown5...")
|
||||||
|
brown = evaluate_model(
|
||||||
|
views, role, board_pts, size, "brown5", roi
|
||||||
|
)
|
||||||
|
calc_log(
|
||||||
|
f"{role.upper()}: Brown5 concluído em "
|
||||||
|
f"{time.perf_counter() - started:.1f}s | "
|
||||||
|
f"status={brown['status'].upper()} | "
|
||||||
|
f"reasons={' | '.join(brown.get('reasons', [])) or 'nenhuma'}"
|
||||||
|
)
|
||||||
|
|
||||||
|
started = time.perf_counter()
|
||||||
|
calc_log(f"{role.upper()}: iniciando modelo Rational8...")
|
||||||
rational = evaluate_model(
|
rational = evaluate_model(
|
||||||
views, role, board_pts, size, "rational8"
|
views, role, board_pts, size, "rational8", roi
|
||||||
|
)
|
||||||
|
calc_log(
|
||||||
|
f"{role.upper()}: Rational8 concluído em "
|
||||||
|
f"{time.perf_counter() - started:.1f}s | "
|
||||||
|
f"status={rational['status'].upper()} | "
|
||||||
|
f"reasons={' | '.join(rational.get('reasons', [])) or 'nenhuma'}"
|
||||||
)
|
)
|
||||||
|
|
||||||
selected, selection = choose_model(
|
selected, selection = choose_model(
|
||||||
brown, rational, requested
|
brown, rational, requested
|
||||||
)
|
)
|
||||||
|
calc_log(
|
||||||
|
f"{role.upper()}: selecionado {selection['selected']} "
|
||||||
|
f"| motivo={selection['reason']}"
|
||||||
|
)
|
||||||
|
|
||||||
edge = selected.get("distortion_shift", {}).get("edge_p95_px", 0.0)
|
edge = selected.get("distortion_shift", {}).get("edge_p95_px", 0.0)
|
||||||
|
|
||||||
|
|
@ -1519,6 +1879,9 @@ def module_fragment(evaluation, specs):
|
||||||
"sensor": specs[role].sensor,
|
"sensor": specs[role].sensor,
|
||||||
"stream_source": specs[role].source,
|
"stream_source": specs[role].source,
|
||||||
"image_size": [specs[role].width, specs[role].height],
|
"image_size": [specs[role].width, specs[role].height],
|
||||||
|
"operational_roi_native": m.get(
|
||||||
|
"operational_roi_native", full_roi(specs[role])
|
||||||
|
),
|
||||||
"distortion_model": cam["model_selection"]["selected"],
|
"distortion_model": cam["model_selection"]["selected"],
|
||||||
"camera_matrix": m["camera_matrix"],
|
"camera_matrix": m["camera_matrix"],
|
||||||
"dist_coeffs": m["dist_coeffs"],
|
"dist_coeffs": m["dist_coeffs"],
|
||||||
|
|
@ -1532,6 +1895,10 @@ def module_fragment(evaluation, specs):
|
||||||
"intrinsics_config": {
|
"intrinsics_config": {
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"calibration_space": CALIBRATION_SPACE,
|
"calibration_space": CALIBRATION_SPACE,
|
||||||
|
"operational_roi_contract": (
|
||||||
|
"ROI afeta somente guia e acceptance. K/D e corners "
|
||||||
|
"permanecem no raster nativo completo."
|
||||||
|
),
|
||||||
"cameras": cameras,
|
"cameras": cameras,
|
||||||
"runtime_undistort": {
|
"runtime_undistort": {
|
||||||
"enabled": False,
|
"enabled": False,
|
||||||
|
|
@ -1648,6 +2015,10 @@ def build_ui(
|
||||||
show_undist,
|
show_undist,
|
||||||
args,
|
args,
|
||||||
preview_orientation,
|
preview_orientation,
|
||||||
|
operational_rois=None,
|
||||||
|
guide_role="rgb",
|
||||||
|
guide_target=None,
|
||||||
|
guide_assessment=None,
|
||||||
msg="",
|
msg="",
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
|
|
@ -1662,6 +2033,9 @@ def build_ui(
|
||||||
"re": (0, 255, 255),
|
"re": (0, 255, 255),
|
||||||
"nir": (255, 255, 0),
|
"nir": (255, 255, 0),
|
||||||
}
|
}
|
||||||
|
operational_rois = operational_rois or {
|
||||||
|
role: full_roi(specs[role]) for role in ROLES
|
||||||
|
}
|
||||||
|
|
||||||
for role in ROLES:
|
for role in ROLES:
|
||||||
frame_native = runtime.frames[role]
|
frame_native = runtime.frames[role]
|
||||||
|
|
@ -1706,6 +2080,39 @@ def build_ui(
|
||||||
preview_orientation[role],
|
preview_orientation[role],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ROI é exibida na orientação canônica, mas armazenada em SENSOR-NATIVE.
|
||||||
|
rp = roi_in_preview(
|
||||||
|
operational_rois[role], frame_native.shape[:2],
|
||||||
|
preview_orientation[role],
|
||||||
|
)
|
||||||
|
roi_poly = np.asarray([
|
||||||
|
[rp["x"], rp["y"]],
|
||||||
|
[rp["x"] + rp["width"] - 1, rp["y"]],
|
||||||
|
[rp["x"] + rp["width"] - 1, rp["y"] + rp["height"] - 1],
|
||||||
|
[rp["x"], rp["y"] + rp["height"] - 1],
|
||||||
|
], dtype=np.int32).reshape(-1, 1, 2)
|
||||||
|
cv2.polylines(bgr, [roi_poly], True, (255, 180, 0), 4, cv2.LINE_AA)
|
||||||
|
|
||||||
|
if role == guide_role and guide_target is not None and not showing_undist:
|
||||||
|
target = target_quad_preview(guide_target, rp)
|
||||||
|
cv2.polylines(
|
||||||
|
bgr, [np.rint(target).astype(np.int32).reshape(-1, 1, 2)],
|
||||||
|
True, (0, 220, 255), 6, cv2.LINE_AA,
|
||||||
|
)
|
||||||
|
if guide_assessment and guide_assessment.get("observed_preview") is not None:
|
||||||
|
observed = np.asarray(
|
||||||
|
guide_assessment["observed_preview"], dtype=np.float32
|
||||||
|
)
|
||||||
|
guide_color = (
|
||||||
|
(0, 255, 0) if guide_assessment.get("ready")
|
||||||
|
else (0, 0, 255)
|
||||||
|
)
|
||||||
|
cv2.polylines(
|
||||||
|
bgr,
|
||||||
|
[np.rint(observed).astype(np.int32).reshape(-1, 1, 2)],
|
||||||
|
True, guide_color, 6, cv2.LINE_AA,
|
||||||
|
)
|
||||||
|
|
||||||
p = cv2.resize(
|
p = cv2.resize(
|
||||||
bgr,
|
bgr,
|
||||||
(pw, ph),
|
(pw, ph),
|
||||||
|
|
@ -1734,6 +2141,12 @@ def build_ui(
|
||||||
f"EXP={runtime.ctrl[role].get('exposure_time_us')}us "
|
f"EXP={runtime.ctrl[role].get('exposure_time_us')}us "
|
||||||
f"ISO={runtime.ctrl[role].get('sensitivity_iso')}"
|
f"ISO={runtime.ctrl[role].get('sensitivity_iso')}"
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
f"ROI={operational_rois[role]['x']},"
|
||||||
|
f"{operational_rois[role]['y']},"
|
||||||
|
f"{operational_rois[role]['width']},"
|
||||||
|
f"{operational_rois[role]['height']}"
|
||||||
|
),
|
||||||
],
|
],
|
||||||
x=8,
|
x=8,
|
||||||
y=18,
|
y=18,
|
||||||
|
|
@ -1752,12 +2165,30 @@ def build_ui(
|
||||||
f"preview={selected_role.upper()} undist={'ON' if show_undist else 'OFF'}",
|
f"preview={selected_role.upper()} undist={'ON' if show_undist else 'OFF'}",
|
||||||
f"measurement=NATIVE | display_rot={selected_orient['rotate_deg']}",
|
f"measurement=NATIVE | display_rot={selected_orient['rotate_deg']}",
|
||||||
"",
|
"",
|
||||||
"G capture | A calibrate",
|
"G guided capture | F force valid",
|
||||||
"V clear | S snapshot",
|
"A calibrate | V clear | S snapshot",
|
||||||
"1/2/3 select | U undist preview",
|
"R select ROI | C full frame | 1/2/3",
|
||||||
"Q/ESC cancel",
|
"U undist preview | Q/ESC cancel",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if guide_target is not None:
|
||||||
|
instruction = (
|
||||||
|
guide_assessment.get("instruction", "PROCURANDO CHARUCO")
|
||||||
|
if guide_assessment else "PROCURANDO CHARUCO"
|
||||||
|
)
|
||||||
|
lines += [
|
||||||
|
"",
|
||||||
|
f"GUIDE {guide_target['index']:02d}/25 [{guide_role.upper()}]",
|
||||||
|
guide_target["name"],
|
||||||
|
instruction,
|
||||||
|
]
|
||||||
|
if guide_assessment and guide_assessment.get("center_error_norm") is not None:
|
||||||
|
lines.append(
|
||||||
|
f"center={guide_assessment['center_error_norm']:.3f} "
|
||||||
|
f"area={guide_assessment['area_ratio']:.2f} "
|
||||||
|
f"quad={guide_assessment['quad_rms_norm']:.3f}"
|
||||||
|
)
|
||||||
|
|
||||||
if evaluation is not None:
|
if evaluation is not None:
|
||||||
lines += ["", f"EVAL={evaluation['status'].upper()}"]
|
lines += ["", f"EVAL={evaluation['status'].upper()}"]
|
||||||
|
|
||||||
|
|
@ -1827,6 +2258,23 @@ def main():
|
||||||
ap.add_argument("--panel-height", type=int, default=400)
|
ap.add_argument("--panel-height", type=int, default=400)
|
||||||
ap.add_argument("--preview-alpha", type=float, default=0.0,
|
ap.add_argument("--preview-alpha", type=float, default=0.0,
|
||||||
choices=[0.0, 0.5, 1.0])
|
choices=[0.0, 0.5, 1.0])
|
||||||
|
ap.add_argument(
|
||||||
|
"--guide-role", default="rgb", choices=list(ROLES),
|
||||||
|
help="Câmera usada para o enquadramento visual das poses guiadas.",
|
||||||
|
)
|
||||||
|
ap.add_argument(
|
||||||
|
"--no-guide", action="store_true",
|
||||||
|
help="Desativa o roteiro guiado; mantém captura livre por G.",
|
||||||
|
)
|
||||||
|
for role in ROLES:
|
||||||
|
ap.add_argument(
|
||||||
|
f"--{role}-operational-roi", default="",
|
||||||
|
metavar="X,Y,W,H",
|
||||||
|
help=(
|
||||||
|
"ROI operacional em coordenadas SENSOR-NATIVE. "
|
||||||
|
"Vazio usa o frame completo; também pode ser selecionada com R."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
ap.add_argument(
|
ap.add_argument(
|
||||||
"--candidate-root",
|
"--candidate-root",
|
||||||
|
|
@ -1902,6 +2350,13 @@ def main():
|
||||||
rows, actual_mx, usb = discover(args.mx_id)
|
rows, actual_mx, usb = discover(args.mx_id)
|
||||||
specs = validate_specs(rows)
|
specs = validate_specs(rows)
|
||||||
preview_orientation = resolve_preview_orientation(specs, args)
|
preview_orientation = resolve_preview_orientation(specs, args)
|
||||||
|
operational_rois = {
|
||||||
|
role: parse_roi_arg(
|
||||||
|
getattr(args, f"{role}_operational_roi"), specs[role]
|
||||||
|
)
|
||||||
|
for role in ROLES
|
||||||
|
}
|
||||||
|
guide_targets = [] if args.no_guide else build_guide_targets()
|
||||||
|
|
||||||
pipeline, isp = build_pipeline(
|
pipeline, isp = build_pipeline(
|
||||||
specs,
|
specs,
|
||||||
|
|
@ -1949,6 +2404,13 @@ def main():
|
||||||
"usb_speed": usb,
|
"usb_speed": usb,
|
||||||
"hardware_signature": hardware_signature(specs),
|
"hardware_signature": hardware_signature(specs),
|
||||||
"calibration_space": CALIBRATION_SPACE,
|
"calibration_space": CALIBRATION_SPACE,
|
||||||
|
"operational_rois_native": operational_rois,
|
||||||
|
"guided_capture": {
|
||||||
|
"enabled": not args.no_guide,
|
||||||
|
"guide_role": args.guide_role,
|
||||||
|
"thresholds": GUIDE,
|
||||||
|
"targets": guide_targets,
|
||||||
|
},
|
||||||
"geometry_contract": {
|
"geometry_contract": {
|
||||||
"intrinsics_measurement_space": CALIBRATION_SPACE,
|
"intrinsics_measurement_space": CALIBRATION_SPACE,
|
||||||
"measurement_orientation": "sensor_native",
|
"measurement_orientation": "sensor_native",
|
||||||
|
|
@ -2096,6 +2558,9 @@ def main():
|
||||||
show_undist = False
|
show_undist = False
|
||||||
message = ""
|
message = ""
|
||||||
message_t = 0.0
|
message_t = 0.0
|
||||||
|
live_det = None
|
||||||
|
guide_assessment = None
|
||||||
|
last_live_detection_t = 0.0
|
||||||
|
|
||||||
print(
|
print(
|
||||||
f"[CAPTURE] mínimo={QA['min_views']}, ideal={QA['recommended_views']}. "
|
f"[CAPTURE] mínimo={QA['min_views']}, ideal={QA['recommended_views']}. "
|
||||||
|
|
@ -2105,13 +2570,46 @@ def main():
|
||||||
while True:
|
while True:
|
||||||
runtime.poll()
|
runtime.poll()
|
||||||
|
|
||||||
|
guide_target = (
|
||||||
|
guide_targets[min(len(views), len(guide_targets) - 1)]
|
||||||
|
if guide_targets and len(views) < len(guide_targets)
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
guide_target is not None
|
||||||
|
and time.time() - last_live_detection_t
|
||||||
|
>= GUIDE["live_detection_period_s"]
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
live_det = detect_triplet(
|
||||||
|
{"frames": runtime.frames}, board, dictionary, params
|
||||||
|
)
|
||||||
|
guide_assessment = assess_guide(
|
||||||
|
live_det["detections"][args.guide_role],
|
||||||
|
board_pts,
|
||||||
|
runtime.frames[args.guide_role].shape[:2],
|
||||||
|
preview_orientation[args.guide_role],
|
||||||
|
operational_rois[args.guide_role],
|
||||||
|
guide_target,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
live_det = None
|
||||||
|
guide_assessment = None
|
||||||
|
last_live_detection_t = time.time()
|
||||||
|
|
||||||
msg = message if (
|
msg = message if (
|
||||||
message and time.time() - message_t < 4.0
|
message and time.time() - message_t < 4.0
|
||||||
) else ""
|
) else ""
|
||||||
|
|
||||||
ui = build_ui(
|
ui = build_ui(
|
||||||
runtime, specs, views, last_det, evaluation,
|
runtime, specs, views, live_det or last_det, evaluation,
|
||||||
selected_role, show_undist, args, preview_orientation, msg
|
selected_role, show_undist, args, preview_orientation,
|
||||||
|
operational_rois=operational_rois,
|
||||||
|
guide_role=args.guide_role,
|
||||||
|
guide_target=guide_target,
|
||||||
|
guide_assessment=guide_assessment,
|
||||||
|
msg=msg,
|
||||||
)
|
)
|
||||||
|
|
||||||
cv2.imshow(window, ui)
|
cv2.imshow(window, ui)
|
||||||
|
|
@ -2151,8 +2649,47 @@ def main():
|
||||||
message = f"Snapshot={snap.name}"
|
message = f"Snapshot={snap.name}"
|
||||||
message_t = time.time()
|
message_t = time.time()
|
||||||
|
|
||||||
elif k in (ord("g"), ord("G")):
|
elif k in (ord("r"), ord("R")):
|
||||||
|
if views:
|
||||||
|
message = (
|
||||||
|
"ROI bloqueada após capturas. Use V antes de alterar."
|
||||||
|
)
|
||||||
|
message_t = time.time()
|
||||||
|
continue
|
||||||
|
chosen = select_operational_roi(
|
||||||
|
runtime.frames[selected_role], selected_role,
|
||||||
|
preview_orientation[selected_role],
|
||||||
|
)
|
||||||
|
if chosen is not None:
|
||||||
|
operational_rois[selected_role] = chosen
|
||||||
|
report["operational_rois_native"] = operational_rois
|
||||||
|
save_json_atomic(report_path, report)
|
||||||
|
message = (
|
||||||
|
f"ROI {selected_role.upper()}="
|
||||||
|
f"{chosen['x']},{chosen['y']},"
|
||||||
|
f"{chosen['width']},{chosen['height']}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
message = "Seleção de ROI cancelada."
|
||||||
|
message_t = time.time()
|
||||||
|
|
||||||
|
elif k in (ord("c"), ord("C")):
|
||||||
|
if views:
|
||||||
|
message = (
|
||||||
|
"ROI bloqueada após capturas. Use V antes de alterar."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
operational_rois[selected_role] = full_roi(
|
||||||
|
specs[selected_role]
|
||||||
|
)
|
||||||
|
report["operational_rois_native"] = operational_rois
|
||||||
|
save_json_atomic(report_path, report)
|
||||||
|
message = f"ROI {selected_role.upper()} = frame completo."
|
||||||
|
message_t = time.time()
|
||||||
|
|
||||||
|
elif k in (ord("g"), ord("G"), ord("f"), ord("F")):
|
||||||
try:
|
try:
|
||||||
|
forced = k in (ord("f"), ord("F"))
|
||||||
trip = runtime.fresh_triplet()
|
trip = runtime.fresh_triplet()
|
||||||
det = detect_triplet(
|
det = detect_triplet(
|
||||||
trip, board, dictionary, params
|
trip, board, dictionary, params
|
||||||
|
|
@ -2164,10 +2701,34 @@ def main():
|
||||||
message_t = time.time()
|
message_t = time.time()
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
current_target = (
|
||||||
|
guide_targets[len(views)]
|
||||||
|
if guide_targets and len(views) < len(guide_targets)
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
capture_assessment = None
|
||||||
|
if current_target is not None:
|
||||||
|
capture_assessment = assess_guide(
|
||||||
|
det["detections"][args.guide_role],
|
||||||
|
board_pts,
|
||||||
|
trip["frames"][args.guide_role].shape[:2],
|
||||||
|
preview_orientation[args.guide_role],
|
||||||
|
operational_rois[args.guide_role],
|
||||||
|
current_target,
|
||||||
|
)
|
||||||
|
if not forced and not capture_assessment["ready"]:
|
||||||
|
message = (
|
||||||
|
"GUIA: " + capture_assessment["instruction"]
|
||||||
|
+ " | F força captura válida"
|
||||||
|
)
|
||||||
|
message_t = time.time()
|
||||||
|
continue
|
||||||
|
|
||||||
pose = {
|
pose = {
|
||||||
role: pose_signature(
|
role: pose_signature(
|
||||||
det["detections"][role]["points"],
|
det["detections"][role]["points"],
|
||||||
trip["frames"][role].shape[:2],
|
trip["frames"][role].shape[:2],
|
||||||
|
operational_rois[role],
|
||||||
)
|
)
|
||||||
for role in ROLES
|
for role in ROLES
|
||||||
}
|
}
|
||||||
|
|
@ -2189,6 +2750,15 @@ def main():
|
||||||
"controls": trip["controls"],
|
"controls": trip["controls"],
|
||||||
"measurement_space": CALIBRATION_SPACE,
|
"measurement_space": CALIBRATION_SPACE,
|
||||||
"measurement_orientation": "sensor_native",
|
"measurement_orientation": "sensor_native",
|
||||||
|
"operational_rois_native": {
|
||||||
|
role: dict(operational_rois[role]) for role in ROLES
|
||||||
|
},
|
||||||
|
"guided_capture": {
|
||||||
|
"forced": forced,
|
||||||
|
"guide_role": args.guide_role,
|
||||||
|
"target": current_target,
|
||||||
|
"assessment": capture_assessment,
|
||||||
|
},
|
||||||
"preview_orientation_by_role": {
|
"preview_orientation_by_role": {
|
||||||
role: dict(preview_orientation[role])
|
role: dict(preview_orientation[role])
|
||||||
for role in ROLES
|
for role in ROLES
|
||||||
|
|
@ -2225,6 +2795,13 @@ def main():
|
||||||
message_t = time.time()
|
message_t = time.time()
|
||||||
print("[VIEW]", message)
|
print("[VIEW]", message)
|
||||||
|
|
||||||
|
if guide_targets and len(views) == len(guide_targets):
|
||||||
|
message = (
|
||||||
|
f"ROTEIRO COMPLETO: {len(views)} views. "
|
||||||
|
"Pressione A para calibrar."
|
||||||
|
)
|
||||||
|
message_t = time.time()
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
message = f"Captura rejeitada: {exc}"
|
message = f"Captura rejeitada: {exc}"
|
||||||
message_t = time.time()
|
message_t = time.time()
|
||||||
|
|
@ -2236,14 +2813,35 @@ def main():
|
||||||
)
|
)
|
||||||
message_t = time.time()
|
message_t = time.time()
|
||||||
continue
|
continue
|
||||||
|
if guide_targets and len(views) < len(guide_targets):
|
||||||
|
message = (
|
||||||
|
f"Complete o guia: {len(views)}/{len(guide_targets)} views."
|
||||||
|
)
|
||||||
|
message_t = time.time()
|
||||||
|
continue
|
||||||
|
|
||||||
|
calc_started = time.perf_counter()
|
||||||
|
calc_log(
|
||||||
|
f"Iniciando avaliação de {len(views)} views. "
|
||||||
|
"Serão avaliadas 3 câmeras e 2 modelos por câmera."
|
||||||
|
)
|
||||||
evaluation = evaluate_all(
|
evaluation = evaluate_all(
|
||||||
views, board_pts, specs, args.distortion_model
|
views, board_pts, specs, args.distortion_model,
|
||||||
|
operational_rois,
|
||||||
|
)
|
||||||
|
calc_log(
|
||||||
|
f"Avaliação concluída em "
|
||||||
|
f"{time.perf_counter() - calc_started:.1f} segundos."
|
||||||
)
|
)
|
||||||
|
|
||||||
report["evaluation"] = evaluation
|
report["evaluation"] = evaluation
|
||||||
report["module_params_fragment"] = module_fragment(
|
complete_models = all(
|
||||||
evaluation, specs
|
"camera_matrix" in evaluation["cameras"][role]["selected_model"]
|
||||||
|
for role in ROLES
|
||||||
|
)
|
||||||
|
report["module_params_fragment"] = (
|
||||||
|
module_fragment(evaluation, specs)
|
||||||
|
if complete_models else None
|
||||||
)
|
)
|
||||||
save_json_atomic(report_path, report)
|
save_json_atomic(report_path, report)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -197,6 +197,7 @@ PREVIEW_ORIENTATION_BY_SENSOR = {
|
||||||
RAW10_MAX = 1023.0
|
RAW10_MAX = 1023.0
|
||||||
RAW10_WHITE_SAT = 1018
|
RAW10_WHITE_SAT = 1018
|
||||||
RAW10_DARK_FLOOR = 16
|
RAW10_DARK_FLOOR = 16
|
||||||
|
COMPARE_EPS = 1e-6
|
||||||
|
|
||||||
SCHEMA = "multispec_flatfield_production_v2"
|
SCHEMA = "multispec_flatfield_production_v2"
|
||||||
|
|
||||||
|
|
@ -1425,11 +1426,24 @@ class AcquisitionContext:
|
||||||
spec = self.specs[role]
|
spec = self.specs[role]
|
||||||
|
|
||||||
# Produto: RAW10 obrigatório para estes três sensores.
|
# Produto: RAW10 obrigatório para estes três sensores.
|
||||||
raw_type = str(pkt.getType()).upper()
|
# RAW10 MIPI pode aparecer como RAW10 ou PACK10,
|
||||||
|
# dependendo da versão do DepthAI/firmware.
|
||||||
|
frame_type = pkt.getType()
|
||||||
|
raw_type = str(frame_type).upper()
|
||||||
|
|
||||||
if "RAW10" not in raw_type:
|
accepted_types = {
|
||||||
|
value
|
||||||
|
for value in (
|
||||||
|
getattr(dai.ImgFrame.Type, "RAW10", None),
|
||||||
|
getattr(dai.ImgFrame.Type, "PACK10", None),
|
||||||
|
)
|
||||||
|
if value is not None
|
||||||
|
}
|
||||||
|
|
||||||
|
if frame_type not in accepted_types:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"{role.upper()}/{spec.sensor_name}: esperado RAW10, recebido {pkt.getType()}"
|
f"{role.upper()}/{spec.sensor_name}: esperado RAW10/PACK10, "
|
||||||
|
f"recebido {frame_type}"
|
||||||
)
|
)
|
||||||
|
|
||||||
width = int(pkt.getWidth())
|
width = int(pkt.getWidth())
|
||||||
|
|
@ -2340,24 +2354,24 @@ def evaluate_gain_channel(gain: np.ndarray, th: dict) -> dict:
|
||||||
status = "good"
|
status = "good"
|
||||||
reasons = []
|
reasons = []
|
||||||
|
|
||||||
if st["p99"] > th["gain_p99_bad"]:
|
if st["p99"] > th["gain_p99_bad"] + COMPARE_EPS:
|
||||||
status = merge_status(status, "bad")
|
status = merge_status(status, "bad")
|
||||||
reasons.append(f"gain_p99_bad:{st['p99']:.3f}")
|
reasons.append(f"gain_p99_bad:{st['p99']:.3f}")
|
||||||
elif st["p99"] > th["gain_p99_warning"]:
|
elif st["p99"] > th["gain_p99_warning"] + COMPARE_EPS:
|
||||||
status = merge_status(status, "warning")
|
status = merge_status(status, "warning")
|
||||||
reasons.append(f"gain_p99_warn:{st['p99']:.3f}")
|
reasons.append(f"gain_p99_warn:{st['p99']:.3f}")
|
||||||
|
|
||||||
if st["max"] > th["gain_max_bad"]:
|
if st["max"] > th["gain_max_bad"] + COMPARE_EPS:
|
||||||
status = merge_status(status, "bad")
|
status = merge_status(status, "bad")
|
||||||
reasons.append(f"gain_max_bad:{st['max']:.3f}")
|
reasons.append(f"gain_max_bad:{st['max']:.3f}")
|
||||||
elif st["max"] > th["gain_max_warning"]:
|
elif st["max"] > th["gain_max_warning"] + COMPARE_EPS:
|
||||||
status = merge_status(status, "warning")
|
status = merge_status(status, "warning")
|
||||||
reasons.append(f"gain_max_warn:{st['max']:.3f}")
|
reasons.append(f"gain_max_warn:{st['max']:.3f}")
|
||||||
|
|
||||||
if st["min"] < th["gain_min_bad"]:
|
if st["min"] < th["gain_min_bad"] - COMPARE_EPS:
|
||||||
status = merge_status(status, "bad")
|
status = merge_status(status, "bad")
|
||||||
reasons.append(f"gain_min_bad:{st['min']:.3f}")
|
reasons.append(f"gain_min_bad:{st['min']:.3f}")
|
||||||
elif st["min"] < th["gain_min_warning"]:
|
elif st["min"] < th["gain_min_warning"] - COMPARE_EPS:
|
||||||
status = merge_status(status, "warning")
|
status = merge_status(status, "warning")
|
||||||
reasons.append(f"gain_min_warn:{st['min']:.3f}")
|
reasons.append(f"gain_min_warn:{st['min']:.3f}")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -236,9 +236,9 @@ PREVIEW_ORIENTATION_BY_SENSOR = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
SCHEMA = "multispec_radiometric_calibration_v5"
|
SCHEMA = "multispec_radiometric_calibration_v6"
|
||||||
CALIBRATION_DOMAIN = "native_sensor_raw_linear"
|
CALIBRATION_DOMAIN = "native_sensor_raw_linear"
|
||||||
NORMALIZATION_METHOD = "oak_ae_frame_controls_v1"
|
NORMALIZATION_METHOD = "oak_ae_frame_controls_affine_v2"
|
||||||
FACTOR_MODEL = "exposure_time_us_x_iso"
|
FACTOR_MODEL = "exposure_time_us_x_iso"
|
||||||
ISO_BASE = 100.0
|
ISO_BASE = 100.0
|
||||||
|
|
||||||
|
|
@ -270,8 +270,8 @@ DEFAULT_QA = {
|
||||||
# Regressão
|
# Regressão
|
||||||
"r2_warning": 0.995,
|
"r2_warning": 0.995,
|
||||||
"r2_bad": 0.985,
|
"r2_bad": 0.985,
|
||||||
"intercept_fraction_warning": 0.035,
|
"black_offset_warning": 0.10,
|
||||||
"intercept_fraction_bad": 0.080,
|
"black_offset_bad": 0.20,
|
||||||
|
|
||||||
# Validação do modelo de normalização
|
# Validação do modelo de normalização
|
||||||
"norm_median_rel_error_warning": 0.030,
|
"norm_median_rel_error_warning": 0.030,
|
||||||
|
|
@ -302,14 +302,15 @@ DEFAULT_QA = {
|
||||||
|
|
||||||
DEFAULT_SWEEP_FACTORS = (
|
DEFAULT_SWEEP_FACTORS = (
|
||||||
0.30,
|
0.30,
|
||||||
0.42,
|
0.45,
|
||||||
0.55,
|
0.65,
|
||||||
0.70,
|
0.90,
|
||||||
0.85,
|
1.20,
|
||||||
1.00,
|
1.60,
|
||||||
1.18,
|
2.10,
|
||||||
1.38,
|
2.80,
|
||||||
1.58,
|
3.60,
|
||||||
|
4.20,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1083,7 +1084,10 @@ def decode_raw_packet(
|
||||||
packet.getType()
|
packet.getType()
|
||||||
).upper()
|
).upper()
|
||||||
|
|
||||||
if "RAW10" in raw_type:
|
if (
|
||||||
|
"RAW10" in raw_type
|
||||||
|
or "PACK10" in raw_type
|
||||||
|
):
|
||||||
raw = unpack_raw10(
|
raw = unpack_raw10(
|
||||||
packet.getData(),
|
packet.getData(),
|
||||||
width,
|
width,
|
||||||
|
|
@ -2282,6 +2286,18 @@ class RadiometricRuntime:
|
||||||
role
|
role
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
|
||||||
|
"requested_controls": {
|
||||||
|
"exposure_time_us": int(
|
||||||
|
controls_by_role[role].exposure_time_us
|
||||||
|
),
|
||||||
|
"sensitivity_iso": int(
|
||||||
|
controls_by_role[role].sensitivity_iso
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"last_actual_controls": dict(
|
||||||
|
self.controls[role]
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
return out
|
return out
|
||||||
|
|
@ -3247,10 +3263,18 @@ def analyze_role_sweep(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Testa exatamente o modelo utilizado no runtime:
|
# Normalização considerando o pedestal preto do sensor:
|
||||||
# normalized = measured * reference_factor / actual_factor
|
#
|
||||||
|
# normalized =
|
||||||
|
# intercept
|
||||||
|
# + (measured - intercept)
|
||||||
|
# * reference_factor
|
||||||
|
# / actual_factor
|
||||||
normalized_values = (
|
normalized_values = (
|
||||||
y
|
intercept
|
||||||
|
+ (
|
||||||
|
y - intercept
|
||||||
|
)
|
||||||
* reference_factor
|
* reference_factor
|
||||||
/ x
|
/ x
|
||||||
)
|
)
|
||||||
|
|
@ -3321,36 +3345,18 @@ def analyze_role_sweep(
|
||||||
f"r2_warn:{r2:.6f}"
|
f"r2_warn:{r2:.6f}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
black_offset = abs(intercept)
|
||||||
intercept_fraction
|
|
||||||
> qa[
|
|
||||||
"intercept_fraction_bad"
|
|
||||||
]
|
|
||||||
):
|
|
||||||
status = merge_status(
|
|
||||||
status,
|
|
||||||
"bad",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
if black_offset > qa["black_offset_bad"]:
|
||||||
|
status = merge_status(status, "bad")
|
||||||
reasons.append(
|
reasons.append(
|
||||||
"intercept_fraction_bad:"
|
f"black_offset_bad:{black_offset:.4f}"
|
||||||
f"{intercept_fraction:.4f}"
|
|
||||||
)
|
|
||||||
|
|
||||||
elif (
|
|
||||||
intercept_fraction
|
|
||||||
> qa[
|
|
||||||
"intercept_fraction_warning"
|
|
||||||
]
|
|
||||||
):
|
|
||||||
status = merge_status(
|
|
||||||
status,
|
|
||||||
"warning",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
elif black_offset > qa["black_offset_warning"]:
|
||||||
|
status = merge_status(status, "warning")
|
||||||
reasons.append(
|
reasons.append(
|
||||||
"intercept_fraction_warn:"
|
f"black_offset_warn:{black_offset:.4f}"
|
||||||
f"{intercept_fraction:.4f}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
rel_med = float(
|
rel_med = float(
|
||||||
|
|
@ -4102,6 +4108,13 @@ def build_normalization_fragment(
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
black_offset_by_role = {
|
||||||
|
role: float(
|
||||||
|
role_analyses[role]["fit"]["intercept"]
|
||||||
|
)
|
||||||
|
for role in ROLES
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"radiometric_normalization": {
|
"radiometric_normalization": {
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
|
|
@ -4132,6 +4145,12 @@ def build_normalization_fragment(
|
||||||
),
|
),
|
||||||
"clip_output": False,
|
"clip_output": False,
|
||||||
"save_debug": True,
|
"save_debug": True,
|
||||||
|
"black_offset_model": "per_role_scalar_raw01",
|
||||||
|
"black_offset_by_role": black_offset_by_role,
|
||||||
|
"formula": (
|
||||||
|
"offset + (value - offset) * "
|
||||||
|
"reference_factor / actual_factor"
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -5230,47 +5249,64 @@ def main():
|
||||||
role: LockedControl(
|
role: LockedControl(
|
||||||
role=role,
|
role=role,
|
||||||
exposure_time_us=int(
|
exposure_time_us=int(
|
||||||
role_analyses[
|
role_analyses[role]["reference"]["exposure_time_us"]
|
||||||
role
|
|
||||||
][
|
|
||||||
"reference"
|
|
||||||
][
|
|
||||||
"exposure_time_us"
|
|
||||||
]
|
|
||||||
),
|
),
|
||||||
sensitivity_iso=int(
|
sensitivity_iso=int(
|
||||||
role_analyses[
|
role_analyses[role]["reference"]["sensitivity_iso"]
|
||||||
role
|
|
||||||
][
|
|
||||||
"reference"
|
|
||||||
][
|
|
||||||
"sensitivity_iso"
|
|
||||||
]
|
|
||||||
),
|
|
||||||
source=(
|
|
||||||
"radiometric_reference_validation"
|
|
||||||
),
|
),
|
||||||
|
source="radiometric_reference_validation",
|
||||||
)
|
)
|
||||||
for role in ROLES
|
for role in ROLES
|
||||||
}
|
}
|
||||||
|
|
||||||
runtime.send_controls(
|
# Validação independente por câmera.
|
||||||
reference_controls
|
# Para RE/NIR, coloca as duas OV9282 no mesmo controle durante
|
||||||
)
|
# a validação, evitando interferência ou acoplamento de exposição.
|
||||||
|
validation_stats = {}
|
||||||
|
|
||||||
runtime.verify_controls(
|
for validation_role in ROLES:
|
||||||
reference_controls,
|
validation_controls = dict(reference_controls)
|
||||||
settle_frames=(
|
|
||||||
args.settle_frames
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
validation_stats = (
|
if validation_role in ("re", "nir"):
|
||||||
runtime.capture_level(
|
selected = reference_controls[validation_role]
|
||||||
args.validation_frames,
|
|
||||||
reference_controls,
|
for mono_role in ("re", "nir"):
|
||||||
|
validation_controls[mono_role] = LockedControl(
|
||||||
|
role=mono_role,
|
||||||
|
exposure_time_us=selected.exposure_time_us,
|
||||||
|
sensitivity_iso=selected.sensitivity_iso,
|
||||||
|
source=(
|
||||||
|
"radiometric_reference_validation_"
|
||||||
|
f"{validation_role}"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"[VALIDATION] {validation_role.upper()} | "
|
||||||
|
f"EXP={validation_controls[validation_role].exposure_time_us}us "
|
||||||
|
f"ISO={validation_controls[validation_role].sensitivity_iso}"
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
# Reenvia para robustez.
|
||||||
|
for _ in range(3):
|
||||||
|
runtime.send_controls(validation_controls)
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
runtime.verify_controls(
|
||||||
|
validation_controls,
|
||||||
|
settle_frames=max(
|
||||||
|
int(args.settle_frames),
|
||||||
|
15,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
captured = runtime.capture_level(
|
||||||
|
args.validation_frames,
|
||||||
|
validation_controls,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Guarda somente a câmera que está sendo validada nesta rodada.
|
||||||
|
validation_stats[validation_role] = captured[validation_role]
|
||||||
|
|
||||||
validation_report = {}
|
validation_report = {}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -432,10 +432,10 @@ def validate_radiometry_artifact(
|
||||||
):
|
):
|
||||||
schema = str(data.get("schema", ""))
|
schema = str(data.get("schema", ""))
|
||||||
|
|
||||||
if schema != "multispec_radiometric_calibration_v5":
|
if schema != "multispec_radiometric_calibration_v6":
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Schema radiométrico inesperado: {schema!r}. "
|
f"Schema radiométrico inesperado: {schema!r}. "
|
||||||
"Esperado 'multispec_radiometric_calibration_v5'."
|
"Esperado 'multispec_radiometric_calibration_v6'."
|
||||||
)
|
)
|
||||||
|
|
||||||
status = str(data.get("status", "")).lower()
|
status = str(data.get("status", "")).lower()
|
||||||
|
|
|
||||||
|
|
@ -302,7 +302,7 @@ DEFAULT_QA = {
|
||||||
|
|
||||||
# Overlap útil das imagens
|
# Overlap útil das imagens
|
||||||
"overlap_warning": 0.80,
|
"overlap_warning": 0.80,
|
||||||
"overlap_bad": 0.70,
|
"overlap_bad": 0.65,
|
||||||
|
|
||||||
# Sanidade matricial
|
# Sanidade matricial
|
||||||
"condition_number_warning": 1.0e5,
|
"condition_number_warning": 1.0e5,
|
||||||
|
|
|
||||||
|
|
@ -158,7 +158,7 @@ ASSEMBLER_SCHEMA = "multispec_module_params_assembly_v1"
|
||||||
EXPECTED_SCHEMAS = {
|
EXPECTED_SCHEMAS = {
|
||||||
"focus": "multispec_focus_qc_v3",
|
"focus": "multispec_focus_qc_v3",
|
||||||
"flatfield": "multispec_flatfield_production_v2",
|
"flatfield": "multispec_flatfield_production_v2",
|
||||||
"radiometry": "multispec_radiometric_calibration_v5",
|
"radiometry": "multispec_radiometric_calibration_v6",
|
||||||
"startup": "multispec_camera_startup_profile_v3",
|
"startup": "multispec_camera_startup_profile_v3",
|
||||||
"intrinsics": "multispec_intrinsics_calibration_v1",
|
"intrinsics": "multispec_intrinsics_calibration_v1",
|
||||||
"homography": "multispec_homography_calibration_v4",
|
"homography": "multispec_homography_calibration_v4",
|
||||||
|
|
@ -478,13 +478,23 @@ def normalize_role_item(
|
||||||
)
|
)
|
||||||
|
|
||||||
if size is None:
|
if size is None:
|
||||||
width = item.get(
|
# Formato padrão.
|
||||||
"width"
|
width = item.get("width")
|
||||||
)
|
height = item.get("height")
|
||||||
|
|
||||||
height = item.get(
|
# Formato emitido pelo Focus Calibration.
|
||||||
"height"
|
if width is None:
|
||||||
)
|
width = item.get("configured_width")
|
||||||
|
|
||||||
|
if height is None:
|
||||||
|
height = item.get("configured_height")
|
||||||
|
|
||||||
|
# Último fallback: dimensão anunciada pelo hardware.
|
||||||
|
if width is None:
|
||||||
|
width = item.get("feature_width")
|
||||||
|
|
||||||
|
if height is None:
|
||||||
|
height = item.get("feature_height")
|
||||||
|
|
||||||
if (
|
if (
|
||||||
width is not None
|
width is not None
|
||||||
|
|
@ -1111,16 +1121,46 @@ def validate_radiometry(
|
||||||
"Radiometric normalization homologada, mas disabled."
|
"Radiometric normalization homologada, mas disabled."
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
method = str(
|
||||||
cfg.get(
|
cfg.get("method") or ""
|
||||||
"method"
|
)
|
||||||
)
|
|
||||||
!= "oak_ae_frame_controls_v1"
|
allowed_methods = {
|
||||||
):
|
"oak_ae_frame_controls_v1",
|
||||||
|
"oak_ae_frame_controls_affine_v2",
|
||||||
|
}
|
||||||
|
|
||||||
|
if method not in allowed_methods:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Método radiométrico inesperado: {cfg.get('method')}"
|
f"Método radiométrico inesperado: {method}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# O affine_v2 exige um offset preto para cada câmera.
|
||||||
|
if method == "oak_ae_frame_controls_affine_v2":
|
||||||
|
offsets = (
|
||||||
|
cfg.get("black_offset_by_role", {})
|
||||||
|
or {}
|
||||||
|
)
|
||||||
|
|
||||||
|
for role in ROLES:
|
||||||
|
if role not in offsets:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Radiometry affine_v2 sem black_offset para {role}."
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
offset = float(offsets[role])
|
||||||
|
except Exception as exc:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"black_offset inválido para {role}: "
|
||||||
|
f"{offsets.get(role)!r}"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
if not (0.0 <= offset < 0.50):
|
||||||
|
raise RuntimeError(
|
||||||
|
f"black_offset implausível para {role}: {offset}"
|
||||||
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
cfg.get(
|
cfg.get(
|
||||||
"factor_model"
|
"factor_model"
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
|
@ -0,0 +1,902 @@
|
||||||
|
{
|
||||||
|
"schema": "multispec_module_params_v3",
|
||||||
|
"saved_at": "2026-09-08 17:02:25",
|
||||||
|
"frame_type": "RAW_BRUTO",
|
||||||
|
"capture_mode_requested": "AUTO",
|
||||||
|
"capture_mode_effective": "AUTO",
|
||||||
|
"raw_policy": "allow_single",
|
||||||
|
"sensor_width": 1920,
|
||||||
|
"sensor_height": 1200,
|
||||||
|
"bayer_pattern": "GRBG",
|
||||||
|
"sensor_size_by_role": {
|
||||||
|
"rgb": [
|
||||||
|
1920,
|
||||||
|
1200
|
||||||
|
],
|
||||||
|
"re": [
|
||||||
|
1280,
|
||||||
|
800
|
||||||
|
],
|
||||||
|
"nir": [
|
||||||
|
1280,
|
||||||
|
800
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"camera_hardware": {
|
||||||
|
"rgb": {
|
||||||
|
"socket": "CAM_A",
|
||||||
|
"sensor": "AR0234",
|
||||||
|
"size": [
|
||||||
|
1920,
|
||||||
|
1200
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"re": {
|
||||||
|
"socket": "CAM_B",
|
||||||
|
"sensor": "OV9282",
|
||||||
|
"size": [
|
||||||
|
1280,
|
||||||
|
800
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nir": {
|
||||||
|
"socket": "CAM_C",
|
||||||
|
"sensor": "OV9282",
|
||||||
|
"size": [
|
||||||
|
1280,
|
||||||
|
800
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"camera_orientation": {
|
||||||
|
"schema": "multispec_camera_orientation_v1",
|
||||||
|
"enabled": true,
|
||||||
|
"input_space": "native_stream_no_external_undistort",
|
||||||
|
"output_space": "canonical_oriented_stream_no_external_undistort",
|
||||||
|
"apply_stage": "after_native_flat_before_fusion",
|
||||||
|
"by_role": {
|
||||||
|
"rgb": {
|
||||||
|
"rotate_deg": 180,
|
||||||
|
"flip_horizontal": false,
|
||||||
|
"flip_vertical": false,
|
||||||
|
"native_size": [
|
||||||
|
1920,
|
||||||
|
1200
|
||||||
|
],
|
||||||
|
"oriented_size": [
|
||||||
|
1920,
|
||||||
|
1200
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"re": {
|
||||||
|
"rotate_deg": 0,
|
||||||
|
"flip_horizontal": false,
|
||||||
|
"flip_vertical": false,
|
||||||
|
"native_size": [
|
||||||
|
1280,
|
||||||
|
800
|
||||||
|
],
|
||||||
|
"oriented_size": [
|
||||||
|
1280,
|
||||||
|
800
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nir": {
|
||||||
|
"rotate_deg": 0,
|
||||||
|
"flip_horizontal": false,
|
||||||
|
"flip_vertical": false,
|
||||||
|
"native_size": [
|
||||||
|
1280,
|
||||||
|
800
|
||||||
|
],
|
||||||
|
"oriented_size": [
|
||||||
|
1280,
|
||||||
|
800
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rgb_processing": {
|
||||||
|
"mode": "linear_demosaic",
|
||||||
|
"demosaic_algorithm": "ea"
|
||||||
|
},
|
||||||
|
"camera_settings": {
|
||||||
|
"rgb": {
|
||||||
|
"ae_enable": true,
|
||||||
|
"awb_enable": true,
|
||||||
|
"exposure_time_us": 631,
|
||||||
|
"analogue_gain": 1.0,
|
||||||
|
"colour_gains": [
|
||||||
|
1.0,
|
||||||
|
1.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"re": {
|
||||||
|
"ae_enable": true,
|
||||||
|
"awb_enable": false,
|
||||||
|
"exposure_time_us": 694,
|
||||||
|
"analogue_gain": 1.0,
|
||||||
|
"colour_gains": null
|
||||||
|
},
|
||||||
|
"nir": {
|
||||||
|
"ae_enable": true,
|
||||||
|
"awb_enable": false,
|
||||||
|
"exposure_time_us": 1048,
|
||||||
|
"analogue_gain": 1.0,
|
||||||
|
"colour_gains": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fusion_config": {
|
||||||
|
"alignment_mode": "homography",
|
||||||
|
"baseline_mm": 75.0,
|
||||||
|
"reference_camera": "rgb",
|
||||||
|
"homography_profile": "media",
|
||||||
|
"homography_profile_by_role": {
|
||||||
|
"re": "media",
|
||||||
|
"nir": "media"
|
||||||
|
},
|
||||||
|
"homography_profiles": {
|
||||||
|
"media": {
|
||||||
|
"description": "Plano de calibração ChArUco.",
|
||||||
|
"depth": 66.0,
|
||||||
|
"depth_unit": "cm",
|
||||||
|
"homography_source": "charuco_oriented_multi_sample_group_cv",
|
||||||
|
"native_coordinate_space": "native_stream_no_external_undistort",
|
||||||
|
"coordinate_space": "canonical_oriented_stream_no_external_undistort",
|
||||||
|
"camera_orientation": {
|
||||||
|
"schema": "multispec_camera_orientation_v1",
|
||||||
|
"enabled": true,
|
||||||
|
"input_space": "native_stream_no_external_undistort",
|
||||||
|
"output_space": "canonical_oriented_stream_no_external_undistort",
|
||||||
|
"apply_stage": "after_native_flat_before_fusion",
|
||||||
|
"by_role": {
|
||||||
|
"rgb": {
|
||||||
|
"rotate_deg": 180,
|
||||||
|
"flip_horizontal": false,
|
||||||
|
"flip_vertical": false,
|
||||||
|
"native_size": [
|
||||||
|
1920,
|
||||||
|
1200
|
||||||
|
],
|
||||||
|
"oriented_size": [
|
||||||
|
1920,
|
||||||
|
1200
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"re": {
|
||||||
|
"rotate_deg": 0,
|
||||||
|
"flip_horizontal": false,
|
||||||
|
"flip_vertical": false,
|
||||||
|
"native_size": [
|
||||||
|
1280,
|
||||||
|
800
|
||||||
|
],
|
||||||
|
"oriented_size": [
|
||||||
|
1280,
|
||||||
|
800
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nir": {
|
||||||
|
"rotate_deg": 0,
|
||||||
|
"flip_horizontal": false,
|
||||||
|
"flip_vertical": false,
|
||||||
|
"native_size": [
|
||||||
|
1280,
|
||||||
|
800
|
||||||
|
],
|
||||||
|
"oriented_size": [
|
||||||
|
1280,
|
||||||
|
800
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reference_size": [
|
||||||
|
1920,
|
||||||
|
1200
|
||||||
|
],
|
||||||
|
"source_size_by_role": {
|
||||||
|
"re": [
|
||||||
|
1280,
|
||||||
|
800
|
||||||
|
],
|
||||||
|
"nir": [
|
||||||
|
1280,
|
||||||
|
800
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"homography_calibration_size": [
|
||||||
|
1920,
|
||||||
|
1200
|
||||||
|
],
|
||||||
|
"homography_stats": {
|
||||||
|
"re_total_points": 456,
|
||||||
|
"re_inliers": 456,
|
||||||
|
"re_inlier_pct": 100.0,
|
||||||
|
"nir_total_points": 575,
|
||||||
|
"nir_inliers": 575,
|
||||||
|
"nir_inlier_pct": 100.0,
|
||||||
|
"re_frames_used": 9,
|
||||||
|
"nir_frames_used": 9,
|
||||||
|
"common_frames_used": 9,
|
||||||
|
"re_fit_median_error_px": 0.38758599758148193,
|
||||||
|
"nir_fit_median_error_px": 0.35749897360801697,
|
||||||
|
"re_cv_median_error_px": 0.5654353499412537,
|
||||||
|
"nir_cv_median_error_px": 0.45016223192214966,
|
||||||
|
"re_overlap_pct": 67.63589409722222,
|
||||||
|
"nir_overlap_pct": 66.90980902777778,
|
||||||
|
"overlap_common_pct": 66.90980902777778
|
||||||
|
},
|
||||||
|
"homographies": {
|
||||||
|
"re_to_rgb": [
|
||||||
|
[
|
||||||
|
1.229438054753987,
|
||||||
|
-0.023146873859868553,
|
||||||
|
191.17641195023432
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.0068483713406142345,
|
||||||
|
1.215811502284338,
|
||||||
|
145.9976466757219
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.6951263574907942e-06,
|
||||||
|
-1.7656569375171528e-05,
|
||||||
|
1.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"nir_to_rgb": [
|
||||||
|
[
|
||||||
|
1.1985871377943254,
|
||||||
|
-0.029840166013639494,
|
||||||
|
172.11865189505187
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-0.005034022942030897,
|
||||||
|
1.1882575097648682,
|
||||||
|
109.71619856540441
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-7.354123495506985e-06,
|
||||||
|
-3.0667664963341365e-05,
|
||||||
|
1.0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"homography_calibration_size": [
|
||||||
|
1920,
|
||||||
|
1200
|
||||||
|
],
|
||||||
|
"use_remap_cache": true,
|
||||||
|
"use_remap_for_rgb": false,
|
||||||
|
"use_remap_for_spec": true,
|
||||||
|
"crop_valid_common": true,
|
||||||
|
"resize_after_crop": true,
|
||||||
|
"target_size": null
|
||||||
|
},
|
||||||
|
"intrinsics_config": {
|
||||||
|
"enabled": true,
|
||||||
|
"calibration_space": "native_stream_no_external_undistort",
|
||||||
|
"operational_roi_contract": "ROI afeta somente guia e acceptance. K/D e corners permanecem no raster nativo completo.",
|
||||||
|
"cameras": {
|
||||||
|
"rgb": {
|
||||||
|
"socket": "CAM_A",
|
||||||
|
"sensor": "AR0234",
|
||||||
|
"stream_source": "ISP",
|
||||||
|
"image_size": [
|
||||||
|
1920,
|
||||||
|
1200
|
||||||
|
],
|
||||||
|
"operational_roi_native": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"width": 1920,
|
||||||
|
"height": 1200
|
||||||
|
},
|
||||||
|
"distortion_model": "brown5",
|
||||||
|
"camera_matrix": [
|
||||||
|
[
|
||||||
|
1166.0979784837227,
|
||||||
|
0.0,
|
||||||
|
951.5767142097442
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
1168.7643259842318,
|
||||||
|
604.1279550529616
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
1.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"dist_coeffs": [
|
||||||
|
0.039391307619914294,
|
||||||
|
0.01630033368807952,
|
||||||
|
0.0009880549612935289,
|
||||||
|
-0.00013672753943063363,
|
||||||
|
-0.06837030351164153
|
||||||
|
],
|
||||||
|
"undistort_products": {
|
||||||
|
"0.0": {
|
||||||
|
"alpha": 0.0,
|
||||||
|
"new_camera_matrix": [
|
||||||
|
[
|
||||||
|
1181.756418296315,
|
||||||
|
0.0,
|
||||||
|
950.9467122149669
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
1186.0086782696878,
|
||||||
|
605.3891162485107
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
1.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"valid_roi": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1919,
|
||||||
|
1199
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"0.5": {
|
||||||
|
"alpha": 0.5,
|
||||||
|
"new_camera_matrix": [
|
||||||
|
[
|
||||||
|
1168.6413181324342,
|
||||||
|
0.0,
|
||||||
|
950.1682075221445
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
1172.1119850711932,
|
||||||
|
605.9709290057904
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
1.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"valid_roi": [
|
||||||
|
10,
|
||||||
|
8,
|
||||||
|
1898,
|
||||||
|
1185
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1.0": {
|
||||||
|
"alpha": 1.0,
|
||||||
|
"new_camera_matrix": [
|
||||||
|
[
|
||||||
|
1155.5262179685533,
|
||||||
|
0.0,
|
||||||
|
949.3897028293222
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
1158.2152918726986,
|
||||||
|
606.55274176307
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
1.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"valid_roi": [
|
||||||
|
20,
|
||||||
|
15,
|
||||||
|
1876,
|
||||||
|
1171
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"re": {
|
||||||
|
"socket": "CAM_B",
|
||||||
|
"sensor": "OV9282",
|
||||||
|
"stream_source": "MONO_OUT",
|
||||||
|
"image_size": [
|
||||||
|
1280,
|
||||||
|
800
|
||||||
|
],
|
||||||
|
"operational_roi_native": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"width": 1280,
|
||||||
|
"height": 800
|
||||||
|
},
|
||||||
|
"distortion_model": "brown5",
|
||||||
|
"camera_matrix": [
|
||||||
|
[
|
||||||
|
929.0286416157797,
|
||||||
|
0.0,
|
||||||
|
644.1026229147756
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
931.8815590920124,
|
||||||
|
371.82566695003317
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
1.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"dist_coeffs": [
|
||||||
|
0.06986318959627301,
|
||||||
|
-0.013902621968219604,
|
||||||
|
0.001143262770443756,
|
||||||
|
0.0007927956015904726,
|
||||||
|
-0.07759630612045954
|
||||||
|
],
|
||||||
|
"undistort_products": {
|
||||||
|
"0.0": {
|
||||||
|
"alpha": 0.0,
|
||||||
|
"new_camera_matrix": [
|
||||||
|
[
|
||||||
|
949.6779297598379,
|
||||||
|
0.0,
|
||||||
|
645.1710508964194
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
952.5670641843998,
|
||||||
|
372.7899174010669
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
1.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"valid_roi": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1279,
|
||||||
|
799
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"0.5": {
|
||||||
|
"alpha": 0.5,
|
||||||
|
"new_camera_matrix": [
|
||||||
|
[
|
||||||
|
948.0566137041557,
|
||||||
|
0.0,
|
||||||
|
645.3356001200425
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
947.784668586652,
|
||||||
|
372.8618574128032
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
1.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"valid_roi": [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
1277,
|
||||||
|
795
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1.0": {
|
||||||
|
"alpha": 1.0,
|
||||||
|
"new_camera_matrix": [
|
||||||
|
[
|
||||||
|
946.4352976484735,
|
||||||
|
0.0,
|
||||||
|
645.5001493436657
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
943.0022729889041,
|
||||||
|
372.93379742453953
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
1.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"valid_roi": [
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
1275,
|
||||||
|
791
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nir": {
|
||||||
|
"socket": "CAM_C",
|
||||||
|
"sensor": "OV9282",
|
||||||
|
"stream_source": "MONO_OUT",
|
||||||
|
"image_size": [
|
||||||
|
1280,
|
||||||
|
800
|
||||||
|
],
|
||||||
|
"operational_roi_native": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"width": 1280,
|
||||||
|
"height": 800
|
||||||
|
},
|
||||||
|
"distortion_model": "brown5",
|
||||||
|
"camera_matrix": [
|
||||||
|
[
|
||||||
|
931.8852709072933,
|
||||||
|
0.0,
|
||||||
|
655.5142810977045
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
934.1328900504861,
|
||||||
|
421.25400633846596
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
1.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"dist_coeffs": [
|
||||||
|
0.0722754387140124,
|
||||||
|
-0.04222665782931207,
|
||||||
|
0.001515428546208944,
|
||||||
|
-0.0005902584650630938,
|
||||||
|
-0.03362807732640043
|
||||||
|
],
|
||||||
|
"undistort_products": {
|
||||||
|
"0.0": {
|
||||||
|
"alpha": 0.0,
|
||||||
|
"new_camera_matrix": [
|
||||||
|
[
|
||||||
|
952.6875209672814,
|
||||||
|
0.0,
|
||||||
|
654.749436544307
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
954.3413650330856,
|
||||||
|
422.427394072531
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
1.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"valid_roi": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1279,
|
||||||
|
799
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"0.5": {
|
||||||
|
"alpha": 0.5,
|
||||||
|
"new_camera_matrix": [
|
||||||
|
[
|
||||||
|
950.916539490878,
|
||||||
|
0.0,
|
||||||
|
654.8789818964838
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
949.5012472686025,
|
||||||
|
422.0098330004064
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
1.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"valid_roi": [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
1277,
|
||||||
|
795
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1.0": {
|
||||||
|
"alpha": 1.0,
|
||||||
|
"new_camera_matrix": [
|
||||||
|
[
|
||||||
|
949.1455580144744,
|
||||||
|
0.0,
|
||||||
|
655.0085272486606
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
944.6611295041193,
|
||||||
|
421.5922719282818
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
1.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"valid_roi": [
|
||||||
|
3,
|
||||||
|
3,
|
||||||
|
1274,
|
||||||
|
791
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"runtime_undistort": {
|
||||||
|
"enabled": false,
|
||||||
|
"alpha": 0.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"reason": "Homography atual foi calibrada no espaço nativo distorcido. Ative undistort somente após recalibrar a homografia no novo espaço."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"radiometric_config": {
|
||||||
|
"enabled": false,
|
||||||
|
"policy": "disabled_product_default",
|
||||||
|
"reason": "Field radiometric controller is not part of factory calibration. Per-frame exposure variation is handled by radiometric_normalization."
|
||||||
|
},
|
||||||
|
"radiometric_normalization": {
|
||||||
|
"enabled": true,
|
||||||
|
"method": "oak_ae_frame_controls_affine_v2",
|
||||||
|
"apply_stage": "after_dark_before_flat_gain",
|
||||||
|
"control_source": "stream_meta.frame_controls",
|
||||||
|
"role_mapping_source": "camera_info",
|
||||||
|
"factor_model": "exposure_time_us_x_iso",
|
||||||
|
"iso_base": 100.0,
|
||||||
|
"reference_mode": "fixed",
|
||||||
|
"reference_controls": {
|
||||||
|
"rgb": {
|
||||||
|
"exposure_time_us": 631,
|
||||||
|
"sensitivity_iso": 100
|
||||||
|
},
|
||||||
|
"re": {
|
||||||
|
"exposure_time_us": 694,
|
||||||
|
"sensitivity_iso": 100
|
||||||
|
},
|
||||||
|
"nir": {
|
||||||
|
"exposure_time_us": 1048,
|
||||||
|
"sensitivity_iso": 100
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scale_limits": {
|
||||||
|
"rgb": {
|
||||||
|
"min": 0.05,
|
||||||
|
"max": 3.0
|
||||||
|
},
|
||||||
|
"re": {
|
||||||
|
"min": 0.05,
|
||||||
|
"max": 3.0
|
||||||
|
},
|
||||||
|
"nir": {
|
||||||
|
"min": 0.05,
|
||||||
|
"max": 3.0
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"min": 0.05,
|
||||||
|
"max": 3.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"missing_controls_policy": "skip",
|
||||||
|
"invalid_controls_policy": "skip",
|
||||||
|
"clip_output": false,
|
||||||
|
"save_debug": true,
|
||||||
|
"black_offset_model": "per_role_scalar_raw01",
|
||||||
|
"black_offset_by_role": {
|
||||||
|
"rgb": 0.043317917158965276,
|
||||||
|
"re": 0.07195387680152925,
|
||||||
|
"nir": 0.06753712138856606
|
||||||
|
},
|
||||||
|
"formula": "offset + (value - offset) * reference_factor / actual_factor"
|
||||||
|
},
|
||||||
|
"patch_normalization": {
|
||||||
|
"enabled": false,
|
||||||
|
"policy": "disabled_product_default",
|
||||||
|
"reason": "Reference-patch post-fusion normalization is not active in the homologated product pipeline."
|
||||||
|
},
|
||||||
|
"rgb_calibration": {
|
||||||
|
"enabled": false,
|
||||||
|
"gains": {
|
||||||
|
"R": 1.0,
|
||||||
|
"G": 1.0,
|
||||||
|
"B": 1.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flatfield_config": {
|
||||||
|
"enabled": true,
|
||||||
|
"npz_file": "calibration/flatfield_maps_v1.npz",
|
||||||
|
"apply_before_fusion": true,
|
||||||
|
"apply_after_decode": true,
|
||||||
|
"apply_space": "native_camera_space",
|
||||||
|
"map_type": "gain",
|
||||||
|
"channels": [
|
||||||
|
"R",
|
||||||
|
"G",
|
||||||
|
"B",
|
||||||
|
"RE",
|
||||||
|
"NIR"
|
||||||
|
],
|
||||||
|
"channel_maps": {
|
||||||
|
"R": {
|
||||||
|
"gain_key": "gain_R",
|
||||||
|
"dark_median_key": "dark_median_R"
|
||||||
|
},
|
||||||
|
"G": {
|
||||||
|
"gain_key": "gain_G",
|
||||||
|
"dark_median_key": "dark_median_G"
|
||||||
|
},
|
||||||
|
"B": {
|
||||||
|
"gain_key": "gain_B",
|
||||||
|
"dark_median_key": "dark_median_B"
|
||||||
|
},
|
||||||
|
"RE": {
|
||||||
|
"gain_key": "gain_RE",
|
||||||
|
"dark_median_key": "dark_median_RE"
|
||||||
|
},
|
||||||
|
"NIR": {
|
||||||
|
"gain_key": "gain_NIR",
|
||||||
|
"dark_median_key": "dark_median_NIR"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"subtract_dark": false,
|
||||||
|
"clip_output": true,
|
||||||
|
"json_file": "calibration/flatfield_maps_v1.json",
|
||||||
|
"schema": "multispec_flatfield_production_v2",
|
||||||
|
"created_at": "2026-09-08 16:31:07"
|
||||||
|
},
|
||||||
|
"calibration_provenance": {
|
||||||
|
"hardware_signature": {
|
||||||
|
"rgb": {
|
||||||
|
"socket": "CAM_A",
|
||||||
|
"sensor": "AR0234",
|
||||||
|
"size": [
|
||||||
|
1920,
|
||||||
|
1200
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"re": {
|
||||||
|
"socket": "CAM_B",
|
||||||
|
"sensor": "OV9282",
|
||||||
|
"size": [
|
||||||
|
1280,
|
||||||
|
800
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nir": {
|
||||||
|
"socket": "CAM_C",
|
||||||
|
"sensor": "OV9282",
|
||||||
|
"size": [
|
||||||
|
1280,
|
||||||
|
800
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"device_mx_id": "194430108133AC2F00",
|
||||||
|
"module_id": null,
|
||||||
|
"camera_orientation": {
|
||||||
|
"source": "homography.camera_orientation_signature",
|
||||||
|
"schema": "multispec_camera_orientation_v1",
|
||||||
|
"enabled": true,
|
||||||
|
"input_space": "native_stream_no_external_undistort",
|
||||||
|
"output_space": "canonical_oriented_stream_no_external_undistort",
|
||||||
|
"apply_stage": "after_native_flat_before_fusion",
|
||||||
|
"selected_homography_profile": "media"
|
||||||
|
},
|
||||||
|
"artifacts": {
|
||||||
|
"focus": {
|
||||||
|
"path": "calibration/focus_qc_active.json",
|
||||||
|
"sha256": "8c0f3ff2303a64f60b43e5a2f2966548b7720a2e43e7a46504a0a949ae6856f9",
|
||||||
|
"schema": "multispec_focus_qc_v3",
|
||||||
|
"status": "pass",
|
||||||
|
"promoted": true,
|
||||||
|
"session_id": null,
|
||||||
|
"created_at": "2026-09-08 16:03:05",
|
||||||
|
"finished_at": "2026-09-08 16:04:46"
|
||||||
|
},
|
||||||
|
"flatfield": {
|
||||||
|
"path": "calibration/flatfield_maps_v1.json",
|
||||||
|
"sha256": "1871312008b14ae4efb5d77ad7c9eb580af9c330775cccc9fa787f58f6f64648",
|
||||||
|
"schema": "multispec_flatfield_production_v2",
|
||||||
|
"status": "warning",
|
||||||
|
"promoted": true,
|
||||||
|
"session_id": "20260908_163058",
|
||||||
|
"created_at": "2026-09-08 16:31:07",
|
||||||
|
"finished_at": "2026-09-08 16:32:57",
|
||||||
|
"npz_path": "calibration/flatfield_maps_v1.npz",
|
||||||
|
"npz_sha256": "869a25192d51464a65f55852404fba01089c7aa181d975e5960a8154096cbcd4"
|
||||||
|
},
|
||||||
|
"radiometry": {
|
||||||
|
"path": "calibration/radiometry_calibration_v5.json",
|
||||||
|
"sha256": "0c9934f20a7a5e48b8c42e682e193941e841f696e799ae9834576bd9e2522ac3",
|
||||||
|
"schema": "multispec_radiometric_calibration_v6",
|
||||||
|
"status": "warning",
|
||||||
|
"promoted": true,
|
||||||
|
"session_id": "20260908_154252_449094",
|
||||||
|
"created_at": "2026-09-08 15:43:01",
|
||||||
|
"finished_at": "2026-09-08 15:44:01"
|
||||||
|
},
|
||||||
|
"startup": {
|
||||||
|
"path": "calibration/camera_startup_profile_v3.json",
|
||||||
|
"sha256": "e63e423a5d7b4fe3231352bd27e5dd3f26d0e03bb5d7c2ee6b2fe48084298599",
|
||||||
|
"schema": "multispec_camera_startup_profile_v3",
|
||||||
|
"status": "good",
|
||||||
|
"promoted": true,
|
||||||
|
"session_id": null,
|
||||||
|
"created_at": "2026-09-08 15:45:46",
|
||||||
|
"finished_at": null
|
||||||
|
},
|
||||||
|
"intrinsics": {
|
||||||
|
"path": "calibration/intrinsics_calibration_v1.json",
|
||||||
|
"sha256": "2b5d36825e40d9208cbfb7ab7f49d09c694ad26ab6f5db52fba706ab5a2d4f7d",
|
||||||
|
"schema": "multispec_intrinsics_calibration_v1",
|
||||||
|
"status": "warning_promoted",
|
||||||
|
"promoted": true,
|
||||||
|
"session_id": "20260908_144510_049990",
|
||||||
|
"created_at": "2026-09-08 14:45:15",
|
||||||
|
"finished_at": "2026-09-08 14:55:09"
|
||||||
|
},
|
||||||
|
"homography": {
|
||||||
|
"path": "calibration/homography_calibration_v4.json",
|
||||||
|
"sha256": "945af322860e5e704735cb5ac7f37918ee79e67826088e4ff13e1e54adf73378",
|
||||||
|
"schema": "multispec_homography_calibration_v4",
|
||||||
|
"status": null,
|
||||||
|
"promoted": null,
|
||||||
|
"session_id": null,
|
||||||
|
"created_at": "2026-09-08 15:11:12",
|
||||||
|
"finished_at": null,
|
||||||
|
"selected_profile": "media"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"assembly_metadata": {
|
||||||
|
"schema": "multispec_module_params_assembly_v1",
|
||||||
|
"assembled_at": "2026-09-08 17:02:25",
|
||||||
|
"assembly_report": "calibration/module_params_assembly_report.json",
|
||||||
|
"calibration_line": [
|
||||||
|
"focus",
|
||||||
|
"intrinsics",
|
||||||
|
"flatfield",
|
||||||
|
"radiometry",
|
||||||
|
"camera_startup",
|
||||||
|
"homography",
|
||||||
|
"module_params_assembler"
|
||||||
|
],
|
||||||
|
"runtime_undistort_enabled": false,
|
||||||
|
"camera_orientation_schema": "multispec_camera_orientation_v1",
|
||||||
|
"camera_orientation_enabled": true,
|
||||||
|
"geometry_chain": {
|
||||||
|
"intrinsics_space": "native_stream_no_external_undistort",
|
||||||
|
"orientation_input_space": "native_stream_no_external_undistort",
|
||||||
|
"orientation_output_space": "canonical_oriented_stream_no_external_undistort",
|
||||||
|
"orientation_apply_stage": "after_native_flat_before_fusion",
|
||||||
|
"homography_space": "canonical_oriented_stream_no_external_undistort",
|
||||||
|
"runtime_order": [
|
||||||
|
"decode",
|
||||||
|
"radiometric_normalization",
|
||||||
|
"flat_native",
|
||||||
|
"camera_orientation",
|
||||||
|
"homography",
|
||||||
|
"common_crop",
|
||||||
|
"final_resample"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,269 @@
|
||||||
|
{
|
||||||
|
"schema": "multispec_module_params_assembly_v1",
|
||||||
|
"created_at": "2026-09-08 17:02:25",
|
||||||
|
"status": "pass",
|
||||||
|
"check_only": false,
|
||||||
|
"output": "calibration/module_params.json",
|
||||||
|
"runtime_base_json": "calibration/module_params.json",
|
||||||
|
"runtime_base_found": false,
|
||||||
|
"hardware_signature": {
|
||||||
|
"rgb": {
|
||||||
|
"socket": "CAM_A",
|
||||||
|
"sensor": "AR0234",
|
||||||
|
"size": [
|
||||||
|
1920,
|
||||||
|
1200
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"re": {
|
||||||
|
"socket": "CAM_B",
|
||||||
|
"sensor": "OV9282",
|
||||||
|
"size": [
|
||||||
|
1280,
|
||||||
|
800
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nir": {
|
||||||
|
"socket": "CAM_C",
|
||||||
|
"sensor": "OV9282",
|
||||||
|
"size": [
|
||||||
|
1280,
|
||||||
|
800
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"device_mx_id": "194430108133AC2F00",
|
||||||
|
"module_id": null,
|
||||||
|
"bayer_pattern": "GRBG",
|
||||||
|
"homography_profile": "media",
|
||||||
|
"geometric_contract": {
|
||||||
|
"intrinsics_space": "native_stream_no_external_undistort",
|
||||||
|
"orientation_input_space": "native_stream_no_external_undistort",
|
||||||
|
"orientation_output_space": "canonical_oriented_stream_no_external_undistort",
|
||||||
|
"homography_space": "canonical_oriented_stream_no_external_undistort",
|
||||||
|
"runtime_undistort_enabled": false,
|
||||||
|
"selected_profile": "media",
|
||||||
|
"native_sizes": {
|
||||||
|
"rgb": [
|
||||||
|
1920,
|
||||||
|
1200
|
||||||
|
],
|
||||||
|
"re": [
|
||||||
|
1280,
|
||||||
|
800
|
||||||
|
],
|
||||||
|
"nir": [
|
||||||
|
1280,
|
||||||
|
800
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"oriented_sizes": {
|
||||||
|
"rgb": [
|
||||||
|
1920,
|
||||||
|
1200
|
||||||
|
],
|
||||||
|
"re": [
|
||||||
|
1280,
|
||||||
|
800
|
||||||
|
],
|
||||||
|
"nir": [
|
||||||
|
1280,
|
||||||
|
800
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"runtime_order": [
|
||||||
|
"decode",
|
||||||
|
"radiometric_normalization",
|
||||||
|
"flat_native",
|
||||||
|
"camera_orientation",
|
||||||
|
"homography",
|
||||||
|
"common_crop",
|
||||||
|
"final_resample"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"camera_orientation": {
|
||||||
|
"schema": "multispec_camera_orientation_v1",
|
||||||
|
"enabled": true,
|
||||||
|
"input_space": "native_stream_no_external_undistort",
|
||||||
|
"output_space": "canonical_oriented_stream_no_external_undistort",
|
||||||
|
"apply_stage": "after_native_flat_before_fusion",
|
||||||
|
"by_role": {
|
||||||
|
"rgb": {
|
||||||
|
"rotate_deg": 180,
|
||||||
|
"flip_horizontal": false,
|
||||||
|
"flip_vertical": false,
|
||||||
|
"native_size": [
|
||||||
|
1920,
|
||||||
|
1200
|
||||||
|
],
|
||||||
|
"oriented_size": [
|
||||||
|
1920,
|
||||||
|
1200
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"re": {
|
||||||
|
"rotate_deg": 0,
|
||||||
|
"flip_horizontal": false,
|
||||||
|
"flip_vertical": false,
|
||||||
|
"native_size": [
|
||||||
|
1280,
|
||||||
|
800
|
||||||
|
],
|
||||||
|
"oriented_size": [
|
||||||
|
1280,
|
||||||
|
800
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nir": {
|
||||||
|
"rotate_deg": 0,
|
||||||
|
"flip_horizontal": false,
|
||||||
|
"flip_vertical": false,
|
||||||
|
"native_size": [
|
||||||
|
1280,
|
||||||
|
800
|
||||||
|
],
|
||||||
|
"oriented_size": [
|
||||||
|
1280,
|
||||||
|
800
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"runtime_policy": {
|
||||||
|
"onnx_model_path": null,
|
||||||
|
"frame_type": "RAW_BRUTO",
|
||||||
|
"capture_mode_requested": "AUTO",
|
||||||
|
"capture_mode_effective": "AUTO",
|
||||||
|
"raw_policy": "allow_single",
|
||||||
|
"rgb_processing": {
|
||||||
|
"mode": "bayer_planes"
|
||||||
|
},
|
||||||
|
"fusion_runtime": {
|
||||||
|
"use_remap_cache": true,
|
||||||
|
"use_remap_for_rgb": false,
|
||||||
|
"use_remap_for_spec": true,
|
||||||
|
"crop_valid_common": true,
|
||||||
|
"resize_after_crop": true,
|
||||||
|
"target_size": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"calibration_provenance": {
|
||||||
|
"hardware_signature": {
|
||||||
|
"rgb": {
|
||||||
|
"socket": "CAM_A",
|
||||||
|
"sensor": "AR0234",
|
||||||
|
"size": [
|
||||||
|
1920,
|
||||||
|
1200
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"re": {
|
||||||
|
"socket": "CAM_B",
|
||||||
|
"sensor": "OV9282",
|
||||||
|
"size": [
|
||||||
|
1280,
|
||||||
|
800
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nir": {
|
||||||
|
"socket": "CAM_C",
|
||||||
|
"sensor": "OV9282",
|
||||||
|
"size": [
|
||||||
|
1280,
|
||||||
|
800
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"device_mx_id": "194430108133AC2F00",
|
||||||
|
"module_id": null,
|
||||||
|
"camera_orientation": {
|
||||||
|
"source": "homography.camera_orientation_signature",
|
||||||
|
"schema": "multispec_camera_orientation_v1",
|
||||||
|
"enabled": true,
|
||||||
|
"input_space": "native_stream_no_external_undistort",
|
||||||
|
"output_space": "canonical_oriented_stream_no_external_undistort",
|
||||||
|
"apply_stage": "after_native_flat_before_fusion",
|
||||||
|
"selected_homography_profile": "media"
|
||||||
|
},
|
||||||
|
"artifacts": {
|
||||||
|
"focus": {
|
||||||
|
"path": "calibration/focus_qc_active.json",
|
||||||
|
"sha256": "8c0f3ff2303a64f60b43e5a2f2966548b7720a2e43e7a46504a0a949ae6856f9",
|
||||||
|
"schema": "multispec_focus_qc_v3",
|
||||||
|
"status": "pass",
|
||||||
|
"promoted": true,
|
||||||
|
"session_id": null,
|
||||||
|
"created_at": "2026-09-08 16:03:05",
|
||||||
|
"finished_at": "2026-09-08 16:04:46"
|
||||||
|
},
|
||||||
|
"flatfield": {
|
||||||
|
"path": "calibration/flatfield_maps_v1.json",
|
||||||
|
"sha256": "1871312008b14ae4efb5d77ad7c9eb580af9c330775cccc9fa787f58f6f64648",
|
||||||
|
"schema": "multispec_flatfield_production_v2",
|
||||||
|
"status": "warning",
|
||||||
|
"promoted": true,
|
||||||
|
"session_id": "20260908_163058",
|
||||||
|
"created_at": "2026-09-08 16:31:07",
|
||||||
|
"finished_at": "2026-09-08 16:32:57",
|
||||||
|
"npz_path": "calibration/flatfield_maps_v1.npz",
|
||||||
|
"npz_sha256": "869a25192d51464a65f55852404fba01089c7aa181d975e5960a8154096cbcd4"
|
||||||
|
},
|
||||||
|
"radiometry": {
|
||||||
|
"path": "calibration/radiometry_calibration_v5.json",
|
||||||
|
"sha256": "0c9934f20a7a5e48b8c42e682e193941e841f696e799ae9834576bd9e2522ac3",
|
||||||
|
"schema": "multispec_radiometric_calibration_v6",
|
||||||
|
"status": "warning",
|
||||||
|
"promoted": true,
|
||||||
|
"session_id": "20260908_154252_449094",
|
||||||
|
"created_at": "2026-09-08 15:43:01",
|
||||||
|
"finished_at": "2026-09-08 15:44:01"
|
||||||
|
},
|
||||||
|
"startup": {
|
||||||
|
"path": "calibration/camera_startup_profile_v3.json",
|
||||||
|
"sha256": "e63e423a5d7b4fe3231352bd27e5dd3f26d0e03bb5d7c2ee6b2fe48084298599",
|
||||||
|
"schema": "multispec_camera_startup_profile_v3",
|
||||||
|
"status": "good",
|
||||||
|
"promoted": true,
|
||||||
|
"session_id": null,
|
||||||
|
"created_at": "2026-09-08 15:45:46",
|
||||||
|
"finished_at": null
|
||||||
|
},
|
||||||
|
"intrinsics": {
|
||||||
|
"path": "calibration/intrinsics_calibration_v1.json",
|
||||||
|
"sha256": "2b5d36825e40d9208cbfb7ab7f49d09c694ad26ab6f5db52fba706ab5a2d4f7d",
|
||||||
|
"schema": "multispec_intrinsics_calibration_v1",
|
||||||
|
"status": "warning_promoted",
|
||||||
|
"promoted": true,
|
||||||
|
"session_id": "20260908_144510_049990",
|
||||||
|
"created_at": "2026-09-08 14:45:15",
|
||||||
|
"finished_at": "2026-09-08 14:55:09"
|
||||||
|
},
|
||||||
|
"homography": {
|
||||||
|
"path": "calibration/homography_calibration_v4.json",
|
||||||
|
"sha256": "945af322860e5e704735cb5ac7f37918ee79e67826088e4ff13e1e54adf73378",
|
||||||
|
"schema": "multispec_homography_calibration_v4",
|
||||||
|
"status": null,
|
||||||
|
"promoted": null,
|
||||||
|
"session_id": null,
|
||||||
|
"created_at": "2026-09-08 15:11:12",
|
||||||
|
"finished_at": null,
|
||||||
|
"selected_profile": "media"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"final_checks": {
|
||||||
|
"focus_gate": "pass",
|
||||||
|
"flatfield": "pass",
|
||||||
|
"radiometry": "pass",
|
||||||
|
"startup": "pass",
|
||||||
|
"intrinsics": "pass",
|
||||||
|
"homography": "pass",
|
||||||
|
"hardware_consistency": "pass",
|
||||||
|
"geometry_consistency": "pass",
|
||||||
|
"camera_orientation": "pass",
|
||||||
|
"startup_radiometry_consistency": "pass",
|
||||||
|
"flatfield_npz_integrity": "pass",
|
||||||
|
"module_params_structure": "pass"
|
||||||
|
},
|
||||||
|
"output_sha256": "efcaddc1d7687cb3ed1c9c9f9f7e2a9a9a1e6c260c3af967473590dec32b313c"
|
||||||
|
}
|
||||||
|
|
@ -28,7 +28,7 @@ module_params is loaded. The strict fail-closed behavior is enabled only for
|
||||||
module_params generated by the production assembler.
|
module_params generated by the production assembler.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
RAW_PROCESSOR_CORE_VERSION = "production_v1_2026_08_24"
|
RAW_PROCESSOR_CORE_VERSION = "production_v2_2026_09_08"
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
@ -3913,20 +3913,68 @@ class RawProcessorCore:
|
||||||
"radiometric_normalization.enabled=true."
|
"radiometric_normalization.enabled=true."
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
radiometric_method = str(
|
||||||
str(
|
rad_norm.get(
|
||||||
rad_norm.get(
|
"method",
|
||||||
"method",
|
"",
|
||||||
"",
|
)
|
||||||
)
|
).lower()
|
||||||
).lower()
|
|
||||||
!= "oak_ae_frame_controls_v1"
|
allowed_radiometric_methods = {
|
||||||
):
|
"oak_ae_frame_controls_v1",
|
||||||
|
"oak_ae_frame_controls_affine_v2",
|
||||||
|
}
|
||||||
|
|
||||||
|
if radiometric_method not in allowed_radiometric_methods:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"Método radiométrico inválido no produto: "
|
"Método radiométrico inválido no produto: "
|
||||||
f"{rad_norm.get('method')!r}"
|
f"{rad_norm.get('method')!r}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if radiometric_method == "oak_ae_frame_controls_affine_v2":
|
||||||
|
black_offset_model = str(
|
||||||
|
rad_norm.get(
|
||||||
|
"black_offset_model",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
).lower()
|
||||||
|
|
||||||
|
if black_offset_model != "per_role_scalar_raw01":
|
||||||
|
raise RuntimeError(
|
||||||
|
"black_offset_model inválido para affine_v2: "
|
||||||
|
f"{rad_norm.get('black_offset_model')!r}. "
|
||||||
|
"Esperado='per_role_scalar_raw01'."
|
||||||
|
)
|
||||||
|
|
||||||
|
black_offsets = (
|
||||||
|
rad_norm.get(
|
||||||
|
"black_offset_by_role",
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
or {}
|
||||||
|
)
|
||||||
|
|
||||||
|
for role in (
|
||||||
|
"rgb",
|
||||||
|
"re",
|
||||||
|
"nir",
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
offset = float(
|
||||||
|
black_offsets[role]
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
raise RuntimeError(
|
||||||
|
"radiometric_normalization sem black offset "
|
||||||
|
f"válido para role={role}."
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
if not np.isfinite(offset) or not (0.0 <= offset < 1.0):
|
||||||
|
raise RuntimeError(
|
||||||
|
"black offset fora do domínio RAW01 em "
|
||||||
|
f"role={role}: {offset!r}"
|
||||||
|
)
|
||||||
|
|
||||||
factor_model = str(
|
factor_model = str(
|
||||||
rad_norm.get(
|
rad_norm.get(
|
||||||
"factor_model",
|
"factor_model",
|
||||||
|
|
@ -4464,16 +4512,45 @@ class RawProcessorCore:
|
||||||
# FUSION SPACE
|
# FUSION SPACE
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
|
# No contrato com perfis, coordinate_space pertence ao profile ativo.
|
||||||
|
# Mantemos fallback para o campo top-level por compatibilidade legada.
|
||||||
fusion_space = str(
|
fusion_space = str(
|
||||||
fusion.get(
|
fusion.get(
|
||||||
"coordinate_space",
|
"coordinate_space",
|
||||||
"",
|
"",
|
||||||
)
|
)
|
||||||
|
or ""
|
||||||
).strip()
|
).strip()
|
||||||
|
|
||||||
|
if not fusion_space:
|
||||||
|
selected_profile = self._resolve_homography_profile_name_for_role(
|
||||||
|
"re"
|
||||||
|
)
|
||||||
|
profiles = fusion.get(
|
||||||
|
"homography_profiles",
|
||||||
|
{},
|
||||||
|
) or {}
|
||||||
|
profile = profiles.get(selected_profile)
|
||||||
|
|
||||||
|
if profile is None and isinstance(profiles, dict):
|
||||||
|
for profile_name, profile_item in profiles.items():
|
||||||
|
if str(profile_name).lower() == str(selected_profile).lower():
|
||||||
|
profile = profile_item
|
||||||
|
break
|
||||||
|
|
||||||
|
if isinstance(profile, dict):
|
||||||
|
fusion_space = str(
|
||||||
|
profile.get(
|
||||||
|
"coordinate_space",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
or ""
|
||||||
|
).strip()
|
||||||
|
|
||||||
if not fusion_space:
|
if not fusion_space:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"fusion_config.coordinate_space ausente."
|
"coordinate_space ausente no fusion_config e no "
|
||||||
|
"homography_profile ativo."
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|
@ -5650,11 +5727,18 @@ class RawProcessorCore:
|
||||||
return decoded
|
return decoded
|
||||||
|
|
||||||
method = str(cfg.get("method", "oak_ae_frame_controls_v1")).lower()
|
method = str(cfg.get("method", "oak_ae_frame_controls_v1")).lower()
|
||||||
if method not in ("oak_ae_frame_controls_v1", "exposure_iso_reference"):
|
if method not in (
|
||||||
|
"oak_ae_frame_controls_v1",
|
||||||
|
"oak_ae_frame_controls_affine_v2",
|
||||||
|
"exposure_iso_reference",
|
||||||
|
):
|
||||||
result["warnings"].append(f"unsupported_method:{method}")
|
result["warnings"].append(f"unsupported_method:{method}")
|
||||||
self.last_radiometric_normalization_result = result
|
self.last_radiometric_normalization_result = result
|
||||||
return decoded
|
return decoded
|
||||||
|
|
||||||
|
affine_v2 = method == "oak_ae_frame_controls_affine_v2"
|
||||||
|
black_offset_by_role = cfg.get("black_offset_by_role", {}) or {}
|
||||||
|
|
||||||
controls_by_role, controls_by_cam = self._extract_frame_controls_from_meta_by_role(meta)
|
controls_by_role, controls_by_cam = self._extract_frame_controls_from_meta_by_role(meta)
|
||||||
|
|
||||||
if not controls_by_role:
|
if not controls_by_role:
|
||||||
|
|
@ -5677,6 +5761,7 @@ class RawProcessorCore:
|
||||||
|
|
||||||
# Cache por role para não recalcular factor/scale 3x quando RGB tem 3 canais no mesmo item.
|
# Cache por role para não recalcular factor/scale 3x quando RGB tem 3 canais no mesmo item.
|
||||||
scale_by_role = {}
|
scale_by_role = {}
|
||||||
|
offset_by_role = {}
|
||||||
debug_by_role = {}
|
debug_by_role = {}
|
||||||
|
|
||||||
normalized = {}
|
normalized = {}
|
||||||
|
|
@ -5692,6 +5777,7 @@ class RawProcessorCore:
|
||||||
|
|
||||||
if role in scale_by_role:
|
if role in scale_by_role:
|
||||||
scale = scale_by_role[role]
|
scale = scale_by_role[role]
|
||||||
|
offset = offset_by_role[role]
|
||||||
debug = dict(debug_by_role[role])
|
debug = dict(debug_by_role[role])
|
||||||
debug["camera_id"] = cam_id
|
debug["camera_id"] = cam_id
|
||||||
else:
|
else:
|
||||||
|
|
@ -5721,6 +5807,44 @@ class RawProcessorCore:
|
||||||
raw_scale = float(ref_factor / actual_factor)
|
raw_scale = float(ref_factor / actual_factor)
|
||||||
scale = self._clip_radiometric_scale(raw_scale, role, cfg)
|
scale = self._clip_radiometric_scale(raw_scale, role, cfg)
|
||||||
|
|
||||||
|
offset = 0.0
|
||||||
|
if affine_v2:
|
||||||
|
try:
|
||||||
|
offset = float(black_offset_by_role[role])
|
||||||
|
except Exception:
|
||||||
|
normalized[cam_id] = item
|
||||||
|
result["warnings"].append(
|
||||||
|
f"{role}:invalid_black_offset"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
getattr(self, "strict_product_contract", False)
|
||||||
|
or str(cfg.get("invalid_controls_policy", "skip")).lower() == "raise"
|
||||||
|
):
|
||||||
|
raise RuntimeError(
|
||||||
|
"Black offset radiométrico inválido ou ausente "
|
||||||
|
f"para role={role}."
|
||||||
|
)
|
||||||
|
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not np.isfinite(offset) or not (0.0 <= offset < 1.0):
|
||||||
|
normalized[cam_id] = item
|
||||||
|
result["warnings"].append(
|
||||||
|
f"{role}:invalid_black_offset:{offset!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
getattr(self, "strict_product_contract", False)
|
||||||
|
or str(cfg.get("invalid_controls_policy", "skip")).lower() == "raise"
|
||||||
|
):
|
||||||
|
raise RuntimeError(
|
||||||
|
"Black offset radiométrico fora do domínio RAW01 "
|
||||||
|
f"para role={role}: {offset!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
continue
|
||||||
|
|
||||||
debug = self._build_radiometric_debug(
|
debug = self._build_radiometric_debug(
|
||||||
method=method,
|
method=method,
|
||||||
role=role,
|
role=role,
|
||||||
|
|
@ -5732,16 +5856,26 @@ class RawProcessorCore:
|
||||||
raw_scale=raw_scale,
|
raw_scale=raw_scale,
|
||||||
scale=scale,
|
scale=scale,
|
||||||
clip_output=clip_output,
|
clip_output=clip_output,
|
||||||
|
black_offset=offset if affine_v2 else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
scale_by_role[role] = scale
|
scale_by_role[role] = scale
|
||||||
|
offset_by_role[role] = offset
|
||||||
debug_by_role[role] = dict(debug)
|
debug_by_role[role] = dict(debug)
|
||||||
|
|
||||||
out, reused = self._apply_radiometric_scale_inplace(
|
if affine_v2:
|
||||||
img,
|
out, reused = self._apply_radiometric_affine_inplace(
|
||||||
scale=scale,
|
img,
|
||||||
clip_output=clip_output,
|
scale=scale,
|
||||||
)
|
black_offset=offset,
|
||||||
|
clip_output=clip_output,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
out, reused = self._apply_radiometric_scale_inplace(
|
||||||
|
img,
|
||||||
|
scale=scale,
|
||||||
|
clip_output=clip_output,
|
||||||
|
)
|
||||||
|
|
||||||
new_item = dict(item)
|
new_item = dict(item)
|
||||||
new_meta = dict(item.get("meta", {}) or {})
|
new_meta = dict(item.get("meta", {}) or {})
|
||||||
|
|
@ -5753,6 +5887,8 @@ class RawProcessorCore:
|
||||||
new_meta["radiometric_normalization"] = debug
|
new_meta["radiometric_normalization"] = debug
|
||||||
new_meta["radiometric_normalization_applied"] = True
|
new_meta["radiometric_normalization_applied"] = True
|
||||||
new_meta["radiometric_normalization_scale"] = float(scale)
|
new_meta["radiometric_normalization_scale"] = float(scale)
|
||||||
|
if affine_v2:
|
||||||
|
new_meta["radiometric_normalization_black_offset"] = float(offset)
|
||||||
|
|
||||||
new_item["image"] = out
|
new_item["image"] = out
|
||||||
new_item["meta"] = new_meta
|
new_item["meta"] = new_meta
|
||||||
|
|
@ -5775,6 +5911,12 @@ class RawProcessorCore:
|
||||||
"inplace_fast_path": True,
|
"inplace_fast_path": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if affine_v2:
|
||||||
|
result["summary"]["black_offset_by_role"] = {
|
||||||
|
role: float(value)
|
||||||
|
for role, value in offset_by_role.items()
|
||||||
|
}
|
||||||
|
|
||||||
self.last_radiometric_normalization_result = result
|
self.last_radiometric_normalization_result = result
|
||||||
return normalized
|
return normalized
|
||||||
|
|
||||||
|
|
@ -6324,8 +6466,55 @@ class RawProcessorCore:
|
||||||
|
|
||||||
return out, reused
|
return out, reused
|
||||||
|
|
||||||
def _build_radiometric_debug(self, method, role, cam_id, actual_ctrl, ref_ctrl, actual_factor, ref_factor, raw_scale, scale, clip_output):
|
def _apply_radiometric_affine_inplace(
|
||||||
return {
|
self,
|
||||||
|
img,
|
||||||
|
scale: float,
|
||||||
|
black_offset: float,
|
||||||
|
clip_output: bool,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Aplica o contrato radiométrico affine_v2 no domínio RAW01:
|
||||||
|
|
||||||
|
offset + (value - offset) * reference_factor / actual_factor
|
||||||
|
|
||||||
|
O mesmo offset escalar é aplicado aos três canais RGB da role RGB,
|
||||||
|
conforme black_offset_model='per_role_scalar_raw01'.
|
||||||
|
"""
|
||||||
|
out, reused = self._radiometric_get_writable_float32_image(img)
|
||||||
|
|
||||||
|
if out is None:
|
||||||
|
return img, False
|
||||||
|
|
||||||
|
scale = np.float32(float(scale))
|
||||||
|
offset = np.float32(float(black_offset))
|
||||||
|
|
||||||
|
# Para scale=1 a transformação é identidade, independentemente do offset.
|
||||||
|
if abs(float(scale) - 1.0) > 1e-6:
|
||||||
|
np.subtract(out, offset, out=out, casting="unsafe")
|
||||||
|
np.multiply(out, scale, out=out, casting="unsafe")
|
||||||
|
np.add(out, offset, out=out, casting="unsafe")
|
||||||
|
|
||||||
|
if clip_output:
|
||||||
|
np.clip(out, 0.0, 1.0, out=out)
|
||||||
|
|
||||||
|
return out, reused
|
||||||
|
|
||||||
|
def _build_radiometric_debug(
|
||||||
|
self,
|
||||||
|
method,
|
||||||
|
role,
|
||||||
|
cam_id,
|
||||||
|
actual_ctrl,
|
||||||
|
ref_ctrl,
|
||||||
|
actual_factor,
|
||||||
|
ref_factor,
|
||||||
|
raw_scale,
|
||||||
|
scale,
|
||||||
|
clip_output,
|
||||||
|
black_offset=None,
|
||||||
|
):
|
||||||
|
debug = {
|
||||||
"applied": True,
|
"applied": True,
|
||||||
"method": method,
|
"method": method,
|
||||||
"role": role,
|
"role": role,
|
||||||
|
|
@ -6339,6 +6528,16 @@ class RawProcessorCore:
|
||||||
"clip_output": bool(clip_output),
|
"clip_output": bool(clip_output),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if black_offset is not None:
|
||||||
|
debug["black_offset_model"] = "per_role_scalar_raw01"
|
||||||
|
debug["black_offset"] = float(black_offset)
|
||||||
|
debug["formula"] = (
|
||||||
|
"offset + (value - offset) * "
|
||||||
|
"reference_factor / actual_factor"
|
||||||
|
)
|
||||||
|
|
||||||
|
return debug
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _bayer_pattern_to_code_fast(self, bayer_pattern: str | None) -> int:
|
def _bayer_pattern_to_code_fast(self, bayer_pattern: str | None) -> int:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue