691 lines
25 KiB
Python
691 lines
25 KiB
Python
|
|
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())
|