541 lines
18 KiB
Python
541 lines
18 KiB
Python
import os
|
|
import json
|
|
import argparse
|
|
from pathlib import Path
|
|
|
|
import cv2
|
|
import numpy as np
|
|
|
|
from cam_3.pi.raw_processor_core import RawProcessorCore
|
|
from cam_3.pi.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 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 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_frames", {}) 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] < 3:
|
|
raise RuntimeError(f"Payload MULTISPEC inválido, shape={arr.shape}")
|
|
|
|
rgb_hwc = chw_to_hwc(arr[:3].astype(np.float32))
|
|
preview_bgr = normalize_float01_to_bgr(rgb_hwc)
|
|
desc = f"Reconstruido de MULTISPEC salvo | dtype={arr.dtype} | shape={arr.shape}"
|
|
return preview_bgr, 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"])
|
|
|
|
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]}"))
|
|
|
|
if group["final_raw"] is not None:
|
|
img, desc = build_visual_from_saved_payload(group["final_raw"], meta)
|
|
panels.append(("Reconstruido (final)", img, desc))
|
|
|
|
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", "cam2", "cam0", "cam1"]
|
|
|
|
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:
|
|
cv2.putText(out, subtitle, (12, 56), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (0, 0, 0), 3, cv2.LINE_AA)
|
|
cv2.putText(out, subtitle, (12, 56), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (255, 255, 255), 1, cv2.LINE_AA)
|
|
|
|
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 = {
|
|
"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 do payload salvo | A=anterior | D=proximo | 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)
|
|
|
|
cv2.destroyAllWindows()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |