iniciado testes com depth

This commit is contained in:
Diego Freitas 2026-05-26 08:01:47 -03:00
parent 4ec3c35759
commit 7581989647
12 changed files with 7841 additions and 43 deletions

View File

@ -0,0 +1,247 @@
{
"batchName": "",
"batchTime": 1756192307,
"boardConf": "nIR-C00M05-00",
"boardCustom": "",
"boardName": "DM1090",
"boardOptions": 0,
"boardRev": "R3M0E3",
"cameraData": [
[
1,
{
"cameraType": 0,
"distortionCoeff": [
-0.2667059302330017,
0.24688485264778137,
-0.000222075788769871,
0.00012030570360366255,
-0.039670947939157486,
-0.26931697130203247,
0.24513530731201172,
-0.039285946637392044,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0
],
"extrinsics": {
"rotationMatrix": [
[
0.9999990463256836,
-0.00016664451686665416,
-0.0013813182013109326
],
[
0.00011326302046654746,
0.999256432056427,
-0.038555704057216644
],
[
0.0013867162633687258,
0.038555510342121124,
0.9992554783821106
]
],
"specTranslation": {
"x": 2.5,
"y": 0.0,
"z": 0.0
},
"toCameraSocket": 2,
"translation": {
"x": 2.3664393424987793,
"y": 0.2041260451078415,
"z": 8.34726333618164
}
},
"height": 800,
"intrinsicMatrix": [
[
329.8727111816406,
0.0,
655.0603637695312
],
[
0.0,
329.14971923828125,
353.3602600097656
],
[
0.0,
0.0,
1.0
]
],
"lensPosition": 0,
"specHfovDeg": 70.9000015258789,
"width": 1280
}
],
[
2,
{
"cameraType": 0,
"distortionCoeff": [
2.599353075027466,
1.7201688289642334,
-0.0002882175613194704,
0.0005083332071080804,
-2.265909433364868,
2.5841195583343506,
1.6573377847671509,
-2.2209253311157227,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0
],
"extrinsics": {
"rotationMatrix": [
[
0.0,
0.0,
0.0
],
[
0.0,
0.0,
0.0
],
[
0.0,
0.0,
0.0
]
],
"specTranslation": {
"x": -0.0,
"y": -0.0,
"z": -0.0
},
"toCameraSocket": -1,
"translation": {
"x": 0.0,
"y": 0.0,
"z": 0.0
}
},
"height": 800,
"intrinsicMatrix": [
[
577.9281005859375,
0.0,
644.1484375
],
[
0.0,
577.5459594726562,
379.2917175292969
],
[
0.0,
0.0,
1.0
]
],
"lensPosition": 0,
"specHfovDeg": 70.9000015258789,
"width": 1280
}
]
],
"deviceName": "",
"hardwareConf": "F0-FV00-BC000",
"housingExtrinsics": {
"rotationMatrix": [],
"specTranslation": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"toCameraSocket": -1,
"translation": {
"x": 0.0,
"y": 0.0,
"z": 0.0
}
},
"imuExtrinsics": {
"rotationMatrix": [
[
1.0,
0.0,
0.0
],
[
0.0,
1.0,
0.0
],
[
0.0,
0.0,
1.0
]
],
"specTranslation": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"toCameraSocket": -1,
"translation": {
"x": 0.0,
"y": 0.0,
"z": 0.0
}
},
"miscellaneousData": [],
"productName": "OAK-FFC-3P",
"stereoEnableDistortionCorrection": false,
"stereoRectificationData": {
"leftCameraSocket": 1,
"rectifiedRotationLeft": [
[
0.27401065826416016,
0.06054104119539261,
0.9598191976547241
],
[
-0.0419992059469223,
0.9978178143501282,
-0.050947822630405426
],
[
-0.9608091711997986,
-0.02635139599442482,
0.2759554088115692
]
],
"rectifiedRotationRight": [
[
0.2726745009422302,
0.02352055534720421,
0.9618188142776489
],
[
-0.04209506884217262,
0.9990354776382446,
-0.012496757321059704
],
[
-0.9611850380897522,
-0.03708028048276901,
0.2734015882015228
]
],
"rightCameraSocket": 2
},
"stereoUseSpecTranslation": true,
"version": 7,
"verticalCameraSocket": -1
}

View File

@ -0,0 +1,282 @@
{
"created_at": "2026-05-25 13:57:51",
"device_id": "194430108133AC2F00",
"calibration_read_method": "readCalibration",
"connected_cameras": [
{
"socket": "CAM_A",
"sensorName": "OV9782",
"width": 1280,
"height": 800,
"orientation": "CameraImageOrientation.AUTO",
"supportedTypes": [
"CameraSensorType.COLOR",
"CameraSensorType.MONO"
]
},
{
"socket": "CAM_B",
"sensorName": "OV9282",
"width": 1280,
"height": 800,
"orientation": "CameraImageOrientation.AUTO",
"supportedTypes": [
"CameraSensorType.MONO",
"CameraSensorType.COLOR"
]
},
{
"socket": "CAM_C",
"sensorName": "OV9282",
"width": 1280,
"height": 800,
"orientation": "CameraImageOrientation.AUTO",
"supportedTypes": [
"CameraSensorType.MONO",
"CameraSensorType.COLOR"
]
}
],
"sockets_requested": [
"CAM_A",
"CAM_B",
"CAM_C"
],
"width": 1280,
"height": 800,
"calibration_dump": {
"available": true,
"method": "eepromToJson",
"path": "calibration\\calibration_probe_out\\calibration_eepromToJson.json",
"error": null
},
"sockets": [
{
"socket": "CAM_A",
"intrinsics": null,
"intrinsics_method": "failed:getCameraIntrinsics",
"intrinsics_ok": false,
"distortion": null,
"distortion_method": "missing:distortion",
"distortion_ok": false,
"fov_deg": null,
"fov_method": "missing:getFov"
},
{
"socket": "CAM_B",
"intrinsics": [
[
329.8727111816406,
0.0,
655.0603637695312
],
[
0.0,
329.14971923828125,
353.3602600097656
],
[
0.0,
0.0,
1.0
]
],
"intrinsics_method": "getCameraIntrinsics3args",
"intrinsics_ok": true,
"distortion": [
-0.2667059302330017,
0.24688485264778137,
-0.000222075788769871,
0.00012030570360366255,
-0.039670947939157486,
-0.26931697130203247,
0.24513530731201172,
-0.039285946637392044,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0
],
"distortion_method": "getDistortionCoefficients",
"distortion_ok": true,
"fov_deg": 70.9000015258789,
"fov_method": "getFov"
},
{
"socket": "CAM_C",
"intrinsics": [
[
577.9281005859375,
0.0,
644.1484375
],
[
0.0,
577.5459594726562,
379.2917175292969
],
[
0.0,
0.0,
1.0
]
],
"intrinsics_method": "getCameraIntrinsics3args",
"intrinsics_ok": true,
"distortion": [
2.599353075027466,
1.7201688289642334,
-0.0002882175613194704,
0.0005083332071080804,
-2.265909433364868,
2.5841195583343506,
1.6573377847671509,
-2.2209253311157227,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0
],
"distortion_method": "getDistortionCoefficients",
"distortion_ok": true,
"fov_deg": 70.9000015258789,
"fov_method": "getFov"
}
],
"pairs": [
{
"pair": "CAM_A->CAM_B",
"src": "CAM_A",
"dst": "CAM_B",
"extrinsics": null,
"extrinsics_method": "failed:getCameraExtrinsics",
"extrinsics_ok": false,
"translation": null,
"baseline": null,
"baseline_method": "missing:getBaselineDistance",
"baseline_ok": false
},
{
"pair": "CAM_A->CAM_C",
"src": "CAM_A",
"dst": "CAM_C",
"extrinsics": null,
"extrinsics_method": "failed:getCameraExtrinsics",
"extrinsics_ok": false,
"translation": null,
"baseline": null,
"baseline_method": "missing:getBaselineDistance",
"baseline_ok": false
},
{
"pair": "CAM_B->CAM_A",
"src": "CAM_B",
"dst": "CAM_A",
"extrinsics": null,
"extrinsics_method": "failed:getCameraExtrinsics",
"extrinsics_ok": false,
"translation": null,
"baseline": null,
"baseline_method": "missing:getBaselineDistance",
"baseline_ok": false
},
{
"pair": "CAM_B->CAM_C",
"src": "CAM_B",
"dst": "CAM_C",
"extrinsics": [
[
0.9999990463256836,
-0.00016664451686665416,
-0.0013813182013109326,
2.3664393424987793
],
[
0.00011326302046654746,
0.999256432056427,
-0.038555704057216644,
0.2041260451078415
],
[
0.0013867162633687258,
0.038555510342121124,
0.9992554783821106,
8.34726333618164
],
[
0.0,
0.0,
0.0,
1.0
]
],
"extrinsics_method": "getCameraExtrinsics2args",
"extrinsics_ok": true,
"translation": [
2.3664393424987793,
0.2041260451078415,
8.34726333618164
],
"baseline": 2.5,
"baseline_method": "getBaselineDistance2args",
"baseline_ok": true
},
{
"pair": "CAM_C->CAM_A",
"src": "CAM_C",
"dst": "CAM_A",
"extrinsics": null,
"extrinsics_method": "failed:getCameraExtrinsics",
"extrinsics_ok": false,
"translation": null,
"baseline": null,
"baseline_method": "missing:getBaselineDistance",
"baseline_ok": false
},
{
"pair": "CAM_C->CAM_B",
"src": "CAM_C",
"dst": "CAM_B",
"extrinsics": [
[
0.9999990463256836,
0.00011326302046654746,
0.0013867162633687258,
-2.378035545349121
],
[
-0.00016664451686665416,
0.999256432056427,
0.038555510342121124,
-0.525412917137146
],
[
-0.0013813182013109326,
-0.038555704057216644,
0.9992554783821106,
-8.329909324645996
],
[
0.0,
0.0,
0.0,
1.0
]
],
"extrinsics_method": "getCameraExtrinsics2args",
"extrinsics_ok": true,
"translation": [
-2.378035545349121,
-0.525412917137146,
-8.329909324645996
],
"baseline": 2.5,
"baseline_method": "getBaselineDistance2args",
"baseline_ok": true
}
]
}

View File

@ -588,15 +588,15 @@
"reference_mode": "fixed",
"reference_controls": {
"rgb": {
"exposure_time_us": 2200,
"exposure_time_us": 3000,
"sensitivity_iso": 100
},
"re": {
"exposure_time_us": 2500,
"exposure_time_us": 4000,
"sensitivity_iso": 100
},
"nir": {
"exposure_time_us": 2500,
"exposure_time_us": 4000,
"sensitivity_iso": 100
}
},
@ -612,11 +612,11 @@
},
"re": {
"min": 0.15,
"max": 2.5
"max": 3.0
},
"nir": {
"min": 0.15,
"max": 2.5
"max": 3.0
}
},
@ -679,7 +679,7 @@
"rgb_saturation_threshold": 0.97
},
"rgb_calibration": {
"enabled": true,
"enabled": false,
"gains": {
"R": 1.061,
"G": 1.0,

View File

@ -1,7 +1,7 @@
{
"camera": "oak-fcc-3",
"modelo": "segformer_b1",
"model_name": "target_aug",
"model_name": "target_fixed",
"main_class_name": "cana",
"es_classes": "",
"model_to_use": "geral",

View File

@ -878,6 +878,19 @@ class OakFcc3Manager:
self.has_imu_pipeline = False
self.running = False
def _is_fatal_depthai_error(self, erro):
txt = str(erro)
sinais = [
"X_LINK_ERROR",
"Communication exception",
"Couldn't read data from stream",
"Device already closed",
"device has been closed",
]
return any(s in txt for s in sinais)
# ============================================================
# Status
# ============================================================
@ -924,7 +937,15 @@ class OakFcc3Manager:
def get_next_frame(self, timeout=1.0):
if not self.running:
raise RuntimeError("OakFcc3Manager não está rodando. Chame start() primeiro.")
last_error = None
try:
last_error = self._capture_thread_stats.get("last_error")
except Exception:
pass
raise RuntimeError(
f"OakFcc3Manager não está rodando. Último erro: {last_error}"
)
# Fallback síncrono se desligar async.
if not bool(getattr(self, "async_capture_enabled", True)):
@ -1785,10 +1806,34 @@ class OakFcc3Manager:
self._capture_cond.notify_all()
except Exception as e:
erro = f"{type(e).__name__}: {e}"
try:
self._capture_thread_stats["last_error"] = f"{type(e).__name__}: {e}"
self._capture_thread_stats["last_error"] = erro
except Exception:
pass
if self._is_fatal_depthai_error(e):
try:
self._capture_thread_stats["fatal_error"] = True
except Exception:
pass
self.running = False
try:
self._capture_stop_event.set()
except Exception:
pass
try:
with self._capture_cond:
self._capture_cond.notify_all()
except Exception:
pass
break
time.sleep(0.005)
try:

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,420 @@
import argparse
import time
from collections import deque
from typing import Optional
from pathlib import Path
import cv2
import numpy as np
try:
import depthai as dai
except Exception as e:
raise RuntimeError(
"Nao consegui importar depthai. Ative o venv correto e instale depthai antes de rodar. "
f"Erro original: {e}"
)
# ============================================================
# OAK-FCC-3P Charuco Preview Probe
# ------------------------------------------------------------
# Objetivo:
# Abrir CAM_A/CAM_B/CAM_C ao vivo para verificar se o Charuco no monitor
# ou impresso aparece bem nas tres cameras, principalmente nas mono RE/NIR.
#
# Exemplo:
# python -m utils.charuco_preview_probe --fps 10
#
# Teclas:
# Q/ESC = sair
# S = salvar snapshot
# E = alterna detector de bordas
# C = alterna contraste auto/normal nas mono
# ============================================================
# ============================================================
# DepthAI helpers
# ============================================================
def socket_from_name(name: str):
name = str(name).strip().upper()
aliases = {
"A": "CAM_A",
"B": "CAM_B",
"C": "CAM_C",
"RGB": "CAM_A",
"RE": "CAM_B",
"NIR": "CAM_C",
}
name = aliases.get(name, name)
if hasattr(dai.CameraBoardSocket, name):
return getattr(dai.CameraBoardSocket, name)
legacy = {
"CAM_A": getattr(dai.CameraBoardSocket, "RGB", None),
"CAM_B": getattr(dai.CameraBoardSocket, "LEFT", None),
"CAM_C": getattr(dai.CameraBoardSocket, "RIGHT", None),
}
if legacy.get(name) is not None:
return legacy[name]
raise ValueError(f"Socket invalido: {name}. Use CAM_A, CAM_B ou CAM_C.")
def mono_resolution_from_name(name: str):
name = str(name).strip().lower()
r = dai.MonoCameraProperties.SensorResolution
table = {
"400p": getattr(r, "THE_400_P", None),
"480p": getattr(r, "THE_480_P", None),
"720p": getattr(r, "THE_720_P", None),
"800p": getattr(r, "THE_800_P", None),
}
if name not in table or table[name] is None:
valid = ", ".join(k for k, v in table.items() if v is not None)
raise ValueError(f"Resolucao mono invalida: {name}. Valid={valid}")
return table[name]
def create_output_queue(output, name: str, max_size: int = 4, blocking: bool = False):
fn = getattr(output, "createOutputQueue", None)
if callable(fn):
return fn(maxSize=max_size, blocking=blocking)
raise RuntimeError(f"A saida '{name}' nao possui createOutputQueue().")
def get_frame(q) -> Optional[np.ndarray]:
if q is None:
return None
try:
msg = q.tryGet()
except Exception:
return None
if msg is None:
return None
try:
return msg.getCvFrame()
except Exception:
pass
try:
return msg.getFrame()
except Exception:
return None
# ============================================================
# Visual helpers
# ============================================================
def normalize_u8(img: np.ndarray, auto: bool = True) -> np.ndarray:
if img is None:
return np.zeros((300, 400), dtype=np.uint8)
arr = np.asarray(img)
if arr.ndim == 3:
return arr.astype(np.uint8)
arr = arr.astype(np.float32)
if not auto:
if arr.max() <= 1.5:
return np.clip(arr * 255.0, 0, 255).astype(np.uint8)
return np.clip(arr, 0, 255).astype(np.uint8)
finite = np.isfinite(arr)
if not np.any(finite):
return np.zeros(arr.shape[:2], dtype=np.uint8)
vals = arr[finite]
lo = float(np.percentile(vals, 1))
hi = float(np.percentile(vals, 99))
if hi <= lo + 1e-6:
hi = lo + 1.0
out = np.clip((arr - lo) / (hi - lo), 0, 1)
return (out * 255).astype(np.uint8)
def edge_view(gray_u8: np.ndarray) -> np.ndarray:
if gray_u8.ndim == 3:
gray_u8 = cv2.cvtColor(gray_u8, cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(gray_u8, 60, 140)
return edges
def put_label(img: np.ndarray, title: str, subtitle: str = "") -> np.ndarray:
if img.ndim == 2:
img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
out = img.copy()
hbox = 58 if subtitle else 36
cv2.rectangle(out, (0, 0), (out.shape[1], hbox), (0, 0, 0), -1)
cv2.putText(out, str(title)[:80], (10, 24), cv2.FONT_HERSHEY_SIMPLEX, 0.65, (0, 255, 255), 2, cv2.LINE_AA)
if subtitle:
cv2.putText(out, str(subtitle)[:115], (10, 48), cv2.FONT_HERSHEY_SIMPLEX, 0.43, (255, 255, 255), 1, cv2.LINE_AA)
return out
def resize_keep(img: np.ndarray, width: int) -> np.ndarray:
scale = width / img.shape[1]
height = max(1, int(img.shape[0] * scale))
return cv2.resize(img, (width, height), interpolation=cv2.INTER_AREA)
def make_grid(panels, panel_w: int = 520, cols: int = 3) -> np.ndarray:
rendered = []
for title, img, subtitle in panels:
if img.ndim == 2:
img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
small = resize_keep(img, panel_w)
rendered.append(put_label(small, title, subtitle))
max_h = max(x.shape[0] for x in rendered)
padded = []
for im in rendered:
if im.shape[0] < max_h:
pad = np.zeros((max_h - im.shape[0], im.shape[1], 3), dtype=np.uint8)
im = np.vstack([im, pad])
padded.append(im)
gap = 10
gap_w = np.full((max_h, gap, 3), 25, dtype=np.uint8)
rows = []
for i in range(0, len(padded), cols):
items = padded[i:i + cols]
while len(items) < cols:
items.append(np.zeros_like(padded[0]))
row = items[0]
for j in range(1, cols):
row = np.hstack([row, gap_w, items[j]])
rows.append(row)
gap_h = np.full((gap, rows[0].shape[1], 3), 25, dtype=np.uint8)
canvas = rows[0]
for row in rows[1:]:
canvas = np.vstack([canvas, gap_h, row])
return canvas
def stats_line(img: np.ndarray) -> str:
if img is None:
return "sem frame"
arr = np.asarray(img, dtype=np.float32)
if arr.ndim == 3:
gray = cv2.cvtColor(arr.astype(np.uint8), cv2.COLOR_BGR2GRAY).astype(np.float32)
else:
gray = arr
return f"mean={gray.mean():.1f} p05={np.percentile(gray,5):.1f} p95={np.percentile(gray,95):.1f}"
# ============================================================
# Pipeline
# ============================================================
def create_pipeline_and_outputs(args):
pipeline = dai.Pipeline()
# CAM_A color
rgb = pipeline.create(dai.node.ColorCamera)
rgb.setBoardSocket(socket_from_name(args.rgb))
rgb.setResolution(dai.ColorCameraProperties.SensorResolution.THE_800_P)
rgb.setFps(float(args.fps))
rgb.setInterleaved(False)
rgb.setColorOrder(dai.ColorCameraProperties.ColorOrder.BGR)
rgb.setPreviewSize(int(args.preview_w), int(args.preview_h))
# CAM_B/C mono
mono_b = pipeline.create(dai.node.MonoCamera)
mono_c = pipeline.create(dai.node.MonoCamera)
mono_b.setBoardSocket(socket_from_name(args.cam_b))
mono_c.setBoardSocket(socket_from_name(args.cam_c))
mono_b.setResolution(mono_resolution_from_name(args.mono_resolution))
mono_c.setResolution(mono_resolution_from_name(args.mono_resolution))
mono_b.setFps(float(args.fps))
mono_c.setFps(float(args.fps))
outputs = {
"rgb": rgb.preview,
"cam_b": mono_b.out,
"cam_c": mono_c.out,
}
return pipeline, outputs
# ============================================================
# Main
# ============================================================
def start_pipeline(pipeline):
fn = getattr(pipeline, "start", None)
if not callable(fn):
raise RuntimeError("pipeline.start() nao existe nesta versao do DepthAI.")
fn()
def stop_pipeline(pipeline):
try:
fn = getattr(pipeline, "stop", None)
if callable(fn):
fn()
except Exception:
pass
def save_snapshot(out_dir: str, rgb, cam_b, cam_c, canvas):
folder = Path(out_dir)
folder.mkdir(parents=True, exist_ok=True)
ts = time.strftime("%Y%m%d_%H%M%S")
if rgb is not None:
cv2.imwrite(str(folder / f"{ts}_CAM_A_rgb.png"), rgb)
if cam_b is not None:
cv2.imwrite(str(folder / f"{ts}_CAM_B_mono.png"), normalize_u8(cam_b, auto=True))
if cam_c is not None:
cv2.imwrite(str(folder / f"{ts}_CAM_C_mono.png"), normalize_u8(cam_c, auto=True))
if canvas is not None:
cv2.imwrite(str(folder / f"{ts}_canvas.png"), canvas)
print(f"[OK] snapshot salvo em {folder}")
def main(args):
pipeline, outputs = create_pipeline_and_outputs(args)
queues = {
name: create_output_queue(output, name, max_size=4, blocking=False)
for name, output in outputs.items()
}
print("[INFO] Pipeline preview criado sem StereoDepth.")
print("[INFO] Abra o PDF Charuco em tela cheia no monitor e aponte a camera para ele.")
print("[INFO] O objetivo e ver se CAM_B e CAM_C enxergam marcadores/cantos com contraste.")
start_pipeline(pipeline)
cv2.namedWindow("OAK-FCC-3P Charuco Preview Probe", cv2.WINDOW_NORMAL)
cv2.resizeWindow("OAK-FCC-3P Charuco Preview Probe", 1600, 900)
show_edges = False
auto_contrast = True
frame_times = deque(maxlen=40)
last_canvas = None
rgb_frame = None
b_frame = None
c_frame = None
try:
while True:
updated = False
for name, q in queues.items():
frame = get_frame(q)
if frame is None:
continue
updated = True
if name == "rgb":
rgb_frame = frame
elif name == "cam_b":
b_frame = frame
elif name == "cam_c":
c_frame = frame
if updated:
frame_times.append(time.time())
if len(frame_times) >= 2:
fps = (len(frame_times) - 1) / max(1e-6, frame_times[-1] - frame_times[0])
else:
fps = 0.0
if rgb_frame is None or b_frame is None or c_frame is None:
key = cv2.waitKey(1) & 0xFF
if key in (27, ord("q"), ord("Q")):
break
continue
rgb_vis = rgb_frame.copy()
b_vis = normalize_u8(b_frame, auto=auto_contrast)
c_vis = normalize_u8(c_frame, auto=auto_contrast)
if show_edges:
rgb_gray = cv2.cvtColor(rgb_vis, cv2.COLOR_BGR2GRAY)
rgb_panel = edge_view(rgb_gray)
b_panel = edge_view(b_vis)
c_panel = edge_view(c_vis)
mode = "edges"
else:
rgb_panel = rgb_vis
b_panel = b_vis
c_panel = c_vis
mode = "preview"
panels = [
("CAM_A RGB", rgb_panel, f"{stats_line(rgb_frame)} | fps={fps:.1f}"),
("CAM_B mono / RE", b_panel, stats_line(b_frame)),
("CAM_C mono / NIR", c_panel, stats_line(c_frame)),
("CAM_B edges" if not show_edges else "CAM_B preview", edge_view(b_vis) if not show_edges else b_vis, "bordas para ver marcador"),
("CAM_C edges" if not show_edges else "CAM_C preview", edge_view(c_vis) if not show_edges else c_vis, "bordas para ver marcador"),
("Info", np.zeros((300, 600, 3), dtype=np.uint8), f"mode={mode} auto_contrast={auto_contrast} | E edges | C contraste | S save | Q sair"),
]
canvas = make_grid(panels, panel_w=args.panel_w, cols=3)
# Escreve texto grande no painel Info vazio, ultimo quadrante.
info_y0 = canvas.shape[0] - resize_keep(np.zeros((300, 600, 3), dtype=np.uint8), args.panel_w).shape[0]
cv2.putText(canvas, "Charuco visibility test", (2 * (args.panel_w + 10) + 15, info_y0 + 95), cv2.FONT_HERSHEY_SIMPLEX, 0.75, (0, 255, 255), 2, cv2.LINE_AA)
cv2.putText(canvas, "Olhe CAM_B/C: marcadores precisam aparecer nitidos", (2 * (args.panel_w + 10) + 15, info_y0 + 135), cv2.FONT_HERSHEY_SIMPLEX, 0.52, (255, 255, 255), 1, cv2.LINE_AA)
cv2.putText(canvas, "E=edges C=auto contrast S=snapshot Q=sair", (2 * (args.panel_w + 10) + 15, info_y0 + 170), cv2.FONT_HERSHEY_SIMPLEX, 0.52, (255, 255, 255), 1, cv2.LINE_AA)
last_canvas = canvas
cv2.imshow("OAK-FCC-3P Charuco Preview Probe", canvas)
key = cv2.waitKey(1) & 0xFF
if key in (27, ord("q"), ord("Q")):
break
elif key in (ord("e"), ord("E")):
show_edges = not show_edges
elif key in (ord("c"), ord("C")):
auto_contrast = not auto_contrast
elif key in (ord("s"), ord("S")):
save_snapshot(args.out_dir, rgb_frame, b_frame, c_frame, last_canvas)
finally:
stop_pipeline(pipeline)
cv2.destroyAllWindows()
# ============================================================
# CLI
# ============================================================
def build_argparser():
ap = argparse.ArgumentParser(description="Preview rapido CAM_A/CAM_B/CAM_C para testar visibilidade do Charuco.")
ap.add_argument("--rgb", type=str, default="CAM_A")
ap.add_argument("--cam-b", type=str, default="CAM_B")
ap.add_argument("--cam-c", type=str, default="CAM_C")
ap.add_argument("--mono-resolution", type=str, default="800p", choices=["400p", "480p", "720p", "800p"])
ap.add_argument("--fps", type=float, default=10.0)
ap.add_argument("--preview-w", type=int, default=640)
ap.add_argument("--preview-h", type=int, default=400)
ap.add_argument("--panel-w", type=int, default=500)
ap.add_argument("--out-dir", type=str, default="charuco_preview_out")
return ap
if __name__ == "__main__":
main(build_argparser().parse_args())

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,690 @@
import argparse
import json
import time
from pathlib import Path
from collections import deque
from typing import Dict, Optional, Tuple
import cv2
import numpy as np
try:
import depthai as dai
except Exception as e:
raise RuntimeError(
"Nao consegui importar depthai. Ative o venv correto e instale depthai antes de rodar. "
f"Erro original: {e}"
)
# ============================================================
# OAK-FCC-3P Depth Probe - API v3 style
# ------------------------------------------------------------
# Este script evita XLinkOut/getOutputQueue, porque seu ambiente DepthAI
# nao expoe dai.node.XLinkOut. Ele usa createOutputQueue() direto nas saidas.
#
# Exemplo:
# python -m utils.depth_probe --left CAM_B --right CAM_C --rgb CAM_A --enable-rgb --lrcheck --extended --subpixel --confidence 200 --median 7
#
# Se depth/disparity parecer invertido ou muito ruim:
# python -m utils.depth_probe --left CAM_C --right CAM_B --rgb CAM_A --enable-rgb --lrcheck --extended --subpixel
# ============================================================
# ============================================================
# DepthAI helpers
# ============================================================
def socket_from_name(name: str):
name = str(name).strip().upper()
aliases = {
"A": "CAM_A",
"B": "CAM_B",
"C": "CAM_C",
"LEFT": "CAM_B",
"RIGHT": "CAM_C",
"RGB": "CAM_A",
}
name = aliases.get(name, name)
if hasattr(dai.CameraBoardSocket, name):
return getattr(dai.CameraBoardSocket, name)
legacy = {
"CAM_A": getattr(dai.CameraBoardSocket, "RGB", None),
"CAM_B": getattr(dai.CameraBoardSocket, "LEFT", None),
"CAM_C": getattr(dai.CameraBoardSocket, "RIGHT", None),
}
if legacy.get(name) is not None:
return legacy[name]
raise ValueError(f"Socket invalido: {name}. Use CAM_A, CAM_B ou CAM_C.")
def create_node(pipeline: dai.Pipeline, node_type):
"""Wrapper pequeno para manter o codigo legivel."""
return pipeline.create(node_type)
def mono_resolution_from_name(name: str):
name = str(name).strip().lower()
r = dai.MonoCameraProperties.SensorResolution
table = {
"400p": getattr(r, "THE_400_P", None),
"480p": getattr(r, "THE_480_P", None),
"720p": getattr(r, "THE_720_P", None),
"800p": getattr(r, "THE_800_P", None),
}
if name not in table or table[name] is None:
valid = ", ".join(k for k, v in table.items() if v is not None)
raise ValueError(f"Resolucao mono invalida: {name}. Valid={valid}")
return table[name]
def median_filter_from_name(name: str):
name = str(name).strip().upper()
enum_candidates = []
if hasattr(dai, "MedianFilter"):
enum_candidates.append(dai.MedianFilter)
if hasattr(dai, "StereoDepthProperties") and hasattr(dai.StereoDepthProperties, "MedianFilter"):
enum_candidates.append(dai.StereoDepthProperties.MedianFilter)
key_map = {
"OFF": ("MEDIAN_OFF", "KERNEL_NONE", "OFF"),
"3": ("KERNEL_3x3", "MEDIAN_3x3"),
"5": ("KERNEL_5x5", "MEDIAN_5x5"),
"7": ("KERNEL_7x7", "MEDIAN_7x7"),
}
if name not in key_map:
raise ValueError("--median deve ser OFF, 3, 5 ou 7")
for enum in enum_candidates:
for attr in key_map[name]:
value = getattr(enum, attr, None)
if value is not None:
return value
print("[WARN] Esta versao do DepthAI nao expos enum de MedianFilter; seguindo sem aplicar median filter.")
return None
def set_if_exists(obj, method_name: str, *args) -> bool:
fn = getattr(obj, method_name, None)
if callable(fn):
try:
fn(*args)
return True
except Exception as e:
print(f"[WARN] {method_name} falhou: {e}")
return False
def apply_stereo_config(stereo, args):
# Preset: tenta alguns nomes comuns.
try:
preset = getattr(dai.node.StereoDepth.PresetMode, "HIGH_DENSITY", None)
if preset is None:
preset = getattr(dai.node.StereoDepth.PresetMode, "FAST_DENSITY", None)
if preset is not None:
stereo.setDefaultProfilePreset(preset)
except Exception as e:
print(f"[WARN] preset StereoDepth nao aplicado: {e}")
set_if_exists(stereo, "setLeftRightCheck", bool(args.lrcheck))
set_if_exists(stereo, "setExtendedDisparity", bool(args.extended))
set_if_exists(stereo, "setSubpixel", bool(args.subpixel))
# Confidence threshold: mudou bastante entre versoes.
applied_conf = False
applied_conf = set_if_exists(stereo, "setConfidenceThreshold", int(args.confidence)) or applied_conf
if not applied_conf:
try:
applied_conf = set_if_exists(stereo.initialConfig, "setConfidenceThreshold", int(args.confidence)) or applied_conf
except Exception:
pass
# Algumas APIs v3 nao tem initialConfig.get(); tentamos manipular config direto se existir.
try:
cfg = stereo.initialConfig
if hasattr(cfg, "costMatching") and hasattr(cfg.costMatching, "confidenceThreshold"):
cfg.costMatching.confidenceThreshold = int(args.confidence)
applied_conf = True
except Exception:
pass
if not applied_conf:
print("[WARN] Nao consegui aplicar confidenceThreshold nesta versao. Seguindo com default.")
median_value = median_filter_from_name(args.median)
if median_value is not None:
applied_median = False
try:
applied_median = set_if_exists(stereo.initialConfig, "setMedianFilter", median_value)
except Exception:
pass
if not applied_median:
try:
cfg = stereo.initialConfig
if hasattr(cfg, "postProcessing") and hasattr(cfg.postProcessing, "median"):
cfg.postProcessing.median = median_value
applied_median = True
except Exception:
pass
if not applied_median:
print("[WARN] Nao consegui aplicar median filter nesta versao. Seguindo com default.")
# Pos-processamento opcional. Tudo defensivo.
try:
cfg = stereo.initialConfig
pp = getattr(cfg, "postProcessing", None)
if pp is not None:
if hasattr(pp, "speckleFilter"):
pp.speckleFilter.enable = bool(args.speckle)
pp.speckleFilter.speckleRange = int(args.speckle_range)
if hasattr(pp, "temporalFilter"):
pp.temporalFilter.enable = bool(args.temporal)
if hasattr(pp, "spatialFilter"):
pp.spatialFilter.enable = bool(args.spatial)
if hasattr(pp.spatialFilter, "holeFillingRadius"):
pp.spatialFilter.holeFillingRadius = int(args.hole_filling_radius)
if hasattr(pp.spatialFilter, "numIterations"):
pp.spatialFilter.numIterations = int(args.spatial_iterations)
except Exception as e:
print(f"[WARN] Nao consegui aplicar filtros de pos-processamento: {e}")
# ============================================================
# Visual helpers
# ============================================================
def normalize_u8(arr: np.ndarray, p_low: float = 1.0, p_high: float = 99.0) -> np.ndarray:
x = np.asarray(arr, dtype=np.float32)
finite = np.isfinite(x)
if not np.any(finite):
return np.zeros(x.shape[:2], dtype=np.uint8)
vals = x[finite]
lo = float(np.percentile(vals, p_low))
hi = float(np.percentile(vals, p_high))
if hi <= lo + 1e-6:
hi = lo + 1.0
y = np.clip((x - lo) / (hi - lo), 0.0, 1.0)
return (y * 255).astype(np.uint8)
def heatmap(arr: np.ndarray, p_low: float = 1.0, p_high: float = 99.0, cmap=cv2.COLORMAP_TURBO) -> np.ndarray:
return cv2.applyColorMap(normalize_u8(arr, p_low, p_high), cmap)
def put_label(img: np.ndarray, title: str, subtitle: str = "") -> np.ndarray:
if img is None:
img = np.zeros((300, 400, 3), dtype=np.uint8)
if img.ndim == 2:
img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
out = img.copy()
hbox = 58 if subtitle else 36
cv2.rectangle(out, (0, 0), (out.shape[1], hbox), (0, 0, 0), -1)
cv2.putText(out, str(title)[:90], (10, 24), cv2.FONT_HERSHEY_SIMPLEX, 0.65, (0, 255, 255), 2, cv2.LINE_AA)
if subtitle:
cv2.putText(out, str(subtitle)[:120], (10, 48), cv2.FONT_HERSHEY_SIMPLEX, 0.44, (255, 255, 255), 1, cv2.LINE_AA)
return out
def resize_keep(img: np.ndarray, width: int) -> np.ndarray:
scale = width / img.shape[1]
height = max(1, int(img.shape[0] * scale))
return cv2.resize(img, (width, height), interpolation=cv2.INTER_AREA)
def make_grid(panels, panel_w: int = 430, cols: int = 3) -> np.ndarray:
rendered = []
for title, img, subtitle in panels:
if img is None:
img = np.zeros((300, 400, 3), dtype=np.uint8)
if img.ndim == 2:
img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
small = resize_keep(img, panel_w)
rendered.append(put_label(small, title, subtitle))
if not rendered:
return np.zeros((300, 600, 3), dtype=np.uint8)
max_h = max(x.shape[0] for x in rendered)
padded = []
for im in rendered:
if im.shape[0] < max_h:
im = np.vstack([im, np.zeros((max_h - im.shape[0], im.shape[1], 3), dtype=np.uint8)])
padded.append(im)
gap = 10
gap_w = np.full((max_h, gap, 3), 22, dtype=np.uint8)
filler = np.zeros_like(padded[0])
rows = []
for i in range(0, len(padded), cols):
items = padded[i:i + cols]
while len(items) < cols:
items.append(filler.copy())
row = items[0]
for j in range(1, cols):
row = np.hstack([row, gap_w, items[j]])
rows.append(row)
gap_h = np.full((gap, rows[0].shape[1], 3), 22, dtype=np.uint8)
canvas = rows[0]
for row in rows[1:]:
canvas = np.vstack([canvas, gap_h, row])
return canvas
def safe_stats_depth_mm(depth: np.ndarray, min_mm: int, max_mm: int) -> Dict[str, float]:
d = np.asarray(depth, dtype=np.float32)
valid = np.isfinite(d) & (d > min_mm) & (d < max_mm)
total = int(d.size)
count = int(np.count_nonzero(valid))
if count <= 0:
return {
"valid_pct": 0.0,
"count": 0,
"mean_mm": 0.0,
"median_mm": 0.0,
"p10_mm": 0.0,
"p90_mm": 0.0,
"std_mm": 0.0,
}
vals = d[valid]
return {
"valid_pct": float(count * 100.0 / max(1, total)),
"count": count,
"mean_mm": float(np.mean(vals)),
"median_mm": float(np.median(vals)),
"p10_mm": float(np.percentile(vals, 10)),
"p90_mm": float(np.percentile(vals, 90)),
"std_mm": float(np.std(vals)),
}
def stats_disparity(disp: np.ndarray) -> Dict[str, float]:
d = np.asarray(disp, dtype=np.float32)
valid = np.isfinite(d) & (d > 0)
total = int(d.size)
count = int(np.count_nonzero(valid))
if count <= 0:
return {"valid_pct": 0.0, "mean": 0.0, "median": 0.0, "p90": 0.0, "std": 0.0}
vals = d[valid]
return {
"valid_pct": float(count * 100.0 / max(1, total)),
"mean": float(np.mean(vals)),
"median": float(np.median(vals)),
"p90": float(np.percentile(vals, 90)),
"std": float(np.std(vals)),
}
def draw_metrics_panel(metrics: Dict[str, float], disp_stats: Dict[str, float], fps: float, args: argparse.Namespace,
size: Tuple[int, int] = (900, 260)) -> np.ndarray:
w, h = size
img = np.zeros((h, w, 3), dtype=np.uint8)
lines = [
"OAK-FCC-3P depth probe - API v3 queues",
f"left={args.left} right={args.right} rgb={args.rgb} | fps={fps:.1f}",
f"lrcheck={args.lrcheck} extended={args.extended} subpixel={args.subpixel} median={args.median} confidence={args.confidence}",
f"depth valid={metrics['valid_pct']:.1f}% | median={metrics['median_mm']:.0f}mm mean={metrics['mean_mm']:.0f}mm p10={metrics['p10_mm']:.0f} p90={metrics['p90_mm']:.0f} std={metrics['std_mm']:.0f}",
f"disp valid={disp_stats['valid_pct']:.1f}% | median={disp_stats['median']:.2f} mean={disp_stats['mean']:.2f} p90={disp_stats['p90']:.2f} std={disp_stats['std']:.2f}",
"teclas: Q/ESC sair | S salvar snapshot | H ajuda",
"Leitura: heatmap coerente + valid% alto = vale investigar depth. Ruido/sopa = descartar depth metrico.",
]
y = 28
for i, line in enumerate(lines):
color = (0, 255, 255) if i == 0 else (235, 235, 235)
cv2.putText(img, line[:145], (12, y), cv2.FONT_HERSHEY_SIMPLEX, 0.55, color, 1, cv2.LINE_AA)
y += 26
return img
# ============================================================
# Queue helpers
# ============================================================
def create_output_queue(output, name: str, max_size: int = 4, blocking: bool = False):
if output is None:
return None
fn = getattr(output, "createOutputQueue", None)
if callable(fn):
return fn(maxSize=max_size, blocking=blocking)
raise RuntimeError(
f"A saida '{name}' nao possui createOutputQueue(). "
"Seu DepthAI parece nao ter XLinkOut, mas tambem nao expos queues v3 nessa saida."
)
def get_frame(q) -> Optional[np.ndarray]:
if q is None:
return None
try:
msg = q.tryGet()
except Exception:
return None
if msg is None:
return None
# ImgFrame normalmente tem getFrame(). Alguns previews coloridos podem ter getCvFrame().
try:
return msg.getFrame()
except Exception:
pass
try:
return msg.getCvFrame()
except Exception:
return None
# ============================================================
# Pipeline
# ============================================================
def create_pipeline_and_outputs(args: argparse.Namespace):
pipeline = dai.Pipeline()
left = create_node(pipeline, dai.node.MonoCamera)
right = create_node(pipeline, dai.node.MonoCamera)
left.setBoardSocket(socket_from_name(args.left))
right.setBoardSocket(socket_from_name(args.right))
left.setResolution(mono_resolution_from_name(args.mono_resolution))
right.setResolution(mono_resolution_from_name(args.mono_resolution))
left.setFps(float(args.fps))
right.setFps(float(args.fps))
stereo = create_node(pipeline, dai.node.StereoDepth)
apply_stereo_config(stereo, args)
left.out.link(stereo.left)
right.out.link(stereo.right)
outputs = {
"left": left.out,
"right": right.out,
"disparity": stereo.disparity,
"depth": stereo.depth,
"rectified_left": stereo.rectifiedLeft,
"rectified_right": stereo.rectifiedRight,
}
nodes = {
"left": left,
"right": right,
"stereo": stereo,
}
if args.enable_rgb:
rgb = create_node(pipeline, dai.node.ColorCamera)
rgb.setBoardSocket(socket_from_name(args.rgb))
rgb.setResolution(dai.ColorCameraProperties.SensorResolution.THE_800_P)
rgb.setFps(float(args.fps))
rgb.setInterleaved(False)
rgb.setColorOrder(dai.ColorCameraProperties.ColorOrder.BGR)
rgb.setPreviewSize(int(args.rgb_preview_w), int(args.rgb_preview_h))
outputs["rgb"] = rgb.preview
nodes["rgb"] = rgb
return pipeline, outputs, nodes
# ============================================================
# Runtime
# ============================================================
def save_snapshot(out_dir: Path, frames: Dict[str, np.ndarray], metrics: Dict[str, float], disp_stats: Dict[str, float], args: argparse.Namespace):
ts = time.strftime("%Y%m%d_%H%M%S")
folder = out_dir / f"depth_probe_{ts}"
folder.mkdir(parents=True, exist_ok=True)
for name, frame in frames.items():
if frame is None:
continue
if frame.ndim == 2:
if frame.dtype == np.uint16:
np.save(str(folder / f"{name}.npy"), frame)
cv2.imwrite(str(folder / f"{name}_preview.png"), normalize_u8(frame))
else:
cv2.imwrite(str(folder / f"{name}.png"), normalize_u8(frame))
else:
cv2.imwrite(str(folder / f"{name}.png"), frame)
meta = {
"created_at": ts,
"args": vars(args),
"depth_metrics": metrics,
"disparity_metrics": disp_stats,
}
with open(folder / "metrics.json", "w", encoding="utf-8") as f:
json.dump(meta, f, ensure_ascii=False, indent=2)
print(f"[OK] snapshot salvo em: {folder}")
def start_pipeline_v3(pipeline):
fn = getattr(pipeline, "start", None)
if not callable(fn):
raise RuntimeError(
"Este ambiente nao tem pipeline.start(). "
"Tambem nao tinha XLinkOut. Pode ser uma build DepthAI intermediaria/incompleta."
)
fn()
def stop_pipeline_v3(pipeline):
try:
fn = getattr(pipeline, "stop", None)
if callable(fn):
fn()
except Exception:
pass
def pipeline_running(pipeline) -> bool:
fn = getattr(pipeline, "isRunning", None)
if callable(fn):
try:
return bool(fn())
except Exception:
return True
return True
def main(args: argparse.Namespace):
out_dir = Path(args.out_dir)
out_dir.mkdir(parents=True, exist_ok=True)
pipeline, outputs, _nodes = create_pipeline_and_outputs(args)
print("[INFO] Pipeline criado em modo API v3/sem XLinkOut.")
print(f"[INFO] left={args.left} right={args.right} rgb={args.rgb} enable_rgb={args.enable_rgb}")
print("[INFO] Se depth vier ruim, teste invertendo --left/--right.")
queues = {
name: create_output_queue(output, name, max_size=4, blocking=False)
for name, output in outputs.items()
}
start_pipeline_v3(pipeline)
cv2.namedWindow("OAK-FCC-3P Depth Probe", cv2.WINDOW_NORMAL)
cv2.resizeWindow("OAK-FCC-3P Depth Probe", 1500, 900)
last_frames: Dict[str, Optional[np.ndarray]] = {
"left": None,
"right": None,
"rectified_left": None,
"rectified_right": None,
"disparity": None,
"depth": None,
"rgb": None,
"canvas": None,
}
frame_times = deque(maxlen=40)
last_metrics = safe_stats_depth_mm(np.zeros((1, 1), dtype=np.uint16), args.min_depth_mm, args.max_depth_mm)
last_disp_stats = stats_disparity(np.zeros((1, 1), dtype=np.float32))
try:
while pipeline_running(pipeline):
updated = False
for name, queue in queues.items():
frame = get_frame(queue)
if frame is not None:
last_frames[name] = frame
updated = True
if not updated:
key = cv2.waitKey(1) & 0xFF
if key in (27, ord("q"), ord("Q")):
break
continue
if last_frames["disparity"] is not None:
frame_times.append(time.time())
if len(frame_times) >= 2:
fps = (len(frame_times) - 1) / max(1e-6, frame_times[-1] - frame_times[0])
else:
fps = 0.0
left = last_frames["left"]
right = last_frames["right"]
rect_left = last_frames["rectified_left"]
rect_right = last_frames["rectified_right"]
disp = last_frames["disparity"]
depth = last_frames["depth"]
rgb = last_frames["rgb"]
if disp is None or depth is None or left is None or right is None:
continue
metrics = safe_stats_depth_mm(depth, args.min_depth_mm, args.max_depth_mm)
disp_s = stats_disparity(disp)
last_metrics = metrics
last_disp_stats = disp_s
depth_f = depth.astype(np.float32)
depth_valid = np.where(
(depth_f > args.min_depth_mm) & (depth_f < args.max_depth_mm),
depth_f,
np.nan,
)
disp_hm = heatmap(disp, 1, 99, cv2.COLORMAP_TURBO)
depth_hm = heatmap(depth_valid, 1, 99, cv2.COLORMAP_TURBO)
valid_mask = np.where(np.isfinite(depth_valid), 255, 0).astype(np.uint8)
valid_bgr = cv2.cvtColor(valid_mask, cv2.COLOR_GRAY2BGR)
base_for_overlay = rect_left if rect_left is not None else left
base_bgr = cv2.cvtColor(normalize_u8(base_for_overlay), cv2.COLOR_GRAY2BGR)
depth_hm_res = cv2.resize(depth_hm, (base_bgr.shape[1], base_bgr.shape[0]), interpolation=cv2.INTER_AREA)
overlay = cv2.addWeighted(base_bgr, 0.55, depth_hm_res, 0.45, 0)
panels = [
("Left mono", normalize_u8(left), f"{args.left}"),
("Right mono", normalize_u8(right), f"{args.right}"),
("Metrics", draw_metrics_panel(metrics, disp_s, fps, args), ""),
("Rectified left", normalize_u8(rect_left), "stereo.rectifiedLeft"),
("Rectified right", normalize_u8(rect_right), "stereo.rectifiedRight"),
("Disparity heatmap", disp_hm, f"valid={disp_s['valid_pct']:.1f}%"),
("Depth heatmap", depth_hm, f"valid={metrics['valid_pct']:.1f}% median={metrics['median_mm']:.0f}mm"),
("Valid depth mask", valid_bgr, f"range={args.min_depth_mm}-{args.max_depth_mm}mm"),
("Depth overlay", overlay, "heatmap sobre rectified left"),
]
if rgb is not None:
panels.append(("RGB preview", rgb, f"{args.rgb}"))
canvas = make_grid(panels, panel_w=args.panel_w, cols=3)
last_frames["canvas"] = canvas
cv2.imshow("OAK-FCC-3P Depth Probe", canvas)
key = cv2.waitKey(1) & 0xFF
if key in (27, ord("q"), ord("Q")):
break
if key in (ord("s"), ord("S")):
frames_to_save = {k: v for k, v in last_frames.items() if v is not None}
save_snapshot(out_dir, frames_to_save, last_metrics, last_disp_stats, args)
if key in (ord("h"), ord("H")):
print("\n=== HELP ===")
print("Q/ESC : sair")
print("S : salvar snapshot")
print("Teste tambem invertendo --left/--right se disparity/depth parecer quebrado.")
print("===========\n")
finally:
stop_pipeline_v3(pipeline)
cv2.destroyAllWindows()
# ============================================================
# CLI
# ============================================================
def build_argparser() -> argparse.ArgumentParser:
ap = argparse.ArgumentParser(description="Teste de depth/disparity na OAK-FFC-3P usando par mono RE/NIR, sem XLinkOut.")
ap.add_argument("--left", type=str, default="CAM_B", help="Socket mono esquerda. Ex: CAM_B ou CAM_C")
ap.add_argument("--right", type=str, default="CAM_C", help="Socket mono direita. Ex: CAM_C ou CAM_B")
ap.add_argument("--rgb", type=str, default="CAM_A", help="Socket RGB opcional.")
ap.add_argument("--enable-rgb", action="store_true", help="Tambem mostra preview RGB.")
ap.add_argument("--mono-resolution", type=str, default="800p", choices=["400p", "480p", "720p", "800p"])
ap.add_argument("--fps", type=float, default=10.0)
ap.add_argument("--rgb-preview-w", type=int, default=640)
ap.add_argument("--rgb-preview-h", type=int, default=400)
ap.add_argument("--lrcheck", action="store_true", help="Ativa left-right check para remover matches ruins/oclusoes.")
ap.add_argument("--extended", action="store_true", help="Ativa extended disparity, util para curto alcance.")
ap.add_argument("--subpixel", action="store_true", help="Ativa subpixel disparity, util para suavidade/maior precisao.")
ap.add_argument("--confidence", type=int, default=200, help="Confidence threshold do StereoDepth. Tente 180-245.")
ap.add_argument("--median", type=str, default="7", choices=["OFF", "3", "5", "7"], help="Filtro de mediana.")
ap.add_argument("--speckle", action="store_true", help="Ativa speckle filter no post-processing.")
ap.add_argument("--speckle-range", type=int, default=50)
ap.add_argument("--temporal", action="store_true", help="Ativa temporal filter, se suportado pela versao.")
ap.add_argument("--spatial", action="store_true", help="Ativa spatial filter, se suportado pela versao.")
ap.add_argument("--hole-filling-radius", type=int, default=2)
ap.add_argument("--spatial-iterations", type=int, default=1)
ap.add_argument("--min-depth-mm", type=int, default=150)
ap.add_argument("--max-depth-mm", type=int, default=5000)
ap.add_argument("--panel-w", type=int, default=430)
ap.add_argument("--out-dir", type=str, default="depth_probe_out")
return ap
if __name__ == "__main__":
main(build_argparser().parse_args())

View File

@ -0,0 +1,546 @@
import argparse
import json
import math
import time
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
import numpy as np
try:
import depthai as dai
except Exception as e:
raise RuntimeError(
"Nao consegui importar depthai. Ative o venv correto e instale depthai antes de rodar. "
f"Erro original: {e}"
)
# ============================================================
# OAK-FCC-3P Calibration Probe
# ------------------------------------------------------------
# Objetivo:
# Ler o que existe de calibracao no device OAK/DepthAI:
# - cameras conectadas
# - sockets / sensores
# - intrinsecos por camera, quando disponivel
# - distorcao por camera, quando disponivel
# - extrinsecos entre pares CAM_A/CAM_B/CAM_C
# - baseline estimado, quando a API permitir
# - dump JSON bruto da calibracao, quando disponivel
#
# Uso:
# python -m utils.calibration_probe --out_dir calibration_probe_out
#
# Para escolher device por MXID:
# python -m utils.calibration_probe --mx_id 194430108133AC2F00
# ============================================================
# ============================================================
# Helpers gerais
# ============================================================
def to_jsonable(x: Any):
if x is None:
return None
if isinstance(x, (str, int, float, bool)):
if isinstance(x, float) and (math.isnan(x) or math.isinf(x)):
return None
return x
if isinstance(x, np.ndarray):
return x.tolist()
if isinstance(x, (list, tuple)):
return [to_jsonable(v) for v in x]
if isinstance(x, dict):
return {str(k): to_jsonable(v) for k, v in x.items()}
try:
return str(x)
except Exception:
return repr(x)
def safe_call(label: str, fn, *args, default=None, verbose: bool = False):
try:
return fn(*args)
except Exception as e:
if verbose:
print(f"[WARN] {label} falhou: {type(e).__name__}: {e}")
return default
def get_device_id_from_info(dev_info) -> Optional[str]:
for name in ("getMxId", "getDeviceId"):
try:
fn = getattr(dev_info, name, None)
if callable(fn):
value = fn()
if value:
return str(value)
except Exception:
pass
for attr in ("mxid", "deviceId", "name"):
try:
value = getattr(dev_info, attr, None)
if value:
return str(value)
except Exception:
pass
return None
def resolve_device_info(mx_id: Optional[str] = None):
devices = dai.Device.getAllAvailableDevices()
if not devices:
raise RuntimeError("Nenhum dispositivo DepthAI/OAK encontrado.")
if not mx_id:
return devices[0]
target = str(mx_id).strip()
for dev_info in devices:
dev_id = get_device_id_from_info(dev_info)
if dev_id == target:
return dev_info
available = [get_device_id_from_info(d) or str(d) for d in devices]
raise RuntimeError(f"Device mx_id='{target}' nao encontrado. Disponiveis={available}")
def socket_from_name(name: str):
name = str(name).strip().upper()
aliases = {
"A": "CAM_A",
"B": "CAM_B",
"C": "CAM_C",
"D": "CAM_D",
"RGB": "CAM_A",
"LEFT": "CAM_B",
"RIGHT": "CAM_C",
}
name = aliases.get(name, name)
if hasattr(dai.CameraBoardSocket, name):
return getattr(dai.CameraBoardSocket, name)
legacy = {
"CAM_A": getattr(dai.CameraBoardSocket, "RGB", None),
"CAM_B": getattr(dai.CameraBoardSocket, "LEFT", None),
"CAM_C": getattr(dai.CameraBoardSocket, "RIGHT", None),
"CAM_D": getattr(dai.CameraBoardSocket, "CAM_D", None),
}
if legacy.get(name) is not None:
return legacy[name]
raise ValueError(f"Socket invalido: {name}")
def socket_name(socket_obj) -> str:
try:
return str(socket_obj.name)
except Exception:
return str(socket_obj)
def matrix_shape_ok(m, rows: int, cols: int) -> bool:
try:
arr = np.asarray(m, dtype=np.float64)
return arr.shape == (rows, cols) and np.all(np.isfinite(arr))
except Exception:
return False
def flatten_matrix(m):
try:
return np.asarray(m, dtype=np.float64).tolist()
except Exception:
return to_jsonable(m)
# ============================================================
# Calibration read helpers
# ============================================================
def read_calibration(device, verbose: bool = False):
# Contratos comuns: readCalibration(), readCalibration2().
for method in ("readCalibration", "readCalibration2"):
fn = getattr(device, method, None)
if callable(fn):
calib = safe_call(method, fn, default=None, verbose=verbose)
if calib is not None:
print(f"[OK] Calibracao lida via device.{method}()")
return calib, method
raise RuntimeError("Nao encontrei device.readCalibration/readCalibration2 nesta versao do DepthAI.")
def dump_calibration_json(calib, out_dir: Path, verbose: bool = False) -> Dict[str, Any]:
"""Tenta extrair dump bruto da calibracao por varios contratos de API."""
result = {
"available": False,
"method": None,
"path": None,
"data": None,
"error": None,
}
# 1) eepromToJson() costuma devolver dict/json.
for method in ("eepromToJson", "toJson"):
fn = getattr(calib, method, None)
if callable(fn):
try:
data = fn()
if isinstance(data, str):
try:
data_obj = json.loads(data)
except Exception:
data_obj = data
else:
data_obj = data
path = out_dir / f"calibration_{method}.json"
with open(path, "w", encoding="utf-8") as f:
json.dump(to_jsonable(data_obj), f, ensure_ascii=False, indent=2)
result.update({"available": True, "method": method, "path": str(path), "data": to_jsonable(data_obj)})
print(f"[OK] Dump bruto salvo via calib.{method}(): {path}")
return result
except Exception as e:
result["error"] = f"{method}: {type(e).__name__}: {e}"
if verbose:
print(f"[WARN] dump {method} falhou: {e}")
# 2) Alguns handlers escrevem direto em arquivo.
for method in ("saveToJsonFile", "saveCalibrationFile", "saveToFile"):
fn = getattr(calib, method, None)
if callable(fn):
path = out_dir / f"calibration_{method}.json"
try:
fn(str(path))
result.update({"available": True, "method": method, "path": str(path), "data": None})
print(f"[OK] Dump bruto salvo via calib.{method}(): {path}")
return result
except Exception as e:
result["error"] = f"{method}: {type(e).__name__}: {e}"
if verbose:
print(f"[WARN] dump {method} falhou: {e}")
print("[WARN] Nao consegui gerar dump JSON bruto da calibracao por API conhecida.")
return result
def get_connected_cameras(device) -> List[Dict[str, Any]]:
features = safe_call("getConnectedCameraFeatures", device.getConnectedCameraFeatures, default=[], verbose=False)
out = []
for f in features:
item = {}
try:
item["socket"] = socket_name(f.socket)
except Exception:
item["socket"] = None
for attr in ("sensorName", "width", "height", "orientation", "supportedTypes"):
try:
v = getattr(f, attr, None)
item[attr] = to_jsonable(v)
except Exception:
pass
out.append(item)
return out
def get_intrinsics(calib, socket, width: int, height: int, verbose: bool = False):
# Contratos comuns:
# getCameraIntrinsics(socket)
# getCameraIntrinsics(socket, width, height)
fn = getattr(calib, "getCameraIntrinsics", None)
if not callable(fn):
return None, "missing:getCameraIntrinsics"
for args in ((socket, width, height), (socket,)):
try:
value = fn(*args)
if value is not None:
return flatten_matrix(value), f"getCameraIntrinsics{len(args)}args"
except Exception as e:
if verbose:
print(f"[WARN] intrinsics {socket_name(socket)} args={len(args)} falhou: {e}")
return None, "failed:getCameraIntrinsics"
def get_distortion(calib, socket, verbose: bool = False):
for method in ("getDistortionCoefficients", "getDistortionCoeff"):
fn = getattr(calib, method, None)
if callable(fn):
try:
value = fn(socket)
return to_jsonable(value), method
except Exception as e:
if verbose:
print(f"[WARN] distortion {socket_name(socket)} {method} falhou: {e}")
return None, "missing:distortion"
def get_fov(calib, socket, verbose: bool = False):
fn = getattr(calib, "getFov", None)
if callable(fn):
try:
return float(fn(socket)), "getFov"
except Exception as e:
if verbose:
print(f"[WARN] fov {socket_name(socket)} falhou: {e}")
return None, "missing:getFov"
def get_extrinsics(calib, src_socket, dst_socket, verbose: bool = False):
fn = getattr(calib, "getCameraExtrinsics", None)
if not callable(fn):
return None, "missing:getCameraExtrinsics"
# Contratos comuns:
# getCameraExtrinsics(src, dst)
# getCameraExtrinsics(src, dst, useSpecTranslation)
for args in ((src_socket, dst_socket), (src_socket, dst_socket, False), (src_socket, dst_socket, True)):
try:
value = fn(*args)
if value is not None:
return flatten_matrix(value), f"getCameraExtrinsics{len(args)}args"
except Exception as e:
if verbose:
print(f"[WARN] extrinsics {socket_name(src_socket)}->{socket_name(dst_socket)} args={len(args)} falhou: {e}")
return None, "failed:getCameraExtrinsics"
def get_baseline(calib, src_socket, dst_socket, verbose: bool = False):
# Varia entre versoes; em algumas, getBaselineDistance(cam1, cam2, useSpecTranslation)
fn = getattr(calib, "getBaselineDistance", None)
if callable(fn):
for args in ((src_socket, dst_socket), (src_socket, dst_socket, False), (src_socket, dst_socket, True)):
try:
value = fn(*args)
if value is not None:
return float(value), f"getBaselineDistance{len(args)}args"
except Exception as e:
if verbose:
print(f"[WARN] baseline {socket_name(src_socket)}-{socket_name(dst_socket)} args={len(args)} falhou: {e}")
# Fallback: calcula norma da translacao da matriz 4x4, se existir.
ext, method = get_extrinsics(calib, src_socket, dst_socket, verbose=False)
if ext is not None:
try:
arr = np.asarray(ext, dtype=np.float64)
if arr.shape == (4, 4):
t = arr[:3, 3]
return float(np.linalg.norm(t)), f"norm_translation_from_{method}"
except Exception:
pass
return None, "missing:getBaselineDistance"
def inspect_socket(calib, socket_name_str: str, width: int, height: int, verbose: bool = False) -> Dict[str, Any]:
socket = socket_from_name(socket_name_str)
intr, intr_method = get_intrinsics(calib, socket, width, height, verbose=verbose)
dist, dist_method = get_distortion(calib, socket, verbose=verbose)
fov, fov_method = get_fov(calib, socket, verbose=verbose)
intr_ok = matrix_shape_ok(intr, 3, 3)
dist_ok = dist is not None
return {
"socket": socket_name_str,
"intrinsics": intr,
"intrinsics_method": intr_method,
"intrinsics_ok": bool(intr_ok),
"distortion": dist,
"distortion_method": dist_method,
"distortion_ok": bool(dist_ok),
"fov_deg": fov,
"fov_method": fov_method,
}
def inspect_pair(calib, src_name: str, dst_name: str, verbose: bool = False) -> Dict[str, Any]:
src = socket_from_name(src_name)
dst = socket_from_name(dst_name)
ext, ext_method = get_extrinsics(calib, src, dst, verbose=verbose)
base, base_method = get_baseline(calib, src, dst, verbose=verbose)
ext_ok = matrix_shape_ok(ext, 4, 4)
translation = None
if ext_ok:
try:
arr = np.asarray(ext, dtype=np.float64)
translation = arr[:3, 3].tolist()
except Exception:
translation = None
return {
"pair": f"{src_name}->{dst_name}",
"src": src_name,
"dst": dst_name,
"extrinsics": ext,
"extrinsics_method": ext_method,
"extrinsics_ok": bool(ext_ok),
"translation": translation,
"baseline": base,
"baseline_method": base_method,
"baseline_ok": bool(base is not None),
}
# ============================================================
# Report
# ============================================================
def print_summary(report: Dict[str, Any]):
print("\n================ CALIBRATION PROBE SUMMARY ================")
print(f"device_id : {report.get('device_id')}")
print(f"read_method : {report.get('calibration_read_method')}")
print(f"dump_json : {report.get('calibration_dump', {}).get('path')}")
print("\n[Cameras conectadas]")
for cam in report.get("connected_cameras", []):
print(f" - socket={cam.get('socket')} sensor={cam.get('sensorName')} size={cam.get('width')}x{cam.get('height')}")
print("\n[Intrinsecos por socket]")
for s in report.get("sockets", []):
print(
f" - {s['socket']}: intrinsics_ok={s['intrinsics_ok']} "
f"distortion_ok={s['distortion_ok']} fov={s.get('fov_deg')}"
)
print("\n[Extrinsecos entre pares]")
for p in report.get("pairs", []):
flag = "OK" if p.get("extrinsics_ok") else "MISSING"
print(
f" - {p['pair']}: {flag} | baseline={p.get('baseline')} "
f"| method={p.get('extrinsics_method')}"
)
# Diagnostico direto para o caso de depth RE/NIR.
bc = next((p for p in report.get("pairs", []) if p.get("pair") == "CAM_B->CAM_C"), None)
cb = next((p for p in report.get("pairs", []) if p.get("pair") == "CAM_C->CAM_B"), None)
print("\n[Diagnostico CAM_B/CAM_C para StereoDepth]")
if (bc and bc.get("extrinsics_ok")) or (cb and cb.get("extrinsics_ok")):
print(" ✅ Existe extrinseco entre CAM_B e CAM_C. O StereoDepth deve ter chance de iniciar.")
else:
print(" ❌ Nao existe extrinseco CAM_B<->CAM_C legivel pela API.")
print(" Isso explica erro: 'There is no available extrinsic calibration between camera ID: 1 and 2'.")
print(" Proximo passo: calibrar o par CAM_B/CAM_C ou carregar um calibration.json valido.")
print("===========================================================\n")
# ============================================================
# Main
# ============================================================
def main(args: argparse.Namespace):
out_dir = Path(args.out_dir)
out_dir.mkdir(parents=True, exist_ok=True)
dev_info = resolve_device_info(args.mx_id)
device_id = get_device_id_from_info(dev_info)
print(f"[INFO] Abrindo device: {device_id}")
with dai.Device(dev_info) as device:
connected = get_connected_cameras(device)
calib, read_method = read_calibration(device, verbose=args.verbose)
dump = dump_calibration_json(calib, out_dir=out_dir, verbose=args.verbose)
sockets = [s.strip().upper() for s in args.sockets.split(",") if s.strip()]
pairs = []
socket_reports = []
for s in sockets:
try:
socket_reports.append(inspect_socket(calib, s, args.width, args.height, verbose=args.verbose))
except Exception as e:
socket_reports.append({
"socket": s,
"error": f"{type(e).__name__}: {e}",
"intrinsics_ok": False,
"distortion_ok": False,
})
for src in sockets:
for dst in sockets:
if src == dst:
continue
try:
pairs.append(inspect_pair(calib, src, dst, verbose=args.verbose))
except Exception as e:
pairs.append({
"pair": f"{src}->{dst}",
"src": src,
"dst": dst,
"error": f"{type(e).__name__}: {e}",
"extrinsics_ok": False,
"baseline_ok": False,
})
report = {
"created_at": time.strftime("%Y-%m-%d %H:%M:%S"),
"device_id": device_id,
"calibration_read_method": read_method,
"connected_cameras": connected,
"sockets_requested": sockets,
"width": int(args.width),
"height": int(args.height),
"calibration_dump": {
"available": dump.get("available"),
"method": dump.get("method"),
"path": dump.get("path"),
"error": dump.get("error"),
},
"sockets": socket_reports,
"pairs": pairs,
}
out_path = out_dir / "calibration_probe_report.json"
with open(out_path, "w", encoding="utf-8") as f:
json.dump(to_jsonable(report), f, ensure_ascii=False, indent=2)
print(f"[OK] Relatorio salvo em: {out_path}")
print_summary(report)
# ============================================================
# CLI
# ============================================================
def build_argparser() -> argparse.ArgumentParser:
ap = argparse.ArgumentParser(description="Inspeciona calibracao EEPROM/JSON do device OAK/DepthAI.")
ap.add_argument("--mx_id", type=str, default=None, help="MXID opcional do device.")
ap.add_argument("--out_dir", type=str, default="calibration_probe_out", help="Pasta de saida.")
ap.add_argument("--sockets", type=str, default="CAM_A,CAM_B,CAM_C", help="Sockets para testar. Ex: CAM_A,CAM_B,CAM_C")
ap.add_argument("--width", type=int, default=1280, help="Largura usada ao pedir intrinsecos escalados.")
ap.add_argument("--height", type=int, default=800, help="Altura usada ao pedir intrinsecos escalados.")
ap.add_argument("--verbose", action="store_true", help="Mostra warnings detalhados de APIs que falharam.")
return ap
if __name__ == "__main__":
main(build_argparser().parse_args())

View File

@ -10,6 +10,10 @@ import numpy as np
from core.oak_fcc3_client import OakFcc3Client as MultiSpectralClient
# ============================================================
# Utilidades gerais
# ============================================================
def now_str() -> str:
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
@ -100,7 +104,7 @@ def build_overlay_fuse(
if spec01 is None:
return base_bgr
if calibration_mode == "homography":
if calibration_mode in ("homography", "charuco_auto"):
warped = apply_homography(spec01, H)
else:
warped = apply_affine(spec01, dx, dy, theta_deg)
@ -166,9 +170,13 @@ def validate_module_ready(status, frame_type, raw_policy):
raise RuntimeError(f"frame_type desconhecido para validação: {frame_type}")
# ============================================================
# JSON de calibração
# ============================================================
def default_offsets_payload(args, effective_capture_mode):
return {
"schema": "manual_multispec_offsets_v2",
"schema": "manual_multispec_offsets_v3",
"saved_at": now_str(),
"frame_type": "RAW_BRUTO",
"capture_mode_requested": args.capture_mode,
@ -188,6 +196,8 @@ def default_offsets_payload(args, effective_capture_mode):
"re_to_rgb": None,
"nir_to_rgb": None,
},
"homography_metrics": {},
"charuco": {},
"notes": args.notes or "",
}
@ -199,12 +209,14 @@ def load_offsets_json(path, args, effective_capture_mode):
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
data.setdefault("schema", "manual_multispec_offsets_v2")
data.setdefault("schema", "manual_multispec_offsets_v3")
data.setdefault("reference_camera", "rgb")
data.setdefault("baseline_mm", args.baseline_mm)
data.setdefault("alignment_mode", "manual_affine")
data.setdefault("manual_offsets", {})
data.setdefault("homographies", {})
data.setdefault("homography_metrics", {})
data.setdefault("charuco", {})
data["manual_offsets"].setdefault("re", {"dx": 0, "dy": 0, "theta_deg": 0.0})
data["manual_offsets"].setdefault("nir", {"dx": 0, "dy": 0, "theta_deg": 0.0})
@ -224,9 +236,216 @@ def save_offsets_json(path, data):
json.dump(data, f, ensure_ascii=False, indent=2)
# ============================================================
# ChArUco automático para homografia planar
# ============================================================
def require_aruco():
if not hasattr(cv2, "aruco"):
raise RuntimeError("cv2.aruco não disponível. Instale opencv-contrib-python no ambiente.")
def get_aruco_dictionary(dict_name: str):
require_aruco()
if not hasattr(cv2.aruco, dict_name):
available = sorted([x for x in dir(cv2.aruco) if x.startswith("DICT_")])
raise ValueError(f"Dicionário ArUco inválido: {dict_name}. Disponíveis: {available}")
dict_id = getattr(cv2.aruco, dict_name)
if hasattr(cv2.aruco, "getPredefinedDictionary"):
return cv2.aruco.getPredefinedDictionary(dict_id)
return cv2.aruco.Dictionary_get(dict_id)
def create_charuco_board(squares_x, squares_y, square_length, marker_length, dictionary):
require_aruco()
# OpenCV novo: cv2.aruco.CharucoBoard((x, y), squareLength, markerLength, dictionary)
if hasattr(cv2.aruco, "CharucoBoard"):
try:
return cv2.aruco.CharucoBoard((squares_x, squares_y), square_length, marker_length, dictionary)
except TypeError:
pass
# OpenCV legado: cv2.aruco.CharucoBoard_create(x, y, squareLength, markerLength, dictionary)
if hasattr(cv2.aruco, "CharucoBoard_create"):
return cv2.aruco.CharucoBoard_create(squares_x, squares_y, square_length, marker_length, dictionary)
raise RuntimeError("API ChArUco não encontrada no cv2.aruco deste ambiente.")
def create_detector_params():
require_aruco()
if hasattr(cv2.aruco, "DetectorParameters"):
return cv2.aruco.DetectorParameters()
return cv2.aruco.DetectorParameters_create()
def to_gray_u8_for_charuco(img01, equalize=True, invert=False):
if img01 is None:
return None
img = np.asarray(img01)
if img.ndim == 3:
if img.shape[2] == 3:
gray = cv2.cvtColor(np.clip(img * 255.0, 0, 255).astype(np.uint8), cv2.COLOR_RGB2GRAY)
else:
gray = img[..., 0]
else:
gray = img
if gray.dtype != np.uint8:
gray = np.asarray(gray, dtype=np.float32)
finite = np.isfinite(gray)
if not finite.any():
return np.zeros(gray.shape[:2], dtype=np.uint8)
lo = float(np.percentile(gray[finite], 1.0))
hi = float(np.percentile(gray[finite], 99.5))
if hi <= lo + 1e-9:
hi = lo + 1.0
gray = np.clip((gray - lo) / (hi - lo) * 255.0, 0, 255).astype(np.uint8)
if invert:
gray = 255 - gray
if equalize:
try:
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
gray = clahe.apply(gray)
except Exception:
gray = cv2.equalizeHist(gray)
return gray
def detect_charuco_points(
img01,
board,
dictionary,
detector_params,
equalize=True,
invert=False,
min_markers=4,
):
"""
Retorna dict: charuco_id -> (x, y), além de resumo de detecção.
Compatível com APIs nova/legada do OpenCV.
"""
gray = to_gray_u8_for_charuco(img01, equalize=equalize, invert=invert)
if gray is None:
return {}, {"markers": 0, "corners": 0, "ok": False}
# API nova pode ter ArucoDetector, mas detectMarkers continua existindo em quase todos.
corners, ids, rejected = cv2.aruco.detectMarkers(gray, dictionary, parameters=detector_params)
n_markers = 0 if ids is None else int(len(ids))
if ids is None or n_markers < min_markers:
return {}, {"markers": n_markers, "corners": 0, "ok": False}
try:
cv2.aruco.refineDetectedMarkers(gray, board, corners, ids, rejected)
except Exception:
pass
ret, charuco_corners, charuco_ids = cv2.aruco.interpolateCornersCharuco(
markerCorners=corners,
markerIds=ids,
image=gray,
board=board,
)
if charuco_corners is None or charuco_ids is None:
return {}, {"markers": n_markers, "corners": 0, "ok": False}
point_by_id = {}
ids_flat = charuco_ids.reshape(-1)
pts = charuco_corners.reshape(-1, 2)
for cid, pt in zip(ids_flat, pts):
point_by_id[int(cid)] = (float(pt[0]), float(pt[1]))
return point_by_id, {
"markers": n_markers,
"corners": int(len(point_by_id)),
"ok": len(point_by_id) >= 4,
}
def append_charuco_pairs(accum, role, rgb_points, spec_points, sample_id):
common_ids = sorted(set(rgb_points.keys()) & set(spec_points.keys()))
added = 0
for cid in common_ids:
spec_pt = spec_points[cid]
rgb_pt = rgb_points[cid]
accum[role]["spec"].append(spec_pt)
accum[role]["rgb"].append(rgb_pt)
accum[role]["ids"].append(int(cid))
accum[role]["sample_ids"].append(int(sample_id))
added += 1
return added, common_ids
def compute_homography_from_points(src_pts, dst_pts, ransac_reproj_threshold=3.0):
if len(src_pts) < 4 or len(dst_pts) < 4 or len(src_pts) != len(dst_pts):
return None, None, None
src = np.array(src_pts, dtype=np.float32)
dst = np.array(dst_pts, dtype=np.float32)
H, status = cv2.findHomography(src, dst, method=cv2.RANSAC, ransacReprojThreshold=float(ransac_reproj_threshold))
if H is None:
return None, status, None
projected = cv2.perspectiveTransform(src.reshape(-1, 1, 2), H).reshape(-1, 2)
err = np.linalg.norm(projected - dst, axis=1)
if status is not None:
inlier_mask = status.reshape(-1).astype(bool)
else:
inlier_mask = np.ones(len(err), dtype=bool)
if inlier_mask.any():
err_in = err[inlier_mask]
else:
err_in = err
metrics = {
"points": int(len(src_pts)),
"inliers": int(inlier_mask.sum()),
"outliers": int(len(src_pts) - inlier_mask.sum()),
"inlier_ratio": float(inlier_mask.sum() / max(len(src_pts), 1)),
"mean_error_px": float(np.mean(err_in)) if len(err_in) else None,
"median_error_px": float(np.median(err_in)) if len(err_in) else None,
"max_error_px": float(np.max(err_in)) if len(err_in) else None,
"ransac_reproj_threshold_px": float(ransac_reproj_threshold),
}
return H, status, metrics
def draw_charuco_points_on_panel(board_img, rect, point_by_id, color, max_labels=80):
if rect is None or not point_by_id:
return
x0, y0, _, _ = rect
for idx, (cid, pt) in enumerate(point_by_id.items()):
px = int(x0 + pt[0])
py = int(y0 + pt[1])
cv2.circle(board_img, (px, py), 4, color, -1)
if idx < max_labels:
cv2.putText(board_img, str(cid), (px + 5, py - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.38, color, 1, cv2.LINE_AA)
# ============================================================
# Main
# ============================================================
def main():
parser = argparse.ArgumentParser(
description="Calibrador manual de offsets para fusão RGB/RE/NIR a partir do stream RAW_BRUTO.",
description="Calibrador de offsets/homografia para fusão RGB/RE/NIR a partir do stream RAW_BRUTO.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
@ -244,6 +463,24 @@ def main():
parser.add_argument("--out_json", default="calibration/manual_offsets.json")
parser.add_argument("--load_json", default="")
parser.add_argument("--notes", default="")
parser.add_argument("--module_calibration_json", default="calibration/module_params.json")
# ChArUco automático
parser.add_argument("--charuco_auto", action="store_true", help="Inicia direto no modo de homografia automática por ChArUco.")
parser.add_argument("--charuco_dictionary", default="DICT_5X5_100")
parser.add_argument("--charuco_squares_x", type=int, default=7)
parser.add_argument("--charuco_squares_y", type=int, default=5)
parser.add_argument("--charuco_square_length", type=float, default=1.0)
parser.add_argument("--charuco_marker_length", type=float, default=0.70)
parser.add_argument("--charuco_min_markers", type=int, default=4)
parser.add_argument("--charuco_min_common_corners", type=int, default=8)
parser.add_argument("--charuco_ransac_px", type=float, default=3.0)
parser.add_argument("--charuco_equalize", action="store_true", default=True)
parser.add_argument("--charuco_no_equalize", action="store_false", dest="charuco_equalize")
parser.add_argument("--charuco_invert_rgb", action="store_true")
parser.add_argument("--charuco_invert_re", action="store_true")
parser.add_argument("--charuco_invert_nir", action="store_true")
parser.add_argument("--charuco_autocalc", action="store_true", help="Calcula H automaticamente após cada captura ChArUco válida.")
args = parser.parse_args()
@ -253,11 +490,38 @@ def main():
offsets = offsets_data["manual_offsets"]
selected_role = "re"
calibration_mode = offsets_data.get("alignment_mode", "manual_affine")
calibration_mode = "charuco_auto" if args.charuco_auto else offsets_data.get("alignment_mode", "manual_affine")
selected_points_spec = {"re": [], "nir": []}
selected_points_rgb = {"re": [], "nir": []}
charuco_accum = {
"re": {"spec": [], "rgb": [], "ids": [], "sample_ids": []},
"nir": {"spec": [], "rgb": [], "ids": [], "sample_ids": []},
}
charuco_last = {
"rgb": {},
"re": {},
"nir": {},
"summary": {},
}
charuco_sample_id = 0
dictionary = None
board_charuco = None
detector_params = None
if args.charuco_auto:
dictionary = get_aruco_dictionary(args.charuco_dictionary)
board_charuco = create_charuco_board(
args.charuco_squares_x,
args.charuco_squares_y,
args.charuco_square_length,
args.charuco_marker_length,
dictionary,
)
detector_params = create_detector_params()
panel_rects = {
"fuse": None,
"rgb": None,
@ -278,7 +542,7 @@ def main():
stream_frames_accum = 0
last_stream_frame_id = None
window_name = "Manual Fusion Calibrator"
window_name = "Fusion Calibrator - Manual/Homography/ChArUco"
def inside(rect, px, py):
if rect is None:
@ -290,6 +554,21 @@ def main():
x0, y0, _, _ = rect
return float(px - x0), float(py - y0)
def ensure_charuco_runtime():
nonlocal dictionary, board_charuco, detector_params
if dictionary is None:
dictionary = get_aruco_dictionary(args.charuco_dictionary)
if board_charuco is None:
board_charuco = create_charuco_board(
args.charuco_squares_x,
args.charuco_squares_y,
args.charuco_square_length,
args.charuco_marker_length,
dictionary,
)
if detector_params is None:
detector_params = create_detector_params()
def on_mouse(event, x, y, flags, param):
nonlocal last_msg, last_msg_t, selected_role, calibration_mode
@ -329,6 +608,108 @@ def main():
return None
def calculate_and_store_h_for_role(role):
pts = charuco_accum[role]
if len(pts["spec"]) < 4 or len(pts["rgb"]) < 4:
return False, f"{role.upper()}: pontos acumulados insuficientes"
H, status, metrics = compute_homography_from_points(
pts["spec"],
pts["rgb"],
ransac_reproj_threshold=args.charuco_ransac_px,
)
if H is None:
return False, f"{role.upper()}: falha ao calcular H"
offsets_data.setdefault("homographies", {})
offsets_data.setdefault("homography_metrics", {})
offsets_data["homographies"][f"{role}_to_rgb"] = H.tolist()
offsets_data["homography_metrics"][f"{role}_to_rgb"] = metrics
return True, (
f"{role.upper()}: H OK | pts={metrics['points']} | "
f"inliers={metrics['inliers']} | err_med={metrics['mean_error_px']:.2f}px"
)
def calculate_charuco_homographies(all_roles=False):
roles = ("re", "nir") if all_roles else (selected_role,)
messages = []
ok_any = False
for role in roles:
ok, msg = calculate_and_store_h_for_role(role)
ok_any = ok_any or ok
messages.append(msg)
return ok_any, " | ".join(messages)
def grab_charuco_sample(rgb01, re01, nir01):
nonlocal charuco_sample_id
ensure_charuco_runtime()
rgb_pts, rgb_sum = detect_charuco_points(
rgb01,
board_charuco,
dictionary,
detector_params,
equalize=args.charuco_equalize,
invert=args.charuco_invert_rgb,
min_markers=args.charuco_min_markers,
)
re_pts, re_sum = detect_charuco_points(
re01,
board_charuco,
dictionary,
detector_params,
equalize=args.charuco_equalize,
invert=args.charuco_invert_re,
min_markers=args.charuco_min_markers,
) if re01 is not None else ({}, {"markers": 0, "corners": 0, "ok": False})
nir_pts, nir_sum = detect_charuco_points(
nir01,
board_charuco,
dictionary,
detector_params,
equalize=args.charuco_equalize,
invert=args.charuco_invert_nir,
min_markers=args.charuco_min_markers,
) if nir01 is not None else ({}, {"markers": 0, "corners": 0, "ok": False})
charuco_last["rgb"] = rgb_pts
charuco_last["re"] = re_pts
charuco_last["nir"] = nir_pts
charuco_last["summary"] = {"rgb": rgb_sum, "re": re_sum, "nir": nir_sum}
if not rgb_pts:
return False, "ChArUco: RGB não detectou cantos válidos"
charuco_sample_id += 1
messages = []
added_total = 0
for role, spec_pts in (("re", re_pts), ("nir", nir_pts)):
if not spec_pts:
messages.append(f"{role.upper()}: sem detecção")
continue
common = sorted(set(rgb_pts.keys()) & set(spec_pts.keys()))
if len(common) < args.charuco_min_common_corners:
messages.append(f"{role.upper()}: comum={len(common)} < {args.charuco_min_common_corners}")
continue
added, _ = append_charuco_pairs(charuco_accum, role, rgb_pts, spec_pts, charuco_sample_id)
added_total += added
messages.append(f"{role.upper()}: +{added} pares")
if args.charuco_autocalc and added_total > 0:
_, calc_msg = calculate_charuco_homographies(all_roles=True)
messages.append(calc_msg)
return added_total > 0, "ChArUco sample #{:03d}: {}".format(charuco_sample_id, " | ".join(messages))
cv2.namedWindow(window_name, cv2.WINDOW_NORMAL)
cv2.setMouseCallback(window_name, on_mouse)
@ -342,7 +723,7 @@ def main():
output_dtype="uint8",
capture_mode=effective_capture_mode,
raw_policy=args.raw_policy,
module_calibration_json="calibration/module_params.json"
module_calibration_json=args.module_calibration_json,
) as cam:
validate_module_ready(cam.get_status(), "RAW_BRUTO", args.raw_policy)
@ -437,12 +818,19 @@ def main():
spec_pts = len(selected_points_spec[selected_role])
rgb_pts = len(selected_points_rgb[selected_role])
ch_re_pts = len(charuco_accum["re"]["spec"])
ch_nir_pts = len(charuco_accum["nir"]["spec"])
ch_sum = charuco_last.get("summary", {}) or {}
ch_rgb = ch_sum.get("rgb", {}).get("corners", 0)
ch_re = ch_sum.get("re", {}).get("corners", 0)
ch_nir = ch_sum.get("nir", {}).get("corners", 0)
lines_fuse = [
f"FUSE: RGB + {active_spec_name}",
f"mode={calibration_mode} | selecionada={selected_role.upper()}",
f"dx={dx} | dy={dy} | theta={theta_deg:.2f}g | step={args.step} | ang_step={args.angle_step:.2f}g",
f"pts_spec={spec_pts} | pts_rgb={rgb_pts} | min=4 | fps_stream={fps_stream:.1f} | fps_view={fps_view:.1f}",
f"manual pts_spec={spec_pts} pts_rgb={rgb_pts} | charuco RE={ch_re_pts} NIR={ch_nir_pts}",
f"last corners RGB={ch_rgb} RE={ch_re} NIR={ch_nir} | fps_stream={fps_stream:.1f} fps_view={fps_view:.1f}",
]
overlay_hud(fuse_panel, lines_fuse)
@ -490,18 +878,18 @@ def main():
board = np.vstack([top, bottom])
help_lines = [
"M=manual_affine | H=homography | clique pares | >=4 pares | SPACE=salva | C=limpa pts | Z=zera sel | X=zera tudo",
"A/W/S/D movem | J/L rotacionam | O/P ang_step | I/U remove ponto | ENTER calcula H | TAB alterna RE/NIR | Q/Esc sai",
"M=manual | H=homog clique | K=charuco | G=captura charuco | ENTER=calcula H | SPACE=salva | C=limpa pts | V=limpa charuco",
"2/3 seleciona | TAB alterna | A/W/S/D movem | J/L rotacionam | I/U remove clique | Z=zera sel | X=zera tudo | Q/Esc sai",
]
overlay_hud(board, help_lines, x=16, y=board.shape[0] - 44, font_scale=0.55, line_step=20)
if last_msg and (time.time() - last_msg_t) < 2.5:
if last_msg and (time.time() - last_msg_t) < 3.5:
cv2.putText(
board,
last_msg,
(16, board.shape[0] - 72),
cv2.FONT_HERSHEY_SIMPLEX,
0.7,
0.62,
(0, 255, 0),
2,
cv2.LINE_AA,
@ -529,6 +917,11 @@ def main():
cv2.circle(board, (px, py), 5, color_rgb, -1)
cv2.putText(board, str(idx + 1), (px + 6, py - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color_rgb, 1, cv2.LINE_AA)
if calibration_mode == "charuco_auto":
draw_charuco_points_on_panel(board, panel_rects["rgb"], charuco_last.get("rgb", {}), (0, 255, 0))
draw_charuco_points_on_panel(board, panel_rects["re"], charuco_last.get("re", {}), (0, 255, 255))
draw_charuco_points_on_panel(board, panel_rects["nir"], charuco_last.get("nir", {}), (255, 255, 0))
if args.preview_scale != 1.0:
board = cv2.resize(
board,
@ -557,34 +950,72 @@ def main():
elif k in (ord("h"), ord("H")):
calibration_mode = "homography"
offsets_data["alignment_mode"] = calibration_mode
last_msg = "Modo: homography"
last_msg = "Modo: homography manual por cliques"
last_msg_t = time.time()
elif k in (ord("k"), ord("K")):
ensure_charuco_runtime()
calibration_mode = "charuco_auto"
offsets_data["alignment_mode"] = calibration_mode
last_msg = "Modo: charuco_auto"
last_msg_t = time.time()
elif k in (ord("g"), ord("G")):
if decoded_last:
rgb_id, rgb01 = get_image_by_role(decoded_last, "rgb")
_, re01 = get_image_by_role(decoded_last, "re")
_, nir01 = get_image_by_role(decoded_last, "nir")
if rgb01 is not None:
base_h, base_w = rgb01.shape[:2]
re01 = resize_if_needed(re01, (base_h, base_w))
nir01 = resize_if_needed(nir01, (base_h, base_w))
ok, msg = grab_charuco_sample(rgb01, re01, nir01)
last_msg = msg
else:
last_msg = "ChArUco: RGB indisponível"
else:
last_msg = "ChArUco: sem decoded_last"
last_msg_t = time.time()
elif k in (ord("c"), ord("C")):
selected_points_spec[selected_role] = []
selected_points_rgb[selected_role] = []
last_msg = f"Pontos limpos: {selected_role.upper()}"
last_msg = f"Pontos manuais limpos: {selected_role.upper()}"
last_msg_t = time.time()
elif k in (ord("v"), ord("V")):
charuco_accum[selected_role] = {"spec": [], "rgb": [], "ids": [], "sample_ids": []}
last_msg = f"Pontos ChArUco limpos: {selected_role.upper()}"
last_msg_t = time.time()
elif k == 13:
spec_pts = selected_points_spec[selected_role]
rgb_pts = selected_points_rgb[selected_role]
if len(spec_pts) >= 4 and len(rgb_pts) >= 4 and len(spec_pts) == len(rgb_pts):
src = np.array(spec_pts, dtype=np.float32)
dst = np.array(rgb_pts, dtype=np.float32)
H, status = cv2.findHomography(src, dst, method=cv2.RANSAC)
if H is not None:
offsets_data.setdefault("homographies", {})
offsets_data["homographies"][f"{selected_role}_to_rgb"] = H.tolist()
inliers = int(status.sum()) if status is not None else len(spec_pts)
last_msg = f"H calculada para {selected_role.upper()} | pts={len(spec_pts)} | inliers={inliers}"
else:
last_msg = f"Falha ao calcular H para {selected_role.upper()}"
if calibration_mode == "charuco_auto":
ok, msg = calculate_charuco_homographies(all_roles=False)
last_msg = msg
else:
last_msg = f"{selected_role.upper()}: precisa de >=4 pares e mesmo numero de pontos"
spec_pts = selected_points_spec[selected_role]
rgb_pts = selected_points_rgb[selected_role]
if len(spec_pts) >= 4 and len(rgb_pts) >= 4 and len(spec_pts) == len(rgb_pts):
H, status, metrics = compute_homography_from_points(
spec_pts,
rgb_pts,
ransac_reproj_threshold=args.charuco_ransac_px,
)
if H is not None:
offsets_data.setdefault("homographies", {})
offsets_data.setdefault("homography_metrics", {})
offsets_data["homographies"][f"{selected_role}_to_rgb"] = H.tolist()
offsets_data["homography_metrics"][f"{selected_role}_to_rgb"] = metrics
last_msg = (
f"H manual calculada para {selected_role.upper()} | "
f"pts={metrics['points']} | inliers={metrics['inliers']} | "
f"err_med={metrics['mean_error_px']:.2f}px"
)
else:
last_msg = f"Falha ao calcular H para {selected_role.upper()}"
else:
last_msg = f"{selected_role.upper()}: precisa de >=4 pares e mesmo numero de pontos"
last_msg_t = time.time()
elif k == ord("2"):
@ -633,12 +1064,29 @@ def main():
offsets_data["manual_offsets"] = offsets
offsets_data.setdefault("homographies", {})
offsets_data["schema"] = "manual_multispec_offsets_v2"
offsets_data.setdefault("homography_metrics", {})
offsets_data["schema"] = "manual_multispec_offsets_v3"
offsets_data["reference_camera"] = "rgb"
offsets_data["alignment_mode"] = calibration_mode
offsets_data["homography_calibration_size"] = [int(base_w), int(base_h)]
offsets_data["charuco"] = {
"enabled": calibration_mode == "charuco_auto",
"dictionary": args.charuco_dictionary,
"squares_x": int(args.charuco_squares_x),
"squares_y": int(args.charuco_squares_y),
"square_length": float(args.charuco_square_length),
"marker_length": float(args.charuco_marker_length),
"samples": int(charuco_sample_id),
"accumulated_pairs": {
"re": int(len(charuco_accum["re"]["spec"])),
"nir": int(len(charuco_accum["nir"]["spec"])),
},
"same_physical_plane_required": True,
"notes": "Homografias ChArUco estimadas para um único plano físico. Mova o tabuleiro no plano do solo; não misture inclinações/alturas para uma H única.",
}
save_offsets_json(args.out_json, offsets_data)
last_msg = f"Offsets salvos em: {args.out_json}"
last_msg = f"Calibração salva em: {args.out_json}"
last_msg_t = time.time()
elif k in (ord("+"), ord("=")):
@ -709,8 +1157,8 @@ def main():
finally:
cv2.destroyAllWindows()
print("Fim da calibração manual.")
print("Fim da calibração de fusão.")
if __name__ == "__main__":
main()
main()