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/audit_multispec_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_dataset/
|
||||
Python/OAK/datasets/oak-fcc-3/calibration/dataset_homography/
|
||||
Python/OAK/datasets/oak-fcc-3/.cache/
|
||||
Python/OAK/datasets/gal5000/dataset/
|
||||
Python/OAK/datasets/gal5000/backup/
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import json
|
||||
import os
|
||||
import time
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
|
@ -1045,3 +1047,339 @@ class CameraMultispectral:
|
|||
|
||||
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)
|
||||
|
||||
elif mode == "homography":
|
||||
H = cfg.get("homographies", {}).get(f"{role}_to_rgb")
|
||||
|
||||
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, calib_size, profile_name = self._resolve_homography_entry_for_role(role)
|
||||
H = self._scale_homography_to_runtime(
|
||||
H,
|
||||
calib_size=calib_size,
|
||||
|
|
@ -3611,22 +3601,30 @@ class RawProcessorCore:
|
|||
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.
|
||||
|
||||
Suporta:
|
||||
- contrato antigo: fusion_config.homographies.re_to_rgb/nir_to_rgb
|
||||
- contrato novo: fusion_config.homography_profiles.<perfil>.homographies.*
|
||||
"""
|
||||
role = str(role).lower()
|
||||
fusion = getattr(self, "fusion_config", {}) or {}
|
||||
homographies = fusion.get("homographies", {}) or {}
|
||||
|
||||
key = f"{role}_to_rgb"
|
||||
H = homographies.get(key)
|
||||
H, calib_size, profile_name = self._resolve_homography_entry_for_role(role)
|
||||
|
||||
if H is None:
|
||||
# Fallbacks para contratos diferentes.
|
||||
H = homographies.get(role)
|
||||
ref_h, ref_w = int(ref_size[0]), int(ref_size[1])
|
||||
|
||||
if H is None:
|
||||
raise RuntimeError(f"Homografia ausente para role={role}. Esperado fusion_config.homographies.{key}")
|
||||
H_scaled = self._scale_homography_to_runtime(
|
||||
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):
|
||||
"""
|
||||
|
|
@ -3770,6 +3768,7 @@ class RawProcessorCore:
|
|||
"geometry_cache_hit": bool(geom.get("prepare_cache_hit", False)),
|
||||
"geometry_cache_hits": int(geom.get("cache_hits", 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)
|
||||
|
|
@ -3962,34 +3961,38 @@ class RawProcessorCore:
|
|||
"""
|
||||
Chave simples e estável para cache da geometria.
|
||||
|
||||
A geometria depende de:
|
||||
- tamanho do RGB de referência
|
||||
Considera:
|
||||
- tamanho do RGB/ref
|
||||
- target final
|
||||
- roles presentes
|
||||
- crop_valid_common / resize_after_crop
|
||||
- homografias e calibration_size
|
||||
|
||||
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().
|
||||
- crop/resize
|
||||
- homografia efetivamente selecionada por perfil
|
||||
- calibration_size efetivo por role
|
||||
"""
|
||||
ref_h, ref_w = int(ref_size[0]), int(ref_size[1])
|
||||
target_w, target_h = int(target_size[0]), int(target_size[1])
|
||||
|
||||
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()]))
|
||||
|
||||
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 (
|
||||
ref_w,
|
||||
ref_h,
|
||||
|
|
@ -3998,9 +4001,8 @@ class RawProcessorCore:
|
|||
roles,
|
||||
bool(fusion.get("crop_valid_common", False)),
|
||||
bool(fusion.get("resize_after_crop", False)),
|
||||
tuple(fusion.get("homography_calibration_size") or fusion.get("calibration_size") or []),
|
||||
h_sig("re_to_rgb"),
|
||||
h_sig("nir_to_rgb"),
|
||||
h_sig_for_role("re"),
|
||||
h_sig_for_role("nir"),
|
||||
)
|
||||
|
||||
def clear_direct_fusion_geometry_cache(self):
|
||||
|
|
@ -4053,8 +4055,14 @@ class RawProcessorCore:
|
|||
# Homografias escaladas para runtime.
|
||||
# ------------------------------------------------------------
|
||||
H_role_to_rgb = {}
|
||||
homography_profiles_used = {}
|
||||
for role in ("re", "nir"):
|
||||
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)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
|
|
@ -4103,6 +4111,7 @@ class RawProcessorCore:
|
|||
"prepare_cache_hit": False,
|
||||
"cache_hits": int(self._direct_fusion_geometry_cache_hits),
|
||||
"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,
|
||||
|
|
@ -4376,6 +4385,120 @@ class RawProcessorCore:
|
|||
|
||||
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(
|
||||
|
|
@ -4806,3 +4929,6 @@ class RawProcessorCore:
|
|||
self._flatfield_runtime_cache[key] = 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,
|
||||
"bayer_pattern": bayer,
|
||||
"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)),
|
||||
"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.samples = self._collect_samples()
|
||||
self._add_sample_class_stats()
|
||||
|
||||
if not self.samples:
|
||||
raise RuntimeError(f"Nenhuma amostra encontrada em: {self.root}")
|
||||
|
|
@ -450,6 +451,59 @@ class OakFcc3TensorMultiHeadDataset(Dataset):
|
|||
"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):
|
||||
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
|
||||
|
||||
|
||||
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"]
|
||||
|
||||
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("--balanced_sampler", action="store_true")
|
||||
parser.add_argument("--samples_per_epoch", type=int, default=0)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
set_seed(args.seed)
|
||||
|
|
@ -1579,10 +1679,32 @@ def main():
|
|||
|
||||
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(
|
||||
ds_train,
|
||||
batch_size=args.batch,
|
||||
shuffle=True,
|
||||
shuffle=train_shuffle,
|
||||
sampler=train_sampler,
|
||||
num_workers=args.num_workers,
|
||||
pin_memory=True,
|
||||
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],
|
||||
"homographies": {
|
||||
"re_to_rgb": [
|
||||
[
|
||||
1.0103099557765387,
|
||||
0.00879456142897448,
|
||||
-9.666692994320178
|
||||
],
|
||||
[
|
||||
-0.00014434784827104316,
|
||||
1.0214419841673288,
|
||||
50.41247184630176
|
||||
],
|
||||
[
|
||||
6.270900951674823e-06,
|
||||
1.788434277122896e-05,
|
||||
1.0
|
||||
]
|
||||
],
|
||||
"nir_to_rgb": [
|
||||
[
|
||||
0.994769714806237,
|
||||
0.005296878287321148,
|
||||
-6.433772056422987
|
||||
],
|
||||
[
|
||||
-0.00092900679814922,
|
||||
1.0017494877636166,
|
||||
29.883379280823718
|
||||
],
|
||||
[
|
||||
-5.35529902496836e-08,
|
||||
7.331793189295934e-06,
|
||||
1.0
|
||||
]
|
||||
]
|
||||
"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": {
|
||||
"re_to_rgb": [
|
||||
[
|
||||
1.014814963583485,
|
||||
0.015257560646391,
|
||||
-27.5415127674596
|
||||
],
|
||||
[
|
||||
0.000064618247568,
|
||||
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,
|
||||
"resize_after_crop": true,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"camera": "oak-fcc-3",
|
||||
"modelo": "segformer_b1",
|
||||
"model_name": "target_fixed",
|
||||
"model_name": "copypaste",
|
||||
"main_class_name": "cana",
|
||||
"es_classes": "",
|
||||
"model_to_use": "geral",
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
"mask_dir": "masks",
|
||||
"classes": {"chao": 0, "cana": 1, "erva": 2},
|
||||
"ignore_index": 255,
|
||||
"loss_weight": 0.10
|
||||
"loss_weight": 0.20
|
||||
},
|
||||
"vegetation": {
|
||||
"enabled": true,
|
||||
|
|
@ -54,7 +54,7 @@
|
|||
"mask_dir": "__derived_target__",
|
||||
"classes": {"background": 0, "target": 1},
|
||||
"ignore_index": 255,
|
||||
"loss_weight": 0.45,
|
||||
"loss_weight": 0.35,
|
||||
"derived": true
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1090,17 +1090,7 @@ class RawProcessorCore:
|
|||
warped_mask = self._affine_image(mask, dx, dy, theta_deg)
|
||||
|
||||
elif mode == "homography":
|
||||
H = cfg.get("homographies", {}).get(f"{role}_to_rgb")
|
||||
|
||||
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, calib_size, profile_name = self._resolve_homography_entry_for_role(role)
|
||||
H = self._scale_homography_to_runtime(
|
||||
H,
|
||||
calib_size=calib_size,
|
||||
|
|
@ -3611,22 +3601,30 @@ class RawProcessorCore:
|
|||
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.
|
||||
|
||||
Suporta:
|
||||
- contrato antigo: fusion_config.homographies.re_to_rgb/nir_to_rgb
|
||||
- contrato novo: fusion_config.homography_profiles.<perfil>.homographies.*
|
||||
"""
|
||||
role = str(role).lower()
|
||||
fusion = getattr(self, "fusion_config", {}) or {}
|
||||
homographies = fusion.get("homographies", {}) or {}
|
||||
|
||||
key = f"{role}_to_rgb"
|
||||
H = homographies.get(key)
|
||||
H, calib_size, profile_name = self._resolve_homography_entry_for_role(role)
|
||||
|
||||
if H is None:
|
||||
# Fallbacks para contratos diferentes.
|
||||
H = homographies.get(role)
|
||||
ref_h, ref_w = int(ref_size[0]), int(ref_size[1])
|
||||
|
||||
if H is None:
|
||||
raise RuntimeError(f"Homografia ausente para role={role}. Esperado fusion_config.homographies.{key}")
|
||||
H_scaled = self._scale_homography_to_runtime(
|
||||
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):
|
||||
"""
|
||||
|
|
@ -3770,6 +3768,7 @@ class RawProcessorCore:
|
|||
"geometry_cache_hit": bool(geom.get("prepare_cache_hit", False)),
|
||||
"geometry_cache_hits": int(geom.get("cache_hits", 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)
|
||||
|
|
@ -3962,34 +3961,38 @@ class RawProcessorCore:
|
|||
"""
|
||||
Chave simples e estável para cache da geometria.
|
||||
|
||||
A geometria depende de:
|
||||
- tamanho do RGB de referência
|
||||
Considera:
|
||||
- tamanho do RGB/ref
|
||||
- target final
|
||||
- roles presentes
|
||||
- crop_valid_common / resize_after_crop
|
||||
- homografias e calibration_size
|
||||
|
||||
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().
|
||||
- crop/resize
|
||||
- homografia efetivamente selecionada por perfil
|
||||
- calibration_size efetivo por role
|
||||
"""
|
||||
ref_h, ref_w = int(ref_size[0]), int(ref_size[1])
|
||||
target_w, target_h = int(target_size[0]), int(target_size[1])
|
||||
|
||||
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()]))
|
||||
|
||||
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 (
|
||||
ref_w,
|
||||
ref_h,
|
||||
|
|
@ -3998,9 +4001,8 @@ class RawProcessorCore:
|
|||
roles,
|
||||
bool(fusion.get("crop_valid_common", False)),
|
||||
bool(fusion.get("resize_after_crop", False)),
|
||||
tuple(fusion.get("homography_calibration_size") or fusion.get("calibration_size") or []),
|
||||
h_sig("re_to_rgb"),
|
||||
h_sig("nir_to_rgb"),
|
||||
h_sig_for_role("re"),
|
||||
h_sig_for_role("nir"),
|
||||
)
|
||||
|
||||
def clear_direct_fusion_geometry_cache(self):
|
||||
|
|
@ -4053,8 +4055,14 @@ class RawProcessorCore:
|
|||
# Homografias escaladas para runtime.
|
||||
# ------------------------------------------------------------
|
||||
H_role_to_rgb = {}
|
||||
homography_profiles_used = {}
|
||||
for role in ("re", "nir"):
|
||||
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)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
|
|
@ -4103,6 +4111,7 @@ class RawProcessorCore:
|
|||
"prepare_cache_hit": False,
|
||||
"cache_hits": int(self._direct_fusion_geometry_cache_hits),
|
||||
"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,
|
||||
|
|
@ -4376,6 +4385,120 @@ class RawProcessorCore:
|
|||
|
||||
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(
|
||||
|
|
@ -4806,3 +4929,6 @@ class RawProcessorCore:
|
|||
self._flatfield_runtime_cache[key] = gain_tensor
|
||||
return gain_tensor
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -85,11 +85,10 @@ def raw10_bin_to_gray(path: Path, width: int, height: int, *, is_rgb: bool, baye
|
|||
raw = path.read_bytes()
|
||||
raw10 = unpack_raw10_packed(raw, width, height)
|
||||
|
||||
if is_rgb:
|
||||
bgr = debayer_raw10_to_bgr_u8(raw10, bayer=bayer)
|
||||
gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY)
|
||||
else:
|
||||
gray = normalize_to_u8(raw10)
|
||||
# Para detecção ChArUco, usar o RAW Bayer como intensidade costuma ser mais fiel
|
||||
# que debayerizar, porque o debayer pode suavizar os IDs ArUco.
|
||||
# O parâmetro is_rgb fica mantido por compatibilidade com chamadas antigas.
|
||||
gray = normalize_to_u8(raw10)
|
||||
|
||||
if use_clahe:
|
||||
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
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Metadata
|
||||
# ============================================================
|
||||
|
||||
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
|
||||
|
||||
|
||||
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"]:
|
||||
root = meta.get(root_key)
|
||||
if isinstance(root, dict):
|
||||
info = root.get(cam_key)
|
||||
if isinstance(info, dict):
|
||||
if isinstance(info, dict) and "width" in info and "height" in info:
|
||||
return info
|
||||
|
||||
# Busca recursiva, mas só aceita se parecer info geométrica da câmera.
|
||||
stack = [meta]
|
||||
while stack:
|
||||
obj = stack.pop()
|
||||
|
||||
if isinstance(obj, dict):
|
||||
if cam_key in obj and isinstance(obj[cam_key], dict):
|
||||
return obj[cam_key]
|
||||
info = obj[cam_key]
|
||||
if "width" in info and "height" in info:
|
||||
return info
|
||||
|
||||
for v in obj.values():
|
||||
if isinstance(v, (dict, list)):
|
||||
stack.append(v)
|
||||
|
||||
elif isinstance(obj, list):
|
||||
for v in obj:
|
||||
if isinstance(v, (dict, list)):
|
||||
|
|
@ -155,11 +173,11 @@ def try_get_width_height_from_meta(root_dir: Path, cam_key: str):
|
|||
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:
|
||||
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:
|
||||
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}")
|
||||
|
||||
|
||||
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
|
||||
# ============================================================
|
||||
|
|
@ -319,11 +357,34 @@ def get_board_corners(board):
|
|||
|
||||
def create_detector_params():
|
||||
aruco = cv2.aruco
|
||||
|
||||
if hasattr(aruco, "DetectorParameters"):
|
||||
return aruco.DetectorParameters()
|
||||
if hasattr(aruco, "DetectorParameters_create"):
|
||||
return aruco.DetectorParameters_create()
|
||||
return None
|
||||
params = aruco.DetectorParameters()
|
||||
elif hasattr(aruco, "DetectorParameters_create"):
|
||||
params = aruco.DetectorParameters_create()
|
||||
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):
|
||||
|
|
@ -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
|
||||
# ============================================================
|
||||
|
||||
def main():
|
||||
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("--rgb_cam", default="CAM_A")
|
||||
|
|
@ -650,8 +959,8 @@ def main():
|
|||
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("--width", type=int, default=1280)
|
||||
parser.add_argument("--height", type=int, default=800)
|
||||
parser.add_argument("--width", type=int, default=0)
|
||||
parser.add_argument("--height", type=int, default=0)
|
||||
parser.add_argument("--rgb_bayer", default="BGGR")
|
||||
|
||||
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_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(
|
||||
"--homo_ref_frame",
|
||||
default=None,
|
||||
"--homography_mode",
|
||||
default="all_valid",
|
||||
choices=["all_valid", "off"],
|
||||
help=(
|
||||
"Triplet usado como plano de referência para H_RE_to_RGB e H_NIR_to_RGB. "
|
||||
"Aceita índice, nome parcial/stem ou caminho de um .bin do triplet. "
|
||||
"Se omitido, não salva homografias planares."
|
||||
"Modo de homografia planar. 'all_valid' acumula todos os pares válidos "
|
||||
"RE->RGB e NIR->RGB encontrados no dataset. 'off' desativa."
|
||||
),
|
||||
)
|
||||
# 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)
|
||||
|
||||
# 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("--show", action="store_true")
|
||||
|
||||
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)
|
||||
debug_dir = out_dir / "debug"
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
|
@ -701,14 +1060,14 @@ def main():
|
|||
|
||||
sizes = {}
|
||||
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)
|
||||
|
||||
image_w = min(w for w, h in sizes.values())
|
||||
image_h = min(h for w, h in sizes.values())
|
||||
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}")
|
||||
for cam in cams:
|
||||
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] marker_length={args.marker_length}")
|
||||
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:
|
||||
print(f"[INFO] arquivos {cam}: {len(by_cam[cam])}")
|
||||
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.")
|
||||
|
||||
aruco_dict = get_aruco_dict(args.aruco_dict)
|
||||
|
|
@ -741,6 +1105,11 @@ def main():
|
|||
accepted = 0
|
||||
rejected = 0
|
||||
|
||||
homography_pairs = {
|
||||
"RE_to_RGB": [],
|
||||
"NIR_to_RGB": [],
|
||||
}
|
||||
|
||||
for idx, item in enumerate(triplets):
|
||||
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_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[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]))
|
||||
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)
|
||||
rejected += 1
|
||||
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:
|
||||
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)
|
||||
rejected += 1
|
||||
continue
|
||||
|
|
@ -791,7 +1239,7 @@ def main():
|
|||
single_imgpoints[cam].append(imgpoints_by_cam[cam].copy())
|
||||
|
||||
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)
|
||||
|
||||
if args.show:
|
||||
|
|
@ -810,11 +1258,79 @@ def main():
|
|||
cv2.destroyAllWindows()
|
||||
|
||||
print("")
|
||||
print(f"[INFO] triplets aceitos: {accepted}")
|
||||
print(f"[INFO] triplets rejeitados: {rejected}")
|
||||
print(f"[INFO] triplets aceitos para calibração stereo: {accepted}")
|
||||
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:
|
||||
raise RuntimeError(f"Poucos triplets aceitos: {accepted}. Ideal: 20-40+ bons.")
|
||||
homo_result = None
|
||||
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 = {}
|
||||
D = {}
|
||||
|
|
@ -901,41 +1417,12 @@ def main():
|
|||
extr_R_to_ref[cam] = R_cam_to_ref
|
||||
extr_T_to_ref[cam] = T_cam_to_ref
|
||||
|
||||
# Homografia planar opcional.
|
||||
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}%")
|
||||
# Homografia planar all_valid já foi calculada antes da calibração stereo.
|
||||
|
||||
out_path = out_dir / f"multicam_calib_{'_'.join(cams)}_ref_{args.ref_cam}.npz"
|
||||
|
||||
save_dict = {
|
||||
"schema": "multicam_charuco_raw10_v2_planar_homography",
|
||||
"schema": "multicam_charuco_raw10_v4_planar_homography_accumulated",
|
||||
"cams": np.array(cams),
|
||||
"rgb_cam": args.rgb_cam,
|
||||
"nir_cam": args.nir_cam,
|
||||
|
|
@ -950,6 +1437,10 @@ def main():
|
|||
"rectify_alpha": args.rectify_alpha,
|
||||
"accepted": accepted,
|
||||
"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:
|
||||
|
|
@ -974,31 +1465,7 @@ def main():
|
|||
save_dict[f"pair_{key}_{rk}"] = rv
|
||||
|
||||
if homo_result is not None:
|
||||
hs = homo_result["stats"]
|
||||
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
|
||||
add_homography_to_save_dict(save_dict, homo_result, args)
|
||||
else:
|
||||
save_dict["has_planar_homography"] = False
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue