From 6e166748cf4e90836e1a2451e695c1ab4fa29a8d Mon Sep 17 00:00:00 2001 From: Diego Freitas Date: Wed, 6 May 2026 13:27:38 -0300 Subject: [PATCH] ajustes na calibragem e visualizacao --- .../oak-fcc-3/core/oak_fcc3_client.py | 10 +- .../oak-fcc-3/core/raw_processor_core.py | 362 +++++++++++++++--- .../oak-fcc-3/utils/check_saved_files.py | 156 +++++++- .../utils/flatfield_calibration_tool.py | 53 ++- .../utils/radiometric_config_tool.py | 2 +- 5 files changed, 490 insertions(+), 93 deletions(-) diff --git a/Python/OAK/datasets/oak-fcc-3/core/oak_fcc3_client.py b/Python/OAK/datasets/oak-fcc-3/core/oak_fcc3_client.py index b6bbb5126..a5fcdd316 100644 --- a/Python/OAK/datasets/oak-fcc-3/core/oak_fcc3_client.py +++ b/Python/OAK/datasets/oak-fcc-3/core/oak_fcc3_client.py @@ -175,19 +175,15 @@ class OakFcc3Client: return self.svc.capture_frame(timeout=timeout) def get_next_frame(self, timeout=2.0): - frame, meta, _ = self.get_next_decoded( - timeout=timeout, - update_radiometry=False, - ) + frame, meta, _ = self.get_next_decoded(timeout=timeout) return frame, meta - def get_next_decoded(self, timeout=2.0, update_radiometry=True): + def get_next_decoded(self, timeout=2.0): raw_frame, raw_meta = self.get_next_raw_frame(timeout=timeout) decoded = self.decode_stream_cameras(raw_frame, raw_meta) - if update_radiometry: - self.update_radiometry(decoded, raw_meta) + self.update_radiometry(decoded, raw_meta) frame_type = str(raw_meta.get("frame_type", self.frame_type)).upper() diff --git a/Python/OAK/datasets/oak-fcc-3/core/raw_processor_core.py b/Python/OAK/datasets/oak-fcc-3/core/raw_processor_core.py index 00533e590..43fb85cf2 100644 --- a/Python/OAK/datasets/oak-fcc-3/core/raw_processor_core.py +++ b/Python/OAK/datasets/oak-fcc-3/core/raw_processor_core.py @@ -11,6 +11,7 @@ class RawProcessorCore: self.sensor_width = sensor_width self.sensor_height = sensor_height self.bayer_pattern = bayer_pattern.upper() + self.fusion_config = { "alignment_mode": "manual_affine", "baseline_mm": 75.0, @@ -26,6 +27,7 @@ class RawProcessorCore: "resize_after_crop": True, "target_size": None, } + self.rgb_calibration = { "enabled": False, "gains": { @@ -53,6 +55,14 @@ class RawProcessorCore: self.flatfield_maps = {} self.flatfield_loaded = False + self.radiometric_normalization_config = { + "enabled": False, + "method": "exposure_gain_reference", + "reference_controls": {}, + "clip_output": False, + } + self.camera_settings = {} + if calibration_json_path: self.load_fusion_config_json(calibration_json_path) @@ -469,10 +479,67 @@ class RawProcessorCore: return decoded + def _decode_spectral_frame_to_float01(self, data, cam_meta): + arr = data + + if arr.ndim == 3 and arr.shape[2] == 1: + arr = arr[:, :, 0] + + bit_depth = int(cam_meta.get("bit_depth", 8)) + raw_format = str(cam_meta.get("raw_format", "")).upper() + packed = bool(cam_meta.get("packed", False)) + channels = int(cam_meta.get("channels", 1)) if cam_meta.get("channels") is not None else 1 + + sensor_width = int(cam_meta.get("width", self.sensor_width)) + sensor_height = int(cam_meta.get("height", arr.shape[0])) + packed_width = int(cam_meta.get("packed_width", 0) or 0) + + looks_like_raw10_packed = ( + arr.ndim == 2 + and arr.dtype == np.uint8 + and arr.shape[0] == sensor_height + and ( + raw_format == "RAW10_PACKED" + or packed + or bit_depth == 10 + or (packed_width > 0 and arr.shape[1] == packed_width and packed_width != sensor_width) + or arr.shape[1] == int(sensor_width * 10 / 8) + ) + ) + + if looks_like_raw10_packed: + raw16 = self.unpack_raw10_packed( + arr, + sensor_width=sensor_width, + sensor_height=sensor_height, + ) + + max_val = float((1 << bit_depth) - 1) + return np.clip(raw16.astype(np.float32) / max_val, 0.0, 1.0) + + # Caso preview/processado: mono já vem uint8 normal. + if arr.ndim == 2 and arr.dtype == np.uint8: + return np.clip(arr.astype(np.float32) / 255.0, 0.0, 1.0) + + if arr.ndim == 2 and arr.dtype == np.uint16: + max_val = float((1 << bit_depth) - 1) if bit_depth > 0 and bit_depth <= 16 else 65535.0 + return np.clip(arr.astype(np.float32) / max_val, 0.0, 1.0) + + arr01 = arr.astype(np.float32) + if arr01.max() > 1.5: + arr01 /= 255.0 + + return np.clip(arr01, 0.0, 1.0) + def fuse_multispec_cameras(self, decoded, meta, channels_expected): - # Flat-field pertence ao espaço nativo de cada câmera. - # Por isso é aplicado antes de warp/homografia/crop comum. - decoded = self.apply_flatfield_to_decoded(decoded) + # 1) Coloca todos os frames na mesma escala de exposição/ganho de referência + decoded = self.normalize_decoded_by_capture_controls(decoded, meta) + + # 2) Subtrai dark/offset no espaço individual de cada câmera + decoded = self.apply_dark_to_decoded(decoded) + + # 3) Aplica o ganho espacial do flat field no espaço individual de cada câmera + decoded = self.apply_flat_gain_to_decoded(decoded) rgb_cam_id = self._find_cam_by_role(decoded, "rgb") if rgb_cam_id is None: @@ -801,7 +868,6 @@ class RawProcessorCore: return arr.reshape(shape) - def save_rgb_u8_file(self, path: str, arr: np.ndarray): arr.astype(np.uint8).tofile(path) @@ -862,23 +928,47 @@ class RawProcessorCore: fusion = data.get("fusion_config") if isinstance(fusion, dict): - self.fusion_config = self._merge_fusion_config(self.fusion_config, fusion) + self.fusion_config = self._merge_config(self.fusion_config, fusion) else: print("[WARN] JSON sem fusion_config. Mantendo config padrão.") rgb_cal = data.get("rgb_calibration") if isinstance(rgb_cal, dict): - self.rgb_calibration = self._merge_fusion_config(self.rgb_calibration, rgb_cal) + self.rgb_calibration = self._merge_config(self.rgb_calibration, rgb_cal) flatfield = data.get("flatfield_config") if isinstance(flatfield, dict): - self.flatfield_config = self._merge_fusion_config(self.flatfield_config, flatfield) + self.flatfield_config = self._merge_config(self.flatfield_config, flatfield) self.load_flatfield_maps() else: self.flatfield_config["enabled"] = False self.flatfield_maps = {} self.flatfield_loaded = False + rad_norm_config = data.get("radiometric_normalization") + if isinstance(rad_norm_config, dict): + self.radiometric_normalization_config = self._merge_config(self.radiometric_normalization_config, rad_norm_config) + + cam_set = data.get("camera_settings") + if isinstance(cam_set, dict): + self.camera_settings = self._merge_config(self.camera_settings, cam_set) + + def _merge_config(self, default_cfg: dict, loaded_cfg: dict) -> dict: + cfg = json.loads(json.dumps(default_cfg)) + + def merge(dst: dict, src: dict): + for key, value in src.items(): + if isinstance(value, dict) and isinstance(dst.get(key), dict): + merge(dst[key], value) + else: + dst[key] = value + + if isinstance(loaded_cfg, dict): + merge(cfg, loaded_cfg) + + return cfg + + def _resolve_calibration_path(self, path: str) -> str: if not path: return "" @@ -966,12 +1056,16 @@ class RawProcessorCore: return self.flatfield_loaded - def apply_flatfield_to_decoded(self, decoded: dict) -> dict: + def apply_dark_to_decoded(self, decoded: dict) -> dict: cfg = self.flatfield_config or {} if not cfg.get("enabled", False): return decoded + subtract_dark = bool(cfg.get("subtract_dark", True)) + if not subtract_dark: + return decoded + if not self.flatfield_loaded: self.load_flatfield_maps() @@ -990,8 +1084,6 @@ class RawProcessorCore: new_item = dict(item) new_meta = dict(item.get("meta", {}) or {}) - subtract_dark = bool(cfg.get("subtract_dark", False)) - clip_output = bool(cfg.get("clip_output", True)) if role == "rgb": if img.ndim != 3 or img.shape[2] < 3: @@ -999,11 +1091,70 @@ class RawProcessorCore: continue out = img.astype(np.float32).copy() + for idx, ch in enumerate(("R", "G", "B")): - out[:, :, idx] = self._apply_flatfield_single_channel( + out[:, :, idx] = self._subtract_dark_single_channel( + out[:, :, idx], + ch, + ) + + new_item["image"] = out + + elif role in ("re", "nir"): + ch = "RE" if role == "re" else "NIR" + + new_item["image"] = self._subtract_dark_single_channel( + img.astype(np.float32), + ch, + ) + + else: + corrected[cam_id] = item + continue + + new_meta["dark_applied"] = True + new_item["meta"] = new_meta + corrected[cam_id] = new_item + + return corrected + + def apply_flat_gain_to_decoded(self, decoded: dict) -> dict: + cfg = self.flatfield_config or {} + + if not cfg.get("enabled", False): + return decoded + + if not self.flatfield_loaded: + self.load_flatfield_maps() + + if not self.flatfield_loaded: + return decoded + + clip_output = bool(cfg.get("clip_output", True)) + corrected = {} + + for cam_id, item in decoded.items(): + role = str(item.get("role") or item.get("meta", {}).get("role") or "").lower() + img = item.get("image") + + if img is None: + corrected[cam_id] = item + continue + + new_item = dict(item) + new_meta = dict(item.get("meta", {}) or {}) + + if role == "rgb": + if img.ndim != 3 or img.shape[2] < 3: + corrected[cam_id] = item + continue + + out = img.astype(np.float32).copy() + + for idx, ch in enumerate(("R", "G", "B")): + out[:, :, idx] = self._apply_flat_gain_single_channel( out[:, :, idx], ch, - subtract_dark=subtract_dark, clip_output=clip_output, ) @@ -1011,10 +1162,10 @@ class RawProcessorCore: elif role in ("re", "nir"): ch = "RE" if role == "re" else "NIR" - new_item["image"] = self._apply_flatfield_single_channel( + + new_item["image"] = self._apply_flat_gain_single_channel( img.astype(np.float32), ch, - subtract_dark=subtract_dark, clip_output=clip_output, ) @@ -1029,11 +1180,38 @@ class RawProcessorCore: return corrected - def _apply_flatfield_single_channel( + def _subtract_dark_single_channel( + self, + img: np.ndarray, + channel_name: str, + ) -> np.ndarray: + ch = str(channel_name).upper() + entry = self.flatfield_maps.get(ch) + + if not entry: + return img.astype(np.float32, copy=False) + + dark = entry.get("dark") + if dark is None: + return img.astype(np.float32, copy=False) + + base = img.astype(np.float32) + + dark = dark.astype(np.float32) + if dark.shape[:2] != base.shape[:2]: + dark = cv2.resize( + dark, + (base.shape[1], base.shape[0]), + interpolation=cv2.INTER_LINEAR, + ) + + out = np.maximum(base - dark, 0.0) + return out.astype(np.float32, copy=False) + + def _apply_flat_gain_single_channel( self, img: np.ndarray, channel_name: str, - subtract_dark: bool = False, clip_output: bool = True, ) -> np.ndarray: ch = str(channel_name).upper() @@ -1046,71 +1224,133 @@ class RawProcessorCore: if gain is None: return img.astype(np.float32, copy=False) - if gain.shape[:2] != img.shape[:2]: + base = img.astype(np.float32) + + gain = gain.astype(np.float32) + if gain.shape[:2] != base.shape[:2]: gain = cv2.resize( - gain.astype(np.float32), - (img.shape[1], img.shape[0]), + gain, + (base.shape[1], base.shape[0]), interpolation=cv2.INTER_LINEAR, ) - base = img.astype(np.float32) - - if subtract_dark and "dark" in entry: - dark = entry["dark"].astype(np.float32) - if dark.shape[:2] != img.shape[:2]: - dark = cv2.resize(dark, (img.shape[1], img.shape[0]), interpolation=cv2.INTER_LINEAR) - base = np.maximum(base - dark, 0.0) - - out = base * gain.astype(np.float32) + out = base * gain if clip_output: out = np.clip(out, 0.0, 1.0) return out.astype(np.float32, copy=False) - def _merge_fusion_config(self, default_cfg: dict, loaded_cfg: dict) -> dict: - cfg = json.loads(json.dumps(default_cfg)) + + def normalize_decoded_by_capture_controls(self, decoded: dict, meta: dict | None = None) -> dict: + cfg = self.radiometric_normalization_config or {} - for key, value in loaded_cfg.items(): - if isinstance(value, dict) and isinstance(cfg.get(key), dict): - cfg[key].update(value) - else: - cfg[key] = value + if not cfg.get("enabled", False): + return decoded - return cfg + method = str(cfg.get("method", "exposure_gain_reference")).lower() + if method != "exposure_gain_reference": + return decoded - def _decode_spectral_frame_to_float01(self, data, cam_meta): - arr = data + controls = self._extract_actual_controls_from_meta(meta) + if not controls: + return decoded - if arr.ndim == 3 and arr.shape[2] == 1: - arr = arr[:, :, 0] + reference_controls = cfg.get("reference_controls", {}) or {} + clip_output = bool(cfg.get("clip_output", False)) - bit_depth = int(cam_meta.get("bit_depth", 8)) - channels = int(cam_meta.get("channels", 1)) if cam_meta.get("channels") is not None else 1 + normalized = {} - # Caso preview/processado: mono já vem uint8/uint16 normal. - if arr.ndim == 2 and arr.dtype == np.uint8: - return np.clip(arr.astype(np.float32) / 255.0, 0.0, 1.0) + for cam_id, item in decoded.items(): + role = str(item.get("role") or item.get("meta", {}).get("role") or "").lower() + img = item.get("image") - if arr.ndim == 2 and arr.dtype == np.uint16 and bit_depth != 10: - return np.clip(arr.astype(np.float32) / 65535.0, 0.0, 1.0) + if img is None or not role: + normalized[cam_id] = item + continue - if bit_depth == 10: - sensor_width = int(cam_meta.get("width", self.sensor_width)) - sensor_height = int(cam_meta.get("height", arr.shape[0])) + actual_ctrl = controls.get(role, {}) or {} - raw16 = self.unpack_raw10_packed( - arr, - sensor_width=sensor_width, - sensor_height=sensor_height, + ref_ctrl = ( + reference_controls.get(role) + or self.camera_settings.get(role) + or actual_ctrl + or {} ) - max_val = float((1 << bit_depth) - 1) - return np.clip(raw16.astype(np.float32) / max_val, 0.0, 1.0) + actual_factor = self._exposure_gain_factor(actual_ctrl) + ref_factor = self._exposure_gain_factor(ref_ctrl) - # fallback - arr01 = arr.astype(np.float32) - if arr01.max() > 1.5: - arr01 /= 255.0 + if actual_factor <= 0 or ref_factor <= 0: + normalized[cam_id] = item + continue - return np.clip(arr01, 0.0, 1.0) + scale = ref_factor / actual_factor + + new_item = dict(item) + new_meta = dict(item.get("meta", {}) or {}) + + out = img.astype(np.float32) * float(scale) + + if clip_output: + out = np.clip(out, 0.0, 1.0) + + new_meta["radiometric_normalization_applied"] = True + new_meta["radiometric_normalization_method"] = method + new_meta["radiometric_normalization_scale"] = float(scale) + new_meta["radiometric_actual_factor"] = float(actual_factor) + new_meta["radiometric_reference_factor"] = float(ref_factor) + + new_item["image"] = out.astype(np.float32, copy=False) + new_item["meta"] = new_meta + + normalized[cam_id] = new_item + + return normalized + + def _extract_actual_controls_from_meta(self, meta: dict | None) -> dict: + if not meta: + return {} + + # Preferência: controles reais daquele frame. + controls = meta.get("actual_camera_controls") + if isinstance(controls, dict) and controls: + return controls + + # Possíveis nomes alternativos. + controls = meta.get("camera_controls") + if isinstance(controls, dict) and controls: + return controls + + controls = meta.get("startup_camera_controls") + if isinstance(controls, dict) and controls: + return controls + + # Em alguns casos o JSON da captura pode ter stream_meta separado, + # mas se o meta recebido aqui for só stream_meta, talvez não tenha controles. + return {} + + def _exposure_gain_factor(self, ctrl: dict) -> float: + if not isinstance(ctrl, dict): + return 0.0 + + exp = ctrl.get("exposure_time_us", None) + gain = ctrl.get("analogue_gain", None) + + try: + exp = float(exp) + except Exception: + exp = 0.0 + + try: + gain = float(gain) + except Exception: + gain = 1.0 + + if exp <= 0: + return 0.0 + + if gain <= 0: + gain = 1.0 + + return float(exp * gain) diff --git a/Python/OAK/datasets/oak-fcc-3/utils/check_saved_files.py b/Python/OAK/datasets/oak-fcc-3/utils/check_saved_files.py index 37bdc66ba..a09f0dcc3 100644 --- a/Python/OAK/datasets/oak-fcc-3/utils/check_saved_files.py +++ b/Python/OAK/datasets/oak-fcc-3/utils/check_saved_files.py @@ -29,6 +29,112 @@ def chw_to_hwc(arr: np.ndarray) -> np.ndarray: return np.transpose(arr, (1, 2, 0)) +def tensor_to_preview_panels(tensor: np.ndarray): + """ + Recebe tensor CHW [R,G,B,RE,NIR] float32 e devolve painéis visuais. + """ + if tensor.ndim != 3 or tensor.shape[0] < 5: + raise RuntimeError(f"Tensor MULTISPEC inválido: shape={tensor.shape}") + + rgb_hwc = np.transpose(tensor[:3].astype(np.float32), (1, 2, 0)) + rgb_bgr = normalize_float01_to_bgr(rgb_hwc) + + re01 = tensor[3].astype(np.float32) + nir01 = tensor[4].astype(np.float32) + + re_bgr = cv2.cvtColor( + np.clip(re01 * 255.0, 0, 255).astype(np.uint8), + cv2.COLOR_GRAY2BGR + ) + + nir_bgr = cv2.cvtColor( + np.clip(nir01 * 255.0, 0, 255).astype(np.uint8), + cv2.COLOR_GRAY2BGR + ) + + return [ + ("MULTISPEC RGB final", rgb_bgr, f"tensor {list(tensor.shape)} | canais 0,1,2"), + ("MULTISPEC RE final", re_bgr, "tensor canal 3"), + ("MULTISPEC NIR final", nir_bgr, "tensor canal 4"), + ] + + +def build_multispec_from_raw_native_multi(group: dict, meta: dict): + """ + Reconstrói o tensor MULTISPEC final a partir dos .bin RAW_BRUTO salvos. + + Usa: + - saved_payload_paths + - saved_payload_shapes + - saved_payload_dtypes + - stream_meta.camera_info + - camera_params_json/module_params.json + """ + if meta.get("saved_payload_type") != "raw_native_multi": + return None, "captura não é raw_native_multi" + + stream_meta = meta.get("stream_meta", {}) or {} + camera_info = stream_meta.get("camera_info", {}) or {} + + saved_dtypes = meta.get("saved_payload_dtypes", {}) or {} + saved_shapes = meta.get("saved_payload_shapes", {}) or {} + + frame = {} + + for cam_id, path in group["cameras"].items(): + saved_dtype = saved_dtypes.get(cam_id) + saved_shape = saved_shapes.get(cam_id) + + if saved_dtype is None or saved_shape is None: + raise RuntimeError(f"Faltam dtype/shape para {cam_id}") + + arr = np.fromfile(str(path), dtype=np.dtype(saved_dtype)).reshape(tuple(saved_shape)) + frame[cam_id] = arr + + if not frame: + raise RuntimeError("Nenhum payload de câmera encontrado para reconstruir MULTISPEC.") + + sensor_width = int(meta.get("sensor_width", 1280)) + sensor_height = int(meta.get("sensor_height", 800)) + bayer = meta.get("bayer_pattern", "RGGB") + + # Tenta usar o mesmo module_params que foi usado na captura. + calib_path = meta.get("camera_params_json") or "calibration/module_params.json" + + # Se vier relativo, tenta resolver relativo ao diretório atual. + # Normalmente seu script roda da raiz do projeto, então calibration/module_params.json funciona. + if calib_path and not os.path.isfile(calib_path): + # fallback: tenta relativo à pasta do JSON + json_dir = Path(group["json"]).parent + alt = json_dir / calib_path + if alt.exists(): + calib_path = str(alt) + else: + print(f"[WARN] module_params não encontrado: {calib_path}. Tentando sem calibração.") + calib_path = None + + core = RawProcessorCore( + sensor_width=sensor_width, + sensor_height=sensor_height, + bayer_pattern=bayer, + calibration_json_path=calib_path, + ) + + # O decode precisa do stream_meta com camera_info. + processing_meta = dict(stream_meta) + + # A normalização radiométrica precisa dos controles reais salvos no JSON da captura. + if meta.get("actual_camera_controls") is not None: + processing_meta["actual_camera_controls"] = meta.get("actual_camera_controls") + + if meta.get("startup_camera_controls") is not None: + processing_meta["startup_camera_controls"] = meta.get("startup_camera_controls") + + tensor = core.build_infer_tensor_from_stream(frame, processing_meta, 5) + + return tensor, f"MULTISPEC gerado offline do RAW_BRUTO | shape={list(tensor.shape)}" + + def build_visual_from_saved_payload(payload_path: Path, meta: dict, cam_id: str | None = None) -> tuple[np.ndarray, str]: """ Retorna: @@ -236,12 +342,51 @@ def build_panels_from_group(group): panels = [] meta = load_json(group["json"]) + saved_type = meta.get("saved_payload_type") - preview_saved = cv2.imread(str(group["png"]), cv2.IMREAD_COLOR) - if preview_saved is None: - raise RuntimeError(f"Falha ao ler preview PNG: {group['png']}") - panels.append(("Preview salvo", preview_saved, f"{preview_saved.shape[1]}x{preview_saved.shape[0]}")) + # ========================================================= + # Para RAW_BRUTO multi, o primeiro painel vira o tensor final + # gerado offline a partir dos .bin salvos. + # ========================================================= + if saved_type == "raw_native_multi": + try: + tensor, desc = build_multispec_from_raw_native_multi(group, meta) + tensor_panels = tensor_to_preview_panels(tensor) + # Aqui colocamos só o RGB final como painel principal, + # para substituir o antigo PNG salvo. + title, img, subtitle = tensor_panels[0] + panels.append((title, img, desc)) + + # Opcional: se quiser também ver RE/NIR finais do tensor, + # descomente estas duas linhas: + # panels.append(tensor_panels[1]) + # panels.append(tensor_panels[2]) + + except Exception as e: + # Fallback para o PNG salvo caso a reconstrução falhe. + preview_saved = cv2.imread(str(group["png"]), cv2.IMREAD_COLOR) + if preview_saved is None: + raise RuntimeError(f"Falha ao ler preview PNG: {group['png']}") + + panels.append(( + "Preview salvo fallback", + preview_saved, + f"Falha ao gerar MULTISPEC offline: {e}" + )) + + else: + # Para RGB/MULTISPEC salvos direto, mantém comportamento antigo. + preview_saved = cv2.imread(str(group["png"]), cv2.IMREAD_COLOR) + if preview_saved is None: + raise RuntimeError(f"Falha ao ler preview PNG: {group['png']}") + + panels.append(("Preview salvo", preview_saved, f"{preview_saved.shape[1]}x{preview_saved.shape[0]}")) + + # ========================================================= + # Se houver payload final único, reconstrói normalmente. + # Ex: saved_payload_type == multispec + # ========================================================= if group["final_raw"] is not None: result, desc = build_visual_from_saved_payload(group["final_raw"], meta) @@ -251,6 +396,9 @@ def build_panels_from_group(group): else: panels.append(("Reconstruido (final)", result, desc)) + # ========================================================= + # Continua mostrando CAM_A/CAM_B/CAM_C reconstruídas individualmente. + # ========================================================= for cam_id, path in group["cameras"].items(): img, desc = build_visual_from_saved_payload(path, meta, cam_id=cam_id) panels.append((f"{cam_id} reconstruido", img, desc)) diff --git a/Python/OAK/datasets/oak-fcc-3/utils/flatfield_calibration_tool.py b/Python/OAK/datasets/oak-fcc-3/utils/flatfield_calibration_tool.py index 0c48aa0f1..0566aa38f 100644 --- a/Python/OAK/datasets/oak-fcc-3/utils/flatfield_calibration_tool.py +++ b/Python/OAK/datasets/oak-fcc-3/utils/flatfield_calibration_tool.py @@ -96,7 +96,7 @@ def get_image_by_role(decoded: dict, role: str): def validate_module_ready(status: dict, raw_policy: str): if not status.get("ok", True): - raise RuntimeError(f"Status inválido retornado pelo módulo: {status}") + raise RuntimeError(f"Status invalido retornado pelo modulo: {status}") active_roles = status.get("active_roles", {}) or {} active_count = int(status.get("camera_count_active", 0)) @@ -254,6 +254,19 @@ def extract_channels_from_decoded(decoded: dict) -> dict: _, re01 = get_image_by_role(decoded, "re") _, nir01 = get_image_by_role(decoded, "nir") + def assert_not_raw10_packed_image(role, img, expected_w=1280): + if img is None: + return + + if img.ndim == 2 and img.shape[1] == int(expected_w * 10 / 8): + raise RuntimeError( + f"{role.upper()} parece RAW10_PACKED interpretado como imagem: " + f"shape={img.shape}. Esperado decodificado com largura {expected_w}." + ) + + assert_not_raw10_packed_image("re", re01, expected_w=1280) + assert_not_raw10_packed_image("nir", nir01, expected_w=1280) + out = {} if rgb01 is not None: @@ -354,7 +367,7 @@ def build_board(decoded, controls, state_lines, progress_lines, preview_scale=1. "S = pular etapa dark/preto", "Q / ESC = sair sem salvar", "", - "Dica: branco/preto devem preencher todo o campo de visão.", + "Dica: branco/preto devem preencher todo o campo de visao.", "Para dark-frame perfeito, tampe as lentes em vez de usar fundo preto.", ]) @@ -420,12 +433,12 @@ def capture_stage( progress_lines = [ f"Etapa: {stage_name}", f"Descartando frames iniciais: {len(seen_frame_ids)}/{discard_frames}", - "Aguardando estabilização de exposição/stream...", + "Aguardando estabilizacao de exposicao/stream...", ] board = build_board( decoded=last_decoded, controls=last_controls, - state_lines=[f"CALIBRAÇÃO FLAT-FIELD - {stage_name.upper()}"], + state_lines=[f"CALIBRACAO FLAT-FIELD - {stage_name.upper()}"], progress_lines=progress_lines, preview_scale=preview_scale, ) @@ -458,7 +471,7 @@ def capture_stage( board = build_board( decoded=last_decoded, controls=last_controls, - state_lines=[f"CALIBRAÇÃO FLAT-FIELD - {stage_name.upper()}"], + state_lines=[f"CALIBRACAO FLAT-FIELD - {stage_name.upper()}"], progress_lines=progress_lines, preview_scale=preview_scale, ) @@ -473,7 +486,7 @@ def capture_stage( k = cv2.waitKey(1) & 0xFF if k in (ord("q"), ord("Q"), 27): - raise KeyboardInterrupt("Captura cancelada pelo usuário.") + raise KeyboardInterrupt("Captura cancelada pelo usuario.") return channel_stack, controls_log, meta_log @@ -579,10 +592,10 @@ def show_final_preview(window_name: str, gain_maps: dict, preview_scale: float): "Flat-field salvo com sucesso.", "ENTER/qualquer tecla = fechar", "", - "Use estes mapas antes da fusão geométrica.", + "Use estes mapas antes da fusao geometrica.", "", "Obs: RGB e mono podem ter shapes diferentes;", - "isso é normal se o decode gerar resoluções distintas.", + "isso e normal se o decode gerar resolucoes distintas.", ], x=18, y=36) top = np.hstack([panels[0], panels[1], panels[2]]) @@ -635,7 +648,7 @@ def wait_for_enter_or_skip( if allow_skip and k in (ord("s"), ord("S")): return "skip" if k in (ord("q"), ord("Q"), 27): - raise KeyboardInterrupt("Cancelado pelo usuário.") + raise KeyboardInterrupt("Cancelado pelo usuario.") # ============================================================ @@ -644,7 +657,7 @@ def wait_for_enter_or_skip( def main(): parser = argparse.ArgumentParser( - description="Calibrador automático de flat-field/dark-frame para o módulo RGB/RE/NIR OAK-FCC-3.", + description="Calibrador automatico de flat-field/dark-frame para o módulo RGB/RE/NIR OAK-FCC-3.", formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) @@ -721,7 +734,7 @@ def main(): output_dtype="uint8", capture_mode=args.capture_mode, raw_policy=args.raw_policy, - module_calibration_json=args.module_calibration_json + module_calibration_json=args.module_calibration_json or None ) as cam: validate_module_ready(cam.get_status(), args.raw_policy) @@ -731,11 +744,11 @@ def main(): window_name=window_name, title="ETAPA 1/2 - WHITE / FLAT FIELD", instruction_lines=[ - "Posicione o módulo na altura real de operação.", - "Aponte para uma superfície branca/cinza fosca, uniforme e sem textura.", - "Evite reflexos, sombras laterais e saturação.", - "A superfície deve preencher todo o campo de visão.", - "Pressione ENTER para começar a captura WHITE.", + "Posicione o modulo na altura real de operacao.", + "Aponte para uma superficie branca/cinza fosca, uniforme e sem textura.", + "Evite reflexos, sombras laterais e saturacao.", + "A superficie deve preencher todo o campo de visao.", + "Pressione ENTER para comecar a captura WHITE.", ], preview_scale=args.preview_scale, allow_skip=False, @@ -767,10 +780,10 @@ def main(): window_name=window_name, title="ETAPA 2/2 - DARK / PRETO", instruction_lines=[ - "Agora faça a captura dark/preto.", - "Melhor opção: tampe as lentes completamente.", + "Agora faca a captura dark/preto.", + "Melhor opcao: tampe as lentes completamente.", "Alternativa: use fundo preto fosco preenchendo todo o frame.", - "Mantenha exposição/ganho iguais aos da etapa anterior, se possível.", + "Mantenha exposicao/ganho iguais aos da etapa anterior, se possivel.", "Pressione ENTER para capturar DARK/PRETO.", ], preview_scale=args.preview_scale, @@ -801,7 +814,7 @@ def main(): # Processamento robusto. processing_panel = np.zeros((720, 1280, 3), dtype=np.uint8) overlay_hud(processing_panel, [ - "Processando calibração flat-field...", + "Processando calibracao flat-field...", "Calculando medianas robustas por canal.", "Gerando mapas de ganho e previews.", ], x=40, y=80, font_scale=0.8, line_step=34) diff --git a/Python/OAK/datasets/oak-fcc-3/utils/radiometric_config_tool.py b/Python/OAK/datasets/oak-fcc-3/utils/radiometric_config_tool.py index 09cd81e17..57f2da26b 100644 --- a/Python/OAK/datasets/oak-fcc-3/utils/radiometric_config_tool.py +++ b/Python/OAK/datasets/oak-fcc-3/utils/radiometric_config_tool.py @@ -703,7 +703,7 @@ def main(): validate_module_ready(cam.get_status(), args.raw_policy) while True: - raw_frame, raw_meta, decoded = cam.get_next_decoded(timeout=2.0, update_radiometry=False) + raw_frame, raw_meta, decoded = cam.get_next_decoded(timeout=2.0) if raw_meta is not None and raw_meta.get("frame_id") != last_frame_id: last_frame_id = raw_meta.get("frame_id")