criacao de testes para modo stereo

This commit is contained in:
Diego Freitas 2026-05-27 16:34:48 -03:00
parent c304401ef0
commit b4397d0954
11 changed files with 5964 additions and 0 deletions

3
.gitignore vendored
View File

@ -69,6 +69,9 @@ Python/OAK/datasets/oak-fcc-3/backup/
Python/OAK/datasets/oak-fcc-3/dataset/ Python/OAK/datasets/oak-fcc-3/dataset/
Python/OAK/datasets/oak-fcc-3/audit_multispec_out/ Python/OAK/datasets/oak-fcc-3/audit_multispec_out/
Python/OAK/datasets/oak-fcc-3/depth_probe_out/ Python/OAK/datasets/oak-fcc-3/depth_probe_out/
Python/OAK/datasets/oak-fcc-3/calibration/multicam_charuco_calib_out/debug/
Python/OAK/datasets/oak-fcc-3/calibration/stereo_charuco_calib_out/debug/
Python/OAK/datasets/oak-fcc-3/calibration/stereo_dataset/
Python/OAK/datasets/oak-fcc-3/.cache/ Python/OAK/datasets/oak-fcc-3/.cache/
Python/OAK/datasets/gal5000/dataset/ Python/OAK/datasets/gal5000/dataset/
Python/OAK/datasets/gal5000/backup/ Python/OAK/datasets/gal5000/backup/

View File

@ -0,0 +1,236 @@
import argparse
from pathlib import Path
import cv2
import numpy as np
import torch
from PIL import Image
from transformers import pipeline
IMG_EXTS = {".png", ".jpg", ".jpeg", ".bmp", ".webp"}
def robust_normalize(depth: np.ndarray, invert: bool = False) -> np.ndarray:
d = depth.astype(np.float32)
p2 = np.percentile(d, 2)
p98 = np.percentile(d, 98)
dn = (d - p2) / (p98 - p2 + 1e-6)
dn = np.clip(dn, 0.0, 1.0)
if invert:
dn = 1.0 - dn
return dn
def depth_to_colormap(depth_norm: np.ndarray) -> np.ndarray:
u8 = (depth_norm * 255).astype(np.uint8)
return cv2.applyColorMap(u8, cv2.COLORMAP_TURBO)
def make_bands(depth_norm: np.ndarray) -> np.ndarray:
bands = np.zeros_like(depth_norm, dtype=np.uint8)
bands[depth_norm >= 0.33] = 1
bands[depth_norm >= 0.66] = 2
out = np.zeros((bands.shape[0], bands.shape[1], 3), dtype=np.uint8)
# BGR
out[bands == 0] = (80, 80, 255) # faixa 0
out[bands == 1] = (80, 255, 255) # faixa 1
out[bands == 2] = (80, 255, 80) # faixa 2
return out
def run_depth(pipe, image_path: Path, invert: bool):
img_pil = Image.open(image_path).convert("RGB")
rgb = np.array(img_pil)
preview_bgr = cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR)
result = pipe(img_pil)
if "predicted_depth" in result:
depth = result["predicted_depth"]
if hasattr(depth, "detach"):
depth = depth.detach().cpu().numpy()
depth = np.array(depth).squeeze().astype(np.float32)
else:
depth_img = result["depth"]
depth = np.array(depth_img).astype(np.float32)
if depth.ndim == 3:
depth = cv2.cvtColor(
depth.astype(np.uint8),
cv2.COLOR_RGB2GRAY
).astype(np.float32)
depth = cv2.resize(
depth,
(preview_bgr.shape[1], preview_bgr.shape[0]),
interpolation=cv2.INTER_CUBIC
)
depth_norm = robust_normalize(depth, invert=invert)
depth_color = depth_to_colormap(depth_norm)
depth_bands = make_bands(depth_norm)
return preview_bgr, depth_norm, depth_color, depth_bands
def resize_to_height(img: np.ndarray, target_h: int) -> np.ndarray:
h, w = img.shape[:2]
if h == target_h:
return img
scale = target_h / h
new_w = int(w * scale)
return cv2.resize(img, (new_w, target_h), interpolation=cv2.INTER_AREA)
def compose_view(preview_bgr, depth_color, depth_bands, image_path, index, total, mode):
target_h = 520
left = resize_to_height(preview_bgr, target_h)
if mode == "depth":
right_img = depth_color
right_title = "Depth Anything V2"
else:
right_img = depth_bands
right_title = "Depth bands"
right = resize_to_height(right_img, target_h)
# Garante mesma altura
h = min(left.shape[0], right.shape[0])
left = left[:h]
right = right[:h]
canvas = np.hstack([left, right])
text1 = f"{index + 1}/{total} - {image_path.name}"
text2 = f"Modo: {right_title} | N/SPACE prox | A ant | M modo | I invert | S salvar | Q sair"
cv2.rectangle(canvas, (0, 0), (canvas.shape[1], 58), (0, 0, 0), -1)
cv2.putText(canvas, text1, (12, 22), cv2.FONT_HERSHEY_SIMPLEX, 0.58, (255, 255, 255), 1, cv2.LINE_AA)
cv2.putText(canvas, text2, (12, 48), cv2.FONT_HERSHEY_SIMPLEX, 0.50, (220, 220, 220), 1, cv2.LINE_AA)
return canvas
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--input_dir", required=True, help="Pasta com imagens preview PNG/JPG")
parser.add_argument("--model", default="depth-anything/Depth-Anything-V2-Small-hf")
parser.add_argument("--device", default="auto", choices=["auto", "cuda", "cpu"])
parser.add_argument("--invert", action="store_true")
parser.add_argument("--save_dir", default="depth_viewer_saves")
args = parser.parse_args()
input_dir = Path(args.input_dir)
save_dir = Path(args.save_dir)
save_dir.mkdir(parents=True, exist_ok=True)
image_paths = sorted([
p for p in input_dir.rglob("*")
if p.suffix.lower() in IMG_EXTS
])
if not image_paths:
raise RuntimeError(f"Nenhuma imagem encontrada em: {input_dir}")
if args.device == "auto":
device = 0 if torch.cuda.is_available() else -1
elif args.device == "cuda":
device = 0
else:
device = -1
print(f"[INFO] imagens: {len(image_paths)}")
print(f"[INFO] modelo: {args.model}")
print(f"[INFO] device: {'cuda' if device == 0 else 'cpu'}")
print("[INFO] controles:")
print(" N ou SPACE = próxima")
print(" A = anterior")
print(" M = alterna depth/faixas")
print(" I = inverte depth")
print(" S = salva visual atual")
print(" Q ou ESC = sair")
pipe = pipeline(
task="depth-estimation",
model=args.model,
device=device
)
idx = 0
invert = args.invert
mode = "depth"
cached_path = None
cached_data = None
cv2.namedWindow("Depth Anything V2 Viewer", cv2.WINDOW_NORMAL)
while True:
image_path = image_paths[idx]
need_reprocess = cached_path != image_path or cached_data is None
if need_reprocess:
print(f"[RUN] {idx + 1}/{len(image_paths)} - {image_path.name}")
preview_bgr, depth_norm, depth_color, depth_bands = run_depth(pipe, image_path, invert=invert)
cached_data = (preview_bgr, depth_norm, depth_color, depth_bands)
cached_path = image_path
else:
preview_bgr, depth_norm, depth_color, depth_bands = cached_data
view = compose_view(
preview_bgr=preview_bgr,
depth_color=depth_color,
depth_bands=depth_bands,
image_path=image_path,
index=idx,
total=len(image_paths),
mode=mode
)
cv2.imshow("Depth Anything V2 Viewer", view)
key = cv2.waitKey(0) & 0xFF
if key in [27, ord("q"), ord("Q")]:
break
elif key in [ord("n"), ord("N"), 32]:
idx = min(idx + 1, len(image_paths) - 1)
cached_path = None
elif key in [ord("a"), ord("A")]:
idx = max(idx - 1, 0)
cached_path = None
elif key in [ord("m"), ord("M")]:
mode = "bands" if mode == "depth" else "depth"
elif key in [ord("i"), ord("I")]:
invert = not invert
cached_path = None
print(f"[INFO] invert={invert}")
elif key in [ord("s"), ord("S")]:
out_path = save_dir / f"{image_path.stem}_viewer_{mode}.png"
cv2.imwrite(str(out_path), view)
print(f"[SAVE] {out_path}")
cv2.destroyAllWindows()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,771 @@
import argparse
import json
import re
from pathlib import Path
import cv2
import numpy as np
# ============================================================
# RAW10
# ============================================================
def unpack_raw10_packed(raw: bytes, width: int, height: int) -> np.ndarray:
"""
RAW10 packed padrão:
5 bytes = 4 pixels de 10 bits.
Retorna uint16 HxW com valores 0..1023.
"""
arr = np.frombuffer(raw, dtype=np.uint8)
pixel_count = width * height
expected_bytes = (pixel_count // 4) * 5
if pixel_count % 4 != 0:
raise RuntimeError(f"width*height precisa ser múltiplo de 4. Recebido: {pixel_count}")
if arr.size < expected_bytes:
raise RuntimeError(
f"RAW10 menor que esperado. bytes={arr.size}, esperado={expected_bytes}, "
f"width={width}, height={height}"
)
arr = arr[:expected_bytes]
groups = arr.reshape(-1, 5).astype(np.uint16)
p0 = (groups[:, 0] << 2) | ((groups[:, 4] >> 0) & 0x03)
p1 = (groups[:, 1] << 2) | ((groups[:, 4] >> 2) & 0x03)
p2 = (groups[:, 2] << 2) | ((groups[:, 4] >> 4) & 0x03)
p3 = (groups[:, 3] << 2) | ((groups[:, 4] >> 6) & 0x03)
out = np.empty(groups.shape[0] * 4, dtype=np.uint16)
out[0::4] = p0
out[1::4] = p1
out[2::4] = p2
out[3::4] = p3
return out.reshape(height, width)
def normalize_to_u8(img: np.ndarray, p_low=1.0, p_high=99.0) -> np.ndarray:
arr = img.astype(np.float32)
valid = np.isfinite(arr)
if np.count_nonzero(valid) < 20:
return np.zeros(arr.shape[:2], dtype=np.uint8)
vals = arr[valid]
lo = np.percentile(vals, p_low)
hi = np.percentile(vals, p_high)
out = (arr - lo) / (hi - lo + 1e-6)
out = np.clip(out, 0.0, 1.0)
return (out * 255).astype(np.uint8)
def raw10_bin_to_gray(path: Path, width: int, height: int, use_clahe=True) -> np.ndarray:
raw = path.read_bytes()
mono10 = unpack_raw10_packed(raw, width, height)
gray = normalize_to_u8(mono10)
if use_clahe:
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
gray = clahe.apply(gray)
return gray
# ============================================================
# Metadata
# ============================================================
def find_meta(root_dir: Path) -> Path | None:
candidates = list(root_dir.rglob("meta.json")) + list(root_dir.rglob("metadata.json"))
return candidates[0] if candidates else None
def extract_camera_info_from_meta(meta: dict, cam_key: str):
for root_key in ["camera_info", "cameras", "camera_meta", "payload_sources_info"]:
root = meta.get(root_key)
if isinstance(root, dict):
info = root.get(cam_key)
if isinstance(info, dict):
return info
stack = [meta]
while stack:
obj = stack.pop()
if isinstance(obj, dict):
if cam_key in obj and isinstance(obj[cam_key], dict):
return obj[cam_key]
for v in obj.values():
if isinstance(v, (dict, list)):
stack.append(v)
elif isinstance(obj, list):
for v in obj:
if isinstance(v, (dict, list)):
stack.append(v)
return None
def try_get_width_height_from_meta(root_dir: Path, cam_key: str):
meta_path = find_meta(root_dir)
if meta_path is None:
return None, None
try:
meta = json.loads(meta_path.read_text(encoding="utf-8"))
except Exception:
meta = json.loads(meta_path.read_text(encoding="latin-1"))
info = extract_camera_info_from_meta(meta, cam_key)
if not info:
return None, None
width = (
info.get("width")
or info.get("w")
or info.get("sensor_width")
or info.get("frame_width")
)
height = (
info.get("height")
or info.get("h")
or info.get("sensor_height")
or info.get("frame_height")
)
if width is None or height is None:
return None, None
return int(width), int(height)
def resolve_width_height(args, cam_key: str):
if args.width > 0 and args.height > 0:
return args.width, args.height
w, h = try_get_width_height_from_meta(Path(args.root_dir), cam_key)
if w and h:
return w, h
raise RuntimeError(
f"Não consegui descobrir width/height para {cam_key}. "
f"Passe manualmente: --width 1280 --height 800"
)
# ============================================================
# Pairing
# ============================================================
def clean_stem_for_pair(path: Path, cam_key: str):
stem = path.stem
s = stem
variants = [
cam_key,
cam_key.lower(),
cam_key.replace("_", ""),
cam_key.replace("_", "").lower(),
]
for v in variants:
s = s.replace(v, "")
s = re.sub(r"[_\-\s]+", "_", s).strip("_").lower()
return s
def find_cam_bins(root_dir: Path, cam_key: str):
return sorted([
p for p in root_dir.rglob("*.bin")
if cam_key.lower() in p.name.lower()
])
def pair_cam_bins(root_dir: Path, left_cam: str, right_cam: str):
left_bins = find_cam_bins(root_dir, left_cam)
right_bins = find_cam_bins(root_dir, right_cam)
right_by_folder_key = {}
right_by_key = {}
for rp in right_bins:
key = clean_stem_for_pair(rp, right_cam)
right_by_folder_key[(rp.parent, key)] = rp
right_by_key.setdefault(key, rp)
pairs = []
for lp in left_bins:
key = clean_stem_for_pair(lp, left_cam)
rp = right_by_folder_key.get((lp.parent, key))
if rp is None:
rp = right_by_key.get(key)
if rp is None:
same_folder = [r for r in right_bins if r.parent == lp.parent]
if len(same_folder) == 1:
rp = same_folder[0]
if rp is not None:
pairs.append((lp, rp))
if pairs:
return pairs, left_bins, right_bins, "name/key"
n = min(len(left_bins), len(right_bins))
return list(zip(left_bins[:n], right_bins[:n])), left_bins, right_bins, "order"
# ============================================================
# ChArUco helpers
# ============================================================
def get_aruco_dict(dict_name: str):
aruco = cv2.aruco
mapping = {
"4X4_50": aruco.DICT_4X4_50,
"4X4_100": aruco.DICT_4X4_100,
"4X4_250": aruco.DICT_4X4_250,
"4X4_1000": aruco.DICT_4X4_1000,
"5X5_50": aruco.DICT_5X5_50,
"5X5_100": aruco.DICT_5X5_100,
"5X5_250": aruco.DICT_5X5_250,
"5X5_1000": aruco.DICT_5X5_1000,
"6X6_50": aruco.DICT_6X6_50,
"6X6_100": aruco.DICT_6X6_100,
"6X6_250": aruco.DICT_6X6_250,
"6X6_1000": aruco.DICT_6X6_1000,
}
key = dict_name.upper()
if key not in mapping:
raise RuntimeError(f"Dicionário ArUco não suportado: {dict_name}")
if hasattr(aruco, "getPredefinedDictionary"):
return aruco.getPredefinedDictionary(mapping[key])
return aruco.Dictionary_get(mapping[key])
def create_charuco_board(squares_x, squares_y, square_length, marker_length, aruco_dict):
aruco = cv2.aruco
if hasattr(aruco, "CharucoBoard"):
try:
return aruco.CharucoBoard(
(squares_x, squares_y),
square_length,
marker_length,
aruco_dict
)
except Exception:
pass
if hasattr(aruco, "CharucoBoard_create"):
return aruco.CharucoBoard_create(
squares_x,
squares_y,
square_length,
marker_length,
aruco_dict
)
raise RuntimeError("Sua versão do OpenCV não tem CharucoBoard/CharucoBoard_create.")
def get_board_corners(board):
if hasattr(board, "getChessboardCorners"):
return np.array(board.getChessboardCorners(), dtype=np.float32)
if hasattr(board, "chessboardCorners"):
return np.array(board.chessboardCorners, dtype=np.float32)
raise RuntimeError("Não consegui acessar chessboardCorners do ChArUco board.")
def create_detector_params():
aruco = cv2.aruco
if hasattr(aruco, "DetectorParameters"):
return aruco.DetectorParameters()
if hasattr(aruco, "DetectorParameters_create"):
return aruco.DetectorParameters_create()
return None
def detect_charuco(gray: np.ndarray, board, aruco_dict, min_corners: int):
"""
Retorna:
charuco_corners: Nx2 float32
charuco_ids: N int32
marker_corners, marker_ids
"""
aruco = cv2.aruco
params = create_detector_params()
# OpenCV novo
if hasattr(aruco, "CharucoDetector"):
try:
detector = aruco.CharucoDetector(board)
charuco_corners, charuco_ids, marker_corners, marker_ids = detector.detectBoard(gray)
if charuco_corners is None or charuco_ids is None:
return None, None, marker_corners, marker_ids
corners = np.array(charuco_corners, dtype=np.float32).reshape(-1, 2)
ids = np.array(charuco_ids, dtype=np.int32).reshape(-1)
if len(ids) < min_corners:
return None, None, marker_corners, marker_ids
return corners, ids, marker_corners, marker_ids
except Exception:
pass
# OpenCV legado
if params is not None:
marker_corners, marker_ids, rejected = aruco.detectMarkers(
gray,
aruco_dict,
parameters=params
)
else:
marker_corners, marker_ids, rejected = aruco.detectMarkers(gray, aruco_dict)
if marker_ids is None or len(marker_ids) == 0:
return None, None, marker_corners, marker_ids
try:
aruco.refineDetectedMarkers(gray, board, marker_corners, marker_ids, rejected)
except Exception:
pass
retval, charuco_corners, charuco_ids = aruco.interpolateCornersCharuco(
marker_corners,
marker_ids,
gray,
board
)
if charuco_corners is None or charuco_ids is None:
return None, None, marker_corners, marker_ids
corners = np.array(charuco_corners, dtype=np.float32).reshape(-1, 2)
ids = np.array(charuco_ids, dtype=np.int32).reshape(-1)
if len(ids) < min_corners:
return None, None, marker_corners, marker_ids
return corners, ids, marker_corners, marker_ids
def common_charuco_points(corners_l, ids_l, corners_r, ids_r, board_corners, min_common):
map_l = {int(i): corners_l[k] for k, i in enumerate(ids_l)}
map_r = {int(i): corners_r[k] for k, i in enumerate(ids_r)}
common_ids = sorted(set(map_l.keys()) & set(map_r.keys()))
if len(common_ids) < min_common:
return None, None, None, common_ids
obj = []
img_l = []
img_r = []
max_id = len(board_corners) - 1
for cid in common_ids:
if cid < 0 or cid > max_id:
continue
obj.append(board_corners[cid])
img_l.append(map_l[cid])
img_r.append(map_r[cid])
if len(obj) < min_common:
return None, None, None, common_ids
obj = np.array(obj, dtype=np.float32).reshape(-1, 1, 3)
img_l = np.array(img_l, dtype=np.float32).reshape(-1, 1, 2)
img_r = np.array(img_r, dtype=np.float32).reshape(-1, 1, 2)
return obj, img_l, img_r, common_ids
def draw_debug(gray, charuco_corners, charuco_ids, title):
bgr = cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR)
if charuco_corners is not None and charuco_ids is not None:
corners_draw = np.array(charuco_corners, dtype=np.float32).reshape(-1, 1, 2)
ids_draw = np.array(charuco_ids, dtype=np.int32).reshape(-1, 1)
try:
cv2.aruco.drawDetectedCornersCharuco(bgr, corners_draw, ids_draw, (0, 255, 0))
except Exception:
for p in charuco_corners:
cv2.circle(bgr, tuple(np.round(p).astype(int)), 3, (0, 255, 0), -1)
cv2.putText(
bgr,
title,
(20, 35),
cv2.FONT_HERSHEY_SIMPLEX,
0.85,
(255, 255, 255),
2,
cv2.LINE_AA
)
return bgr
# ============================================================
# Calibration
# ============================================================
def calibrate_single_camera(objpoints, imgpoints, image_size):
flags = 0
ret, K, D, rvecs, tvecs = cv2.calibrateCamera(
objectPoints=objpoints,
imagePoints=imgpoints,
imageSize=image_size,
cameraMatrix=None,
distCoeffs=None,
flags=flags
)
return ret, K, D, rvecs, tvecs
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--root_dir", required=True)
parser.add_argument("--out_dir", default="calibration/stereo_charuco_calib_out")
parser.add_argument("--left_cam", default="CAM_C")
parser.add_argument("--right_cam", default="CAM_B")
parser.add_argument("--width", type=int, default=-1)
parser.add_argument("--height", type=int, default=-1)
# ChArUco 7x13
parser.add_argument("--squares_x", type=int, default=7)
parser.add_argument("--squares_y", type=int, default=13)
# Use a unidade que quiser. Recomendo metros.
# Ex: quadrado de 20 mm => 0.020
parser.add_argument("--square_length", type=float, required=True)
parser.add_argument("--marker_length", type=float, required=True)
parser.add_argument("--aruco_dict", default="4X4_50")
parser.add_argument("--min_corners", type=int, default=12)
parser.add_argument("--min_common", type=int, default=10)
parser.add_argument("--no_clahe", action="store_true")
parser.add_argument("--show", action="store_true")
args = parser.parse_args()
root_dir = Path(args.root_dir)
out_dir = Path(args.out_dir)
debug_dir = out_dir / "debug"
out_dir.mkdir(parents=True, exist_ok=True)
debug_dir.mkdir(parents=True, exist_ok=True)
left_cam = args.left_cam
right_cam = args.right_cam
w_left, h_left = resolve_width_height(args, left_cam)
w_right, h_right = resolve_width_height(args, right_cam)
if (w_left, h_left) != (w_right, h_right):
print(f"[WARN] Resoluções diferentes: {left_cam}=({w_left},{h_left}) {right_cam}=({w_right},{h_right})")
print("[WARN] Vou calibrar usando o menor tamanho comum após resize.")
image_w = min(w_left, w_right)
image_h = min(h_left, h_right)
image_size = (image_w, image_h)
print(f"[INFO] root_dir={root_dir}")
print(f"[INFO] left={left_cam} {w_left}x{h_left}")
print(f"[INFO] right={right_cam} {w_right}x{h_right}")
print(f"[INFO] image_size usado={image_size}")
print(f"[INFO] ChArUco squares={args.squares_x}x{args.squares_y}")
print(f"[INFO] square_length={args.square_length}")
print(f"[INFO] marker_length={args.marker_length}")
print(f"[INFO] aruco_dict={args.aruco_dict}")
pairs, left_bins, right_bins, pair_mode = pair_cam_bins(root_dir, left_cam, right_cam)
print(f"[INFO] arquivos {left_cam}: {len(left_bins)}")
print(f"[INFO] arquivos {right_cam}: {len(right_bins)}")
print(f"[INFO] pares encontrados: {len(pairs)}")
print(f"[INFO] pareamento: {pair_mode}")
if len(pairs) < 5:
raise RuntimeError("Poucos pares encontrados. Capture mais imagens ou verifique nomes dos arquivos.")
aruco_dict = get_aruco_dict(args.aruco_dict)
board = create_charuco_board(
args.squares_x,
args.squares_y,
args.square_length,
args.marker_length,
aruco_dict
)
board_corners = get_board_corners(board)
stereo_objpoints = []
stereo_imgpoints_l = []
stereo_imgpoints_r = []
left_objpoints = []
left_imgpoints = []
right_objpoints = []
right_imgpoints = []
accepted = 0
rejected = 0
for idx, (lp, rp) in enumerate(pairs):
print(f"[{idx+1}/{len(pairs)}] L={lp.name} | R={rp.name}")
try:
gray_l = raw10_bin_to_gray(lp, w_left, h_left, use_clahe=not args.no_clahe)
gray_r = raw10_bin_to_gray(rp, w_right, h_right, use_clahe=not args.no_clahe)
if gray_l.shape[::-1] != image_size:
gray_l = cv2.resize(gray_l, image_size, interpolation=cv2.INTER_AREA)
if gray_r.shape[::-1] != image_size:
gray_r = cv2.resize(gray_r, image_size, interpolation=cv2.INTER_AREA)
corners_l, ids_l, _, _ = detect_charuco(
gray_l,
board,
aruco_dict,
min_corners=args.min_corners
)
corners_r, ids_r, _, _ = detect_charuco(
gray_r,
board,
aruco_dict,
min_corners=args.min_corners
)
n_l = 0 if ids_l is None else len(ids_l)
n_r = 0 if ids_r is None else len(ids_r)
if corners_l is None or corners_r is None:
print(f" [REJECT] detect insuficiente: left={n_l}, right={n_r}")
rejected += 1
continue
obj, img_l, img_r, common_ids = common_charuco_points(
corners_l,
ids_l,
corners_r,
ids_r,
board_corners,
min_common=args.min_common
)
if obj is None:
print(f" [REJECT] comuns insuficientes: common={len(common_ids)}")
rejected += 1
continue
stereo_objpoints.append(obj)
stereo_imgpoints_l.append(img_l)
stereo_imgpoints_r.append(img_r)
left_objpoints.append(obj.copy())
left_imgpoints.append(img_l.copy())
right_objpoints.append(obj.copy())
right_imgpoints.append(img_r.copy())
accepted += 1
dbg_l = draw_debug(gray_l, corners_l, ids_l, f"{left_cam} corners={n_l}")
dbg_r = draw_debug(gray_r, corners_r, ids_r, f"{right_cam} corners={n_r}")
dbg = np.hstack([dbg_l, dbg_r])
cv2.putText(
dbg,
f"COMMON={len(common_ids)} ACCEPTED",
(20, dbg.shape[0] - 25),
cv2.FONT_HERSHEY_SIMPLEX,
0.9,
(0, 255, 0),
2,
cv2.LINE_AA
)
cv2.imwrite(str(debug_dir / f"pair_{idx:04d}_accepted.png"), dbg)
if args.show:
cv2.imshow("debug", dbg)
key = cv2.waitKey(1) & 0xFF
if key in [27, ord("q"), ord("Q")]:
break
print(f" [OK] left={n_l}, right={n_r}, common={len(common_ids)}")
except Exception as e:
print(f" [ERRO] {e}")
rejected += 1
if args.show:
cv2.destroyAllWindows()
print("")
print(f"[INFO] aceitos: {accepted}")
print(f"[INFO] rejeitados: {rejected}")
if accepted < 8:
raise RuntimeError(
f"Poucos pares aceitos: {accepted}. Ideal: pelo menos 15-25 bons, melhor 30+."
)
print("[CALIB] Calibrando câmera esquerda...")
ret_l, K_l, D_l, rvecs_l, tvecs_l = calibrate_single_camera(
left_objpoints,
left_imgpoints,
image_size
)
print("[CALIB] Calibrando câmera direita...")
ret_r, K_r, D_r, rvecs_r, tvecs_r = calibrate_single_camera(
right_objpoints,
right_imgpoints,
image_size
)
print(f"[RESULT] RMS left : {ret_l:.6f}")
print(f"[RESULT] RMS right: {ret_r:.6f}")
print("[CALIB] Calibração estéreo...")
stereo_flags = cv2.CALIB_FIX_INTRINSIC
ret_stereo, K_l2, D_l2, K_r2, D_r2, R, T, E, F = cv2.stereoCalibrate(
objectPoints=stereo_objpoints,
imagePoints1=stereo_imgpoints_l,
imagePoints2=stereo_imgpoints_r,
cameraMatrix1=K_l,
distCoeffs1=D_l,
cameraMatrix2=K_r,
distCoeffs2=D_r,
imageSize=image_size,
flags=stereo_flags,
criteria=(cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 200, 1e-7)
)
print(f"[RESULT] RMS stereo: {ret_stereo:.6f}")
print(f"[RESULT] T: {T.ravel()}")
print("[CALIB] stereoRectify...")
R1, R2, P1, P2, Q, roi1, roi2 = cv2.stereoRectify(
cameraMatrix1=K_l2,
distCoeffs1=D_l2,
cameraMatrix2=K_r2,
distCoeffs2=D_r2,
imageSize=image_size,
R=R,
T=T,
flags=cv2.CALIB_ZERO_DISPARITY,
alpha=0
)
map1x, map1y = cv2.initUndistortRectifyMap(
K_l2,
D_l2,
R1,
P1,
image_size,
cv2.CV_32FC1
)
map2x, map2y = cv2.initUndistortRectifyMap(
K_r2,
D_r2,
R2,
P2,
image_size,
cv2.CV_32FC1
)
out_path = out_dir / f"stereo_calib_{left_cam}_{right_cam}.npz"
np.savez_compressed(
out_path,
left_cam=left_cam,
right_cam=right_cam,
image_size=np.array(image_size, dtype=np.int32),
squares_x=args.squares_x,
squares_y=args.squares_y,
square_length=args.square_length,
marker_length=args.marker_length,
aruco_dict=args.aruco_dict,
rms_left=ret_l,
rms_right=ret_r,
rms_stereo=ret_stereo,
K_left=K_l2,
D_left=D_l2,
K_right=K_r2,
D_right=D_r2,
R=R,
T=T,
E=E,
F=F,
R1=R1,
R2=R2,
P1=P1,
P2=P2,
Q=Q,
roi1=np.array(roi1),
roi2=np.array(roi2),
map1x=map1x,
map1y=map1y,
map2x=map2x,
map2y=map2y,
accepted=accepted,
rejected=rejected
)
print("")
print(f"[OK] calibração salva em: {out_path}")
print(f"[OK] debug salvo em: {debug_dir}")
print("")
print("Resumo:")
print(f" RMS left = {ret_l:.6f}")
print(f" RMS right = {ret_r:.6f}")
print(f" RMS stereo = {ret_stereo:.6f}")
print(f" T = {T.ravel()}")
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,705 @@
import argparse
import re
from pathlib import Path
import cv2
import numpy as np
# ============================================================
# RAW10 / image conversion
# ============================================================
def unpack_raw10_packed(raw: bytes, width: int, height: int) -> np.ndarray:
arr = np.frombuffer(raw, dtype=np.uint8)
pixel_count = width * height
expected_bytes = (pixel_count // 4) * 5
if pixel_count % 4 != 0:
raise RuntimeError(f"width*height precisa ser múltiplo de 4. Recebido: {pixel_count}")
if arr.size < expected_bytes:
raise RuntimeError(
f"RAW10 menor que esperado. bytes={arr.size}, esperado={expected_bytes}, "
f"width={width}, height={height}"
)
arr = arr[:expected_bytes]
groups = arr.reshape(-1, 5).astype(np.uint16)
p0 = (groups[:, 0] << 2) | ((groups[:, 4] >> 0) & 0x03)
p1 = (groups[:, 1] << 2) | ((groups[:, 4] >> 2) & 0x03)
p2 = (groups[:, 2] << 2) | ((groups[:, 4] >> 4) & 0x03)
p3 = (groups[:, 3] << 2) | ((groups[:, 4] >> 6) & 0x03)
out = np.empty(groups.shape[0] * 4, dtype=np.uint16)
out[0::4] = p0
out[1::4] = p1
out[2::4] = p2
out[3::4] = p3
return out.reshape(height, width)
def normalize_to_u8(img: np.ndarray, p_low=1.0, p_high=99.0) -> np.ndarray:
arr = img.astype(np.float32)
valid = np.isfinite(arr)
if np.count_nonzero(valid) < 20:
return np.zeros(arr.shape[:2], dtype=np.uint8)
vals = arr[valid]
lo = np.percentile(vals, p_low)
hi = np.percentile(vals, p_high)
out = (arr - lo) / (hi - lo + 1e-6)
out = np.clip(out, 0.0, 1.0)
return (out * 255).astype(np.uint8)
def debayer_raw10_to_bgr_u8(raw10: np.ndarray, bayer: str) -> np.ndarray:
gray_u8 = normalize_to_u8(raw10)
bayer = bayer.upper()
code_map = {
"RGGB": cv2.COLOR_BayerRG2BGR,
"BGGR": cv2.COLOR_BayerBG2BGR,
"GRBG": cv2.COLOR_BayerGR2BGR,
"GBRG": cv2.COLOR_BayerGB2BGR,
}
if bayer not in code_map:
raise RuntimeError(f"Bayer pattern não suportado: {bayer}")
return cv2.cvtColor(gray_u8, code_map[bayer])
def read_rgb(path: Path, width: int, height: int, bayer: str, rgb_view: str, use_clahe: bool):
raw = path.read_bytes()
raw10 = unpack_raw10_packed(raw, width, height)
mode = rgb_view.lower().strip()
if mode == "color":
bgr = debayer_raw10_to_bgr_u8(raw10, bayer)
gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY)
return bgr, gray
if mode == "gray":
bgr_color = debayer_raw10_to_bgr_u8(raw10, bayer)
gray = cv2.cvtColor(bgr_color, cv2.COLOR_BGR2GRAY)
elif mode == "raw_bayer_gray":
gray = normalize_to_u8(raw10)
else:
raise RuntimeError(f"rgb_view inválido: {rgb_view}")
if use_clahe:
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
gray = clahe.apply(gray)
bgr = cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR)
return bgr, gray
def read_mono(path: Path, width: int, height: int, use_clahe: bool):
raw = path.read_bytes()
raw10 = unpack_raw10_packed(raw, width, height)
gray = normalize_to_u8(raw10)
if use_clahe:
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
gray = clahe.apply(gray)
bgr = cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR)
return bgr, gray
def to_gray_u8(img_bgr: np.ndarray):
if img_bgr.ndim == 2:
return img_bgr
return cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)
# ============================================================
# Triplet pairing
# ============================================================
def clean_stem_for_pair(path: Path, cam_key: str):
s = path.stem
variants = [
cam_key,
cam_key.lower(),
cam_key.replace("_", ""),
cam_key.replace("_", "").lower(),
]
for v in variants:
s = s.replace(v, "")
s = re.sub(r"[_\-\s]+", "_", s).strip("_").lower()
return s
def find_cam_bins(root_dir: Path, cam_key: str):
return sorted([p for p in root_dir.rglob("*.bin") if cam_key.lower() in p.name.lower()])
def find_triplets(root_dir: Path, cams: list[str]):
by_cam = {cam: find_cam_bins(root_dir, cam) for cam in cams}
key_maps = {}
for cam, paths in by_cam.items():
m = {}
for p in paths:
key = clean_stem_for_pair(p, cam)
m[(p.parent, key)] = p
m.setdefault((None, key), p)
key_maps[cam] = m
ref_cam = cams[0]
triplets = []
for ref_path in by_cam[ref_cam]:
key = clean_stem_for_pair(ref_path, ref_cam)
folder = ref_path.parent
item = {ref_cam: ref_path}
ok = True
for cam in cams[1:]:
p = key_maps[cam].get((folder, key)) or key_maps[cam].get((None, key))
if p is None:
same_folder = [x for x in by_cam[cam] if x.parent == folder]
if len(same_folder) == 1:
p = same_folder[0]
if p is None:
ok = False
break
item[cam] = p
if ok:
triplets.append(item)
return triplets, by_cam
# ============================================================
# ChArUco helpers
# ============================================================
def get_aruco_dict(dict_name: str):
aruco = cv2.aruco
mapping = {
"4X4_50": aruco.DICT_4X4_50,
"4X4_100": aruco.DICT_4X4_100,
"4X4_250": aruco.DICT_4X4_250,
"4X4_1000": aruco.DICT_4X4_1000,
"5X5_50": aruco.DICT_5X5_50,
"5X5_100": aruco.DICT_5X5_100,
"5X5_250": aruco.DICT_5X5_250,
"5X5_1000": aruco.DICT_5X5_1000,
"6X6_50": aruco.DICT_6X6_50,
"6X6_100": aruco.DICT_6X6_100,
"6X6_250": aruco.DICT_6X6_250,
"6X6_1000": aruco.DICT_6X6_1000,
}
key = dict_name.upper()
if key not in mapping:
raise RuntimeError(f"Dicionário ArUco não suportado: {dict_name}")
if hasattr(aruco, "getPredefinedDictionary"):
return aruco.getPredefinedDictionary(mapping[key])
return aruco.Dictionary_get(mapping[key])
def create_charuco_board(squares_x, squares_y, square_length, marker_length, aruco_dict):
aruco = cv2.aruco
if hasattr(aruco, "CharucoBoard"):
try:
return aruco.CharucoBoard((squares_x, squares_y), square_length, marker_length, aruco_dict)
except Exception:
pass
if hasattr(aruco, "CharucoBoard_create"):
return aruco.CharucoBoard_create(squares_x, squares_y, square_length, marker_length, aruco_dict)
raise RuntimeError("Sua versão do OpenCV não tem CharucoBoard/CharucoBoard_create.")
def create_detector_params():
aruco = cv2.aruco
if hasattr(aruco, "DetectorParameters"):
return aruco.DetectorParameters()
if hasattr(aruco, "DetectorParameters_create"):
return aruco.DetectorParameters_create()
return None
def detect_charuco(gray: np.ndarray, board, aruco_dict, min_corners: int):
aruco = cv2.aruco
params = create_detector_params()
if hasattr(aruco, "CharucoDetector"):
try:
detector = aruco.CharucoDetector(board)
charuco_corners, charuco_ids, marker_corners, marker_ids = detector.detectBoard(gray)
if charuco_corners is None or charuco_ids is None:
return None, None
corners = np.array(charuco_corners, dtype=np.float32).reshape(-1, 2)
ids = np.array(charuco_ids, dtype=np.int32).reshape(-1)
if len(ids) < min_corners:
return None, None
return corners, ids
except Exception:
pass
if params is not None:
marker_corners, marker_ids, rejected = aruco.detectMarkers(gray, aruco_dict, parameters=params)
else:
marker_corners, marker_ids, rejected = aruco.detectMarkers(gray, aruco_dict)
if marker_ids is None or len(marker_ids) == 0:
return None, None
try:
aruco.refineDetectedMarkers(gray, board, marker_corners, marker_ids, rejected)
except Exception:
pass
retval, charuco_corners, charuco_ids = aruco.interpolateCornersCharuco(marker_corners, marker_ids, gray, board)
if charuco_corners is None or charuco_ids is None:
return None, None
corners = np.array(charuco_corners, dtype=np.float32).reshape(-1, 2)
ids = np.array(charuco_ids, dtype=np.int32).reshape(-1)
if len(ids) < min_corners:
return None, None
return corners, ids
def common_points_2cam(corners_a, ids_a, corners_b, ids_b, min_common: int):
map_a = {int(i): corners_a[k] for k, i in enumerate(ids_a)}
map_b = {int(i): corners_b[k] for k, i in enumerate(ids_b)}
common_ids = sorted(set(map_a.keys()) & set(map_b.keys()))
if len(common_ids) < min_common:
return None, None, common_ids
pts_a = np.array([map_a[i] for i in common_ids], dtype=np.float32).reshape(-1, 2)
pts_b = np.array([map_b[i] for i in common_ids], dtype=np.float32).reshape(-1, 2)
return pts_a, pts_b, common_ids
def compute_homography_to_rgb(rgb_det, other_det, min_common: int, ransac_thresh: float):
rgb_corners, rgb_ids = rgb_det
other_corners, other_ids = other_det
if rgb_corners is None or rgb_ids is None or other_corners is None or other_ids is None:
return None, None, []
pts_other, pts_rgb, common_ids = common_points_2cam(
other_corners,
other_ids,
rgb_corners,
rgb_ids,
min_common=min_common,
)
if pts_other is None:
return None, None, common_ids
H, mask = cv2.findHomography(pts_other, pts_rgb, cv2.RANSAC, ransac_thresh)
if H is None:
return None, None, common_ids
inliers = int(np.count_nonzero(mask)) if mask is not None else 0
return H, inliers, common_ids
def draw_charuco_debug(img_bgr, corners, ids, label):
out = img_bgr.copy()
if corners is not None and ids is not None:
corners_draw = np.array(corners, dtype=np.float32).reshape(-1, 1, 2)
ids_draw = np.array(ids, dtype=np.int32).reshape(-1, 1)
try:
cv2.aruco.drawDetectedCornersCharuco(out, corners_draw, ids_draw, (0, 255, 0))
except Exception:
for p in corners:
cv2.circle(out, tuple(np.round(p).astype(int)), 3, (0, 255, 0), -1)
n = 0 if ids is None else len(ids)
cv2.putText(out, f"{label} corners={n}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 1, cv2.LINE_AA)
return out
# ============================================================
# Visualization
# ============================================================
def draw_lines(img_bgr, step=40):
out = img_bgr.copy()
h, w = out.shape[:2]
for y in range(0, h, step):
color = (0, 255, 255) if (y // step) % 2 == 0 else (255, 255, 0)
cv2.line(out, (0, y), (w, y), color, 1, cv2.LINE_AA)
return out
def resize_to_height(img, target_h):
h, w = img.shape[:2]
if h == target_h:
return img
scale = target_h / h
new_w = max(1, int(w * scale))
return cv2.resize(img, (new_w, target_h), interpolation=cv2.INTER_AREA)
def put_label(img, text, color=(255, 255, 255)):
out = img.copy()
cv2.rectangle(out, (0, 0), (out.shape[1], 34), (0, 0, 0), -1)
cv2.putText(out, text, (10, 24), cv2.FONT_HERSHEY_SIMPLEX, 0.65, color, 1, cv2.LINE_AA)
return out
def draw_header(canvas, lines):
header_h = 24 + 24 * len(lines)
cv2.rectangle(canvas, (0, 0), (canvas.shape[1], header_h), (0, 0, 0), -1)
y = 24
for line in lines:
cv2.putText(canvas, line, (12, y), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (255, 255, 255), 1, cv2.LINE_AA)
y += 24
return canvas
def compose_three(left, center, right, lines, args):
if args.lines:
left = draw_lines(left, args.line_step)
center = draw_lines(center, args.line_step)
right = draw_lines(right, args.line_step)
imgs = [resize_to_height(x, args.view_h) for x in [left, center, right]]
h = min(x.shape[0] for x in imgs)
imgs = [x[:h] for x in imgs]
canvas = np.hstack(imgs)
return draw_header(canvas, lines)
def absdiff_bgr(a, b):
ag = to_gray_u8(a)
bg = to_gray_u8(b)
diff = cv2.absdiff(ag, bg)
return cv2.cvtColor(diff, cv2.COLOR_GRAY2BGR)
def overlay_bgr(base, layer, alpha=0.45):
layer = cv2.resize(layer, (base.shape[1], base.shape[0]), interpolation=cv2.INTER_AREA)
return cv2.addWeighted(base, 1.0 - alpha, layer, alpha, 0)
# ============================================================
# Main processing
# ============================================================
def load_triplet(item, args):
rgb_bgr, rgb_gray = read_rgb(
item[args.rgb_cam],
args.width,
args.height,
args.rgb_bayer,
args.rgb_view,
use_clahe=not args.no_clahe,
)
re_bgr, re_gray = read_mono(item[args.re_cam], args.width, args.height, use_clahe=not args.no_clahe)
nir_bgr, nir_gray = read_mono(item[args.nir_cam], args.width, args.height, use_clahe=not args.no_clahe)
return {
"rgb_bgr": rgb_bgr,
"rgb_gray": rgb_gray,
"re_bgr": re_bgr,
"re_gray": re_gray,
"nir_bgr": nir_bgr,
"nir_gray": nir_gray,
}
def build_views(item, board, aruco_dict, args):
frames = load_triplet(item, args)
rgb_det = detect_charuco(frames["rgb_gray"], board, aruco_dict, args.min_corners)
re_det = detect_charuco(frames["re_gray"], board, aruco_dict, args.min_corners)
nir_det = detect_charuco(frames["nir_gray"], board, aruco_dict, args.min_corners)
H_re, inliers_re, common_re = compute_homography_to_rgb(
rgb_det,
re_det,
min_common=args.min_common,
ransac_thresh=args.ransac_thresh,
)
H_nir, inliers_nir, common_nir = compute_homography_to_rgb(
rgb_det,
nir_det,
min_common=args.min_common,
ransac_thresh=args.ransac_thresh,
)
h, w = frames["rgb_bgr"].shape[:2]
if H_re is not None:
re_to_rgb = cv2.warpPerspective(
frames["re_bgr"],
H_re,
(w, h),
flags=cv2.INTER_LINEAR,
borderMode=cv2.BORDER_CONSTANT,
borderValue=0,
)
else:
re_to_rgb = np.zeros_like(frames["rgb_bgr"])
if H_nir is not None:
nir_to_rgb = cv2.warpPerspective(
frames["nir_bgr"],
H_nir,
(w, h),
flags=cv2.INTER_LINEAR,
borderMode=cv2.BORDER_CONSTANT,
borderValue=0,
)
else:
nir_to_rgb = np.zeros_like(frames["rgb_bgr"])
stats = {
"rgb_corners": 0 if rgb_det[1] is None else len(rgb_det[1]),
"re_corners": 0 if re_det[1] is None else len(re_det[1]),
"nir_corners": 0 if nir_det[1] is None else len(nir_det[1]),
"common_re": len(common_re),
"common_nir": len(common_nir),
"inliers_re": 0 if inliers_re is None else int(inliers_re),
"inliers_nir": 0 if inliers_nir is None else int(inliers_nir),
"ok_re": H_re is not None,
"ok_nir": H_nir is not None,
}
return {
**frames,
"rgb_det": rgb_det,
"re_det": re_det,
"nir_det": nir_det,
"re_to_rgb": re_to_rgb,
"nir_to_rgb": nir_to_rgb,
"diff_re": absdiff_bgr(frames["rgb_bgr"], re_to_rgb),
"diff_nir": absdiff_bgr(frames["rgb_bgr"], nir_to_rgb),
"overlay_re": overlay_bgr(frames["rgb_bgr"], re_to_rgb, args.overlay_alpha),
"overlay_nir": overlay_bgr(frames["rgb_bgr"], nir_to_rgb, args.overlay_alpha),
"stats": stats,
}
def compose_mode(views, item, idx, total, mode, args):
stats = views["stats"]
name = item[args.rgb_cam].name
status = (
f"RGB={stats['rgb_corners']} RE={stats['re_corners']} NIR={stats['nir_corners']} | "
f"RE common/inliers={stats['common_re']}/{stats['inliers_re']} | "
f"NIR common/inliers={stats['common_nir']}/{stats['inliers_nir']}"
)
if mode == "native":
left = put_label(views["re_bgr"], "RE native")
center = put_label(views["rgb_bgr"], "RGB REF native")
right = put_label(views["nir_bgr"], "NIR native")
lines = [
f"{idx + 1}/{total} | {name}",
f"mode=native | {status}",
"N/SPACE prox | A ant | M modo | L linhas | S salvar | Q sair",
]
return compose_three(left, center, right, lines, args)
if mode == "rgb_ref":
left = put_label(views["re_to_rgb"], "RE -> RGB plane")
center = put_label(views["rgb_bgr"], "RGB REF")
right = put_label(views["nir_to_rgb"], "NIR -> RGB plane")
lines = [
f"{idx + 1}/{total} | {name}",
f"mode=rgb_ref planar homography | {status}",
"N/SPACE prox | A ant | M modo | L linhas | S salvar | Q sair",
]
return compose_three(left, center, right, lines, args)
if mode == "diff":
left = put_label(views["diff_re"], "diff RGB vs RE_to_RGB")
center = put_label(views["rgb_bgr"], "RGB REF")
right = put_label(views["diff_nir"], "diff RGB vs NIR_to_RGB")
lines = [
f"{idx + 1}/{total} | {name}",
f"mode=diff | {status}",
"N/SPACE prox | A ant | M modo | L linhas | S salvar | Q sair",
]
return compose_three(left, center, right, lines, args)
if mode == "overlay":
left = put_label(views["overlay_re"], "overlay RGB + RE_to_RGB")
center = put_label(views["rgb_bgr"], "RGB REF")
right = put_label(views["overlay_nir"], "overlay RGB + NIR_to_RGB")
lines = [
f"{idx + 1}/{total} | {name}",
f"mode=overlay alpha={args.overlay_alpha:.2f} | {status}",
"N/SPACE prox | A ant | M modo | L linhas | S salvar | Q sair",
]
return compose_three(left, center, right, lines, args)
if mode == "debug_corners":
left = draw_charuco_debug(views["re_bgr"], views["re_det"][0], views["re_det"][1], "RE native")
center = draw_charuco_debug(views["rgb_bgr"], views["rgb_det"][0], views["rgb_det"][1], "RGB native")
right = draw_charuco_debug(views["nir_bgr"], views["nir_det"][0], views["nir_det"][1], "NIR native")
lines = [
f"{idx + 1}/{total} | {name}",
f"mode=debug_corners | {status}",
"N/SPACE prox | A ant | M modo | L linhas | S salvar | Q sair",
]
return compose_three(left, center, right, lines, args)
raise RuntimeError(f"Modo desconhecido: {mode}")
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--root_dir", required=True)
parser.add_argument("--rgb_cam", default="CAM_A")
parser.add_argument("--re_cam", default="CAM_B")
parser.add_argument("--nir_cam", default="CAM_C")
parser.add_argument("--width", type=int, default=1280)
parser.add_argument("--height", type=int, default=800)
parser.add_argument("--rgb_bayer", default="BGGR")
parser.add_argument(
"--rgb_view",
default="gray",
choices=["color", "gray", "raw_bayer_gray"],
help="Como processar CAM_A/RGB no viewer.",
)
parser.add_argument("--squares_x", type=int, default=13)
parser.add_argument("--squares_y", type=int, default=7)
parser.add_argument("--square_length", type=float, default=0.031)
parser.add_argument("--marker_length", type=float, default=0.023)
parser.add_argument("--aruco_dict", default="4X4_50")
parser.add_argument("--min_corners", type=int, default=30)
parser.add_argument("--min_common", type=int, default=20)
parser.add_argument("--ransac_thresh", type=float, default=3.0)
parser.add_argument("--view_h", type=int, default=420)
parser.add_argument("--no_clahe", action="store_true")
parser.add_argument("--lines", action="store_true")
parser.add_argument("--line_step", type=int, default=40)
parser.add_argument("--overlay_alpha", type=float, default=0.45)
parser.add_argument("--save_dir", default="rgb_reference_homography_saves")
args = parser.parse_args()
root_dir = Path(args.root_dir)
save_dir = Path(args.save_dir)
save_dir.mkdir(parents=True, exist_ok=True)
cams = [args.rgb_cam, args.re_cam, args.nir_cam]
triplets, by_cam = find_triplets(root_dir, cams)
print(f"[INFO] root_dir={root_dir}")
print(f"[INFO] cams RGB={args.rgb_cam} RE={args.re_cam} NIR={args.nir_cam}")
print(f"[INFO] rgb_bayer={args.rgb_bayer} rgb_view={args.rgb_view}")
print(f"[INFO] ChArUco squares={args.squares_x}x{args.squares_y}")
for cam in cams:
print(f"[INFO] arquivos {cam}: {len(by_cam[cam])}")
print(f"[INFO] triplets: {len(triplets)}")
if not triplets:
raise RuntimeError("Nenhum triplet encontrado.")
aruco_dict = get_aruco_dict(args.aruco_dict)
board = create_charuco_board(args.squares_x, args.squares_y, args.square_length, args.marker_length, aruco_dict)
modes = ["native", "debug_corners", "rgb_ref", "overlay", "diff"]
mode_idx = 0
idx = 0
cached_key = None
cached_views = None
cv2.namedWindow("RGB Reference Homography Viewer", cv2.WINDOW_NORMAL)
while True:
item = triplets[idx]
mode = modes[mode_idx]
key_cache = (
tuple(str(item[cam]) for cam in cams),
args.width,
args.height,
args.rgb_bayer,
args.rgb_view,
args.no_clahe,
args.min_corners,
args.min_common,
args.ransac_thresh,
args.overlay_alpha,
)
if key_cache != cached_key:
print(f"[RUN] {idx + 1}/{len(triplets)} - {item[args.rgb_cam].name}")
try:
cached_views = build_views(item, board, aruco_dict, args)
cached_key = key_cache
except Exception as e:
print("[ERRO] Falha processando triplet:")
for cam in cams:
print(f" {cam}={item[cam]}")
print(f" erro={e}")
idx = min(idx + 1, len(triplets) - 1)
cached_key = None
cached_views = None
continue
view = compose_mode(cached_views, item, idx, len(triplets), mode, args)
cv2.imshow("RGB Reference Homography Viewer", view)
key = cv2.waitKey(0) & 0xFF
if key in [27, ord("q"), ord("Q")]:
break
elif key in [ord("n"), ord("N"), 32]:
idx = min(idx + 1, len(triplets) - 1)
cached_key = None
elif key in [ord("a"), ord("A")]:
idx = max(idx - 1, 0)
cached_key = None
elif key in [ord("m"), ord("M")]:
mode_idx = (mode_idx + 1) % len(modes)
print(f"[PARAM] mode={modes[mode_idx]}")
elif key in [ord("l"), ord("L")]:
args.lines = not args.lines
print(f"[PARAM] lines={args.lines}")
elif key in [ord("s"), ord("S")]:
out_path = save_dir / f"rgb_ref_homography_{idx:04d}_{mode}.png"
cv2.imwrite(str(out_path), view)
print(f"[SAVE] {out_path}")
cv2.destroyAllWindows()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,653 @@
import argparse
import json
import time
from pathlib import Path
import cv2
import numpy as np
from core.oak_fcc3_client import OakFcc3Client as MultiSpectralClient
# ============================================================
# Config padrão do projeto
# ============================================================
with open("config.json", "r", encoding="utf-8") as f:
config = json.load(f)
RAW_SIZE = config.get("raw_size", [1280, 800])
MODULE_PARAMS = config.get("module_params_json")
# ============================================================
# Visual helpers
# ============================================================
def normalize_to_u8(img: np.ndarray, p_low=1.0, p_high=99.0) -> np.ndarray:
arr = img.astype(np.float32)
valid = np.isfinite(arr)
if np.count_nonzero(valid) < 20:
return np.zeros(arr.shape[:2], dtype=np.uint8)
vals = arr[valid]
lo = np.percentile(vals, p_low)
hi = np.percentile(vals, p_high)
out = (arr - lo) / (hi - lo + 1e-6)
out = np.clip(out, 0.0, 1.0)
return (out * 255).astype(np.uint8)
def ensure_bgr_u8(img) -> np.ndarray:
if img is None:
return None
arr = np.asarray(img)
if arr.ndim == 2:
if arr.dtype != np.uint8:
arr = normalize_to_u8(arr)
return cv2.cvtColor(arr, cv2.COLOR_GRAY2BGR)
if arr.ndim == 3 and arr.shape[2] == 3:
if arr.dtype == np.uint8:
return arr.copy()
arr = np.clip(arr.astype(np.float32), 0.0, 1.0)
return (arr * 255.0).astype(np.uint8)
raise RuntimeError(f"Imagem inválida para BGR: shape={arr.shape}, dtype={arr.dtype}")
def to_gray_u8(img_bgr: np.ndarray) -> np.ndarray:
if img_bgr.ndim == 2:
return img_bgr if img_bgr.dtype == np.uint8 else normalize_to_u8(img_bgr)
return cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)
def resize_to_fit(img: np.ndarray, w: int, h: int) -> np.ndarray:
return cv2.resize(img, (w, h), interpolation=cv2.INTER_AREA)
def put_label(img: np.ndarray, text: str, color=(255, 255, 255)) -> np.ndarray:
out = img.copy()
cv2.rectangle(out, (0, 0), (out.shape[1], 34), (0, 0, 0), -1)
cv2.putText(out, text, (10, 24), cv2.FONT_HERSHEY_SIMPLEX, 0.65, color, 1, cv2.LINE_AA)
return out
def overlay_hud(img_bgr: np.ndarray, lines: list[str]):
y = 24
for s in lines:
cv2.putText(img_bgr, s, (12, y), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (0, 0, 0), 3, cv2.LINE_AA)
cv2.putText(img_bgr, s, (12, y), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (255, 255, 255), 1, cv2.LINE_AA)
y += 24
def colorize_scalar_0_1(x: np.ndarray, cmap=cv2.COLORMAP_TURBO) -> np.ndarray:
x8 = np.clip(x * 255.0, 0, 255).astype(np.uint8)
return cv2.applyColorMap(x8, cmap)
def make_2x2(a, b, c, d, cell_w=640, cell_h=400, hud_lines=None) -> np.ndarray:
a = resize_to_fit(a, cell_w, cell_h)
b = resize_to_fit(b, cell_w, cell_h)
c = resize_to_fit(c, cell_w, cell_h)
d = resize_to_fit(d, cell_w, cell_h)
top = np.hstack([a, b])
bot = np.hstack([c, d])
canvas = np.vstack([top, bot])
if hud_lines:
overlay_hud(canvas, hud_lines)
return canvas
def make_3x2(a, b, c, d, e, f, cell_w=520, cell_h=320, hud_lines=None) -> np.ndarray:
imgs = [resize_to_fit(x, cell_w, cell_h) for x in [a, b, c, d, e, f]]
row1 = np.hstack(imgs[:3])
row2 = np.hstack(imgs[3:])
canvas = np.vstack([row1, row2])
if hud_lines:
overlay_hud(canvas, hud_lines)
return canvas
# ============================================================
# Calibração product bundle
# ============================================================
def scalar_str(x):
arr = np.array(x)
if arr.shape == ():
return str(arr.item())
return str(x)
def load_calibration_bundle(path: str | Path) -> dict:
data = np.load(str(path), allow_pickle=True)
keys = set(data.files)
required = [
"rgb_cam",
"re_cam",
"nir_cam",
"image_size",
"H_RE_to_RGB",
"H_NIR_to_RGB",
]
for k in required:
if k not in keys:
raise RuntimeError(f"Calibração sem chave obrigatória: {k}")
calib = {
"data": data,
"keys": keys,
"rgb_cam": scalar_str(data["rgb_cam"]),
"re_cam": scalar_str(data["re_cam"]),
"nir_cam": scalar_str(data["nir_cam"]),
"image_size": tuple(data["image_size"].astype(int).tolist()),
"H_RE_to_RGB": data["H_RE_to_RGB"].astype(np.float64),
"H_NIR_to_RGB": data["H_NIR_to_RGB"].astype(np.float64),
"overlap_RE_to_RGB": data["overlap_RE_to_RGB"] if "overlap_RE_to_RGB" in keys else None,
"overlap_NIR_to_RGB": data["overlap_NIR_to_RGB"] if "overlap_NIR_to_RGB" in keys else None,
"overlap_common_RGB": data["overlap_common_RGB"] if "overlap_common_RGB" in keys else None,
}
return calib
def get_pair_prefix(calib: dict, cam1: str, cam2: str):
keys = calib["keys"]
direct = f"pair_{cam1}_{cam2}"
inv = f"pair_{cam2}_{cam1}"
if f"{direct}_map1x" in keys:
return direct, False
if f"{inv}_map1x" in keys:
return inv, True
raise RuntimeError(f"Par estéreo {cam1}<->{cam2} não encontrado no .npz")
def rectify_pair_gray(gray1, gray2, calib: dict, cam1: str, cam2: str):
data = calib["data"]
image_w, image_h = calib["image_size"]
if gray1.shape[::-1] != (image_w, image_h):
gray1 = cv2.resize(gray1, (image_w, image_h), interpolation=cv2.INTER_AREA)
if gray2.shape[::-1] != (image_w, image_h):
gray2 = cv2.resize(gray2, (image_w, image_h), interpolation=cv2.INTER_AREA)
prefix, inverted = get_pair_prefix(calib, cam1, cam2)
if not inverted:
map1x = data[f"{prefix}_map1x"]
map1y = data[f"{prefix}_map1y"]
map2x = data[f"{prefix}_map2x"]
map2y = data[f"{prefix}_map2y"]
else:
map1x = data[f"{prefix}_map2x"]
map1y = data[f"{prefix}_map2y"]
map2x = data[f"{prefix}_map1x"]
map2y = data[f"{prefix}_map1y"]
rect1 = cv2.remap(gray1, map1x, map1y, cv2.INTER_LINEAR, borderMode=cv2.BORDER_CONSTANT, borderValue=0)
rect2 = cv2.remap(gray2, map2x, map2y, cv2.INTER_LINEAR, borderMode=cv2.BORDER_CONSTANT, borderValue=0)
return rect1, rect2
# ============================================================
# Frame extraction from OakFcc3Client
# ============================================================
def role_map_from_meta(meta: dict) -> dict:
camera_info = meta.get("camera_info", {}) or {}
out = {}
for cam_id, info in camera_info.items():
role = info.get("role") or cam_id
out[role] = cam_id
return out
def build_live_role_images(cam, frame, meta, calib: dict, beauty_preview=False) -> dict:
"""
Retorna imagens BGR por role: rgb/re/nir.
Usa os helpers do OakFcc3Client para reconstruir previews a partir do RAW_BRUTO.
O script de referência usa get_next_decoded(...) e build_preview_from_raw_payload(...)
para RAW_BRUTO; aqui aproveitamos build_visual_preview_from_raw(...), quando disponível.
"""
frame_type = meta.get("frame_type", "RAW_BRUTO")
if frame_type != "RAW_BRUTO" or not isinstance(frame, dict):
raise RuntimeError("Este viewer espera frame_type=RAW_BRUTO e frame como dict por câmera.")
camera_info = meta.get("camera_info", {}) or {}
previews = None
if hasattr(cam, "build_visual_preview_from_raw"):
try:
previews = cam.build_visual_preview_from_raw(frame, meta)
except Exception:
previews = None
if previews is None:
preview_bgr, _, preview_source_id = cam.build_preview_from_raw_payload(frame=frame, meta=meta)
previews = {preview_source_id: preview_bgr}
by_role = {}
for cam_id, img in previews.items():
role = camera_info.get(cam_id, {}).get("role", cam_id)
by_role[role] = ensure_bgr_u8(img)
missing = [r for r in ["rgb", "re", "nir"] if r not in by_role]
if missing:
raise RuntimeError(f"Previews sem roles necessários: {missing}. Roles disponíveis={list(by_role.keys())}")
return by_role
# ============================================================
# Alignment / depth / confidence
# ============================================================
def warp_spectral_to_rgb(rgb_bgr, re_bgr, nir_bgr, calib: dict):
h, w = rgb_bgr.shape[:2]
re_to_rgb = cv2.warpPerspective(
re_bgr,
calib["H_RE_to_RGB"],
(w, h),
flags=cv2.INTER_LINEAR,
borderMode=cv2.BORDER_CONSTANT,
borderValue=0,
)
nir_to_rgb = cv2.warpPerspective(
nir_bgr,
calib["H_NIR_to_RGB"],
(w, h),
flags=cv2.INTER_LINEAR,
borderMode=cv2.BORDER_CONSTANT,
borderValue=0,
)
return re_to_rgb, nir_to_rgb
def make_sgbm(args):
num_disp = max(16, int(round(args.num_disp / 16)) * 16)
block_size = max(3, int(args.block_size))
if block_size % 2 == 0:
block_size += 1
matcher = cv2.StereoSGBM_create(
minDisparity=args.min_disp,
numDisparities=num_disp,
blockSize=block_size,
P1=8 * block_size * block_size,
P2=32 * block_size * block_size,
disp12MaxDiff=1,
uniquenessRatio=args.uniqueness,
speckleWindowSize=args.speckle_window,
speckleRange=args.speckle_range,
preFilterCap=63,
mode=cv2.STEREO_SGBM_MODE_SGBM_3WAY,
)
return matcher, num_disp, block_size
def compute_stereo_disparity_and_confidence(re_bgr, nir_bgr, calib: dict, args):
re_cam = calib["re_cam"]
nir_cam = calib["nir_cam"]
re_gray = to_gray_u8(re_bgr)
nir_gray = to_gray_u8(nir_bgr)
# Par estéreo no espaço RE/NIR retificado.
re_rect, nir_rect = rectify_pair_gray(re_gray, nir_gray, calib, re_cam, nir_cam)
matcher, num_disp, block_size = make_sgbm(args)
disp = matcher.compute(re_rect, nir_rect).astype(np.float32) / 16.0
valid = disp > args.min_valid_disp
# Normalização visual do depth/disparity.
disp_vis = disp.copy()
disp_vis[~valid] = 0.0
if np.count_nonzero(valid) > 20:
vals = disp_vis[valid]
p2 = np.percentile(vals, 2)
p98 = np.percentile(vals, 98)
disp_norm = (disp_vis - p2) / (p98 - p2 + 1e-6)
disp_norm = np.clip(disp_norm, 0.0, 1.0)
else:
p2 = 0.0
p98 = 1.0
disp_norm = np.zeros_like(disp_vis, dtype=np.float32)
disp_color = colorize_scalar_0_1(disp_norm)
# Confiança geométrica simples:
# - válida no SGBM
# - penaliza saltos fortes de disparity
# - penaliza regiões com baixa textura no par retificado
valid_f = valid.astype(np.float32)
disp_smooth = cv2.GaussianBlur(disp_vis, (5, 5), 0)
grad_x = cv2.Sobel(disp_smooth, cv2.CV_32F, 1, 0, ksize=3)
grad_y = cv2.Sobel(disp_smooth, cv2.CV_32F, 0, 1, ksize=3)
grad_mag = np.sqrt(grad_x * grad_x + grad_y * grad_y)
grad_penalty = np.clip(grad_mag / max(args.parallax_grad_ref, 1e-6), 0.0, 1.0)
tex_re = cv2.Laplacian(re_rect, cv2.CV_32F, ksize=3)
tex_nir = cv2.Laplacian(nir_rect, cv2.CV_32F, ksize=3)
texture = (np.abs(tex_re) + np.abs(tex_nir)) * 0.5
texture_norm = np.clip(texture / max(args.texture_ref, 1e-6), 0.0, 1.0)
texture_norm = cv2.GaussianBlur(texture_norm, (5, 5), 0)
confidence_rect = valid_f * (1.0 - grad_penalty) * (0.35 + 0.65 * texture_norm)
confidence_rect = np.clip(confidence_rect, 0.0, 1.0)
# Para exibir junto com RGB, trazemos a confiança do espaço estéreo RE/NIR para RGB usando a homografia RE->RGB.
h_rgb, w_rgb = re_bgr.shape[:2]
confidence_rgb = cv2.warpPerspective(
confidence_rect.astype(np.float32),
calib["H_RE_to_RGB"],
(w_rgb, h_rgb),
flags=cv2.INTER_LINEAR,
borderMode=cv2.BORDER_CONSTANT,
borderValue=0,
)
disp_rgb = cv2.warpPerspective(
disp_norm.astype(np.float32),
calib["H_RE_to_RGB"],
(w_rgb, h_rgb),
flags=cv2.INTER_LINEAR,
borderMode=cv2.BORDER_CONSTANT,
borderValue=0,
)
confidence_color = colorize_scalar_0_1(confidence_rgb, cmap=cv2.COLORMAP_VIRIDIS)
disp_rgb_color = colorize_scalar_0_1(disp_rgb, cmap=cv2.COLORMAP_TURBO)
stats = {
"num_disp": num_disp,
"block_size": block_size,
"valid_pct": float(np.mean(valid) * 100.0),
"conf_mean": float(np.mean(confidence_rect)),
"disp_p02": float(p2),
"disp_p98": float(p98),
"disp_p50": float(np.percentile(disp_vis[valid], 50)) if np.count_nonzero(valid) > 20 else 0.0,
}
return {
"re_rect": cv2.cvtColor(re_rect, cv2.COLOR_GRAY2BGR),
"nir_rect": cv2.cvtColor(nir_rect, cv2.COLOR_GRAY2BGR),
"disp_rect_color": disp_color,
"disp_rgb_color": disp_rgb_color,
"confidence_rgb": confidence_rgb,
"confidence_color": confidence_color,
"stats": stats,
}
def apply_overlap_mask(img_bgr, calib: dict, enabled=True):
if not enabled:
return img_bgr
mask = calib.get("overlap_common_RGB")
if mask is None:
return img_bgr
if mask.shape[:2] != img_bgr.shape[:2]:
mask = cv2.resize(mask, (img_bgr.shape[1], img_bgr.shape[0]), interpolation=cv2.INTER_NEAREST)
mask_bool = mask > 0
out = img_bgr.copy()
out[~mask_bool] = (out[~mask_bool] * 0.2).astype(np.uint8)
return out
def confidence_overlay_on_rgb(rgb_bgr, confidence_rgb, alpha=0.45):
conf_color = colorize_scalar_0_1(confidence_rgb, cmap=cv2.COLORMAP_VIRIDIS)
return cv2.addWeighted(rgb_bgr, 1.0 - alpha, conf_color, alpha, 0)
# ============================================================
# Main
# ============================================================
def main():
parser = argparse.ArgumentParser(
description="Live preview: RGB referência + RE/NIR alinhados por homografia + depth/confiança por estéreo RE-NIR.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument("--calib_path", required=True, help=".npz com H_RE_to_RGB, H_NIR_to_RGB e mapas estéreo RE-NIR")
parser.add_argument("--fps", type=int, default=20)
parser.add_argument("--width", type=int, default=RAW_SIZE[0])
parser.add_argument("--height", type=int, default=RAW_SIZE[1])
parser.add_argument("--bayer", default="BGGR", choices=["GBRG", "GRBG", "RGGB", "BGGR"])
parser.add_argument("--module_calibration_json", default=MODULE_PARAMS)
parser.add_argument("--output_dtype", default="float32", choices=["uint8", "uint16", "float32"])
parser.add_argument("--capture_mode", default="TRIPLE", choices=["AUTO", "SINGLE", "DOUBLE", "TRIPLE"])
parser.add_argument("--raw_policy", default="require_triple", choices=["allow_single", "require_triple"])
parser.add_argument("--cell_w", type=int, default=640)
parser.add_argument("--cell_h", type=int, default=400)
parser.add_argument("--layout", default="2x2", choices=["2x2", "3x2"])
parser.add_argument("--mask_overlap", action="store_true", help="Escurece área fora da interseção RE/NIR->RGB salva no .npz")
parser.add_argument("--show_conf_overlay", action="store_true")
parser.add_argument("--num_disp", type=int, default=128)
parser.add_argument("--block_size", type=int, default=7)
parser.add_argument("--min_disp", type=int, default=0)
parser.add_argument("--min_valid_disp", type=float, default=1.0)
parser.add_argument("--uniqueness", type=int, default=8)
parser.add_argument("--speckle_window", type=int, default=80)
parser.add_argument("--speckle_range", type=int, default=2)
parser.add_argument("--parallax_grad_ref", type=float, default=6.0)
parser.add_argument("--texture_ref", type=float, default=25.0)
args = parser.parse_args()
calib = load_calibration_bundle(args.calib_path)
print("============================================")
print("Live Multispec Alignment Preview")
print(f"calib_path : {args.calib_path}")
print(f"RGB cam : {calib['rgb_cam']}")
print(f"RE cam : {calib['re_cam']}")
print(f"NIR cam : {calib['nir_cam']}")
print(f"image_size : {calib['image_size']}")
print(f"raw : {args.width}x{args.height} | bayer={args.bayer}")
print(f"capture : RAW_BRUTO | {args.capture_mode} | {args.raw_policy}")
print("Keys : Q/Esc sair | O overlap | C conf overlay | [ ] numDisp | - + block")
print("============================================")
window_name = "Live RGB/RE/NIR + Stereo Confidence"
cv2.namedWindow(window_name, cv2.WINDOW_NORMAL)
fps_view = 0.0
n_view = 0
t_fps = time.time()
last_frame_id = -1
msg = ""
msg_t = 0.0
mask_overlap = args.mask_overlap
conf_overlay = args.show_conf_overlay
try:
with MultiSpectralClient(
width=args.width,
height=args.height,
bayer=args.bayer,
fps=args.fps,
frame_type="RAW_BRUTO",
output_dtype=args.output_dtype,
capture_mode=args.capture_mode,
raw_policy=args.raw_policy,
module_calibration_json=args.module_calibration_json,
) as cam:
while True:
frame, meta, decoded = cam.get_next_decoded(timeout=1.0)
if meta is None or frame is None:
k = cv2.waitKey(1) & 0xFF
if k in (ord("q"), ord("Q"), 27):
break
continue
frame_id = meta.get("frame_id", -1)
if frame_id == last_frame_id:
k = cv2.waitKey(1) & 0xFF
if k in (ord("q"), ord("Q"), 27):
break
continue
last_frame_id = frame_id
try:
role_imgs = build_live_role_images(cam, frame, meta, calib)
rgb = role_imgs["rgb"]
re = role_imgs["re"]
nir = role_imgs["nir"]
# Garante que tudo use o tamanho do RGB como referência visual.
rgb_h, rgb_w = rgb.shape[:2]
if re.shape[:2] != (rgb_h, rgb_w):
re = cv2.resize(re, (rgb_w, rgb_h), interpolation=cv2.INTER_AREA)
if nir.shape[:2] != (rgb_h, rgb_w):
nir = cv2.resize(nir, (rgb_w, rgb_h), interpolation=cv2.INTER_AREA)
re_rgb, nir_rgb = warp_spectral_to_rgb(rgb, re, nir, calib)
stereo = compute_stereo_disparity_and_confidence(re, nir, calib, args)
rgb_show = apply_overlap_mask(rgb, calib, enabled=mask_overlap)
re_show = apply_overlap_mask(re_rgb, calib, enabled=mask_overlap)
nir_show = apply_overlap_mask(nir_rgb, calib, enabled=mask_overlap)
if conf_overlay:
rgb_panel = confidence_overlay_on_rgb(rgb_show, stereo["confidence_rgb"])
rgb_panel = put_label(rgb_panel, "RGB + confidence overlay")
else:
rgb_panel = put_label(rgb_show, "RGB reference")
re_panel = put_label(re_show, "RE -> RGB plane")
nir_panel = put_label(nir_show, "NIR -> RGB plane")
depth_panel = put_label(stereo["disp_rgb_color"], "Stereo disparity RE/NIR -> RGB")
conf_panel = put_label(stereo["confidence_color"], "Spectral confidence / parallax risk")
s = stereo["stats"]
n_view += 1
now = time.time()
dt = now - t_fps
if dt >= 1.0:
fps_view = n_view / dt
n_view = 0
t_fps = now
hud = [
f"frame_id={frame_id} | FPS_VIEW={fps_view:.1f} | overlap={'ON' if mask_overlap else 'OFF'} | conf_overlay={'ON' if conf_overlay else 'OFF'}",
f"stereo valid={s['valid_pct']:.1f}% | conf_mean={s['conf_mean']:.2f} | disp p50={s['disp_p50']:.2f} | p02/p98={s['disp_p02']:.2f}/{s['disp_p98']:.2f}",
f"numDisp={s['num_disp']} | block={s['block_size']} | O overlap | C conf | [ ] numDisp | - + block | Q sair",
]
if msg and (time.time() - msg_t) < 2.0:
hud.append(msg)
if args.layout == "2x2":
canvas = make_2x2(
rgb_panel,
re_panel,
nir_panel,
depth_panel,
cell_w=args.cell_w,
cell_h=args.cell_h,
hud_lines=hud,
)
else:
canvas = make_3x2(
rgb_panel,
re_panel,
nir_panel,
depth_panel,
conf_panel,
put_label(stereo["disp_rect_color"], "Raw rectified disparity space"),
cell_w=args.cell_w,
cell_h=args.cell_h,
hud_lines=hud,
)
cv2.imshow(window_name, canvas)
except Exception as e:
err = np.zeros((500, 1200, 3), dtype=np.uint8)
cv2.putText(err, f"Erro: {e}", (20, 70), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2, cv2.LINE_AA)
cv2.imshow(window_name, err)
print(f"[ERRO FRAME] {e}")
k = cv2.waitKey(1) & 0xFF
if k in (ord("q"), ord("Q"), 27):
break
elif k in (ord("o"), ord("O")):
mask_overlap = not mask_overlap
msg = f"overlap mask -> {mask_overlap}"
msg_t = time.time()
elif k in (ord("c"), ord("C")):
conf_overlay = not conf_overlay
msg = f"confidence overlay -> {conf_overlay}"
msg_t = time.time()
elif k == ord("["):
args.num_disp = max(16, args.num_disp - 16)
msg = f"num_disp -> {args.num_disp}"
msg_t = time.time()
elif k == ord("]"):
args.num_disp = min(512, args.num_disp + 16)
msg = f"num_disp -> {args.num_disp}"
msg_t = time.time()
elif k in (ord("-"), ord("_")):
args.block_size = max(3, args.block_size - 2)
if args.block_size % 2 == 0:
args.block_size -= 1
msg = f"block_size -> {args.block_size}"
msg_t = time.time()
elif k in (ord("+"), ord("=")):
args.block_size = min(31, args.block_size + 2)
if args.block_size % 2 == 0:
args.block_size += 1
msg = f"block_size -> {args.block_size}"
msg_t = time.time()
finally:
cv2.destroyAllWindows()
print("Fim do preview live.")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,565 @@
import argparse
import json
import re
from pathlib import Path
import cv2
import numpy as np
# ============================================================
# RAW10
# ============================================================
def unpack_raw10_packed(raw: bytes, width: int, height: int) -> np.ndarray:
arr = np.frombuffer(raw, dtype=np.uint8)
pixel_count = width * height
expected_bytes = (pixel_count // 4) * 5
if pixel_count % 4 != 0:
raise RuntimeError(f"width*height precisa ser múltiplo de 4. Recebido: {pixel_count}")
if arr.size < expected_bytes:
raise RuntimeError(
f"RAW10 menor que esperado. bytes={arr.size}, esperado={expected_bytes}, "
f"width={width}, height={height}"
)
arr = arr[:expected_bytes]
groups = arr.reshape(-1, 5).astype(np.uint16)
p0 = (groups[:, 0] << 2) | ((groups[:, 4] >> 0) & 0x03)
p1 = (groups[:, 1] << 2) | ((groups[:, 4] >> 2) & 0x03)
p2 = (groups[:, 2] << 2) | ((groups[:, 4] >> 4) & 0x03)
p3 = (groups[:, 3] << 2) | ((groups[:, 4] >> 6) & 0x03)
out = np.empty(groups.shape[0] * 4, dtype=np.uint16)
out[0::4] = p0
out[1::4] = p1
out[2::4] = p2
out[3::4] = p3
return out.reshape(height, width)
def normalize_to_u8(img: np.ndarray, p_low=1.0, p_high=99.0) -> np.ndarray:
arr = img.astype(np.float32)
valid = np.isfinite(arr)
if np.count_nonzero(valid) < 20:
return np.zeros(arr.shape[:2], dtype=np.uint8)
vals = arr[valid]
lo = np.percentile(vals, p_low)
hi = np.percentile(vals, p_high)
out = (arr - lo) / (hi - lo + 1e-6)
out = np.clip(out, 0.0, 1.0)
return (out * 255).astype(np.uint8)
def raw10_bin_to_gray(path: Path, width: int, height: int, use_clahe=True) -> np.ndarray:
raw = path.read_bytes()
mono10 = unpack_raw10_packed(raw, width, height)
gray = normalize_to_u8(mono10)
if use_clahe:
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
gray = clahe.apply(gray)
return gray
# ============================================================
# Pairing CAM bins
# ============================================================
def clean_stem_for_pair(path: Path, cam_key: str):
s = path.stem
variants = [
cam_key,
cam_key.lower(),
cam_key.replace("_", ""),
cam_key.replace("_", "").lower(),
]
for v in variants:
s = s.replace(v, "")
s = re.sub(r"[_\-\s]+", "_", s).strip("_").lower()
return s
def find_cam_bins(root_dir: Path, cam_key: str):
return sorted([
p for p in root_dir.rglob("*.bin")
if cam_key.lower() in p.name.lower()
])
def pair_cam_bins(root_dir: Path, left_cam: str, right_cam: str):
left_bins = find_cam_bins(root_dir, left_cam)
right_bins = find_cam_bins(root_dir, right_cam)
right_by_folder_key = {}
right_by_key = {}
for rp in right_bins:
key = clean_stem_for_pair(rp, right_cam)
right_by_folder_key[(rp.parent, key)] = rp
right_by_key.setdefault(key, rp)
pairs = []
for lp in left_bins:
key = clean_stem_for_pair(lp, left_cam)
rp = right_by_folder_key.get((lp.parent, key))
if rp is None:
rp = right_by_key.get(key)
if rp is None:
same_folder = [r for r in right_bins if r.parent == lp.parent]
if len(same_folder) == 1:
rp = same_folder[0]
if rp is not None:
pairs.append((lp, rp))
if pairs:
return pairs, left_bins, right_bins, "name/key"
n = min(len(left_bins), len(right_bins))
return list(zip(left_bins[:n], right_bins[:n])), left_bins, right_bins, "order"
# ============================================================
# Calibration loading
# ============================================================
def load_stereo_calib(calib_path: Path):
data = np.load(str(calib_path), allow_pickle=True)
required = ["map1x", "map1y", "map2x", "map2y", "image_size"]
for k in required:
if k not in data:
raise RuntimeError(f"Calibração sem chave obrigatória: {k}")
calib = {
"map1x": data["map1x"],
"map1y": data["map1y"],
"map2x": data["map2x"],
"map2y": data["map2y"],
"image_size": tuple(data["image_size"].astype(int).tolist()),
}
for k in ["left_cam", "right_cam", "rms_left", "rms_right", "rms_stereo", "T"]:
if k in data:
calib[k] = data[k]
return calib
def rectify_pair(left_gray, right_gray, calib):
image_w, image_h = calib["image_size"]
if left_gray.shape[::-1] != (image_w, image_h):
left_gray = cv2.resize(left_gray, (image_w, image_h), interpolation=cv2.INTER_AREA)
if right_gray.shape[::-1] != (image_w, image_h):
right_gray = cv2.resize(right_gray, (image_w, image_h), interpolation=cv2.INTER_AREA)
left_rect = cv2.remap(
left_gray,
calib["map1x"],
calib["map1y"],
interpolation=cv2.INTER_LINEAR,
borderMode=cv2.BORDER_CONSTANT,
borderValue=0,
)
right_rect = cv2.remap(
right_gray,
calib["map2x"],
calib["map2y"],
interpolation=cv2.INTER_LINEAR,
borderMode=cv2.BORDER_CONSTANT,
borderValue=0,
)
return left_rect, right_rect
# ============================================================
# SGBM
# ============================================================
def make_sgbm(args):
num_disp = max(16, int(round(args.num_disp / 16)) * 16)
block_size = max(3, int(args.block_size))
if block_size % 2 == 0:
block_size += 1
matcher = cv2.StereoSGBM_create(
minDisparity=args.min_disp,
numDisparities=num_disp,
blockSize=block_size,
P1=8 * block_size * block_size,
P2=32 * block_size * block_size,
disp12MaxDiff=1,
uniquenessRatio=args.uniqueness,
speckleWindowSize=args.speckle_window,
speckleRange=args.speckle_range,
preFilterCap=63,
mode=cv2.STEREO_SGBM_MODE_SGBM_3WAY,
)
return matcher, num_disp, block_size
def compute_disparity(left_rect, right_rect, args):
matcher, num_disp, block_size = make_sgbm(args)
disp_raw = matcher.compute(left_rect, right_rect).astype(np.float32) / 16.0
valid = disp_raw > args.min_valid_disp
disp_vis = disp_raw.copy()
disp_vis[~valid] = 0.0
if np.count_nonzero(valid) > 20:
vals = disp_vis[valid]
p2 = np.percentile(vals, 2)
p98 = np.percentile(vals, 98)
disp_norm = (disp_vis - p2) / (p98 - p2 + 1e-6)
disp_norm = np.clip(disp_norm, 0.0, 1.0)
else:
disp_norm = np.zeros_like(disp_vis, dtype=np.float32)
disp_color = cv2.applyColorMap((disp_norm * 255).astype(np.uint8), cv2.COLORMAP_TURBO)
valid_mask = np.zeros_like(disp_color)
valid_mask[valid] = (255, 255, 255)
stats = {
"num_disp": num_disp,
"block_size": block_size,
"valid_pct": float(np.mean(valid) * 100.0),
"disp_p05": float(np.percentile(disp_vis[valid], 5)) if np.count_nonzero(valid) > 20 else 0.0,
"disp_p50": float(np.percentile(disp_vis[valid], 50)) if np.count_nonzero(valid) > 20 else 0.0,
"disp_p95": float(np.percentile(disp_vis[valid], 95)) if np.count_nonzero(valid) > 20 else 0.0,
}
return disp_raw, disp_color, valid_mask, valid, stats
# ============================================================
# Visualization
# ============================================================
def draw_epipolar_lines(img_bgr, step=40):
out = img_bgr.copy()
h, w = out.shape[:2]
for y in range(0, h, step):
color = (0, 255, 255) if (y // step) % 2 == 0 else (255, 255, 0)
cv2.line(out, (0, y), (w, y), color, 1, cv2.LINE_AA)
return out
def resize_to_height(img, target_h):
h, w = img.shape[:2]
if h == target_h:
return img
scale = target_h / h
new_w = max(1, int(w * scale))
return cv2.resize(img, (new_w, target_h), interpolation=cv2.INTER_AREA)
def draw_header(canvas, lines):
header_h = 24 + 24 * len(lines)
cv2.rectangle(canvas, (0, 0), (canvas.shape[1], header_h), (0, 0, 0), -1)
y = 24
for line in lines:
cv2.putText(
canvas,
line,
(12, y),
cv2.FONT_HERSHEY_SIMPLEX,
0.55,
(255, 255, 255),
1,
cv2.LINE_AA,
)
y += 24
return canvas
def make_overlay(left_bgr, disp_color, alpha=0.45):
disp_resized = cv2.resize(
disp_color,
(left_bgr.shape[1], left_bgr.shape[0]),
interpolation=cv2.INTER_AREA,
)
return cv2.addWeighted(left_bgr, 1.0 - alpha, disp_resized, alpha, 0)
def compose_view(left_rect, right_rect, disp_color, valid_mask, pair, idx, total, mode, stats, args):
left_bgr = cv2.cvtColor(left_rect, cv2.COLOR_GRAY2BGR)
right_bgr = cv2.cvtColor(right_rect, cv2.COLOR_GRAY2BGR)
if args.lines:
left_bgr = draw_epipolar_lines(left_bgr, step=args.line_step)
right_bgr = draw_epipolar_lines(right_bgr, step=args.line_step)
if mode == "disp":
third = disp_color
mode_name = "disparity"
elif mode == "mask":
third = valid_mask
mode_name = "valid mask"
elif mode == "overlay":
third = make_overlay(cv2.cvtColor(left_rect, cv2.COLOR_GRAY2BGR), disp_color)
mode_name = "overlay"
else:
diff = cv2.absdiff(left_rect, right_rect)
third = cv2.cvtColor(diff, cv2.COLOR_GRAY2BGR)
mode_name = "rect diff"
left = resize_to_height(left_bgr, args.view_h)
right = resize_to_height(right_bgr, args.view_h)
third = resize_to_height(third, args.view_h)
h = min(left.shape[0], right.shape[0], third.shape[0])
left = left[:h]
right = right[:h]
third = third[:h]
canvas = np.hstack([left, right, third])
lp, rp = pair
lines = [
f"{idx + 1}/{total} | L={lp.name} | R={rp.name}",
f"mode={mode_name} | valid={stats['valid_pct']:.1f}% | disp p05={stats['disp_p05']:.2f} p50={stats['disp_p50']:.2f} p95={stats['disp_p95']:.2f}",
f"numDisp={stats['num_disp']} | block={stats['block_size']} | uniqueness={args.uniqueness} | lines={args.lines}",
"N/SPACE prox | A ant | M modo | L linhas | [ ] numDisp | - + block | S salvar | Q sair",
]
return draw_header(canvas, lines)
# ============================================================
# Main
# ============================================================
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--root_dir", required=True)
parser.add_argument("--calib_path", required=True)
parser.add_argument("--left_cam", default="CAM_C")
parser.add_argument("--right_cam", default="CAM_B")
parser.add_argument("--width", type=int, default=1280)
parser.add_argument("--height", type=int, default=800)
parser.add_argument("--view_h", type=int, default=480)
parser.add_argument("--num_disp", type=int, default=128)
parser.add_argument("--block_size", type=int, default=7)
parser.add_argument("--min_disp", type=int, default=0)
parser.add_argument("--min_valid_disp", type=float, default=1.0)
parser.add_argument("--uniqueness", type=int, default=8)
parser.add_argument("--speckle_window", type=int, default=80)
parser.add_argument("--speckle_range", type=int, default=2)
parser.add_argument("--no_clahe", action="store_true")
parser.add_argument("--lines", action="store_true")
parser.add_argument("--line_step", type=int, default=40)
parser.add_argument("--save_dir", default="stereo_rectified_sgbm_saves")
args = parser.parse_args()
root_dir = Path(args.root_dir)
calib_path = Path(args.calib_path)
save_dir = Path(args.save_dir)
save_dir.mkdir(parents=True, exist_ok=True)
calib = load_stereo_calib(calib_path)
print(f"[INFO] calib_path: {calib_path}")
print(f"[INFO] calib image_size: {calib['image_size']}")
if "rms_left" in calib:
print(f"[INFO] rms_left: {float(calib['rms_left']):.6f}")
if "rms_right" in calib:
print(f"[INFO] rms_right: {float(calib['rms_right']):.6f}")
if "rms_stereo" in calib:
print(f"[INFO] rms_stereo: {float(calib['rms_stereo']):.6f}")
if "T" in calib:
print(f"[INFO] T: {np.array(calib['T']).ravel()}")
pairs, left_bins, right_bins, pair_mode = pair_cam_bins(root_dir, args.left_cam, args.right_cam)
print(f"[INFO] root_dir: {root_dir}")
print(f"[INFO] left_cam: {args.left_cam} | arquivos: {len(left_bins)}")
print(f"[INFO] right_cam: {args.right_cam} | arquivos: {len(right_bins)}")
print(f"[INFO] pares: {len(pairs)}")
print(f"[INFO] pareamento: {pair_mode}")
if not pairs:
raise RuntimeError("Nenhum par encontrado.")
idx = 0
mode = "disp"
cached_key = None
cached_data = None
cv2.namedWindow("Stereo RAW10 Rectified SGBM Viewer", cv2.WINDOW_NORMAL)
while True:
pair = pairs[idx]
lp, rp = pair
key_cache = (
str(lp),
str(rp),
args.num_disp,
args.block_size,
args.uniqueness,
args.speckle_window,
args.speckle_range,
args.min_disp,
args.min_valid_disp,
args.no_clahe,
args.width,
args.height,
str(args.calib_path),
)
if key_cache != cached_key:
print(f"[RUN] {idx + 1}/{len(pairs)} - L={lp.name} | R={rp.name}")
try:
left_gray = raw10_bin_to_gray(
lp,
width=args.width,
height=args.height,
use_clahe=not args.no_clahe,
)
right_gray = raw10_bin_to_gray(
rp,
width=args.width,
height=args.height,
use_clahe=not args.no_clahe,
)
left_rect, right_rect = rectify_pair(left_gray, right_gray, calib)
_, disp_color, valid_mask, valid, stats = compute_disparity(left_rect, right_rect, args)
cached_data = (left_rect, right_rect, disp_color, valid_mask, stats)
cached_key = key_cache
except Exception as e:
print(f"[ERRO] Falha processando par:")
print(f" L={lp}")
print(f" R={rp}")
print(f" erro={e}")
idx = min(idx + 1, len(pairs) - 1)
cached_key = None
cached_data = None
continue
else:
left_rect, right_rect, disp_color, valid_mask, stats = cached_data
view = compose_view(
left_rect=left_rect,
right_rect=right_rect,
disp_color=disp_color,
valid_mask=valid_mask,
pair=pair,
idx=idx,
total=len(pairs),
mode=mode,
stats=stats,
args=args,
)
cv2.imshow("Stereo RAW10 Rectified SGBM Viewer", view)
key = cv2.waitKey(0) & 0xFF
if key in [27, ord("q"), ord("Q")]:
break
elif key in [ord("n"), ord("N"), 32]:
idx = min(idx + 1, len(pairs) - 1)
cached_key = None
elif key in [ord("a"), ord("A")]:
idx = max(idx - 1, 0)
cached_key = None
elif key in [ord("m"), ord("M")]:
if mode == "disp":
mode = "mask"
elif mode == "mask":
mode = "overlay"
elif mode == "overlay":
mode = "diff"
else:
mode = "disp"
elif key in [ord("l"), ord("L")]:
args.lines = not args.lines
print(f"[PARAM] lines={args.lines}")
elif key == ord("["):
args.num_disp = max(16, args.num_disp - 16)
cached_key = None
print(f"[PARAM] num_disp={args.num_disp}")
elif key == ord("]"):
args.num_disp = min(512, args.num_disp + 16)
cached_key = None
print(f"[PARAM] num_disp={args.num_disp}")
elif key in [ord("-"), ord("_")]:
args.block_size = max(3, args.block_size - 2)
if args.block_size % 2 == 0:
args.block_size -= 1
cached_key = None
print(f"[PARAM] block_size={args.block_size}")
elif key in [ord("+"), ord("=")]:
args.block_size = min(31, args.block_size + 2)
if args.block_size % 2 == 0:
args.block_size += 1
cached_key = None
print(f"[PARAM] block_size={args.block_size}")
elif key in [ord("s"), ord("S")]:
out_path = save_dir / f"stereo_rectified_{idx:04d}_{mode}.png"
cv2.imwrite(str(out_path), view)
print(f"[SAVE] {out_path}")
cv2.destroyAllWindows()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,786 @@
import argparse
import re
from pathlib import Path
import cv2
import numpy as np
# ============================================================
# RAW10 / image conversion
# ============================================================
def unpack_raw10_packed(raw: bytes, width: int, height: int) -> np.ndarray:
arr = np.frombuffer(raw, dtype=np.uint8)
pixel_count = width * height
expected_bytes = (pixel_count // 4) * 5
if pixel_count % 4 != 0:
raise RuntimeError(f"width*height precisa ser múltiplo de 4. Recebido: {pixel_count}")
if arr.size < expected_bytes:
raise RuntimeError(
f"RAW10 menor que esperado. bytes={arr.size}, esperado={expected_bytes}, "
f"width={width}, height={height}"
)
arr = arr[:expected_bytes]
groups = arr.reshape(-1, 5).astype(np.uint16)
p0 = (groups[:, 0] << 2) | ((groups[:, 4] >> 0) & 0x03)
p1 = (groups[:, 1] << 2) | ((groups[:, 4] >> 2) & 0x03)
p2 = (groups[:, 2] << 2) | ((groups[:, 4] >> 4) & 0x03)
p3 = (groups[:, 3] << 2) | ((groups[:, 4] >> 6) & 0x03)
out = np.empty(groups.shape[0] * 4, dtype=np.uint16)
out[0::4] = p0
out[1::4] = p1
out[2::4] = p2
out[3::4] = p3
return out.reshape(height, width)
def normalize_to_u8(img: np.ndarray, p_low=1.0, p_high=99.0) -> np.ndarray:
arr = img.astype(np.float32)
valid = np.isfinite(arr)
if np.count_nonzero(valid) < 20:
return np.zeros(arr.shape[:2], dtype=np.uint8)
vals = arr[valid]
lo = np.percentile(vals, p_low)
hi = np.percentile(vals, p_high)
out = (arr - lo) / (hi - lo + 1e-6)
out = np.clip(out, 0.0, 1.0)
return (out * 255).astype(np.uint8)
def debayer_raw10_to_bgr_u8(raw10: np.ndarray, bayer: str) -> np.ndarray:
gray_u8 = normalize_to_u8(raw10)
bayer = bayer.upper()
code_map = {
"RGGB": cv2.COLOR_BayerRG2BGR,
"BGGR": cv2.COLOR_BayerBG2BGR,
"GRBG": cv2.COLOR_BayerGR2BGR,
"GBRG": cv2.COLOR_BayerGB2BGR,
}
if bayer not in code_map:
raise RuntimeError(f"Bayer pattern não suportado: {bayer}")
return cv2.cvtColor(gray_u8, code_map[bayer])
def read_raw10_rgb_bgr(path: Path, width: int, height: int, bayer: str) -> np.ndarray:
raw = path.read_bytes()
raw10 = unpack_raw10_packed(raw, width, height)
return debayer_raw10_to_bgr_u8(raw10, bayer=bayer)
def read_raw10_rgb_view_bgr(
path: Path,
width: int,
height: int,
bayer: str,
rgb_view: str,
use_clahe: bool = True,
) -> np.ndarray:
"""
Carrega CAM_A/RGB em três modos:
color:
RAW10 Bayer -> debayer BGR -> visual colorido.
gray:
RAW10 Bayer -> debayer BGR -> grayscale -> CLAHE -> BGR fake.
Este é o mais parecido com o caminho usado na calibração ChArUco.
raw_bayer_gray:
RAW10 Bayer -> normalize direto -> CLAHE -> BGR fake.
Não faz debayer; útil para testar se a interpolação do debayer está influenciando.
"""
raw = path.read_bytes()
raw10 = unpack_raw10_packed(raw, width, height)
mode = rgb_view.lower().strip()
if mode == "color":
return debayer_raw10_to_bgr_u8(raw10, bayer=bayer)
if mode == "gray":
bgr = debayer_raw10_to_bgr_u8(raw10, bayer=bayer)
gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY)
elif mode == "raw_bayer_gray":
gray = normalize_to_u8(raw10)
else:
raise RuntimeError(
f"rgb_view inválido: {rgb_view}. Use: color, gray ou raw_bayer_gray"
)
if use_clahe:
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
gray = clahe.apply(gray)
return cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR)
def read_raw10_mono_bgr(path: Path, width: int, height: int, use_clahe=True) -> np.ndarray:
raw = path.read_bytes()
raw10 = unpack_raw10_packed(raw, width, height)
gray = normalize_to_u8(raw10)
if use_clahe:
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
gray = clahe.apply(gray)
return cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR)
def to_gray_u8(img_bgr: np.ndarray) -> np.ndarray:
if img_bgr.ndim == 2:
return img_bgr
return cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)
# ============================================================
# Triplet pairing
# ============================================================
def clean_stem_for_pair(path: Path, cam_key: str):
s = path.stem
variants = [
cam_key,
cam_key.lower(),
cam_key.replace("_", ""),
cam_key.replace("_", "").lower(),
]
for v in variants:
s = s.replace(v, "")
s = re.sub(r"[_\-\s]+", "_", s).strip("_").lower()
return s
def find_cam_bins(root_dir: Path, cam_key: str):
return sorted([p for p in root_dir.rglob("*.bin") if cam_key.lower() in p.name.lower()])
def find_triplets(root_dir: Path, cams: list[str]):
by_cam = {cam: find_cam_bins(root_dir, cam) for cam in cams}
key_maps = {}
for cam, paths in by_cam.items():
m = {}
for p in paths:
key = clean_stem_for_pair(p, cam)
m[(p.parent, key)] = p
m.setdefault((None, key), p)
key_maps[cam] = m
ref_cam = cams[0]
triplets = []
for ref_path in by_cam[ref_cam]:
key = clean_stem_for_pair(ref_path, ref_cam)
folder = ref_path.parent
item = {ref_cam: ref_path}
ok = True
for cam in cams[1:]:
p = key_maps[cam].get((folder, key)) or key_maps[cam].get((None, key))
if p is None:
same_folder = [x for x in by_cam[cam] if x.parent == folder]
if len(same_folder) == 1:
p = same_folder[0]
if p is None:
ok = False
break
item[cam] = p
if ok:
triplets.append(item)
return triplets, by_cam
# ============================================================
# Calibration loading
# ============================================================
def scalar_str(x):
arr = np.array(x)
if arr.shape == ():
return str(arr.item())
return str(x)
def load_multicam_calib(calib_path: Path):
data = np.load(str(calib_path), allow_pickle=True)
keys = set(data.files)
required = ["image_size", "rgb_cam", "nir_cam", "re_cam", "ref_cam"]
for k in required:
if k not in keys:
raise RuntimeError(f"Calibração multicam sem chave obrigatória: {k}")
calib = {
"data": data,
"keys": keys,
"image_size": tuple(data["image_size"].astype(int).tolist()),
"rgb_cam": scalar_str(data["rgb_cam"]),
"nir_cam": scalar_str(data["nir_cam"]),
"re_cam": scalar_str(data["re_cam"]),
"ref_cam": scalar_str(data["ref_cam"]),
}
return calib
def get_pair_prefix(calib, cam1: str, cam2: str):
keys = calib["keys"]
direct = f"pair_{cam1}_{cam2}"
inv = f"pair_{cam2}_{cam1}"
if f"{direct}_map1x" in keys:
return direct, False
if f"{inv}_map1x" in keys:
return inv, True
raise RuntimeError(f"Par {cam1}<->{cam2} não encontrado no .npz")
def rectify_pair_from_calib(img1_bgr, img2_bgr, calib, cam1: str, cam2: str):
"""
Retifica duas imagens usando o par salvo no .npz.
Retorna imagens na ordem solicitada: cam1_rect, cam2_rect.
Se o par salvo estiver invertido, troca map1/map2 automaticamente.
"""
data = calib["data"]
image_w, image_h = calib["image_size"]
if img1_bgr.shape[1] != image_w or img1_bgr.shape[0] != image_h:
img1_bgr = cv2.resize(img1_bgr, (image_w, image_h), interpolation=cv2.INTER_AREA)
if img2_bgr.shape[1] != image_w or img2_bgr.shape[0] != image_h:
img2_bgr = cv2.resize(img2_bgr, (image_w, image_h), interpolation=cv2.INTER_AREA)
prefix, inverted = get_pair_prefix(calib, cam1, cam2)
if not inverted:
map1x = data[f"{prefix}_map1x"]
map1y = data[f"{prefix}_map1y"]
map2x = data[f"{prefix}_map2x"]
map2y = data[f"{prefix}_map2y"]
else:
map1x = data[f"{prefix}_map2x"]
map1y = data[f"{prefix}_map2y"]
map2x = data[f"{prefix}_map1x"]
map2y = data[f"{prefix}_map1y"]
rect1 = cv2.remap(
img1_bgr,
map1x,
map1y,
interpolation=cv2.INTER_LINEAR,
borderMode=cv2.BORDER_CONSTANT,
borderValue=0,
)
rect2 = cv2.remap(
img2_bgr,
map2x,
map2y,
interpolation=cv2.INTER_LINEAR,
borderMode=cv2.BORDER_CONSTANT,
borderValue=0,
)
return rect1, rect2
# ============================================================
# Disparity RE/NIR from multicam npz
# ============================================================
def make_sgbm(args):
num_disp = max(16, int(round(args.num_disp / 16)) * 16)
block_size = max(3, int(args.block_size))
if block_size % 2 == 0:
block_size += 1
matcher = cv2.StereoSGBM_create(
minDisparity=args.min_disp,
numDisparities=num_disp,
blockSize=block_size,
P1=8 * block_size * block_size,
P2=32 * block_size * block_size,
disp12MaxDiff=1,
uniquenessRatio=args.uniqueness,
speckleWindowSize=args.speckle_window,
speckleRange=args.speckle_range,
preFilterCap=63,
mode=cv2.STEREO_SGBM_MODE_SGBM_3WAY,
)
return matcher, num_disp, block_size
def compute_disparity(left_rect_bgr, right_rect_bgr, args):
left_gray = to_gray_u8(left_rect_bgr)
right_gray = to_gray_u8(right_rect_bgr)
matcher, num_disp, block_size = make_sgbm(args)
disp_raw = matcher.compute(left_gray, right_gray).astype(np.float32) / 16.0
valid = disp_raw > args.min_valid_disp
disp_vis = disp_raw.copy()
disp_vis[~valid] = 0.0
if np.count_nonzero(valid) > 20:
vals = disp_vis[valid]
p2 = np.percentile(vals, 2)
p98 = np.percentile(vals, 98)
disp_norm = (disp_vis - p2) / (p98 - p2 + 1e-6)
disp_norm = np.clip(disp_norm, 0.0, 1.0)
else:
disp_norm = np.zeros_like(disp_vis, dtype=np.float32)
disp_color = cv2.applyColorMap((disp_norm * 255).astype(np.uint8), cv2.COLORMAP_TURBO)
valid_mask = np.zeros_like(disp_color)
valid_mask[valid] = (255, 255, 255)
stats = {
"num_disp": num_disp,
"block_size": block_size,
"valid_pct": float(np.mean(valid) * 100.0),
"disp_p05": float(np.percentile(disp_vis[valid], 5)) if np.count_nonzero(valid) > 20 else 0.0,
"disp_p50": float(np.percentile(disp_vis[valid], 50)) if np.count_nonzero(valid) > 20 else 0.0,
"disp_p95": float(np.percentile(disp_vis[valid], 95)) if np.count_nonzero(valid) > 20 else 0.0,
}
return disp_raw, disp_color, valid_mask, stats
# ============================================================
# Visualization
# ============================================================
def draw_epipolar_lines(img_bgr, step=40):
out = img_bgr.copy()
h, w = out.shape[:2]
for y in range(0, h, step):
color = (0, 255, 255) if (y // step) % 2 == 0 else (255, 255, 0)
cv2.line(out, (0, y), (w, y), color, 1, cv2.LINE_AA)
return out
def resize_to_height(img, target_h):
h, w = img.shape[:2]
if h == target_h:
return img
scale = target_h / h
new_w = max(1, int(w * scale))
return cv2.resize(img, (new_w, target_h), interpolation=cv2.INTER_AREA)
def draw_header(canvas, lines):
header_h = 24 + 24 * len(lines)
cv2.rectangle(canvas, (0, 0), (canvas.shape[1], header_h), (0, 0, 0), -1)
y = 24
for line in lines:
cv2.putText(
canvas,
line,
(12, y),
cv2.FONT_HERSHEY_SIMPLEX,
0.55,
(255, 255, 255),
1,
cv2.LINE_AA,
)
y += 24
return canvas
def put_label(img, text, color=(255, 255, 255)):
out = img.copy()
cv2.rectangle(out, (0, 0), (out.shape[1], 34), (0, 0, 0), -1)
cv2.putText(out, text, (10, 24), cv2.FONT_HERSHEY_SIMPLEX, 0.65, color, 1, cv2.LINE_AA)
return out
def compose_three(left_bgr, center_bgr, right_bgr, title_lines, args):
if args.lines:
left_bgr = draw_epipolar_lines(left_bgr, step=args.line_step)
center_bgr = draw_epipolar_lines(center_bgr, step=args.line_step)
right_bgr = draw_epipolar_lines(right_bgr, step=args.line_step)
left = resize_to_height(left_bgr, args.view_h)
center = resize_to_height(center_bgr, args.view_h)
right = resize_to_height(right_bgr, args.view_h)
h = min(left.shape[0], center.shape[0], right.shape[0])
left = left[:h]
center = center[:h]
right = right[:h]
canvas = np.hstack([left, center, right])
return draw_header(canvas, title_lines)
def compose_four(a_bgr, b_bgr, c_bgr, d_bgr, title_lines, args):
if args.lines:
a_bgr = draw_epipolar_lines(a_bgr, step=args.line_step)
b_bgr = draw_epipolar_lines(b_bgr, step=args.line_step)
c_bgr = draw_epipolar_lines(c_bgr, step=args.line_step)
d_bgr = draw_epipolar_lines(d_bgr, step=args.line_step)
imgs = [resize_to_height(x, args.view_h) for x in [a_bgr, b_bgr, c_bgr, d_bgr]]
h = min(x.shape[0] for x in imgs)
imgs = [x[:h] for x in imgs]
canvas = np.hstack(imgs)
return draw_header(canvas, title_lines)
def absdiff_bgr(a, b):
ag = to_gray_u8(a)
bg = to_gray_u8(b)
diff = cv2.absdiff(ag, bg)
return cv2.cvtColor(diff, cv2.COLOR_GRAY2BGR)
def make_overlay(base_bgr, layer_bgr, alpha=0.45):
layer = cv2.resize(layer_bgr, (base_bgr.shape[1], base_bgr.shape[0]), interpolation=cv2.INTER_AREA)
return cv2.addWeighted(base_bgr, 1.0 - alpha, layer, alpha, 0)
# ============================================================
# Frame loading and modes
# ============================================================
def load_triplet_images(item, calib, args):
rgb_cam = calib["rgb_cam"]
nir_cam = calib["nir_cam"]
re_cam = calib["re_cam"]
rgb = read_raw10_rgb_view_bgr(
item[rgb_cam],
args.width,
args.height,
args.rgb_bayer,
args.rgb_view,
use_clahe=not args.no_clahe,
)
nir = read_raw10_mono_bgr(item[nir_cam], args.width, args.height, use_clahe=not args.no_clahe)
re = read_raw10_mono_bgr(item[re_cam], args.width, args.height, use_clahe=not args.no_clahe)
return {
rgb_cam: rgb,
nir_cam: nir,
re_cam: re,
}
def build_views(images, calib, args):
rgb_cam = calib["rgb_cam"]
nir_cam = calib["nir_cam"]
re_cam = calib["re_cam"]
rgb = images[rgb_cam]
nir = images[nir_cam]
re = images[re_cam]
# RGB <-> NIR
rgb_ab, nir_ab = rectify_pair_from_calib(rgb, nir, calib, rgb_cam, nir_cam)
# RGB <-> RE
rgb_ac, re_ac = rectify_pair_from_calib(rgb, re, calib, rgb_cam, re_cam)
# RE <-> NIR
re_cb, nir_cb = rectify_pair_from_calib(re, nir, calib, re_cam, nir_cam)
_, disp_color, valid_mask, disp_stats = compute_disparity(re_cb, nir_cb, args)
return {
"rgb_native": rgb,
"nir_native": nir,
"re_native": re,
"rgb_ab": rgb_ab,
"nir_ab": nir_ab,
"rgb_ac": rgb_ac,
"re_ac": re_ac,
"re_cb": re_cb,
"nir_cb": nir_cb,
"disp_color": disp_color,
"valid_mask": valid_mask,
"disp_stats": disp_stats,
}
def compose_mode(views, item, idx, total, mode, calib, args):
rgb_cam = calib["rgb_cam"]
nir_cam = calib["nir_cam"]
re_cam = calib["re_cam"]
rgb_name = item[rgb_cam].name
if mode == "triple_native":
left = put_label(views["re_native"], "RE native")
center = put_label(views["rgb_native"], "RGB native REF")
right = put_label(views["nir_native"], "NIR native")
lines = [
f"{idx + 1}/{total} | {rgb_name}",
"mode=triple_native | sem retificação, RGB no centro",
"N/SPACE prox | A ant | M modo | L linhas | S salvar | Q sair",
]
return compose_three(left, center, right, lines, args)
if mode == "rgb_re":
left = put_label(views["re_ac"], "RE rectificado no par RGB-RE")
center = put_label(views["rgb_ac"], "RGB rectificado no par RGB-RE")
right = absdiff_bgr(views["rgb_ac"], views["re_ac"])
right = put_label(right, "diff RGB-RE")
lines = [
f"{idx + 1}/{total} | {rgb_name}",
"mode=rgb_re | valida alinhamento epipolar RGB<->RE",
"N/SPACE prox | A ant | M modo | L linhas | S salvar | Q sair",
]
return compose_three(left, center, right, lines, args)
if mode == "rgb_nir":
left = absdiff_bgr(views["rgb_ab"], views["nir_ab"])
left = put_label(left, "diff RGB-NIR")
center = put_label(views["rgb_ab"], "RGB rectificado no par RGB-NIR")
right = put_label(views["nir_ab"], "NIR rectificado no par RGB-NIR")
lines = [
f"{idx + 1}/{total} | {rgb_name}",
"mode=rgb_nir | valida alinhamento epipolar RGB<->NIR",
"N/SPACE prox | A ant | M modo | L linhas | S salvar | Q sair",
]
return compose_three(left, center, right, lines, args)
if mode == "re_nir":
left = put_label(views["re_cb"], "RE rectificado no par RE-NIR")
center = put_label(views["disp_color"], "disparity RE-NIR")
right = put_label(views["nir_cb"], "NIR rectificado no par RE-NIR")
st = views["disp_stats"]
lines = [
f"{idx + 1}/{total} | {rgb_name}",
f"mode=re_nir | valid={st['valid_pct']:.1f}% | disp p05={st['disp_p05']:.2f} p50={st['disp_p50']:.2f} p95={st['disp_p95']:.2f}",
f"numDisp={st['num_disp']} | block={st['block_size']} | linhas={args.lines}",
"N/SPACE prox | A ant | M modo | L linhas | [ ] numDisp | - + block | S salvar | Q sair",
]
return compose_three(left, center, right, lines, args)
if mode == "quad_pairs":
a = put_label(views["re_ac"], "RE em RGB-RE")
b = put_label(views["rgb_ac"], "RGB em RGB-RE")
c = put_label(views["rgb_ab"], "RGB em RGB-NIR")
d = put_label(views["nir_ab"], "NIR em RGB-NIR")
lines = [
f"{idx + 1}/{total} | {rgb_name}",
"mode=quad_pairs | mostra os dois mundos retificados que usam RGB",
"N/SPACE prox | A ant | M modo | L linhas | S salvar | Q sair",
]
return compose_four(a, b, c, d, lines, args)
raise RuntimeError(f"Modo desconhecido: {mode}")
# ============================================================
# Main
# ============================================================
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--root_dir", required=True)
parser.add_argument("--calib_path", required=True)
parser.add_argument("--width", type=int, default=1280)
parser.add_argument("--height", type=int, default=800)
parser.add_argument("--rgb_bayer", default="BGGR", help="Use o mesmo padrão que funcionou na calibração. Ex: BGGR ou RGGB")
parser.add_argument(
"--rgb_view",
default="gray",
choices=["color", "gray", "raw_bayer_gray"],
help="Como mostrar/processar CAM_A no viewer. gray replica melhor a calibração.",
)
parser.add_argument("--view_h", type=int, default=420)
parser.add_argument("--no_clahe", action="store_true")
parser.add_argument("--lines", action="store_true")
parser.add_argument("--line_step", type=int, default=40)
parser.add_argument("--num_disp", type=int, default=128)
parser.add_argument("--block_size", type=int, default=7)
parser.add_argument("--min_disp", type=int, default=0)
parser.add_argument("--min_valid_disp", type=float, default=1.0)
parser.add_argument("--uniqueness", type=int, default=8)
parser.add_argument("--speckle_window", type=int, default=80)
parser.add_argument("--speckle_range", type=int, default=2)
parser.add_argument("--save_dir", default="multicam_rectified_viewer_saves")
args = parser.parse_args()
root_dir = Path(args.root_dir)
calib_path = Path(args.calib_path)
save_dir = Path(args.save_dir)
save_dir.mkdir(parents=True, exist_ok=True)
calib = load_multicam_calib(calib_path)
rgb_cam = calib["rgb_cam"]
nir_cam = calib["nir_cam"]
re_cam = calib["re_cam"]
cams = [rgb_cam, nir_cam, re_cam]
print(f"[INFO] calib_path: {calib_path}")
print(f"[INFO] image_size: {calib['image_size']}")
print(f"[INFO] rgb_cam={rgb_cam} nir_cam={nir_cam} re_cam={re_cam} ref_cam={calib['ref_cam']}")
for k in calib["keys"]:
if k.startswith("pair_") and k.endswith("_rms"):
print(f"[INFO] {k}: {float(calib['data'][k]):.6f}")
if k.startswith("pair_") and k.endswith("_T"):
print(f"[INFO] {k}: {np.array(calib['data'][k]).ravel()}")
triplets, by_cam = find_triplets(root_dir, cams)
for cam in cams:
print(f"[INFO] arquivos {cam}: {len(by_cam[cam])}")
print(f"[INFO] triplets: {len(triplets)}")
if not triplets:
raise RuntimeError("Nenhum triplet CAM_A/CAM_B/CAM_C encontrado.")
modes = ["triple_native", "rgb_re", "rgb_nir", "re_nir", "quad_pairs"]
mode_idx = 0
idx = 0
cached_key = None
cached_views = None
cv2.namedWindow("Multicam RAW10 Rectified Viewer", cv2.WINDOW_NORMAL)
while True:
item = triplets[idx]
mode = modes[mode_idx]
key_cache = (
tuple(str(item[cam]) for cam in cams),
args.width,
args.height,
args.rgb_bayer,
args.rgb_view,
args.no_clahe,
str(calib_path),
args.num_disp,
args.block_size,
args.min_disp,
args.min_valid_disp,
args.uniqueness,
args.speckle_window,
args.speckle_range,
)
if key_cache != cached_key:
print(f"[RUN] {idx + 1}/{len(triplets)} - {item[rgb_cam].name}")
try:
images = load_triplet_images(item, calib, args)
cached_views = build_views(images, calib, args)
cached_key = key_cache
except Exception as e:
print("[ERRO] Falha processando triplet:")
for cam in cams:
print(f" {cam}={item[cam]}")
print(f" erro={e}")
idx = min(idx + 1, len(triplets) - 1)
cached_key = None
cached_views = None
continue
view = compose_mode(
views=cached_views,
item=item,
idx=idx,
total=len(triplets),
mode=mode,
calib=calib,
args=args,
)
cv2.imshow("Multicam RAW10 Rectified Viewer", view)
key = cv2.waitKey(0) & 0xFF
if key in [27, ord("q"), ord("Q")]:
break
elif key in [ord("n"), ord("N"), 32]:
idx = min(idx + 1, len(triplets) - 1)
cached_key = None
elif key in [ord("a"), ord("A")]:
idx = max(idx - 1, 0)
cached_key = None
elif key in [ord("m"), ord("M")]:
mode_idx = (mode_idx + 1) % len(modes)
print(f"[PARAM] mode={modes[mode_idx]}")
elif key in [ord("l"), ord("L")]:
args.lines = not args.lines
print(f"[PARAM] lines={args.lines}")
elif key == ord("["):
args.num_disp = max(16, args.num_disp - 16)
cached_key = None
print(f"[PARAM] num_disp={args.num_disp}")
elif key == ord("]"):
args.num_disp = min(512, args.num_disp + 16)
cached_key = None
print(f"[PARAM] num_disp={args.num_disp}")
elif key in [ord("-"), ord("_")]:
args.block_size = max(3, args.block_size - 2)
if args.block_size % 2 == 0:
args.block_size -= 1
cached_key = None
print(f"[PARAM] block_size={args.block_size}")
elif key in [ord("+"), ord("=")]:
args.block_size = min(31, args.block_size + 2)
if args.block_size % 2 == 0:
args.block_size += 1
cached_key = None
print(f"[PARAM] block_size={args.block_size}")
elif key in [ord("s"), ord("S")]:
out_path = save_dir / f"multicam_{idx:04d}_{mode}.png"
cv2.imwrite(str(out_path), view)
print(f"[SAVE] {out_path}")
cv2.destroyAllWindows()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,638 @@
import argparse
import json
import re
from pathlib import Path
import cv2
import numpy as np
IMG_EXTS = {".png", ".jpg", ".jpeg", ".bmp", ".webp"}
# ============================================================
# RAW10 unpack
# ============================================================
def unpack_raw10_packed(raw: bytes, width: int, height: int) -> np.ndarray:
"""
Desempacota RAW10 packed padrão:
a cada 5 bytes = 4 pixels de 10 bits.
Layout comum:
b0 = p0[9:2]
b1 = p1[9:2]
b2 = p2[9:2]
b3 = p3[9:2]
b4 = p0[1:0] | p1[1:0]<<2 | p2[1:0]<<4 | p3[1:0]<<6
Retorna uint16 HxW com valores 0..1023.
"""
arr = np.frombuffer(raw, dtype=np.uint8)
expected_groups = (width * height) // 4
expected_bytes = expected_groups * 5
if arr.size < expected_bytes:
raise RuntimeError(
f"RAW10 menor que esperado. bytes={arr.size}, esperado={expected_bytes}, "
f"width={width}, height={height}"
)
arr = arr[:expected_bytes]
groups = arr.reshape(-1, 5).astype(np.uint16)
p0 = (groups[:, 0] << 2) | ((groups[:, 4] >> 0) & 0x03)
p1 = (groups[:, 1] << 2) | ((groups[:, 4] >> 2) & 0x03)
p2 = (groups[:, 2] << 2) | ((groups[:, 4] >> 4) & 0x03)
p3 = (groups[:, 3] << 2) | ((groups[:, 4] >> 6) & 0x03)
out = np.empty(groups.shape[0] * 4, dtype=np.uint16)
out[0::4] = p0
out[1::4] = p1
out[2::4] = p2
out[3::4] = p3
out = out.reshape(height, width)
return out
def normalize_to_u8(img: np.ndarray, p_low: float = 1.0, p_high: float = 99.0) -> np.ndarray:
arr = img.astype(np.float32)
valid = np.isfinite(arr)
if np.count_nonzero(valid) < 20:
return np.zeros(arr.shape[:2], dtype=np.uint8)
vals = arr[valid]
lo = np.percentile(vals, p_low)
hi = np.percentile(vals, p_high)
out = (arr - lo) / (hi - lo + 1e-6)
out = np.clip(out, 0.0, 1.0)
return (out * 255).astype(np.uint8)
def raw10_bin_to_gray_u8(path: Path, width: int, height: int, clahe: bool = True) -> tuple[np.ndarray, np.ndarray]:
raw = path.read_bytes()
mono10 = unpack_raw10_packed(raw, width=width, height=height)
gray_u8 = normalize_to_u8(mono10)
if clahe:
eq = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
gray_u8 = eq.apply(gray_u8)
bgr = cv2.cvtColor(gray_u8, cv2.COLOR_GRAY2BGR)
return gray_u8, bgr
# ============================================================
# Metadata
# ============================================================
def find_meta_for_bin(bin_path: Path) -> Path | None:
"""
Procura meta.json na pasta do .bin ou nas pastas acima próximas.
"""
candidates = [
bin_path.parent / "meta.json",
bin_path.parent / "metadata.json",
bin_path.parent.parent / "meta.json",
bin_path.parent.parent / "metadata.json",
]
for c in candidates:
if c.exists():
return c
return None
def extract_camera_info_from_meta(meta: dict, cam_key: str):
"""
Tenta extrair width/height/raw_format para CAM_B ou CAM_C em vários formatos de meta.
"""
# Caso meta["camera_info"]["CAM_B"]
for root_key in ["camera_info", "cameras", "camera_meta", "payload_sources_info"]:
root = meta.get(root_key)
if isinstance(root, dict):
info = root.get(cam_key)
if isinstance(info, dict):
return info
# Caso meta tenha lista de câmeras
for root_key in ["camera_info", "cameras", "sources"]:
root = meta.get(root_key)
if isinstance(root, list):
for item in root:
if not isinstance(item, dict):
continue
name = item.get("camera") or item.get("name") or item.get("id") or item.get("socket")
if name == cam_key:
return item
# Fallback: procura recursivamente dicionário que tenha CAM_B/C
stack = [meta]
while stack:
obj = stack.pop()
if isinstance(obj, dict):
if cam_key in obj and isinstance(obj[cam_key], dict):
return obj[cam_key]
for v in obj.values():
if isinstance(v, (dict, list)):
stack.append(v)
elif isinstance(obj, list):
for v in obj:
if isinstance(v, (dict, list)):
stack.append(v)
return None
def parse_width_height_from_meta(meta_path: Path, cam_key: str):
try:
meta = json.loads(meta_path.read_text(encoding="utf-8"))
except Exception:
meta = json.loads(meta_path.read_text(encoding="latin-1"))
info = extract_camera_info_from_meta(meta, cam_key)
if not info:
return None, None
width = (
info.get("width")
or info.get("w")
or info.get("sensor_width")
or info.get("frame_width")
)
height = (
info.get("height")
or info.get("h")
or info.get("sensor_height")
or info.get("frame_height")
)
if width is None or height is None:
return None, None
return int(width), int(height)
def resolve_width_height(bin_path: Path, cam_key: str, args):
if args.width > 0 and args.height > 0:
return args.width, args.height
meta_path = find_meta_for_bin(bin_path)
if meta_path:
w, h = parse_width_height_from_meta(meta_path, cam_key)
if w and h:
return w, h
raise RuntimeError(
f"Não consegui descobrir width/height para {bin_path}. "
f"Passe manualmente: --width 1280 --height 800"
)
# ============================================================
# Pairing CAM_B/C
# ============================================================
def clean_stem_for_pair(path: Path, cam_key: str):
"""
Remove CAM_B/C do nome para tentar parear frames.
"""
stem = path.stem
s = stem
patterns = [
cam_key,
cam_key.lower(),
cam_key.replace("_", ""),
cam_key.replace("_", "").lower(),
]
for p in patterns:
s = s.replace(p, "")
s = re.sub(r"[_\-\s]+", "_", s).strip("_").lower()
return s
def find_cam_bins(root_dir: Path, cam_key: str):
bins = []
for p in root_dir.rglob("*.bin"):
name = p.name.lower()
if cam_key.lower() in name:
bins.append(p)
return sorted(bins)
def pair_cam_bins(root_dir: Path, left_cam: str, right_cam: str):
left_bins = find_cam_bins(root_dir, left_cam)
right_bins = find_cam_bins(root_dir, right_cam)
right_by_folder_and_key = {}
right_by_key = {}
for rp in right_bins:
key = clean_stem_for_pair(rp, right_cam)
right_by_folder_and_key[(rp.parent, key)] = rp
right_by_key.setdefault(key, rp)
pairs = []
for lp in left_bins:
key = clean_stem_for_pair(lp, left_cam)
rp = right_by_folder_and_key.get((lp.parent, key))
if rp is None:
rp = right_by_key.get(key)
# Fallback comum: CAM_B/C dentro da mesma pasta, mas nomes não batem
if rp is None:
candidates_same_folder = [r for r in right_bins if r.parent == lp.parent]
if len(candidates_same_folder) == 1:
rp = candidates_same_folder[0]
if rp is not None:
pairs.append((lp, rp))
if pairs:
return pairs, left_bins, right_bins, "cam-key"
# Fallback por ordem
n = min(len(left_bins), len(right_bins))
pairs = list(zip(left_bins[:n], right_bins[:n]))
return pairs, left_bins, right_bins, "order"
# ============================================================
# Stereo SGBM
# ============================================================
def ensure_same_size(left, right):
h = min(left.shape[0], right.shape[0])
w = min(left.shape[1], right.shape[1])
left2 = cv2.resize(left, (w, h), interpolation=cv2.INTER_AREA)
right2 = cv2.resize(right, (w, h), interpolation=cv2.INTER_AREA)
return left2, right2
def make_sgbm(num_disp, block_size, min_disp=0, uniqueness=8, speckle_window=80, speckle_range=2):
num_disp = max(16, int(round(num_disp / 16)) * 16)
block_size = max(3, int(block_size))
if block_size % 2 == 0:
block_size += 1
matcher = cv2.StereoSGBM_create(
minDisparity=min_disp,
numDisparities=num_disp,
blockSize=block_size,
P1=8 * block_size * block_size,
P2=32 * block_size * block_size,
disp12MaxDiff=1,
uniquenessRatio=uniqueness,
speckleWindowSize=speckle_window,
speckleRange=speckle_range,
preFilterCap=63,
mode=cv2.STEREO_SGBM_MODE_SGBM_3WAY,
)
return matcher, num_disp, block_size
def compute_disparity(left_gray, right_gray, args):
matcher, num_disp, block_size = make_sgbm(
num_disp=args.num_disp,
block_size=args.block_size,
min_disp=args.min_disp,
uniqueness=args.uniqueness,
speckle_window=args.speckle_window,
speckle_range=args.speckle_range,
)
disp_raw = matcher.compute(left_gray, right_gray).astype(np.float32) / 16.0
valid = disp_raw > args.min_valid_disp
disp_vis = disp_raw.copy()
disp_vis[~valid] = 0.0
if np.count_nonzero(valid) > 20:
vals = disp_vis[valid]
p2 = np.percentile(vals, 2)
p98 = np.percentile(vals, 98)
disp_norm = (disp_vis - p2) / (p98 - p2 + 1e-6)
disp_norm = np.clip(disp_norm, 0.0, 1.0)
else:
disp_norm = np.zeros_like(disp_vis, dtype=np.float32)
disp_color = cv2.applyColorMap((disp_norm * 255).astype(np.uint8), cv2.COLORMAP_TURBO)
valid_mask = np.zeros_like(disp_color)
valid_mask[valid] = (255, 255, 255)
stats = {
"num_disp": num_disp,
"block_size": block_size,
"valid_pct": float(np.mean(valid) * 100.0),
"disp_p05": float(np.percentile(disp_vis[valid], 5)) if np.count_nonzero(valid) > 20 else 0.0,
"disp_p50": float(np.percentile(disp_vis[valid], 50)) if np.count_nonzero(valid) > 20 else 0.0,
"disp_p95": float(np.percentile(disp_vis[valid], 95)) if np.count_nonzero(valid) > 20 else 0.0,
}
return disp_raw, disp_color, valid_mask, valid, stats
# ============================================================
# View
# ============================================================
def resize_to_height(img, target_h):
h, w = img.shape[:2]
if h == target_h:
return img
scale = target_h / h
new_w = max(1, int(w * scale))
return cv2.resize(img, (new_w, target_h), interpolation=cv2.INTER_AREA)
def draw_header(canvas, lines):
header_h = 24 + 24 * len(lines)
cv2.rectangle(canvas, (0, 0), (canvas.shape[1], header_h), (0, 0, 0), -1)
y = 24
for line in lines:
cv2.putText(
canvas,
line,
(12, y),
cv2.FONT_HERSHEY_SIMPLEX,
0.55,
(255, 255, 255),
1,
cv2.LINE_AA,
)
y += 24
return canvas
def make_overlay(left_bgr, disp_color, alpha=0.45):
disp_resized = cv2.resize(
disp_color,
(left_bgr.shape[1], left_bgr.shape[0]),
interpolation=cv2.INTER_AREA,
)
return cv2.addWeighted(left_bgr, 1.0 - alpha, disp_resized, alpha, 0)
def compose_view(left_bgr, right_bgr, disp_color, valid_mask, pair, idx, total, mode, stats, args):
if mode == "disp":
third = disp_color
mode_name = "disparity"
elif mode == "mask":
third = valid_mask
mode_name = "valid mask"
else:
third = make_overlay(left_bgr, disp_color)
mode_name = "overlay"
left = resize_to_height(left_bgr, args.view_h)
right = resize_to_height(right_bgr, args.view_h)
third = resize_to_height(third, args.view_h)
h = min(left.shape[0], right.shape[0], third.shape[0])
left = left[:h]
right = right[:h]
third = third[:h]
canvas = np.hstack([left, right, third])
lp, rp = pair
lines = [
f"{idx + 1}/{total} | L={lp.name} | R={rp.name}",
f"mode={mode_name} | valid={stats['valid_pct']:.1f}% | disp p05={stats['disp_p05']:.2f} p50={stats['disp_p50']:.2f} p95={stats['disp_p95']:.2f}",
f"numDisp={stats['num_disp']} | block={stats['block_size']} | uniqueness={args.uniqueness}",
"N/SPACE prox | A ant | M modo | [ ] numDisp | - + block | S salvar | Q sair",
]
return draw_header(canvas, lines)
# ============================================================
# Main
# ============================================================
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--root_dir", required=True, help="Pasta raiz onde estão os .bin e meta.json")
parser.add_argument("--left_cam", default="CAM_B", help="Câmera esquerda. Ex: CAM_B")
parser.add_argument("--right_cam", default="CAM_C", help="Câmera direita. Ex: CAM_C")
parser.add_argument("--width", type=int, default=-1, help="Largura manual caso não tenha meta.json")
parser.add_argument("--height", type=int, default=-1, help="Altura manual caso não tenha meta.json")
parser.add_argument("--view_h", type=int, default=480)
parser.add_argument("--num_disp", type=int, default=128)
parser.add_argument("--block_size", type=int, default=7)
parser.add_argument("--min_disp", type=int, default=0)
parser.add_argument("--min_valid_disp", type=float, default=1.0)
parser.add_argument("--uniqueness", type=int, default=8)
parser.add_argument("--speckle_window", type=int, default=80)
parser.add_argument("--speckle_range", type=int, default=2)
parser.add_argument("--no_clahe", action="store_true")
parser.add_argument("--swap", action="store_true", help="Inverte left/right depois do pareamento")
parser.add_argument("--save_dir", default="stereo_raw10_sgbm_saves")
args = parser.parse_args()
root_dir = Path(args.root_dir)
save_dir = Path(args.save_dir)
save_dir.mkdir(parents=True, exist_ok=True)
pairs, left_bins, right_bins, pair_mode = pair_cam_bins(
root_dir=root_dir,
left_cam=args.left_cam,
right_cam=args.right_cam,
)
if args.swap:
pairs = [(r, l) for l, r in pairs]
args.left_cam, args.right_cam = args.right_cam, args.left_cam
if not left_bins:
raise RuntimeError(f"Nenhum .bin encontrado com {args.left_cam} em {root_dir}")
if not right_bins:
raise RuntimeError(f"Nenhum .bin encontrado com {args.right_cam} em {root_dir}")
if not pairs:
raise RuntimeError("Não consegui parear CAM_B/C.")
print(f"[INFO] root_dir: {root_dir}")
print(f"[INFO] left_cam: {args.left_cam} | arquivos: {len(left_bins)}")
print(f"[INFO] right_cam: {args.right_cam} | arquivos: {len(right_bins)}")
print(f"[INFO] pares: {len(pairs)}")
print(f"[INFO] pareamento: {pair_mode}")
print("[INFO] controles:")
print(" N ou SPACE = próxima")
print(" A = anterior")
print(" M = modo disparity/mask/overlay")
print(" [ / ] = diminui/aumenta numDisparities")
print(" - / + = diminui/aumenta blockSize")
print(" S = salva visual atual")
print(" Q ou ESC = sair")
idx = 0
mode = "disp"
cached_key = None
cached_data = None
cv2.namedWindow("Stereo RAW10 SGBM Viewer", cv2.WINDOW_NORMAL)
while True:
pair = pairs[idx]
lp, rp = pair
key_cache = (
str(lp),
str(rp),
args.num_disp,
args.block_size,
args.uniqueness,
args.speckle_window,
args.speckle_range,
args.min_disp,
args.min_valid_disp,
args.no_clahe,
args.width,
args.height,
)
if key_cache != cached_key:
print(f"[RUN] {idx + 1}/{len(pairs)} - L={lp.name} | R={rp.name}")
try:
lw, lh = resolve_width_height(lp, args.left_cam, args)
rw, rh = resolve_width_height(rp, args.right_cam, args)
left_gray, left_bgr = raw10_bin_to_gray_u8(
lp,
width=lw,
height=lh,
clahe=not args.no_clahe,
)
right_gray, right_bgr = raw10_bin_to_gray_u8(
rp,
width=rw,
height=rh,
clahe=not args.no_clahe,
)
left_gray, right_gray = ensure_same_size(left_gray, right_gray)
left_bgr, right_bgr = ensure_same_size(left_bgr, right_bgr)
_, disp_color, valid_mask, valid, stats = compute_disparity(left_gray, right_gray, args)
cached_data = (left_bgr, right_bgr, disp_color, valid_mask, stats)
cached_key = key_cache
except Exception as e:
print(f"[ERRO] Falha processando par:")
print(f" L={lp}")
print(f" R={rp}")
print(f" erro={e}")
idx = min(idx + 1, len(pairs) - 1)
cached_key = None
cached_data = None
continue
else:
left_bgr, right_bgr, disp_color, valid_mask, stats = cached_data
view = compose_view(
left_bgr=left_bgr,
right_bgr=right_bgr,
disp_color=disp_color,
valid_mask=valid_mask,
pair=pair,
idx=idx,
total=len(pairs),
mode=mode,
stats=stats,
args=args,
)
cv2.imshow("Stereo RAW10 SGBM Viewer", view)
key = cv2.waitKey(0) & 0xFF
if key in [27, ord("q"), ord("Q")]:
break
elif key in [ord("n"), ord("N"), 32]:
idx = min(idx + 1, len(pairs) - 1)
cached_key = None
elif key in [ord("a"), ord("A")]:
idx = max(idx - 1, 0)
cached_key = None
elif key in [ord("m"), ord("M")]:
if mode == "disp":
mode = "mask"
elif mode == "mask":
mode = "overlay"
else:
mode = "disp"
elif key == ord("["):
args.num_disp = max(16, args.num_disp - 16)
cached_key = None
print(f"[PARAM] num_disp={args.num_disp}")
elif key == ord("]"):
args.num_disp = min(512, args.num_disp + 16)
cached_key = None
print(f"[PARAM] num_disp={args.num_disp}")
elif key in [ord("-"), ord("_")]:
args.block_size = max(3, args.block_size - 2)
if args.block_size % 2 == 0:
args.block_size -= 1
cached_key = None
print(f"[PARAM] block_size={args.block_size}")
elif key in [ord("+"), ord("=")]:
args.block_size = min(31, args.block_size + 2)
if args.block_size % 2 == 0:
args.block_size += 1
cached_key = None
print(f"[PARAM] block_size={args.block_size}")
elif key in [ord("s"), ord("S")]:
out_path = save_dir / f"stereo_raw10_{idx:04d}_{mode}.png"
cv2.imwrite(str(out_path), view)
print(f"[SAVE] {out_path}")
cv2.destroyAllWindows()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,573 @@
import argparse
import time
from pathlib import Path
import cv2
import numpy as np
import onnxruntime as ort
from PIL import Image
IMG_EXTS = {".png", ".jpg", ".jpeg", ".bmp", ".webp"}
def get_providers(force_cpu: bool = False):
available = ort.get_available_providers()
if force_cpu:
return ["CPUExecutionProvider"]
providers = []
if "CUDAExecutionProvider" in available:
providers.append("CUDAExecutionProvider")
providers.append("CPUExecutionProvider")
return providers
def print_model_io(session: ort.InferenceSession):
print("\n[MODEL INPUTS]")
for i, inp in enumerate(session.get_inputs()):
print(f" {i}: name={inp.name} shape={inp.shape} type={inp.type}")
print("\n[MODEL OUTPUTS]")
for i, out in enumerate(session.get_outputs()):
print(f" {i}: name={out.name} shape={out.shape} type={out.type}")
print("")
def resolve_hw_from_input_shape(shape, fallback_h: int, fallback_w: int):
"""
Tenta descobrir H/W do input ONNX.
Esperado normalmente: [1, 3, H, W] ou [1, H, W, 3].
Se for dinâmico, usa fallback.
"""
if shape is None:
return fallback_h, fallback_w
dims = list(shape)
def is_int(x):
return isinstance(x, int) and x > 0
# NCHW
if len(dims) == 4 and dims[1] == 3:
h = dims[2] if is_int(dims[2]) else fallback_h
w = dims[3] if is_int(dims[3]) else fallback_w
return int(h), int(w)
# NHWC
if len(dims) == 4 and dims[-1] == 3:
h = dims[1] if is_int(dims[1]) else fallback_h
w = dims[2] if is_int(dims[2]) else fallback_w
return int(h), int(w)
return fallback_h, fallback_w
def is_nchw_input(shape):
if shape is None:
return True
dims = list(shape)
return len(dims) == 4 and dims[1] == 3
def preprocess_image(image_path: Path, input_shape, input_h: int, input_w: int, mean, std):
img_pil = Image.open(image_path).convert("RGB")
rgb_orig = np.array(img_pil)
bgr_orig = cv2.cvtColor(rgb_orig, cv2.COLOR_RGB2BGR)
rgb_resized = cv2.resize(rgb_orig, (input_w, input_h), interpolation=cv2.INTER_AREA)
x = rgb_resized.astype(np.float32) / 255.0
mean_arr = np.array(mean, dtype=np.float32).reshape(1, 1, 3)
std_arr = np.array(std, dtype=np.float32).reshape(1, 1, 3)
x = (x - mean_arr) / std_arr
if is_nchw_input(input_shape):
x = np.transpose(x, (2, 0, 1)) # CHW
x = np.expand_dims(x, axis=0).astype(np.float32)
return bgr_orig, x
def robust_normalize_for_view(depth_m: np.ndarray, invert: bool = False):
"""
para visualização colorida.
O modo bandas usa o depth_m direto.
"""
d = depth_m.astype(np.float32)
valid = np.isfinite(d) & (d > 0)
if np.count_nonzero(valid) < 20:
return np.zeros_like(d, dtype=np.float32)
vals = d[valid]
p2 = np.percentile(vals, 2)
p98 = np.percentile(vals, 98)
dn = (d - p2) / (p98 - p2 + 1e-6)
dn = np.clip(dn, 0.0, 1.0)
if invert:
dn = 1.0 - dn
dn[~valid] = 0.0
return dn.astype(np.float32)
def depth_to_colormap(depth_norm: np.ndarray):
u8 = (depth_norm * 255).astype(np.uint8)
return cv2.applyColorMap(u8, cv2.COLORMAP_TURBO)
def make_metric_bands(depth_m: np.ndarray, near_m: float, mid_m: float, far_m: float):
"""
Bandas baseadas no valor métrico estimado.
0: < near_m
1: near_m até mid_m
2: mid_m até far_m
3: >= far_m
"""
d = depth_m.astype(np.float32)
valid = np.isfinite(d) & (d > 0)
bands = np.zeros_like(d, dtype=np.uint8)
bands[(d >= near_m) & (d < mid_m)] = 1
bands[(d >= mid_m) & (d < far_m)] = 2
bands[d >= far_m] = 3
bands[~valid] = 255
out = np.zeros((bands.shape[0], bands.shape[1], 3), dtype=np.uint8)
# BGR
out[bands == 0] = (40, 40, 255) # muito perto
out[bands == 1] = (40, 180, 255) # perto/médio
out[bands == 2] = (40, 255, 120) # médio/longe
out[bands == 3] = (255, 180, 40) # longe
out[bands == 255] = (0, 0, 0) # inválido
return out
def extract_depth_from_outputs(outputs, output_names):
"""
Tenta achar depth em diferentes formatos:
- output chamado depth
- output chamado points/xyz, usando canal Z
- saída única
"""
name_to_out = {
name.lower(): out
for name, out in zip(output_names, outputs)
}
# 1) Procura saída com nome depth
for name, out in name_to_out.items():
if "depth" in name:
return squeeze_depth(out)
# 2) Procura points/xyz e usa Z
for name, out in name_to_out.items():
if "point" in name or "xyz" in name:
arr = np.array(out)
return extract_z_from_points(arr)
# 3) Se tiver só uma saída, usa ela
if len(outputs) == 1:
arr = np.array(outputs[0])
# Se parece points, extrai Z
if arr.ndim == 4 and (arr.shape[1] == 3 or arr.shape[-1] == 3):
return extract_z_from_points(arr)
return squeeze_depth(arr)
# 4) Fallback: pega a primeira saída 2D/3D/4D plausível
for out in outputs:
arr = np.array(out)
try:
d = squeeze_depth(arr)
if d.ndim == 2:
return d
except Exception:
pass
raise RuntimeError("Não consegui identificar o mapa de depth nas saídas ONNX.")
def squeeze_depth(arr):
arr = np.array(arr)
# Remove batch/canal unitário
arr = np.squeeze(arr)
if arr.ndim == 2:
return arr.astype(np.float32)
if arr.ndim == 3:
# CHW com 1 canal
if arr.shape[0] == 1:
return arr[0].astype(np.float32)
# HWC com 1 canal
if arr.shape[-1] == 1:
return arr[..., 0].astype(np.float32)
# Se for 3 canais, talvez seja XYZ
if arr.shape[0] == 3:
return arr[2].astype(np.float32)
if arr.shape[-1] == 3:
return arr[..., 2].astype(np.float32)
raise RuntimeError(f"Formato de depth não suportado: shape={arr.shape}")
def extract_z_from_points(arr):
arr = np.array(arr)
# NCHW: [1, 3, H, W]
if arr.ndim == 4 and arr.shape[1] == 3:
return arr[0, 2].astype(np.float32)
# NHWC: [1, H, W, 3]
if arr.ndim == 4 and arr.shape[-1] == 3:
return arr[0, ..., 2].astype(np.float32)
# CHW: [3, H, W]
if arr.ndim == 3 and arr.shape[0] == 3:
return arr[2].astype(np.float32)
# HWC: [H, W, 3]
if arr.ndim == 3 and arr.shape[-1] == 3:
return arr[..., 2].astype(np.float32)
raise RuntimeError(f"Formato de points/xyz não suportado: shape={arr.shape}")
def maybe_invert_depth_if_needed(depth: np.ndarray, auto_invert: bool):
"""
Alguns modelos podem devolver inverso/disparity.
Aqui deixei opcional. Por padrão não mexe.
"""
if not auto_invert:
return depth
d = depth.astype(np.float32)
valid = np.isfinite(d) & (d > 1e-6)
out = np.zeros_like(d, dtype=np.float32)
out[valid] = 1.0 / d[valid]
return out
def infer_depth(session, image_path: Path, args):
input0 = session.get_inputs()[0]
input_shape = input0.shape
fallback_h, fallback_w = args.input_h, args.input_w
input_h, input_w = resolve_hw_from_input_shape(input_shape, fallback_h, fallback_w)
preview_bgr, x = preprocess_image(
image_path=image_path,
input_shape=input_shape,
input_h=input_h,
input_w=input_w,
mean=args.mean,
std=args.std,
)
feed = {}
# Alimenta input principal
feed[input0.name] = x
# Alguns modelos têm input extra de intrinsics/K.
# Se existir, manda uma matriz aproximada baseada no tamanho de entrada.
# Para nosso teste visual, isso é melhor do que travar.
for inp in session.get_inputs()[1:]:
name = inp.name.lower()
shape = inp.shape
fx = args.fx if args.fx > 0 else input_w * 0.9
fy = args.fy if args.fy > 0 else input_w * 0.9
cx = args.cx if args.cx >= 0 else input_w / 2.0
cy = args.cy if args.cy >= 0 else input_h / 2.0
K = np.array(
[
[fx, 0.0, cx],
[0.0, fy, cy],
[0.0, 0.0, 1.0],
],
dtype=np.float32,
)
if "k" == name or "intr" in name or "camera" in name:
if len(shape) == 3:
feed[inp.name] = K[None, ...]
else:
feed[inp.name] = K
else:
# Fallback para input extra desconhecido
# Evita crash, mas imprime para a gente ajustar se precisar.
print(f"[WARN] Input extra desconhecido: {inp.name}, shape={inp.shape}. Enviando zeros.")
concrete_shape = []
for dim in shape:
concrete_shape.append(dim if isinstance(dim, int) and dim > 0 else 1)
feed[inp.name] = np.zeros(concrete_shape, dtype=np.float32)
t0 = time.time()
outputs = session.run(None, feed)
infer_ms = (time.time() - t0) * 1000.0
output_names = [o.name for o in session.get_outputs()]
depth = extract_depth_from_outputs(outputs, output_names)
depth = maybe_invert_depth_if_needed(depth, auto_invert=args.inv_depth)
# Redimensiona para o preview original
if depth.shape[:2] != preview_bgr.shape[:2]:
depth = cv2.resize(
depth.astype(np.float32),
(preview_bgr.shape[1], preview_bgr.shape[0]),
interpolation=cv2.INTER_CUBIC,
)
# Remove valores absurdos só para visual/estatística
depth = depth.astype(np.float32)
depth[~np.isfinite(depth)] = 0.0
depth[depth < 0] = 0.0
return preview_bgr, depth, infer_ms
def calc_stats(depth_m: np.ndarray, infer_ms: float):
valid = np.isfinite(depth_m) & (depth_m > 0)
if np.count_nonzero(valid) < 20:
return {
"p01": 0.0,
"p05": 0.0,
"p50": 0.0,
"p95": 0.0,
"p99": 0.0,
"mean": 0.0,
"std": 0.0,
"infer_ms": infer_ms,
"valid_pct": 0.0,
}
vals = depth_m[valid]
return {
"p01": float(np.percentile(vals, 1)),
"p05": float(np.percentile(vals, 5)),
"p50": float(np.percentile(vals, 50)),
"p95": float(np.percentile(vals, 95)),
"p99": float(np.percentile(vals, 99)),
"mean": float(np.mean(vals)),
"std": float(np.std(vals)),
"infer_ms": float(infer_ms),
"valid_pct": float(np.mean(valid) * 100.0),
}
def resize_to_height(img: np.ndarray, target_h: int):
h, w = img.shape[:2]
if h == target_h:
return img
scale = target_h / h
new_w = max(1, int(w * scale))
return cv2.resize(img, (new_w, target_h), interpolation=cv2.INTER_AREA)
def draw_header(canvas, lines):
header_h = 24 + 24 * len(lines)
cv2.rectangle(canvas, (0, 0), (canvas.shape[1], header_h), (0, 0, 0), -1)
y = 24
for line in lines:
cv2.putText(
canvas,
line,
(12, y),
cv2.FONT_HERSHEY_SIMPLEX,
0.55,
(255, 255, 255),
1,
cv2.LINE_AA,
)
y += 24
return canvas
def compose_view(preview_bgr, depth_m, image_path, idx, total, mode, invert_view, stats, args):
depth_norm = robust_normalize_for_view(depth_m, invert=invert_view)
depth_color = depth_to_colormap(depth_norm)
bands_color = make_metric_bands(depth_m, args.near_m, args.mid_m, args.far_m)
right_src = depth_color if mode == "depth" else bands_color
left = resize_to_height(preview_bgr, args.view_h)
right = resize_to_height(right_src, args.view_h)
h = min(left.shape[0], right.shape[0])
left = left[:h]
right = right[:h]
canvas = np.hstack([left, right])
mode_name = "depth colormap" if mode == "depth" else "metric bands"
lines = [
f"{idx + 1}/{total} - {image_path.name}",
f"modo={mode_name} | p05={stats['p05']:.2f}m p50={stats['p50']:.2f}m p95={stats['p95']:.2f}m valid={stats['valid_pct']:.1f}% infer={stats['infer_ms']:.1f}ms",
f"bands: <{args.near_m:.2f}m | {args.near_m:.2f}-{args.mid_m:.2f}m | {args.mid_m:.2f}-{args.far_m:.2f}m | >{args.far_m:.2f}m",
"N/SPACE prox | A ant | M modo | I inverte visual | S salvar | Q sair",
]
return draw_header(canvas, lines)
def parse_mean_std(text):
vals = [float(x.strip()) for x in text.split(",")]
if len(vals) != 3:
raise argparse.ArgumentTypeError("Use 3 valores separados por vírgula. Ex: 0.485,0.456,0.406")
return vals
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--input_dir", required=True, help="Pasta com previews PNG/JPG")
parser.add_argument("--model_path", required=True, help="Caminho do arquivo .onnx")
parser.add_argument("--cpu", action="store_true", help="Força CPUExecutionProvider")
# Usado se o modelo tiver tamanho dinâmico
parser.add_argument("--input_h", type=int, default=384)
parser.add_argument("--input_w", type=int, default=512)
# Normalização padrão ImageNet. Se o repo do ONNX pedir outra, ajustamos.
parser.add_argument("--mean", type=parse_mean_std, default=[0.485, 0.456, 0.406])
parser.add_argument("--std", type=parse_mean_std, default=[0.229, 0.224, 0.225])
# Intrínsecos aproximados se o ONNX pedir K/intrinsics
parser.add_argument("--fx", type=float, default=-1.0)
parser.add_argument("--fy", type=float, default=-1.0)
parser.add_argument("--cx", type=float, default=-1.0)
parser.add_argument("--cy", type=float, default=-1.0)
# Se o output for inverso/disparity, ativa isto
parser.add_argument("--inv_depth", action="store_true")
# Bandas métricas para visual
parser.add_argument("--near_m", type=float, default=0.6)
parser.add_argument("--mid_m", type=float, default=1.1)
parser.add_argument("--far_m", type=float, default=1.8)
parser.add_argument("--view_h", type=int, default=560)
parser.add_argument("--save_dir", default="unidepth_onnx_viewer_saves")
args = parser.parse_args()
input_dir = Path(args.input_dir)
model_path = Path(args.model_path)
save_dir = Path(args.save_dir)
save_dir.mkdir(parents=True, exist_ok=True)
if not model_path.exists():
raise FileNotFoundError(f"Modelo ONNX não encontrado: {model_path}")
image_paths = sorted([
p for p in input_dir.rglob("*")
if p.suffix.lower() in IMG_EXTS
])
if not image_paths:
raise RuntimeError(f"Nenhuma imagem encontrada em: {input_dir}")
providers = get_providers(force_cpu=args.cpu)
print(f"[INFO] imagens: {len(image_paths)}")
print(f"[INFO] model_path: {model_path}")
print(f"[INFO] providers: {providers}")
print(f"[INFO] ONNX Runtime providers disponiveis: {ort.get_available_providers()}")
session = ort.InferenceSession(str(model_path), providers=providers)
print_model_io(session)
idx = 0
mode = "depth"
invert_view = False
cached_path = None
cached_data = None
cv2.namedWindow("UniDepth ONNX Metric Viewer", cv2.WINDOW_NORMAL)
while True:
image_path = image_paths[idx]
if cached_path != image_path or cached_data is None:
print(f"[RUN] {idx + 1}/{len(image_paths)} - {image_path.name}")
try:
preview_bgr, depth_m, infer_ms = infer_depth(session, image_path, args)
stats = calc_stats(depth_m, infer_ms)
cached_data = (preview_bgr, depth_m, stats)
cached_path = image_path
except Exception as e:
print(f"[ERRO] Falha em {image_path}: {e}")
idx = min(idx + 1, len(image_paths) - 1)
cached_path = None
cached_data = None
continue
else:
preview_bgr, depth_m, stats = cached_data
view = compose_view(
preview_bgr=preview_bgr,
depth_m=depth_m,
image_path=image_path,
idx=idx,
total=len(image_paths),
mode=mode,
invert_view=invert_view,
stats=stats,
args=args,
)
cv2.imshow("UniDepth ONNX Metric Viewer", view)
key = cv2.waitKey(0) & 0xFF
if key in [27, ord("q"), ord("Q")]:
break
elif key in [ord("n"), ord("N"), 32]:
idx = min(idx + 1, len(image_paths) - 1)
cached_path = None
elif key in [ord("a"), ord("A")]:
idx = max(idx - 1, 0)
cached_path = None
elif key in [ord("m"), ord("M")]:
mode = "bands" if mode == "depth" else "depth"
elif key in [ord("i"), ord("I")]:
invert_view = not invert_view
print(f"[INFO] invert_view={invert_view}")
elif key in [ord("s"), ord("S")]:
out_path = save_dir / f"{image_path.stem}_unidepth_onnx_{mode}.png"
cv2.imwrite(str(out_path), view)
print(f"[SAVE] {out_path}")
cv2.destroyAllWindows()
if __name__ == "__main__":
main()