ajustes no sistema de treinamento multiespectral e perfil de homografia

This commit is contained in:
Diego Freitas 2026-06-03 07:37:15 -03:00
parent 441d5f5d01
commit c591e53d67
15 changed files with 6195 additions and 2103 deletions

3
.gitignore vendored
View File

@ -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/

View File

@ -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 []

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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()

View File

@ -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()

View File

@ -58,41 +58,184 @@
} }
}, },
"homography_calibration_size": [1280, 800], "homography_calibration_size": [1280, 800],
"homographies": { "homography_profile": "baixa",
"re_to_rgb": [ "homography_profile_by_role": {
[ "re": "baixa",
1.0103099557765387, "nir": "baixa"
0.00879456142897448, },
-9.666692994320178 "homography_profiles": {
], "baixa": {
[ "description": "Plano mais baixo/distante da câmera, normalmente mais próximo do chão.",
-0.00014434784827104316, "depth": 120.0,
1.0214419841673288, "homography_calibration_size": [1280, 800],
50.41247184630176 "homography_source": "all_valid_triplets",
], "homography_stats": {
[ "re_total_points": 267,
6.270900951674823e-06, "re_inliers": 267,
1.788434277122896e-05, "re_inlier_pct": 100.0,
1.0 "nir_total_points": 282,
] "nir_inliers": 276,
], "nir_inlier_pct": 97.87234042553192,
"nir_to_rgb": [ "re_frames_used": 13,
[ "nir_frames_used": 13,
0.994769714806237, "common_frames_used": 13,
0.005296878287321148, "overlap_common_pct": 92.09267578125
-6.433772056422987 },
], "homographies": {
[ "re_to_rgb": [
-0.00092900679814922, [
1.0017494877636166, 1.014814963583485,
29.883379280823718 0.015257560646391,
], -27.5415127674596
[ ],
-5.35529902496836e-08, [
7.331793189295934e-06, 0.000064618247568,
1.0 1.020756196943496,
] 51.80289590472656
] ],
[
0.000003411885963,
0.0000210631067,
1.0
]
],
"nir_to_rgb": [
[
0.993261705358079,
0.008537634512529,
-19.84130896890106
],
[
-0.004568701240477,
1.002385627262319,
37.83216994660247
],
[
-0.00000725852956,
0.000011588890195,
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,

View File

@ -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
} }
}, },

View File

@ -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

View File

@ -85,11 +85,10 @@ 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:
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
@ -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,11 +357,34 @@ 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()
return None else:
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):
@ -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