agrobot_base/Python/OAK/datasets/oak-fcc-3/utils/check_saved_files.py

906 lines
31 KiB
Python

import os
import json
import argparse
from pathlib import Path
from datetime import datetime
import cv2
import numpy as np
from core.raw_processor_core import RawProcessorCore
from core.raw_processor_preview import RawProcessorPreview
def load_json(path: Path) -> dict:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def ts_name() -> str:
return datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3]
def ensure_dir(path: Path | str):
Path(path).mkdir(parents=True, exist_ok=True)
def save_multispec_tensor_from_raw_group(
group: dict,
meta: dict,
out_dir: str = "calibration/offline_samples",
):
"""
Gera e salva um tensor MULTISPEC [5,H,W] float32 a partir de uma captura RAW_BRUTO.
Saídas:
.raw -> tensor float32 CHW
.json -> metadados do tensor gerado offline
.png -> preview RGB do tensor
"""
if meta.get("saved_payload_type") != "raw_native_multi":
raise RuntimeError("Só é possível gerar tensor offline a partir de saved_payload_type='raw_native_multi'.")
ensure_dir(out_dir)
out_dir = Path(out_dir)
tensor, desc, processing_info = build_multispec_from_raw_native_multi(group, meta)
if tensor is None:
raise RuntimeError(f"Falha ao gerar tensor MULTISPEC: {desc}")
base_name = Path(group["json"]).stem
name = f"{base_name}_offline_multispec"
raw_path = out_dir / f"{name}.raw"
json_path = out_dir / f"{name}.json"
png_path = out_dir / f"{name}.png"
tensor = np.ascontiguousarray(tensor.astype(np.float32, copy=False))
tensor.tofile(str(raw_path))
# Preview RGB do tensor
rgb_hwc = np.transpose(tensor[:3], (1, 2, 0))
preview_bgr = normalize_float01_to_bgr(rgb_hwc)
cv2.imwrite(str(png_path), preview_bgr)
# JSON compatível com o validador e com análise posterior
out_meta = {
"ts": datetime.now().isoformat(timespec="milliseconds"),
"schema": "offline_multispec_from_raw_native_multi_v1",
"source_json": str(group["json"]),
"source_saved_payload_type": meta.get("saved_payload_type"),
"source_saved_payload_paths": meta.get("saved_payload_paths"),
"source_saved_payload_shapes": meta.get("saved_payload_shapes"),
"source_saved_payload_dtypes": meta.get("saved_payload_dtypes"),
"camera_params_json": meta.get("camera_params_json"),
"frame_type": "MULTISPEC",
"saved_payload_type": "multispec",
"saved_payload_path": raw_path.name,
"saved_payload_dtype": "float32",
"saved_payload_shape": list(tensor.shape),
"channels": ["R", "G", "B", "RE", "NIR"],
"saved_preview_path": png_path.name,
"generation": {
"method": "build_multispec_from_raw_native_multi",
"description": desc,
"same_frame_as_raw_bruto": True,
},
"processing": processing_info or {},
"frame_quality": (processing_info or {}).get("frame_quality"),
"patch_normalization_result": (processing_info or {}).get("patch_normalization_result"),
"source_capture_meta": {
"ts": meta.get("ts"),
"sensor_width": meta.get("sensor_width"),
"sensor_height": meta.get("sensor_height"),
"bayer_pattern": meta.get("bayer_pattern"),
"fps_target": meta.get("fps_target"),
"startup_camera_controls": meta.get("startup_camera_controls"),
"actual_camera_controls": meta.get("actual_camera_controls"),
"radiometric_last_result": meta.get("radiometric_last_result"),
"stream_meta": meta.get("stream_meta"),
},
}
with open(json_path, "w", encoding="utf-8") as f:
json.dump(out_meta, f, ensure_ascii=False, indent=2)
return {
"tensor": tensor,
"raw_path": raw_path,
"json_path": json_path,
"png_path": png_path,
"desc": desc,
}
def normalize_float01_to_bgr(img_float: np.ndarray) -> np.ndarray:
"""
Recebe RGB float32 [0..1] em HWC e devolve BGR uint8.
"""
rgb_u8 = np.clip(img_float * 255.0, 0, 255).astype(np.uint8)
return cv2.cvtColor(rgb_u8, cv2.COLOR_RGB2BGR)
def chw_to_hwc(arr: np.ndarray) -> np.ndarray:
if arr.ndim != 3:
raise ValueError(f"Esperado CHW 3D, recebido shape={arr.shape}")
return np.transpose(arr, (1, 2, 0))
def format_frame_quality_for_overlay(frame_quality: dict | None, patch_result: dict | None = None) -> str:
"""
Gera uma linha curta para mostrar no preview do tensor final.
Exemplo:
Q=good | sat=0.00% | dark=12.3% | clip=0.00% | scale=0.91-1.08
"""
if not isinstance(frame_quality, dict):
return "Q=n/a"
status = frame_quality.get("status", "unknown")
metrics = frame_quality.get("metrics", {}) or {}
sat = float(metrics.get("max_tensor_sat_pct", 0.0) or 0.0)
dark = float(metrics.get("max_tensor_dark_pct", 0.0) or 0.0)
clip = float(metrics.get("max_patch_would_clip_pct", 0.0) or 0.0)
scale_txt = "scale=n/a"
if isinstance(patch_result, dict):
summary = patch_result.get("summary", {}) or {}
smin = summary.get("scale_min_applied")
smax = summary.get("scale_max_applied")
if smin is not None and smax is not None:
try:
scale_txt = f"scale={float(smin):.2f}-{float(smax):.2f}"
except Exception:
pass
reasons = frame_quality.get("reasons", []) or []
reason_txt = ""
if status != "good" and reasons:
reason_txt = f" | {str(reasons[0])[:38]}"
return f"Q={status} | sat={sat:.2f}% | dark={dark:.1f}% | clip={clip:.2f}% | {scale_txt}{reason_txt}"
def tensor_to_preview_panels(tensor: np.ndarray, frame_quality: dict | None = None, patch_result: dict | None = None):
"""
Recebe tensor CHW [R,G,B,RE,NIR] float32 e devolve painéis visuais.
Quando disponível, adiciona um resumo de qualidade no subtítulo do painel RGB final.
"""
if tensor.ndim != 3 or tensor.shape[0] < 5:
raise RuntimeError(f"Tensor MULTISPEC inválido: shape={tensor.shape}")
rgb_hwc = np.transpose(tensor[:3].astype(np.float32), (1, 2, 0))
rgb_bgr = normalize_float01_to_bgr(rgb_hwc)
re01 = tensor[3].astype(np.float32)
nir01 = tensor[4].astype(np.float32)
re_bgr = cv2.cvtColor(
np.clip(re01 * 255.0, 0, 255).astype(np.uint8),
cv2.COLOR_GRAY2BGR
)
nir_bgr = cv2.cvtColor(
np.clip(nir01 * 255.0, 0, 255).astype(np.uint8),
cv2.COLOR_GRAY2BGR
)
quality_subtitle = format_frame_quality_for_overlay(frame_quality, patch_result)
rgb_subtitle = f"tensor {list(tensor.shape)} | canais 0,1,2"
if quality_subtitle:
rgb_subtitle = f"{rgb_subtitle} | {quality_subtitle}"
return [
("MULTISPEC RGB final", rgb_bgr, rgb_subtitle),
("MULTISPEC RE final", re_bgr, "tensor canal 3"),
("MULTISPEC NIR final", nir_bgr, "tensor canal 4"),
]
def build_multispec_from_raw_native_multi(group: dict, meta: dict):
"""
Reconstrói o tensor MULTISPEC final a partir dos .bin RAW_BRUTO salvos.
Usa:
- saved_payload_paths
- saved_payload_shapes
- saved_payload_dtypes
- stream_meta.camera_info
- camera_params_json/module_params.json
"""
if meta.get("saved_payload_type") != "raw_native_multi":
return None, "captura não é raw_native_multi", {}
stream_meta = meta.get("stream_meta", {}) or {}
camera_info = stream_meta.get("camera_info", {}) or {}
saved_dtypes = meta.get("saved_payload_dtypes", {}) or {}
saved_shapes = meta.get("saved_payload_shapes", {}) or {}
frame = {}
for cam_id, path in group["cameras"].items():
saved_dtype = saved_dtypes.get(cam_id)
saved_shape = saved_shapes.get(cam_id)
if saved_dtype is None or saved_shape is None:
raise RuntimeError(f"Faltam dtype/shape para {cam_id}")
arr = np.fromfile(str(path), dtype=np.dtype(saved_dtype)).reshape(tuple(saved_shape))
frame[cam_id] = arr
if not frame:
raise RuntimeError("Nenhum payload de câmera encontrado para reconstruir MULTISPEC.")
sensor_width = int(meta.get("sensor_width", 1280))
sensor_height = int(meta.get("sensor_height", 800))
bayer = meta.get("bayer_pattern", "RGGB")
# Tenta usar o mesmo module_params que foi usado na captura.
calib_path = meta.get("camera_params_json") or "calibration/module_params.json"
# Se vier relativo, tenta resolver relativo ao diretório atual.
# Normalmente seu script roda da raiz do projeto, então calibration/module_params.json funciona.
if calib_path and not os.path.isfile(calib_path):
# fallback: tenta relativo à pasta do JSON
json_dir = Path(group["json"]).parent
alt = json_dir / calib_path
if alt.exists():
calib_path = str(alt)
else:
print(f"[WARN] module_params não encontrado: {calib_path}. Tentando sem calibração.")
calib_path = None
core = RawProcessorCore(
sensor_width=sensor_width,
sensor_height=sensor_height,
bayer_pattern=bayer,
calibration_json_path=calib_path,
)
# O decode precisa do stream_meta com camera_info.
processing_meta = dict(stream_meta)
# A normalização radiométrica precisa dos controles reais salvos no JSON da captura.
if meta.get("actual_camera_controls") is not None:
processing_meta["actual_camera_controls"] = meta.get("actual_camera_controls")
if meta.get("startup_camera_controls") is not None:
processing_meta["startup_camera_controls"] = meta.get("startup_camera_controls")
tensor = core.build_infer_tensor_from_stream(frame, processing_meta, 5)
processing_info = {
"patch_normalization_result": getattr(core, "last_patch_normalization_result", None),
"frame_quality": getattr(core, "last_frame_quality_result", None),
}
return tensor, f"MULTISPEC gerado offline do RAW_BRUTO | shape={list(tensor.shape)}", processing_info
def build_visual_from_saved_payload(payload_path: Path, meta: dict, cam_id: str | None = None) -> tuple[np.ndarray, str]:
"""
Retorna:
preview_bgr_reconstructed
texto_descritivo
"""
saved_type = meta.get("saved_payload_type")
# =========================================================
# Caso MULTI payload por câmera
# =========================================================
if saved_type == "raw_native_multi":
if cam_id is None:
raise RuntimeError("cam_id é obrigatório para saved_payload_type='raw_native_multi'")
saved_dtypes = meta.get("saved_payload_dtypes", {}) or {}
saved_shapes = meta.get("saved_payload_shapes", {}) or {}
saved_dtype = saved_dtypes.get(cam_id)
saved_shape = saved_shapes.get(cam_id)
if saved_dtype is None or saved_shape is None:
raise RuntimeError(
f"JSON não contém saved_payload_dtypes/saved_payload_shapes para {cam_id}"
)
np_dtype = np.dtype(saved_dtype)
raw = np.fromfile(str(payload_path), dtype=np_dtype)
arr = raw.reshape(tuple(saved_shape))
# Busca metadados da câmera no stream_meta
stream_meta = meta.get("stream_meta", {}) or {}
cam_frames = stream_meta.get("camera_info", {}) or {}
cam_meta = cam_frames.get(cam_id, {}) or {}
role = cam_meta.get("role", cam_id)
interface = cam_meta.get("interface", "")
bit_depth = int(cam_meta.get("bit_depth", 10))
bayer = cam_meta.get("bayer_pattern", meta.get("bayer_pattern", "GBRG"))
# USB RGB nativo
if interface.upper() == "USB" or (arr.ndim == 3 and arr.shape[2] == 3 and arr.dtype == np.uint8):
preview_bgr = arr.copy()
desc = f"{cam_id} | role={role} | USB/RGB nativo | dtype={arr.dtype} | shape={arr.shape}"
return preview_bgr, desc
# CSI RAW packed mono
sensor_width, sensor_height = resolve_sensor_dims_for_raw10_packed(arr, cam_meta, meta)
core = RawProcessorCore(
sensor_width=sensor_width,
sensor_height=sensor_height,
bayer_pattern=bayer,
)
preview = RawProcessorPreview(
sensor_width=sensor_width,
sensor_height=sensor_height,
bayer_pattern=bayer,
)
packed = arr
if packed.ndim == 3 and packed.shape[2] == 1:
packed = packed[:, :, 0]
raw16 = core.unpack_raw10_packed(packed)
preview_bgr = preview.raw16_to_preview_bgr(raw16, bit_depth=bit_depth)
desc = (
f"{cam_id} | role={role} | RAW packed mono | "
f"dtype={arr.dtype} | shape={arr.shape} | "
f"sensor={sensor_width}x{sensor_height} | "
f"bayer={bayer} | bit_depth={bit_depth}"
)
return preview_bgr, desc
# =========================================================
# Caso payload único
# =========================================================
saved_dtype = meta.get("saved_payload_dtype")
saved_shape = meta.get("saved_payload_shape")
if saved_type is None or saved_dtype is None or saved_shape is None:
raise RuntimeError(
"JSON não contém saved_payload_type / saved_payload_dtype / saved_payload_shape"
)
np_dtype = np.dtype(saved_dtype)
raw = np.fromfile(str(payload_path), dtype=np_dtype)
arr = raw.reshape(tuple(saved_shape))
if saved_type == "rgb":
if arr.ndim != 3 or arr.shape[0] != 3:
raise RuntimeError(f"Payload RGB inválido, shape={arr.shape}")
rgb_hwc = chw_to_hwc(arr.astype(np.float32))
preview_bgr = normalize_float01_to_bgr(rgb_hwc)
desc = f"Reconstruido de RGB salvo | dtype={arr.dtype} | shape={arr.shape}"
return preview_bgr, desc
if saved_type == "multispec":
if arr.ndim != 3 or arr.shape[0] < 5:
raise RuntimeError(f"Payload MULTISPEC inválido, shape={arr.shape}")
processing = meta.get("processing", {}) or {}
frame_quality = meta.get("frame_quality") or processing.get("frame_quality")
patch_result = meta.get("patch_normalization_result") or processing.get("patch_normalization_result")
panels = tensor_to_preview_panels(
arr.astype(np.float32),
frame_quality=frame_quality,
patch_result=patch_result,
)
# Renomeia os painéis para indicar que vieram de um MULTISPEC já salvo.
renamed = []
for title, img, subtitle in panels:
title = title.replace("MULTISPEC RGB final", "RGB reconstruido")
title = title.replace("MULTISPEC RE final", "RE reconstruido")
title = title.replace("MULTISPEC NIR final", "NIR reconstruido")
renamed.append((title, img, subtitle))
desc = f"Reconstruido de MULTISPEC | dtype={arr.dtype} | shape={arr.shape} | canais=[R,G,B,RE,NIR]"
return renamed, desc
if saved_type == "raw_native_single":
if arr.ndim == 3 and arr.shape[2] == 3 and arr.dtype == np.uint8:
preview_bgr = arr.copy()
desc = f"Reconstruido de RAW nativo USB | dtype={arr.dtype} | shape={arr.shape}"
return preview_bgr, desc
stream_meta = meta.get("stream_meta", {})
source_camera = stream_meta.get("source_camera", {}) or {}
bayer = source_camera.get("bayer_pattern", meta.get("bayer_pattern", "GBRG"))
bit_depth = int(source_camera.get("bit_depth", 10))
sensor_height = int(meta.get("sensor_height"))
sensor_width = int(meta.get("sensor_width"))
core = RawProcessorCore(
sensor_width=sensor_width,
sensor_height=sensor_height,
bayer_pattern=bayer,
)
preview = RawProcessorPreview(
sensor_width=sensor_width,
sensor_height=sensor_height,
bayer_pattern=bayer,
)
packed = arr
if packed.ndim == 3 and packed.shape[2] == 1:
packed = packed[:, :, 0]
raw16 = core.unpack_raw10_packed(packed)
preview_bgr = preview.raw16_to_preview_bgr(raw16, bit_depth=bit_depth)
desc = f"Reconstruido de RAW packed mono | dtype={arr.dtype} | shape={arr.shape} | bayer={bayer} | bit_depth={bit_depth}"
return preview_bgr, desc
if saved_type == "raw10_packed":
stream_meta = meta.get("stream_meta", {}) or {}
source_camera = stream_meta.get("source_camera", {}) or {}
bayer = source_camera.get("bayer_pattern", meta.get("bayer_pattern", "GBRG"))
bit_depth = int(source_camera.get("bit_depth", 10))
sensor_height = int(meta.get("sensor_height"))
sensor_width = int(meta.get("sensor_width"))
core = RawProcessorCore(
sensor_width=sensor_width,
sensor_height=sensor_height,
bayer_pattern=bayer,
)
preview = RawProcessorPreview(
sensor_width=sensor_width,
sensor_height=sensor_height,
bayer_pattern=bayer,
)
packed = arr
if packed.ndim == 3 and packed.shape[2] == 1:
packed = packed[:, :, 0]
raw16 = core.unpack_raw10_packed(packed)
preview_bgr = preview.raw16_to_preview_bgr(raw16, bit_depth=bit_depth)
desc = (
f"Reconstruido de RAW10 packed | "
f"dtype={arr.dtype} | shape={arr.shape} | "
f"sensor={sensor_width}x{sensor_height} | "
f"bayer={bayer} | bit_depth={bit_depth}"
)
return preview_bgr, desc
raise RuntimeError(f"saved_payload_type não suportado neste script: {saved_type}")
def build_panels_from_group(group):
panels = []
meta = load_json(group["json"])
saved_type = meta.get("saved_payload_type")
# =========================================================
# Para RAW_BRUTO multi, o primeiro painel vira o tensor final
# gerado offline a partir dos .bin salvos.
# =========================================================
if saved_type == "raw_native_multi":
try:
tensor, desc, processing_info = build_multispec_from_raw_native_multi(group, meta)
tensor_panels = tensor_to_preview_panels(
tensor,
frame_quality=(processing_info or {}).get("frame_quality"),
patch_result=(processing_info or {}).get("patch_normalization_result"),
)
# Aqui colocamos só o RGB final como painel principal,
# para substituir o antigo PNG salvo.
title, img, subtitle = tensor_panels[0]
# Mostra a qualidade do tensor gerado offline a partir do RAW_BRUTO.
# O desc completo continua sendo impresso no terminal/salvo no JSON offline.
panels.append((title, img, subtitle))
# Opcional: se quiser também ver RE/NIR finais do tensor,
# descomente estas duas linhas:
# panels.append(tensor_panels[1])
# panels.append(tensor_panels[2])
except Exception as e:
# Fallback para o PNG salvo caso a reconstrução falhe.
preview_saved = cv2.imread(str(group["png"]), cv2.IMREAD_COLOR)
if preview_saved is None:
raise RuntimeError(f"Falha ao ler preview PNG: {group['png']}")
panels.append((
"Preview salvo fallback",
preview_saved,
f"Falha ao gerar MULTISPEC offline: {e}"
))
else:
# Para RGB/MULTISPEC salvos direto, mantém comportamento antigo.
preview_saved = cv2.imread(str(group["png"]), cv2.IMREAD_COLOR)
if preview_saved is None:
raise RuntimeError(f"Falha ao ler preview PNG: {group['png']}")
panels.append(("Preview salvo", preview_saved, f"{preview_saved.shape[1]}x{preview_saved.shape[0]}"))
# =========================================================
# Se houver payload final único, reconstrói normalmente.
# Ex: saved_payload_type == multispec
# =========================================================
if group["final_raw"] is not None:
result, desc = build_visual_from_saved_payload(group["final_raw"], meta)
if isinstance(result, list):
for title, img, subtitle in result:
panels.append((title, img, subtitle))
else:
panels.append(("Reconstruido (final)", result, desc))
# =========================================================
# Continua mostrando CAM_A/CAM_B/CAM_C reconstruídas individualmente.
# =========================================================
for cam_id, path in group["cameras"].items():
img, desc = build_visual_from_saved_payload(path, meta, cam_id=cam_id)
panels.append((f"{cam_id} reconstruido", img, desc))
return panels
def compose_panels(panels, max_width=1600):
imgs = []
# aplica label
for title, img, subtitle in panels:
img_labeled = put_label(img, title, subtitle)
imgs.append(img_labeled)
# normaliza tamanho base
max_h = max(img.shape[0] for img in imgs)
resized = []
for img in imgs:
scale = max_h / img.shape[0]
w = int(img.shape[1] * scale)
resized.append(cv2.resize(img, (w, max_h), interpolation=cv2.INTER_NEAREST))
# =========================
# Montagem em grid 2x2
# =========================
rows = []
gap = np.full((max_h, 20, 3), 30, dtype=np.uint8)
for i in range(0, len(resized), 2):
row_imgs = resized[i:i+2]
# se só tiver 1 imagem na linha, duplica espaço vazio
if len(row_imgs) == 1:
blank = np.zeros_like(row_imgs[0])
row_imgs.append(blank)
row = np.hstack([row_imgs[0], gap, row_imgs[1]])
rows.append(row)
# junta linhas
gap_h = np.full((20, rows[0].shape[1], 3), 30, dtype=np.uint8)
canvas = rows[0]
for r in rows[1:]:
canvas = np.vstack([canvas, gap_h, r])
# =========================
# Resize final
# =========================
if canvas.shape[1] > max_width:
scale = max_width / canvas.shape[1]
canvas = cv2.resize(
canvas,
(int(canvas.shape[1] * scale), int(canvas.shape[0] * scale)),
interpolation=cv2.INTER_AREA
)
return canvas
def sort_panels(panels):
order = [
"preview salvo",
"rgb reconstruido",
"re reconstruido",
"nir reconstruido",
"rgb",
"re",
"nir",
"cam_a",
"cam_b",
"cam_c",
]
def key(p):
title = p[0].lower()
for i, k in enumerate(order):
if k in title:
return i
return 99
return sorted(panels, key=key)
def fit_same_height(img_a: np.ndarray, img_b: np.ndarray, target_h: int = None):
if target_h is None:
target_h = max(img_a.shape[0], img_b.shape[0])
def resize_to_h(img, h):
scale = h / img.shape[0]
w = int(img.shape[1] * scale)
return cv2.resize(img, (w, h), interpolation=cv2.INTER_NEAREST)
return resize_to_h(img_a, target_h), resize_to_h(img_b, target_h)
def put_label(img: np.ndarray, title: str, subtitle: str = "") -> np.ndarray:
out = img.copy()
cv2.putText(out, title, (12, 28), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 0), 3, cv2.LINE_AA)
cv2.putText(out, title, (12, 28), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 255), 2, cv2.LINE_AA)
if subtitle:
# Quebra visual simples para linhas longas de debug/qualidade.
subtitle_lines = []
current = ""
for part in str(subtitle).split(" | "):
candidate = part if not current else current + " | " + part
if len(candidate) > 95 and current:
subtitle_lines.append(current)
current = part
else:
current = candidate
if current:
subtitle_lines.append(current)
y = 56
for line in subtitle_lines[:3]:
cv2.putText(out, line, (12, y), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (0, 0, 0), 3, cv2.LINE_AA)
cv2.putText(out, line, (12, y), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (255, 255, 255), 1, cv2.LINE_AA)
y += 22
return out
def resolve_capture_group(input_path: Path):
"""
Resolve todos os arquivos relacionados a uma captura.
Retorna:
{
"json": Path,
"png": Path,
"final_raw": Path | None,
"cameras": { "cam0": Path, ... }
}
"""
input_path = input_path.resolve()
folder = input_path.parent
name = input_path.stem
# remove sufixo _camX se existir
if "_cam" in name:
base_name = name.split("_cam")[0]
else:
base_name = name
json_path = folder / f"{base_name}.json"
png_path = folder / f"{base_name}.png"
if not json_path.exists():
raise FileNotFoundError(f"JSON não encontrado: {json_path}")
if not png_path.exists():
raise FileNotFoundError(f"PNG não encontrado: {png_path}")
meta = load_json(json_path)
group = {
"json": json_path,
"png": png_path,
"final_raw": None,
"cameras": {}
}
# =========================
# Caso MULTI payload
# =========================
if "saved_payload_paths" in meta:
for cam_id, fname in meta["saved_payload_paths"].items():
path = folder / fname
if path.exists():
group["cameras"][cam_id] = path
# =========================
# Caso payload único (.raw)
# =========================
elif "saved_payload_path" in meta:
path = folder / meta["saved_payload_path"]
if path.exists():
group["final_raw"] = path
return group
def resolve_sensor_dims_for_raw10_packed(arr: np.ndarray, cam_meta: dict, meta: dict) -> tuple[int, int]:
"""
Para CSI RAW10 packed:
packed_width = ceil(sensor_width * 5 / 4)
Na prática aqui usamos:
sensor_width = packed_width * 4 // 5
Altura permanece a mesma.
"""
packed_h = int(arr.shape[0])
packed_w = int(arr.shape[1])
interface = str(cam_meta.get("interface", "")).upper()
bit_depth = int(cam_meta.get("bit_depth", 10))
# USB ou RGB HWC não entra nessa lógica
if interface == "USB":
return packed_w, packed_h
# Caso esperado: CSI RAW10 packed mono
if bit_depth == 10:
sensor_w = (packed_w * 4) // 5
sensor_h = packed_h
return sensor_w, sensor_h
# fallback conservador
return packed_w, packed_h
def list_capture_groups_from_dir(folder: Path) -> list[Path]:
"""
Lista todos os JSONs de captura do diretório, ordenados por nome.
Cada JSON representa uma captura.
"""
if not folder.exists() or not folder.is_dir():
raise FileNotFoundError(f"Diretório não encontrado: {folder}")
items = sorted(folder.glob("*.json"))
if not items:
raise RuntimeError(f"Nenhum arquivo .json encontrado em: {folder}")
return items
def resolve_navigation_inputs(input_path: Path) -> tuple[list[Path], int]:
"""
Retorna:
entries: lista de JSONs de captura
start_index: índice inicial baseado no input fornecido
"""
input_path = input_path.resolve()
# Caso 1: usuário passou uma pasta
if input_path.is_dir():
entries = list_capture_groups_from_dir(input_path)
return entries, 0
# Caso 2: usuário passou arquivo
if not input_path.exists():
raise FileNotFoundError(f"Arquivo não encontrado: {input_path}")
folder = input_path.parent
entries = list_capture_groups_from_dir(folder)
# Tenta descobrir qual JSON corresponde ao input
if input_path.suffix.lower() == ".json":
target_json = input_path.resolve()
else:
group = resolve_capture_group(input_path)
target_json = group["json"].resolve()
try:
idx = entries.index(target_json)
except ValueError:
idx = 0
return entries, idx
def render_group_to_canvas(json_path: Path, max_width: int):
group = resolve_capture_group(json_path)
meta = load_json(group["json"])
panels = build_panels_from_group(group)
panels = sort_panels(panels)
canvas = compose_panels(panels, max_width=max_width)
info = {
"group": group,
"json": group["json"],
"png": group["png"],
"final_raw": group["final_raw"],
"cameras": group["cameras"],
"meta": meta,
}
return canvas, info
def main():
parser = argparse.ArgumentParser(
description="Valida visualmente payload salvo (.bin/.raw/.json/.png) comparando com o preview .png"
)
parser.add_argument("--input_path", help="Caminho para .json, .png, .bin, .raw ou diretório")
parser.add_argument("--max-width", type=int, default=1600, help="Largura máxima da janela final")
args = parser.parse_args()
input_path = Path(args.input_path)
entries, current_idx = resolve_navigation_inputs(input_path)
window_name = "Validacao payload | A=anterior | D=proximo | T=salva tensor offline | Q/Esc=sair"
cv2.namedWindow(window_name, cv2.WINDOW_NORMAL)
while True:
current_json = entries[current_idx]
canvas, info = render_group_to_canvas(current_json, max_width=args.max_width)
# Cabeçalho adicional na imagem
overlay = canvas.copy()
text = f"{current_idx + 1}/{len(entries)} | {current_json.name}"
cv2.putText(overlay, text, (12, overlay.shape[0] - 16),
cv2.FONT_HERSHEY_SIMPLEX, 0.65, (0, 0, 0), 3, cv2.LINE_AA)
cv2.putText(overlay, text, (12, overlay.shape[0] - 16),
cv2.FONT_HERSHEY_SIMPLEX, 0.65, (0, 255, 0), 1, cv2.LINE_AA)
cv2.imshow(window_name, overlay)
meta = info["meta"]
print("=" * 60)
print(f"[{current_idx + 1}/{len(entries)}]")
print("Entrada JSON :", info["json"])
print("PNG :", info["png"])
print("Final RAW :", info["final_raw"])
print("Câmeras :", {k: str(v) for k, v in info["cameras"].items()})
print("saved_payload_type :", meta.get("saved_payload_type"))
print("saved_payload_dtype:", meta.get("saved_payload_dtype"))
print("saved_payload_shape:", meta.get("saved_payload_shape"))
print("saved_payload_dtypes:", meta.get("saved_payload_dtypes"))
print("saved_payload_shapes:", meta.get("saved_payload_shapes"))
print("stream frame_type :", (meta.get("stream_meta") or {}).get("frame_type"))
print("=" * 60)
k = cv2.waitKey(0) & 0xFF
if k in (ord("q"), ord("Q"), 27):
break
elif k in (ord("d"), ord("D")):
current_idx = min(current_idx + 1, len(entries) - 1)
elif k in (ord("a"), ord("A")):
current_idx = max(current_idx - 1, 0)
elif k in (ord("t"), ord("T")):
meta = info["meta"]
group = info["group"]
try:
result = save_multispec_tensor_from_raw_group(
group=group,
meta=meta,
out_dir="calibration/offline_samples",
)
print("[OK] Tensor MULTISPEC offline salvo:")
print(" RAW :", result["raw_path"])
print(" JSON:", result["json_path"])
print(" PNG :", result["png_path"])
print(" DESC:", result["desc"])
except Exception as e:
print("[ERRO] Falha ao salvar tensor MULTISPEC offline:", e)
cv2.destroyAllWindows()
if __name__ == "__main__":
main()