ajustes no sistema de treinamento multiespectral e perfil de homografia
This commit is contained in:
parent
441d5f5d01
commit
c591e53d67
|
|
@ -69,9 +69,10 @@ 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/multicam_charuco_calib_out/
|
||||||
Python/OAK/datasets/oak-fcc-3/calibration/stereo_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/calibration/stereo_dataset/
|
||||||
|
Python/OAK/datasets/oak-fcc-3/calibration/dataset_homography/
|
||||||
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/
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
@ -1045,3 +1047,339 @@ class CameraMultispectral:
|
||||||
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Salvamento científico / pós-processamento
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def _ts_name(self) -> str:
|
||||||
|
return datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3]
|
||||||
|
|
||||||
|
def requisitar_bundle_raw_multispec(self, force: bool = True, max_age_s: float = None):
|
||||||
|
"""
|
||||||
|
Retorna um bundle científico RAW_BRUTO completo.
|
||||||
|
|
||||||
|
Retorno:
|
||||||
|
bundle, resultado
|
||||||
|
|
||||||
|
bundle = {
|
||||||
|
"raw_frame": {
|
||||||
|
"CAM_A": np.ndarray RAW10 packed,
|
||||||
|
"CAM_B": np.ndarray RAW10 packed,
|
||||||
|
"CAM_C": np.ndarray RAW10 packed,
|
||||||
|
},
|
||||||
|
"raw_meta": dict,
|
||||||
|
"preview_bgr": np.ndarray BGR uint8 ou None,
|
||||||
|
"preview_method": str,
|
||||||
|
}
|
||||||
|
|
||||||
|
Este método não salva nada em disco.
|
||||||
|
Ele apenas coleta e organiza o pacote bruto.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
agora = time.perf_counter()
|
||||||
|
|
||||||
|
if max_age_s is None:
|
||||||
|
max_age_s = self._cache_max_age_s
|
||||||
|
|
||||||
|
with self._lock:
|
||||||
|
cache_ok = (
|
||||||
|
self.ultimo_raw_multi is not None
|
||||||
|
and self.ultimo_meta is not None
|
||||||
|
and self.timestamp_ultimo_raw_multi is not None
|
||||||
|
and (agora - self.timestamp_ultimo_raw_multi) < max_age_s
|
||||||
|
)
|
||||||
|
|
||||||
|
if cache_ok and not force:
|
||||||
|
raw_frame = {
|
||||||
|
cam_id: arr.copy()
|
||||||
|
for cam_id, arr in self.ultimo_raw_multi.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
raw_meta = dict(self.ultimo_meta)
|
||||||
|
|
||||||
|
preview_bgr, preview_method = self._build_preview_raw_multispec(
|
||||||
|
raw_frame=raw_frame,
|
||||||
|
raw_meta=raw_meta,
|
||||||
|
)
|
||||||
|
|
||||||
|
bundle = {
|
||||||
|
"raw_frame": raw_frame,
|
||||||
|
"raw_meta": raw_meta,
|
||||||
|
"preview_bgr": preview_bgr,
|
||||||
|
"preview_method": preview_method,
|
||||||
|
}
|
||||||
|
|
||||||
|
return bundle, dict(self._ultimo_resultado_raw)
|
||||||
|
|
||||||
|
if self.client is None:
|
||||||
|
raise RuntimeError("OakFcc3Client não inicializado")
|
||||||
|
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
|
||||||
|
raw_frame, raw_meta = self.client.get_next_raw_frame(
|
||||||
|
timeout=self.timeout_s
|
||||||
|
)
|
||||||
|
|
||||||
|
dur = time.perf_counter() - t0
|
||||||
|
|
||||||
|
if not isinstance(raw_frame, dict) or not raw_frame:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"RAW_BRUTO inválido. Esperado dict por câmera, veio {type(raw_frame)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
raw_meta = dict(raw_meta or {})
|
||||||
|
|
||||||
|
frame_type = str(raw_meta.get("frame_type", "")).upper()
|
||||||
|
if frame_type and frame_type != "RAW_BRUTO":
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Bundle científico esperado em RAW_BRUTO, mas veio frame_type={frame_type}"
|
||||||
|
)
|
||||||
|
|
||||||
|
required = {"CAM_A", "CAM_B", "CAM_C"}
|
||||||
|
presentes = set(raw_frame.keys())
|
||||||
|
faltando = sorted(required - presentes)
|
||||||
|
|
||||||
|
if faltando:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"RAW_BRUTO incompleto. Faltando câmeras: {faltando}. Presentes: {sorted(presentes)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
raw_frame_copy = {
|
||||||
|
cam_id: arr.copy()
|
||||||
|
for cam_id, arr in raw_frame.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
preview_bgr, preview_method = self._build_preview_raw_multispec(
|
||||||
|
raw_frame=raw_frame_copy,
|
||||||
|
raw_meta=raw_meta,
|
||||||
|
)
|
||||||
|
|
||||||
|
resultado = {
|
||||||
|
"erro": None,
|
||||||
|
"duracao": dur,
|
||||||
|
"frame_valido": True,
|
||||||
|
"cameras": list(raw_frame_copy.keys()),
|
||||||
|
"sync_ok": bool(raw_meta.get("sync_ok", True)),
|
||||||
|
"sync_dt_ms": float(raw_meta.get("sync_dt_ms", 0.0) or 0.0),
|
||||||
|
"frame_id": raw_meta.get("frame_id"),
|
||||||
|
"preview_method": preview_method,
|
||||||
|
}
|
||||||
|
|
||||||
|
with self._lock:
|
||||||
|
self.ultimo_raw_multi = raw_frame_copy
|
||||||
|
self.ultimo_meta = raw_meta
|
||||||
|
self.timestamp_ultimo_raw_multi = agora
|
||||||
|
self._ultimo_resultado_raw = resultado
|
||||||
|
|
||||||
|
bundle = {
|
||||||
|
"raw_frame": raw_frame_copy,
|
||||||
|
"raw_meta": raw_meta,
|
||||||
|
"preview_bgr": preview_bgr,
|
||||||
|
"preview_method": preview_method,
|
||||||
|
}
|
||||||
|
|
||||||
|
return bundle, resultado
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
resultado = {
|
||||||
|
"erro": str(e),
|
||||||
|
"duracao": 0.0,
|
||||||
|
"frame_valido": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
with self._lock:
|
||||||
|
self._ultimo_resultado_raw = resultado
|
||||||
|
|
||||||
|
self.mostrar_log(
|
||||||
|
f"[CameraMultispectral] Erro ao requisitar bundle RAW multispec: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if self._is_erro_fatal_depthai(e):
|
||||||
|
self._falha_fatal_depthai(e)
|
||||||
|
|
||||||
|
return None, resultado
|
||||||
|
|
||||||
|
def _build_preview_raw_multispec(self, raw_frame: dict, raw_meta: dict):
|
||||||
|
"""
|
||||||
|
Gera preview visual para acompanhar o bundle RAW_BRUTO.
|
||||||
|
|
||||||
|
Preferência:
|
||||||
|
1) build_save_preview_from_cam_a(), igual ao capture atual.
|
||||||
|
2) build_preview_from_raw_payload(), fallback.
|
||||||
|
3) None.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.client is None:
|
||||||
|
return None, "client_indisponivel"
|
||||||
|
|
||||||
|
try:
|
||||||
|
preview = self.client.build_save_preview_from_cam_a(
|
||||||
|
packed_raw_by_camera=raw_frame,
|
||||||
|
meta_stream=raw_meta,
|
||||||
|
sensor_width=self.width,
|
||||||
|
sensor_height=self.height,
|
||||||
|
bayer_pattern="BGGR",
|
||||||
|
)
|
||||||
|
|
||||||
|
if preview is not None:
|
||||||
|
return preview, "cam_a_reconstructed_raw10"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.mostrar_log(
|
||||||
|
f"[CameraMultispectral] Falha ao gerar preview CAM_A RAW10: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
preview, _, preview_source_id = self.client.build_preview_from_raw_payload(
|
||||||
|
frame=raw_frame,
|
||||||
|
meta=raw_meta,
|
||||||
|
)
|
||||||
|
|
||||||
|
if preview is not None:
|
||||||
|
return preview, f"raw_payload_preview_{preview_source_id}"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.mostrar_log(
|
||||||
|
f"[CameraMultispectral] Falha no fallback de preview RAW: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return None, "preview_indisponivel"
|
||||||
|
|
||||||
|
def salvar_bundle_raw_multispec(
|
||||||
|
self,
|
||||||
|
pasta: str,
|
||||||
|
nome: str = "",
|
||||||
|
nota: str = "operacao",
|
||||||
|
extra_meta: dict = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Salva pacote RAW_BRUTO multiespectral no mesmo espírito do capture de dataset.
|
||||||
|
|
||||||
|
Saída:
|
||||||
|
<nome>.png
|
||||||
|
<nome>.json
|
||||||
|
<nome>_CAM_A.bin
|
||||||
|
<nome>_CAM_B.bin
|
||||||
|
<nome>_CAM_C.bin
|
||||||
|
|
||||||
|
Retorna:
|
||||||
|
list[str] com os caminhos salvos.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.makedirs(pasta, exist_ok=True)
|
||||||
|
|
||||||
|
nome_base = nome.strip() if nome else self._ts_name()
|
||||||
|
|
||||||
|
bundle, resultado = self.requisitar_bundle_raw_multispec(
|
||||||
|
force=True,
|
||||||
|
max_age_s=0.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not resultado.get("frame_valido", False):
|
||||||
|
raise RuntimeError(
|
||||||
|
resultado.get("erro") or "Bundle RAW multispec inválido"
|
||||||
|
)
|
||||||
|
|
||||||
|
raw_frame = bundle["raw_frame"]
|
||||||
|
raw_meta = bundle["raw_meta"]
|
||||||
|
preview_bgr = bundle.get("preview_bgr")
|
||||||
|
preview_method = bundle.get("preview_method")
|
||||||
|
|
||||||
|
caminhos = []
|
||||||
|
|
||||||
|
payload_files = {}
|
||||||
|
payload_shapes = {}
|
||||||
|
payload_dtypes = {}
|
||||||
|
|
||||||
|
for cam_id, arr in raw_frame.items():
|
||||||
|
if arr is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
caminho_bin = os.path.join(
|
||||||
|
pasta,
|
||||||
|
f"{nome_base}_{cam_id}.bin"
|
||||||
|
)
|
||||||
|
|
||||||
|
arr.tofile(caminho_bin)
|
||||||
|
|
||||||
|
payload_files[cam_id] = os.path.basename(caminho_bin)
|
||||||
|
payload_shapes[cam_id] = list(arr.shape)
|
||||||
|
payload_dtypes[cam_id] = str(arr.dtype)
|
||||||
|
|
||||||
|
caminhos.append(caminho_bin)
|
||||||
|
|
||||||
|
if not payload_files:
|
||||||
|
raise RuntimeError("Nenhum payload RAW foi salvo.")
|
||||||
|
|
||||||
|
caminho_preview = None
|
||||||
|
|
||||||
|
if preview_bgr is not None and hasattr(preview_bgr, "size") and preview_bgr.size > 0:
|
||||||
|
caminho_preview = os.path.join(
|
||||||
|
pasta,
|
||||||
|
f"{nome_base}.png"
|
||||||
|
)
|
||||||
|
|
||||||
|
cv2.imwrite(caminho_preview, preview_bgr)
|
||||||
|
caminhos.append(caminho_preview)
|
||||||
|
|
||||||
|
meta_save = {
|
||||||
|
"ts": datetime.now().isoformat(timespec="milliseconds"),
|
||||||
|
"source": "operacao_robo",
|
||||||
|
"note": nota,
|
||||||
|
"camera_model": self.modelo,
|
||||||
|
"mx_id": self.mx_id,
|
||||||
|
"module_calibration_json": self.module_calibration_json,
|
||||||
|
|
||||||
|
"sensor_width": self.width,
|
||||||
|
"sensor_height": self.height,
|
||||||
|
"bayer_pattern": "BGGR",
|
||||||
|
"fps_target": self.fps,
|
||||||
|
|
||||||
|
"frame_type": "RAW_BRUTO",
|
||||||
|
"capture_mode_requested": "TRIPLE",
|
||||||
|
"capture_mode_effective": "TRIPLE",
|
||||||
|
"raw_policy": "require_triple",
|
||||||
|
|
||||||
|
"saved_payload_type": "raw_native_multi",
|
||||||
|
"saved_payload_paths": payload_files,
|
||||||
|
"saved_payload_shapes": payload_shapes,
|
||||||
|
"saved_payload_dtypes": payload_dtypes,
|
||||||
|
|
||||||
|
"saved_preview_path": os.path.basename(caminho_preview) if caminho_preview else None,
|
||||||
|
"saved_preview_method": preview_method,
|
||||||
|
|
||||||
|
"stream_meta": raw_meta,
|
||||||
|
"actual_camera_controls": self.client.get_current_camera_controls() if self.client else None,
|
||||||
|
"radiometric_last_result": self.client.get_radiometric_last_result() if self.client else None,
|
||||||
|
|
||||||
|
"resultado": resultado,
|
||||||
|
}
|
||||||
|
|
||||||
|
if extra_meta:
|
||||||
|
meta_save["extra"] = extra_meta
|
||||||
|
|
||||||
|
caminho_json = os.path.join(
|
||||||
|
pasta,
|
||||||
|
f"{nome_base}.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(caminho_json, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(meta_save, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
caminhos.append(caminho_json)
|
||||||
|
|
||||||
|
return caminhos
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.mostrar_log(
|
||||||
|
f"[CameraMultispectral] Erro ao salvar bundle RAW multispec: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if self._is_erro_fatal_depthai(e):
|
||||||
|
self._falha_fatal_depthai(e)
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1090,17 +1090,7 @@ class RawProcessorCore:
|
||||||
warped_mask = self._affine_image(mask, dx, dy, theta_deg)
|
warped_mask = self._affine_image(mask, dx, dy, theta_deg)
|
||||||
|
|
||||||
elif mode == "homography":
|
elif mode == "homography":
|
||||||
H = cfg.get("homographies", {}).get(f"{role}_to_rgb")
|
H, calib_size, profile_name = self._resolve_homography_entry_for_role(role)
|
||||||
|
|
||||||
if H is None:
|
|
||||||
raise RuntimeError(
|
|
||||||
f"fusion_config.alignment_mode='homography', "
|
|
||||||
f"mas homografia '{role}_to_rgb' está ausente. "
|
|
||||||
f"Isso deixaria o canal {role.upper()} sem alinhamento."
|
|
||||||
)
|
|
||||||
|
|
||||||
calib_size = cfg.get("homography_calibration_size", None)
|
|
||||||
|
|
||||||
H = self._scale_homography_to_runtime(
|
H = self._scale_homography_to_runtime(
|
||||||
H,
|
H,
|
||||||
calib_size=calib_size,
|
calib_size=calib_size,
|
||||||
|
|
@ -3611,22 +3601,30 @@ class RawProcessorCore:
|
||||||
def _direct_fusion_get_role_homography_fast(self, role, meta, ref_size):
|
def _direct_fusion_get_role_homography_fast(self, role, meta, ref_size):
|
||||||
"""
|
"""
|
||||||
Retorna H_role_to_rgb escalada para o espaço da referência RGB.
|
Retorna H_role_to_rgb escalada para o espaço da referência RGB.
|
||||||
|
|
||||||
|
Suporta:
|
||||||
|
- contrato antigo: fusion_config.homographies.re_to_rgb/nir_to_rgb
|
||||||
|
- contrato novo: fusion_config.homography_profiles.<perfil>.homographies.*
|
||||||
"""
|
"""
|
||||||
role = str(role).lower()
|
role = str(role).lower()
|
||||||
fusion = getattr(self, "fusion_config", {}) or {}
|
|
||||||
homographies = fusion.get("homographies", {}) or {}
|
|
||||||
|
|
||||||
key = f"{role}_to_rgb"
|
H, calib_size, profile_name = self._resolve_homography_entry_for_role(role)
|
||||||
H = homographies.get(key)
|
|
||||||
|
|
||||||
if H is None:
|
ref_h, ref_w = int(ref_size[0]), int(ref_size[1])
|
||||||
# Fallbacks para contratos diferentes.
|
|
||||||
H = homographies.get(role)
|
|
||||||
|
|
||||||
if H is None:
|
H_scaled = self._scale_homography_to_runtime(
|
||||||
raise RuntimeError(f"Homografia ausente para role={role}. Esperado fusion_config.homographies.{key}")
|
H,
|
||||||
|
calib_size=calib_size,
|
||||||
|
runtime_size=(ref_w, ref_h),
|
||||||
|
)
|
||||||
|
|
||||||
return self._direct_fusion_scale_homography_for_ref_fast(H, meta, ref_size)
|
if H_scaled is None or H_scaled.shape != (3, 3):
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Homografia inválida para role={role}, profile={profile_name}: "
|
||||||
|
f"shape={None if H_scaled is None else H_scaled.shape}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return H_scaled.astype(np.float32)
|
||||||
|
|
||||||
def _direct_fusion_resize_spec_to_ref_if_needed_fast(self, img, ref_size):
|
def _direct_fusion_resize_spec_to_ref_if_needed_fast(self, img, ref_size):
|
||||||
"""
|
"""
|
||||||
|
|
@ -3770,6 +3768,7 @@ class RawProcessorCore:
|
||||||
"geometry_cache_hit": bool(geom.get("prepare_cache_hit", False)),
|
"geometry_cache_hit": bool(geom.get("prepare_cache_hit", False)),
|
||||||
"geometry_cache_hits": int(geom.get("cache_hits", 0)),
|
"geometry_cache_hits": int(geom.get("cache_hits", 0)),
|
||||||
"geometry_cache_misses": int(geom.get("cache_misses", 0)),
|
"geometry_cache_misses": int(geom.get("cache_misses", 0)),
|
||||||
|
"homography_profiles_used": geom.get("homography_profiles_used", {}),
|
||||||
}
|
}
|
||||||
|
|
||||||
tensor = np.empty((int(channels_expected), target_h, target_w), dtype=np.float32)
|
tensor = np.empty((int(channels_expected), target_h, target_w), dtype=np.float32)
|
||||||
|
|
@ -3962,34 +3961,38 @@ class RawProcessorCore:
|
||||||
"""
|
"""
|
||||||
Chave simples e estável para cache da geometria.
|
Chave simples e estável para cache da geometria.
|
||||||
|
|
||||||
A geometria depende de:
|
Considera:
|
||||||
- tamanho do RGB de referência
|
- tamanho do RGB/ref
|
||||||
- target final
|
- target final
|
||||||
- roles presentes
|
- roles presentes
|
||||||
- crop_valid_common / resize_after_crop
|
- crop/resize
|
||||||
- homografias e calibration_size
|
- homografia efetivamente selecionada por perfil
|
||||||
|
- calibration_size efetivo por role
|
||||||
Para evitar custo de serializar o JSON todo por frame, usamos uma versão
|
|
||||||
simples. Se você editar module_params em runtime, chame
|
|
||||||
clear_direct_fusion_geometry_cache().
|
|
||||||
"""
|
"""
|
||||||
ref_h, ref_w = int(ref_size[0]), int(ref_size[1])
|
ref_h, ref_w = int(ref_size[0]), int(ref_size[1])
|
||||||
target_w, target_h = int(target_size[0]), int(target_size[1])
|
target_w, target_h = int(target_size[0]), int(target_size[1])
|
||||||
|
|
||||||
fusion = getattr(self, "fusion_config", {}) or {}
|
fusion = getattr(self, "fusion_config", {}) or {}
|
||||||
homographies = fusion.get("homographies", {}) or {}
|
|
||||||
|
|
||||||
# Pequena assinatura numérica das homografias.
|
|
||||||
def h_sig(key):
|
|
||||||
H = homographies.get(key)
|
|
||||||
if H is None:
|
|
||||||
return None
|
|
||||||
arr = np.asarray(H, dtype=np.float32).reshape(-1)
|
|
||||||
# arredonda para evitar ruído float/json, mas detecta mudança real.
|
|
||||||
return tuple(np.round(arr, 8).tolist())
|
|
||||||
|
|
||||||
roles = tuple(sorted([str(r).lower() for r in role_to_cam.keys()]))
|
roles = tuple(sorted([str(r).lower() for r in role_to_cam.keys()]))
|
||||||
|
|
||||||
|
def h_sig_for_role(role):
|
||||||
|
role = str(role).lower()
|
||||||
|
|
||||||
|
if role not in role_to_cam:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
H, calib_size, profile_name = self._resolve_homography_entry_for_role(role)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
arr = np.asarray(H, dtype=np.float32).reshape(-1)
|
||||||
|
return (
|
||||||
|
str(profile_name),
|
||||||
|
tuple(calib_size or []),
|
||||||
|
tuple(np.round(arr, 8).tolist()),
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
ref_w,
|
ref_w,
|
||||||
ref_h,
|
ref_h,
|
||||||
|
|
@ -3998,9 +4001,8 @@ class RawProcessorCore:
|
||||||
roles,
|
roles,
|
||||||
bool(fusion.get("crop_valid_common", False)),
|
bool(fusion.get("crop_valid_common", False)),
|
||||||
bool(fusion.get("resize_after_crop", False)),
|
bool(fusion.get("resize_after_crop", False)),
|
||||||
tuple(fusion.get("homography_calibration_size") or fusion.get("calibration_size") or []),
|
h_sig_for_role("re"),
|
||||||
h_sig("re_to_rgb"),
|
h_sig_for_role("nir"),
|
||||||
h_sig("nir_to_rgb"),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def clear_direct_fusion_geometry_cache(self):
|
def clear_direct_fusion_geometry_cache(self):
|
||||||
|
|
@ -4053,8 +4055,14 @@ class RawProcessorCore:
|
||||||
# Homografias escaladas para runtime.
|
# Homografias escaladas para runtime.
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
H_role_to_rgb = {}
|
H_role_to_rgb = {}
|
||||||
|
homography_profiles_used = {}
|
||||||
for role in ("re", "nir"):
|
for role in ("re", "nir"):
|
||||||
if role in role_to_cam:
|
if role in role_to_cam:
|
||||||
|
H_raw, calib_size, profile_name = self._resolve_homography_entry_for_role(role)
|
||||||
|
homography_profiles_used[role] = {
|
||||||
|
"profile": profile_name,
|
||||||
|
"calib_size": list(calib_size) if calib_size is not None else None,
|
||||||
|
}
|
||||||
H_role_to_rgb[role] = self._direct_fusion_get_role_homography_fast(role, meta, ref_size)
|
H_role_to_rgb[role] = self._direct_fusion_get_role_homography_fast(role, meta, ref_size)
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
|
|
@ -4103,6 +4111,7 @@ class RawProcessorCore:
|
||||||
"prepare_cache_hit": False,
|
"prepare_cache_hit": False,
|
||||||
"cache_hits": int(self._direct_fusion_geometry_cache_hits),
|
"cache_hits": int(self._direct_fusion_geometry_cache_hits),
|
||||||
"cache_misses": int(self._direct_fusion_geometry_cache_misses),
|
"cache_misses": int(self._direct_fusion_geometry_cache_misses),
|
||||||
|
"homography_profiles_used": homography_profiles_used,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Cache pequeno: normalmente só uma geometria. Se mudar resolução/config,
|
# Cache pequeno: normalmente só uma geometria. Se mudar resolução/config,
|
||||||
|
|
@ -4376,6 +4385,120 @@ class RawProcessorCore:
|
||||||
|
|
||||||
tensor[int(channel_index)] = out
|
tensor[int(channel_index)] = out
|
||||||
|
|
||||||
|
def _resolve_homography_profile_name_for_role(self, role: str) -> str:
|
||||||
|
"""
|
||||||
|
Resolve qual perfil de homografia usar para uma role.
|
||||||
|
|
||||||
|
Prioridade:
|
||||||
|
1) fusion_config.homography_profile_by_role[role]
|
||||||
|
2) fusion_config.homography_profile
|
||||||
|
3) "default"
|
||||||
|
"""
|
||||||
|
role = str(role).lower()
|
||||||
|
fusion = getattr(self, "fusion_config", {}) or {}
|
||||||
|
|
||||||
|
by_role = fusion.get("homography_profile_by_role", {}) or {}
|
||||||
|
if isinstance(by_role, dict):
|
||||||
|
selected = by_role.get(role)
|
||||||
|
if selected:
|
||||||
|
return str(selected).lower()
|
||||||
|
|
||||||
|
selected = fusion.get("homography_profile", None)
|
||||||
|
if selected:
|
||||||
|
return str(selected).lower()
|
||||||
|
|
||||||
|
return "default"
|
||||||
|
|
||||||
|
def _resolve_homography_entry_for_role(self, role: str):
|
||||||
|
"""
|
||||||
|
Resolve a homografia no contrato novo ou antigo.
|
||||||
|
|
||||||
|
Contrato novo:
|
||||||
|
fusion_config.homography_profiles.<perfil>.homographies.<role>_to_rgb
|
||||||
|
|
||||||
|
Contrato antigo:
|
||||||
|
fusion_config.homographies.<role>_to_rgb
|
||||||
|
|
||||||
|
Retorna:
|
||||||
|
H, calib_size, profile_name
|
||||||
|
"""
|
||||||
|
role = str(role).lower()
|
||||||
|
fusion = getattr(self, "fusion_config", {}) or {}
|
||||||
|
|
||||||
|
key = f"{role}_to_rgb"
|
||||||
|
|
||||||
|
selected_profile = self._resolve_homography_profile_name_for_role(role)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Futuro: auto por profundidade.
|
||||||
|
# Por enquanto, cai em media/default de forma explícita.
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
if selected_profile == "auto":
|
||||||
|
profiles = fusion.get("homography_profiles", {}) or {}
|
||||||
|
if "media" in profiles:
|
||||||
|
selected_profile = "media"
|
||||||
|
elif "default" in profiles:
|
||||||
|
selected_profile = "default"
|
||||||
|
else:
|
||||||
|
selected_profile = ""
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Contrato novo: homography_profiles
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
profiles = fusion.get("homography_profiles", {}) or {}
|
||||||
|
if isinstance(profiles, dict) and selected_profile:
|
||||||
|
profile = profiles.get(selected_profile)
|
||||||
|
|
||||||
|
if profile is None:
|
||||||
|
# tolera nomes com caixa diferente
|
||||||
|
for name, item in profiles.items():
|
||||||
|
if str(name).lower() == selected_profile:
|
||||||
|
profile = item
|
||||||
|
selected_profile = str(name)
|
||||||
|
break
|
||||||
|
|
||||||
|
if isinstance(profile, dict):
|
||||||
|
profile_homographies = profile.get("homographies", {}) or {}
|
||||||
|
H = profile_homographies.get(key)
|
||||||
|
|
||||||
|
if H is None:
|
||||||
|
# fallback curto: "re" ou "nir"
|
||||||
|
H = profile_homographies.get(role)
|
||||||
|
|
||||||
|
if H is not None:
|
||||||
|
calib_size = (
|
||||||
|
profile.get("homography_calibration_size")
|
||||||
|
or profile.get("calibration_size")
|
||||||
|
or fusion.get("homography_calibration_size")
|
||||||
|
or fusion.get("calibration_size")
|
||||||
|
or None
|
||||||
|
)
|
||||||
|
return H, calib_size, selected_profile
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Contrato antigo: homographies direto
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
homographies = fusion.get("homographies", {}) or {}
|
||||||
|
H = homographies.get(key)
|
||||||
|
|
||||||
|
if H is None:
|
||||||
|
H = homographies.get(role)
|
||||||
|
|
||||||
|
if H is not None:
|
||||||
|
calib_size = (
|
||||||
|
fusion.get("homography_calibration_size")
|
||||||
|
or fusion.get("calibration_size")
|
||||||
|
or None
|
||||||
|
)
|
||||||
|
return H, calib_size, "legacy"
|
||||||
|
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Homografia ausente para role={role}. "
|
||||||
|
f"Procurei profile='{selected_profile}' em "
|
||||||
|
f"fusion_config.homography_profiles.*.homographies.{key} "
|
||||||
|
f"e fallback fusion_config.homographies.{key}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _raw10_rgb_linear_demosaic_to_rgb_float01_fast(
|
def _raw10_rgb_linear_demosaic_to_rgb_float01_fast(
|
||||||
|
|
@ -4806,3 +4929,6 @@ class RawProcessorCore:
|
||||||
self._flatfield_runtime_cache[key] = gain_tensor
|
self._flatfield_runtime_cache[key] = gain_tensor
|
||||||
return gain_tensor
|
return gain_tensor
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -372,6 +372,7 @@ def build_tensor_from_sample(
|
||||||
"module_params": module_params_path,
|
"module_params": module_params_path,
|
||||||
"bayer_pattern": bayer,
|
"bayer_pattern": bayer,
|
||||||
"fusion_result": copy_json_safe(getattr(core, "last_fusion_result", None)),
|
"fusion_result": copy_json_safe(getattr(core, "last_fusion_result", None)),
|
||||||
|
"radiometric_normalization_result": copy_json_safe(getattr(core, "last_radiometric_normalization_result", None)),
|
||||||
"patch_normalization_result": copy_json_safe(getattr(core, "last_patch_normalization_result", None)),
|
"patch_normalization_result": copy_json_safe(getattr(core, "last_patch_normalization_result", None)),
|
||||||
"frame_quality": copy_json_safe(getattr(core, "last_frame_quality_result", None)),
|
"frame_quality": copy_json_safe(getattr(core, "last_frame_quality_result", None)),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -288,6 +288,7 @@ class OakFcc3TensorMultiHeadDataset(Dataset):
|
||||||
self.channel_indices = channel_indices
|
self.channel_indices = channel_indices
|
||||||
|
|
||||||
self.samples = self._collect_samples()
|
self.samples = self._collect_samples()
|
||||||
|
self._add_sample_class_stats()
|
||||||
|
|
||||||
if not self.samples:
|
if not self.samples:
|
||||||
raise RuntimeError(f"Nenhuma amostra encontrada em: {self.root}")
|
raise RuntimeError(f"Nenhuma amostra encontrada em: {self.root}")
|
||||||
|
|
@ -450,6 +451,59 @@ class OakFcc3TensorMultiHeadDataset(Dataset):
|
||||||
"base": s["base"],
|
"base": s["base"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _add_sample_class_stats(self):
|
||||||
|
for s in self.samples:
|
||||||
|
stats = {
|
||||||
|
"pixels_total": 0,
|
||||||
|
"pixels_chao": 0,
|
||||||
|
"pixels_cana": 0,
|
||||||
|
"pixels_erva": 0,
|
||||||
|
"pixels_vegetation": 0,
|
||||||
|
"pixels_target": 0,
|
||||||
|
"pct_cana": 0.0,
|
||||||
|
"pct_erva": 0.0,
|
||||||
|
"pct_target": 0.0,
|
||||||
|
"has_cana": False,
|
||||||
|
"has_erva": False,
|
||||||
|
"has_target": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
sem_path = s["masks"].get("semantic")
|
||||||
|
veg_path = s["masks"].get("vegetation")
|
||||||
|
cana_path = s["masks"].get("cana")
|
||||||
|
|
||||||
|
if sem_path is not None and Path(sem_path).exists():
|
||||||
|
sem = np.load(str(sem_path)).astype(np.int64)
|
||||||
|
valid = sem != 255
|
||||||
|
total = int(valid.sum())
|
||||||
|
stats["pixels_total"] = total
|
||||||
|
|
||||||
|
if total > 0:
|
||||||
|
stats["pixels_chao"] = int(((sem == 0) & valid).sum())
|
||||||
|
stats["pixels_cana"] = int(((sem == 1) & valid).sum())
|
||||||
|
stats["pixels_erva"] = int(((sem == 2) & valid).sum())
|
||||||
|
stats["pct_cana"] = stats["pixels_cana"] / total
|
||||||
|
stats["pct_erva"] = stats["pixels_erva"] / total
|
||||||
|
|
||||||
|
if veg_path is not None and cana_path is not None and Path(veg_path).exists() and Path(cana_path).exists():
|
||||||
|
veg = np.load(str(veg_path)).astype(np.int64)
|
||||||
|
cana = np.load(str(cana_path)).astype(np.int64)
|
||||||
|
|
||||||
|
valid = (veg != 255) & (cana != 255)
|
||||||
|
total = int(valid.sum())
|
||||||
|
|
||||||
|
if total > 0:
|
||||||
|
target = (veg == 1) & (cana == 0) & valid
|
||||||
|
stats["pixels_vegetation"] = int(((veg == 1) & valid).sum())
|
||||||
|
stats["pixels_target"] = int(target.sum())
|
||||||
|
stats["pct_target"] = stats["pixels_target"] / total
|
||||||
|
|
||||||
|
stats["has_cana"] = stats["pixels_cana"] > 0
|
||||||
|
stats["has_erva"] = stats["pixels_erva"] > 0
|
||||||
|
stats["has_target"] = stats["pixels_target"] > 0
|
||||||
|
|
||||||
|
s["class_stats"] = stats
|
||||||
|
|
||||||
|
|
||||||
def collate_fn(batch):
|
def collate_fn(batch):
|
||||||
imgs = torch.stack([b["image"] for b in batch], dim=0)
|
imgs = torch.stack([b["image"] for b in batch], dim=0)
|
||||||
|
|
@ -534,6 +588,49 @@ def build_normalizer(config: dict, args, device: torch.device):
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def build_sample_weights(ds, mode="target_focus"):
|
||||||
|
weights = []
|
||||||
|
|
||||||
|
for s in ds.samples:
|
||||||
|
st = s.get("class_stats", {})
|
||||||
|
group = str(s.get("group", "")).lower()
|
||||||
|
|
||||||
|
pct_cana = float(st.get("pct_cana", 0.0))
|
||||||
|
pct_erva = float(st.get("pct_erva", 0.0))
|
||||||
|
pct_target = float(st.get("pct_target", 0.0))
|
||||||
|
|
||||||
|
has_cana = bool(st.get("has_cana", False))
|
||||||
|
has_erva = bool(st.get("has_erva", False))
|
||||||
|
has_target = bool(st.get("has_target", False))
|
||||||
|
|
||||||
|
w = 1.0
|
||||||
|
|
||||||
|
# Reduz chão puro
|
||||||
|
if not has_cana and not has_erva and not has_target:
|
||||||
|
w *= 0.35
|
||||||
|
|
||||||
|
# Aumenta cana
|
||||||
|
if has_cana:
|
||||||
|
w *= 1.25
|
||||||
|
|
||||||
|
# Aumenta erva/target com força
|
||||||
|
if has_erva:
|
||||||
|
w *= 3.0
|
||||||
|
|
||||||
|
if has_target:
|
||||||
|
w *= 4.0
|
||||||
|
|
||||||
|
# Bônus suave por área real de target/erva
|
||||||
|
w *= 1.0 + min(5.0, 80.0 * pct_target)
|
||||||
|
w *= 1.0 + min(3.0, 50.0 * pct_erva)
|
||||||
|
|
||||||
|
# Evita pesos absurdos
|
||||||
|
w = max(0.05, min(w, 20.0))
|
||||||
|
weights.append(w)
|
||||||
|
|
||||||
|
return torch.tensor(weights, dtype=torch.double)
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_CHANNEL_ORDER = ["R", "G", "B", "RE", "NIR"]
|
DEFAULT_CHANNEL_ORDER = ["R", "G", "B", "RE", "NIR"]
|
||||||
|
|
||||||
def get_input_channel_names(config: dict) -> List[str]:
|
def get_input_channel_names(config: dict) -> List[str]:
|
||||||
|
|
@ -1493,6 +1590,9 @@ def main():
|
||||||
|
|
||||||
parser.add_argument("--early-stop", type=int, default=25)
|
parser.add_argument("--early-stop", type=int, default=25)
|
||||||
|
|
||||||
|
parser.add_argument("--balanced_sampler", action="store_true")
|
||||||
|
parser.add_argument("--samples_per_epoch", type=int, default=0)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
set_seed(args.seed)
|
set_seed(args.seed)
|
||||||
|
|
@ -1579,10 +1679,32 @@ def main():
|
||||||
|
|
||||||
print(f"[DATA] train={len(ds_train)} | val={len(ds_val)}")
|
print(f"[DATA] train={len(ds_train)} | val={len(ds_val)}")
|
||||||
|
|
||||||
|
train_sampler = None
|
||||||
|
train_shuffle = True
|
||||||
|
|
||||||
|
if args.balanced_sampler:
|
||||||
|
from torch.utils.data import WeightedRandomSampler
|
||||||
|
sample_weights = build_sample_weights(ds_train)
|
||||||
|
num_samples = int(args.samples_per_epoch) if args.samples_per_epoch > 0 else len(ds_train)
|
||||||
|
|
||||||
|
train_sampler = WeightedRandomSampler(
|
||||||
|
weights=sample_weights,
|
||||||
|
num_samples=num_samples,
|
||||||
|
replacement=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
train_shuffle = False
|
||||||
|
|
||||||
|
print("[SAMPLER] WeightedRandomSampler ativado")
|
||||||
|
print(f"[SAMPLER] peso min={float(sample_weights.min()):.3f} "
|
||||||
|
f"max={float(sample_weights.max()):.3f} "
|
||||||
|
f"mean={float(sample_weights.mean()):.3f}")
|
||||||
|
|
||||||
dl_train = DataLoader(
|
dl_train = DataLoader(
|
||||||
ds_train,
|
ds_train,
|
||||||
batch_size=args.batch,
|
batch_size=args.batch,
|
||||||
shuffle=True,
|
shuffle=train_shuffle,
|
||||||
|
sampler=train_sampler,
|
||||||
num_workers=args.num_workers,
|
num_workers=args.num_workers,
|
||||||
pin_memory=True,
|
pin_memory=True,
|
||||||
collate_fn=collate_fn,
|
collate_fn=collate_fn,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,257 @@
|
||||||
|
# export_depthai_stereo_dataset.py
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# RAW10 unpack / preview
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def unpack_raw10_packed(raw: bytes, width: int, height: int) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
RAW10 packed:
|
||||||
|
5 bytes = 4 pixels de 10 bits.
|
||||||
|
Retorna uint16 HxW em 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 read_raw10_mono_png_ready(path: Path, width: int, height: int, use_clahe: bool) -> 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 gray
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Pairing
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def clean_stem_for_pair(path: Path, cam_key: str) -> 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_pairs(root_dir: Path, left_cam: str, right_cam: str):
|
||||||
|
left_paths = find_cam_bins(root_dir, left_cam)
|
||||||
|
right_paths = find_cam_bins(root_dir, right_cam)
|
||||||
|
|
||||||
|
right_map = {}
|
||||||
|
|
||||||
|
for p in right_paths:
|
||||||
|
key = clean_stem_for_pair(p, right_cam)
|
||||||
|
right_map[(p.parent, key)] = p
|
||||||
|
right_map.setdefault((None, key), p)
|
||||||
|
|
||||||
|
pairs = []
|
||||||
|
|
||||||
|
for lp in left_paths:
|
||||||
|
key = clean_stem_for_pair(lp, left_cam)
|
||||||
|
folder = lp.parent
|
||||||
|
|
||||||
|
rp = right_map.get((folder, key)) or right_map.get((None, key))
|
||||||
|
|
||||||
|
if rp is None:
|
||||||
|
same_folder = [x for x in right_paths if x.parent == folder]
|
||||||
|
if len(same_folder) == 1:
|
||||||
|
rp = same_folder[0]
|
||||||
|
|
||||||
|
if rp is not None:
|
||||||
|
pairs.append((lp, rp))
|
||||||
|
|
||||||
|
return pairs, left_paths, right_paths
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Main
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
|
||||||
|
parser.add_argument("--root_dir", required=True, help="Pasta onde estão os .bin CAM_A/CAM_B/CAM_C")
|
||||||
|
parser.add_argument("--out_dir", required=True, help="Pasta de saída. Ex: C:/dev/depthai/dataset")
|
||||||
|
|
||||||
|
parser.add_argument("--width", type=int, default=1280)
|
||||||
|
parser.add_argument("--height", type=int, default=800)
|
||||||
|
|
||||||
|
# Para DepthAI stereo:
|
||||||
|
# left = CAM_C / NIR
|
||||||
|
# right = CAM_B / RE
|
||||||
|
parser.add_argument("--left_cam", default="CAM_C")
|
||||||
|
parser.add_argument("--right_cam", default="CAM_B")
|
||||||
|
|
||||||
|
parser.add_argument("--prefix", default="p", help="Prefixo dos arquivos. Default: p")
|
||||||
|
parser.add_argument("--suffix", default="_0", help="Sufixo depois do índice. Default: _0")
|
||||||
|
parser.add_argument("--start_index", type=int, default=0)
|
||||||
|
|
||||||
|
parser.add_argument("--no_clahe", action="store_true")
|
||||||
|
parser.add_argument("--overwrite", action="store_true")
|
||||||
|
parser.add_argument("--save_preview", action="store_true")
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--images_per_pose",
|
||||||
|
type=int,
|
||||||
|
default=3,
|
||||||
|
help="Quantidade de imagens por pose para gerar nomes tipo p0_0, p0_1, p0_2, p1_3..."
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
root_dir = Path(args.root_dir)
|
||||||
|
out_dir = Path(args.out_dir)
|
||||||
|
|
||||||
|
left_dir = out_dir / "left"
|
||||||
|
right_dir = out_dir / "right"
|
||||||
|
|
||||||
|
left_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
right_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
preview_dir = out_dir / "_preview_pairs"
|
||||||
|
if args.save_preview:
|
||||||
|
preview_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
pairs, left_paths, right_paths = find_pairs(root_dir, args.left_cam, args.right_cam)
|
||||||
|
|
||||||
|
print(f"[INFO] root_dir={root_dir}")
|
||||||
|
print(f"[INFO] out_dir={out_dir}")
|
||||||
|
print(f"[INFO] left_cam={args.left_cam} -> {left_dir}")
|
||||||
|
print(f"[INFO] right_cam={args.right_cam} -> {right_dir}")
|
||||||
|
print(f"[INFO] arquivos left encontrados: {len(left_paths)}")
|
||||||
|
print(f"[INFO] arquivos right encontrados: {len(right_paths)}")
|
||||||
|
print(f"[INFO] pares encontrados: {len(pairs)}")
|
||||||
|
print(f"[INFO] size={args.width}x{args.height}")
|
||||||
|
print(f"[INFO] clahe={not args.no_clahe}")
|
||||||
|
|
||||||
|
if not pairs:
|
||||||
|
raise RuntimeError("Nenhum par encontrado. Verifique nomes dos arquivos e CAMs.")
|
||||||
|
|
||||||
|
for i, (left_path, right_path) in enumerate(pairs):
|
||||||
|
idx = args.start_index + i
|
||||||
|
pose_idx = idx // args.images_per_pose
|
||||||
|
name = f"{args.prefix}{pose_idx}_{idx}.png"
|
||||||
|
|
||||||
|
left_out = left_dir / name
|
||||||
|
right_out = right_dir / name
|
||||||
|
|
||||||
|
if not args.overwrite and (left_out.exists() or right_out.exists()):
|
||||||
|
print(f"[SKIP] {name} já existe. Use --overwrite para sobrescrever.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
left_img = read_raw10_mono_png_ready(
|
||||||
|
left_path,
|
||||||
|
width=args.width,
|
||||||
|
height=args.height,
|
||||||
|
use_clahe=not args.no_clahe,
|
||||||
|
)
|
||||||
|
|
||||||
|
right_img = read_raw10_mono_png_ready(
|
||||||
|
right_path,
|
||||||
|
width=args.width,
|
||||||
|
height=args.height,
|
||||||
|
use_clahe=not args.no_clahe,
|
||||||
|
)
|
||||||
|
|
||||||
|
cv2.imwrite(str(left_out), left_img)
|
||||||
|
cv2.imwrite(str(right_out), right_img)
|
||||||
|
|
||||||
|
if args.save_preview:
|
||||||
|
left_bgr = cv2.cvtColor(left_img, cv2.COLOR_GRAY2BGR)
|
||||||
|
right_bgr = cv2.cvtColor(right_img, cv2.COLOR_GRAY2BGR)
|
||||||
|
|
||||||
|
cv2.putText(left_bgr, f"left {args.left_cam}", (20, 35),
|
||||||
|
cv2.FONT_HERSHEY_SIMPLEX, 0.9, (255, 255, 255), 2, cv2.LINE_AA)
|
||||||
|
cv2.putText(right_bgr, f"right {args.right_cam}", (20, 35),
|
||||||
|
cv2.FONT_HERSHEY_SIMPLEX, 0.9, (255, 255, 255), 2, cv2.LINE_AA)
|
||||||
|
|
||||||
|
preview = np.hstack([left_bgr, right_bgr])
|
||||||
|
preview = cv2.resize(preview, None, fx=0.5, fy=0.5, interpolation=cv2.INTER_AREA)
|
||||||
|
cv2.imwrite(str(preview_dir / name), preview)
|
||||||
|
|
||||||
|
print(f"[OK] {idx:04d}: {left_path.name} -> left/{name} | {right_path.name} -> right/{name}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERRO] par {i}:")
|
||||||
|
print(f" left ={left_path}")
|
||||||
|
print(f" right={right_path}")
|
||||||
|
print(f" erro ={e}")
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print("[DONE] Dataset exportado.")
|
||||||
|
print(f" left : {left_dir}")
|
||||||
|
print(f" right: {right_dir}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,307 @@
|
||||||
|
import argparse
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
def get_aruco_dict(name: str):
|
||||||
|
aruco = cv2.aruco
|
||||||
|
name = name.upper()
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
if name not in mapping:
|
||||||
|
raise RuntimeError(f"Dicionário ArUco não suportado: {name}")
|
||||||
|
|
||||||
|
if hasattr(aruco, "Dictionary_get"):
|
||||||
|
return aruco.Dictionary_get(mapping[name])
|
||||||
|
|
||||||
|
return aruco.getPredefinedDictionary(mapping[name])
|
||||||
|
|
||||||
|
|
||||||
|
def count_markers(path: Path, aruco_dict, use_clahe: bool):
|
||||||
|
img = cv2.imread(str(path), cv2.IMREAD_GRAYSCALE)
|
||||||
|
|
||||||
|
if img is None:
|
||||||
|
return 0, None
|
||||||
|
|
||||||
|
proc = img
|
||||||
|
|
||||||
|
if use_clahe:
|
||||||
|
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
|
||||||
|
proc = clahe.apply(proc)
|
||||||
|
|
||||||
|
corners, ids, rejected = cv2.aruco.detectMarkers(proc, aruco_dict)
|
||||||
|
|
||||||
|
count = 0 if ids is None else len(ids)
|
||||||
|
return count, proc
|
||||||
|
|
||||||
|
|
||||||
|
def create_charuco_board(squares_x, squares_y, square_size_cm, marker_size_cm, aruco_dict):
|
||||||
|
return cv2.aruco.CharucoBoard_create(
|
||||||
|
int(squares_x),
|
||||||
|
int(squares_y),
|
||||||
|
float(square_size_cm),
|
||||||
|
float(marker_size_cm),
|
||||||
|
aruco_dict
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def count_charuco(path: Path, aruco_dict, board, use_clahe: bool):
|
||||||
|
img = cv2.imread(str(path), cv2.IMREAD_GRAYSCALE)
|
||||||
|
|
||||||
|
if img is None:
|
||||||
|
return 0, 0, None
|
||||||
|
|
||||||
|
proc = img.copy()
|
||||||
|
|
||||||
|
if use_clahe:
|
||||||
|
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
|
||||||
|
proc = clahe.apply(proc)
|
||||||
|
|
||||||
|
marker_corners, marker_ids, rejected = cv2.aruco.detectMarkers(proc, aruco_dict)
|
||||||
|
|
||||||
|
marker_count = 0 if marker_ids is None else len(marker_ids)
|
||||||
|
|
||||||
|
if marker_ids is None or marker_count == 0:
|
||||||
|
return marker_count, 0, proc
|
||||||
|
|
||||||
|
try:
|
||||||
|
cv2.aruco.refineDetectedMarkers(
|
||||||
|
proc,
|
||||||
|
board,
|
||||||
|
marker_corners,
|
||||||
|
marker_ids,
|
||||||
|
rejectedCorners=rejected
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
ret, charuco_corners, charuco_ids = cv2.aruco.interpolateCornersCharuco(
|
||||||
|
marker_corners,
|
||||||
|
marker_ids,
|
||||||
|
proc,
|
||||||
|
board,
|
||||||
|
minMarkers=1
|
||||||
|
)
|
||||||
|
|
||||||
|
charuco_count = 0 if charuco_ids is None else len(charuco_ids)
|
||||||
|
|
||||||
|
return marker_count, charuco_count, proc
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
|
||||||
|
parser.add_argument("--dataset_dir", required=True, help="Pasta dataset com left/ e right/")
|
||||||
|
parser.add_argument("--out_dir", default=None, help="Se informado, cria dataset filtrado em outra pasta")
|
||||||
|
parser.add_argument("--min_markers", type=int, default=4)
|
||||||
|
parser.add_argument("--aruco_dict", default="4X4_1000")
|
||||||
|
parser.add_argument("--images_per_pose", type=int, default=3)
|
||||||
|
parser.add_argument("--squares_x", type=int, default=13)
|
||||||
|
parser.add_argument("--squares_y", type=int, default=7)
|
||||||
|
parser.add_argument("--square_size_cm", type=float, default=3.1)
|
||||||
|
parser.add_argument("--marker_size_cm", type=float, default=2.3)
|
||||||
|
parser.add_argument("--min_charuco", type=int, default=20)
|
||||||
|
parser.add_argument("--no_clahe", action="store_true")
|
||||||
|
parser.add_argument("--max_pairs", type=int, default=0, help="Limita a quantidade final de pares exportados. Use 39 para DepthAI padrão: 13 poses x 3 imagens.")
|
||||||
|
parser.add_argument("--overwrite", action="store_true")
|
||||||
|
parser.add_argument("--copy", action="store_true", help="Copia em vez de mover/reescrever")
|
||||||
|
parser.add_argument("--save_debug", action="store_true")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
dataset_dir = Path(args.dataset_dir)
|
||||||
|
left_dir = dataset_dir / "left"
|
||||||
|
right_dir = dataset_dir / "right"
|
||||||
|
|
||||||
|
if not left_dir.exists() or not right_dir.exists():
|
||||||
|
raise RuntimeError(f"Dataset precisa ter left/ e right/: {dataset_dir}")
|
||||||
|
|
||||||
|
out_dir = Path(args.out_dir) if args.out_dir else dataset_dir
|
||||||
|
out_left = out_dir / "left"
|
||||||
|
out_right = out_dir / "right"
|
||||||
|
rejected_dir = out_dir / "_rejected"
|
||||||
|
debug_dir = out_dir / "_debug_marker_check"
|
||||||
|
|
||||||
|
if args.out_dir:
|
||||||
|
if out_dir.exists() and args.overwrite:
|
||||||
|
shutil.rmtree(out_dir)
|
||||||
|
|
||||||
|
out_left.mkdir(parents=True, exist_ok=True)
|
||||||
|
out_right.mkdir(parents=True, exist_ok=True)
|
||||||
|
else:
|
||||||
|
# Se for filtrar in-place, primeiro joga tudo para staging.
|
||||||
|
staging_dir = dataset_dir / "_staging_original"
|
||||||
|
if staging_dir.exists() and args.overwrite:
|
||||||
|
shutil.rmtree(staging_dir)
|
||||||
|
|
||||||
|
if staging_dir.exists():
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Staging já existe: {staging_dir}. "
|
||||||
|
f"Apague manualmente ou use --overwrite."
|
||||||
|
)
|
||||||
|
|
||||||
|
staging_left = staging_dir / "left"
|
||||||
|
staging_right = staging_dir / "right"
|
||||||
|
staging_left.mkdir(parents=True, exist_ok=True)
|
||||||
|
staging_right.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
for p in left_dir.glob("*.png"):
|
||||||
|
shutil.move(str(p), str(staging_left / p.name))
|
||||||
|
for p in right_dir.glob("*.png"):
|
||||||
|
shutil.move(str(p), str(staging_right / p.name))
|
||||||
|
|
||||||
|
left_dir = staging_left
|
||||||
|
right_dir = staging_right
|
||||||
|
|
||||||
|
out_left.mkdir(parents=True, exist_ok=True)
|
||||||
|
out_right.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
rejected_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
if args.save_debug:
|
||||||
|
debug_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
aruco_dict = get_aruco_dict(args.aruco_dict)
|
||||||
|
|
||||||
|
board = create_charuco_board(
|
||||||
|
args.squares_x,
|
||||||
|
args.squares_y,
|
||||||
|
args.square_size_cm,
|
||||||
|
args.marker_size_cm,
|
||||||
|
aruco_dict
|
||||||
|
)
|
||||||
|
|
||||||
|
left_files = sorted(left_dir.glob("*.png"))
|
||||||
|
right_map = {p.name: p for p in right_dir.glob("*.png")}
|
||||||
|
|
||||||
|
valid_pairs = []
|
||||||
|
rejected = []
|
||||||
|
|
||||||
|
print(f"[INFO] dataset_dir={dataset_dir}")
|
||||||
|
print(f"[INFO] out_dir={out_dir}")
|
||||||
|
print(f"[INFO] left files={len(left_files)}")
|
||||||
|
print(f"[INFO] aruco_dict={args.aruco_dict}")
|
||||||
|
print(f"[INFO] min_markers={args.min_markers}")
|
||||||
|
print(f"[INFO] clahe={not args.no_clahe}")
|
||||||
|
|
||||||
|
for left_path in left_files:
|
||||||
|
right_path = right_map.get(left_path.name)
|
||||||
|
|
||||||
|
if right_path is None:
|
||||||
|
rejected.append((left_path, None, "missing_right", 0, 0))
|
||||||
|
continue
|
||||||
|
|
||||||
|
left_markers, left_charuco, left_img = count_charuco(
|
||||||
|
left_path,
|
||||||
|
aruco_dict,
|
||||||
|
board,
|
||||||
|
use_clahe=not args.no_clahe
|
||||||
|
)
|
||||||
|
|
||||||
|
right_markers, right_charuco, right_img = count_charuco(
|
||||||
|
right_path,
|
||||||
|
aruco_dict,
|
||||||
|
board,
|
||||||
|
use_clahe=not args.no_clahe
|
||||||
|
)
|
||||||
|
|
||||||
|
ok = (
|
||||||
|
left_markers >= args.min_markers and
|
||||||
|
right_markers >= args.min_markers and
|
||||||
|
left_charuco >= args.min_charuco and
|
||||||
|
right_charuco >= args.min_charuco
|
||||||
|
)
|
||||||
|
|
||||||
|
if ok:
|
||||||
|
valid_pairs.append((left_path, right_path, left_charuco, right_charuco))
|
||||||
|
print(
|
||||||
|
f"[OK] {left_path.name}: "
|
||||||
|
f"left markers={left_markers} charuco={left_charuco} | "
|
||||||
|
f"right markers={right_markers} charuco={right_charuco}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
rejected.append((left_path, right_path, "low_charuco", left_charuco, right_charuco))
|
||||||
|
print(
|
||||||
|
f"[REJECT] {left_path.name}: "
|
||||||
|
f"left markers={left_markers} charuco={left_charuco} | "
|
||||||
|
f"right markers={right_markers} charuco={right_charuco}"
|
||||||
|
)
|
||||||
|
|
||||||
|
rej_left_dir = rejected_dir / "left"
|
||||||
|
rej_right_dir = rejected_dir / "right"
|
||||||
|
rej_left_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
rej_right_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
shutil.copy2(left_path, rej_left_dir / left_path.name)
|
||||||
|
shutil.copy2(right_path, rej_right_dir / right_path.name)
|
||||||
|
|
||||||
|
if args.save_debug and left_img is not None and right_img is not None:
|
||||||
|
left_bgr = cv2.cvtColor(left_img, cv2.COLOR_GRAY2BGR)
|
||||||
|
right_bgr = cv2.cvtColor(right_img, cv2.COLOR_GRAY2BGR)
|
||||||
|
|
||||||
|
cv2.putText(left_bgr, f"left markers={left_markers}", (20, 35),
|
||||||
|
cv2.FONT_HERSHEY_SIMPLEX, 0.9, (255, 255, 255), 2, cv2.LINE_AA)
|
||||||
|
cv2.putText(right_bgr, f"right markers={right_markers}", (20, 35),
|
||||||
|
cv2.FONT_HERSHEY_SIMPLEX, 0.9, (255, 255, 255), 2, cv2.LINE_AA)
|
||||||
|
|
||||||
|
dbg = np.hstack([left_bgr, right_bgr])
|
||||||
|
cv2.imwrite(str(debug_dir / left_path.name), dbg)
|
||||||
|
|
||||||
|
valid_pairs = sorted(
|
||||||
|
valid_pairs,
|
||||||
|
key=lambda x: min(x[2], x[3]),
|
||||||
|
reverse=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if args.max_pairs > 0:
|
||||||
|
valid_pairs = valid_pairs[:args.max_pairs]
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print(f"[INFO] pares válidos: {len(valid_pairs)}")
|
||||||
|
print(f"[INFO] pares rejeitados: {len(rejected)}")
|
||||||
|
|
||||||
|
if len(valid_pairs) < 10:
|
||||||
|
print("[WARN] Poucos pares válidos. Talvez o dicionário ArUco esteja errado ou as imagens estejam ruins.")
|
||||||
|
|
||||||
|
# Reindexa os válidos no padrão DepthAI: p0_0, p0_1, p0_2, p1_3...
|
||||||
|
for new_idx, (left_path, right_path, left_markers, right_markers) in enumerate(valid_pairs):
|
||||||
|
pose_idx = new_idx // args.images_per_pose
|
||||||
|
new_name = f"p{pose_idx}_{new_idx}.png"
|
||||||
|
|
||||||
|
dst_left = out_left / new_name
|
||||||
|
dst_right = out_right / new_name
|
||||||
|
|
||||||
|
if dst_left.exists() or dst_right.exists():
|
||||||
|
if not args.overwrite:
|
||||||
|
raise RuntimeError(f"Arquivo já existe: {new_name}. Use --overwrite.")
|
||||||
|
dst_left.unlink(missing_ok=True)
|
||||||
|
dst_right.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
if args.copy or args.out_dir:
|
||||||
|
shutil.copy2(left_path, dst_left)
|
||||||
|
shutil.copy2(right_path, dst_right)
|
||||||
|
else:
|
||||||
|
shutil.copy2(left_path, dst_left)
|
||||||
|
shutil.copy2(right_path, dst_right)
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print("[DONE] Dataset filtrado/reindexado.")
|
||||||
|
print(f" left : {out_left}")
|
||||||
|
print(f" right: {out_right}")
|
||||||
|
print(f" rejected: {rejected_dir}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -58,41 +58,184 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"homography_calibration_size": [1280, 800],
|
"homography_calibration_size": [1280, 800],
|
||||||
|
"homography_profile": "baixa",
|
||||||
|
"homography_profile_by_role": {
|
||||||
|
"re": "baixa",
|
||||||
|
"nir": "baixa"
|
||||||
|
},
|
||||||
|
"homography_profiles": {
|
||||||
|
"baixa": {
|
||||||
|
"description": "Plano mais baixo/distante da câmera, normalmente mais próximo do chão.",
|
||||||
|
"depth": 120.0,
|
||||||
|
"homography_calibration_size": [1280, 800],
|
||||||
|
"homography_source": "all_valid_triplets",
|
||||||
|
"homography_stats": {
|
||||||
|
"re_total_points": 267,
|
||||||
|
"re_inliers": 267,
|
||||||
|
"re_inlier_pct": 100.0,
|
||||||
|
"nir_total_points": 282,
|
||||||
|
"nir_inliers": 276,
|
||||||
|
"nir_inlier_pct": 97.87234042553192,
|
||||||
|
"re_frames_used": 13,
|
||||||
|
"nir_frames_used": 13,
|
||||||
|
"common_frames_used": 13,
|
||||||
|
"overlap_common_pct": 92.09267578125
|
||||||
|
},
|
||||||
"homographies": {
|
"homographies": {
|
||||||
"re_to_rgb": [
|
"re_to_rgb": [
|
||||||
[
|
[
|
||||||
1.0103099557765387,
|
1.014814963583485,
|
||||||
0.00879456142897448,
|
0.015257560646391,
|
||||||
-9.666692994320178
|
-27.5415127674596
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
-0.00014434784827104316,
|
0.000064618247568,
|
||||||
1.0214419841673288,
|
1.020756196943496,
|
||||||
50.41247184630176
|
51.80289590472656
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
6.270900951674823e-06,
|
0.000003411885963,
|
||||||
1.788434277122896e-05,
|
0.0000210631067,
|
||||||
1.0
|
1.0
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"nir_to_rgb": [
|
"nir_to_rgb": [
|
||||||
[
|
[
|
||||||
0.994769714806237,
|
0.993261705358079,
|
||||||
0.005296878287321148,
|
0.008537634512529,
|
||||||
-6.433772056422987
|
-19.84130896890106
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
-0.00092900679814922,
|
-0.004568701240477,
|
||||||
1.0017494877636166,
|
1.002385627262319,
|
||||||
29.883379280823718
|
37.83216994660247
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
-5.35529902496836e-08,
|
-0.00000725852956,
|
||||||
7.331793189295934e-06,
|
0.000011588890195,
|
||||||
1.0
|
1.0
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"media": {
|
||||||
|
"description": "Plano médio, calibrado com ChArUco a aproximadamente 66 cm da lente.",
|
||||||
|
"depth": 66.0,
|
||||||
|
"homography_calibration_size": [1280, 800],
|
||||||
|
"homography_source": "all_valid_triplets",
|
||||||
|
"homography_stats": {
|
||||||
|
"min_common_frame": 4,
|
||||||
|
"min_total_points": 30,
|
||||||
|
"re_total_points": 681,
|
||||||
|
"re_inliers": 666,
|
||||||
|
"re_inlier_pct": 97.79735682819384,
|
||||||
|
"nir_total_points": 682,
|
||||||
|
"nir_inliers": 662,
|
||||||
|
"nir_inlier_pct": 97.0674486803519,
|
||||||
|
"re_frames_used": 10,
|
||||||
|
"nir_frames_used": 10,
|
||||||
|
"common_frames_used": 10,
|
||||||
|
"overlap_re_pct": 92.06279296874999,
|
||||||
|
"overlap_nir_pct": 92.97744140625001,
|
||||||
|
"overlap_common_pct": 90.9361328125
|
||||||
|
},
|
||||||
|
"homographies": {
|
||||||
|
"re_to_rgb": [
|
||||||
|
[
|
||||||
|
1.0178493693104127,
|
||||||
|
0.01417807024780309,
|
||||||
|
-21.68597109171115
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.0010063750504059967,
|
||||||
|
1.0231248578982763,
|
||||||
|
57.68510361307461
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.000003886299643192958,
|
||||||
|
0.0000197941570901694,
|
||||||
|
1.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"nir_to_rgb": [
|
||||||
|
[
|
||||||
|
0.9967828768745967,
|
||||||
|
0.007204312783804796,
|
||||||
|
-27.956111536702632
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-0.0033537915240536544,
|
||||||
|
1.005150033976991,
|
||||||
|
43.58722588915352
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-0.000005956433574512551,
|
||||||
|
0.000010346309041666395,
|
||||||
|
1.0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"alta": {
|
||||||
|
"description": "Plano mais alto/próximo da câmera, calibrado com ChArUco acima do plano médio.",
|
||||||
|
"depth": 36.0,
|
||||||
|
"homography_calibration_size": [1280, 800],
|
||||||
|
"homography_source": "all_valid_triplets",
|
||||||
|
"homography_stats": {
|
||||||
|
"min_common_frame": 4,
|
||||||
|
"min_total_points": 30,
|
||||||
|
"re_total_points": 336,
|
||||||
|
"re_inliers": 305,
|
||||||
|
"re_inlier_pct": 90.77380952380952,
|
||||||
|
"nir_total_points": 333,
|
||||||
|
"nir_inliers": 330,
|
||||||
|
"nir_inlier_pct": 99.09909909909909,
|
||||||
|
"re_frames_used": 5,
|
||||||
|
"nir_frames_used": 5,
|
||||||
|
"common_frames_used": 5,
|
||||||
|
"overlap_re_pct": 91.26904296875,
|
||||||
|
"overlap_nir_pct": 90.83525390625,
|
||||||
|
"overlap_common_pct": 88.8357421875
|
||||||
|
},
|
||||||
|
"homographies": {
|
||||||
|
"re_to_rgb": [
|
||||||
|
[
|
||||||
|
1.022058360214937,
|
||||||
|
0.013411355134898,
|
||||||
|
-11.893143823943664
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.001034227529954,
|
||||||
|
1.026408605195186,
|
||||||
|
69.3538160647828
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.000003600847244,
|
||||||
|
0.000019493014216,
|
||||||
|
1.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"nir_to_rgb": [
|
||||||
|
[
|
||||||
|
1.001478266241777,
|
||||||
|
0.007623070623269,
|
||||||
|
-43.01507447243508
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-0.002877662918552,
|
||||||
|
1.008411842924782,
|
||||||
|
55.15260407195015
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-0.000005444722264,
|
||||||
|
0.000009647719914,
|
||||||
|
1.0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"crop_valid_common": true,
|
"crop_valid_common": true,
|
||||||
"resize_after_crop": true,
|
"resize_after_crop": true,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"camera": "oak-fcc-3",
|
"camera": "oak-fcc-3",
|
||||||
"modelo": "segformer_b1",
|
"modelo": "segformer_b1",
|
||||||
"model_name": "target_fixed",
|
"model_name": "copypaste",
|
||||||
"main_class_name": "cana",
|
"main_class_name": "cana",
|
||||||
"es_classes": "",
|
"es_classes": "",
|
||||||
"model_to_use": "geral",
|
"model_to_use": "geral",
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
"mask_dir": "masks",
|
"mask_dir": "masks",
|
||||||
"classes": {"chao": 0, "cana": 1, "erva": 2},
|
"classes": {"chao": 0, "cana": 1, "erva": 2},
|
||||||
"ignore_index": 255,
|
"ignore_index": 255,
|
||||||
"loss_weight": 0.10
|
"loss_weight": 0.20
|
||||||
},
|
},
|
||||||
"vegetation": {
|
"vegetation": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
|
@ -54,7 +54,7 @@
|
||||||
"mask_dir": "__derived_target__",
|
"mask_dir": "__derived_target__",
|
||||||
"classes": {"background": 0, "target": 1},
|
"classes": {"background": 0, "target": 1},
|
||||||
"ignore_index": 255,
|
"ignore_index": 255,
|
||||||
"loss_weight": 0.45,
|
"loss_weight": 0.35,
|
||||||
"derived": true
|
"derived": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1090,17 +1090,7 @@ class RawProcessorCore:
|
||||||
warped_mask = self._affine_image(mask, dx, dy, theta_deg)
|
warped_mask = self._affine_image(mask, dx, dy, theta_deg)
|
||||||
|
|
||||||
elif mode == "homography":
|
elif mode == "homography":
|
||||||
H = cfg.get("homographies", {}).get(f"{role}_to_rgb")
|
H, calib_size, profile_name = self._resolve_homography_entry_for_role(role)
|
||||||
|
|
||||||
if H is None:
|
|
||||||
raise RuntimeError(
|
|
||||||
f"fusion_config.alignment_mode='homography', "
|
|
||||||
f"mas homografia '{role}_to_rgb' está ausente. "
|
|
||||||
f"Isso deixaria o canal {role.upper()} sem alinhamento."
|
|
||||||
)
|
|
||||||
|
|
||||||
calib_size = cfg.get("homography_calibration_size", None)
|
|
||||||
|
|
||||||
H = self._scale_homography_to_runtime(
|
H = self._scale_homography_to_runtime(
|
||||||
H,
|
H,
|
||||||
calib_size=calib_size,
|
calib_size=calib_size,
|
||||||
|
|
@ -3611,22 +3601,30 @@ class RawProcessorCore:
|
||||||
def _direct_fusion_get_role_homography_fast(self, role, meta, ref_size):
|
def _direct_fusion_get_role_homography_fast(self, role, meta, ref_size):
|
||||||
"""
|
"""
|
||||||
Retorna H_role_to_rgb escalada para o espaço da referência RGB.
|
Retorna H_role_to_rgb escalada para o espaço da referência RGB.
|
||||||
|
|
||||||
|
Suporta:
|
||||||
|
- contrato antigo: fusion_config.homographies.re_to_rgb/nir_to_rgb
|
||||||
|
- contrato novo: fusion_config.homography_profiles.<perfil>.homographies.*
|
||||||
"""
|
"""
|
||||||
role = str(role).lower()
|
role = str(role).lower()
|
||||||
fusion = getattr(self, "fusion_config", {}) or {}
|
|
||||||
homographies = fusion.get("homographies", {}) or {}
|
|
||||||
|
|
||||||
key = f"{role}_to_rgb"
|
H, calib_size, profile_name = self._resolve_homography_entry_for_role(role)
|
||||||
H = homographies.get(key)
|
|
||||||
|
|
||||||
if H is None:
|
ref_h, ref_w = int(ref_size[0]), int(ref_size[1])
|
||||||
# Fallbacks para contratos diferentes.
|
|
||||||
H = homographies.get(role)
|
|
||||||
|
|
||||||
if H is None:
|
H_scaled = self._scale_homography_to_runtime(
|
||||||
raise RuntimeError(f"Homografia ausente para role={role}. Esperado fusion_config.homographies.{key}")
|
H,
|
||||||
|
calib_size=calib_size,
|
||||||
|
runtime_size=(ref_w, ref_h),
|
||||||
|
)
|
||||||
|
|
||||||
return self._direct_fusion_scale_homography_for_ref_fast(H, meta, ref_size)
|
if H_scaled is None or H_scaled.shape != (3, 3):
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Homografia inválida para role={role}, profile={profile_name}: "
|
||||||
|
f"shape={None if H_scaled is None else H_scaled.shape}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return H_scaled.astype(np.float32)
|
||||||
|
|
||||||
def _direct_fusion_resize_spec_to_ref_if_needed_fast(self, img, ref_size):
|
def _direct_fusion_resize_spec_to_ref_if_needed_fast(self, img, ref_size):
|
||||||
"""
|
"""
|
||||||
|
|
@ -3770,6 +3768,7 @@ class RawProcessorCore:
|
||||||
"geometry_cache_hit": bool(geom.get("prepare_cache_hit", False)),
|
"geometry_cache_hit": bool(geom.get("prepare_cache_hit", False)),
|
||||||
"geometry_cache_hits": int(geom.get("cache_hits", 0)),
|
"geometry_cache_hits": int(geom.get("cache_hits", 0)),
|
||||||
"geometry_cache_misses": int(geom.get("cache_misses", 0)),
|
"geometry_cache_misses": int(geom.get("cache_misses", 0)),
|
||||||
|
"homography_profiles_used": geom.get("homography_profiles_used", {}),
|
||||||
}
|
}
|
||||||
|
|
||||||
tensor = np.empty((int(channels_expected), target_h, target_w), dtype=np.float32)
|
tensor = np.empty((int(channels_expected), target_h, target_w), dtype=np.float32)
|
||||||
|
|
@ -3962,34 +3961,38 @@ class RawProcessorCore:
|
||||||
"""
|
"""
|
||||||
Chave simples e estável para cache da geometria.
|
Chave simples e estável para cache da geometria.
|
||||||
|
|
||||||
A geometria depende de:
|
Considera:
|
||||||
- tamanho do RGB de referência
|
- tamanho do RGB/ref
|
||||||
- target final
|
- target final
|
||||||
- roles presentes
|
- roles presentes
|
||||||
- crop_valid_common / resize_after_crop
|
- crop/resize
|
||||||
- homografias e calibration_size
|
- homografia efetivamente selecionada por perfil
|
||||||
|
- calibration_size efetivo por role
|
||||||
Para evitar custo de serializar o JSON todo por frame, usamos uma versão
|
|
||||||
simples. Se você editar module_params em runtime, chame
|
|
||||||
clear_direct_fusion_geometry_cache().
|
|
||||||
"""
|
"""
|
||||||
ref_h, ref_w = int(ref_size[0]), int(ref_size[1])
|
ref_h, ref_w = int(ref_size[0]), int(ref_size[1])
|
||||||
target_w, target_h = int(target_size[0]), int(target_size[1])
|
target_w, target_h = int(target_size[0]), int(target_size[1])
|
||||||
|
|
||||||
fusion = getattr(self, "fusion_config", {}) or {}
|
fusion = getattr(self, "fusion_config", {}) or {}
|
||||||
homographies = fusion.get("homographies", {}) or {}
|
|
||||||
|
|
||||||
# Pequena assinatura numérica das homografias.
|
|
||||||
def h_sig(key):
|
|
||||||
H = homographies.get(key)
|
|
||||||
if H is None:
|
|
||||||
return None
|
|
||||||
arr = np.asarray(H, dtype=np.float32).reshape(-1)
|
|
||||||
# arredonda para evitar ruído float/json, mas detecta mudança real.
|
|
||||||
return tuple(np.round(arr, 8).tolist())
|
|
||||||
|
|
||||||
roles = tuple(sorted([str(r).lower() for r in role_to_cam.keys()]))
|
roles = tuple(sorted([str(r).lower() for r in role_to_cam.keys()]))
|
||||||
|
|
||||||
|
def h_sig_for_role(role):
|
||||||
|
role = str(role).lower()
|
||||||
|
|
||||||
|
if role not in role_to_cam:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
H, calib_size, profile_name = self._resolve_homography_entry_for_role(role)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
arr = np.asarray(H, dtype=np.float32).reshape(-1)
|
||||||
|
return (
|
||||||
|
str(profile_name),
|
||||||
|
tuple(calib_size or []),
|
||||||
|
tuple(np.round(arr, 8).tolist()),
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
ref_w,
|
ref_w,
|
||||||
ref_h,
|
ref_h,
|
||||||
|
|
@ -3998,9 +4001,8 @@ class RawProcessorCore:
|
||||||
roles,
|
roles,
|
||||||
bool(fusion.get("crop_valid_common", False)),
|
bool(fusion.get("crop_valid_common", False)),
|
||||||
bool(fusion.get("resize_after_crop", False)),
|
bool(fusion.get("resize_after_crop", False)),
|
||||||
tuple(fusion.get("homography_calibration_size") or fusion.get("calibration_size") or []),
|
h_sig_for_role("re"),
|
||||||
h_sig("re_to_rgb"),
|
h_sig_for_role("nir"),
|
||||||
h_sig("nir_to_rgb"),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def clear_direct_fusion_geometry_cache(self):
|
def clear_direct_fusion_geometry_cache(self):
|
||||||
|
|
@ -4053,8 +4055,14 @@ class RawProcessorCore:
|
||||||
# Homografias escaladas para runtime.
|
# Homografias escaladas para runtime.
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
H_role_to_rgb = {}
|
H_role_to_rgb = {}
|
||||||
|
homography_profiles_used = {}
|
||||||
for role in ("re", "nir"):
|
for role in ("re", "nir"):
|
||||||
if role in role_to_cam:
|
if role in role_to_cam:
|
||||||
|
H_raw, calib_size, profile_name = self._resolve_homography_entry_for_role(role)
|
||||||
|
homography_profiles_used[role] = {
|
||||||
|
"profile": profile_name,
|
||||||
|
"calib_size": list(calib_size) if calib_size is not None else None,
|
||||||
|
}
|
||||||
H_role_to_rgb[role] = self._direct_fusion_get_role_homography_fast(role, meta, ref_size)
|
H_role_to_rgb[role] = self._direct_fusion_get_role_homography_fast(role, meta, ref_size)
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
|
|
@ -4103,6 +4111,7 @@ class RawProcessorCore:
|
||||||
"prepare_cache_hit": False,
|
"prepare_cache_hit": False,
|
||||||
"cache_hits": int(self._direct_fusion_geometry_cache_hits),
|
"cache_hits": int(self._direct_fusion_geometry_cache_hits),
|
||||||
"cache_misses": int(self._direct_fusion_geometry_cache_misses),
|
"cache_misses": int(self._direct_fusion_geometry_cache_misses),
|
||||||
|
"homography_profiles_used": homography_profiles_used,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Cache pequeno: normalmente só uma geometria. Se mudar resolução/config,
|
# Cache pequeno: normalmente só uma geometria. Se mudar resolução/config,
|
||||||
|
|
@ -4376,6 +4385,120 @@ class RawProcessorCore:
|
||||||
|
|
||||||
tensor[int(channel_index)] = out
|
tensor[int(channel_index)] = out
|
||||||
|
|
||||||
|
def _resolve_homography_profile_name_for_role(self, role: str) -> str:
|
||||||
|
"""
|
||||||
|
Resolve qual perfil de homografia usar para uma role.
|
||||||
|
|
||||||
|
Prioridade:
|
||||||
|
1) fusion_config.homography_profile_by_role[role]
|
||||||
|
2) fusion_config.homography_profile
|
||||||
|
3) "default"
|
||||||
|
"""
|
||||||
|
role = str(role).lower()
|
||||||
|
fusion = getattr(self, "fusion_config", {}) or {}
|
||||||
|
|
||||||
|
by_role = fusion.get("homography_profile_by_role", {}) or {}
|
||||||
|
if isinstance(by_role, dict):
|
||||||
|
selected = by_role.get(role)
|
||||||
|
if selected:
|
||||||
|
return str(selected).lower()
|
||||||
|
|
||||||
|
selected = fusion.get("homography_profile", None)
|
||||||
|
if selected:
|
||||||
|
return str(selected).lower()
|
||||||
|
|
||||||
|
return "default"
|
||||||
|
|
||||||
|
def _resolve_homography_entry_for_role(self, role: str):
|
||||||
|
"""
|
||||||
|
Resolve a homografia no contrato novo ou antigo.
|
||||||
|
|
||||||
|
Contrato novo:
|
||||||
|
fusion_config.homography_profiles.<perfil>.homographies.<role>_to_rgb
|
||||||
|
|
||||||
|
Contrato antigo:
|
||||||
|
fusion_config.homographies.<role>_to_rgb
|
||||||
|
|
||||||
|
Retorna:
|
||||||
|
H, calib_size, profile_name
|
||||||
|
"""
|
||||||
|
role = str(role).lower()
|
||||||
|
fusion = getattr(self, "fusion_config", {}) or {}
|
||||||
|
|
||||||
|
key = f"{role}_to_rgb"
|
||||||
|
|
||||||
|
selected_profile = self._resolve_homography_profile_name_for_role(role)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Futuro: auto por profundidade.
|
||||||
|
# Por enquanto, cai em media/default de forma explícita.
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
if selected_profile == "auto":
|
||||||
|
profiles = fusion.get("homography_profiles", {}) or {}
|
||||||
|
if "media" in profiles:
|
||||||
|
selected_profile = "media"
|
||||||
|
elif "default" in profiles:
|
||||||
|
selected_profile = "default"
|
||||||
|
else:
|
||||||
|
selected_profile = ""
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Contrato novo: homography_profiles
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
profiles = fusion.get("homography_profiles", {}) or {}
|
||||||
|
if isinstance(profiles, dict) and selected_profile:
|
||||||
|
profile = profiles.get(selected_profile)
|
||||||
|
|
||||||
|
if profile is None:
|
||||||
|
# tolera nomes com caixa diferente
|
||||||
|
for name, item in profiles.items():
|
||||||
|
if str(name).lower() == selected_profile:
|
||||||
|
profile = item
|
||||||
|
selected_profile = str(name)
|
||||||
|
break
|
||||||
|
|
||||||
|
if isinstance(profile, dict):
|
||||||
|
profile_homographies = profile.get("homographies", {}) or {}
|
||||||
|
H = profile_homographies.get(key)
|
||||||
|
|
||||||
|
if H is None:
|
||||||
|
# fallback curto: "re" ou "nir"
|
||||||
|
H = profile_homographies.get(role)
|
||||||
|
|
||||||
|
if H is not None:
|
||||||
|
calib_size = (
|
||||||
|
profile.get("homography_calibration_size")
|
||||||
|
or profile.get("calibration_size")
|
||||||
|
or fusion.get("homography_calibration_size")
|
||||||
|
or fusion.get("calibration_size")
|
||||||
|
or None
|
||||||
|
)
|
||||||
|
return H, calib_size, selected_profile
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Contrato antigo: homographies direto
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
homographies = fusion.get("homographies", {}) or {}
|
||||||
|
H = homographies.get(key)
|
||||||
|
|
||||||
|
if H is None:
|
||||||
|
H = homographies.get(role)
|
||||||
|
|
||||||
|
if H is not None:
|
||||||
|
calib_size = (
|
||||||
|
fusion.get("homography_calibration_size")
|
||||||
|
or fusion.get("calibration_size")
|
||||||
|
or None
|
||||||
|
)
|
||||||
|
return H, calib_size, "legacy"
|
||||||
|
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Homografia ausente para role={role}. "
|
||||||
|
f"Procurei profile='{selected_profile}' em "
|
||||||
|
f"fusion_config.homography_profiles.*.homographies.{key} "
|
||||||
|
f"e fallback fusion_config.homographies.{key}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _raw10_rgb_linear_demosaic_to_rgb_float01_fast(
|
def _raw10_rgb_linear_demosaic_to_rgb_float01_fast(
|
||||||
|
|
@ -4806,3 +4929,6 @@ class RawProcessorCore:
|
||||||
self._flatfield_runtime_cache[key] = gain_tensor
|
self._flatfield_runtime_cache[key] = gain_tensor
|
||||||
return gain_tensor
|
return gain_tensor
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -85,10 +85,9 @@ def raw10_bin_to_gray(path: Path, width: int, height: int, *, is_rgb: bool, baye
|
||||||
raw = path.read_bytes()
|
raw = path.read_bytes()
|
||||||
raw10 = unpack_raw10_packed(raw, width, height)
|
raw10 = unpack_raw10_packed(raw, width, height)
|
||||||
|
|
||||||
if is_rgb:
|
# Para detecção ChArUco, usar o RAW Bayer como intensidade costuma ser mais fiel
|
||||||
bgr = debayer_raw10_to_bgr_u8(raw10, bayer=bayer)
|
# que debayerizar, porque o debayer pode suavizar os IDs ArUco.
|
||||||
gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY)
|
# O parâmetro is_rgb fica mantido por compatibilidade com chamadas antigas.
|
||||||
else:
|
|
||||||
gray = normalize_to_u8(raw10)
|
gray = normalize_to_u8(raw10)
|
||||||
|
|
||||||
if use_clahe:
|
if use_clahe:
|
||||||
|
|
@ -97,33 +96,52 @@ def raw10_bin_to_gray(path: Path, width: int, height: int, *, is_rgb: bool, baye
|
||||||
|
|
||||||
return gray
|
return gray
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Metadata
|
# Metadata
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
def find_meta(root_dir: Path) -> Path | None:
|
def find_meta(root_dir: Path) -> Path | None:
|
||||||
candidates = list(root_dir.rglob("meta.json")) + list(root_dir.rglob("metadata.json"))
|
candidates = (
|
||||||
|
list(root_dir.rglob("meta.json")) +
|
||||||
|
list(root_dir.rglob("metadata.json")) +
|
||||||
|
sorted(root_dir.rglob("*.json"))
|
||||||
|
)
|
||||||
return candidates[0] if candidates else None
|
return candidates[0] if candidates else None
|
||||||
|
|
||||||
|
|
||||||
def extract_camera_info_from_meta(meta: dict, cam_key: str):
|
def extract_camera_info_from_meta(meta: dict, cam_key: str):
|
||||||
|
# Caminho usado pelo meta atual do oak_fcc3.
|
||||||
|
stream_meta = meta.get("stream_meta")
|
||||||
|
if isinstance(stream_meta, dict):
|
||||||
|
camera_info = stream_meta.get("camera_info")
|
||||||
|
if isinstance(camera_info, dict):
|
||||||
|
info = camera_info.get(cam_key)
|
||||||
|
if isinstance(info, dict) and "width" in info and "height" in info:
|
||||||
|
return info
|
||||||
|
|
||||||
|
# Formatos alternativos.
|
||||||
for root_key in ["camera_info", "cameras", "camera_meta", "payload_sources_info"]:
|
for root_key in ["camera_info", "cameras", "camera_meta", "payload_sources_info"]:
|
||||||
root = meta.get(root_key)
|
root = meta.get(root_key)
|
||||||
if isinstance(root, dict):
|
if isinstance(root, dict):
|
||||||
info = root.get(cam_key)
|
info = root.get(cam_key)
|
||||||
if isinstance(info, dict):
|
if isinstance(info, dict) and "width" in info and "height" in info:
|
||||||
return info
|
return info
|
||||||
|
|
||||||
|
# Busca recursiva, mas só aceita se parecer info geométrica da câmera.
|
||||||
stack = [meta]
|
stack = [meta]
|
||||||
while stack:
|
while stack:
|
||||||
obj = stack.pop()
|
obj = stack.pop()
|
||||||
|
|
||||||
if isinstance(obj, dict):
|
if isinstance(obj, dict):
|
||||||
if cam_key in obj and isinstance(obj[cam_key], dict):
|
if cam_key in obj and isinstance(obj[cam_key], dict):
|
||||||
return obj[cam_key]
|
info = obj[cam_key]
|
||||||
|
if "width" in info and "height" in info:
|
||||||
|
return info
|
||||||
|
|
||||||
for v in obj.values():
|
for v in obj.values():
|
||||||
if isinstance(v, (dict, list)):
|
if isinstance(v, (dict, list)):
|
||||||
stack.append(v)
|
stack.append(v)
|
||||||
|
|
||||||
elif isinstance(obj, list):
|
elif isinstance(obj, list):
|
||||||
for v in obj:
|
for v in obj:
|
||||||
if isinstance(v, (dict, list)):
|
if isinstance(v, (dict, list)):
|
||||||
|
|
@ -155,11 +173,11 @@ def try_get_width_height_from_meta(root_dir: Path, cam_key: str):
|
||||||
return int(width), int(height)
|
return int(width), int(height)
|
||||||
|
|
||||||
|
|
||||||
def resolve_width_height(args, cam_key: str):
|
def resolve_width_height(args, cam_key: str, search_root: Path):
|
||||||
if args.width > 0 and args.height > 0:
|
if args.width > 0 and args.height > 0:
|
||||||
return args.width, args.height
|
return args.width, args.height
|
||||||
|
|
||||||
w, h = try_get_width_height_from_meta(Path(args.root_dir), cam_key)
|
w, h = try_get_width_height_from_meta(search_root, cam_key)
|
||||||
if w and h:
|
if w and h:
|
||||||
return w, h
|
return w, h
|
||||||
|
|
||||||
|
|
@ -268,6 +286,26 @@ def resolve_homography_triplet(triplets: list[dict], homo_ref_frame: str | None,
|
||||||
raise RuntimeError(f"Não encontrei triplet correspondente a --homo_ref_frame={homo_ref_frame}")
|
raise RuntimeError(f"Não encontrei triplet correspondente a --homo_ref_frame={homo_ref_frame}")
|
||||||
|
|
||||||
|
|
||||||
|
def find_triplets_multi(root_dirs: list[Path], cams: list[str]):
|
||||||
|
all_triplets = []
|
||||||
|
all_by_cam = {cam: [] for cam in cams}
|
||||||
|
|
||||||
|
for root in root_dirs:
|
||||||
|
triplets, by_cam = find_triplets(root, cams)
|
||||||
|
|
||||||
|
for cam in cams:
|
||||||
|
all_by_cam[cam].extend(by_cam[cam])
|
||||||
|
|
||||||
|
for item in triplets:
|
||||||
|
item = dict(item)
|
||||||
|
item["__root_dir"] = root
|
||||||
|
all_triplets.append(item)
|
||||||
|
|
||||||
|
print(f"[INFO] root_dir={root} triplets={len(triplets)}")
|
||||||
|
|
||||||
|
return all_triplets, all_by_cam
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# ChArUco helpers
|
# ChArUco helpers
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
@ -319,12 +357,35 @@ def get_board_corners(board):
|
||||||
|
|
||||||
def create_detector_params():
|
def create_detector_params():
|
||||||
aruco = cv2.aruco
|
aruco = cv2.aruco
|
||||||
|
|
||||||
if hasattr(aruco, "DetectorParameters"):
|
if hasattr(aruco, "DetectorParameters"):
|
||||||
return aruco.DetectorParameters()
|
params = aruco.DetectorParameters()
|
||||||
if hasattr(aruco, "DetectorParameters_create"):
|
elif hasattr(aruco, "DetectorParameters_create"):
|
||||||
return aruco.DetectorParameters_create()
|
params = aruco.DetectorParameters_create()
|
||||||
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
params.adaptiveThreshWinSizeMin = 3
|
||||||
|
params.adaptiveThreshWinSizeMax = 53
|
||||||
|
params.adaptiveThreshWinSizeStep = 4
|
||||||
|
|
||||||
|
params.minMarkerPerimeterRate = 0.01
|
||||||
|
params.maxMarkerPerimeterRate = 4.0
|
||||||
|
|
||||||
|
params.polygonalApproxAccuracyRate = 0.05
|
||||||
|
params.minCornerDistanceRate = 0.02
|
||||||
|
params.minDistanceToBorder = 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
params.cornerRefinementMethod = aruco.CORNER_REFINE_SUBPIX
|
||||||
|
params.cornerRefinementWinSize = 5
|
||||||
|
params.cornerRefinementMaxIterations = 50
|
||||||
|
params.cornerRefinementMinAccuracy = 0.01
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return params
|
||||||
|
|
||||||
|
|
||||||
def detect_charuco(gray: np.ndarray, board, aruco_dict, min_corners: int):
|
def detect_charuco(gray: np.ndarray, board, aruco_dict, min_corners: int):
|
||||||
aruco = cv2.aruco
|
aruco = cv2.aruco
|
||||||
|
|
@ -636,13 +697,261 @@ def compute_planar_homography_from_triplet(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def detect_charuco_best(gray: np.ndarray, board, aruco_dict, min_corners: int, cam: str = ""):
|
||||||
|
"""
|
||||||
|
Tenta múltiplos pré-processamentos e escalas.
|
||||||
|
Retorna a melhor detecção mesmo quando ela fica abaixo de min_corners.
|
||||||
|
"""
|
||||||
|
variants = []
|
||||||
|
|
||||||
|
base = gray.copy()
|
||||||
|
variants.append(("raw", base))
|
||||||
|
|
||||||
|
clahe2 = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
|
||||||
|
clahe4 = cv2.createCLAHE(clipLimit=4.0, tileGridSize=(8, 8))
|
||||||
|
|
||||||
|
variants.append(("clahe_2", clahe2.apply(base)))
|
||||||
|
variants.append(("clahe_4", clahe4.apply(base)))
|
||||||
|
|
||||||
|
blur = cv2.GaussianBlur(base, (3, 3), 0)
|
||||||
|
variants.append(("blur_clahe_2", clahe2.apply(blur)))
|
||||||
|
|
||||||
|
th = cv2.adaptiveThreshold(
|
||||||
|
base,
|
||||||
|
255,
|
||||||
|
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
|
||||||
|
cv2.THRESH_BINARY,
|
||||||
|
31,
|
||||||
|
5,
|
||||||
|
)
|
||||||
|
variants.append(("adaptive", th))
|
||||||
|
|
||||||
|
variants.append(("invert_raw", 255 - base))
|
||||||
|
variants.append(("invert_clahe_2", 255 - clahe2.apply(base)))
|
||||||
|
|
||||||
|
best = {
|
||||||
|
"name": None,
|
||||||
|
"corners": None,
|
||||||
|
"ids": None,
|
||||||
|
"count": 0,
|
||||||
|
"accepted": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, img in variants:
|
||||||
|
for scale in [1.0, 2.0, 3.0]:
|
||||||
|
if scale == 1.0:
|
||||||
|
test_img = img
|
||||||
|
else:
|
||||||
|
test_img = cv2.resize(
|
||||||
|
img,
|
||||||
|
None,
|
||||||
|
fx=scale,
|
||||||
|
fy=scale,
|
||||||
|
interpolation=cv2.INTER_CUBIC,
|
||||||
|
)
|
||||||
|
|
||||||
|
corners, ids = detect_charuco(test_img, board, aruco_dict, min_corners=1)
|
||||||
|
count = 0 if ids is None else len(ids)
|
||||||
|
|
||||||
|
if count > best["count"]:
|
||||||
|
if corners is not None and scale != 1.0:
|
||||||
|
corners = corners / scale
|
||||||
|
|
||||||
|
best.update({
|
||||||
|
"name": f"{name}_x{scale:g}" if scale != 1.0 else name,
|
||||||
|
"corners": corners,
|
||||||
|
"ids": ids,
|
||||||
|
"count": count,
|
||||||
|
"accepted": count >= min_corners,
|
||||||
|
})
|
||||||
|
|
||||||
|
return best["corners"], best["ids"], best["name"], best["count"], best["accepted"]
|
||||||
|
|
||||||
|
|
||||||
|
def _make_overlap_masks_from_homographies(H_re, H_nir, image_size: tuple[int, int]):
|
||||||
|
image_w, image_h = image_size
|
||||||
|
ones = np.ones((image_h, image_w), dtype=np.uint8) * 255
|
||||||
|
|
||||||
|
overlap_re_to_rgb = cv2.warpPerspective(
|
||||||
|
ones,
|
||||||
|
H_re,
|
||||||
|
(image_w, image_h),
|
||||||
|
flags=cv2.INTER_NEAREST,
|
||||||
|
borderMode=cv2.BORDER_CONSTANT,
|
||||||
|
borderValue=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
overlap_nir_to_rgb = cv2.warpPerspective(
|
||||||
|
ones,
|
||||||
|
H_nir,
|
||||||
|
(image_w, image_h),
|
||||||
|
flags=cv2.INTER_NEAREST,
|
||||||
|
borderMode=cv2.BORDER_CONSTANT,
|
||||||
|
borderValue=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
overlap_common_rgb = cv2.bitwise_and(overlap_re_to_rgb, overlap_nir_to_rgb)
|
||||||
|
return overlap_re_to_rgb, overlap_nir_to_rgb, overlap_common_rgb
|
||||||
|
|
||||||
|
|
||||||
|
def compute_planar_homography_from_collected_pairs(
|
||||||
|
homography_pairs: dict,
|
||||||
|
image_size: tuple[int, int],
|
||||||
|
args,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Calcula H_RE_to_RGB e H_NIR_to_RGB usando TODOS os pares válidos acumulados
|
||||||
|
durante a varredura do dataset.
|
||||||
|
|
||||||
|
Premissa: todos os frames usados representam o mesmo plano físico.
|
||||||
|
"""
|
||||||
|
re_items = homography_pairs.get("RE_to_RGB", [])
|
||||||
|
nir_items = homography_pairs.get("NIR_to_RGB", [])
|
||||||
|
|
||||||
|
if len(re_items) == 0:
|
||||||
|
raise RuntimeError("Nenhum par válido acumulado para homografia RE -> RGB.")
|
||||||
|
|
||||||
|
if len(nir_items) == 0:
|
||||||
|
raise RuntimeError("Nenhum par válido acumulado para homografia NIR -> RGB.")
|
||||||
|
|
||||||
|
def stack_points(items, label):
|
||||||
|
src = np.vstack([x["pts_src"] for x in items]).astype(np.float32)
|
||||||
|
dst = np.vstack([x["pts_dst"] for x in items]).astype(np.float32)
|
||||||
|
|
||||||
|
min_total = max(4, int(args.homo_min_total_points))
|
||||||
|
if len(src) < min_total:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Poucos pontos totais para homografia {label}: {len(src)}. "
|
||||||
|
f"Mínimo configurado={min_total}."
|
||||||
|
)
|
||||||
|
|
||||||
|
return src, dst
|
||||||
|
|
||||||
|
pts_re_src, pts_re_dst = stack_points(re_items, "RE_to_RGB")
|
||||||
|
pts_nir_src, pts_nir_dst = stack_points(nir_items, "NIR_to_RGB")
|
||||||
|
|
||||||
|
H_re, mask_re = cv2.findHomography(
|
||||||
|
pts_re_src,
|
||||||
|
pts_re_dst,
|
||||||
|
cv2.RANSAC,
|
||||||
|
args.homo_ransac_thresh,
|
||||||
|
)
|
||||||
|
|
||||||
|
H_nir, mask_nir = cv2.findHomography(
|
||||||
|
pts_nir_src,
|
||||||
|
pts_nir_dst,
|
||||||
|
cv2.RANSAC,
|
||||||
|
args.homo_ransac_thresh,
|
||||||
|
)
|
||||||
|
|
||||||
|
if H_re is None:
|
||||||
|
raise RuntimeError("cv2.findHomography falhou para RE -> RGB usando todos os frames válidos.")
|
||||||
|
|
||||||
|
if H_nir is None:
|
||||||
|
raise RuntimeError("cv2.findHomography falhou para NIR -> RGB usando todos os frames válidos.")
|
||||||
|
|
||||||
|
re_inliers = int(np.count_nonzero(mask_re)) if mask_re is not None else 0
|
||||||
|
nir_inliers = int(np.count_nonzero(mask_nir)) if mask_nir is not None else 0
|
||||||
|
|
||||||
|
overlap_re_to_rgb, overlap_nir_to_rgb, overlap_common_rgb = _make_overlap_masks_from_homographies(
|
||||||
|
H_re,
|
||||||
|
H_nir,
|
||||||
|
image_size,
|
||||||
|
)
|
||||||
|
|
||||||
|
re_frame_indices = sorted({int(x["triplet_idx"]) for x in re_items})
|
||||||
|
nir_frame_indices = sorted({int(x["triplet_idx"]) for x in nir_items})
|
||||||
|
common_frame_indices = sorted(set(re_frame_indices) & set(nir_frame_indices))
|
||||||
|
|
||||||
|
stats = {
|
||||||
|
"mode": "all_valid_triplets",
|
||||||
|
"min_common_frame": int(args.homo_min_common_frame),
|
||||||
|
"min_total_points": int(args.homo_min_total_points),
|
||||||
|
"re_total_points": int(len(pts_re_src)),
|
||||||
|
"nir_total_points": int(len(pts_nir_src)),
|
||||||
|
"re_inliers": re_inliers,
|
||||||
|
"nir_inliers": nir_inliers,
|
||||||
|
"re_inlier_pct": float(re_inliers / max(1, len(pts_re_src)) * 100.0),
|
||||||
|
"nir_inlier_pct": float(nir_inliers / max(1, len(pts_nir_src)) * 100.0),
|
||||||
|
"re_frames_used": int(len(re_frame_indices)),
|
||||||
|
"nir_frames_used": int(len(nir_frame_indices)),
|
||||||
|
"common_frames_used": int(len(common_frame_indices)),
|
||||||
|
"re_pair_records": int(len(re_items)),
|
||||||
|
"nir_pair_records": int(len(nir_items)),
|
||||||
|
"overlap_re_pct": float(np.mean(overlap_re_to_rgb > 0) * 100.0),
|
||||||
|
"overlap_nir_pct": float(np.mean(overlap_nir_to_rgb > 0) * 100.0),
|
||||||
|
"overlap_common_pct": float(np.mean(overlap_common_rgb > 0) * 100.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
common_ids_re = np.concatenate([x["common_ids"] for x in re_items]).astype(np.int32)
|
||||||
|
common_ids_nir = np.concatenate([x["common_ids"] for x in nir_items]).astype(np.int32)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"H_RE_to_RGB": H_re,
|
||||||
|
"H_NIR_to_RGB": H_nir,
|
||||||
|
"mask_RE_to_RGB": mask_re,
|
||||||
|
"mask_NIR_to_RGB": mask_nir,
|
||||||
|
"common_ids_RE_to_RGB": common_ids_re,
|
||||||
|
"common_ids_NIR_to_RGB": common_ids_nir,
|
||||||
|
"overlap_RE_to_RGB": overlap_re_to_rgb,
|
||||||
|
"overlap_NIR_to_RGB": overlap_nir_to_rgb,
|
||||||
|
"overlap_common_RGB": overlap_common_rgb,
|
||||||
|
"stats": stats,
|
||||||
|
"frame_indices_RE_to_RGB": np.array(re_frame_indices, dtype=np.int32),
|
||||||
|
"frame_indices_NIR_to_RGB": np.array(nir_frame_indices, dtype=np.int32),
|
||||||
|
"frame_indices_common": np.array(common_frame_indices, dtype=np.int32),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def add_homography_to_save_dict(save_dict: dict, homo_result: dict, args):
|
||||||
|
hs = homo_result["stats"]
|
||||||
|
|
||||||
|
save_dict["has_planar_homography"] = True
|
||||||
|
save_dict["planar_homography_source"] = "all_valid_triplets"
|
||||||
|
save_dict["planar_homography_resolved"] = "all_valid_triplets"
|
||||||
|
save_dict["planar_homography_note"] = (
|
||||||
|
"Homography maps RE/NIR to RGB using all valid ChArUco detections "
|
||||||
|
"from the same physical plane."
|
||||||
|
)
|
||||||
|
|
||||||
|
save_dict["H_RE_to_RGB"] = homo_result["H_RE_to_RGB"]
|
||||||
|
save_dict["H_NIR_to_RGB"] = homo_result["H_NIR_to_RGB"]
|
||||||
|
save_dict[f"H_{args.re_cam}_to_{args.rgb_cam}"] = homo_result["H_RE_to_RGB"]
|
||||||
|
save_dict[f"H_{args.nir_cam}_to_{args.rgb_cam}"] = homo_result["H_NIR_to_RGB"]
|
||||||
|
|
||||||
|
save_dict["homography_mask_RE_to_RGB"] = homo_result["mask_RE_to_RGB"]
|
||||||
|
save_dict["homography_mask_NIR_to_RGB"] = homo_result["mask_NIR_to_RGB"]
|
||||||
|
save_dict["homography_common_ids_RE_to_RGB"] = homo_result["common_ids_RE_to_RGB"]
|
||||||
|
save_dict["homography_common_ids_NIR_to_RGB"] = homo_result["common_ids_NIR_to_RGB"]
|
||||||
|
|
||||||
|
save_dict["homography_frame_indices_RE_to_RGB"] = homo_result["frame_indices_RE_to_RGB"]
|
||||||
|
save_dict["homography_frame_indices_NIR_to_RGB"] = homo_result["frame_indices_NIR_to_RGB"]
|
||||||
|
save_dict["homography_frame_indices_common"] = homo_result["frame_indices_common"]
|
||||||
|
|
||||||
|
save_dict["overlap_RE_to_RGB"] = homo_result["overlap_RE_to_RGB"]
|
||||||
|
save_dict["overlap_NIR_to_RGB"] = homo_result["overlap_NIR_to_RGB"]
|
||||||
|
save_dict["overlap_common_RGB"] = homo_result["overlap_common_RGB"]
|
||||||
|
|
||||||
|
for k, v in hs.items():
|
||||||
|
save_dict[f"planar_homography_{k}"] = v
|
||||||
|
|
||||||
|
return save_dict
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Main
|
# Main
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--root_dir", default="calibration/stereo_dataset", required=True)
|
parser.add_argument("--root_dir", default=None)
|
||||||
|
parser.add_argument(
|
||||||
|
"--root_dirs",
|
||||||
|
nargs="+",
|
||||||
|
default=None,
|
||||||
|
help="Lista de diretórios de calibração para juntar no mesmo cálculo stereo. Ex: baixa media alta",
|
||||||
|
)
|
||||||
parser.add_argument("--out_dir", default="calibration/multicam_charuco_calib_out")
|
parser.add_argument("--out_dir", default="calibration/multicam_charuco_calib_out")
|
||||||
|
|
||||||
parser.add_argument("--rgb_cam", default="CAM_A")
|
parser.add_argument("--rgb_cam", default="CAM_A")
|
||||||
|
|
@ -650,8 +959,8 @@ def main():
|
||||||
parser.add_argument("--nir_cam", default="CAM_C")
|
parser.add_argument("--nir_cam", default="CAM_C")
|
||||||
parser.add_argument("--ref_cam", default="CAM_A", help="Referência global. Recomendo CAM_A/RGB.")
|
parser.add_argument("--ref_cam", default="CAM_A", help="Referência global. Recomendo CAM_A/RGB.")
|
||||||
|
|
||||||
parser.add_argument("--width", type=int, default=1280)
|
parser.add_argument("--width", type=int, default=0)
|
||||||
parser.add_argument("--height", type=int, default=800)
|
parser.add_argument("--height", type=int, default=0)
|
||||||
parser.add_argument("--rgb_bayer", default="BGGR")
|
parser.add_argument("--rgb_bayer", default="BGGR")
|
||||||
|
|
||||||
parser.add_argument("--squares_x", type=int, default=13)
|
parser.add_argument("--squares_x", type=int, default=13)
|
||||||
|
|
@ -664,26 +973,76 @@ def main():
|
||||||
parser.add_argument("--min_corners", type=int, default=40)
|
parser.add_argument("--min_corners", type=int, default=40)
|
||||||
parser.add_argument("--min_common", type=int, default=40)
|
parser.add_argument("--min_common", type=int, default=40)
|
||||||
|
|
||||||
# Homografia planar de referência.
|
# Homografia planar usando todos os frames válidos do mesmo plano físico.
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--homo_ref_frame",
|
"--homography_mode",
|
||||||
default=None,
|
default="all_valid",
|
||||||
|
choices=["all_valid", "off"],
|
||||||
help=(
|
help=(
|
||||||
"Triplet usado como plano de referência para H_RE_to_RGB e H_NIR_to_RGB. "
|
"Modo de homografia planar. 'all_valid' acumula todos os pares válidos "
|
||||||
"Aceita índice, nome parcial/stem ou caminho de um .bin do triplet. "
|
"RE->RGB e NIR->RGB encontrados no dataset. 'off' desativa."
|
||||||
"Se omitido, não salva homografias planares."
|
),
|
||||||
|
)
|
||||||
|
# Homografia all_valid:
|
||||||
|
# - Não corta detecções fracas por câmera antes de acumular pontos.
|
||||||
|
# - Cada frame/par contribui se tiver pelo menos homo_min_common_frame IDs comuns.
|
||||||
|
# - O corte forte acontece no acumulado total, em homo_min_total_points.
|
||||||
|
parser.add_argument(
|
||||||
|
"--homo_min_common_frame",
|
||||||
|
type=int,
|
||||||
|
default=4,
|
||||||
|
help=(
|
||||||
|
"Mínimo de IDs comuns por frame/par para adicionar pontos à homografia. "
|
||||||
|
"Use 4 como mínimo matemático; 5-8 para ficar menos permissivo."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--homo_min_total_points",
|
||||||
|
type=int,
|
||||||
|
default=30,
|
||||||
|
help=(
|
||||||
|
"Mínimo de pontos acumulados no dataset inteiro para calcular cada homografia. "
|
||||||
|
"Ex: 30 para teste, 50-100 para calibração mais robusta."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
parser.add_argument("--homo_min_corners", type=int, default=30)
|
|
||||||
parser.add_argument("--homo_min_common", type=int, default=20)
|
|
||||||
parser.add_argument("--homo_ransac_thresh", type=float, default=3.0)
|
parser.add_argument("--homo_ransac_thresh", type=float, default=3.0)
|
||||||
|
|
||||||
|
# Compatibilidade com comandos antigos. Não são mais usados como corte da homografia all_valid.
|
||||||
|
parser.add_argument("--homo_min_corners", type=int, default=None, help=argparse.SUPPRESS)
|
||||||
|
parser.add_argument("--homo_min_common", type=int, default=None, help=argparse.SUPPRESS)
|
||||||
|
parser.add_argument(
|
||||||
|
"--min_calib_triplets",
|
||||||
|
type=int,
|
||||||
|
default=5,
|
||||||
|
help="Mínimo de triplets aceitos para executar calibração intrínseca/stereo.",
|
||||||
|
)
|
||||||
|
|
||||||
parser.add_argument("--no_clahe", action="store_true")
|
parser.add_argument("--no_clahe", action="store_true")
|
||||||
parser.add_argument("--show", action="store_true")
|
parser.add_argument("--show", action="store_true")
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
root_dir = Path(args.root_dir)
|
if args.root_dirs:
|
||||||
|
root_dirs = [Path(p) for p in args.root_dirs]
|
||||||
|
elif args.root_dir:
|
||||||
|
root_dirs = [Path(args.root_dir)]
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Informe --root_dir ou --root_dirs.")
|
||||||
|
|
||||||
|
# Compatibilidade com comandos antigos:
|
||||||
|
# --homo_min_common antigo vira o novo corte mínimo por frame/par.
|
||||||
|
# --homo_min_corners antigo não é mais usado como corte para homografia all_valid,
|
||||||
|
# porque agora aceitamos detecções pequenas e filtramos pelo total acumulado.
|
||||||
|
if args.homo_min_common is not None:
|
||||||
|
args.homo_min_common_frame = int(args.homo_min_common)
|
||||||
|
|
||||||
|
if getattr(args, "root_dirs", None):
|
||||||
|
root_dirs = [Path(p) for p in args.root_dirs]
|
||||||
|
elif getattr(args, "root_dir", None):
|
||||||
|
root_dirs = [Path(args.root_dir)]
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Informe --root_dir ou --root_dirs.")
|
||||||
|
|
||||||
out_dir = Path(args.out_dir)
|
out_dir = Path(args.out_dir)
|
||||||
debug_dir = out_dir / "debug"
|
debug_dir = out_dir / "debug"
|
||||||
out_dir.mkdir(parents=True, exist_ok=True)
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
@ -701,14 +1060,14 @@ def main():
|
||||||
|
|
||||||
sizes = {}
|
sizes = {}
|
||||||
for cam in cams:
|
for cam in cams:
|
||||||
w, h = resolve_width_height(args, cam)
|
w, h = resolve_width_height(args, cam, root_dirs[0])
|
||||||
sizes[cam] = (w, h)
|
sizes[cam] = (w, h)
|
||||||
|
|
||||||
image_w = min(w for w, h in sizes.values())
|
image_w = min(w for w, h in sizes.values())
|
||||||
image_h = min(h for w, h in sizes.values())
|
image_h = min(h for w, h in sizes.values())
|
||||||
image_size = (image_w, image_h)
|
image_size = (image_w, image_h)
|
||||||
|
|
||||||
print(f"[INFO] root_dir={root_dir}")
|
print(f"[INFO] root_dir={root_dirs}")
|
||||||
print(f"[INFO] cams={cams} ref_cam={args.ref_cam}")
|
print(f"[INFO] cams={cams} ref_cam={args.ref_cam}")
|
||||||
for cam in cams:
|
for cam in cams:
|
||||||
print(f"[INFO] {cam} role={cam_roles[cam]} size={sizes[cam]}")
|
print(f"[INFO] {cam} role={cam_roles[cam]} size={sizes[cam]}")
|
||||||
|
|
@ -717,13 +1076,18 @@ def main():
|
||||||
print(f"[INFO] square_length={args.square_length}")
|
print(f"[INFO] square_length={args.square_length}")
|
||||||
print(f"[INFO] marker_length={args.marker_length}")
|
print(f"[INFO] marker_length={args.marker_length}")
|
||||||
print(f"[INFO] rectify_alpha={args.rectify_alpha}")
|
print(f"[INFO] rectify_alpha={args.rectify_alpha}")
|
||||||
|
print(f"[INFO] homography_mode={args.homography_mode}")
|
||||||
|
if args.homography_mode == "all_valid":
|
||||||
|
print(f"[INFO] homo_min_common_frame={args.homo_min_common_frame}")
|
||||||
|
print(f"[INFO] homo_min_total_points={args.homo_min_total_points}")
|
||||||
|
print(f"[INFO] homo_ransac_thresh={args.homo_ransac_thresh}")
|
||||||
|
|
||||||
triplets, by_cam = find_triplets(root_dir, cams)
|
triplets, by_cam = find_triplets_multi(root_dirs, cams)
|
||||||
for cam in cams:
|
for cam in cams:
|
||||||
print(f"[INFO] arquivos {cam}: {len(by_cam[cam])}")
|
print(f"[INFO] arquivos {cam}: {len(by_cam[cam])}")
|
||||||
print(f"[INFO] triplets encontrados: {len(triplets)}")
|
print(f"[INFO] triplets encontrados: {len(triplets)}")
|
||||||
|
|
||||||
if len(triplets) < 8:
|
if len(triplets) < 5:
|
||||||
raise RuntimeError("Poucos triplets encontrados. Verifique nomes dos arquivos CAM_A/B/C.")
|
raise RuntimeError("Poucos triplets encontrados. Verifique nomes dos arquivos CAM_A/B/C.")
|
||||||
|
|
||||||
aruco_dict = get_aruco_dict(args.aruco_dict)
|
aruco_dict = get_aruco_dict(args.aruco_dict)
|
||||||
|
|
@ -741,6 +1105,11 @@ def main():
|
||||||
accepted = 0
|
accepted = 0
|
||||||
rejected = 0
|
rejected = 0
|
||||||
|
|
||||||
|
homography_pairs = {
|
||||||
|
"RE_to_RGB": [],
|
||||||
|
"NIR_to_RGB": [],
|
||||||
|
}
|
||||||
|
|
||||||
for idx, item in enumerate(triplets):
|
for idx, item in enumerate(triplets):
|
||||||
print(f"[{idx + 1}/{len(triplets)}] " + " | ".join([f"{cam}={item[cam].name}" for cam in cams]))
|
print(f"[{idx + 1}/{len(triplets)}] " + " | ".join([f"{cam}={item[cam].name}" for cam in cams]))
|
||||||
|
|
||||||
|
|
@ -764,22 +1133,101 @@ def main():
|
||||||
gray = cv2.resize(gray, image_size, interpolation=cv2.INTER_AREA)
|
gray = cv2.resize(gray, image_size, interpolation=cv2.INTER_AREA)
|
||||||
|
|
||||||
gray_by_cam[cam] = gray
|
gray_by_cam[cam] = gray
|
||||||
corners, ids = detect_charuco(gray, board, aruco_dict, min_corners=args.min_corners)
|
corners, ids, det_mode, det_count, det_ok = detect_charuco_best(
|
||||||
|
gray,
|
||||||
|
board,
|
||||||
|
aruco_dict,
|
||||||
|
min_corners=args.min_corners,
|
||||||
|
cam=cam,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mantém a melhor detecção bruta para homografia, mesmo quando
|
||||||
|
# ela fica abaixo do mínimo mais rígido da calibração stereo.
|
||||||
detections[cam] = (corners, ids)
|
detections[cam] = (corners, ids)
|
||||||
|
detections[f"{cam}__count"] = det_count
|
||||||
|
detections[f"{cam}__mode"] = det_mode
|
||||||
|
|
||||||
counts = {cam: (0 if detections[cam][1] is None else len(detections[cam][1])) for cam in cams}
|
print(
|
||||||
|
f" [DETECT] {cam}: best={det_mode} corners={det_count} "
|
||||||
|
f"{'OK' if det_ok else f'LOW<{args.min_corners}'}"
|
||||||
|
)
|
||||||
|
|
||||||
if any(detections[cam][0] is None for cam in cams):
|
raw_counts = {cam: (0 if detections[cam][1] is None else len(detections[cam][1])) for cam in cams}
|
||||||
|
|
||||||
|
# Homografia all_valid:
|
||||||
|
# Aqui não usamos corte por câmera tipo "RGB precisa ter 20/40 pontos".
|
||||||
|
# Se um frame achou poucos pontos, mas tem pelo menos 4 IDs comuns no par,
|
||||||
|
# esses pontos entram no acumulado. O corte forte é feito depois, no total.
|
||||||
|
if args.homography_mode == "all_valid":
|
||||||
|
rgb_corners, rgb_ids = detections[args.rgb_cam]
|
||||||
|
re_corners, re_ids = detections[args.re_cam]
|
||||||
|
nir_corners, nir_ids = detections[args.nir_cam]
|
||||||
|
|
||||||
|
pts_re, pts_rgb_re, common_re = common_points_pair(
|
||||||
|
re_corners,
|
||||||
|
re_ids,
|
||||||
|
rgb_corners,
|
||||||
|
rgb_ids,
|
||||||
|
min_common=args.homo_min_common_frame,
|
||||||
|
)
|
||||||
|
if pts_re is not None:
|
||||||
|
homography_pairs["RE_to_RGB"].append({
|
||||||
|
"triplet_idx": idx,
|
||||||
|
"pts_src": pts_re,
|
||||||
|
"pts_dst": pts_rgb_re,
|
||||||
|
"common_ids": np.array(common_re, dtype=np.int32),
|
||||||
|
"src_file": str(item[args.re_cam]),
|
||||||
|
"dst_file": str(item[args.rgb_cam]),
|
||||||
|
"src_count": raw_counts[args.re_cam],
|
||||||
|
"dst_count": raw_counts[args.rgb_cam],
|
||||||
|
})
|
||||||
|
print(f" [HOMO ADD] RE->RGB common={len(common_re)} total_records={len(homography_pairs['RE_to_RGB'])}")
|
||||||
|
else:
|
||||||
|
print(f" [HOMO SKIP] RE->RGB common={len(common_re)} < {args.homo_min_common_frame}")
|
||||||
|
|
||||||
|
pts_nir, pts_rgb_nir, common_nir = common_points_pair(
|
||||||
|
nir_corners,
|
||||||
|
nir_ids,
|
||||||
|
rgb_corners,
|
||||||
|
rgb_ids,
|
||||||
|
min_common=args.homo_min_common_frame,
|
||||||
|
)
|
||||||
|
if pts_nir is not None:
|
||||||
|
homography_pairs["NIR_to_RGB"].append({
|
||||||
|
"triplet_idx": idx,
|
||||||
|
"pts_src": pts_nir,
|
||||||
|
"pts_dst": pts_rgb_nir,
|
||||||
|
"common_ids": np.array(common_nir, dtype=np.int32),
|
||||||
|
"src_file": str(item[args.nir_cam]),
|
||||||
|
"dst_file": str(item[args.rgb_cam]),
|
||||||
|
"src_count": raw_counts[args.nir_cam],
|
||||||
|
"dst_count": raw_counts[args.rgb_cam],
|
||||||
|
})
|
||||||
|
print(f" [HOMO ADD] NIR->RGB common={len(common_nir)} total_records={len(homography_pairs['NIR_to_RGB'])}")
|
||||||
|
else:
|
||||||
|
print(f" [HOMO SKIP] NIR->RGB common={len(common_nir)} < {args.homo_min_common_frame}")
|
||||||
|
|
||||||
|
detections_calib = {}
|
||||||
|
for cam in cams:
|
||||||
|
corners, ids = detections[cam]
|
||||||
|
if raw_counts[cam] >= args.min_corners:
|
||||||
|
detections_calib[cam] = (corners, ids)
|
||||||
|
else:
|
||||||
|
detections_calib[cam] = (None, None)
|
||||||
|
|
||||||
|
counts = {cam: (0 if detections_calib[cam][1] is None else len(detections_calib[cam][1])) for cam in cams}
|
||||||
|
|
||||||
|
if any(detections_calib[cam][0] is None for cam in cams):
|
||||||
print(" [REJECT] detect insuficiente: " + ", ".join([f"{cam}={counts[cam]}" for cam in cams]))
|
print(" [REJECT] detect insuficiente: " + ", ".join([f"{cam}={counts[cam]}" for cam in cams]))
|
||||||
dbg = draw_debug_panel(gray_by_cam, detections, 0, False, f"triplet_{idx:04d}")
|
dbg = draw_debug_panel(gray_by_cam, detections_calib, 0, False, f"triplet_{idx:04d}")
|
||||||
cv2.imwrite(str(debug_dir / f"triplet_{idx:04d}_rejected.png"), dbg)
|
cv2.imwrite(str(debug_dir / f"triplet_{idx:04d}_rejected.png"), dbg)
|
||||||
rejected += 1
|
rejected += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
obj, imgpoints_by_cam, common_ids = common_points_multi(detections, board_corners, min_common=args.min_common)
|
obj, imgpoints_by_cam, common_ids = common_points_multi(detections_calib, board_corners, min_common=args.min_common)
|
||||||
if obj is None:
|
if obj is None:
|
||||||
print(f" [REJECT] comuns insuficientes nas 3 cams: common={len(common_ids)}")
|
print(f" [REJECT] comuns insuficientes nas 3 cams: common={len(common_ids)}")
|
||||||
dbg = draw_debug_panel(gray_by_cam, detections, len(common_ids), False, f"triplet_{idx:04d}")
|
dbg = draw_debug_panel(gray_by_cam, detections_calib, len(common_ids), False, f"triplet_{idx:04d}")
|
||||||
cv2.imwrite(str(debug_dir / f"triplet_{idx:04d}_rejected.png"), dbg)
|
cv2.imwrite(str(debug_dir / f"triplet_{idx:04d}_rejected.png"), dbg)
|
||||||
rejected += 1
|
rejected += 1
|
||||||
continue
|
continue
|
||||||
|
|
@ -791,7 +1239,7 @@ def main():
|
||||||
single_imgpoints[cam].append(imgpoints_by_cam[cam].copy())
|
single_imgpoints[cam].append(imgpoints_by_cam[cam].copy())
|
||||||
|
|
||||||
accepted += 1
|
accepted += 1
|
||||||
dbg = draw_debug_panel(gray_by_cam, detections, len(common_ids), True, f"triplet_{idx:04d}")
|
dbg = draw_debug_panel(gray_by_cam, detections_calib, len(common_ids), True, f"triplet_{idx:04d}")
|
||||||
cv2.imwrite(str(debug_dir / f"triplet_{idx:04d}_accepted.png"), dbg)
|
cv2.imwrite(str(debug_dir / f"triplet_{idx:04d}_accepted.png"), dbg)
|
||||||
|
|
||||||
if args.show:
|
if args.show:
|
||||||
|
|
@ -810,11 +1258,79 @@ def main():
|
||||||
cv2.destroyAllWindows()
|
cv2.destroyAllWindows()
|
||||||
|
|
||||||
print("")
|
print("")
|
||||||
print(f"[INFO] triplets aceitos: {accepted}")
|
print(f"[INFO] triplets aceitos para calibração stereo: {accepted}")
|
||||||
print(f"[INFO] triplets rejeitados: {rejected}")
|
print(f"[INFO] triplets rejeitados para calibração stereo: {rejected}")
|
||||||
|
re_acc_points = sum(len(x["pts_src"]) for x in homography_pairs["RE_to_RGB"])
|
||||||
|
nir_acc_points = sum(len(x["pts_src"]) for x in homography_pairs["NIR_to_RGB"])
|
||||||
|
print(f"[INFO] pares acumulados homografia RE->RGB: {len(homography_pairs['RE_to_RGB'])} | pontos={re_acc_points}")
|
||||||
|
print(f"[INFO] pares acumulados homografia NIR->RGB: {len(homography_pairs['NIR_to_RGB'])} | pontos={nir_acc_points}")
|
||||||
|
|
||||||
if accepted < 8:
|
homo_result = None
|
||||||
raise RuntimeError(f"Poucos triplets aceitos: {accepted}. Ideal: 20-40+ bons.")
|
if args.homography_mode == "all_valid":
|
||||||
|
print("")
|
||||||
|
print("[HOMO] Calculando homografia planar com TODOS os pares válidos do dataset...")
|
||||||
|
try:
|
||||||
|
homo_result = compute_planar_homography_from_collected_pairs(
|
||||||
|
homography_pairs=homography_pairs,
|
||||||
|
image_size=image_size,
|
||||||
|
args=args,
|
||||||
|
)
|
||||||
|
|
||||||
|
hs = homo_result["stats"]
|
||||||
|
print("[HOMO] Resultado planar all_valid:")
|
||||||
|
print(f" RE points/inliers={hs['re_total_points']}/{hs['re_inliers']} ({hs['re_inlier_pct']:.1f}%)")
|
||||||
|
print(f" NIR points/inliers={hs['nir_total_points']}/{hs['nir_inliers']} ({hs['nir_inlier_pct']:.1f}%)")
|
||||||
|
print(f" frames RE/NIR/common={hs['re_frames_used']}/{hs['nir_frames_used']}/{hs['common_frames_used']}")
|
||||||
|
print(f" overlap RE={hs['overlap_re_pct']:.1f}% NIR={hs['overlap_nir_pct']:.1f}% common={hs['overlap_common_pct']:.1f}%")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[HOMO][WARN] Não foi possível calcular homografia all_valid: {e}")
|
||||||
|
homo_result = None
|
||||||
|
|
||||||
|
out_path = out_dir / f"multicam_calib_{'_'.join(cams)}_ref_{args.ref_cam}.npz"
|
||||||
|
|
||||||
|
if accepted < args.min_calib_triplets:
|
||||||
|
if homo_result is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Poucos triplets aceitos para calibração stereo: {accepted}. "
|
||||||
|
f"Mínimo configurado={args.min_calib_triplets}. "
|
||||||
|
f"Também não foi possível salvar homografia."
|
||||||
|
)
|
||||||
|
|
||||||
|
save_dict = {
|
||||||
|
"schema": "multicam_charuco_raw10_v4_planar_homography_accumulated_only",
|
||||||
|
"cams": np.array(cams),
|
||||||
|
"rgb_cam": args.rgb_cam,
|
||||||
|
"nir_cam": args.nir_cam,
|
||||||
|
"re_cam": args.re_cam,
|
||||||
|
"ref_cam": args.ref_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,
|
||||||
|
"rectify_alpha": args.rectify_alpha,
|
||||||
|
"accepted": accepted,
|
||||||
|
"rejected": rejected,
|
||||||
|
"stereo_calibration_available": False,
|
||||||
|
"stereo_calibration_note": (
|
||||||
|
f"Calibração stereo não executada porque accepted={accepted} "
|
||||||
|
f"< min_calib_triplets={args.min_calib_triplets}."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
for cam in cams:
|
||||||
|
save_dict[f"role_{cam}"] = cam_roles[cam]
|
||||||
|
|
||||||
|
add_homography_to_save_dict(save_dict, homo_result, args)
|
||||||
|
np.savez_compressed(out_path, **save_dict)
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print(f"[OK] homografia planar salva em: {out_path}")
|
||||||
|
print("[OK] calibração stereo não foi executada por falta de triplets aceitos.")
|
||||||
|
print(f"[OK] debug salvo em: {debug_dir}")
|
||||||
|
return
|
||||||
|
|
||||||
K = {}
|
K = {}
|
||||||
D = {}
|
D = {}
|
||||||
|
|
@ -901,41 +1417,12 @@ def main():
|
||||||
extr_R_to_ref[cam] = R_cam_to_ref
|
extr_R_to_ref[cam] = R_cam_to_ref
|
||||||
extr_T_to_ref[cam] = T_cam_to_ref
|
extr_T_to_ref[cam] = T_cam_to_ref
|
||||||
|
|
||||||
# Homografia planar opcional.
|
# Homografia planar all_valid já foi calculada antes da calibração stereo.
|
||||||
homo_result = None
|
|
||||||
homo_triplet = None
|
|
||||||
homo_ref_resolved = ""
|
|
||||||
|
|
||||||
if args.homo_ref_frame:
|
|
||||||
print("")
|
|
||||||
print(f"[HOMO] Resolvendo frame de referência planar: {args.homo_ref_frame}")
|
|
||||||
homo_triplet, homo_ref_resolved = resolve_homography_triplet(triplets, args.homo_ref_frame, args.rgb_cam)
|
|
||||||
print(f"[HOMO] Usando triplet: {homo_ref_resolved}")
|
|
||||||
for cam in cams:
|
|
||||||
print(f" {cam}: {homo_triplet[cam]}")
|
|
||||||
|
|
||||||
homo_result = compute_planar_homography_from_triplet(
|
|
||||||
triplet=homo_triplet,
|
|
||||||
cams=cams,
|
|
||||||
cam_roles=cam_roles,
|
|
||||||
sizes=sizes,
|
|
||||||
image_size=image_size,
|
|
||||||
args=args,
|
|
||||||
board=board,
|
|
||||||
aruco_dict=aruco_dict,
|
|
||||||
)
|
|
||||||
|
|
||||||
hs = homo_result["stats"]
|
|
||||||
print("[HOMO] Resultado planar:")
|
|
||||||
print(f" RGB corners={hs['rgb_corners']} RE corners={hs['re_corners']} NIR corners={hs['nir_corners']}")
|
|
||||||
print(f" RE common/inliers={hs['re_common']}/{hs['re_inliers']}")
|
|
||||||
print(f" NIR common/inliers={hs['nir_common']}/{hs['nir_inliers']}")
|
|
||||||
print(f" overlap RE={hs['overlap_re_pct']:.1f}% NIR={hs['overlap_nir_pct']:.1f}% common={hs['overlap_common_pct']:.1f}%")
|
|
||||||
|
|
||||||
out_path = out_dir / f"multicam_calib_{'_'.join(cams)}_ref_{args.ref_cam}.npz"
|
out_path = out_dir / f"multicam_calib_{'_'.join(cams)}_ref_{args.ref_cam}.npz"
|
||||||
|
|
||||||
save_dict = {
|
save_dict = {
|
||||||
"schema": "multicam_charuco_raw10_v2_planar_homography",
|
"schema": "multicam_charuco_raw10_v4_planar_homography_accumulated",
|
||||||
"cams": np.array(cams),
|
"cams": np.array(cams),
|
||||||
"rgb_cam": args.rgb_cam,
|
"rgb_cam": args.rgb_cam,
|
||||||
"nir_cam": args.nir_cam,
|
"nir_cam": args.nir_cam,
|
||||||
|
|
@ -950,6 +1437,10 @@ def main():
|
||||||
"rectify_alpha": args.rectify_alpha,
|
"rectify_alpha": args.rectify_alpha,
|
||||||
"accepted": accepted,
|
"accepted": accepted,
|
||||||
"rejected": rejected,
|
"rejected": rejected,
|
||||||
|
"stereo_calibration_available": True,
|
||||||
|
"calibration_mode": "stereo_global",
|
||||||
|
"source_root_dirs": np.array([str(p) for p in root_dirs]),
|
||||||
|
"source_root_count": len(root_dirs),
|
||||||
}
|
}
|
||||||
|
|
||||||
for cam in cams:
|
for cam in cams:
|
||||||
|
|
@ -974,31 +1465,7 @@ def main():
|
||||||
save_dict[f"pair_{key}_{rk}"] = rv
|
save_dict[f"pair_{key}_{rk}"] = rv
|
||||||
|
|
||||||
if homo_result is not None:
|
if homo_result is not None:
|
||||||
hs = homo_result["stats"]
|
add_homography_to_save_dict(save_dict, homo_result, args)
|
||||||
save_dict["has_planar_homography"] = True
|
|
||||||
save_dict["planar_homography_source"] = str(args.homo_ref_frame)
|
|
||||||
save_dict["planar_homography_resolved"] = str(homo_ref_resolved)
|
|
||||||
save_dict["planar_homography_note"] = "Homography maps RE/NIR to RGB for the physical plane visible in homo_ref_frame."
|
|
||||||
|
|
||||||
save_dict["H_RE_to_RGB"] = homo_result["H_RE_to_RGB"]
|
|
||||||
save_dict["H_NIR_to_RGB"] = homo_result["H_NIR_to_RGB"]
|
|
||||||
save_dict[f"H_{args.re_cam}_to_{args.rgb_cam}"] = homo_result["H_RE_to_RGB"]
|
|
||||||
save_dict[f"H_{args.nir_cam}_to_{args.rgb_cam}"] = homo_result["H_NIR_to_RGB"]
|
|
||||||
|
|
||||||
save_dict["homography_mask_RE_to_RGB"] = homo_result["mask_RE_to_RGB"]
|
|
||||||
save_dict["homography_mask_NIR_to_RGB"] = homo_result["mask_NIR_to_RGB"]
|
|
||||||
save_dict["homography_common_ids_RE_to_RGB"] = homo_result["common_ids_RE_to_RGB"]
|
|
||||||
save_dict["homography_common_ids_NIR_to_RGB"] = homo_result["common_ids_NIR_to_RGB"]
|
|
||||||
|
|
||||||
save_dict["overlap_RE_to_RGB"] = homo_result["overlap_RE_to_RGB"]
|
|
||||||
save_dict["overlap_NIR_to_RGB"] = homo_result["overlap_NIR_to_RGB"]
|
|
||||||
save_dict["overlap_common_RGB"] = homo_result["overlap_common_RGB"]
|
|
||||||
|
|
||||||
for cam in cams:
|
|
||||||
save_dict[f"planar_homography_file_{cam}"] = str(homo_triplet[cam])
|
|
||||||
|
|
||||||
for k, v in hs.items():
|
|
||||||
save_dict[f"planar_homography_{k}"] = v
|
|
||||||
else:
|
else:
|
||||||
save_dict["has_planar_homography"] = False
|
save_dict["has_planar_homography"] = False
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue