import json import argparse import os from datetime import datetime def now_str(): return datetime.now().strftime("%Y-%m-%d %H:%M:%S") def load_json(path): with open(path, "r", encoding="utf-8") as f: return json.load(f) def rel_or_abs(path): """ Mantém o caminho como veio, mas normaliza separadores. Isso evita quebrar projetos Windows/Linux e deixa o module_params legível. """ if path is None: return None return str(path).replace("\\", "/") def build_flatfield_config(flatfield_json_path, flatfield_data): """ Espera o JSON gerado pelo flatfield_calibration_tool_v2.py. Estrutura esperada: schema: multispec_flatfield_v1 outputs.npz: calibration/flatfield_maps_v1.npz channels: ["R", "G", "B", "RE", "NIR"] maps.CH.gain_key / flat_norm_key / ... """ if not isinstance(flatfield_data, dict): return { "enabled": False, "reason": "flatfield_json ausente ou inválido", } outputs = flatfield_data.get("outputs", {}) or {} maps = flatfield_data.get("maps", {}) or {} npz_path = outputs.get("npz") if not npz_path: # Fallback: tenta deduzir pelo nome do json. base, _ = os.path.splitext(flatfield_json_path) npz_path = base + ".npz" channels = flatfield_data.get("channels") or ["R", "G", "B", "RE", "NIR"] channel_maps = {} for ch in channels: m = maps.get(ch, {}) or {} channel_maps[ch] = { "gain_key": m.get("gain_key", f"gain_{ch}"), "flat_norm_key": m.get("flat_norm_key", f"flat_norm_{ch}"), "white_median_key": m.get("white_median_key", f"white_median_{ch}"), "dark_median_key": m.get("dark_median_key", f"dark_median_{ch}"), "shape": m.get("shape"), "gain_min": m.get("gain_min"), "gain_max": m.get("gain_max"), "gain_mean": m.get("gain_mean"), "gain_std": m.get("gain_std"), } return { "enabled": True, "subtract_dark": True, "schema": flatfield_data.get("schema", "multispec_flatfield_v1"), "created_at": flatfield_data.get("created_at"), "json_file": rel_or_abs(flatfield_json_path), "npz_file": rel_or_abs(npz_path), "apply_before_fusion": True, "apply_after_decode": True, "apply_space": "native_camera_space", "map_type": "gain", "formula": "channel_corrected = max(channel_linear - dark, 0) * gain_map", "channels": channels, "channel_maps": channel_maps, "exp_gain_correct_during_flat_capture": bool(flatfield_data.get("exp_gain_correct", False)), "smooth_ksize": flatfield_data.get("smooth_ksize"), "min_gain": flatfield_data.get("min_gain"), "max_gain": flatfield_data.get("max_gain"), "notes": flatfield_data.get("notes", ""), } def pick_radiometric_config(radiometric_data: dict, selected_profile: str | None = None): if not isinstance(radiometric_data, dict): return None # 1) Novo contrato: usa radiometric_config da raiz se existir. root_cfg = radiometric_data.get("radiometric_config") if isinstance(root_cfg, dict): return root_cfg # 2) Usa active_profile se existir. active_profile = radiometric_data.get("active_profile") if active_profile in ("global_scene_mode", "three_reference_patches_mode"): cfg = radiometric_data.get(active_profile, {}).get("radiometric_config") if isinstance(cfg, dict): return cfg # 3) Fallback explícito por argumento. if selected_profile: cfg = radiometric_data.get(selected_profile, {}).get("radiometric_config") if isinstance(cfg, dict): return cfg return None def main(): parser = argparse.ArgumentParser( description="Monta o module_params.json unificando calibração de câmera, fusão, radiometria e flat-field.", formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) parser.add_argument("--camera_json", default="calibration/sensor_calibration.json") parser.add_argument("--fusion_json", default="calibration/manual_offsets.json") parser.add_argument("--radiometric_json", default="calibration/radiometric_config.json") parser.add_argument("--radiometric_profile", default="global_scene_mode", choices=["global_scene_mode", "three_reference_patches_mode"]) parser.add_argument("--flatfield_json", default="calibration/flatfield_maps_v1.json") parser.add_argument("--disable_flatfield", action="store_true") parser.add_argument("--out", default="calibration/module_params.json") args = parser.parse_args() cam_data = load_json(args.camera_json) fusion_data = load_json(args.fusion_json) radiometric_data = {} if args.radiometric_json: radiometric_data = load_json(args.radiometric_json) flatfield_data = {} flatfield_config = { "enabled": False, "reason": "flatfield não informado ou arquivo inexistente", } if not args.disable_flatfield and args.flatfield_json and os.path.isfile(args.flatfield_json): flatfield_data = load_json(args.flatfield_json) flatfield_config = build_flatfield_config(args.flatfield_json, flatfield_data) elif args.disable_flatfield: flatfield_config = { "enabled": False, "reason": "desabilitado via --disable_flatfield", } # ========================= # CAMERA SETTINGS # ========================= camera_settings = cam_data.get("camera_settings") if not isinstance(camera_settings, dict): raise RuntimeError("camera_json sem camera_settings válido") # ========================= # RGB CALIBRATION # ========================= rgb_calibration = cam_data.get("rgb_calibration") if not isinstance(rgb_calibration, dict): rgb_calibration = { "enabled": False, "gains": { "R": 1.0, "G": 1.0, "B": 1.0, } } # ========================= # RADIOMETRIC # ========================= radiometric_config = pick_radiometric_config(radiometric_data, selected_profile=args.radiometric_profile) if not isinstance(radiometric_config, dict): radiometric_config = cam_data.get("radiometric_config") if not isinstance(radiometric_config, dict): radiometric_config = { "enabled": True, "interval_s": 0.20, "verbose": True, "metering_mode": "global", "spectral_control_mode": "shared", "control_metric": "p50", "target_value": 0.40, "deadband": 0.04, "p95_limit": 0.90, "saturation_limit_pct": 0.50, "saturation_hard_pct": 10.0, "saturation_extreme_pct": 50.0, "dark_limit_pct": 35.0, "control_strategy": "ratio", "ratio_alpha": 0.55, "ratio_min": 0.55, "ratio_max": 1.85, "reduce_fast_factor": 0.70, "gain_return_enabled": True, "gain_return_factor": 0.50, "gain_reduce_on_saturation": True, "gain_hard_reset_on_saturation": False, "gain_increase_required_cycles": 5, "gain_decrease_required_cycles": 2, "gain_step_up": 0.20, "gain_step_down": 0.50, "exp_high_ratio_for_gain": 0.95, "exp_low_ratio_for_gain_return": 0.75, "prefer_exposure": True, "exp_min_us": 100, "exp_max_us": 80000, "gain_min": 1.0, "gain_max": 4.0, "role_limits": { "rgb": {"exp_min_us": 100, "exp_max_us": 80000, "gain_min": 1.0, "gain_max": 4.0}, "re": {"exp_min_us": 100, "exp_max_us": 80000, "gain_min": 1.0, "gain_max": 3.0}, "nir": {"exp_min_us": 100, "exp_max_us": 80000, "gain_min": 1.0, "gain_max": 3.0}, }, "exp_apply_threshold_us": 40, "gain_apply_threshold": 0.03, "ready_required_cycles": 3, "apply_same_spectral_to_both": True, "spectral_roles": ["re", "nir"], } radiometric_normalization_config = radiometric_data.get("radiometric_normalization") if not isinstance(radiometric_normalization_config, dict): radiometric_normalization_config = cam_data.get("radiometric_normalization") if not isinstance(radiometric_normalization_config, dict): radiometric_normalization_config = { "enabled": False, "method": "exposure_gain_reference", "apply_stage": "after_dark_before_flat_gain", "reference_controls": { "rgb": {"exposure_time_us": 3000, "analogue_gain": 1.0}, "re": {"exposure_time_us": 7000, "analogue_gain": 1.0}, "nir": {"exposure_time_us": 7000, "analogue_gain": 1.0}, }, "clip_output": True, } else: radiometric_normalization_config["enabled"] = bool( radiometric_normalization_config.get("enabled", False) ) patch_normalization_config = radiometric_data.get("patch_normalization") if not isinstance(patch_normalization_config, dict): patch_normalization_config = cam_data.get("patch_normalization") if not isinstance(patch_normalization_config, dict): patch_normalization_config = { "enabled": True, "apply_when_metering_mode": "reference_patches", "apply_stage": "after_fusion", "method": "gray_scale_with_white_guard", "space": "multispec_tensor", "targets_by_patch_channel": { "black": { "R": 0.06, "G": 0.06, "B": 0.06, "RE": 0.06, "NIR": 0.06 }, "gray": { "R": 0.34, "G": 0.34, "B": 0.34, "RE": 0.24, "NIR": 0.30 }, "white": { "R": 0.78, "G": 0.78, "B": 0.78, "RE": 0.78, "NIR": 0.78 }, }, "white_guard_max": 0.92, "white_guard_max_by_channel": { "R": 0.92, "G": 0.92, "B": 0.92, "RE": 0.88, "NIR": 0.88, }, "scale_min": 0.35, "scale_max": 2.50, "clip_output": True, "require_valid_gray": True, "use_black_for_offset": False, "save_patch_stats": True, } # ========================= # FUSION CONFIG # ========================= fusion_config = { "alignment_mode": fusion_data.get("alignment_mode", "manual_affine"), "baseline_mm": fusion_data.get("baseline_mm", 75.0), "reference_camera": fusion_data.get("reference_camera", "rgb"), "manual_offsets": fusion_data.get("manual_offsets", {}), "homographies": fusion_data.get("homographies", {}), "crop_valid_common": fusion_data.get("crop_valid_common", True), "resize_after_crop": fusion_data.get("resize_after_crop", True), "target_size": fusion_data.get("target_size", None), } # ========================= # MODULE PARAMS FINAL # ========================= module_params = { "schema": "multispec_module_params_v3", "saved_at": now_str(), "frame_type": cam_data.get("frame_type", fusion_data.get("frame_type", "RAW_BRUTO")), "capture_mode_requested": cam_data.get("capture_mode_requested", "AUTO"), "capture_mode_effective": cam_data.get("capture_mode_effective", "AUTO"), "raw_policy": cam_data.get("raw_policy", "allow_single"), "sensor_width": cam_data.get("sensor_width", fusion_data.get("sensor_width")), "sensor_height": cam_data.get("sensor_height", fusion_data.get("sensor_height")), "bayer_pattern": cam_data.get("bayer_pattern", fusion_data.get("bayer_pattern", "GBRG")), "camera_settings": camera_settings, "fusion_config": fusion_config, "radiometric_config": radiometric_config, "radiometric_normalization": radiometric_normalization_config, "patch_normalization": patch_normalization_config, "rgb_calibration": rgb_calibration, "flatfield_config": flatfield_config, } with open(args.out, "w", encoding="utf-8") as f: json.dump(module_params, f, ensure_ascii=False, indent=2) print(f"[OK] module_params gerado em: {args.out}") if flatfield_config.get("enabled"): print(f"[OK] flatfield habilitado: {flatfield_config.get('npz_file')}") else: print(f"[WARN] flatfield desabilitado: {flatfield_config.get('reason')}") if __name__ == "__main__": main()