ajustes na calibragem pelos cartões wb
|
|
@ -407,6 +407,7 @@ def main():
|
||||||
"camera_params_json": args.module_calibration_json,
|
"camera_params_json": args.module_calibration_json,
|
||||||
"note": "autosave",
|
"note": "autosave",
|
||||||
"raw_preview_reference_camera": preview_source_id,
|
"raw_preview_reference_camera": preview_source_id,
|
||||||
|
"patch_normalization_result": cam.get_last_patch_normalization_result()
|
||||||
}
|
}
|
||||||
|
|
||||||
save_sample(
|
save_sample(
|
||||||
|
|
@ -469,6 +470,7 @@ def main():
|
||||||
"camera_params_json": args.module_calibration_json,
|
"camera_params_json": args.module_calibration_json,
|
||||||
"note": "manual",
|
"note": "manual",
|
||||||
"raw_preview_reference_camera": preview_source_id,
|
"raw_preview_reference_camera": preview_source_id,
|
||||||
|
"patch_normalization_result": cam.get_last_patch_normalization_result()
|
||||||
}
|
}
|
||||||
|
|
||||||
save_sample(
|
save_sample(
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 412 KiB After Width: | Height: | Size: 283 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 602 KiB After Width: | Height: | Size: 298 KiB |
|
Before Width: | Height: | Size: 319 KiB After Width: | Height: | Size: 306 KiB |
|
Before Width: | Height: | Size: 264 KiB After Width: | Height: | Size: 254 KiB |
|
Before Width: | Height: | Size: 3.0 MiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 327 KiB After Width: | Height: | Size: 312 KiB |
|
Before Width: | Height: | Size: 2.8 MiB After Width: | Height: | Size: 1.5 MiB |
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"schema": "multispec_module_params_v3",
|
"schema": "multispec_module_params_v3",
|
||||||
"saved_at": "2026-05-06 10:16:07",
|
"saved_at": "2026-05-07 09:03:53",
|
||||||
"frame_type": "RAW_BRUTO",
|
"frame_type": "RAW_BRUTO",
|
||||||
"capture_mode_requested": "AUTO",
|
"capture_mode_requested": "AUTO",
|
||||||
"capture_mode_effective": "AUTO",
|
"capture_mode_effective": "AUTO",
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
"rgb": {
|
"rgb": {
|
||||||
"ae_enable": false,
|
"ae_enable": false,
|
||||||
"awb_enable": false,
|
"awb_enable": false,
|
||||||
"exposure_time_us": 2000,
|
"exposure_time_us": 4800,
|
||||||
"analogue_gain": 1.0,
|
"analogue_gain": 1.0,
|
||||||
"colour_gains": [
|
"colour_gains": [
|
||||||
1.0,
|
1.0,
|
||||||
|
|
@ -22,14 +22,14 @@
|
||||||
"re": {
|
"re": {
|
||||||
"ae_enable": false,
|
"ae_enable": false,
|
||||||
"awb_enable": false,
|
"awb_enable": false,
|
||||||
"exposure_time_us": 5000,
|
"exposure_time_us": 13200,
|
||||||
"analogue_gain": 1.0,
|
"analogue_gain": 1.0,
|
||||||
"colour_gains": null
|
"colour_gains": null
|
||||||
},
|
},
|
||||||
"nir": {
|
"nir": {
|
||||||
"ae_enable": false,
|
"ae_enable": false,
|
||||||
"awb_enable": false,
|
"awb_enable": false,
|
||||||
"exposure_time_us": 5000,
|
"exposure_time_us": 13200,
|
||||||
"analogue_gain": 1.0,
|
"analogue_gain": 1.0,
|
||||||
"colour_gains": null
|
"colour_gains": null
|
||||||
}
|
}
|
||||||
|
|
@ -94,20 +94,13 @@
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"interval_s": 0.5,
|
"interval_s": 0.5,
|
||||||
"verbose": true,
|
"verbose": true,
|
||||||
"metering_mode": "global",
|
"metering_mode": "reference_patches",
|
||||||
"spectral_control_mode": "shared",
|
"spectral_control_mode": "shared",
|
||||||
"global_roi_pct": {
|
|
||||||
"x0": 0.082812,
|
|
||||||
"y0": 0.08,
|
|
||||||
"x1": 0.903125,
|
|
||||||
"y1": 0.905
|
|
||||||
},
|
|
||||||
"control_metric": "p50",
|
"control_metric": "p50",
|
||||||
"target_value": 0.4,
|
"target_value": 0.5,
|
||||||
"deadband": 0.04,
|
"deadband": 0.035,
|
||||||
"p95_limit": 0.94,
|
"p95_limit": 0.95,
|
||||||
"saturation_limit_pct": 1.0,
|
"saturation_limit_pct": 2.0,
|
||||||
"dark_limit_pct": 35.0,
|
|
||||||
"alpha": 0.18,
|
"alpha": 0.18,
|
||||||
"exp_step_gain": 0.55,
|
"exp_step_gain": 0.55,
|
||||||
"prefer_exposure": true,
|
"prefer_exposure": true,
|
||||||
|
|
@ -115,46 +108,231 @@
|
||||||
"exp_max_us": 80000,
|
"exp_max_us": 80000,
|
||||||
"gain_min": 1.0,
|
"gain_min": 1.0,
|
||||||
"gain_max": 4.0,
|
"gain_max": 4.0,
|
||||||
"role_limits": {
|
"reference_patches": [
|
||||||
"rgb": {
|
{
|
||||||
"exp_min_us": 100,
|
"name": "black_reference",
|
||||||
"exp_max_us": 80000,
|
"type": "black",
|
||||||
"gain_min": 1.0,
|
"roles": [
|
||||||
"gain_max": 4.0
|
"rgb",
|
||||||
|
"re",
|
||||||
|
"nir"
|
||||||
|
],
|
||||||
|
"roi_pct": {
|
||||||
|
"x0": 0.015625,
|
||||||
|
"y0": 0.9025,
|
||||||
|
"x1": 0.1125,
|
||||||
|
"y1": 0.995
|
||||||
|
},
|
||||||
|
"target_value": 0.08,
|
||||||
|
"weight": 0.7,
|
||||||
|
"roi_pct_by_role": {
|
||||||
|
"rgb": {
|
||||||
|
"x0": 0.015625,
|
||||||
|
"y0": 0.9025,
|
||||||
|
"x1": 0.1125,
|
||||||
|
"y1": 0.995
|
||||||
|
},
|
||||||
|
"re": {
|
||||||
|
"x0": 0.034375,
|
||||||
|
"y0": 0.83,
|
||||||
|
"x1": 0.132812,
|
||||||
|
"y1": 0.925
|
||||||
|
},
|
||||||
|
"nir": {
|
||||||
|
"x0": 0.0125,
|
||||||
|
"y0": 0.86,
|
||||||
|
"x1": 0.107813,
|
||||||
|
"y1": 0.95
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"re": {
|
{
|
||||||
"exp_min_us": 100,
|
"name": "gray_reference",
|
||||||
"exp_max_us": 80000,
|
"type": "gray",
|
||||||
"gain_min": 1.0,
|
"roles": [
|
||||||
"gain_max": 3.0
|
"rgb",
|
||||||
|
"re",
|
||||||
|
"nir"
|
||||||
|
],
|
||||||
|
"roi_pct": {
|
||||||
|
"x0": 0.475,
|
||||||
|
"y0": 0.895,
|
||||||
|
"x1": 0.56875,
|
||||||
|
"y1": 0.995
|
||||||
|
},
|
||||||
|
"target_value": 0.42,
|
||||||
|
"target_value_by_role": {
|
||||||
|
"rgb": 0.42,
|
||||||
|
"re": 0.36,
|
||||||
|
"nir": 0.40
|
||||||
|
},
|
||||||
|
"weight": 1.0,
|
||||||
|
"roi_pct_by_role": {
|
||||||
|
"rgb": {
|
||||||
|
"x0": 0.475,
|
||||||
|
"y0": 0.895,
|
||||||
|
"x1": 0.56875,
|
||||||
|
"y1": 0.995
|
||||||
|
},
|
||||||
|
"re": {
|
||||||
|
"x0": 0.5,
|
||||||
|
"y0": 0.83,
|
||||||
|
"x1": 0.590625,
|
||||||
|
"y1": 0.9225
|
||||||
|
},
|
||||||
|
"nir": {
|
||||||
|
"x0": 0.470313,
|
||||||
|
"y0": 0.86,
|
||||||
|
"x1": 0.565625,
|
||||||
|
"y1": 0.9625
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"nir": {
|
{
|
||||||
"exp_min_us": 100,
|
"name": "white_reference",
|
||||||
"exp_max_us": 80000,
|
"type": "white",
|
||||||
"gain_min": 1.0,
|
"roles": [
|
||||||
"gain_max": 3.0
|
"rgb",
|
||||||
|
"re",
|
||||||
|
"nir"
|
||||||
|
],
|
||||||
|
"roi_pct": {
|
||||||
|
"x0": 0.870313,
|
||||||
|
"y0": 0.9025,
|
||||||
|
"x1": 0.9625,
|
||||||
|
"y1": 0.995
|
||||||
|
},
|
||||||
|
"target_value": 0.82,
|
||||||
|
"weight": 0.8,
|
||||||
|
"roi_pct_by_role": {
|
||||||
|
"rgb": {
|
||||||
|
"x0": 0.870313,
|
||||||
|
"y0": 0.9025,
|
||||||
|
"x1": 0.9625,
|
||||||
|
"y1": 0.995
|
||||||
|
},
|
||||||
|
"re": {
|
||||||
|
"x0": 0.89375,
|
||||||
|
"y0": 0.8325,
|
||||||
|
"x1": 0.984375,
|
||||||
|
"y1": 0.93
|
||||||
|
},
|
||||||
|
"nir": {
|
||||||
|
"x0": 0.875,
|
||||||
|
"y0": 0.86,
|
||||||
|
"x1": 0.970313,
|
||||||
|
"y1": 0.96
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
],
|
||||||
"exp_apply_threshold_us": 80,
|
"exp_apply_threshold_us": 80,
|
||||||
"gain_apply_threshold": 0.05,
|
"gain_apply_threshold": 0.05,
|
||||||
"apply_same_spectral_to_both": true,
|
"apply_same_spectral_to_both": true,
|
||||||
"spectral_roles": [
|
"spectral_roles": [
|
||||||
"re",
|
"re",
|
||||||
"nir"
|
"nir"
|
||||||
]
|
],
|
||||||
|
"dark_limit_pct": 35.0,
|
||||||
|
"control_strategy": "ratio",
|
||||||
|
"ratio_alpha": 0.55,
|
||||||
|
"ratio_min": 0.55,
|
||||||
|
"ratio_max": 1.20,
|
||||||
|
"reduce_fast_factor": 0.60,
|
||||||
|
"factor_min": 0.72,
|
||||||
|
"factor_max": 1.28,
|
||||||
|
"gain_return_enabled": true,
|
||||||
|
"gain_reduce_on_saturation": true,
|
||||||
|
"gain_increase_required_cycles": 5,
|
||||||
|
"gain_decrease_required_cycles": 2,
|
||||||
|
"gain_step_up": 0.2,
|
||||||
|
"gain_step_down": 0.5,
|
||||||
|
"gain_hard_reset_on_saturation": false,
|
||||||
|
"exp_high_ratio_for_gain": 0.95,
|
||||||
|
"exp_low_ratio_for_gain_return": 0.75,
|
||||||
|
"role_limits": {
|
||||||
|
"rgb": {
|
||||||
|
"exp_min_us": 100,
|
||||||
|
"exp_max_us": 80000,
|
||||||
|
"gain_min": 1.0,
|
||||||
|
"gain_max": 2.0
|
||||||
|
},
|
||||||
|
"re": {
|
||||||
|
"exp_min_us": 100,
|
||||||
|
"exp_max_us": 80000,
|
||||||
|
"gain_min": 1.0,
|
||||||
|
"gain_max": 2.0
|
||||||
|
},
|
||||||
|
"nir": {
|
||||||
|
"exp_min_us": 100,
|
||||||
|
"exp_max_us": 80000,
|
||||||
|
"gain_min": 1.0,
|
||||||
|
"gain_max": 2.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ready_required_cycles": 3,
|
||||||
|
"patch_control_mode": "gray_primary",
|
||||||
|
"patch_require_order": true,
|
||||||
|
"patch_min_separation": 0.08,
|
||||||
|
"patch_white_sat_limit_pct": 2.0,
|
||||||
|
"patch_white_p95_limit": 0.94,
|
||||||
|
"patch_black_dark_limit_pct": 80.0,
|
||||||
|
"patch_black_max_p50": 0.2,
|
||||||
|
"patch_gray_min_p50": 0.08,
|
||||||
|
"patch_gray_max_p50": 0.85
|
||||||
|
},
|
||||||
|
"radiometric_normalization": {
|
||||||
|
"enabled": true,
|
||||||
|
"method": "exposure_gain_reference",
|
||||||
|
"apply_stage": "after_dark_before_flat_gain",
|
||||||
|
"reference_controls": {
|
||||||
|
"rgb": {
|
||||||
|
"exposure_time_us": 4800,
|
||||||
|
"analogue_gain": 1.0
|
||||||
|
},
|
||||||
|
"re": {
|
||||||
|
"exposure_time_us": 13200,
|
||||||
|
"analogue_gain": 1.0
|
||||||
|
},
|
||||||
|
"nir": {
|
||||||
|
"exposure_time_us": 13200,
|
||||||
|
"analogue_gain": 1.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"clip_output": true
|
||||||
|
},
|
||||||
|
"patch_normalization": {
|
||||||
|
"enabled": true,
|
||||||
|
"apply_when_metering_mode": "reference_patches",
|
||||||
|
"apply_stage": "after_fusion",
|
||||||
|
"method": "gray_scale_with_white_guard",
|
||||||
|
"space": "multispec_tensor",
|
||||||
|
"targets": {
|
||||||
|
"black": 0.06,
|
||||||
|
"gray": 0.4,
|
||||||
|
"white": 0.78
|
||||||
|
},
|
||||||
|
"white_guard_max": 0.92,
|
||||||
|
"scale_min": 0.35,
|
||||||
|
"scale_max": 2.5,
|
||||||
|
"clip_output": true,
|
||||||
|
"require_valid_gray": true,
|
||||||
|
"use_black_for_offset": false,
|
||||||
|
"save_patch_stats": true
|
||||||
},
|
},
|
||||||
"rgb_calibration": {
|
"rgb_calibration": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"gains": {
|
"gains": {
|
||||||
"R": 1.0,
|
"R": 1.2500000000000002,
|
||||||
"G": 1.0,
|
"G": 1.0,
|
||||||
"B": 1.0
|
"B": 1.5500000000000005
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"flatfield_config": {
|
"flatfield_config": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
"subtract_dark": true,
|
||||||
"schema": "multispec_flatfield_v1",
|
"schema": "multispec_flatfield_v1",
|
||||||
"created_at": "2026-05-06 10:14:01",
|
"created_at": "2026-05-06 13:37:25",
|
||||||
"json_file": "calibration/flatfield_maps_v1.json",
|
"json_file": "calibration/flatfield_maps_v1.json",
|
||||||
"npz_file": "calibration/flatfield_maps_v1.npz",
|
"npz_file": "calibration/flatfield_maps_v1.npz",
|
||||||
"apply_before_fusion": true,
|
"apply_before_fusion": true,
|
||||||
|
|
@ -179,10 +357,10 @@
|
||||||
400,
|
400,
|
||||||
640
|
640
|
||||||
],
|
],
|
||||||
"gain_min": 0.5480560660362244,
|
"gain_min": 0.5639018416404724,
|
||||||
"gain_max": 1.6087802648544312,
|
"gain_max": 1.8886771202087402,
|
||||||
"gain_mean": 1.0039905309677124,
|
"gain_mean": 1.0681155920028687,
|
||||||
"gain_std": 0.28920215368270874
|
"gain_std": 0.3506295084953308
|
||||||
},
|
},
|
||||||
"G": {
|
"G": {
|
||||||
"gain_key": "gain_G",
|
"gain_key": "gain_G",
|
||||||
|
|
@ -193,10 +371,10 @@
|
||||||
400,
|
400,
|
||||||
640
|
640
|
||||||
],
|
],
|
||||||
"gain_min": 0.5317091345787048,
|
"gain_min": 0.5427238941192627,
|
||||||
"gain_max": 1.8951553106307983,
|
"gain_max": 2.177884101867676,
|
||||||
"gain_mean": 1.0328274965286255,
|
"gain_mean": 1.0985527038574219,
|
||||||
"gain_std": 0.3529214859008789
|
"gain_std": 0.4235461354255676
|
||||||
},
|
},
|
||||||
"B": {
|
"B": {
|
||||||
"gain_key": "gain_B",
|
"gain_key": "gain_B",
|
||||||
|
|
@ -207,10 +385,10 @@
|
||||||
400,
|
400,
|
||||||
640
|
640
|
||||||
],
|
],
|
||||||
"gain_min": 0.5656915903091431,
|
"gain_min": 0.57332444190979,
|
||||||
"gain_max": 1.8551995754241943,
|
"gain_max": 1.991808533668518,
|
||||||
"gain_mean": 1.0326939821243286,
|
"gain_mean": 1.0765669345855713,
|
||||||
"gain_std": 0.3330632746219635
|
"gain_std": 0.36871764063835144
|
||||||
},
|
},
|
||||||
"RE": {
|
"RE": {
|
||||||
"gain_key": "gain_RE",
|
"gain_key": "gain_RE",
|
||||||
|
|
@ -219,12 +397,12 @@
|
||||||
"dark_median_key": "dark_median_RE",
|
"dark_median_key": "dark_median_RE",
|
||||||
"shape": [
|
"shape": [
|
||||||
800,
|
800,
|
||||||
1600
|
1280
|
||||||
],
|
],
|
||||||
"gain_min": 0.955564022064209,
|
"gain_min": 0.7719405889511108,
|
||||||
"gain_max": 4.0,
|
"gain_max": 1.6437114477157593,
|
||||||
"gain_mean": 1.2053359746932983,
|
"gain_mean": 1.0222759246826172,
|
||||||
"gain_std": 0.28413665294647217
|
"gain_std": 0.19225353002548218
|
||||||
},
|
},
|
||||||
"NIR": {
|
"NIR": {
|
||||||
"gain_key": "gain_NIR",
|
"gain_key": "gain_NIR",
|
||||||
|
|
@ -233,12 +411,12 @@
|
||||||
"dark_median_key": "dark_median_NIR",
|
"dark_median_key": "dark_median_NIR",
|
||||||
"shape": [
|
"shape": [
|
||||||
800,
|
800,
|
||||||
1600
|
1280
|
||||||
],
|
],
|
||||||
"gain_min": 0.8534727096557617,
|
"gain_min": 0.808167576789856,
|
||||||
"gain_max": 4.0,
|
"gain_max": 1.5716955661773682,
|
||||||
"gain_mean": 1.146588921546936,
|
"gain_mean": 1.021193265914917,
|
||||||
"gain_std": 0.4995245635509491
|
"gain_std": 0.1498267650604248
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"exp_gain_correct_during_flat_capture": true,
|
"exp_gain_correct_during_flat_capture": true,
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,44 @@
|
||||||
"spectral_roles": [
|
"spectral_roles": [
|
||||||
"re",
|
"re",
|
||||||
"nir"
|
"nir"
|
||||||
]
|
],
|
||||||
|
"control_strategy": "ratio",
|
||||||
|
"ratio_alpha": 0.55,
|
||||||
|
"ratio_min": 0.55,
|
||||||
|
"ratio_max": 1.85,
|
||||||
|
"reduce_fast_factor": 0.75,
|
||||||
|
"factor_min": 0.72,
|
||||||
|
"factor_max": 1.28,
|
||||||
|
"gain_return_enabled": true,
|
||||||
|
"gain_reduce_on_saturation": true,
|
||||||
|
"gain_increase_required_cycles": 5,
|
||||||
|
"gain_decrease_required_cycles": 2,
|
||||||
|
"gain_step_up": 0.2,
|
||||||
|
"gain_step_down": 0.5,
|
||||||
|
"gain_hard_reset_on_saturation": false,
|
||||||
|
"exp_high_ratio_for_gain": 0.95,
|
||||||
|
"exp_low_ratio_for_gain_return": 0.75,
|
||||||
|
"ready_required_cycles": 3,
|
||||||
|
"global_roi_pct_by_role": {
|
||||||
|
"rgb": {
|
||||||
|
"x0": 0.08,
|
||||||
|
"y0": 0.08,
|
||||||
|
"x1": 0.92,
|
||||||
|
"y1": 0.92
|
||||||
|
},
|
||||||
|
"re": {
|
||||||
|
"x0": 0.08,
|
||||||
|
"y0": 0.08,
|
||||||
|
"x1": 0.92,
|
||||||
|
"y1": 0.92
|
||||||
|
},
|
||||||
|
"nir": {
|
||||||
|
"x0": 0.08,
|
||||||
|
"y0": 0.08,
|
||||||
|
"x1": 0.92,
|
||||||
|
"y1": 0.92
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"three_reference_patches_mode": {
|
"three_reference_patches_mode": {
|
||||||
|
|
@ -83,13 +120,33 @@
|
||||||
"nir"
|
"nir"
|
||||||
],
|
],
|
||||||
"roi_pct": {
|
"roi_pct": {
|
||||||
"x0": 0.05,
|
"x0": 0.015625,
|
||||||
"y0": 0.92,
|
"y0": 0.9025,
|
||||||
"x1": 0.18,
|
"x1": 0.1125,
|
||||||
"y1": 0.99
|
"y1": 0.995
|
||||||
},
|
},
|
||||||
"target_value": 0.08,
|
"target_value": 0.08,
|
||||||
"weight": 0.7
|
"weight": 0.7,
|
||||||
|
"roi_pct_by_role": {
|
||||||
|
"rgb": {
|
||||||
|
"x0": 0.015625,
|
||||||
|
"y0": 0.9025,
|
||||||
|
"x1": 0.1125,
|
||||||
|
"y1": 0.995
|
||||||
|
},
|
||||||
|
"re": {
|
||||||
|
"x0": 0.034375,
|
||||||
|
"y0": 0.83,
|
||||||
|
"x1": 0.132812,
|
||||||
|
"y1": 0.925
|
||||||
|
},
|
||||||
|
"nir": {
|
||||||
|
"x0": 0.0125,
|
||||||
|
"y0": 0.86,
|
||||||
|
"x1": 0.107813,
|
||||||
|
"y1": 0.95
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "gray_reference",
|
"name": "gray_reference",
|
||||||
|
|
@ -100,13 +157,33 @@
|
||||||
"nir"
|
"nir"
|
||||||
],
|
],
|
||||||
"roi_pct": {
|
"roi_pct": {
|
||||||
"x0": 0.35,
|
"x0": 0.475,
|
||||||
"y0": 0.92,
|
"y0": 0.895,
|
||||||
"x1": 0.55,
|
"x1": 0.56875,
|
||||||
"y1": 0.99
|
"y1": 0.995
|
||||||
},
|
},
|
||||||
"target_value": 0.5,
|
"target_value": 0.5,
|
||||||
"weight": 1.0
|
"weight": 1.0,
|
||||||
|
"roi_pct_by_role": {
|
||||||
|
"rgb": {
|
||||||
|
"x0": 0.475,
|
||||||
|
"y0": 0.895,
|
||||||
|
"x1": 0.56875,
|
||||||
|
"y1": 0.995
|
||||||
|
},
|
||||||
|
"re": {
|
||||||
|
"x0": 0.5,
|
||||||
|
"y0": 0.83,
|
||||||
|
"x1": 0.590625,
|
||||||
|
"y1": 0.9225
|
||||||
|
},
|
||||||
|
"nir": {
|
||||||
|
"x0": 0.470313,
|
||||||
|
"y0": 0.86,
|
||||||
|
"x1": 0.565625,
|
||||||
|
"y1": 0.9625
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "white_reference",
|
"name": "white_reference",
|
||||||
|
|
@ -117,13 +194,33 @@
|
||||||
"nir"
|
"nir"
|
||||||
],
|
],
|
||||||
"roi_pct": {
|
"roi_pct": {
|
||||||
"x0": 0.75,
|
"x0": 0.870313,
|
||||||
"y0": 0.92,
|
"y0": 0.9025,
|
||||||
"x1": 0.95,
|
"x1": 0.9625,
|
||||||
"y1": 0.99
|
"y1": 0.995
|
||||||
},
|
},
|
||||||
"target_value": 0.82,
|
"target_value": 0.82,
|
||||||
"weight": 0.8
|
"weight": 0.8,
|
||||||
|
"roi_pct_by_role": {
|
||||||
|
"rgb": {
|
||||||
|
"x0": 0.870313,
|
||||||
|
"y0": 0.9025,
|
||||||
|
"x1": 0.9625,
|
||||||
|
"y1": 0.995
|
||||||
|
},
|
||||||
|
"re": {
|
||||||
|
"x0": 0.89375,
|
||||||
|
"y0": 0.8325,
|
||||||
|
"x1": 0.984375,
|
||||||
|
"y1": 0.93
|
||||||
|
},
|
||||||
|
"nir": {
|
||||||
|
"x0": 0.875,
|
||||||
|
"y0": 0.86,
|
||||||
|
"x1": 0.970313,
|
||||||
|
"y1": 0.96
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"exp_apply_threshold_us": 80,
|
"exp_apply_threshold_us": 80,
|
||||||
|
|
@ -132,7 +229,54 @@
|
||||||
"spectral_roles": [
|
"spectral_roles": [
|
||||||
"re",
|
"re",
|
||||||
"nir"
|
"nir"
|
||||||
]
|
],
|
||||||
|
"dark_limit_pct": 35.0,
|
||||||
|
"control_strategy": "ratio",
|
||||||
|
"ratio_alpha": 0.55,
|
||||||
|
"ratio_min": 0.55,
|
||||||
|
"ratio_max": 1.85,
|
||||||
|
"reduce_fast_factor": 0.75,
|
||||||
|
"factor_min": 0.72,
|
||||||
|
"factor_max": 1.28,
|
||||||
|
"gain_return_enabled": true,
|
||||||
|
"gain_reduce_on_saturation": true,
|
||||||
|
"gain_increase_required_cycles": 5,
|
||||||
|
"gain_decrease_required_cycles": 2,
|
||||||
|
"gain_step_up": 0.2,
|
||||||
|
"gain_step_down": 0.5,
|
||||||
|
"gain_hard_reset_on_saturation": false,
|
||||||
|
"exp_high_ratio_for_gain": 0.95,
|
||||||
|
"exp_low_ratio_for_gain_return": 0.75,
|
||||||
|
"role_limits": {
|
||||||
|
"rgb": {
|
||||||
|
"exp_min_us": 100,
|
||||||
|
"exp_max_us": 80000,
|
||||||
|
"gain_min": 1.0,
|
||||||
|
"gain_max": 2.0
|
||||||
|
},
|
||||||
|
"re": {
|
||||||
|
"exp_min_us": 100,
|
||||||
|
"exp_max_us": 80000,
|
||||||
|
"gain_min": 1.0,
|
||||||
|
"gain_max": 2.0
|
||||||
|
},
|
||||||
|
"nir": {
|
||||||
|
"exp_min_us": 100,
|
||||||
|
"exp_max_us": 80000,
|
||||||
|
"gain_min": 1.0,
|
||||||
|
"gain_max": 2.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ready_required_cycles": 3,
|
||||||
|
"patch_control_mode": "gray_primary",
|
||||||
|
"patch_require_order": true,
|
||||||
|
"patch_min_separation": 0.08,
|
||||||
|
"patch_white_sat_limit_pct": 0.5,
|
||||||
|
"patch_white_p95_limit": 0.9,
|
||||||
|
"patch_black_dark_limit_pct": 80.0,
|
||||||
|
"patch_black_max_p50": 0.2,
|
||||||
|
"patch_gray_min_p50": 0.08,
|
||||||
|
"patch_gray_max_p50": 0.85
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"legacy_patch_mode": {
|
"legacy_patch_mode": {
|
||||||
|
|
@ -158,6 +302,212 @@
|
||||||
"verbose": true
|
"verbose": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"schema": "multispec_radiometric_config_profiles_v1",
|
"schema": "multispec_radiometric_config_profiles_v3",
|
||||||
"saved_at": "2026-05-06 09:58:16"
|
"saved_at": "2026-05-07 08:55:15",
|
||||||
|
"active_profile": "three_reference_patches_mode",
|
||||||
|
"patch_normalization": {
|
||||||
|
"enabled": true,
|
||||||
|
"apply_when_metering_mode": "reference_patches",
|
||||||
|
"apply_stage": "after_fusion",
|
||||||
|
"method": "gray_scale_with_white_guard",
|
||||||
|
"space": "multispec_tensor",
|
||||||
|
"targets": {
|
||||||
|
"black": 0.06,
|
||||||
|
"gray": 0.4,
|
||||||
|
"white": 0.78
|
||||||
|
},
|
||||||
|
"white_guard_max": 0.92,
|
||||||
|
"scale_min": 0.35,
|
||||||
|
"scale_max": 2.5,
|
||||||
|
"clip_output": true,
|
||||||
|
"require_valid_gray": true,
|
||||||
|
"use_black_for_offset": false,
|
||||||
|
"save_patch_stats": true
|
||||||
|
},
|
||||||
|
"radiometric_config": {
|
||||||
|
"enabled": true,
|
||||||
|
"interval_s": 0.5,
|
||||||
|
"verbose": true,
|
||||||
|
"metering_mode": "reference_patches",
|
||||||
|
"spectral_control_mode": "shared",
|
||||||
|
"control_metric": "p50",
|
||||||
|
"target_value": 0.5,
|
||||||
|
"deadband": 0.035,
|
||||||
|
"p95_limit": 0.92,
|
||||||
|
"saturation_limit_pct": 0.5,
|
||||||
|
"alpha": 0.18,
|
||||||
|
"exp_step_gain": 0.55,
|
||||||
|
"prefer_exposure": true,
|
||||||
|
"exp_min_us": 100,
|
||||||
|
"exp_max_us": 80000,
|
||||||
|
"gain_min": 1.0,
|
||||||
|
"gain_max": 4.0,
|
||||||
|
"reference_patches": [
|
||||||
|
{
|
||||||
|
"name": "black_reference",
|
||||||
|
"type": "black",
|
||||||
|
"roles": [
|
||||||
|
"rgb",
|
||||||
|
"re",
|
||||||
|
"nir"
|
||||||
|
],
|
||||||
|
"roi_pct": {
|
||||||
|
"x0": 0.015625,
|
||||||
|
"y0": 0.9025,
|
||||||
|
"x1": 0.1125,
|
||||||
|
"y1": 0.995
|
||||||
|
},
|
||||||
|
"target_value": 0.08,
|
||||||
|
"weight": 0.7,
|
||||||
|
"roi_pct_by_role": {
|
||||||
|
"rgb": {
|
||||||
|
"x0": 0.015625,
|
||||||
|
"y0": 0.9025,
|
||||||
|
"x1": 0.1125,
|
||||||
|
"y1": 0.995
|
||||||
|
},
|
||||||
|
"re": {
|
||||||
|
"x0": 0.034375,
|
||||||
|
"y0": 0.83,
|
||||||
|
"x1": 0.132812,
|
||||||
|
"y1": 0.925
|
||||||
|
},
|
||||||
|
"nir": {
|
||||||
|
"x0": 0.0125,
|
||||||
|
"y0": 0.86,
|
||||||
|
"x1": 0.107813,
|
||||||
|
"y1": 0.95
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "gray_reference",
|
||||||
|
"type": "gray",
|
||||||
|
"roles": [
|
||||||
|
"rgb",
|
||||||
|
"re",
|
||||||
|
"nir"
|
||||||
|
],
|
||||||
|
"roi_pct": {
|
||||||
|
"x0": 0.475,
|
||||||
|
"y0": 0.895,
|
||||||
|
"x1": 0.56875,
|
||||||
|
"y1": 0.995
|
||||||
|
},
|
||||||
|
"target_value": 0.5,
|
||||||
|
"weight": 1.0,
|
||||||
|
"roi_pct_by_role": {
|
||||||
|
"rgb": {
|
||||||
|
"x0": 0.475,
|
||||||
|
"y0": 0.895,
|
||||||
|
"x1": 0.56875,
|
||||||
|
"y1": 0.995
|
||||||
|
},
|
||||||
|
"re": {
|
||||||
|
"x0": 0.5,
|
||||||
|
"y0": 0.83,
|
||||||
|
"x1": 0.590625,
|
||||||
|
"y1": 0.9225
|
||||||
|
},
|
||||||
|
"nir": {
|
||||||
|
"x0": 0.470313,
|
||||||
|
"y0": 0.86,
|
||||||
|
"x1": 0.565625,
|
||||||
|
"y1": 0.9625
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "white_reference",
|
||||||
|
"type": "white",
|
||||||
|
"roles": [
|
||||||
|
"rgb",
|
||||||
|
"re",
|
||||||
|
"nir"
|
||||||
|
],
|
||||||
|
"roi_pct": {
|
||||||
|
"x0": 0.870313,
|
||||||
|
"y0": 0.9025,
|
||||||
|
"x1": 0.9625,
|
||||||
|
"y1": 0.995
|
||||||
|
},
|
||||||
|
"target_value": 0.82,
|
||||||
|
"weight": 0.8,
|
||||||
|
"roi_pct_by_role": {
|
||||||
|
"rgb": {
|
||||||
|
"x0": 0.870313,
|
||||||
|
"y0": 0.9025,
|
||||||
|
"x1": 0.9625,
|
||||||
|
"y1": 0.995
|
||||||
|
},
|
||||||
|
"re": {
|
||||||
|
"x0": 0.89375,
|
||||||
|
"y0": 0.8325,
|
||||||
|
"x1": 0.984375,
|
||||||
|
"y1": 0.93
|
||||||
|
},
|
||||||
|
"nir": {
|
||||||
|
"x0": 0.875,
|
||||||
|
"y0": 0.86,
|
||||||
|
"x1": 0.970313,
|
||||||
|
"y1": 0.96
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"exp_apply_threshold_us": 80,
|
||||||
|
"gain_apply_threshold": 0.05,
|
||||||
|
"apply_same_spectral_to_both": true,
|
||||||
|
"spectral_roles": [
|
||||||
|
"re",
|
||||||
|
"nir"
|
||||||
|
],
|
||||||
|
"dark_limit_pct": 35.0,
|
||||||
|
"control_strategy": "ratio",
|
||||||
|
"ratio_alpha": 0.55,
|
||||||
|
"ratio_min": 0.55,
|
||||||
|
"ratio_max": 1.85,
|
||||||
|
"reduce_fast_factor": 0.75,
|
||||||
|
"factor_min": 0.72,
|
||||||
|
"factor_max": 1.28,
|
||||||
|
"gain_return_enabled": true,
|
||||||
|
"gain_reduce_on_saturation": true,
|
||||||
|
"gain_increase_required_cycles": 5,
|
||||||
|
"gain_decrease_required_cycles": 2,
|
||||||
|
"gain_step_up": 0.2,
|
||||||
|
"gain_step_down": 0.5,
|
||||||
|
"gain_hard_reset_on_saturation": false,
|
||||||
|
"exp_high_ratio_for_gain": 0.95,
|
||||||
|
"exp_low_ratio_for_gain_return": 0.75,
|
||||||
|
"role_limits": {
|
||||||
|
"rgb": {
|
||||||
|
"exp_min_us": 100,
|
||||||
|
"exp_max_us": 80000,
|
||||||
|
"gain_min": 1.0,
|
||||||
|
"gain_max": 2.0
|
||||||
|
},
|
||||||
|
"re": {
|
||||||
|
"exp_min_us": 100,
|
||||||
|
"exp_max_us": 80000,
|
||||||
|
"gain_min": 1.0,
|
||||||
|
"gain_max": 2.0
|
||||||
|
},
|
||||||
|
"nir": {
|
||||||
|
"exp_min_us": 100,
|
||||||
|
"exp_max_us": 80000,
|
||||||
|
"gain_min": 1.0,
|
||||||
|
"gain_max": 2.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ready_required_cycles": 3,
|
||||||
|
"patch_control_mode": "gray_primary",
|
||||||
|
"patch_require_order": true,
|
||||||
|
"patch_min_separation": 0.08,
|
||||||
|
"patch_white_sat_limit_pct": 0.5,
|
||||||
|
"patch_white_p95_limit": 0.9,
|
||||||
|
"patch_black_dark_limit_pct": 80.0,
|
||||||
|
"patch_black_max_p50": 0.2,
|
||||||
|
"patch_gray_min_p50": 0.08,
|
||||||
|
"patch_gray_max_p50": 0.85
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"schema": "multispec_camera_params_v2",
|
"schema": "multispec_camera_params_v2",
|
||||||
"saved_at": "2026-05-06 09:26:40",
|
"saved_at": "2026-05-06 13:41:54",
|
||||||
"frame_type": "RAW_BRUTO",
|
"frame_type": "RAW_BRUTO",
|
||||||
"capture_mode_requested": "AUTO",
|
"capture_mode_requested": "AUTO",
|
||||||
"capture_mode_effective": "AUTO",
|
"capture_mode_effective": "AUTO",
|
||||||
|
|
@ -37,9 +37,9 @@
|
||||||
"rgb_calibration": {
|
"rgb_calibration": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"gains": {
|
"gains": {
|
||||||
"R": 1.0,
|
"R": 1.2500000000000002,
|
||||||
"G": 1.0,
|
"G": 1.0,
|
||||||
"B": 1.0
|
"B": 1.5500000000000005
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rois": {
|
"rois": {
|
||||||
|
|
|
||||||
|
|
@ -249,6 +249,12 @@ class OakFcc3Client:
|
||||||
|
|
||||||
return previews, meta
|
return previews, meta
|
||||||
|
|
||||||
|
def get_last_patch_normalization_result(self):
|
||||||
|
try:
|
||||||
|
return self.core.last_patch_normalization_result
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
def build_infer_tensor(self, frame, meta, channels_expected, target_size=None):
|
def build_infer_tensor(self, frame, meta, channels_expected, target_size=None):
|
||||||
return self.core.build_infer_tensor_from_stream(
|
return self.core.build_infer_tensor_from_stream(
|
||||||
frame,
|
frame,
|
||||||
|
|
|
||||||
|
|
@ -61,16 +61,41 @@ class RadiometricController:
|
||||||
self.patch_x1_pct = float(cfg.get("patch_x1_pct", patch_x1_pct))
|
self.patch_x1_pct = float(cfg.get("patch_x1_pct", patch_x1_pct))
|
||||||
|
|
||||||
global_roi = cfg.get("global_roi_pct", {}) or {}
|
global_roi = cfg.get("global_roi_pct", {}) or {}
|
||||||
self.global_roi_pct = {
|
self.global_roi_pct = self._safe_roi_pct(
|
||||||
"x0": float(global_roi.get("x0", 0.08)),
|
global_roi,
|
||||||
"y0": float(global_roi.get("y0", 0.08)),
|
fallback={"x0": 0.08, "y0": 0.08, "x1": 0.92, "y1": 0.92},
|
||||||
"x1": float(global_roi.get("x1", 0.92)),
|
)
|
||||||
"y1": float(global_roi.get("y1", 0.92)),
|
|
||||||
}
|
raw_global_by_role = cfg.get("global_roi_pct_by_role", {}) or {}
|
||||||
|
self.global_roi_pct_by_role = {}
|
||||||
|
|
||||||
|
for role in self.ROLES:
|
||||||
|
roi = raw_global_by_role.get(role)
|
||||||
|
if isinstance(roi, dict) and roi:
|
||||||
|
self.global_roi_pct_by_role[role] = self._safe_roi_pct(
|
||||||
|
roi,
|
||||||
|
fallback=self.global_roi_pct,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.global_roi_pct_by_role[role] = dict(self.global_roi_pct)
|
||||||
|
|
||||||
self.reference_patches = cfg.get("reference_patches", []) or []
|
self.reference_patches = cfg.get("reference_patches", []) or []
|
||||||
self.patch_aggregation = str(cfg.get("patch_aggregation", "weighted_mean")).lower()
|
self.patch_aggregation = str(cfg.get("patch_aggregation", "weighted_mean")).lower()
|
||||||
|
|
||||||
|
self.patch_control_mode = str(cfg.get("patch_control_mode", "gray_primary")).lower()
|
||||||
|
|
||||||
|
self.patch_require_order = bool(cfg.get("patch_require_order", True))
|
||||||
|
self.patch_min_separation = float(cfg.get("patch_min_separation", 0.08))
|
||||||
|
|
||||||
|
self.patch_white_sat_limit_pct = float(cfg.get("patch_white_sat_limit_pct", 0.50))
|
||||||
|
self.patch_white_p95_limit = float(cfg.get("patch_white_p95_limit", 0.90))
|
||||||
|
|
||||||
|
self.patch_black_dark_limit_pct = float(cfg.get("patch_black_dark_limit_pct", 80.0))
|
||||||
|
self.patch_black_max_p50 = float(cfg.get("patch_black_max_p50", 0.20))
|
||||||
|
|
||||||
|
self.patch_gray_min_p50 = float(cfg.get("patch_gray_min_p50", 0.08))
|
||||||
|
self.patch_gray_max_p50 = float(cfg.get("patch_gray_max_p50", 0.85))
|
||||||
|
|
||||||
self.control_metric = str(cfg.get("control_metric", "p50")).lower()
|
self.control_metric = str(cfg.get("control_metric", "p50")).lower()
|
||||||
self.target_value = float(cfg.get("target_value", cfg.get("target_mean", target_mean)))
|
self.target_value = float(cfg.get("target_value", cfg.get("target_mean", target_mean)))
|
||||||
self.target_mean = self.target_value
|
self.target_mean = self.target_value
|
||||||
|
|
@ -85,6 +110,52 @@ class RadiometricController:
|
||||||
self.factor_min = float(cfg.get("factor_min", 0.72))
|
self.factor_min = float(cfg.get("factor_min", 0.72))
|
||||||
self.factor_max = float(cfg.get("factor_max", 1.28))
|
self.factor_max = float(cfg.get("factor_max", 1.28))
|
||||||
|
|
||||||
|
self.saturation_hard_pct = float(cfg.get("saturation_hard_pct", 20.0))
|
||||||
|
self.saturation_extreme_pct = float(cfg.get("saturation_extreme_pct", 60.0))
|
||||||
|
|
||||||
|
self.gain_return_enabled = bool(cfg.get("gain_return_enabled", True))
|
||||||
|
self.gain_return_factor = float(cfg.get("gain_return_factor", 0.60))
|
||||||
|
self.gain_reduce_on_saturation = bool(cfg.get("gain_reduce_on_saturation", True))
|
||||||
|
|
||||||
|
self.exp_high_ratio_for_gain = float(cfg.get("exp_high_ratio_for_gain", 0.85))
|
||||||
|
self.exp_low_ratio_for_gain_return = float(cfg.get("exp_low_ratio_for_gain_return", 0.65))
|
||||||
|
|
||||||
|
self.gain_increase_required_cycles = int(cfg.get("gain_increase_required_cycles", 5))
|
||||||
|
self.gain_decrease_required_cycles = int(cfg.get("gain_decrease_required_cycles", 2))
|
||||||
|
|
||||||
|
self.gain_step_up = float(cfg.get("gain_step_up", 0.25))
|
||||||
|
self.gain_step_down = float(cfg.get("gain_step_down", 0.50))
|
||||||
|
|
||||||
|
self.gain_hard_reset_on_saturation = bool(cfg.get("gain_hard_reset_on_saturation", False))
|
||||||
|
|
||||||
|
self._underexposed_cycles = {
|
||||||
|
"rgb": 0,
|
||||||
|
"re": 0,
|
||||||
|
"nir": 0,
|
||||||
|
"spectral_shared": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
self._overexposed_cycles = {
|
||||||
|
"rgb": 0,
|
||||||
|
"re": 0,
|
||||||
|
"nir": 0,
|
||||||
|
"spectral_shared": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.control_strategy = str(cfg.get("control_strategy", "ratio")).lower()
|
||||||
|
|
||||||
|
self.ratio_alpha = float(cfg.get("ratio_alpha", 0.55))
|
||||||
|
self.ratio_min = float(cfg.get("ratio_min", 0.55))
|
||||||
|
self.ratio_max = float(cfg.get("ratio_max", 1.85))
|
||||||
|
|
||||||
|
self.ready_required_cycles = int(cfg.get("ready_required_cycles", 3))
|
||||||
|
self._ready_cycles = {
|
||||||
|
"rgb": 0,
|
||||||
|
"re": 0,
|
||||||
|
"nir": 0,
|
||||||
|
"spectral_shared": 0,
|
||||||
|
}
|
||||||
|
|
||||||
self.exp_min_us = int(cfg.get("exp_min_us", exp_min_us))
|
self.exp_min_us = int(cfg.get("exp_min_us", exp_min_us))
|
||||||
self.exp_max_us = int(cfg.get("exp_max_us", exp_max_us))
|
self.exp_max_us = int(cfg.get("exp_max_us", exp_max_us))
|
||||||
self.gain_min = float(cfg.get("gain_min", gain_min))
|
self.gain_min = float(cfg.get("gain_min", gain_min))
|
||||||
|
|
@ -127,6 +198,60 @@ class RadiometricController:
|
||||||
cfg = data.get("radiometric_config", {})
|
cfg = data.get("radiometric_config", {})
|
||||||
return cfg if isinstance(cfg, dict) else {}
|
return cfg if isinstance(cfg, dict) else {}
|
||||||
|
|
||||||
|
ROLES = ("rgb", "re", "nir")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _normalize_role(cls, role: str) -> str:
|
||||||
|
role = str(role or "").lower()
|
||||||
|
return role if role in cls.ROLES else "rgb"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _safe_roi_pct(roi_pct, fallback=None) -> dict:
|
||||||
|
if fallback is None:
|
||||||
|
fallback = {"x0": 0.08, "y0": 0.08, "x1": 0.92, "y1": 0.92}
|
||||||
|
|
||||||
|
if not isinstance(roi_pct, dict):
|
||||||
|
roi_pct = fallback
|
||||||
|
|
||||||
|
return {
|
||||||
|
"x0": float(roi_pct.get("x0", fallback.get("x0", 0.08))),
|
||||||
|
"y0": float(roi_pct.get("y0", fallback.get("y0", 0.08))),
|
||||||
|
"x1": float(roi_pct.get("x1", fallback.get("x1", 0.92))),
|
||||||
|
"y1": float(roi_pct.get("y1", fallback.get("y1", 0.92))),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_global_roi_pct_for_role(self, role: str) -> dict:
|
||||||
|
role = self._normalize_role(role)
|
||||||
|
|
||||||
|
by_role = getattr(self, "global_roi_pct_by_role", {}) or {}
|
||||||
|
if isinstance(by_role, dict):
|
||||||
|
roi = by_role.get(role)
|
||||||
|
if isinstance(roi, dict) and roi:
|
||||||
|
return self._safe_roi_pct(roi, fallback=self.global_roi_pct)
|
||||||
|
|
||||||
|
return self._safe_roi_pct(self.global_roi_pct)
|
||||||
|
|
||||||
|
def _get_patch_roi_pct_for_role(self, patch: dict, role: str):
|
||||||
|
role = self._normalize_role(role)
|
||||||
|
|
||||||
|
by_role = patch.get("roi_pct_by_role", {})
|
||||||
|
if isinstance(by_role, dict):
|
||||||
|
roi = by_role.get(role)
|
||||||
|
if isinstance(roi, dict) and roi:
|
||||||
|
return self._safe_roi_pct(roi), "roi_pct_by_role"
|
||||||
|
|
||||||
|
legacy = patch.get("roi_pct")
|
||||||
|
if isinstance(legacy, dict) and legacy:
|
||||||
|
return self._safe_roi_pct(legacy), "roi_pct"
|
||||||
|
|
||||||
|
return None, "missing"
|
||||||
|
|
||||||
|
def get_patch_target_for_role(self, patch, role, fallback):
|
||||||
|
by_role = patch.get("target_value_by_role", {})
|
||||||
|
if isinstance(by_role, dict) and role in by_role:
|
||||||
|
return float(by_role[role])
|
||||||
|
return float(patch.get("target_value", fallback))
|
||||||
|
|
||||||
def sync_from_camera_controls(self, camera_controls: dict | None):
|
def sync_from_camera_controls(self, camera_controls: dict | None):
|
||||||
if not isinstance(camera_controls, dict):
|
if not isinstance(camera_controls, dict):
|
||||||
return
|
return
|
||||||
|
|
@ -193,7 +318,7 @@ class RadiometricController:
|
||||||
metrics = self.measure_image(img, role=role)
|
metrics = self.measure_image(img, role=role)
|
||||||
decision = self.compute_control(role, metrics)
|
decision = self.compute_control(role, metrics)
|
||||||
apply_resp = self.apply_control(role, decision)
|
apply_resp = self.apply_control(role, decision)
|
||||||
return {
|
result = {
|
||||||
"mode": "single_role",
|
"mode": "single_role",
|
||||||
"cam_id": cam_id,
|
"cam_id": cam_id,
|
||||||
"role": role,
|
"role": role,
|
||||||
|
|
@ -202,6 +327,9 @@ class RadiometricController:
|
||||||
"decision": decision,
|
"decision": decision,
|
||||||
"apply": apply_resp,
|
"apply": apply_resp,
|
||||||
}
|
}
|
||||||
|
self._print_metrics_debug(role, result)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
def _update_spectral_shared(self, decoded: dict):
|
def _update_spectral_shared(self, decoded: dict):
|
||||||
role_items = {}
|
role_items = {}
|
||||||
|
|
@ -230,7 +358,7 @@ class RadiometricController:
|
||||||
else:
|
else:
|
||||||
apply_resp[state_role] = self.apply_control(state_role, decision)
|
apply_resp[state_role] = self.apply_control(state_role, decision)
|
||||||
|
|
||||||
return {
|
result = {
|
||||||
"mode": "shared_spectral",
|
"mode": "shared_spectral",
|
||||||
"roles": role_items,
|
"roles": role_items,
|
||||||
"metering_mode": self.metering_mode,
|
"metering_mode": self.metering_mode,
|
||||||
|
|
@ -238,6 +366,70 @@ class RadiometricController:
|
||||||
"decision": decision,
|
"decision": decision,
|
||||||
"apply": apply_resp,
|
"apply": apply_resp,
|
||||||
}
|
}
|
||||||
|
self._print_metrics_debug("spectral_shared", result)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _update_ready_state(
|
||||||
|
self,
|
||||||
|
log_role: str,
|
||||||
|
action: str,
|
||||||
|
error: float,
|
||||||
|
p95: float,
|
||||||
|
sat_pct: float,
|
||||||
|
) -> tuple[bool, int]:
|
||||||
|
key = str(log_role).lower()
|
||||||
|
|
||||||
|
is_ready_now = (
|
||||||
|
action == "hold"
|
||||||
|
and abs(float(error)) <= self.deadband
|
||||||
|
and float(p95) <= self.p95_limit
|
||||||
|
and float(sat_pct) <= self.saturation_limit_pct
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_ready_now:
|
||||||
|
self._ready_cycles[key] = self._ready_cycles.get(key, 0) + 1
|
||||||
|
else:
|
||||||
|
self._ready_cycles[key] = 0
|
||||||
|
|
||||||
|
cycles = self._ready_cycles.get(key, 0)
|
||||||
|
return cycles >= self.ready_required_cycles, cycles
|
||||||
|
|
||||||
|
def _update_exposure_pressure_state(
|
||||||
|
self,
|
||||||
|
log_role: str,
|
||||||
|
error: float,
|
||||||
|
p95: float,
|
||||||
|
sat_pct: float,
|
||||||
|
) -> tuple[int, int]:
|
||||||
|
key = str(log_role).lower()
|
||||||
|
|
||||||
|
under = (
|
||||||
|
error > self.deadband
|
||||||
|
and p95 < self.p95_limit
|
||||||
|
and sat_pct <= self.saturation_limit_pct
|
||||||
|
)
|
||||||
|
|
||||||
|
over = (
|
||||||
|
error < -self.deadband
|
||||||
|
or p95 > self.p95_limit
|
||||||
|
or sat_pct > self.saturation_limit_pct
|
||||||
|
)
|
||||||
|
|
||||||
|
if under:
|
||||||
|
self._underexposed_cycles[key] = self._underexposed_cycles.get(key, 0) + 1
|
||||||
|
else:
|
||||||
|
self._underexposed_cycles[key] = 0
|
||||||
|
|
||||||
|
if over:
|
||||||
|
self._overexposed_cycles[key] = self._overexposed_cycles.get(key, 0) + 1
|
||||||
|
else:
|
||||||
|
self._overexposed_cycles[key] = 0
|
||||||
|
|
||||||
|
return (
|
||||||
|
self._underexposed_cycles.get(key, 0),
|
||||||
|
self._overexposed_cycles.get(key, 0),
|
||||||
|
)
|
||||||
|
|
||||||
def _resolve_cam_id(self, decoded, role):
|
def _resolve_cam_id(self, decoded, role):
|
||||||
role = str(role).lower()
|
role = str(role).lower()
|
||||||
|
|
@ -247,12 +439,16 @@ class RadiometricController:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def measure_image(self, img01: np.ndarray, role: str) -> dict:
|
def measure_image(self, img01: np.ndarray, role: str) -> dict:
|
||||||
|
role = self._normalize_role(role)
|
||||||
gray = self.to_luma_or_gray(img01)
|
gray = self.to_luma_or_gray(img01)
|
||||||
|
|
||||||
if self.metering_mode == "reference_patches":
|
if self.metering_mode == "reference_patches":
|
||||||
return self.measure_reference_patches(gray, role=role)
|
return self.measure_reference_patches(gray, role=role)
|
||||||
|
|
||||||
if self.metering_mode == "legacy_patch":
|
if self.metering_mode == "legacy_patch":
|
||||||
return self.measure_legacy_patch(gray)
|
return self.measure_legacy_patch(gray)
|
||||||
return self.measure_global(gray)
|
|
||||||
|
return self.measure_global(gray, role=role)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def to_luma_or_gray(img01: np.ndarray) -> np.ndarray:
|
def to_luma_or_gray(img01: np.ndarray) -> np.ndarray:
|
||||||
|
|
@ -264,19 +460,29 @@ class RadiometricController:
|
||||||
).astype(np.float32)
|
).astype(np.float32)
|
||||||
return img01.astype(np.float32)
|
return img01.astype(np.float32)
|
||||||
|
|
||||||
def measure_global(self, img_gray: np.ndarray) -> dict:
|
def measure_global(self, img_gray: np.ndarray, role: str = "rgb") -> dict:
|
||||||
|
role = self._normalize_role(role)
|
||||||
|
|
||||||
h, w = img_gray.shape[:2]
|
h, w = img_gray.shape[:2]
|
||||||
|
roi_pct = self._get_global_roi_pct_for_role(role)
|
||||||
|
|
||||||
roi = self._roi_pct_to_pixels(
|
roi = self._roi_pct_to_pixels(
|
||||||
h, w,
|
h, w,
|
||||||
self.global_roi_pct["x0"],
|
roi_pct["x0"],
|
||||||
self.global_roi_pct["y0"],
|
roi_pct["y0"],
|
||||||
self.global_roi_pct["x1"],
|
roi_pct["x1"],
|
||||||
self.global_roi_pct["y1"],
|
roi_pct["y1"],
|
||||||
)
|
)
|
||||||
|
|
||||||
arr = self._crop_array(img_gray, roi)
|
arr = self._crop_array(img_gray, roi)
|
||||||
stats = self.compute_stats(arr)
|
stats = self.compute_stats(arr)
|
||||||
|
|
||||||
stats["roi"] = list(roi)
|
stats["roi"] = list(roi)
|
||||||
|
stats["roi_pct"] = dict(roi_pct)
|
||||||
|
stats["roi_source"] = "global_roi_pct_by_role"
|
||||||
|
stats["role"] = role
|
||||||
stats["source"] = "global"
|
stats["source"] = "global"
|
||||||
|
|
||||||
return stats
|
return stats
|
||||||
|
|
||||||
def measure_legacy_patch(self, img_gray: np.ndarray) -> dict:
|
def measure_legacy_patch(self, img_gray: np.ndarray) -> dict:
|
||||||
|
|
@ -295,46 +501,69 @@ class RadiometricController:
|
||||||
return stats
|
return stats
|
||||||
|
|
||||||
def measure_reference_patches(self, img_gray: np.ndarray, role: str) -> dict:
|
def measure_reference_patches(self, img_gray: np.ndarray, role: str) -> dict:
|
||||||
|
role = self._normalize_role(role)
|
||||||
|
|
||||||
h, w = img_gray.shape[:2]
|
h, w = img_gray.shape[:2]
|
||||||
patch_results = []
|
patch_results = []
|
||||||
|
|
||||||
for patch in self.reference_patches:
|
for patch in self.reference_patches:
|
||||||
if not isinstance(patch, dict):
|
if not isinstance(patch, dict):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
roles = patch.get("roles", ["rgb", "re", "nir", "all"])
|
roles = patch.get("roles", ["rgb", "re", "nir", "all"])
|
||||||
|
roles = [str(r).lower() for r in roles]
|
||||||
|
|
||||||
if role not in roles and "all" not in roles:
|
if role not in roles and "all" not in roles:
|
||||||
continue
|
continue
|
||||||
roi_pct = patch.get("roi_pct")
|
|
||||||
|
roi_pct, roi_source = self._get_patch_roi_pct_for_role(patch, role)
|
||||||
|
|
||||||
if not isinstance(roi_pct, dict):
|
if not isinstance(roi_pct, dict):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
x0 = float(roi_pct.get("x0", 0.0))
|
x0 = float(roi_pct.get("x0", 0.0))
|
||||||
y0 = float(roi_pct.get("y0", 0.0))
|
y0 = float(roi_pct.get("y0", 0.0))
|
||||||
x1 = float(roi_pct.get("x1", 1.0))
|
x1 = float(roi_pct.get("x1", 1.0))
|
||||||
y1 = float(roi_pct.get("y1", 1.0))
|
y1 = float(roi_pct.get("y1", 1.0))
|
||||||
|
|
||||||
roi = self._roi_pct_to_pixels(h, w, x0, y0, x1, y1)
|
roi = self._roi_pct_to_pixels(h, w, x0, y0, x1, y1)
|
||||||
arr = self._crop_array(img_gray, roi)
|
arr = self._crop_array(img_gray, roi)
|
||||||
stats = self.compute_stats(arr)
|
stats = self.compute_stats(arr)
|
||||||
target = patch.get("target_value", patch.get("target_mean", None))
|
|
||||||
|
target = self.get_patch_target_for_role(
|
||||||
|
patch=patch,
|
||||||
|
role=role,
|
||||||
|
fallback=self.target_value,
|
||||||
|
)
|
||||||
if target is not None:
|
if target is not None:
|
||||||
target = float(target)
|
target = float(target)
|
||||||
|
|
||||||
patch_results.append({
|
patch_results.append({
|
||||||
"name": patch.get("name", f"patch_{len(patch_results)+1}"),
|
"name": patch.get("name", f"patch_{len(patch_results) + 1}"),
|
||||||
"type": patch.get("type", "reference"),
|
"type": patch.get("type", "reference"),
|
||||||
|
"role": role,
|
||||||
"roi": list(roi),
|
"roi": list(roi),
|
||||||
"roi_pct": {"x0": x0, "y0": y0, "x1": x1, "y1": y1},
|
"roi_pct": {"x0": x0, "y0": y0, "x1": x1, "y1": y1},
|
||||||
|
"roi_source": roi_source,
|
||||||
"weight": float(patch.get("weight", 1.0)),
|
"weight": float(patch.get("weight", 1.0)),
|
||||||
"target_value": target,
|
"target_value": target,
|
||||||
"stats": stats,
|
"stats": stats,
|
||||||
})
|
})
|
||||||
|
|
||||||
if not patch_results:
|
if not patch_results:
|
||||||
stats = self.measure_global(img_gray)
|
stats = self.measure_global(img_gray, role=role)
|
||||||
stats["source"] = "reference_patches_fallback_global"
|
stats["source"] = "reference_patches_fallback_global"
|
||||||
stats["patches"] = []
|
stats["patches"] = []
|
||||||
return stats
|
return stats
|
||||||
return self.aggregate_patch_metrics(patch_results)
|
|
||||||
|
metrics = self.aggregate_patch_metrics(patch_results)
|
||||||
|
metrics["role"] = role
|
||||||
|
|
||||||
|
return metrics
|
||||||
|
|
||||||
def aggregate_patch_metrics(self, patch_results: list[dict]) -> dict:
|
def aggregate_patch_metrics(self, patch_results: list[dict]) -> dict:
|
||||||
valid = [p for p in patch_results if p["stats"].get("valid")]
|
valid = [p for p in patch_results if p["stats"].get("valid")]
|
||||||
|
|
||||||
if not valid:
|
if not valid:
|
||||||
return {
|
return {
|
||||||
"valid": False,
|
"valid": False,
|
||||||
|
|
@ -347,45 +576,172 @@ class RadiometricController:
|
||||||
"dark_pct": 0.0,
|
"dark_pct": 0.0,
|
||||||
"control_value": 0.0,
|
"control_value": 0.0,
|
||||||
"target_value": self.target_value,
|
"target_value": self.target_value,
|
||||||
|
"weighted_error": 0.0,
|
||||||
|
"patch_quality": {
|
||||||
|
"valid": False,
|
||||||
|
"warnings": ["no_valid_patches"],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
weights = np.array([max(0.0, p.get("weight", 1.0)) for p in valid], dtype=np.float32)
|
black = self._find_patch_result(valid, "black")
|
||||||
if float(weights.sum()) <= 1e-9:
|
gray = self._find_patch_result(valid, "gray")
|
||||||
weights = np.ones(len(valid), dtype=np.float32)
|
white = self._find_patch_result(valid, "white")
|
||||||
weights = weights / weights.sum()
|
|
||||||
|
|
||||||
|
warnings = []
|
||||||
|
|
||||||
|
# Stats gerais de proteção.
|
||||||
|
p95s = np.array([p["stats"]["p95"] for p in valid], dtype=np.float32)
|
||||||
|
sats = np.array([p["stats"]["sat_pct"] for p in valid], dtype=np.float32)
|
||||||
|
darks = np.array([p["stats"]["dark_pct"] for p in valid], dtype=np.float32)
|
||||||
means = np.array([p["stats"]["mean"] for p in valid], dtype=np.float32)
|
means = np.array([p["stats"]["mean"] for p in valid], dtype=np.float32)
|
||||||
p50s = np.array([p["stats"]["p50"] for p in valid], dtype=np.float32)
|
p50s = np.array([p["stats"]["p50"] for p in valid], dtype=np.float32)
|
||||||
p95s = np.array([p["stats"]["p95"] for p in valid], dtype=np.float32)
|
|
||||||
sat = np.array([p["stats"]["sat_pct"] for p in valid], dtype=np.float32)
|
|
||||||
dark = np.array([p["stats"]["dark_pct"] for p in valid], dtype=np.float32)
|
|
||||||
|
|
||||||
patch_errors = []
|
p95_max = float(np.max(p95s))
|
||||||
control_values = []
|
sat_max = float(np.max(sats))
|
||||||
for p in valid:
|
dark_mean = float(np.mean(darks))
|
||||||
target = p.get("target_value")
|
mean_mean = float(np.mean(means))
|
||||||
if target is None:
|
p50_mean = float(np.mean(p50s))
|
||||||
target = self.target_value
|
|
||||||
value = p["stats"].get(self.control_metric, p["stats"].get("p50", p["stats"].get("mean", 0.0)))
|
|
||||||
control_values.append(float(value))
|
|
||||||
patch_errors.append(float(target) - float(value))
|
|
||||||
|
|
||||||
weighted_error = float(np.sum(np.array(patch_errors, dtype=np.float32) * weights))
|
# Valores por patch, quando existem.
|
||||||
control_value = float(np.sum(np.array(control_values, dtype=np.float32) * weights))
|
black_p50 = float(black["stats"]["p50"]) if black else None
|
||||||
|
gray_p50 = float(gray["stats"]["p50"]) if gray else None
|
||||||
|
white_p50 = float(white["stats"]["p50"]) if white else None
|
||||||
|
|
||||||
|
black_target = float(black.get("target_value", 0.06)) if black else 0.06
|
||||||
|
gray_target = float(gray.get("target_value", self.target_value)) if gray else self.target_value
|
||||||
|
white_target = float(white.get("target_value", 0.80)) if white else 0.80
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Validações de coerência dos cartões
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
if gray is None:
|
||||||
|
warnings.append("missing_gray_patch")
|
||||||
|
|
||||||
|
if self.patch_require_order and black and gray and white:
|
||||||
|
if not (black_p50 < gray_p50 < white_p50):
|
||||||
|
warnings.append(
|
||||||
|
f"patch_order_invalid: black={black_p50:.3f}, gray={gray_p50:.3f}, white={white_p50:.3f}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (gray_p50 - black_p50) < self.patch_min_separation:
|
||||||
|
warnings.append(
|
||||||
|
f"black_gray_separation_low: diff={gray_p50 - black_p50:.3f}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (white_p50 - gray_p50) < self.patch_min_separation:
|
||||||
|
warnings.append(
|
||||||
|
f"gray_white_separation_low: diff={white_p50 - gray_p50:.3f}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if white:
|
||||||
|
white_sat = float(white["stats"]["sat_pct"])
|
||||||
|
white_p95 = float(white["stats"]["p95"])
|
||||||
|
if white_sat > self.patch_white_sat_limit_pct:
|
||||||
|
warnings.append(f"white_patch_saturated: sat={white_sat:.2f}%")
|
||||||
|
if white_p95 > self.patch_white_p95_limit:
|
||||||
|
warnings.append(f"white_patch_p95_high: p95={white_p95:.3f}")
|
||||||
|
|
||||||
|
if black:
|
||||||
|
black_dark = float(black["stats"]["dark_pct"])
|
||||||
|
if black_dark > self.patch_black_dark_limit_pct:
|
||||||
|
warnings.append(f"black_patch_too_dark: dark={black_dark:.1f}%")
|
||||||
|
if black_p50 > self.patch_black_max_p50:
|
||||||
|
warnings.append(f"black_patch_too_bright: p50={black_p50:.3f}")
|
||||||
|
|
||||||
|
if gray:
|
||||||
|
if gray_p50 < self.patch_gray_min_p50:
|
||||||
|
warnings.append(f"gray_patch_too_dark: p50={gray_p50:.3f}")
|
||||||
|
if gray_p50 > self.patch_gray_max_p50:
|
||||||
|
warnings.append(f"gray_patch_too_bright: p50={gray_p50:.3f}")
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Modo recomendado: cinza como controle principal
|
||||||
|
# ============================================================
|
||||||
|
if self.patch_control_mode == "gray_primary" and gray is not None:
|
||||||
|
control_value = gray_p50
|
||||||
|
target_value = gray_target
|
||||||
|
weighted_error = target_value - control_value
|
||||||
|
|
||||||
|
control_source = "gray_primary"
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Fallback: média ponderada original, mas preservando guardas.
|
||||||
|
weights = np.array([max(0.0, p.get("weight", 1.0)) for p in valid], dtype=np.float32)
|
||||||
|
|
||||||
|
if float(weights.sum()) <= 1e-9:
|
||||||
|
weights = np.ones(len(valid), dtype=np.float32)
|
||||||
|
|
||||||
|
weights = weights / weights.sum()
|
||||||
|
|
||||||
|
patch_errors = []
|
||||||
|
control_values = []
|
||||||
|
|
||||||
|
for p in valid:
|
||||||
|
target = p.get("target_value")
|
||||||
|
if target is None:
|
||||||
|
target = self.target_value
|
||||||
|
|
||||||
|
value = p["stats"].get(
|
||||||
|
self.control_metric,
|
||||||
|
p["stats"].get("p50", p["stats"].get("mean", 0.0))
|
||||||
|
)
|
||||||
|
|
||||||
|
control_values.append(float(value))
|
||||||
|
patch_errors.append(float(target) - float(value))
|
||||||
|
|
||||||
|
weighted_error = float(np.sum(np.array(patch_errors, dtype=np.float32) * weights))
|
||||||
|
control_value = float(np.sum(np.array(control_values, dtype=np.float32) * weights))
|
||||||
|
target_value = self.target_value
|
||||||
|
control_source = "weighted_patches"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Guardas de saturação e faixa útil
|
||||||
|
# ============================================================
|
||||||
|
# Se o branco saturou, queremos que o compute_control reduza exposição,
|
||||||
|
# mesmo que o cinza esteja aparentemente bom.
|
||||||
|
if white:
|
||||||
|
white_sat = float(white["stats"]["sat_pct"])
|
||||||
|
white_p95 = float(white["stats"]["p95"])
|
||||||
|
|
||||||
|
sat_max = max(sat_max, white_sat)
|
||||||
|
p95_max = max(p95_max, white_p95)
|
||||||
|
|
||||||
|
# Se o cinza está ausente, a métrica ainda pode funcionar por fallback,
|
||||||
|
# mas marcamos warning para debug.
|
||||||
|
quality_valid = gray is not None and len(warnings) == 0
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"valid": True,
|
"valid": True,
|
||||||
"source": "reference_patches",
|
"source": "reference_patches",
|
||||||
"patches": patch_results,
|
"patches": patch_results,
|
||||||
"mean": float(np.sum(means * weights)),
|
|
||||||
"p50": float(np.sum(p50s * weights)),
|
# Métricas agregadas informativas.
|
||||||
"p95": float(np.max(p95s)),
|
"mean": mean_mean,
|
||||||
"sat_pct": float(np.max(sat)),
|
"p50": p50_mean,
|
||||||
"dark_pct": float(np.sum(dark * weights)),
|
"p95": p95_max,
|
||||||
|
"sat_pct": sat_max,
|
||||||
|
"dark_pct": dark_mean,
|
||||||
|
|
||||||
|
# Métricas usadas pelo controle.
|
||||||
"control_metric": self.control_metric,
|
"control_metric": self.control_metric,
|
||||||
"control_value": control_value,
|
"control_value": float(control_value),
|
||||||
"target_value": self.target_value,
|
"target_value": float(target_value),
|
||||||
"weighted_error": weighted_error,
|
"weighted_error": float(weighted_error),
|
||||||
|
|
||||||
|
# Debug/qualidade.
|
||||||
|
"patch_control_mode": self.patch_control_mode,
|
||||||
|
"control_source": control_source,
|
||||||
|
"patch_quality": {
|
||||||
|
"valid": bool(quality_valid),
|
||||||
|
"warnings": warnings,
|
||||||
|
"black_p50": black_p50,
|
||||||
|
"gray_p50": gray_p50,
|
||||||
|
"white_p50": white_p50,
|
||||||
|
"black_target": black_target,
|
||||||
|
"gray_target": gray_target,
|
||||||
|
"white_target": white_target,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
def aggregate_spectral_metrics(self, role_items: dict) -> dict:
|
def aggregate_spectral_metrics(self, role_items: dict) -> dict:
|
||||||
|
|
@ -411,11 +767,23 @@ class RadiometricController:
|
||||||
p50 = float(np.mean([float(m.get("p50", m.get("mean", 0.0))) for m in metrics_list]))
|
p50 = float(np.mean([float(m.get("p50", m.get("mean", 0.0))) for m in metrics_list]))
|
||||||
dark_pct = float(np.mean([float(m.get("dark_pct", 0.0)) for m in metrics_list]))
|
dark_pct = float(np.mean([float(m.get("dark_pct", 0.0)) for m in metrics_list]))
|
||||||
control_values = [
|
control_values = [
|
||||||
float(m.get(self.control_metric, m.get("control_value", m.get("p50", m.get("mean", 0.0)))))
|
float(m.get("control_value", m.get(self.control_metric, m.get("p50", m.get("mean", 0.0)))))
|
||||||
for m in metrics_list
|
for m in metrics_list
|
||||||
]
|
]
|
||||||
|
|
||||||
|
target_values = [
|
||||||
|
float(m.get("target_value", self.target_value))
|
||||||
|
for m in metrics_list
|
||||||
|
]
|
||||||
|
|
||||||
|
errors = [
|
||||||
|
float(m.get("weighted_error", target - value))
|
||||||
|
for m, target, value in zip(metrics_list, target_values, control_values)
|
||||||
|
]
|
||||||
|
|
||||||
control_value = float(np.mean(control_values))
|
control_value = float(np.mean(control_values))
|
||||||
weighted_error = self.target_value - control_value
|
target_value = float(np.mean(target_values))
|
||||||
|
weighted_error = float(np.mean(errors))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"valid": True,
|
"valid": True,
|
||||||
|
|
@ -428,8 +796,20 @@ class RadiometricController:
|
||||||
"dark_pct": dark_pct,
|
"dark_pct": dark_pct,
|
||||||
"control_metric": self.control_metric,
|
"control_metric": self.control_metric,
|
||||||
"control_value": control_value,
|
"control_value": control_value,
|
||||||
"target_value": self.target_value,
|
"target_value": target_value,
|
||||||
"weighted_error": weighted_error,
|
"weighted_error": weighted_error,
|
||||||
|
"control_values_by_role": {
|
||||||
|
role: float(item["metrics"].get("control_value", item["metrics"].get("p50", 0.0)))
|
||||||
|
for role, item in valid_items.items()
|
||||||
|
},
|
||||||
|
"targets_by_role": {
|
||||||
|
role: float(item["metrics"].get("target_value", self.target_value))
|
||||||
|
for role, item in valid_items.items()
|
||||||
|
},
|
||||||
|
"patch_quality_by_role": {
|
||||||
|
role: item["metrics"].get("patch_quality", {})
|
||||||
|
for role, item in valid_items.items()
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
def compute_stats(self, arr: np.ndarray) -> dict:
|
def compute_stats(self, arr: np.ndarray) -> dict:
|
||||||
|
|
@ -475,15 +855,31 @@ class RadiometricController:
|
||||||
y1 = max(y0 + 1, min(h, y1))
|
y1 = max(y0 + 1, min(h, y1))
|
||||||
return x0, y0, x1, y1
|
return x0, y0, x1, y1
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _find_patch_result(patch_results: list[dict], patch_type: str):
|
||||||
|
patch_type = str(patch_type).lower()
|
||||||
|
for p in patch_results:
|
||||||
|
if str(p.get("type", "")).lower() == patch_type:
|
||||||
|
return p
|
||||||
|
return None
|
||||||
|
|
||||||
def compute_control(self, role: str, metrics: dict, virtual_role: str | None = None) -> dict:
|
def compute_control(self, role: str, metrics: dict, virtual_role: str | None = None) -> dict:
|
||||||
state_role = str(role).lower()
|
state_role = str(role).lower()
|
||||||
log_role = virtual_role or state_role
|
log_role = virtual_role or state_role
|
||||||
|
|
||||||
st = self.state.setdefault(state_role, {"exp": 15000, "gain": 1.0})
|
st = self.state.setdefault(state_role, {"exp": 15000, "gain": 1.0})
|
||||||
old_exp = int(st["exp"])
|
old_exp = int(st["exp"])
|
||||||
old_gain = float(st["gain"])
|
old_gain = float(st["gain"])
|
||||||
limits = self._limits_for_role(state_role)
|
limits = self._limits_for_role(state_role)
|
||||||
|
|
||||||
if not metrics.get("valid"):
|
if not metrics.get("valid"):
|
||||||
|
ready, ready_cycles = self._update_ready_state(
|
||||||
|
log_role=log_role,
|
||||||
|
action="hold",
|
||||||
|
error=999.0,
|
||||||
|
p95=1.0,
|
||||||
|
sat_pct=100.0,
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
"role": log_role,
|
"role": log_role,
|
||||||
"state_role": state_role,
|
"state_role": state_role,
|
||||||
|
|
@ -493,46 +889,189 @@ class RadiometricController:
|
||||||
"new_exp": old_exp,
|
"new_exp": old_exp,
|
||||||
"old_gain": old_gain,
|
"old_gain": old_gain,
|
||||||
"new_gain": old_gain,
|
"new_gain": old_gain,
|
||||||
|
"ready": ready,
|
||||||
|
"ready_cycles": ready_cycles,
|
||||||
|
"ready_required_cycles": self.ready_required_cycles,
|
||||||
}
|
}
|
||||||
|
|
||||||
control_value = float(metrics.get("control_value", metrics.get(self.control_metric, metrics.get("p50", metrics.get("mean", 0.0)))))
|
control_value = float(metrics.get(
|
||||||
|
"control_value",
|
||||||
|
metrics.get(self.control_metric, metrics.get("p50", metrics.get("mean", 0.0)))
|
||||||
|
))
|
||||||
target = float(metrics.get("target_value", self.target_value))
|
target = float(metrics.get("target_value", self.target_value))
|
||||||
error = float(metrics.get("weighted_error", target - control_value))
|
error = float(metrics.get("weighted_error", target - control_value))
|
||||||
p95 = float(metrics.get("p95", 0.0))
|
p95 = float(metrics.get("p95", 0.0))
|
||||||
sat_pct = float(metrics.get("sat_pct", 0.0))
|
sat_pct = float(metrics.get("sat_pct", 0.0))
|
||||||
|
|
||||||
|
under_cycles, over_cycles = self._update_exposure_pressure_state(
|
||||||
|
log_role=log_role,
|
||||||
|
error=error,
|
||||||
|
p95=p95,
|
||||||
|
sat_pct=sat_pct,
|
||||||
|
)
|
||||||
|
|
||||||
|
exp_min = int(limits["exp_min_us"])
|
||||||
|
exp_max = int(limits["exp_max_us"])
|
||||||
|
gain_min = float(limits["gain_min"])
|
||||||
|
gain_max = float(limits["gain_max"])
|
||||||
|
|
||||||
new_exp = old_exp
|
new_exp = old_exp
|
||||||
new_gain = old_gain
|
new_gain = old_gain
|
||||||
action = "hold"
|
action = "hold"
|
||||||
reason = "dentro da faixa morta"
|
reason = "dentro da faixa morta"
|
||||||
|
|
||||||
|
ratio = None
|
||||||
|
factor = 1.0
|
||||||
|
gain_policy = "hold"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 1) Proteção forte contra saturação / p95 alto
|
||||||
|
# ============================================================
|
||||||
if sat_pct > self.saturation_limit_pct or p95 > self.p95_limit:
|
if sat_pct > self.saturation_limit_pct or p95 > self.p95_limit:
|
||||||
desired_exp = max(limits["exp_min_us"], int(old_exp * self.reduce_fast_factor))
|
if sat_pct >= self.saturation_extreme_pct:
|
||||||
new_exp = self._smooth_int(old_exp, desired_exp)
|
exp_factor = 0.45
|
||||||
|
elif sat_pct >= self.saturation_hard_pct:
|
||||||
|
exp_factor = 0.32
|
||||||
|
elif sat_pct > self.saturation_limit_pct:
|
||||||
|
exp_factor = 0.55
|
||||||
|
else:
|
||||||
|
exp_factor = self.reduce_fast_factor
|
||||||
|
|
||||||
|
new_exp = int(self._clamp(old_exp * exp_factor, exp_min, exp_max))
|
||||||
|
|
||||||
|
if self.gain_reduce_on_saturation and old_gain > gain_min:
|
||||||
|
if self.gain_hard_reset_on_saturation and sat_pct >= self.saturation_extreme_pct:
|
||||||
|
new_gain = gain_min
|
||||||
|
gain_policy = "hard_reset_gain_on_extreme_saturation"
|
||||||
|
else:
|
||||||
|
desired_gain = old_gain - self.gain_step_down
|
||||||
|
new_gain = float(self._clamp(desired_gain, gain_min, gain_max))
|
||||||
|
gain_policy = "decrease_gain_step_on_saturation"
|
||||||
|
else:
|
||||||
|
new_gain = old_gain
|
||||||
|
gain_policy = "hold_gain"
|
||||||
|
|
||||||
action = "decrease_exposure"
|
action = "decrease_exposure"
|
||||||
reason = f"saturação/p95 alto: sat={sat_pct:.2f}% p95={p95:.3f}"
|
reason = (
|
||||||
|
f"saturação/p95 alto: sat={sat_pct:.2f}% p95={p95:.3f} "
|
||||||
|
f"exp_factor={exp_factor:.3f} gain_policy={gain_policy}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 2) Fora da faixa morta: controle por ratio/linear
|
||||||
|
# ============================================================
|
||||||
elif abs(error) > self.deadband:
|
elif abs(error) > self.deadband:
|
||||||
factor = 1.0 + self.exp_step_gain * error
|
if self.control_strategy == "ratio":
|
||||||
factor = max(self.factor_min, min(self.factor_max, factor))
|
safe_value = max(control_value, 1e-6)
|
||||||
|
ratio = target / safe_value
|
||||||
|
ratio = self._clamp(ratio, self.ratio_min, self.ratio_max)
|
||||||
|
factor = 1.0 + self.ratio_alpha * (ratio - 1.0)
|
||||||
|
else:
|
||||||
|
factor = 1.0 + self.exp_step_gain * error
|
||||||
|
factor = max(self.factor_min, min(self.factor_max, factor))
|
||||||
|
|
||||||
if self.prefer_exposure:
|
if self.prefer_exposure:
|
||||||
desired_exp = int(old_exp * factor)
|
# ----------------------------------------------------
|
||||||
desired_exp = self._clamp(desired_exp, limits["exp_min_us"], limits["exp_max_us"])
|
# 2A) Cena escura: subir exposição primeiro.
|
||||||
new_exp = self._smooth_int(old_exp, desired_exp)
|
# Só subir ganho se exposição já estiver perto do máximo.
|
||||||
if desired_exp in (limits["exp_min_us"], limits["exp_max_us"]):
|
# ----------------------------------------------------
|
||||||
desired_gain = old_gain * factor
|
if error > 0:
|
||||||
desired_gain = self._clamp(desired_gain, limits["gain_min"], limits["gain_max"])
|
desired_exp = int(self._clamp(old_exp * factor, exp_min, exp_max))
|
||||||
new_gain = self._smooth_float(old_gain, desired_gain)
|
new_exp = desired_exp
|
||||||
action = "increase_exposure" if error > 0 else "decrease_exposure"
|
new_gain = old_gain
|
||||||
reason = f"corrigindo {self.control_metric}: value={control_value:.3f} target={target:.3f} error={error:.3f}"
|
gain_policy = "hold_gain_prefer_exposure"
|
||||||
|
|
||||||
|
exp_high_threshold = int(exp_max * self.exp_high_ratio_for_gain)
|
||||||
|
|
||||||
|
if (
|
||||||
|
desired_exp >= exp_high_threshold
|
||||||
|
and under_cycles >= self.gain_increase_required_cycles
|
||||||
|
):
|
||||||
|
# Sobe ganho devagar, em degrau fixo.
|
||||||
|
desired_gain = old_gain + self.gain_step_up
|
||||||
|
new_gain = float(self._clamp(desired_gain, gain_min, gain_max))
|
||||||
|
gain_policy = f"increase_gain_slow_under_cycles_{under_cycles}"
|
||||||
|
else:
|
||||||
|
new_gain = old_gain
|
||||||
|
gain_policy = f"hold_gain_under_cycles_{under_cycles}"
|
||||||
|
|
||||||
|
action = "increase_exposure"
|
||||||
|
reason = (
|
||||||
|
f"subindo exposição por {self.control_metric}: "
|
||||||
|
f"value={control_value:.3f} target={target:.3f} "
|
||||||
|
f"error={error:.3f} factor={factor:.3f} gain_policy={gain_policy}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ----------------------------------------------------
|
||||||
|
# 2B) Cena clara: se ganho está acima do mínimo,
|
||||||
|
# reduzir ganho primeiro ou junto.
|
||||||
|
# ----------------------------------------------------
|
||||||
|
else:
|
||||||
|
desired_exp = int(self._clamp(old_exp * factor, exp_min, exp_max))
|
||||||
|
new_exp = desired_exp
|
||||||
|
|
||||||
|
if (
|
||||||
|
self.gain_return_enabled
|
||||||
|
and old_gain > gain_min
|
||||||
|
and over_cycles >= self.gain_decrease_required_cycles
|
||||||
|
):
|
||||||
|
desired_gain = old_gain - self.gain_step_down
|
||||||
|
new_gain = float(self._clamp(desired_gain, gain_min, gain_max))
|
||||||
|
gain_policy = f"return_gain_step_over_cycles_{over_cycles}"
|
||||||
|
else:
|
||||||
|
new_gain = old_gain
|
||||||
|
gain_policy = f"hold_gain_over_cycles_{over_cycles}"
|
||||||
|
|
||||||
|
action = "decrease_exposure"
|
||||||
|
reason = (
|
||||||
|
f"reduzindo brilho por {self.control_metric}: "
|
||||||
|
f"value={control_value:.3f} target={target:.3f} "
|
||||||
|
f"error={error:.3f} factor={factor:.3f} gain_policy={gain_policy}"
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
desired_gain = old_gain * factor
|
desired_gain = old_gain * factor
|
||||||
desired_gain = self._clamp(desired_gain, limits["gain_min"], limits["gain_max"])
|
new_gain = float(self._clamp(desired_gain, gain_min, gain_max))
|
||||||
new_gain = self._smooth_float(old_gain, desired_gain)
|
|
||||||
action = "increase_gain" if error > 0 else "decrease_gain"
|
action = "increase_gain" if error > 0 else "decrease_gain"
|
||||||
reason = f"corrigindo ganho por {self.control_metric}: value={control_value:.3f} target={target:.3f} error={error:.3f}"
|
gain_policy = "direct_gain_control"
|
||||||
|
reason = (
|
||||||
|
f"corrigindo ganho por {self.control_metric}: "
|
||||||
|
f"value={control_value:.3f} target={target:.3f} "
|
||||||
|
f"error={error:.3f} factor={factor:.3f}"
|
||||||
|
)
|
||||||
|
|
||||||
new_exp = int(self._clamp(new_exp, limits["exp_min_us"], limits["exp_max_us"]))
|
# ============================================================
|
||||||
new_gain = float(self._clamp(new_gain, limits["gain_min"], limits["gain_max"]))
|
# 3) Dentro da faixa morta: opcionalmente devolver ganho
|
||||||
|
# se ganho alto não é mais necessário.
|
||||||
|
# ============================================================
|
||||||
|
else:
|
||||||
|
if self.gain_return_enabled and old_gain > gain_min:
|
||||||
|
exp_low_threshold = int(exp_max * self.exp_low_ratio_for_gain_return)
|
||||||
|
|
||||||
|
if old_exp < exp_low_threshold:
|
||||||
|
new_gain = float(self._clamp(old_gain * self.gain_return_factor, gain_min, gain_max))
|
||||||
|
gain_policy = "return_gain_while_ready"
|
||||||
|
action = "decrease_gain"
|
||||||
|
reason = (
|
||||||
|
f"dentro da faixa, devolvendo ganho: "
|
||||||
|
f"value={control_value:.3f} target={target:.3f} "
|
||||||
|
f"gain={old_gain:.2f}->{new_gain:.2f}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
gain_policy = "hold_gain_high_exp"
|
||||||
|
else:
|
||||||
|
gain_policy = "hold_gain"
|
||||||
|
|
||||||
|
new_exp = int(self._clamp(new_exp, exp_min, exp_max))
|
||||||
|
new_gain = float(self._clamp(new_gain, gain_min, gain_max))
|
||||||
|
|
||||||
|
ready, ready_cycles = self._update_ready_state(
|
||||||
|
log_role=log_role,
|
||||||
|
action=action,
|
||||||
|
error=error,
|
||||||
|
p95=p95,
|
||||||
|
sat_pct=sat_pct,
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"role": log_role,
|
"role": log_role,
|
||||||
|
|
@ -547,11 +1086,25 @@ class RadiometricController:
|
||||||
"error": error,
|
"error": error,
|
||||||
"p95": p95,
|
"p95": p95,
|
||||||
"sat_pct": sat_pct,
|
"sat_pct": sat_pct,
|
||||||
|
#"metrics_source": metrics.get("source"),
|
||||||
|
#"control_source": metrics.get("control_source"),
|
||||||
|
#"patch_quality": metrics.get("patch_quality"),
|
||||||
|
#"patches": metrics.get("patches"),
|
||||||
|
#"control_values_by_role": metrics.get("control_values_by_role"),
|
||||||
|
#"targets_by_role": metrics.get("targets_by_role"),
|
||||||
|
#"patch_quality_by_role": metrics.get("patch_quality_by_role"),
|
||||||
"old_exp": old_exp,
|
"old_exp": old_exp,
|
||||||
"new_exp": new_exp,
|
"new_exp": new_exp,
|
||||||
"old_gain": old_gain,
|
"old_gain": old_gain,
|
||||||
"new_gain": new_gain,
|
"new_gain": new_gain,
|
||||||
"limits": limits,
|
"limits": limits,
|
||||||
|
"ready": ready,
|
||||||
|
"ready_cycles": ready_cycles,
|
||||||
|
"ready_required_cycles": self.ready_required_cycles,
|
||||||
|
"control_strategy": self.control_strategy,
|
||||||
|
"ratio": ratio,
|
||||||
|
"factor": float(factor),
|
||||||
|
"gain_policy": gain_policy,
|
||||||
}
|
}
|
||||||
|
|
||||||
def apply_control(self, role: str, decision: dict):
|
def apply_control(self, role: str, decision: dict):
|
||||||
|
|
@ -577,7 +1130,13 @@ class RadiometricController:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
responses["error"] = str(e)
|
responses["error"] = str(e)
|
||||||
if self.verbose:
|
if self.verbose:
|
||||||
print(f"[RAD] {role}: {json.dumps(decision, ensure_ascii=False)} | apply={responses}")
|
print(
|
||||||
|
f"[RAD_APPLY] role={role} "
|
||||||
|
f"action={decision.get('action')} "
|
||||||
|
f"exp={decision.get('old_exp')}->{decision.get('new_exp')} "
|
||||||
|
f"gain={decision.get('old_gain'):.2f}->{decision.get('new_gain'):.2f} "
|
||||||
|
f"ok={'error' not in responses}"
|
||||||
|
)
|
||||||
return responses
|
return responses
|
||||||
|
|
||||||
def _limits_for_role(self, role: str) -> dict:
|
def _limits_for_role(self, role: str) -> dict:
|
||||||
|
|
@ -598,3 +1157,67 @@ class RadiometricController:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _clamp(v, lo, hi):
|
def _clamp(v, lo, hi):
|
||||||
return max(lo, min(hi, v))
|
return max(lo, min(hi, v))
|
||||||
|
|
||||||
|
def _print_metrics_debug(self, role: str, result: dict):
|
||||||
|
if not self.verbose:
|
||||||
|
return
|
||||||
|
|
||||||
|
metrics = result.get("metrics", {})
|
||||||
|
decision = result.get("decision", {})
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"[RAD_METRICS] role={role} "
|
||||||
|
f"mode={result.get('mode')} "
|
||||||
|
f"metering={result.get('metering_mode')} "
|
||||||
|
f"action={decision.get('action')} "
|
||||||
|
f"exp={decision.get('old_exp')}->{decision.get('new_exp')} "
|
||||||
|
f"gain={decision.get('old_gain')}->{decision.get('new_gain')} "
|
||||||
|
f"control={decision.get('control_value'):.3f} "
|
||||||
|
f"target={decision.get('target_value'):.3f} "
|
||||||
|
f"p95={decision.get('p95'):.3f} "
|
||||||
|
f"sat={decision.get('sat_pct'):.2f}%"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Caso normal: rgb individual
|
||||||
|
patches = metrics.get("patches", [])
|
||||||
|
if patches:
|
||||||
|
for p in patches:
|
||||||
|
st = p.get("stats", {})
|
||||||
|
print(
|
||||||
|
f" [PATCH] role={p.get('role', role)} "
|
||||||
|
f"type={p.get('type')} "
|
||||||
|
f"roi_source={p.get('roi_source')} "
|
||||||
|
f"roi_pct={p.get('roi_pct')} "
|
||||||
|
f"p50={st.get('p50', 0):.3f} "
|
||||||
|
f"p95={st.get('p95', 0):.3f} "
|
||||||
|
f"sat={st.get('sat_pct', 0):.2f}% "
|
||||||
|
f"dark={st.get('dark_pct', 0):.1f}%"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Caso spectral_shared: RE/NIR agregados
|
||||||
|
roles = metrics.get("roles", {})
|
||||||
|
if roles:
|
||||||
|
for r, item in roles.items():
|
||||||
|
m = item.get("metrics", {})
|
||||||
|
print(
|
||||||
|
f" [ROLE_METRICS] role={r} "
|
||||||
|
f"cam_id={item.get('cam_id')} "
|
||||||
|
f"control={m.get('control_value', 0):.3f} "
|
||||||
|
f"target={m.get('target_value', 0):.3f} "
|
||||||
|
f"p95={m.get('p95', 0):.3f} "
|
||||||
|
f"sat={m.get('sat_pct', 0):.2f}% "
|
||||||
|
f"warnings={m.get('patch_quality', {}).get('warnings', [])}"
|
||||||
|
)
|
||||||
|
|
||||||
|
for p in m.get("patches", []):
|
||||||
|
st = p.get("stats", {})
|
||||||
|
print(
|
||||||
|
f" [PATCH] role={p.get('role', r)} "
|
||||||
|
f"type={p.get('type')} "
|
||||||
|
f"roi_source={p.get('roi_source')} "
|
||||||
|
f"roi_pct={p.get('roi_pct')} "
|
||||||
|
f"p50={st.get('p50', 0):.3f} "
|
||||||
|
f"p95={st.get('p95', 0):.3f} "
|
||||||
|
f"sat={st.get('sat_pct', 0):.2f}% "
|
||||||
|
f"dark={st.get('dark_pct', 0):.1f}%"
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -61,10 +61,31 @@ class RawProcessorCore:
|
||||||
"reference_controls": {},
|
"reference_controls": {},
|
||||||
"clip_output": False,
|
"clip_output": False,
|
||||||
}
|
}
|
||||||
|
self.radiometric_config = {}
|
||||||
|
self.patch_normalization_config = {
|
||||||
|
"enabled": False,
|
||||||
|
"apply_when_metering_mode": "reference_patches",
|
||||||
|
"apply_stage": "after_fusion",
|
||||||
|
"method": "gray_scale_with_white_guard",
|
||||||
|
"space": "multispec_tensor",
|
||||||
|
"targets": {
|
||||||
|
"black": 0.06,
|
||||||
|
"gray": 0.40,
|
||||||
|
"white": 0.78,
|
||||||
|
},
|
||||||
|
"white_guard_max": 0.92,
|
||||||
|
"scale_min": 0.35,
|
||||||
|
"scale_max": 2.50,
|
||||||
|
"clip_output": True,
|
||||||
|
"require_valid_gray": True,
|
||||||
|
"use_black_for_offset": False,
|
||||||
|
"save_patch_stats": True,
|
||||||
|
}
|
||||||
|
self.last_patch_normalization_result = None
|
||||||
self.camera_settings = {}
|
self.camera_settings = {}
|
||||||
|
|
||||||
if calibration_json_path:
|
if calibration_json_path:
|
||||||
self.load_fusion_config_json(calibration_json_path)
|
self.load_config_json(calibration_json_path)
|
||||||
|
|
||||||
def unpack_raw10_packed(
|
def unpack_raw10_packed(
|
||||||
self,
|
self,
|
||||||
|
|
@ -260,107 +281,23 @@ class RawProcessorCore:
|
||||||
raise RuntimeError("RGB obrigatório")
|
raise RuntimeError("RGB obrigatório")
|
||||||
|
|
||||||
channel_names = self._channel_names_from_decoded(decoded)
|
channel_names = self._channel_names_from_decoded(decoded)
|
||||||
|
tensor = self.fuse_multispec_cameras(decoded, meta=None, channels_expected=len(channel_names))
|
||||||
tensor = self.fuse_multispec_cameras(
|
|
||||||
decoded,
|
|
||||||
meta=None,
|
|
||||||
channels_expected=len(channel_names)
|
|
||||||
)
|
|
||||||
|
|
||||||
tensor = self.resize_tensor_chw(tensor, target_size=target_size)
|
tensor = self.resize_tensor_chw(tensor, target_size=target_size)
|
||||||
|
tensor = self.apply_patch_normalization_to_tensor(tensor)
|
||||||
|
|
||||||
return tensor, channel_names
|
return tensor, channel_names
|
||||||
|
|
||||||
def build_infer_tensor_from_stream_old(self, frame, meta, channels_expected):
|
def build_infer_tensor_from_stream(self, frame, meta, channels_expected, target_size=None):
|
||||||
"""
|
|
||||||
Converte o frame vindo do stream do Pi em tensor (C,H,W) float32 0..1
|
|
||||||
compatível com o modelo.
|
|
||||||
Suporta:
|
|
||||||
- RGB uint8/float32 já pronto
|
|
||||||
- MULTISPEC uint8/float32 já pronto
|
|
||||||
- RAW_BRUTO multi_payload (cam2 RGB + cam0/cam1 packed)
|
|
||||||
"""
|
|
||||||
frame_type = meta.get("frame_type")
|
frame_type = meta.get("frame_type")
|
||||||
dtype_str = meta.get("dtype") or meta.get("output_dtype", "uint8")
|
|
||||||
camera_frames = meta.get("camera_frames", {}) or {}
|
|
||||||
|
|
||||||
# -------------------------------------------------
|
|
||||||
# RAW_BRUTO multi_payload
|
|
||||||
# -------------------------------------------------
|
|
||||||
if frame_type == "RAW_BRUTO":
|
if frame_type == "RAW_BRUTO":
|
||||||
if not isinstance(frame, dict):
|
decoded = self.decode_stream_cameras(frame, meta)
|
||||||
raise RuntimeError("RAW_BRUTO esperado como dict de câmeras no modo multi")
|
tensor = self.fuse_multispec_cameras(decoded, meta, channels_expected)
|
||||||
|
tensor = self.resize_tensor_chw(tensor, target_size=target_size)
|
||||||
|
tensor = self.apply_patch_normalization_to_tensor(tensor)
|
||||||
|
return tensor
|
||||||
|
|
||||||
arrays = []
|
if frame_type in ("RGB", "MULTISPEC"):
|
||||||
channel_names = []
|
|
||||||
|
|
||||||
# RGB USB
|
|
||||||
if "cam2" in frame:
|
|
||||||
rgb_bgr = frame["cam2"]
|
|
||||||
if rgb_bgr.ndim != 3 or rgb_bgr.shape[2] != 3:
|
|
||||||
raise RuntimeError(f"cam2 RGB inválida: shape={rgb_bgr.shape}")
|
|
||||||
|
|
||||||
rgb = rgb_bgr[:, :, ::-1].astype(np.float32) / 255.0
|
|
||||||
rgb_chw = np.transpose(rgb, (2, 0, 1))
|
|
||||||
arrays.append(rgb_chw)
|
|
||||||
channel_names.extend(["R", "G", "B"])
|
|
||||||
else:
|
|
||||||
raise RuntimeError("RAW_BRUTO para inferência precisa incluir cam2 (RGB)")
|
|
||||||
|
|
||||||
# RE / NIR
|
|
||||||
for cam_id, spec_name in (("cam0", "RE"), ("cam1", "NIR")):
|
|
||||||
if cam_id not in frame:
|
|
||||||
continue
|
|
||||||
|
|
||||||
packed = frame[cam_id]
|
|
||||||
if packed.ndim == 3 and packed.shape[2] == 1:
|
|
||||||
packed = packed[:, :, 0]
|
|
||||||
|
|
||||||
cam_meta = camera_frames.get(cam_id, {})
|
|
||||||
packed_width = int(cam_meta.get("width", packed.shape[1]))
|
|
||||||
height = int(cam_meta.get("height", packed.shape[0]))
|
|
||||||
bayer = cam_meta.get("bayer_pattern", self.bayer_pattern)
|
|
||||||
bit_depth = int(cam_meta.get("bit_depth", 10))
|
|
||||||
|
|
||||||
if bit_depth == 10:
|
|
||||||
real_width = int((packed_width * 8) / 10)
|
|
||||||
else:
|
|
||||||
real_width = packed_width
|
|
||||||
|
|
||||||
rp = RawProcessorCore(
|
|
||||||
sensor_width=real_width,
|
|
||||||
sensor_height=height,
|
|
||||||
bayer_pattern=bayer,
|
|
||||||
)
|
|
||||||
|
|
||||||
raw16 = rp.unpack_raw10_packed(packed)
|
|
||||||
|
|
||||||
max_val = float((1 << bit_depth) - 1)
|
|
||||||
single = np.clip(raw16.astype(np.float32) / max_val, 0.0, 1.0)[None, :, :]
|
|
||||||
|
|
||||||
arrays.append(single)
|
|
||||||
channel_names.append(spec_name)
|
|
||||||
|
|
||||||
if len(arrays) < 2:
|
|
||||||
raise RuntimeError("RAW_BRUTO requer RGB + pelo menos um canal espectral para inferência")
|
|
||||||
|
|
||||||
min_h = min(a.shape[1] for a in arrays)
|
|
||||||
min_w = min(a.shape[2] for a in arrays)
|
|
||||||
arrays = [a[:, :min_h, :min_w] for a in arrays]
|
|
||||||
|
|
||||||
raw_np = np.concatenate(arrays, axis=0)
|
|
||||||
|
|
||||||
if raw_np.shape[0] != channels_expected:
|
|
||||||
raise RuntimeError(
|
|
||||||
f"Tensor RAW_BRUTO montado com canais inesperados: {raw_np.shape[0]} | esperado={channels_expected} | got={channel_names}"
|
|
||||||
)
|
|
||||||
|
|
||||||
return raw_np
|
|
||||||
|
|
||||||
# -------------------------------------------------
|
|
||||||
# RGB ou MULTISPEC já pronto
|
|
||||||
# -------------------------------------------------
|
|
||||||
if frame_type == "RGB" or frame_type == "MULTISPEC":
|
|
||||||
if not isinstance(frame, np.ndarray):
|
if not isinstance(frame, np.ndarray):
|
||||||
raise RuntimeError(f"Frame {frame_type} esperado como ndarray")
|
raise RuntimeError(f"Frame {frame_type} esperado como ndarray")
|
||||||
|
|
||||||
|
|
@ -377,26 +314,11 @@ class RawProcessorCore:
|
||||||
raise RuntimeError(f"dtype {frame_type} não suportado: {dtype_str}")
|
raise RuntimeError(f"dtype {frame_type} não suportado: {dtype_str}")
|
||||||
|
|
||||||
if raw_np.shape[0] != channels_expected:
|
if raw_np.shape[0] != channels_expected:
|
||||||
raise RuntimeError(
|
raise RuntimeError(f"Frame {frame_type} com canais inesperados: {raw_np.shape[0]} | esperado={channels_expected}")
|
||||||
f"Frame {frame_type} com canais inesperados: {raw_np.shape[0]} | esperado={channels_expected}"
|
tensor = raw_np
|
||||||
)
|
|
||||||
|
|
||||||
return raw_np
|
tensor = self.resize_tensor_chw(tensor, target_size=target_size)
|
||||||
|
return tensor
|
||||||
|
|
||||||
raise RuntimeError(f"frame_type não suportado para inferência: {frame_type}")
|
|
||||||
|
|
||||||
def build_infer_tensor_from_stream(self, frame, meta, channels_expected, target_size=None):
|
|
||||||
frame_type = meta.get("frame_type")
|
|
||||||
|
|
||||||
if frame_type == "RAW_BRUTO":
|
|
||||||
decoded = self.decode_stream_cameras(frame, meta)
|
|
||||||
tensor = self.fuse_multispec_cameras(decoded, meta, channels_expected)
|
|
||||||
return self.resize_tensor_chw(tensor, target_size=target_size)
|
|
||||||
|
|
||||||
if frame_type in ("RGB", "MULTISPEC"):
|
|
||||||
tensor = self.build_infer_tensor_from_stream_old(frame, meta, channels_expected)
|
|
||||||
return self.resize_tensor_chw(tensor, target_size=target_size)
|
|
||||||
|
|
||||||
raise RuntimeError(f"frame_type não suportado para inferência: {frame_type}")
|
raise RuntimeError(f"frame_type não suportado para inferência: {frame_type}")
|
||||||
|
|
||||||
|
|
@ -532,13 +454,8 @@ class RawProcessorCore:
|
||||||
return np.clip(arr01, 0.0, 1.0)
|
return np.clip(arr01, 0.0, 1.0)
|
||||||
|
|
||||||
def fuse_multispec_cameras(self, decoded, meta, channels_expected):
|
def fuse_multispec_cameras(self, decoded, meta, channels_expected):
|
||||||
# 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)
|
decoded = self.apply_dark_to_decoded(decoded)
|
||||||
|
decoded = self.normalize_decoded_by_capture_controls(decoded, meta)
|
||||||
# 3) Aplica o ganho espacial do flat field no espaço individual de cada câmera
|
|
||||||
decoded = self.apply_flat_gain_to_decoded(decoded)
|
decoded = self.apply_flat_gain_to_decoded(decoded)
|
||||||
|
|
||||||
rgb_cam_id = self._find_cam_by_role(decoded, "rgb")
|
rgb_cam_id = self._find_cam_by_role(decoded, "rgb")
|
||||||
|
|
@ -751,6 +668,218 @@ class RawProcessorCore:
|
||||||
|
|
||||||
return np.stack(chans, axis=0)
|
return np.stack(chans, axis=0)
|
||||||
|
|
||||||
|
def apply_patch_normalization_to_tensor(self, tensor: np.ndarray) -> np.ndarray:
|
||||||
|
self.last_patch_normalization_result = None
|
||||||
|
|
||||||
|
cfg = self.patch_normalization_config or {}
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"enabled": bool(cfg.get("enabled", False)),
|
||||||
|
"applied": False,
|
||||||
|
"method": cfg.get("method", "gray_scale_with_white_guard"),
|
||||||
|
"space": cfg.get("space", "multispec_tensor"),
|
||||||
|
"warnings": [],
|
||||||
|
"scales": {},
|
||||||
|
"patch_stats": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
if not cfg.get("enabled", False):
|
||||||
|
result["warnings"].append("patch_normalization_disabled")
|
||||||
|
self.last_patch_normalization_result = result
|
||||||
|
return tensor
|
||||||
|
|
||||||
|
rad_cfg = self.radiometric_config or {}
|
||||||
|
|
||||||
|
if cfg.get("apply_when_metering_mode") == "reference_patches":
|
||||||
|
if rad_cfg.get("metering_mode") != "reference_patches":
|
||||||
|
result["warnings"].append(
|
||||||
|
f"metering_mode_not_reference_patches: {rad_cfg.get('metering_mode')}"
|
||||||
|
)
|
||||||
|
self.last_patch_normalization_result = result
|
||||||
|
return tensor
|
||||||
|
|
||||||
|
if tensor is None or tensor.ndim != 3 or tensor.shape[0] < 5:
|
||||||
|
result["warnings"].append(f"invalid_tensor_shape: {None if tensor is None else tensor.shape}")
|
||||||
|
self.last_patch_normalization_result = result
|
||||||
|
return tensor
|
||||||
|
|
||||||
|
patches = rad_cfg.get("reference_patches", []) or []
|
||||||
|
patch_by_type = {
|
||||||
|
str(p.get("type", "")).lower(): p
|
||||||
|
for p in patches
|
||||||
|
if isinstance(p, dict)
|
||||||
|
}
|
||||||
|
|
||||||
|
gray = patch_by_type.get("gray")
|
||||||
|
white = patch_by_type.get("white")
|
||||||
|
black = patch_by_type.get("black")
|
||||||
|
|
||||||
|
if gray is None:
|
||||||
|
result["warnings"].append("missing_gray_patch")
|
||||||
|
if cfg.get("require_valid_gray", True):
|
||||||
|
self.last_patch_normalization_result = result
|
||||||
|
return tensor
|
||||||
|
|
||||||
|
targets = cfg.get("targets", {}) or {}
|
||||||
|
gray_target = float(targets.get("gray", 0.40))
|
||||||
|
|
||||||
|
scale_min = float(cfg.get("scale_min", 0.35))
|
||||||
|
scale_max = float(cfg.get("scale_max", 2.50))
|
||||||
|
white_guard_max = float(cfg.get("white_guard_max", 0.92))
|
||||||
|
clip_output = bool(cfg.get("clip_output", True))
|
||||||
|
|
||||||
|
channel_names = ["R", "G", "B", "RE", "NIR"]
|
||||||
|
|
||||||
|
out = tensor.astype(np.float32).copy()
|
||||||
|
h, w = out.shape[1], out.shape[2]
|
||||||
|
|
||||||
|
def roi_from_patch(patch):
|
||||||
|
if not patch:
|
||||||
|
return None
|
||||||
|
return self._roi_pct_to_pixels_from_patch(patch.get("roi_pct", {}) or {}, w, h)
|
||||||
|
|
||||||
|
gray_roi = roi_from_patch(gray)
|
||||||
|
white_roi = roi_from_patch(white)
|
||||||
|
black_roi = roi_from_patch(black)
|
||||||
|
|
||||||
|
if gray_roi is None:
|
||||||
|
result["warnings"].append("invalid_gray_roi")
|
||||||
|
self.last_patch_normalization_result = result
|
||||||
|
return tensor
|
||||||
|
|
||||||
|
for ci, ch_name in enumerate(channel_names):
|
||||||
|
ch = out[ci]
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Stats do gray
|
||||||
|
# -----------------------------
|
||||||
|
gx0, gy0, gx1, gy1 = gray_roi
|
||||||
|
gray_vals = ch[gy0:gy1, gx0:gx1].reshape(-1)
|
||||||
|
|
||||||
|
if gray_vals.size <= 0:
|
||||||
|
result["warnings"].append(f"{ch_name}: empty_gray_roi")
|
||||||
|
continue
|
||||||
|
|
||||||
|
gray_p50 = float(np.percentile(gray_vals, 50))
|
||||||
|
gray_p05 = float(np.percentile(gray_vals, 5))
|
||||||
|
gray_p95 = float(np.percentile(gray_vals, 95))
|
||||||
|
gray_sat = float((gray_vals >= 0.98).mean() * 100.0)
|
||||||
|
gray_dark = float((gray_vals <= 0.02).mean() * 100.0)
|
||||||
|
|
||||||
|
result["patch_stats"].setdefault("gray", {})[ch_name] = {
|
||||||
|
"p05": gray_p05,
|
||||||
|
"p50": gray_p50,
|
||||||
|
"p95": gray_p95,
|
||||||
|
"sat_pct": gray_sat,
|
||||||
|
"dark_pct": gray_dark,
|
||||||
|
"roi_px": list(gray_roi),
|
||||||
|
}
|
||||||
|
|
||||||
|
if gray_p50 <= 1e-6:
|
||||||
|
result["warnings"].append(f"{ch_name}: gray_p50_too_low")
|
||||||
|
continue
|
||||||
|
|
||||||
|
scale = gray_target / gray_p50
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Stats do white + guarda
|
||||||
|
# -----------------------------
|
||||||
|
if white_roi is not None:
|
||||||
|
wx0, wy0, wx1, wy1 = white_roi
|
||||||
|
white_vals = ch[wy0:wy1, wx0:wx1].reshape(-1)
|
||||||
|
|
||||||
|
if white_vals.size > 0:
|
||||||
|
white_p50 = float(np.percentile(white_vals, 50))
|
||||||
|
white_p05 = float(np.percentile(white_vals, 5))
|
||||||
|
white_p95 = float(np.percentile(white_vals, 95))
|
||||||
|
white_sat = float((white_vals >= 0.98).mean() * 100.0)
|
||||||
|
white_dark = float((white_vals <= 0.02).mean() * 100.0)
|
||||||
|
|
||||||
|
result["patch_stats"].setdefault("white", {})[ch_name] = {
|
||||||
|
"p05": white_p05,
|
||||||
|
"p50": white_p50,
|
||||||
|
"p95": white_p95,
|
||||||
|
"sat_pct": white_sat,
|
||||||
|
"dark_pct": white_dark,
|
||||||
|
"roi_px": list(white_roi),
|
||||||
|
}
|
||||||
|
|
||||||
|
if white_sat > 0.5:
|
||||||
|
result["warnings"].append(f"{ch_name}: white_patch_saturated_{white_sat:.2f}%")
|
||||||
|
|
||||||
|
if white_p50 > 1e-6:
|
||||||
|
max_scale_by_white = white_guard_max / white_p50
|
||||||
|
if scale > max_scale_by_white:
|
||||||
|
result["warnings"].append(
|
||||||
|
f"{ch_name}: scale_limited_by_white_guard "
|
||||||
|
f"{scale:.3f}->{max_scale_by_white:.3f}"
|
||||||
|
)
|
||||||
|
scale = min(scale, max_scale_by_white)
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Stats do black, só diagnóstico
|
||||||
|
# -----------------------------
|
||||||
|
if black_roi is not None:
|
||||||
|
bx0, by0, bx1, by1 = black_roi
|
||||||
|
black_vals = ch[by0:by1, bx0:bx1].reshape(-1)
|
||||||
|
|
||||||
|
if black_vals.size > 0:
|
||||||
|
black_p50 = float(np.percentile(black_vals, 50))
|
||||||
|
black_p05 = float(np.percentile(black_vals, 5))
|
||||||
|
black_p95 = float(np.percentile(black_vals, 95))
|
||||||
|
black_sat = float((black_vals >= 0.98).mean() * 100.0)
|
||||||
|
black_dark = float((black_vals <= 0.02).mean() * 100.0)
|
||||||
|
|
||||||
|
result["patch_stats"].setdefault("black", {})[ch_name] = {
|
||||||
|
"p05": black_p05,
|
||||||
|
"p50": black_p50,
|
||||||
|
"p95": black_p95,
|
||||||
|
"sat_pct": black_sat,
|
||||||
|
"dark_pct": black_dark,
|
||||||
|
"roi_px": list(black_roi),
|
||||||
|
}
|
||||||
|
|
||||||
|
scale_before_clip = float(scale)
|
||||||
|
scale = float(np.clip(scale, scale_min, scale_max))
|
||||||
|
|
||||||
|
if abs(scale - scale_before_clip) > 1e-6:
|
||||||
|
result["warnings"].append(
|
||||||
|
f"{ch_name}: scale_clipped {scale_before_clip:.3f}->{scale:.3f}"
|
||||||
|
)
|
||||||
|
|
||||||
|
out[ci] = ch * scale
|
||||||
|
|
||||||
|
result["scales"][ch_name] = {
|
||||||
|
"scale": scale,
|
||||||
|
"gray_target": gray_target,
|
||||||
|
"gray_measured_p50": gray_p50,
|
||||||
|
}
|
||||||
|
|
||||||
|
if clip_output:
|
||||||
|
out = np.clip(out, 0.0, 1.0)
|
||||||
|
|
||||||
|
result["applied"] = True
|
||||||
|
result["valid"] = bool(len(result["scales"]) == len(channel_names))
|
||||||
|
result["clip_output"] = clip_output
|
||||||
|
result["shape"] = list(out.shape)
|
||||||
|
result["channel_names"] = channel_names
|
||||||
|
|
||||||
|
self.last_patch_normalization_result = result
|
||||||
|
return out.astype(np.float32, copy=False)
|
||||||
|
|
||||||
|
def _roi_pct_to_pixels_from_patch(self, roi_pct: dict, w: int, h: int):
|
||||||
|
x0 = int(float(roi_pct.get("x0", 0.0)) * w)
|
||||||
|
y0 = int(float(roi_pct.get("y0", 0.0)) * h)
|
||||||
|
x1 = int(float(roi_pct.get("x1", 1.0)) * w)
|
||||||
|
y1 = int(float(roi_pct.get("y1", 1.0)) * h)
|
||||||
|
|
||||||
|
x0 = max(0, min(w - 1, x0))
|
||||||
|
x1 = max(x0 + 1, min(w, x1))
|
||||||
|
y0 = max(0, min(h - 1, y0))
|
||||||
|
y1 = max(y0 + 1, min(h, y1))
|
||||||
|
|
||||||
|
return x0, y0, x1, y1
|
||||||
|
|
||||||
|
|
||||||
def extract_camera_meta(self, meta_json: dict, cam_id: str) -> dict:
|
def extract_camera_meta(self, meta_json: dict, cam_id: str) -> dict:
|
||||||
cam_frames = meta_json.get("camera_frames", {}) or meta_json.get("stream_meta", {}).get("camera_frames", {})
|
cam_frames = meta_json.get("camera_frames", {}) or meta_json.get("stream_meta", {}).get("camera_frames", {})
|
||||||
|
|
@ -919,7 +1048,7 @@ class RawProcessorCore:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def load_fusion_config_json(self, path: str):
|
def load_config_json(self, path: str):
|
||||||
if not path or not os.path.isfile(path):
|
if not path or not os.path.isfile(path):
|
||||||
raise FileNotFoundError(f"Arquivo de calibração não encontrado: {path}")
|
raise FileNotFoundError(f"Arquivo de calibração não encontrado: {path}")
|
||||||
|
|
||||||
|
|
@ -945,10 +1074,18 @@ class RawProcessorCore:
|
||||||
self.flatfield_maps = {}
|
self.flatfield_maps = {}
|
||||||
self.flatfield_loaded = False
|
self.flatfield_loaded = False
|
||||||
|
|
||||||
|
radiometric = data.get("radiometric_config")
|
||||||
|
if isinstance(radiometric, dict):
|
||||||
|
self.radiometric_config = self._merge_config(self.radiometric_config, radiometric)
|
||||||
|
|
||||||
rad_norm_config = data.get("radiometric_normalization")
|
rad_norm_config = data.get("radiometric_normalization")
|
||||||
if isinstance(rad_norm_config, dict):
|
if isinstance(rad_norm_config, dict):
|
||||||
self.radiometric_normalization_config = self._merge_config(self.radiometric_normalization_config, rad_norm_config)
|
self.radiometric_normalization_config = self._merge_config(self.radiometric_normalization_config, rad_norm_config)
|
||||||
|
|
||||||
|
patch_norm = data.get("patch_normalization")
|
||||||
|
if isinstance(patch_norm, dict):
|
||||||
|
self.patch_normalization_config = self._merge_config(self.patch_normalization_config, patch_norm)
|
||||||
|
|
||||||
cam_set = data.get("camera_settings")
|
cam_set = data.get("camera_settings")
|
||||||
if isinstance(cam_set, dict):
|
if isinstance(cam_set, dict):
|
||||||
self.camera_settings = self._merge_config(self.camera_settings, cam_set)
|
self.camera_settings = self._merge_config(self.camera_settings, cam_set)
|
||||||
|
|
@ -1312,22 +1449,18 @@ class RawProcessorCore:
|
||||||
if not meta:
|
if not meta:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
# Preferência: controles reais daquele frame.
|
for key in ("actual_camera_controls", "camera_controls", "startup_camera_controls"):
|
||||||
controls = meta.get("actual_camera_controls")
|
controls = meta.get(key)
|
||||||
if isinstance(controls, dict) and controls:
|
if isinstance(controls, dict) and controls:
|
||||||
return controls
|
return controls
|
||||||
|
|
||||||
# Possíveis nomes alternativos.
|
stream_meta = meta.get("stream_meta")
|
||||||
controls = meta.get("camera_controls")
|
if isinstance(stream_meta, dict):
|
||||||
if isinstance(controls, dict) and controls:
|
for key in ("actual_camera_controls", "camera_controls", "startup_camera_controls"):
|
||||||
return controls
|
controls = stream_meta.get(key)
|
||||||
|
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 {}
|
return {}
|
||||||
|
|
||||||
def _exposure_gain_factor(self, ctrl: dict) -> float:
|
def _exposure_gain_factor(self, ctrl: dict) -> float:
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ def build_flatfield_config(flatfield_json_path, flatfield_data):
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
|
"subtract_dark": True,
|
||||||
"schema": flatfield_data.get("schema", "multispec_flatfield_v1"),
|
"schema": flatfield_data.get("schema", "multispec_flatfield_v1"),
|
||||||
"created_at": flatfield_data.get("created_at"),
|
"created_at": flatfield_data.get("created_at"),
|
||||||
"json_file": rel_or_abs(flatfield_json_path),
|
"json_file": rel_or_abs(flatfield_json_path),
|
||||||
|
|
@ -86,6 +87,31 @@ def build_flatfield_config(flatfield_json_path, flatfield_data):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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():
|
def main():
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Monta o module_params.json unificando calibração de câmera, fusão, radiometria e flat-field.",
|
description="Monta o module_params.json unificando calibração de câmera, fusão, radiometria e flat-field.",
|
||||||
|
|
@ -147,7 +173,7 @@ def main():
|
||||||
# =========================
|
# =========================
|
||||||
# RADIOMETRIC
|
# RADIOMETRIC
|
||||||
# =========================
|
# =========================
|
||||||
radiometric_config = radiometric_data.get(args.radiometric_profile, {}).get("radiometric_config")
|
radiometric_config = pick_radiometric_config(radiometric_data, selected_profile=args.radiometric_profile)
|
||||||
|
|
||||||
if not isinstance(radiometric_config, dict):
|
if not isinstance(radiometric_config, dict):
|
||||||
radiometric_config = cam_data.get("radiometric_config")
|
radiometric_config = cam_data.get("radiometric_config")
|
||||||
|
|
@ -155,30 +181,104 @@ def main():
|
||||||
if not isinstance(radiometric_config, dict):
|
if not isinstance(radiometric_config, dict):
|
||||||
radiometric_config = {
|
radiometric_config = {
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"interval_s": 0.5,
|
"interval_s": 0.20,
|
||||||
"verbose": True,
|
"verbose": True,
|
||||||
|
|
||||||
"metering_mode": "global",
|
"metering_mode": "global",
|
||||||
"spectral_control_mode": "shared",
|
"spectral_control_mode": "shared",
|
||||||
"global_roi_pct": {
|
|
||||||
"x0": 0.08,
|
|
||||||
"y0": 0.08,
|
|
||||||
"x1": 0.92,
|
|
||||||
"y1": 0.92
|
|
||||||
},
|
|
||||||
"control_metric": "p50",
|
"control_metric": "p50",
|
||||||
"target_value": 0.40,
|
"target_value": 0.40,
|
||||||
"deadband": 0.04,
|
"deadband": 0.04,
|
||||||
"p95_limit": 0.94,
|
|
||||||
"saturation_limit_pct": 1.0,
|
"p95_limit": 0.90,
|
||||||
"alpha": 0.18,
|
"saturation_limit_pct": 0.50,
|
||||||
"exp_step_gain": 0.55,
|
"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,
|
"prefer_exposure": True,
|
||||||
|
|
||||||
"exp_min_us": 100,
|
"exp_min_us": 100,
|
||||||
"exp_max_us": 80000,
|
"exp_max_us": 80000,
|
||||||
"gain_min": 1.0,
|
"gain_min": 1.0,
|
||||||
"gain_max": 4.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,
|
"apply_same_spectral_to_both": True,
|
||||||
"spectral_roles": ["re", "nir"]
|
"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": True,
|
||||||
|
"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,
|
||||||
|
}
|
||||||
|
|
||||||
|
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": False,
|
||||||
|
"apply_when_metering_mode": "reference_patches",
|
||||||
|
"apply_stage": "after_fusion",
|
||||||
|
"method": "gray_scale_with_white_guard",
|
||||||
|
"space": "multispec_tensor",
|
||||||
|
"targets": {
|
||||||
|
"black": 0.06,
|
||||||
|
"gray": 0.40,
|
||||||
|
"white": 0.78,
|
||||||
|
},
|
||||||
|
"white_guard_max": 0.92,
|
||||||
|
"scale_min": 0.35,
|
||||||
|
"scale_max": 2.50,
|
||||||
|
"clip_output": True,
|
||||||
|
"require_valid_gray": True,
|
||||||
|
"use_black_for_offset": False,
|
||||||
|
"save_patch_stats": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
# =========================
|
# =========================
|
||||||
|
|
@ -216,6 +316,8 @@ def main():
|
||||||
"camera_settings": camera_settings,
|
"camera_settings": camera_settings,
|
||||||
"fusion_config": fusion_config,
|
"fusion_config": fusion_config,
|
||||||
"radiometric_config": radiometric_config,
|
"radiometric_config": radiometric_config,
|
||||||
|
"radiometric_normalization": radiometric_normalization_config,
|
||||||
|
"patch_normalization": patch_normalization_config,
|
||||||
"rgb_calibration": rgb_calibration,
|
"rgb_calibration": rgb_calibration,
|
||||||
"flatfield_config": flatfield_config,
|
"flatfield_config": flatfield_config,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import os
|
||||||
import json
|
import json
|
||||||
import argparse
|
import argparse
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
@ -15,6 +16,100 @@ def load_json(path: Path) -> dict:
|
||||||
return json.load(f)
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def ts_name() -> str:
|
||||||
|
return datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3]
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_dir(path: Path | str):
|
||||||
|
Path(path).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def save_multispec_tensor_from_raw_group(
|
||||||
|
group: dict,
|
||||||
|
meta: dict,
|
||||||
|
out_dir: str = "calibration/offline_samples",
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Gera e salva um tensor MULTISPEC [5,H,W] float32 a partir de uma captura RAW_BRUTO.
|
||||||
|
|
||||||
|
Saídas:
|
||||||
|
.raw -> tensor float32 CHW
|
||||||
|
.json -> metadados do tensor gerado offline
|
||||||
|
.png -> preview RGB do tensor
|
||||||
|
"""
|
||||||
|
if meta.get("saved_payload_type") != "raw_native_multi":
|
||||||
|
raise RuntimeError("Só é possível gerar tensor offline a partir de saved_payload_type='raw_native_multi'.")
|
||||||
|
|
||||||
|
ensure_dir(out_dir)
|
||||||
|
out_dir = Path(out_dir)
|
||||||
|
|
||||||
|
tensor, desc = build_multispec_from_raw_native_multi(group, meta)
|
||||||
|
|
||||||
|
if tensor is None:
|
||||||
|
raise RuntimeError(f"Falha ao gerar tensor MULTISPEC: {desc}")
|
||||||
|
|
||||||
|
base_name = Path(group["json"]).stem
|
||||||
|
name = f"{base_name}_offline_multispec"
|
||||||
|
|
||||||
|
raw_path = out_dir / f"{name}.raw"
|
||||||
|
json_path = out_dir / f"{name}.json"
|
||||||
|
png_path = out_dir / f"{name}.png"
|
||||||
|
|
||||||
|
tensor = np.ascontiguousarray(tensor.astype(np.float32, copy=False))
|
||||||
|
tensor.tofile(str(raw_path))
|
||||||
|
|
||||||
|
# Preview RGB do tensor
|
||||||
|
rgb_hwc = np.transpose(tensor[:3], (1, 2, 0))
|
||||||
|
preview_bgr = normalize_float01_to_bgr(rgb_hwc)
|
||||||
|
cv2.imwrite(str(png_path), preview_bgr)
|
||||||
|
|
||||||
|
# JSON compatível com o validador e com análise posterior
|
||||||
|
out_meta = {
|
||||||
|
"ts": datetime.now().isoformat(timespec="milliseconds"),
|
||||||
|
"schema": "offline_multispec_from_raw_native_multi_v1",
|
||||||
|
"source_json": str(group["json"]),
|
||||||
|
"source_saved_payload_type": meta.get("saved_payload_type"),
|
||||||
|
"source_saved_payload_paths": meta.get("saved_payload_paths"),
|
||||||
|
"source_saved_payload_shapes": meta.get("saved_payload_shapes"),
|
||||||
|
"source_saved_payload_dtypes": meta.get("saved_payload_dtypes"),
|
||||||
|
"camera_params_json": meta.get("camera_params_json"),
|
||||||
|
"frame_type": "MULTISPEC",
|
||||||
|
"saved_payload_type": "multispec",
|
||||||
|
"saved_payload_path": raw_path.name,
|
||||||
|
"saved_payload_dtype": "float32",
|
||||||
|
"saved_payload_shape": list(tensor.shape),
|
||||||
|
"channels": ["R", "G", "B", "RE", "NIR"],
|
||||||
|
"saved_preview_path": png_path.name,
|
||||||
|
"generation": {
|
||||||
|
"method": "build_multispec_from_raw_native_multi",
|
||||||
|
"description": desc,
|
||||||
|
"same_frame_as_raw_bruto": True,
|
||||||
|
},
|
||||||
|
"source_capture_meta": {
|
||||||
|
"ts": meta.get("ts"),
|
||||||
|
"sensor_width": meta.get("sensor_width"),
|
||||||
|
"sensor_height": meta.get("sensor_height"),
|
||||||
|
"bayer_pattern": meta.get("bayer_pattern"),
|
||||||
|
"fps_target": meta.get("fps_target"),
|
||||||
|
"startup_camera_controls": meta.get("startup_camera_controls"),
|
||||||
|
"actual_camera_controls": meta.get("actual_camera_controls"),
|
||||||
|
"radiometric_last_result": meta.get("radiometric_last_result"),
|
||||||
|
"stream_meta": meta.get("stream_meta"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(json_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(out_meta, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"tensor": tensor,
|
||||||
|
"raw_path": raw_path,
|
||||||
|
"json_path": json_path,
|
||||||
|
"png_path": png_path,
|
||||||
|
"desc": desc,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def normalize_float01_to_bgr(img_float: np.ndarray) -> np.ndarray:
|
def normalize_float01_to_bgr(img_float: np.ndarray) -> np.ndarray:
|
||||||
"""
|
"""
|
||||||
Recebe RGB float32 [0..1] em HWC e devolve BGR uint8.
|
Recebe RGB float32 [0..1] em HWC e devolve BGR uint8.
|
||||||
|
|
@ -657,6 +752,7 @@ def render_group_to_canvas(json_path: Path, max_width: int):
|
||||||
canvas = compose_panels(panels, max_width=max_width)
|
canvas = compose_panels(panels, max_width=max_width)
|
||||||
|
|
||||||
info = {
|
info = {
|
||||||
|
"group": group,
|
||||||
"json": group["json"],
|
"json": group["json"],
|
||||||
"png": group["png"],
|
"png": group["png"],
|
||||||
"final_raw": group["final_raw"],
|
"final_raw": group["final_raw"],
|
||||||
|
|
@ -677,7 +773,7 @@ def main():
|
||||||
input_path = Path(args.input_path)
|
input_path = Path(args.input_path)
|
||||||
entries, current_idx = resolve_navigation_inputs(input_path)
|
entries, current_idx = resolve_navigation_inputs(input_path)
|
||||||
|
|
||||||
window_name = "Validacao do payload salvo | A=anterior | D=proximo | Q/Esc=sair"
|
window_name = "Validacao payload | A=anterior | D=proximo | T=salva tensor offline | Q/Esc=sair"
|
||||||
cv2.namedWindow(window_name, cv2.WINDOW_NORMAL)
|
cv2.namedWindow(window_name, cv2.WINDOW_NORMAL)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
|
@ -717,6 +813,25 @@ def main():
|
||||||
current_idx = min(current_idx + 1, len(entries) - 1)
|
current_idx = min(current_idx + 1, len(entries) - 1)
|
||||||
elif k in (ord("a"), ord("A")):
|
elif k in (ord("a"), ord("A")):
|
||||||
current_idx = max(current_idx - 1, 0)
|
current_idx = max(current_idx - 1, 0)
|
||||||
|
elif k in (ord("t"), ord("T")):
|
||||||
|
meta = info["meta"]
|
||||||
|
group = info["group"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = save_multispec_tensor_from_raw_group(
|
||||||
|
group=group,
|
||||||
|
meta=meta,
|
||||||
|
out_dir="calibration/offline_samples",
|
||||||
|
)
|
||||||
|
|
||||||
|
print("[OK] Tensor MULTISPEC offline salvo:")
|
||||||
|
print(" RAW :", result["raw_path"])
|
||||||
|
print(" JSON:", result["json_path"])
|
||||||
|
print(" PNG :", result["png_path"])
|
||||||
|
print(" DESC:", result["desc"])
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print("[ERRO] Falha ao salvar tensor MULTISPEC offline:", e)
|
||||||
|
|
||||||
cv2.destroyAllWindows()
|
cv2.destroyAllWindows()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -172,9 +172,30 @@ def get_image_by_role(decoded: dict, role: str):
|
||||||
return cam_id, item.get("image")
|
return cam_id, item.get("image")
|
||||||
|
|
||||||
|
|
||||||
|
def get_visual_preview_by_role(visual_previews: dict, meta: dict, role: str):
|
||||||
|
"""
|
||||||
|
Busca uma imagem visual BGR dentro do retorno de cam.build_visual_preview_from_raw(),
|
||||||
|
usando camera_info para descobrir o role rgb/re/nir.
|
||||||
|
|
||||||
|
Retorna: cam_id, img_bgr
|
||||||
|
"""
|
||||||
|
if not visual_previews:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
camera_info = (meta or {}).get("camera_info", {}) or {}
|
||||||
|
role = str(role).lower()
|
||||||
|
|
||||||
|
for cam_id, img in visual_previews.items():
|
||||||
|
cam_role = str(camera_info.get(cam_id, {}).get("role", "")).lower()
|
||||||
|
if cam_role == role:
|
||||||
|
return cam_id, img
|
||||||
|
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
def validate_module_ready(status: dict, raw_policy: str):
|
def validate_module_ready(status: dict, raw_policy: str):
|
||||||
if not status.get("ok", True):
|
if not status.get("ok", True):
|
||||||
raise RuntimeError(f"Status inválido retornado pelo módulo: {status}")
|
raise RuntimeError(f"Status inválido retornado pelo modulo: {status}")
|
||||||
|
|
||||||
active_roles = status.get("active_roles", {}) or {}
|
active_roles = status.get("active_roles", {}) or {}
|
||||||
active_count = int(status.get("camera_count_active", 0))
|
active_count = int(status.get("camera_count_active", 0))
|
||||||
|
|
@ -194,124 +215,174 @@ def validate_module_ready(status: dict, raw_policy: str):
|
||||||
# Config radiométrico
|
# Config radiométrico
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
def default_profile_global():
|
def base_ae_contract():
|
||||||
return {
|
return {
|
||||||
"radiometric_config": {
|
"enabled": True,
|
||||||
"enabled": True,
|
"interval_s": 0.20,
|
||||||
"interval_s": 0.5,
|
"verbose": True,
|
||||||
"verbose": True,
|
|
||||||
|
|
||||||
"metering_mode": "global",
|
"control_metric": "p50",
|
||||||
"spectral_control_mode": "shared",
|
"target_value": 0.40,
|
||||||
|
"deadband": 0.04,
|
||||||
|
|
||||||
"global_roi_pct": {
|
"p95_limit": 0.90,
|
||||||
"x0": 0.08,
|
"saturation_limit_pct": 0.50,
|
||||||
"y0": 0.08,
|
"dark_limit_pct": 35.0,
|
||||||
"x1": 0.92,
|
|
||||||
"y1": 0.92,
|
|
||||||
},
|
|
||||||
|
|
||||||
"control_metric": "p50",
|
# Novo controle proporcional por razão
|
||||||
"target_value": 0.40,
|
"control_strategy": "ratio",
|
||||||
"deadband": 0.04,
|
"ratio_alpha": 0.55,
|
||||||
|
"ratio_min": 0.55,
|
||||||
|
"ratio_max": 1.85,
|
||||||
|
|
||||||
"p95_limit": 0.94,
|
# Redução rápida quando satura
|
||||||
"saturation_limit_pct": 1.0,
|
"reduce_fast_factor": 0.75,
|
||||||
"dark_limit_pct": 35.0,
|
|
||||||
|
|
||||||
"alpha": 0.18,
|
# Mantém compatibilidade com o modo antigo
|
||||||
"exp_step_gain": 0.55,
|
"alpha": 0.18,
|
||||||
"prefer_exposure": True,
|
"exp_step_gain": 0.55,
|
||||||
|
"factor_min": 0.72,
|
||||||
|
"factor_max": 1.28,
|
||||||
|
|
||||||
"exp_min_us": 100,
|
"prefer_exposure": True,
|
||||||
"exp_max_us": 80000,
|
|
||||||
"gain_min": 1.0,
|
|
||||||
"gain_max": 4.0,
|
|
||||||
|
|
||||||
"role_limits": {
|
"exp_min_us": 100,
|
||||||
"rgb": {"exp_min_us": 100, "exp_max_us": 80000, "gain_min": 1.0, "gain_max": 4.0},
|
"exp_max_us": 80000,
|
||||||
"re": {"exp_min_us": 100, "exp_max_us": 80000, "gain_min": 1.0, "gain_max": 3.0},
|
"gain_min": 1.0,
|
||||||
"nir": {"exp_min_us": 100, "exp_max_us": 80000, "gain_min": 1.0, "gain_max": 3.0},
|
"gain_max": 4.0,
|
||||||
},
|
|
||||||
|
|
||||||
"exp_apply_threshold_us": 80,
|
"gain_return_enabled": True,
|
||||||
"gain_apply_threshold": 0.05,
|
"gain_reduce_on_saturation": True,
|
||||||
|
"gain_increase_required_cycles": 5,
|
||||||
|
"gain_decrease_required_cycles": 2,
|
||||||
|
"gain_step_up": 0.20,
|
||||||
|
"gain_step_down": 0.50,
|
||||||
|
"gain_hard_reset_on_saturation": False,
|
||||||
|
"exp_high_ratio_for_gain": 0.95,
|
||||||
|
"exp_low_ratio_for_gain_return": 0.75,
|
||||||
|
|
||||||
"apply_same_spectral_to_both": True,
|
"role_limits": {
|
||||||
"spectral_roles": ["re", "nir"],
|
"rgb": {"exp_min_us": 100, "exp_max_us": 80000, "gain_min": 1.0, "gain_max": 2.0},
|
||||||
}
|
"re": {"exp_min_us": 100, "exp_max_us": 80000, "gain_min": 1.0, "gain_max": 2.0},
|
||||||
|
"nir": {"exp_min_us": 100, "exp_max_us": 80000, "gain_min": 1.0, "gain_max": 2.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"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def default_profile_global():
|
||||||
|
cfg = base_ae_contract()
|
||||||
|
base = {
|
||||||
|
"x0": 0.08,
|
||||||
|
"y0": 0.08,
|
||||||
|
"x1": 0.92,
|
||||||
|
"y1": 0.92,
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.update({
|
||||||
|
"metering_mode": "global",
|
||||||
|
"spectral_control_mode": "shared",
|
||||||
|
"global_roi_pct": base,
|
||||||
|
"global_roi_pct_by_role": {
|
||||||
|
"rgb": dict(base),
|
||||||
|
"re": dict(base),
|
||||||
|
"nir": dict(base),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"radiometric_config": cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def default_profile_patches():
|
def default_profile_patches():
|
||||||
return {
|
cfg = base_ae_contract()
|
||||||
"radiometric_config": {
|
cfg.update({
|
||||||
"enabled": True,
|
"metering_mode": "reference_patches",
|
||||||
"interval_s": 0.5,
|
"spectral_control_mode": "shared",
|
||||||
"verbose": True,
|
"deadband": 0.035,
|
||||||
|
|
||||||
"metering_mode": "reference_patches",
|
"metering_mode": "reference_patches",
|
||||||
"spectral_control_mode": "shared",
|
"patch_control_mode": "gray_primary",
|
||||||
|
"patch_require_order": True,
|
||||||
|
"patch_min_separation": 0.08,
|
||||||
|
|
||||||
"control_metric": "p50",
|
"patch_white_sat_limit_pct": 0.50,
|
||||||
"target_value": 0.40,
|
"patch_white_p95_limit": 0.90,
|
||||||
"deadband": 0.035,
|
|
||||||
|
|
||||||
"p95_limit": 0.94,
|
"patch_black_dark_limit_pct": 80.0,
|
||||||
"saturation_limit_pct": 1.0,
|
"patch_black_max_p50": 0.20,
|
||||||
"dark_limit_pct": 35.0,
|
|
||||||
|
|
||||||
"alpha": 0.18,
|
"patch_gray_min_p50": 0.08,
|
||||||
"exp_step_gain": 0.55,
|
"patch_gray_max_p50": 0.85,
|
||||||
"prefer_exposure": True,
|
|
||||||
|
|
||||||
"exp_min_us": 100,
|
"reference_patches": [
|
||||||
"exp_max_us": 80000,
|
{
|
||||||
"gain_min": 1.0,
|
"name": "black_reference",
|
||||||
"gain_max": 4.0,
|
"type": "black",
|
||||||
|
"roles": ["rgb", "re", "nir"],
|
||||||
"role_limits": {
|
"target_value": 0.06,
|
||||||
"rgb": {"exp_min_us": 100, "exp_max_us": 80000, "gain_min": 1.0, "gain_max": 4.0},
|
"weight": 0.25,
|
||||||
"re": {"exp_min_us": 100, "exp_max_us": 80000, "gain_min": 1.0, "gain_max": 3.0},
|
"roi_pct": {},
|
||||||
"nir": {"exp_min_us": 100, "exp_max_us": 80000, "gain_min": 1.0, "gain_max": 3.0},
|
"roi_pct_by_role": {"rgb": {}, "re": {}, "nir": {}}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "gray_reference",
|
||||||
|
"type": "gray",
|
||||||
|
"roles": ["rgb", "re", "nir"],
|
||||||
|
"target_value": 0.40,
|
||||||
|
"weight": 1.0,
|
||||||
|
"roi_pct": {},
|
||||||
|
"roi_pct_by_role": {"rgb": {}, "re": {}, "nir": {}}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "white_reference",
|
||||||
|
"type": "white",
|
||||||
|
"roles": ["rgb", "re", "nir"],
|
||||||
|
"target_value": 0.78,
|
||||||
|
"weight": 0.7,
|
||||||
|
"roi_pct": {},
|
||||||
|
"roi_pct_by_role": {"rgb": {}, "re": {}, "nir": {}}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
"reference_patches": [
|
return {
|
||||||
{
|
"radiometric_config": cfg
|
||||||
"name": "black_reference",
|
|
||||||
"type": "black",
|
|
||||||
"roles": ["rgb", "re", "nir"],
|
|
||||||
"roi_pct": {"x0": 0.05, "y0": 0.92, "x1": 0.18, "y1": 0.99},
|
|
||||||
"target_value": 0.08,
|
|
||||||
"weight": 0.7,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "gray_reference",
|
|
||||||
"type": "gray",
|
|
||||||
"roles": ["rgb", "re", "nir"],
|
|
||||||
"roi_pct": {"x0": 0.35, "y0": 0.92, "x1": 0.55, "y1": 0.99},
|
|
||||||
"target_value": 0.40,
|
|
||||||
"weight": 1.0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "white_reference",
|
|
||||||
"type": "white",
|
|
||||||
"roles": ["rgb", "re", "nir"],
|
|
||||||
"roi_pct": {"x0": 0.75, "y0": 0.92, "x1": 0.95, "y1": 0.99},
|
|
||||||
"target_value": 0.82,
|
|
||||||
"weight": 0.8,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
"exp_apply_threshold_us": 80,
|
|
||||||
"gain_apply_threshold": 0.05,
|
|
||||||
|
|
||||||
"apply_same_spectral_to_both": True,
|
|
||||||
"spectral_roles": ["re", "nir"],
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_active_profile_name(data: dict) -> str:
|
||||||
|
name = str(data.get("active_profile", "global_scene_mode"))
|
||||||
|
if name not in ("global_scene_mode", "three_reference_patches_mode"):
|
||||||
|
return "global_scene_mode"
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def set_active_profile_name(data: dict, profile_name: str):
|
||||||
|
if profile_name not in ("global_scene_mode", "three_reference_patches_mode"):
|
||||||
|
profile_name = "global_scene_mode"
|
||||||
|
data["active_profile"] = profile_name
|
||||||
|
|
||||||
|
|
||||||
|
def get_active_radiometric_config(data: dict) -> dict:
|
||||||
|
profile_name = get_active_profile_name(data)
|
||||||
|
profile = data.get(profile_name, {}) or {}
|
||||||
|
cfg = profile.get("radiometric_config", {}) or {}
|
||||||
|
return json.loads(json.dumps(cfg))
|
||||||
|
|
||||||
|
|
||||||
|
def update_root_radiometric_config(data: dict):
|
||||||
|
data["radiometric_config"] = get_active_radiometric_config(data)
|
||||||
|
|
||||||
|
|
||||||
def load_or_default_config(path: str):
|
def load_or_default_config(path: str):
|
||||||
if path and os.path.isfile(path):
|
if path and os.path.isfile(path):
|
||||||
with open(path, "r", encoding="utf-8") as f:
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
|
@ -319,36 +390,120 @@ def load_or_default_config(path: str):
|
||||||
else:
|
else:
|
||||||
data = {}
|
data = {}
|
||||||
|
|
||||||
data.setdefault("schema", "multispec_radiometric_config_profiles_v1")
|
data.setdefault("schema", "multispec_radiometric_config_profiles_v3")
|
||||||
data.setdefault("saved_at", now_str())
|
data.setdefault("saved_at", now_str())
|
||||||
|
data.setdefault("active_profile", "global_scene_mode")
|
||||||
data.setdefault("global_scene_mode", default_profile_global())
|
data.setdefault("global_scene_mode", default_profile_global())
|
||||||
data.setdefault("three_reference_patches_mode", default_profile_patches())
|
data.setdefault("three_reference_patches_mode", default_profile_patches())
|
||||||
|
data.setdefault("patch_normalization", {
|
||||||
|
"enabled": True,
|
||||||
|
"apply_when_metering_mode": "reference_patches",
|
||||||
|
"apply_stage": "after_fusion",
|
||||||
|
"method": "gray_scale_with_white_guard",
|
||||||
|
"space": "multispec_tensor",
|
||||||
|
"targets": {
|
||||||
|
"black": 0.06,
|
||||||
|
"gray": 0.40,
|
||||||
|
"white": 0.78
|
||||||
|
},
|
||||||
|
"white_guard_max": 0.92,
|
||||||
|
"scale_min": 0.35,
|
||||||
|
"scale_max": 2.50,
|
||||||
|
"clip_output": True,
|
||||||
|
"require_valid_gray": True,
|
||||||
|
"use_black_for_offset": False,
|
||||||
|
"save_patch_stats": True
|
||||||
|
})
|
||||||
|
|
||||||
|
# Migração: se vier arquivo antigo sem contrato novo, injeta defaults novos
|
||||||
|
for profile_name, default_fn in (
|
||||||
|
("global_scene_mode", default_profile_global),
|
||||||
|
("three_reference_patches_mode", default_profile_patches),
|
||||||
|
):
|
||||||
|
default_profile = default_fn()
|
||||||
|
data.setdefault(profile_name, default_profile)
|
||||||
|
data[profile_name].setdefault("radiometric_config", {})
|
||||||
|
|
||||||
|
default_cfg = default_profile["radiometric_config"]
|
||||||
|
cfg = data[profile_name]["radiometric_config"]
|
||||||
|
|
||||||
|
for k, v in default_cfg.items():
|
||||||
|
cfg.setdefault(k, v)
|
||||||
|
|
||||||
|
update_root_radiometric_config(data)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def save_config(path: str, data: dict):
|
def save_config(path: str, data: dict):
|
||||||
ensure_dir(os.path.dirname(path) or ".")
|
ensure_dir(os.path.dirname(path) or ".")
|
||||||
data = dict(data)
|
data = dict(data)
|
||||||
data["schema"] = "multispec_radiometric_config_profiles_v1"
|
data["schema"] = "multispec_radiometric_config_profiles_v3"
|
||||||
data["saved_at"] = now_str()
|
data["saved_at"] = now_str()
|
||||||
|
|
||||||
|
update_root_radiometric_config(data)
|
||||||
|
|
||||||
with open(path, "w", encoding="utf-8") as f:
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
|
||||||
def get_global_roi(data: dict):
|
ROLES = ["rgb", "re", "nir"]
|
||||||
return (
|
|
||||||
data.get("global_scene_mode", {})
|
|
||||||
.get("radiometric_config", {})
|
def normalize_role(role: str) -> str:
|
||||||
.get("global_roi_pct", {"x0": 0.08, "y0": 0.08, "x1": 0.92, "y1": 0.92})
|
role = str(role or "rgb").lower()
|
||||||
|
return role if role in ROLES else "rgb"
|
||||||
|
|
||||||
|
|
||||||
|
def default_roi():
|
||||||
|
return {"x0": 0.08, "y0": 0.08, "x1": 0.92, "y1": 0.92}
|
||||||
|
|
||||||
|
|
||||||
|
def clone_roi(roi: dict) -> dict:
|
||||||
|
roi = roi or {}
|
||||||
|
return {
|
||||||
|
"x0": float(roi.get("x0", 0.08)),
|
||||||
|
"y0": float(roi.get("y0", 0.08)),
|
||||||
|
"x1": float(roi.get("x1", 0.92)),
|
||||||
|
"y1": float(roi.get("y1", 0.92)),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def make_roi_by_role(base_roi=None):
|
||||||
|
base = clone_roi(base_roi or default_roi())
|
||||||
|
return {role: dict(base) for role in ROLES}
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_global_roi_by_role(data: dict):
|
||||||
|
data.setdefault("global_scene_mode", default_profile_global())
|
||||||
|
cfg = data["global_scene_mode"].setdefault(
|
||||||
|
"radiometric_config",
|
||||||
|
default_profile_global()["radiometric_config"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
legacy = cfg.get("global_roi_pct", default_roi())
|
||||||
|
by_role = cfg.setdefault("global_roi_pct_by_role", make_roi_by_role(legacy))
|
||||||
|
|
||||||
def set_global_roi(data: dict, roi_pct: dict):
|
for role in ROLES:
|
||||||
data.setdefault("global_scene_mode", default_profile_global())
|
if role not in by_role or not by_role[role]:
|
||||||
data["global_scene_mode"].setdefault("radiometric_config", default_profile_global()["radiometric_config"])
|
by_role[role] = clone_roi(legacy)
|
||||||
data["global_scene_mode"]["radiometric_config"]["global_roi_pct"] = roi_pct
|
|
||||||
|
return by_role
|
||||||
|
|
||||||
|
|
||||||
|
def get_global_roi_for_role(data: dict, role: str):
|
||||||
|
role = normalize_role(role)
|
||||||
|
by_role = ensure_global_roi_by_role(data)
|
||||||
|
return by_role.get(role, clone_roi(default_roi()))
|
||||||
|
|
||||||
|
|
||||||
|
def set_global_roi_for_role(data: dict, role: str, roi_pct: dict):
|
||||||
|
role = normalize_role(role)
|
||||||
|
by_role = ensure_global_roi_by_role(data)
|
||||||
|
by_role[role] = roi_pct
|
||||||
|
|
||||||
|
# Compatibilidade: mantém uma ROI antiga preenchida.
|
||||||
|
# Uso: média/legado/visual antigo. O controller novo deverá usar by_role.
|
||||||
|
data["global_scene_mode"]["radiometric_config"]["global_roi_pct"] = by_role.get("rgb", roi_pct)
|
||||||
|
|
||||||
|
|
||||||
def get_patches(data: dict):
|
def get_patches(data: dict):
|
||||||
|
|
@ -359,32 +514,77 @@ def get_patches(data: dict):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def set_patch_roi(data: dict, patch_type: str, roi_pct: dict):
|
def ensure_patch_roi_by_role(patch: dict):
|
||||||
|
legacy = patch.get("roi_pct", {})
|
||||||
|
by_role = patch.setdefault("roi_pct_by_role", {})
|
||||||
|
|
||||||
|
for role in ROLES:
|
||||||
|
if role not in by_role or not by_role[role]:
|
||||||
|
by_role[role] = clone_roi(legacy) if legacy else {}
|
||||||
|
|
||||||
|
return by_role
|
||||||
|
|
||||||
|
|
||||||
|
def get_patch_by_type(data: dict, patch_type: str):
|
||||||
|
patch_type = str(patch_type).lower()
|
||||||
|
for p in get_patches(data):
|
||||||
|
if str(p.get("type", "")).lower() == patch_type:
|
||||||
|
return p
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_patch_roi_for_role(data: dict, patch_type: str, role: str):
|
||||||
|
role = normalize_role(role)
|
||||||
|
patch = get_patch_by_type(data, patch_type)
|
||||||
|
if not patch:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
by_role = ensure_patch_roi_by_role(patch)
|
||||||
|
return by_role.get(role, {}) or patch.get("roi_pct", {}) or {}
|
||||||
|
|
||||||
|
|
||||||
|
def set_patch_roi_for_role(data: dict, patch_type: str, role: str, roi_pct: dict):
|
||||||
data.setdefault("three_reference_patches_mode", default_profile_patches())
|
data.setdefault("three_reference_patches_mode", default_profile_patches())
|
||||||
cfg = data["three_reference_patches_mode"].setdefault(
|
cfg = data["three_reference_patches_mode"].setdefault(
|
||||||
"radiometric_config",
|
"radiometric_config",
|
||||||
default_profile_patches()["radiometric_config"],
|
default_profile_patches()["radiometric_config"],
|
||||||
)
|
)
|
||||||
patches = cfg.setdefault("reference_patches", default_profile_patches()["radiometric_config"]["reference_patches"])
|
|
||||||
|
patches = cfg.setdefault(
|
||||||
|
"reference_patches",
|
||||||
|
default_profile_patches()["radiometric_config"]["reference_patches"],
|
||||||
|
)
|
||||||
|
|
||||||
patch_type = str(patch_type).lower()
|
patch_type = str(patch_type).lower()
|
||||||
|
role = normalize_role(role)
|
||||||
|
|
||||||
|
target = {"black": 0.06, "gray": 0.40, "white": 0.78}.get(patch_type, 0.40)
|
||||||
|
weight = {"black": 0.25, "gray": 1.0, "white": 0.7}.get(patch_type, 1.0)
|
||||||
|
|
||||||
|
patch = None
|
||||||
for p in patches:
|
for p in patches:
|
||||||
if str(p.get("type", "")).lower() == patch_type:
|
if str(p.get("type", "")).lower() == patch_type:
|
||||||
p["roi_pct"] = roi_pct
|
patch = p
|
||||||
return
|
break
|
||||||
|
|
||||||
# Fallback se não existir.
|
if patch is None:
|
||||||
target = {"black": 0.08, "gray": 0.40, "white": 0.82}.get(patch_type, 0.40)
|
patch = {
|
||||||
weight = {"black": 0.7, "gray": 1.0, "white": 0.8}.get(patch_type, 1.0)
|
"name": f"{patch_type}_reference",
|
||||||
|
"type": patch_type,
|
||||||
|
"roles": ROLES[:],
|
||||||
|
"target_value": target,
|
||||||
|
"weight": weight,
|
||||||
|
"roi_pct": {},
|
||||||
|
"roi_pct_by_role": {},
|
||||||
|
}
|
||||||
|
patches.append(patch)
|
||||||
|
|
||||||
patches.append({
|
by_role = ensure_patch_roi_by_role(patch)
|
||||||
"name": f"{patch_type}_reference",
|
by_role[role] = roi_pct
|
||||||
"type": patch_type,
|
|
||||||
"roles": ["rgb", "re", "nir"],
|
# Compatibilidade com formato antigo.
|
||||||
"roi_pct": roi_pct,
|
# Mantém roi_pct como RGB, para scripts antigos não quebrarem.
|
||||||
"target_value": target,
|
patch["roi_pct"] = by_role.get("rgb", roi_pct)
|
||||||
"weight": weight,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
def set_shared_mode(data: dict, shared: bool):
|
def set_shared_mode(data: dict, shared: bool):
|
||||||
|
|
@ -394,6 +594,8 @@ def set_shared_mode(data: dict, shared: bool):
|
||||||
cfg["spectral_control_mode"] = "shared" if shared else "independent"
|
cfg["spectral_control_mode"] = "shared" if shared else "independent"
|
||||||
cfg["apply_same_spectral_to_both"] = bool(shared)
|
cfg["apply_same_spectral_to_both"] = bool(shared)
|
||||||
|
|
||||||
|
update_root_radiometric_config(data)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# UI
|
# UI
|
||||||
|
|
@ -422,47 +624,113 @@ def draw_roi_on_panel(panel, roi_pct, label, color, thickness=2):
|
||||||
0.55, color, 1, cv2.LINE_AA)
|
0.55, color, 1, cv2.LINE_AA)
|
||||||
|
|
||||||
|
|
||||||
def draw_all_rois(panel, data, selected_target, mode):
|
def draw_all_rois(panel, data, selected_target, mode, panel_role, edit_role):
|
||||||
|
panel_role = normalize_role(panel_role)
|
||||||
|
edit_role = normalize_role(edit_role)
|
||||||
|
|
||||||
|
is_edit_panel = panel_role == edit_role
|
||||||
|
|
||||||
if mode == "global":
|
if mode == "global":
|
||||||
roi = get_global_roi(data)
|
roi = get_global_roi_for_role(data, panel_role)
|
||||||
draw_roi_on_panel(panel, roi, "GLOBAL", PATCH_COLORS["global"], 2)
|
label = f"GLOBAL/{panel_role.upper()}"
|
||||||
|
thickness = 3 if is_edit_panel else 2
|
||||||
|
draw_roi_on_panel(panel, roi, label, PATCH_COLORS["global"], thickness)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
for p in get_patches(data):
|
for p in get_patches(data):
|
||||||
typ = str(p.get("type", "")).lower()
|
typ = str(p.get("type", "")).lower()
|
||||||
color = PATCH_COLORS.get(typ, (0, 255, 255))
|
color = PATCH_COLORS.get(typ, (0, 255, 255))
|
||||||
label = typ.upper()
|
roi = get_patch_roi_for_role(data, typ, panel_role)
|
||||||
thickness = 3 if typ == selected_target else 2
|
|
||||||
draw_roi_on_panel(panel, p.get("roi_pct"), label, color, thickness)
|
label = f"{typ.upper()}/{panel_role.upper()}"
|
||||||
|
selected = typ == selected_target and is_edit_panel
|
||||||
|
thickness = 3 if selected else 2
|
||||||
|
|
||||||
|
draw_roi_on_panel(panel, roi, label, color, thickness)
|
||||||
|
|
||||||
|
|
||||||
def build_board(decoded, data, mode, selected_target, drag_rect_local, panel_rects, preview_scale=1.0):
|
def build_board(
|
||||||
|
decoded,
|
||||||
|
data,
|
||||||
|
mode,
|
||||||
|
selected_target,
|
||||||
|
edit_role,
|
||||||
|
drag_rect_local,
|
||||||
|
drag_role,
|
||||||
|
panel_rects,
|
||||||
|
preview_scale=1.0,
|
||||||
|
visual_previews=None,
|
||||||
|
meta=None,
|
||||||
|
beauty_preview=True,
|
||||||
|
):
|
||||||
rgb_id, rgb01 = get_image_by_role(decoded, "rgb")
|
rgb_id, rgb01 = get_image_by_role(decoded, "rgb")
|
||||||
re_id, re01 = get_image_by_role(decoded, "re")
|
re_id, re01 = get_image_by_role(decoded, "re")
|
||||||
nir_id, nir01 = get_image_by_role(decoded, "nir")
|
nir_id, nir01 = get_image_by_role(decoded, "nir")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Tamanho base SEMPRE vem do decoded, porque ROI/stats usam dado real.
|
||||||
|
# O preview visual é só para desenhar bonito.
|
||||||
|
# ------------------------------------------------------------
|
||||||
if rgb01 is not None:
|
if rgb01 is not None:
|
||||||
rgb_panel = to_bgr_u8_from_rgb01(rgb01)
|
|
||||||
base_h, base_w = rgb01.shape[:2]
|
base_h, base_w = rgb01.shape[:2]
|
||||||
|
elif re01 is not None:
|
||||||
|
base_h, base_w = re01.shape[:2]
|
||||||
|
elif nir01 is not None:
|
||||||
|
base_h, base_w = nir01.shape[:2]
|
||||||
else:
|
else:
|
||||||
base_h, base_w = 800, 1280
|
base_h, base_w = 800, 1280
|
||||||
rgb_panel = np.zeros((base_h, base_w, 3), dtype=np.uint8)
|
|
||||||
overlay_hud(rgb_panel, ["RGB", "sem frame"])
|
|
||||||
|
|
||||||
re01 = resize_if_needed(re01, (base_h, base_w)) if re01 is not None else None
|
# ------------------------------------------------------------
|
||||||
nir01 = resize_if_needed(nir01, (base_h, base_w)) if nir01 is not None else None
|
# Preview bonito, igual ao capture.
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
rgb_vis_id, rgb_vis = get_visual_preview_by_role(visual_previews, meta, "rgb")
|
||||||
|
re_vis_id, re_vis = get_visual_preview_by_role(visual_previews, meta, "re")
|
||||||
|
nir_vis_id, nir_vis = get_visual_preview_by_role(visual_previews, meta, "nir")
|
||||||
|
|
||||||
re_panel = gray_to_bgr_u8(re01) if re01 is not None else np.zeros_like(rgb_panel)
|
if beauty_preview and rgb_vis is not None:
|
||||||
nir_panel = gray_to_bgr_u8(nir01) if nir01 is not None else np.zeros_like(rgb_panel)
|
rgb_panel = rgb_vis.copy()
|
||||||
|
if rgb_panel.shape[:2] != (base_h, base_w):
|
||||||
|
rgb_panel = cv2.resize(rgb_panel, (base_w, base_h), interpolation=cv2.INTER_LINEAR)
|
||||||
|
rgb_id = rgb_vis_id
|
||||||
|
else:
|
||||||
|
if rgb01 is not None:
|
||||||
|
rgb_panel = to_bgr_u8_from_rgb01(rgb01)
|
||||||
|
else:
|
||||||
|
rgb_panel = np.zeros((base_h, base_w, 3), dtype=np.uint8)
|
||||||
|
overlay_hud(rgb_panel, ["RGB", "sem frame"])
|
||||||
|
|
||||||
for p in (rgb_panel, re_panel, nir_panel):
|
if beauty_preview and re_vis is not None:
|
||||||
draw_all_rois(p, data, selected_target, mode)
|
re_panel = re_vis.copy()
|
||||||
|
if re_panel.shape[:2] != (base_h, base_w):
|
||||||
|
re_panel = cv2.resize(re_panel, (base_w, base_h), interpolation=cv2.INTER_LINEAR)
|
||||||
|
re_id = re_vis_id
|
||||||
|
else:
|
||||||
|
re01_show = resize_if_needed(re01, (base_h, base_w)) if re01 is not None else None
|
||||||
|
re_panel = gray_to_bgr_u8(re01_show) if re01_show is not None else np.zeros_like(rgb_panel)
|
||||||
|
|
||||||
|
if beauty_preview and nir_vis is not None:
|
||||||
|
nir_panel = nir_vis.copy()
|
||||||
|
if nir_panel.shape[:2] != (base_h, base_w):
|
||||||
|
nir_panel = cv2.resize(nir_panel, (base_w, base_h), interpolation=cv2.INTER_LINEAR)
|
||||||
|
nir_id = nir_vis_id
|
||||||
|
else:
|
||||||
|
nir01_show = resize_if_needed(nir01, (base_h, base_w)) if nir01 is not None else None
|
||||||
|
nir_panel = gray_to_bgr_u8(nir01_show) if nir01_show is not None else np.zeros_like(rgb_panel)
|
||||||
|
|
||||||
|
draw_all_rois(rgb_panel, data, selected_target, mode, "rgb", edit_role)
|
||||||
|
draw_all_rois(re_panel, data, selected_target, mode, "re", edit_role)
|
||||||
|
draw_all_rois(nir_panel, data, selected_target, mode, "nir", edit_role)
|
||||||
|
|
||||||
if drag_rect_local is not None:
|
if drag_rect_local is not None:
|
||||||
x0, y0, x1, y1 = drag_rect_local
|
x0, y0, x1, y1 = drag_rect_local
|
||||||
color = PATCH_COLORS["global"] if mode == "global" else PATCH_COLORS.get(selected_target, (0, 255, 255))
|
color = PATCH_COLORS["global"] if mode == "global" else PATCH_COLORS.get(selected_target, (0, 255, 255))
|
||||||
for p in (rgb_panel, re_panel, nir_panel):
|
|
||||||
cv2.rectangle(p, (x0, y0), (x1, y1), color, 1)
|
if drag_role == "rgb":
|
||||||
|
cv2.rectangle(rgb_panel, (x0, y0), (x1, y1), color, 1)
|
||||||
|
elif drag_role == "re":
|
||||||
|
cv2.rectangle(re_panel, (x0, y0), (x1, y1), color, 1)
|
||||||
|
elif drag_role == "nir":
|
||||||
|
cv2.rectangle(nir_panel, (x0, y0), (x1, y1), color, 1)
|
||||||
|
|
||||||
overlay_hud(rgb_panel, [f"RGB ({rgb_id})"], y=24)
|
overlay_hud(rgb_panel, [f"RGB ({rgb_id})"], y=24)
|
||||||
overlay_hud(re_panel, [f"RE ({re_id})"], y=24)
|
overlay_hud(re_panel, [f"RE ({re_id})"], y=24)
|
||||||
|
|
@ -492,7 +760,7 @@ def build_board(decoded, data, mode, selected_target, drag_rect_local, panel_rec
|
||||||
board = np.vstack([top, bottom])
|
board = np.vstack([top, bottom])
|
||||||
|
|
||||||
x0, y0, x1, y1 = panel_rects["data"]
|
x0, y0, x1, y1 = panel_rects["data"]
|
||||||
lines = build_data_lines(decoded, data, mode, selected_target, base_w, base_h)
|
lines = build_data_lines(decoded, data, mode, selected_target, edit_role, base_w, base_h)
|
||||||
overlay_hud(board, lines, x=x0 + 16, y=y0 + 28, font_scale=0.53, line_step=21)
|
overlay_hud(board, lines, x=x0 + 16, y=y0 + 28, font_scale=0.53, line_step=21)
|
||||||
|
|
||||||
if preview_scale != 1.0:
|
if preview_scale != 1.0:
|
||||||
|
|
@ -505,51 +773,58 @@ def build_board(decoded, data, mode, selected_target, drag_rect_local, panel_rec
|
||||||
return board
|
return board
|
||||||
|
|
||||||
|
|
||||||
def build_data_lines(decoded, data, mode, selected_target, base_w, base_h):
|
def build_data_lines(decoded, data, mode, selected_target, edit_role, base_w, base_h):
|
||||||
shared = (
|
edit_role = normalize_role(edit_role)
|
||||||
data.get("global_scene_mode", {})
|
active_profile = get_active_profile_name(data)
|
||||||
.get("radiometric_config", {})
|
active_cfg = get_active_radiometric_config(data)
|
||||||
.get("spectral_control_mode", "shared")
|
|
||||||
)
|
|
||||||
|
|
||||||
lines = [
|
lines = [
|
||||||
"RADIOMETRIC CONFIG TOOL",
|
"RADIOMETRIC CONFIG TOOL",
|
||||||
f"modo_edição={mode.upper()} | spectral={shared}",
|
f"modo_edicao={mode.upper()} | camera_editada={edit_role.upper()} | active={active_profile}",
|
||||||
|
f"spectral={active_cfg.get('spectral_control_mode')} | strategy={active_cfg.get('control_strategy')}",
|
||||||
|
f"interval={active_cfg.get('interval_s')}s | ratio_alpha={active_cfg.get('ratio_alpha')} | ready={active_cfg.get('ready_required_cycles')}",
|
||||||
"",
|
"",
|
||||||
"Arraste com o mouse no painel RGB para definir ROI.",
|
"Arraste no painel da camera editada para definir a ROI.",
|
||||||
"A ROI é salva em percentuais e aplicada aos 3 sensores.",
|
"Cada camera salva sua propria ROI: RGB / RE / NIR.",
|
||||||
"",
|
"",
|
||||||
]
|
]
|
||||||
|
|
||||||
if mode == "global":
|
if mode == "global":
|
||||||
roi_pct = get_global_roi(data)
|
lines.append("GLOBAL ROI por camera:")
|
||||||
lines.append(f"GLOBAL ROI: {roi_pct}")
|
for role in ROLES:
|
||||||
lines.extend(stats_lines_for_roi(decoded, roi_pct, base_w, base_h))
|
roi_pct = get_global_roi_for_role(data, role)
|
||||||
|
marker = "*" if role == edit_role else " "
|
||||||
|
lines.append(f"{marker} {role.upper()}: roi={roi_pct}")
|
||||||
|
|
||||||
|
lines.append("")
|
||||||
|
lines.append("Stats GLOBAL:")
|
||||||
|
lines.extend(stats_lines_for_mode(data, decoded, mode="global", patch_type=None))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
lines.append(f"PATCH selecionado: {selected_target.upper()}")
|
lines.append(f"PATCH selecionado: {selected_target.upper()}")
|
||||||
for p in get_patches(data):
|
lines.append("ROIs do patch selecionado:")
|
||||||
typ = str(p.get("type", "")).lower()
|
for role in ROLES:
|
||||||
roi_pct = p.get("roi_pct", {})
|
roi_pct = get_patch_roi_for_role(data, selected_target, role)
|
||||||
lines.append(
|
marker = "*" if role == edit_role else " "
|
||||||
f"{typ}: target={float(p.get('target_value', 0.0)):.2f} "
|
lines.append(f"{marker} {role.upper()}: roi={roi_pct}")
|
||||||
f"weight={float(p.get('weight', 1.0)):.2f}"
|
|
||||||
)
|
|
||||||
lines.append(f" roi={roi_pct}")
|
|
||||||
|
|
||||||
sel_patch = None
|
|
||||||
for p in get_patches(data):
|
|
||||||
if str(p.get("type", "")).lower() == selected_target:
|
|
||||||
sel_patch = p
|
|
||||||
break
|
|
||||||
|
|
||||||
|
sel_patch = get_patch_by_type(data, selected_target)
|
||||||
if sel_patch:
|
if sel_patch:
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append(f"Stats do patch {selected_target.upper()}:")
|
lines.append(
|
||||||
lines.extend(stats_lines_for_roi(decoded, sel_patch.get("roi_pct", {}), base_w, base_h))
|
f"target={float(sel_patch.get('target_value', 0.0)):.2f} "
|
||||||
|
f"weight={float(sel_patch.get('weight', 1.0)):.2f}"
|
||||||
|
)
|
||||||
|
|
||||||
|
lines.append("")
|
||||||
|
lines.append(f"Stats do patch {selected_target.upper()}:")
|
||||||
|
lines.extend(stats_lines_for_mode(data, decoded, mode="patches", patch_type=selected_target))
|
||||||
|
|
||||||
lines.extend([
|
lines.extend([
|
||||||
"",
|
"",
|
||||||
"M = alterna GLOBAL / 3 PATCHES",
|
"M = alterna GLOBAL / 3 PATCHES",
|
||||||
|
"C = alterna camera RGB / RE / NIR",
|
||||||
|
"V = alterna preview bonito / bruto",
|
||||||
"1/2/3 = BLACK / GRAY / WHITE",
|
"1/2/3 = BLACK / GRAY / WHITE",
|
||||||
"S = alterna spectral shared/independent",
|
"S = alterna spectral shared/independent",
|
||||||
"P ou SPACE = salva JSON",
|
"P ou SPACE = salva JSON",
|
||||||
|
|
@ -559,17 +834,24 @@ def build_data_lines(decoded, data, mode, selected_target, base_w, base_h):
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
|
|
||||||
def stats_lines_for_roi(decoded, roi_pct, base_w, base_h):
|
def stats_lines_for_mode(data, decoded, mode: str, patch_type: str | None = None):
|
||||||
if not roi_pct:
|
|
||||||
return ["sem ROI"]
|
|
||||||
|
|
||||||
lines = []
|
lines = []
|
||||||
for role in ("rgb", "re", "nir"):
|
|
||||||
|
for role in ROLES:
|
||||||
_, img = get_image_by_role(decoded, role)
|
_, img = get_image_by_role(decoded, role)
|
||||||
if img is None:
|
if img is None:
|
||||||
lines.append(f"{role.upper()}: sem frame")
|
lines.append(f"{role.upper()}: sem frame")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if mode == "global":
|
||||||
|
roi_pct = get_global_roi_for_role(data, role)
|
||||||
|
else:
|
||||||
|
roi_pct = get_patch_roi_for_role(data, patch_type, role)
|
||||||
|
|
||||||
|
if not roi_pct:
|
||||||
|
lines.append(f"{role.upper()}: sem ROI")
|
||||||
|
continue
|
||||||
|
|
||||||
h, w = img.shape[:2]
|
h, w = img.shape[:2]
|
||||||
roi = pct_to_px(roi_pct, w, h)
|
roi = pct_to_px(roi_pct, w, h)
|
||||||
st = compute_stats(img, roi)
|
st = compute_stats(img, roi)
|
||||||
|
|
@ -622,6 +904,11 @@ def main():
|
||||||
|
|
||||||
mode = "global"
|
mode = "global"
|
||||||
selected_target = "gray"
|
selected_target = "gray"
|
||||||
|
edit_role = "rgb"
|
||||||
|
drag_role = None
|
||||||
|
beauty_preview = True
|
||||||
|
visual_previews_last = {}
|
||||||
|
raw_meta_last = {}
|
||||||
|
|
||||||
panel_rects = {"rgb": None, "re": None, "nir": None, "data": None}
|
panel_rects = {"rgb": None, "re": None, "nir": None, "data": None}
|
||||||
dragging = False
|
dragging = False
|
||||||
|
|
@ -636,21 +923,22 @@ def main():
|
||||||
window_name = "Radiometric Config Tool"
|
window_name = "Radiometric Config Tool"
|
||||||
|
|
||||||
def on_mouse(event, x, y, flags, param):
|
def on_mouse(event, x, y, flags, param):
|
||||||
nonlocal dragging, drag_start, drag_rect_local, last_msg, last_msg_t, data
|
nonlocal dragging, drag_start, drag_rect_local, last_msg, last_msg_t, data, drag_role
|
||||||
|
|
||||||
# Coordenadas vêm depois do preview_scale. Reescala para board real.
|
# Coordenadas vêm depois do preview_scale. Reescala para board real.
|
||||||
if args.preview_scale != 1.0:
|
if args.preview_scale != 1.0:
|
||||||
x = int(x / args.preview_scale)
|
x = int(x / args.preview_scale)
|
||||||
y = int(y / args.preview_scale)
|
y = int(y / args.preview_scale)
|
||||||
|
|
||||||
rgb_rect = panel_rects.get("rgb")
|
edit_rect = panel_rects.get(edit_role)
|
||||||
if not rect_inside(rgb_rect, x, y):
|
if not rect_inside(edit_rect, x, y):
|
||||||
return
|
return
|
||||||
|
|
||||||
lx, ly = local_from_rect(rgb_rect, x, y)
|
lx, ly = local_from_rect(edit_rect, x, y)
|
||||||
|
|
||||||
if event == cv2.EVENT_LBUTTONDOWN:
|
if event == cv2.EVENT_LBUTTONDOWN:
|
||||||
dragging = True
|
dragging = True
|
||||||
|
drag_role = edit_role
|
||||||
drag_start = (lx, ly)
|
drag_start = (lx, ly)
|
||||||
drag_rect_local = (lx, ly, lx + 1, ly + 1)
|
drag_rect_local = (lx, ly, lx + 1, ly + 1)
|
||||||
|
|
||||||
|
|
@ -664,24 +952,28 @@ def main():
|
||||||
rect = (sx, sy, lx, ly)
|
rect = (sx, sy, lx, ly)
|
||||||
drag_rect_local = None
|
drag_rect_local = None
|
||||||
|
|
||||||
# Descobre tamanho local do painel RGB.
|
# Descobre tamanho local do painel da camera editada.
|
||||||
if rgb_rect is None:
|
edit_rect = panel_rects.get(drag_role or edit_role)
|
||||||
|
if edit_rect is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
_, _, x1, y1 = rgb_rect
|
_, _, x1, y1 = edit_rect
|
||||||
x0r, y0r, _, _ = rgb_rect
|
x0r, y0r, _, _ = edit_rect
|
||||||
w = x1 - x0r
|
w = x1 - x0r
|
||||||
h = y1 - y0r
|
h = y1 - y0r
|
||||||
|
|
||||||
roi_pct = px_to_pct(rect, w, h)
|
roi_pct = px_to_pct(rect, w, h)
|
||||||
|
|
||||||
if mode == "global":
|
role_to_save = normalize_role(drag_role or edit_role)
|
||||||
set_global_roi(data, roi_pct)
|
|
||||||
last_msg = f"GLOBAL ROI atualizada: {roi_pct}"
|
|
||||||
else:
|
|
||||||
set_patch_roi(data, selected_target, roi_pct)
|
|
||||||
last_msg = f"{selected_target.upper()} ROI atualizada: {roi_pct}"
|
|
||||||
|
|
||||||
|
if mode == "global":
|
||||||
|
set_global_roi_for_role(data, role_to_save, roi_pct)
|
||||||
|
last_msg = f"GLOBAL ROI {role_to_save.upper()} atualizada: {roi_pct}"
|
||||||
|
else:
|
||||||
|
set_patch_roi_for_role(data, selected_target, role_to_save, roi_pct)
|
||||||
|
last_msg = f"{selected_target.upper()} ROI {role_to_save.upper()} atualizada: {roi_pct}"
|
||||||
|
|
||||||
|
drag_role = None
|
||||||
last_msg_t = time.time()
|
last_msg_t = time.time()
|
||||||
|
|
||||||
cv2.namedWindow(window_name, cv2.WINDOW_NORMAL)
|
cv2.namedWindow(window_name, cv2.WINDOW_NORMAL)
|
||||||
|
|
@ -704,10 +996,20 @@ def main():
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
raw_frame, raw_meta, decoded = cam.get_next_decoded(timeout=2.0)
|
raw_frame, raw_meta, decoded = cam.get_next_decoded(timeout=2.0)
|
||||||
|
visual_previews = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
if isinstance(raw_frame, dict):
|
||||||
|
visual_previews = cam.build_visual_preview_from_raw(raw_frame, raw_meta)
|
||||||
|
except Exception as e:
|
||||||
|
visual_previews = {}
|
||||||
|
print(f"[WARN] Falha ao gerar beauty preview: {e}")
|
||||||
|
|
||||||
if raw_meta is not None and raw_meta.get("frame_id") != last_frame_id:
|
if raw_meta is not None and raw_meta.get("frame_id") != last_frame_id:
|
||||||
last_frame_id = raw_meta.get("frame_id")
|
last_frame_id = raw_meta.get("frame_id")
|
||||||
decoded_last = decoded
|
decoded_last = decoded
|
||||||
|
visual_previews_last = visual_previews
|
||||||
|
raw_meta_last = raw_meta
|
||||||
|
|
||||||
if decoded_last:
|
if decoded_last:
|
||||||
board = build_board(
|
board = build_board(
|
||||||
|
|
@ -715,9 +1017,14 @@ def main():
|
||||||
data=data,
|
data=data,
|
||||||
mode=mode,
|
mode=mode,
|
||||||
selected_target=selected_target,
|
selected_target=selected_target,
|
||||||
|
edit_role=edit_role,
|
||||||
drag_rect_local=drag_rect_local,
|
drag_rect_local=drag_rect_local,
|
||||||
|
drag_role=drag_role,
|
||||||
panel_rects=panel_rects,
|
panel_rects=panel_rects,
|
||||||
preview_scale=args.preview_scale,
|
preview_scale=args.preview_scale,
|
||||||
|
visual_previews=visual_previews_last,
|
||||||
|
meta=raw_meta_last,
|
||||||
|
beauty_preview=beauty_preview,
|
||||||
)
|
)
|
||||||
|
|
||||||
if last_msg and (time.time() - last_msg_t) < 2.5:
|
if last_msg and (time.time() - last_msg_t) < 2.5:
|
||||||
|
|
@ -737,24 +1044,38 @@ def main():
|
||||||
|
|
||||||
elif k in (ord("m"), ord("M")):
|
elif k in (ord("m"), ord("M")):
|
||||||
mode = "patches" if mode == "global" else "global"
|
mode = "patches" if mode == "global" else "global"
|
||||||
last_msg = f"Modo -> {mode}"
|
|
||||||
|
if mode == "global":
|
||||||
|
set_active_profile_name(data, "global_scene_mode")
|
||||||
|
else:
|
||||||
|
set_active_profile_name(data, "three_reference_patches_mode")
|
||||||
|
|
||||||
|
update_root_radiometric_config(data)
|
||||||
|
|
||||||
|
last_msg = f"Modo -> {mode} | active_profile={data['active_profile']}"
|
||||||
last_msg_t = time.time()
|
last_msg_t = time.time()
|
||||||
|
|
||||||
elif k == ord("1"):
|
elif k == ord("1"):
|
||||||
mode = "patches"
|
mode = "patches"
|
||||||
selected_target = "black"
|
selected_target = "black"
|
||||||
|
set_active_profile_name(data, "three_reference_patches_mode")
|
||||||
|
update_root_radiometric_config(data)
|
||||||
last_msg = "Selecionado: BLACK"
|
last_msg = "Selecionado: BLACK"
|
||||||
last_msg_t = time.time()
|
last_msg_t = time.time()
|
||||||
|
|
||||||
elif k == ord("2"):
|
elif k == ord("2"):
|
||||||
mode = "patches"
|
mode = "patches"
|
||||||
selected_target = "gray"
|
selected_target = "gray"
|
||||||
|
set_active_profile_name(data, "three_reference_patches_mode")
|
||||||
|
update_root_radiometric_config(data)
|
||||||
last_msg = "Selecionado: GRAY"
|
last_msg = "Selecionado: GRAY"
|
||||||
last_msg_t = time.time()
|
last_msg_t = time.time()
|
||||||
|
|
||||||
elif k == ord("3"):
|
elif k == ord("3"):
|
||||||
mode = "patches"
|
mode = "patches"
|
||||||
selected_target = "white"
|
selected_target = "white"
|
||||||
|
set_active_profile_name(data, "three_reference_patches_mode")
|
||||||
|
update_root_radiometric_config(data)
|
||||||
last_msg = "Selecionado: WHITE"
|
last_msg = "Selecionado: WHITE"
|
||||||
last_msg_t = time.time()
|
last_msg_t = time.time()
|
||||||
|
|
||||||
|
|
@ -772,11 +1093,32 @@ def main():
|
||||||
|
|
||||||
elif k in (ord("r"), ord("R")):
|
elif k in (ord("r"), ord("R")):
|
||||||
data = {
|
data = {
|
||||||
"schema": "multispec_radiometric_config_profiles_v1",
|
"schema": "multispec_radiometric_config_profiles_v3",
|
||||||
"saved_at": now_str(),
|
"saved_at": now_str(),
|
||||||
|
"active_profile": "global_scene_mode",
|
||||||
"global_scene_mode": default_profile_global(),
|
"global_scene_mode": default_profile_global(),
|
||||||
"three_reference_patches_mode": default_profile_patches(),
|
"three_reference_patches_mode": default_profile_patches(),
|
||||||
|
"patch_normalization": {
|
||||||
|
"enabled": True,
|
||||||
|
"apply_when_metering_mode": "reference_patches",
|
||||||
|
"apply_stage": "after_fusion",
|
||||||
|
"method": "gray_scale_with_white_guard",
|
||||||
|
"space": "multispec_tensor",
|
||||||
|
"targets": {
|
||||||
|
"black": 0.06,
|
||||||
|
"gray": 0.40,
|
||||||
|
"white": 0.78
|
||||||
|
},
|
||||||
|
"white_guard_max": 0.92,
|
||||||
|
"scale_min": 0.35,
|
||||||
|
"scale_max": 2.50,
|
||||||
|
"clip_output": True,
|
||||||
|
"require_valid_gray": True,
|
||||||
|
"use_black_for_offset": False,
|
||||||
|
"save_patch_stats": True
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
update_root_radiometric_config(data)
|
||||||
last_msg = "Defaults restaurados"
|
last_msg = "Defaults restaurados"
|
||||||
last_msg_t = time.time()
|
last_msg_t = time.time()
|
||||||
|
|
||||||
|
|
@ -786,9 +1128,20 @@ def main():
|
||||||
last_msg_t = time.time()
|
last_msg_t = time.time()
|
||||||
print(f"[OK] radiometric config salvo em: {args.out_json}")
|
print(f"[OK] radiometric config salvo em: {args.out_json}")
|
||||||
|
|
||||||
|
elif k in (ord("c"), ord("C")):
|
||||||
|
idx = ROLES.index(edit_role) if edit_role in ROLES else 0
|
||||||
|
edit_role = ROLES[(idx + 1) % len(ROLES)]
|
||||||
|
last_msg = f"Camera editada -> {edit_role.upper()}"
|
||||||
|
last_msg_t = time.time()
|
||||||
|
|
||||||
|
elif k in (ord("v"), ord("V")):
|
||||||
|
beauty_preview = not beauty_preview
|
||||||
|
last_msg = f"Beauty Preview -> {beauty_preview}"
|
||||||
|
last_msg_t = time.time()
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
cv2.destroyAllWindows()
|
cv2.destroyAllWindows()
|
||||||
print("Fim da parametrização radiométrica.")
|
print("Fim da parametrizacao radiometrica.")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||