pequenos ajustes na calibragem e montagem do tensor
This commit is contained in:
parent
e850864607
commit
7c5c8b54ea
|
|
@ -407,7 +407,6 @@ def main():
|
|||
"camera_params_json": args.module_calibration_json,
|
||||
"note": "autosave",
|
||||
"raw_preview_reference_camera": preview_source_id,
|
||||
"patch_normalization_result": cam.get_last_patch_normalization_result()
|
||||
}
|
||||
|
||||
save_sample(
|
||||
|
|
@ -470,7 +469,6 @@ def main():
|
|||
"camera_params_json": args.module_calibration_json,
|
||||
"note": "manual",
|
||||
"raw_preview_reference_camera": preview_source_id,
|
||||
"patch_normalization_result": cam.get_last_patch_normalization_result()
|
||||
}
|
||||
|
||||
save_sample(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"schema": "manual_multispec_offsets_v2",
|
||||
"saved_at": "2026-05-06 09:54:59",
|
||||
"saved_at": "2026-05-08 09:17:15",
|
||||
"frame_type": "RAW_BRUTO",
|
||||
"capture_mode_requested": "AUTO",
|
||||
"capture_mode_effective": "AUTO",
|
||||
|
|
@ -26,35 +26,35 @@
|
|||
"homographies": {
|
||||
"re_to_rgb": [
|
||||
[
|
||||
0.9836077983170345,
|
||||
-0.00414576215755023,
|
||||
-7.995670153079967
|
||||
0.9891488988352489,
|
||||
-0.0057973762346762115,
|
||||
-15.173511767385348
|
||||
],
|
||||
[
|
||||
-0.009155951570825544,
|
||||
0.9872093474367271,
|
||||
65.61737450021248
|
||||
-0.015114489970267686,
|
||||
0.9903282034674085,
|
||||
58.93120393758602
|
||||
],
|
||||
[
|
||||
-8.515127189657688e-06,
|
||||
-3.4530877754475106e-06,
|
||||
-2.1528138783460464e-05,
|
||||
-1.5606269505459822e-05,
|
||||
1.0
|
||||
]
|
||||
],
|
||||
"nir_to_rgb": [
|
||||
[
|
||||
0.9950840771095427,
|
||||
-0.01203311522937068,
|
||||
-0.37678489564225515
|
||||
1.015255211995985,
|
||||
-0.011641307565439515,
|
||||
-15.440297021288165
|
||||
],
|
||||
[
|
||||
0.005818513521498549,
|
||||
0.9844634830519817,
|
||||
40.58679770900771
|
||||
0.006224807373165827,
|
||||
1.0059040970450264,
|
||||
21.05815660535078
|
||||
],
|
||||
[
|
||||
1.1689856045746963e-05,
|
||||
-1.643688958474221e-05,
|
||||
5.830335800966281e-06,
|
||||
-1.5297165752796802e-05,
|
||||
1.0
|
||||
]
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"schema": "multispec_module_params_v3",
|
||||
"saved_at": "2026-05-07 14:41:12",
|
||||
"saved_at": "2026-05-08 09:23:01",
|
||||
"frame_type": "RAW_BRUTO",
|
||||
"capture_mode_requested": "AUTO",
|
||||
"capture_mode_effective": "AUTO",
|
||||
|
|
@ -53,35 +53,35 @@
|
|||
"homographies": {
|
||||
"re_to_rgb": [
|
||||
[
|
||||
0.9836077983170345,
|
||||
-0.00414576215755023,
|
||||
-7.995670153079967
|
||||
0.9891488988352489,
|
||||
-0.0057973762346762115,
|
||||
-15.173511767385348
|
||||
],
|
||||
[
|
||||
-0.009155951570825544,
|
||||
0.9872093474367271,
|
||||
65.61737450021248
|
||||
-0.015114489970267686,
|
||||
0.9903282034674085,
|
||||
58.93120393758602
|
||||
],
|
||||
[
|
||||
-8.515127189657688e-06,
|
||||
-3.4530877754475106e-06,
|
||||
-2.1528138783460464e-05,
|
||||
-1.5606269505459822e-05,
|
||||
1.0
|
||||
]
|
||||
],
|
||||
"nir_to_rgb": [
|
||||
[
|
||||
0.9950840771095427,
|
||||
-0.01203311522937068,
|
||||
-0.37678489564225515
|
||||
1.015255211995985,
|
||||
-0.011641307565439515,
|
||||
-15.440297021288165
|
||||
],
|
||||
[
|
||||
0.005818513521498549,
|
||||
0.9844634830519817,
|
||||
40.58679770900771
|
||||
0.006224807373165827,
|
||||
1.0059040970450264,
|
||||
21.05815660535078
|
||||
],
|
||||
[
|
||||
1.1689856045746963e-05,
|
||||
-1.643688958474221e-05,
|
||||
5.830335800966281e-06,
|
||||
-1.5297165752796802e-05,
|
||||
1.0
|
||||
]
|
||||
]
|
||||
|
|
@ -92,7 +92,7 @@
|
|||
},
|
||||
"radiometric_config": {
|
||||
"enabled": true,
|
||||
"interval_s": 0.5,
|
||||
"interval_s": 0.25,
|
||||
"verbose": true,
|
||||
"metering_mode": "reference_patches",
|
||||
"spectral_control_mode": "shared",
|
||||
|
|
@ -118,31 +118,31 @@
|
|||
"nir"
|
||||
],
|
||||
"roi_pct": {
|
||||
"x0": 0.376563,
|
||||
"y0": 0.81,
|
||||
"x1": 0.440625,
|
||||
"y1": 0.985
|
||||
"x0": 0.365625,
|
||||
"y0": 0.8225,
|
||||
"x1": 0.432812,
|
||||
"y1": 0.995
|
||||
},
|
||||
"target_value": 0.08,
|
||||
"weight": 0.7,
|
||||
"roi_pct_by_role": {
|
||||
"rgb": {
|
||||
"x0": 0.376563,
|
||||
"y0": 0.81,
|
||||
"x1": 0.440625,
|
||||
"y1": 0.985
|
||||
"x0": 0.365625,
|
||||
"y0": 0.8225,
|
||||
"x1": 0.432812,
|
||||
"y1": 0.995
|
||||
},
|
||||
"re": {
|
||||
"x0": 0.403125,
|
||||
"x0": 0.395313,
|
||||
"y0": 0.745,
|
||||
"x1": 0.459375,
|
||||
"y1": 0.92
|
||||
"x1": 0.4625,
|
||||
"y1": 0.9225
|
||||
},
|
||||
"nir": {
|
||||
"x0": 0.364063,
|
||||
"y0": 0.775,
|
||||
"x1": 0.428125,
|
||||
"y1": 0.9425
|
||||
"x0": 0.353125,
|
||||
"y0": 0.78,
|
||||
"x1": 0.420312,
|
||||
"y1": 0.9525
|
||||
}
|
||||
},
|
||||
"roi_list_by_role": {
|
||||
|
|
@ -151,13 +151,13 @@
|
|||
"name": "rgb_legacy_01",
|
||||
"enabled": true,
|
||||
"roi_pct": {
|
||||
"x0": 0.376563,
|
||||
"y0": 0.81,
|
||||
"x1": 0.440625,
|
||||
"y1": 0.985
|
||||
"x0": 0.365625,
|
||||
"y0": 0.8225,
|
||||
"x1": 0.432812,
|
||||
"y1": 0.995
|
||||
},
|
||||
"created_at": "2026-05-07 14:36:25",
|
||||
"updated_at": "2026-05-07 14:39:47"
|
||||
"updated_at": "2026-05-08 09:19:46"
|
||||
}
|
||||
],
|
||||
"re": [
|
||||
|
|
@ -165,13 +165,13 @@
|
|||
"name": "re_legacy_01",
|
||||
"enabled": true,
|
||||
"roi_pct": {
|
||||
"x0": 0.403125,
|
||||
"x0": 0.395313,
|
||||
"y0": 0.745,
|
||||
"x1": 0.459375,
|
||||
"y1": 0.92
|
||||
"x1": 0.4625,
|
||||
"y1": 0.9225
|
||||
},
|
||||
"created_at": "2026-05-07 14:36:25",
|
||||
"updated_at": "2026-05-07 14:40:05"
|
||||
"updated_at": "2026-05-08 09:20:14"
|
||||
}
|
||||
],
|
||||
"nir": [
|
||||
|
|
@ -179,13 +179,13 @@
|
|||
"name": "nir_legacy_01",
|
||||
"enabled": true,
|
||||
"roi_pct": {
|
||||
"x0": 0.364063,
|
||||
"y0": 0.775,
|
||||
"x1": 0.428125,
|
||||
"y1": 0.9425
|
||||
"x0": 0.353125,
|
||||
"y0": 0.78,
|
||||
"x1": 0.420312,
|
||||
"y1": 0.9525
|
||||
},
|
||||
"created_at": "2026-05-07 14:36:25",
|
||||
"updated_at": "2026-05-07 14:40:37"
|
||||
"updated_at": "2026-05-08 09:20:47"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -199,12 +199,12 @@
|
|||
"nir"
|
||||
],
|
||||
"roi_pct": {
|
||||
"x0": 0.30625,
|
||||
"y0": 0.8225,
|
||||
"x1": 0.370312,
|
||||
"y1": 0.985
|
||||
"x0": 0.29375,
|
||||
"y0": 0.825,
|
||||
"x1": 0.3625,
|
||||
"y1": 0.995
|
||||
},
|
||||
"target_value": 0.5,
|
||||
"target_value": 0.35,
|
||||
"target_value_by_role": {
|
||||
"rgb": 0.34,
|
||||
"re": 0.24,
|
||||
|
|
@ -213,22 +213,22 @@
|
|||
"weight": 1.0,
|
||||
"roi_pct_by_role": {
|
||||
"rgb": {
|
||||
"x0": 0.30625,
|
||||
"y0": 0.8225,
|
||||
"x1": 0.370312,
|
||||
"y1": 0.985
|
||||
"x0": 0.29375,
|
||||
"y0": 0.825,
|
||||
"x1": 0.3625,
|
||||
"y1": 0.995
|
||||
},
|
||||
"re": {
|
||||
"x0": 0.332813,
|
||||
"y0": 0.745,
|
||||
"x1": 0.395313,
|
||||
"x0": 0.325,
|
||||
"y0": 0.75,
|
||||
"x1": 0.389062,
|
||||
"y1": 0.915
|
||||
},
|
||||
"nir": {
|
||||
"x0": 0.29375,
|
||||
"y0": 0.775,
|
||||
"x1": 0.35625,
|
||||
"y1": 0.94
|
||||
"x0": 0.284375,
|
||||
"y0": 0.79,
|
||||
"x1": 0.35,
|
||||
"y1": 0.9525
|
||||
}
|
||||
},
|
||||
"roi_list_by_role": {
|
||||
|
|
@ -237,13 +237,13 @@
|
|||
"name": "rgb_legacy_01",
|
||||
"enabled": true,
|
||||
"roi_pct": {
|
||||
"x0": 0.30625,
|
||||
"y0": 0.8225,
|
||||
"x1": 0.370312,
|
||||
"y1": 0.985
|
||||
"x0": 0.29375,
|
||||
"y0": 0.825,
|
||||
"x1": 0.3625,
|
||||
"y1": 0.995
|
||||
},
|
||||
"created_at": "2026-05-07 14:36:25",
|
||||
"updated_at": "2026-05-07 14:39:15"
|
||||
"updated_at": "2026-05-08 09:19:29"
|
||||
}
|
||||
],
|
||||
"re": [
|
||||
|
|
@ -251,13 +251,13 @@
|
|||
"name": "re_legacy_01",
|
||||
"enabled": true,
|
||||
"roi_pct": {
|
||||
"x0": 0.332813,
|
||||
"y0": 0.745,
|
||||
"x1": 0.395313,
|
||||
"x0": 0.325,
|
||||
"y0": 0.75,
|
||||
"x1": 0.389062,
|
||||
"y1": 0.915
|
||||
},
|
||||
"created_at": "2026-05-07 14:36:25",
|
||||
"updated_at": "2026-05-07 14:40:15"
|
||||
"updated_at": "2026-05-08 09:20:22"
|
||||
}
|
||||
],
|
||||
"nir": [
|
||||
|
|
@ -265,13 +265,13 @@
|
|||
"name": "nir_legacy_01",
|
||||
"enabled": true,
|
||||
"roi_pct": {
|
||||
"x0": 0.29375,
|
||||
"y0": 0.775,
|
||||
"x1": 0.35625,
|
||||
"y1": 0.94
|
||||
"x0": 0.284375,
|
||||
"y0": 0.79,
|
||||
"x1": 0.35,
|
||||
"y1": 0.9525
|
||||
},
|
||||
"created_at": "2026-05-07 14:36:25",
|
||||
"updated_at": "2026-05-07 14:40:46"
|
||||
"updated_at": "2026-05-08 09:20:53"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -285,31 +285,31 @@
|
|||
"nir"
|
||||
],
|
||||
"roi_pct": {
|
||||
"x0": 0.232813,
|
||||
"x0": 0.220312,
|
||||
"y0": 0.8225,
|
||||
"x1": 0.3,
|
||||
"y1": 0.99
|
||||
"x1": 0.2875,
|
||||
"y1": 0.995
|
||||
},
|
||||
"target_value": 0.82,
|
||||
"weight": 0.8,
|
||||
"roi_pct_by_role": {
|
||||
"rgb": {
|
||||
"x0": 0.232813,
|
||||
"x0": 0.220312,
|
||||
"y0": 0.8225,
|
||||
"x1": 0.3,
|
||||
"y1": 0.99
|
||||
"x1": 0.2875,
|
||||
"y1": 0.995
|
||||
},
|
||||
"re": {
|
||||
"x0": 0.2625,
|
||||
"x0": 0.25,
|
||||
"y0": 0.7525,
|
||||
"x1": 0.321875,
|
||||
"x1": 0.315625,
|
||||
"y1": 0.9225
|
||||
},
|
||||
"nir": {
|
||||
"x0": 0.220312,
|
||||
"y0": 0.775,
|
||||
"x1": 0.284375,
|
||||
"y1": 0.9425
|
||||
"x0": 0.214062,
|
||||
"y0": 0.785,
|
||||
"x1": 0.282813,
|
||||
"y1": 0.9525
|
||||
}
|
||||
},
|
||||
"roi_list_by_role": {
|
||||
|
|
@ -318,13 +318,13 @@
|
|||
"name": "rgb_legacy_01",
|
||||
"enabled": true,
|
||||
"roi_pct": {
|
||||
"x0": 0.232813,
|
||||
"x0": 0.220312,
|
||||
"y0": 0.8225,
|
||||
"x1": 0.3,
|
||||
"y1": 0.99
|
||||
"x1": 0.2875,
|
||||
"y1": 0.995
|
||||
},
|
||||
"created_at": "2026-05-07 14:36:25",
|
||||
"updated_at": "2026-05-07 14:39:38"
|
||||
"updated_at": "2026-05-08 09:19:04"
|
||||
}
|
||||
],
|
||||
"re": [
|
||||
|
|
@ -332,13 +332,13 @@
|
|||
"name": "re_legacy_01",
|
||||
"enabled": true,
|
||||
"roi_pct": {
|
||||
"x0": 0.2625,
|
||||
"x0": 0.25,
|
||||
"y0": 0.7525,
|
||||
"x1": 0.321875,
|
||||
"x1": 0.315625,
|
||||
"y1": 0.9225
|
||||
},
|
||||
"created_at": "2026-05-07 14:36:25",
|
||||
"updated_at": "2026-05-07 14:40:24"
|
||||
"updated_at": "2026-05-08 09:20:27"
|
||||
}
|
||||
],
|
||||
"nir": [
|
||||
|
|
@ -346,13 +346,13 @@
|
|||
"name": "nir_legacy_01",
|
||||
"enabled": true,
|
||||
"roi_pct": {
|
||||
"x0": 0.220312,
|
||||
"y0": 0.775,
|
||||
"x1": 0.284375,
|
||||
"y1": 0.9425
|
||||
"x0": 0.214062,
|
||||
"y0": 0.785,
|
||||
"x1": 0.282813,
|
||||
"y1": 0.9525
|
||||
},
|
||||
"created_at": "2026-05-07 14:36:25",
|
||||
"updated_at": "2026-05-07 14:40:55"
|
||||
"updated_at": "2026-05-08 09:21:02"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -418,7 +418,7 @@
|
|||
"patch_roi_max_p50_delta": 0.12
|
||||
},
|
||||
"radiometric_normalization": {
|
||||
"enabled": true,
|
||||
"enabled": false,
|
||||
"method": "exposure_gain_reference",
|
||||
"apply_stage": "after_dark_before_flat_gain",
|
||||
"reference_controls": {
|
||||
|
|
@ -465,7 +465,7 @@
|
|||
}
|
||||
},
|
||||
"flatfield_config": {
|
||||
"enabled": true,
|
||||
"enabled": false,
|
||||
"subtract_dark": true,
|
||||
"schema": "multispec_flatfield_v1",
|
||||
"created_at": "2026-05-06 13:37:25",
|
||||
|
|
|
|||
|
|
@ -120,31 +120,31 @@
|
|||
"nir"
|
||||
],
|
||||
"roi_pct": {
|
||||
"x0": 0.376563,
|
||||
"y0": 0.81,
|
||||
"x1": 0.440625,
|
||||
"y1": 0.985
|
||||
"x0": 0.365625,
|
||||
"y0": 0.8225,
|
||||
"x1": 0.432812,
|
||||
"y1": 0.995
|
||||
},
|
||||
"target_value": 0.08,
|
||||
"weight": 0.7,
|
||||
"roi_pct_by_role": {
|
||||
"rgb": {
|
||||
"x0": 0.376563,
|
||||
"y0": 0.81,
|
||||
"x1": 0.440625,
|
||||
"y1": 0.985
|
||||
"x0": 0.365625,
|
||||
"y0": 0.8225,
|
||||
"x1": 0.432812,
|
||||
"y1": 0.995
|
||||
},
|
||||
"re": {
|
||||
"x0": 0.403125,
|
||||
"x0": 0.395313,
|
||||
"y0": 0.745,
|
||||
"x1": 0.459375,
|
||||
"y1": 0.92
|
||||
"x1": 0.4625,
|
||||
"y1": 0.9225
|
||||
},
|
||||
"nir": {
|
||||
"x0": 0.364063,
|
||||
"y0": 0.775,
|
||||
"x1": 0.428125,
|
||||
"y1": 0.9425
|
||||
"x0": 0.353125,
|
||||
"y0": 0.78,
|
||||
"x1": 0.420312,
|
||||
"y1": 0.9525
|
||||
}
|
||||
},
|
||||
"roi_list_by_role": {
|
||||
|
|
@ -153,13 +153,13 @@
|
|||
"name": "rgb_legacy_01",
|
||||
"enabled": true,
|
||||
"roi_pct": {
|
||||
"x0": 0.376563,
|
||||
"y0": 0.81,
|
||||
"x1": 0.440625,
|
||||
"y1": 0.985
|
||||
"x0": 0.365625,
|
||||
"y0": 0.8225,
|
||||
"x1": 0.432812,
|
||||
"y1": 0.995
|
||||
},
|
||||
"created_at": "2026-05-07 14:36:25",
|
||||
"updated_at": "2026-05-07 14:39:47"
|
||||
"updated_at": "2026-05-08 09:19:46"
|
||||
}
|
||||
],
|
||||
"re": [
|
||||
|
|
@ -167,13 +167,13 @@
|
|||
"name": "re_legacy_01",
|
||||
"enabled": true,
|
||||
"roi_pct": {
|
||||
"x0": 0.403125,
|
||||
"x0": 0.395313,
|
||||
"y0": 0.745,
|
||||
"x1": 0.459375,
|
||||
"y1": 0.92
|
||||
"x1": 0.4625,
|
||||
"y1": 0.9225
|
||||
},
|
||||
"created_at": "2026-05-07 14:36:25",
|
||||
"updated_at": "2026-05-07 14:40:05"
|
||||
"updated_at": "2026-05-08 09:20:14"
|
||||
}
|
||||
],
|
||||
"nir": [
|
||||
|
|
@ -181,13 +181,13 @@
|
|||
"name": "nir_legacy_01",
|
||||
"enabled": true,
|
||||
"roi_pct": {
|
||||
"x0": 0.364063,
|
||||
"y0": 0.775,
|
||||
"x1": 0.428125,
|
||||
"y1": 0.9425
|
||||
"x0": 0.353125,
|
||||
"y0": 0.78,
|
||||
"x1": 0.420312,
|
||||
"y1": 0.9525
|
||||
},
|
||||
"created_at": "2026-05-07 14:36:25",
|
||||
"updated_at": "2026-05-07 14:40:37"
|
||||
"updated_at": "2026-05-08 09:20:47"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -201,31 +201,31 @@
|
|||
"nir"
|
||||
],
|
||||
"roi_pct": {
|
||||
"x0": 0.30625,
|
||||
"y0": 0.8225,
|
||||
"x1": 0.370312,
|
||||
"y1": 0.985
|
||||
"x0": 0.29375,
|
||||
"y0": 0.825,
|
||||
"x1": 0.3625,
|
||||
"y1": 0.995
|
||||
},
|
||||
"target_value": 0.5,
|
||||
"weight": 1.0,
|
||||
"roi_pct_by_role": {
|
||||
"rgb": {
|
||||
"x0": 0.30625,
|
||||
"y0": 0.8225,
|
||||
"x1": 0.370312,
|
||||
"y1": 0.985
|
||||
"x0": 0.29375,
|
||||
"y0": 0.825,
|
||||
"x1": 0.3625,
|
||||
"y1": 0.995
|
||||
},
|
||||
"re": {
|
||||
"x0": 0.332813,
|
||||
"y0": 0.745,
|
||||
"x1": 0.395313,
|
||||
"x0": 0.325,
|
||||
"y0": 0.75,
|
||||
"x1": 0.389062,
|
||||
"y1": 0.915
|
||||
},
|
||||
"nir": {
|
||||
"x0": 0.29375,
|
||||
"y0": 0.775,
|
||||
"x1": 0.35625,
|
||||
"y1": 0.94
|
||||
"x0": 0.284375,
|
||||
"y0": 0.79,
|
||||
"x1": 0.35,
|
||||
"y1": 0.9525
|
||||
}
|
||||
},
|
||||
"roi_list_by_role": {
|
||||
|
|
@ -234,13 +234,13 @@
|
|||
"name": "rgb_legacy_01",
|
||||
"enabled": true,
|
||||
"roi_pct": {
|
||||
"x0": 0.30625,
|
||||
"y0": 0.8225,
|
||||
"x1": 0.370312,
|
||||
"y1": 0.985
|
||||
"x0": 0.29375,
|
||||
"y0": 0.825,
|
||||
"x1": 0.3625,
|
||||
"y1": 0.995
|
||||
},
|
||||
"created_at": "2026-05-07 14:36:25",
|
||||
"updated_at": "2026-05-07 14:39:15"
|
||||
"updated_at": "2026-05-08 09:19:29"
|
||||
}
|
||||
],
|
||||
"re": [
|
||||
|
|
@ -248,13 +248,13 @@
|
|||
"name": "re_legacy_01",
|
||||
"enabled": true,
|
||||
"roi_pct": {
|
||||
"x0": 0.332813,
|
||||
"y0": 0.745,
|
||||
"x1": 0.395313,
|
||||
"x0": 0.325,
|
||||
"y0": 0.75,
|
||||
"x1": 0.389062,
|
||||
"y1": 0.915
|
||||
},
|
||||
"created_at": "2026-05-07 14:36:25",
|
||||
"updated_at": "2026-05-07 14:40:15"
|
||||
"updated_at": "2026-05-08 09:20:22"
|
||||
}
|
||||
],
|
||||
"nir": [
|
||||
|
|
@ -262,13 +262,13 @@
|
|||
"name": "nir_legacy_01",
|
||||
"enabled": true,
|
||||
"roi_pct": {
|
||||
"x0": 0.29375,
|
||||
"y0": 0.775,
|
||||
"x1": 0.35625,
|
||||
"y1": 0.94
|
||||
"x0": 0.284375,
|
||||
"y0": 0.79,
|
||||
"x1": 0.35,
|
||||
"y1": 0.9525
|
||||
},
|
||||
"created_at": "2026-05-07 14:36:25",
|
||||
"updated_at": "2026-05-07 14:40:46"
|
||||
"updated_at": "2026-05-08 09:20:53"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -282,31 +282,31 @@
|
|||
"nir"
|
||||
],
|
||||
"roi_pct": {
|
||||
"x0": 0.232813,
|
||||
"x0": 0.220312,
|
||||
"y0": 0.8225,
|
||||
"x1": 0.3,
|
||||
"y1": 0.99
|
||||
"x1": 0.2875,
|
||||
"y1": 0.995
|
||||
},
|
||||
"target_value": 0.82,
|
||||
"weight": 0.8,
|
||||
"roi_pct_by_role": {
|
||||
"rgb": {
|
||||
"x0": 0.232813,
|
||||
"x0": 0.220312,
|
||||
"y0": 0.8225,
|
||||
"x1": 0.3,
|
||||
"y1": 0.99
|
||||
"x1": 0.2875,
|
||||
"y1": 0.995
|
||||
},
|
||||
"re": {
|
||||
"x0": 0.2625,
|
||||
"x0": 0.25,
|
||||
"y0": 0.7525,
|
||||
"x1": 0.321875,
|
||||
"x1": 0.315625,
|
||||
"y1": 0.9225
|
||||
},
|
||||
"nir": {
|
||||
"x0": 0.220312,
|
||||
"y0": 0.775,
|
||||
"x1": 0.284375,
|
||||
"y1": 0.9425
|
||||
"x0": 0.214062,
|
||||
"y0": 0.785,
|
||||
"x1": 0.282813,
|
||||
"y1": 0.9525
|
||||
}
|
||||
},
|
||||
"roi_list_by_role": {
|
||||
|
|
@ -315,13 +315,13 @@
|
|||
"name": "rgb_legacy_01",
|
||||
"enabled": true,
|
||||
"roi_pct": {
|
||||
"x0": 0.232813,
|
||||
"x0": 0.220312,
|
||||
"y0": 0.8225,
|
||||
"x1": 0.3,
|
||||
"y1": 0.99
|
||||
"x1": 0.2875,
|
||||
"y1": 0.995
|
||||
},
|
||||
"created_at": "2026-05-07 14:36:25",
|
||||
"updated_at": "2026-05-07 14:39:38"
|
||||
"updated_at": "2026-05-08 09:19:04"
|
||||
}
|
||||
],
|
||||
"re": [
|
||||
|
|
@ -329,13 +329,13 @@
|
|||
"name": "re_legacy_01",
|
||||
"enabled": true,
|
||||
"roi_pct": {
|
||||
"x0": 0.2625,
|
||||
"x0": 0.25,
|
||||
"y0": 0.7525,
|
||||
"x1": 0.321875,
|
||||
"x1": 0.315625,
|
||||
"y1": 0.9225
|
||||
},
|
||||
"created_at": "2026-05-07 14:36:25",
|
||||
"updated_at": "2026-05-07 14:40:24"
|
||||
"updated_at": "2026-05-08 09:20:27"
|
||||
}
|
||||
],
|
||||
"nir": [
|
||||
|
|
@ -343,13 +343,13 @@
|
|||
"name": "nir_legacy_01",
|
||||
"enabled": true,
|
||||
"roi_pct": {
|
||||
"x0": 0.220312,
|
||||
"y0": 0.775,
|
||||
"x1": 0.284375,
|
||||
"y1": 0.9425
|
||||
"x0": 0.214062,
|
||||
"y0": 0.785,
|
||||
"x1": 0.282813,
|
||||
"y1": 0.9525
|
||||
},
|
||||
"created_at": "2026-05-07 14:36:25",
|
||||
"updated_at": "2026-05-07 14:40:55"
|
||||
"updated_at": "2026-05-08 09:21:02"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -439,7 +439,7 @@
|
|||
}
|
||||
},
|
||||
"schema": "multispec_radiometric_config_profiles_v3",
|
||||
"saved_at": "2026-05-07 14:40:58",
|
||||
"saved_at": "2026-05-08 09:21:09",
|
||||
"active_profile": "three_reference_patches_mode",
|
||||
"patch_normalization": {
|
||||
"enabled": true,
|
||||
|
|
@ -488,31 +488,31 @@
|
|||
"nir"
|
||||
],
|
||||
"roi_pct": {
|
||||
"x0": 0.376563,
|
||||
"y0": 0.81,
|
||||
"x1": 0.440625,
|
||||
"y1": 0.985
|
||||
"x0": 0.365625,
|
||||
"y0": 0.8225,
|
||||
"x1": 0.432812,
|
||||
"y1": 0.995
|
||||
},
|
||||
"target_value": 0.08,
|
||||
"weight": 0.7,
|
||||
"roi_pct_by_role": {
|
||||
"rgb": {
|
||||
"x0": 0.376563,
|
||||
"y0": 0.81,
|
||||
"x1": 0.440625,
|
||||
"y1": 0.985
|
||||
"x0": 0.365625,
|
||||
"y0": 0.8225,
|
||||
"x1": 0.432812,
|
||||
"y1": 0.995
|
||||
},
|
||||
"re": {
|
||||
"x0": 0.403125,
|
||||
"x0": 0.395313,
|
||||
"y0": 0.745,
|
||||
"x1": 0.459375,
|
||||
"y1": 0.92
|
||||
"x1": 0.4625,
|
||||
"y1": 0.9225
|
||||
},
|
||||
"nir": {
|
||||
"x0": 0.364063,
|
||||
"y0": 0.775,
|
||||
"x1": 0.428125,
|
||||
"y1": 0.9425
|
||||
"x0": 0.353125,
|
||||
"y0": 0.78,
|
||||
"x1": 0.420312,
|
||||
"y1": 0.9525
|
||||
}
|
||||
},
|
||||
"roi_list_by_role": {
|
||||
|
|
@ -521,13 +521,13 @@
|
|||
"name": "rgb_legacy_01",
|
||||
"enabled": true,
|
||||
"roi_pct": {
|
||||
"x0": 0.376563,
|
||||
"y0": 0.81,
|
||||
"x1": 0.440625,
|
||||
"y1": 0.985
|
||||
"x0": 0.365625,
|
||||
"y0": 0.8225,
|
||||
"x1": 0.432812,
|
||||
"y1": 0.995
|
||||
},
|
||||
"created_at": "2026-05-07 14:36:25",
|
||||
"updated_at": "2026-05-07 14:39:47"
|
||||
"updated_at": "2026-05-08 09:19:46"
|
||||
}
|
||||
],
|
||||
"re": [
|
||||
|
|
@ -535,13 +535,13 @@
|
|||
"name": "re_legacy_01",
|
||||
"enabled": true,
|
||||
"roi_pct": {
|
||||
"x0": 0.403125,
|
||||
"x0": 0.395313,
|
||||
"y0": 0.745,
|
||||
"x1": 0.459375,
|
||||
"y1": 0.92
|
||||
"x1": 0.4625,
|
||||
"y1": 0.9225
|
||||
},
|
||||
"created_at": "2026-05-07 14:36:25",
|
||||
"updated_at": "2026-05-07 14:40:05"
|
||||
"updated_at": "2026-05-08 09:20:14"
|
||||
}
|
||||
],
|
||||
"nir": [
|
||||
|
|
@ -549,13 +549,13 @@
|
|||
"name": "nir_legacy_01",
|
||||
"enabled": true,
|
||||
"roi_pct": {
|
||||
"x0": 0.364063,
|
||||
"y0": 0.775,
|
||||
"x1": 0.428125,
|
||||
"y1": 0.9425
|
||||
"x0": 0.353125,
|
||||
"y0": 0.78,
|
||||
"x1": 0.420312,
|
||||
"y1": 0.9525
|
||||
},
|
||||
"created_at": "2026-05-07 14:36:25",
|
||||
"updated_at": "2026-05-07 14:40:37"
|
||||
"updated_at": "2026-05-08 09:20:47"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -569,31 +569,31 @@
|
|||
"nir"
|
||||
],
|
||||
"roi_pct": {
|
||||
"x0": 0.30625,
|
||||
"y0": 0.8225,
|
||||
"x1": 0.370312,
|
||||
"y1": 0.985
|
||||
"x0": 0.29375,
|
||||
"y0": 0.825,
|
||||
"x1": 0.3625,
|
||||
"y1": 0.995
|
||||
},
|
||||
"target_value": 0.5,
|
||||
"weight": 1.0,
|
||||
"roi_pct_by_role": {
|
||||
"rgb": {
|
||||
"x0": 0.30625,
|
||||
"y0": 0.8225,
|
||||
"x1": 0.370312,
|
||||
"y1": 0.985
|
||||
"x0": 0.29375,
|
||||
"y0": 0.825,
|
||||
"x1": 0.3625,
|
||||
"y1": 0.995
|
||||
},
|
||||
"re": {
|
||||
"x0": 0.332813,
|
||||
"y0": 0.745,
|
||||
"x1": 0.395313,
|
||||
"x0": 0.325,
|
||||
"y0": 0.75,
|
||||
"x1": 0.389062,
|
||||
"y1": 0.915
|
||||
},
|
||||
"nir": {
|
||||
"x0": 0.29375,
|
||||
"y0": 0.775,
|
||||
"x1": 0.35625,
|
||||
"y1": 0.94
|
||||
"x0": 0.284375,
|
||||
"y0": 0.79,
|
||||
"x1": 0.35,
|
||||
"y1": 0.9525
|
||||
}
|
||||
},
|
||||
"roi_list_by_role": {
|
||||
|
|
@ -602,13 +602,13 @@
|
|||
"name": "rgb_legacy_01",
|
||||
"enabled": true,
|
||||
"roi_pct": {
|
||||
"x0": 0.30625,
|
||||
"y0": 0.8225,
|
||||
"x1": 0.370312,
|
||||
"y1": 0.985
|
||||
"x0": 0.29375,
|
||||
"y0": 0.825,
|
||||
"x1": 0.3625,
|
||||
"y1": 0.995
|
||||
},
|
||||
"created_at": "2026-05-07 14:36:25",
|
||||
"updated_at": "2026-05-07 14:39:15"
|
||||
"updated_at": "2026-05-08 09:19:29"
|
||||
}
|
||||
],
|
||||
"re": [
|
||||
|
|
@ -616,13 +616,13 @@
|
|||
"name": "re_legacy_01",
|
||||
"enabled": true,
|
||||
"roi_pct": {
|
||||
"x0": 0.332813,
|
||||
"y0": 0.745,
|
||||
"x1": 0.395313,
|
||||
"x0": 0.325,
|
||||
"y0": 0.75,
|
||||
"x1": 0.389062,
|
||||
"y1": 0.915
|
||||
},
|
||||
"created_at": "2026-05-07 14:36:25",
|
||||
"updated_at": "2026-05-07 14:40:15"
|
||||
"updated_at": "2026-05-08 09:20:22"
|
||||
}
|
||||
],
|
||||
"nir": [
|
||||
|
|
@ -630,13 +630,13 @@
|
|||
"name": "nir_legacy_01",
|
||||
"enabled": true,
|
||||
"roi_pct": {
|
||||
"x0": 0.29375,
|
||||
"y0": 0.775,
|
||||
"x1": 0.35625,
|
||||
"y1": 0.94
|
||||
"x0": 0.284375,
|
||||
"y0": 0.79,
|
||||
"x1": 0.35,
|
||||
"y1": 0.9525
|
||||
},
|
||||
"created_at": "2026-05-07 14:36:25",
|
||||
"updated_at": "2026-05-07 14:40:46"
|
||||
"updated_at": "2026-05-08 09:20:53"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -650,31 +650,31 @@
|
|||
"nir"
|
||||
],
|
||||
"roi_pct": {
|
||||
"x0": 0.232813,
|
||||
"x0": 0.220312,
|
||||
"y0": 0.8225,
|
||||
"x1": 0.3,
|
||||
"y1": 0.99
|
||||
"x1": 0.2875,
|
||||
"y1": 0.995
|
||||
},
|
||||
"target_value": 0.82,
|
||||
"weight": 0.8,
|
||||
"roi_pct_by_role": {
|
||||
"rgb": {
|
||||
"x0": 0.232813,
|
||||
"x0": 0.220312,
|
||||
"y0": 0.8225,
|
||||
"x1": 0.3,
|
||||
"y1": 0.99
|
||||
"x1": 0.2875,
|
||||
"y1": 0.995
|
||||
},
|
||||
"re": {
|
||||
"x0": 0.2625,
|
||||
"x0": 0.25,
|
||||
"y0": 0.7525,
|
||||
"x1": 0.321875,
|
||||
"x1": 0.315625,
|
||||
"y1": 0.9225
|
||||
},
|
||||
"nir": {
|
||||
"x0": 0.220312,
|
||||
"y0": 0.775,
|
||||
"x1": 0.284375,
|
||||
"y1": 0.9425
|
||||
"x0": 0.214062,
|
||||
"y0": 0.785,
|
||||
"x1": 0.282813,
|
||||
"y1": 0.9525
|
||||
}
|
||||
},
|
||||
"roi_list_by_role": {
|
||||
|
|
@ -683,13 +683,13 @@
|
|||
"name": "rgb_legacy_01",
|
||||
"enabled": true,
|
||||
"roi_pct": {
|
||||
"x0": 0.232813,
|
||||
"x0": 0.220312,
|
||||
"y0": 0.8225,
|
||||
"x1": 0.3,
|
||||
"y1": 0.99
|
||||
"x1": 0.2875,
|
||||
"y1": 0.995
|
||||
},
|
||||
"created_at": "2026-05-07 14:36:25",
|
||||
"updated_at": "2026-05-07 14:39:38"
|
||||
"updated_at": "2026-05-08 09:19:04"
|
||||
}
|
||||
],
|
||||
"re": [
|
||||
|
|
@ -697,13 +697,13 @@
|
|||
"name": "re_legacy_01",
|
||||
"enabled": true,
|
||||
"roi_pct": {
|
||||
"x0": 0.2625,
|
||||
"x0": 0.25,
|
||||
"y0": 0.7525,
|
||||
"x1": 0.321875,
|
||||
"x1": 0.315625,
|
||||
"y1": 0.9225
|
||||
},
|
||||
"created_at": "2026-05-07 14:36:25",
|
||||
"updated_at": "2026-05-07 14:40:24"
|
||||
"updated_at": "2026-05-08 09:20:27"
|
||||
}
|
||||
],
|
||||
"nir": [
|
||||
|
|
@ -711,13 +711,13 @@
|
|||
"name": "nir_legacy_01",
|
||||
"enabled": true,
|
||||
"roi_pct": {
|
||||
"x0": 0.220312,
|
||||
"y0": 0.775,
|
||||
"x1": 0.284375,
|
||||
"y1": 0.9425
|
||||
"x0": 0.214062,
|
||||
"y0": 0.785,
|
||||
"x1": 0.282813,
|
||||
"y1": 0.9525
|
||||
},
|
||||
"created_at": "2026-05-07 14:36:25",
|
||||
"updated_at": "2026-05-07 14:40:55"
|
||||
"updated_at": "2026-05-08 09:21:02"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,12 +68,37 @@ class RawProcessorCore:
|
|||
"apply_stage": "after_fusion",
|
||||
"method": "gray_scale_with_white_guard",
|
||||
"space": "multispec_tensor",
|
||||
"targets": {
|
||||
"black": 0.06,
|
||||
"gray": 0.40,
|
||||
"white": 0.78,
|
||||
"targets_by_patch_channel": {
|
||||
"black": {
|
||||
"R": 0.06,
|
||||
"G": 0.06,
|
||||
"B": 0.06,
|
||||
"RE": 0.06,
|
||||
"NIR": 0.06
|
||||
},
|
||||
"gray": {
|
||||
"R": 0.34,
|
||||
"G": 0.34,
|
||||
"B": 0.34,
|
||||
"RE": 0.24,
|
||||
"NIR": 0.30
|
||||
},
|
||||
"white": {
|
||||
"R": 0.78,
|
||||
"G": 0.78,
|
||||
"B": 0.78,
|
||||
"RE": 0.78,
|
||||
"NIR": 0.78
|
||||
}
|
||||
},
|
||||
"white_guard_max": 0.92,
|
||||
"white_guard_max_by_channel": {
|
||||
"R": 0.92,
|
||||
"G": 0.92,
|
||||
"B": 0.92,
|
||||
"RE": 0.88,
|
||||
"NIR": 0.88
|
||||
},
|
||||
"scale_min": 0.35,
|
||||
"scale_max": 2.50,
|
||||
"clip_output": True,
|
||||
|
|
@ -82,6 +107,7 @@ class RawProcessorCore:
|
|||
"save_patch_stats": True,
|
||||
}
|
||||
self.last_patch_normalization_result = None
|
||||
self.last_frame_quality_result = None
|
||||
self.camera_settings = {}
|
||||
|
||||
if calibration_json_path:
|
||||
|
|
@ -284,6 +310,7 @@ class RawProcessorCore:
|
|||
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.apply_patch_normalization_to_tensor(tensor)
|
||||
self.last_frame_quality_result = self.evaluate_frame_quality(tensor)
|
||||
|
||||
return tensor, channel_names
|
||||
|
||||
|
|
@ -295,6 +322,7 @@ class RawProcessorCore:
|
|||
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)
|
||||
self.last_frame_quality_result = self.evaluate_frame_quality(tensor)
|
||||
return tensor
|
||||
|
||||
if frame_type in ("RGB", "MULTISPEC"):
|
||||
|
|
@ -318,6 +346,7 @@ class RawProcessorCore:
|
|||
tensor = raw_np
|
||||
|
||||
tensor = self.resize_tensor_chw(tensor, target_size=target_size)
|
||||
self.last_frame_quality_result = self.evaluate_frame_quality(tensor)
|
||||
return tensor
|
||||
|
||||
raise RuntimeError(f"frame_type não suportado para inferência: {frame_type}")
|
||||
|
|
@ -681,6 +710,10 @@ class RawProcessorCore:
|
|||
"warnings": [],
|
||||
"scales": {},
|
||||
"patch_stats": {},
|
||||
"channel_debug": {},
|
||||
"tensor_stats_before": {},
|
||||
"tensor_stats_after": {},
|
||||
"summary": {},
|
||||
}
|
||||
|
||||
if not cfg.get("enabled", False):
|
||||
|
|
@ -720,124 +753,106 @@ class RawProcessorCore:
|
|||
self.last_patch_normalization_result = result
|
||||
return tensor
|
||||
|
||||
targets = cfg.get("targets", {}) or {}
|
||||
gray_target = float(targets.get("gray", 0.40))
|
||||
targets_by_patch_channel = self._resolve_targets_by_patch_channel(cfg, rad_cfg)
|
||||
|
||||
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))
|
||||
white_guard_max_global = float(cfg.get("white_guard_max", 0.92))
|
||||
white_guard_by_channel = cfg.get("white_guard_max_by_channel", {}) or {}
|
||||
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]
|
||||
result["tensor_stats_before"] = self._tensor_channel_stats(out, channel_names)
|
||||
|
||||
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)
|
||||
# Para o tensor final fusionado, a geometria de referência é o espaço do RGB.
|
||||
# Como RE/NIR são alinhados por homografia para casar no RGB, as ROIs usadas
|
||||
# na normalização final devem ser as ROIs da role rgb, com suporte a múltiplas
|
||||
# ROIs por cor/cartão.
|
||||
gray_rois = self._resolve_patch_tensor_rois(gray, w, h, reference_role="rgb")
|
||||
white_rois = self._resolve_patch_tensor_rois(white, w, h, reference_role="rgb")
|
||||
black_rois = self._resolve_patch_tensor_rois(black, w, h, reference_role="rgb")
|
||||
|
||||
gray_roi = roi_from_patch(gray)
|
||||
white_roi = roi_from_patch(white)
|
||||
black_roi = roi_from_patch(black)
|
||||
result["roi_source"] = "roi_list_by_role.rgb"
|
||||
result["roi_counts"] = {
|
||||
"black": len(black_rois),
|
||||
"gray": len(gray_rois),
|
||||
"white": len(white_rois),
|
||||
}
|
||||
|
||||
if gray_roi is None:
|
||||
result["warnings"].append("invalid_gray_roi")
|
||||
if not gray_rois:
|
||||
result["warnings"].append("missing_or_invalid_gray_rois")
|
||||
self.last_patch_normalization_result = result
|
||||
return tensor
|
||||
|
||||
for ci, ch_name in enumerate(channel_names):
|
||||
ch = out[ci]
|
||||
input_stats = self._array01_stats(ch)
|
||||
|
||||
gray_target = float(targets_by_patch_channel["gray"][ch_name])
|
||||
white_target = float(targets_by_patch_channel["white"][ch_name])
|
||||
|
||||
# -----------------------------
|
||||
# Stats do gray
|
||||
# -----------------------------
|
||||
gx0, gy0, gx1, gy1 = gray_roi
|
||||
gray_vals = ch[gy0:gy1, gx0:gx1].reshape(-1)
|
||||
gray_stats = self._measure_patch_rois_for_channel(ch, gray_rois)
|
||||
|
||||
if gray_vals.size <= 0:
|
||||
result["warnings"].append(f"{ch_name}: empty_gray_roi")
|
||||
if not gray_stats:
|
||||
result["warnings"].append(f"{ch_name}: empty_gray_rois")
|
||||
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),
|
||||
}
|
||||
result["patch_stats"].setdefault("gray", {})[ch_name] = gray_stats
|
||||
gray_p50 = float(gray_stats["p50"])
|
||||
|
||||
if gray_p50 <= 1e-6:
|
||||
result["warnings"].append(f"{ch_name}: gray_p50_too_low")
|
||||
continue
|
||||
|
||||
scale = gray_target / gray_p50
|
||||
scale_raw = float(gray_target / gray_p50)
|
||||
scale = scale_raw
|
||||
scale_after_white_guard = float(scale)
|
||||
|
||||
white_guard_ch = white_guard_max_global
|
||||
if isinstance(white_guard_by_channel, dict) and ch_name in white_guard_by_channel:
|
||||
try:
|
||||
white_guard_ch = float(white_guard_by_channel[ch_name])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# -----------------------------
|
||||
# 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_rois:
|
||||
white_stats = self._measure_patch_rois_for_channel(ch, white_rois)
|
||||
|
||||
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)
|
||||
if white_stats:
|
||||
result["patch_stats"].setdefault("white", {})[ch_name] = white_stats
|
||||
|
||||
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),
|
||||
}
|
||||
white_p50 = float(white_stats["p50"])
|
||||
white_sat = float(white_stats["sat_pct"])
|
||||
|
||||
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
|
||||
max_scale_by_white = white_guard_ch / 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)
|
||||
scale_after_white_guard = float(scale)
|
||||
|
||||
# -----------------------------
|
||||
# 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),
|
||||
}
|
||||
if black_rois:
|
||||
black_stats = self._measure_patch_rois_for_channel(ch, black_rois)
|
||||
if black_stats:
|
||||
result["patch_stats"].setdefault("black", {})[ch_name] = black_stats
|
||||
|
||||
scale_before_clip = float(scale)
|
||||
scale = float(np.clip(scale, scale_min, scale_max))
|
||||
|
|
@ -847,17 +862,59 @@ class RawProcessorCore:
|
|||
f"{ch_name}: scale_clipped {scale_before_clip:.3f}->{scale:.3f}"
|
||||
)
|
||||
|
||||
out[ci] = ch * scale
|
||||
scaled = ch * scale
|
||||
output_before_clip_stats = self._array01_stats(scaled)
|
||||
would_clip_low_pct = float((scaled < 0.0).mean() * 100.0)
|
||||
would_clip_high_pct = float((scaled > 1.0).mean() * 100.0)
|
||||
would_clip_pct = would_clip_low_pct + would_clip_high_pct
|
||||
|
||||
if clip_output:
|
||||
out_ch = np.clip(scaled, 0.0, 1.0)
|
||||
else:
|
||||
out_ch = scaled
|
||||
|
||||
output_after_clip_stats = self._array01_stats(out_ch)
|
||||
out[ci] = out_ch
|
||||
|
||||
result["scales"][ch_name] = {
|
||||
"scale": scale,
|
||||
"scale_raw_gray": scale_raw,
|
||||
"scale_after_white_guard": scale_after_white_guard,
|
||||
"scale_before_clip_limits": scale_before_clip,
|
||||
"scale_min": scale_min,
|
||||
"scale_max": scale_max,
|
||||
"gray_target": gray_target,
|
||||
"gray_measured_p50": gray_p50,
|
||||
"white_target": white_target,
|
||||
"white_guard_max": white_guard_ch,
|
||||
"would_clip_pct": would_clip_pct,
|
||||
"would_clip_high_pct": would_clip_high_pct,
|
||||
"would_clip_low_pct": would_clip_low_pct,
|
||||
}
|
||||
|
||||
result["channel_debug"][ch_name] = {
|
||||
"input_stats": input_stats,
|
||||
"output_before_clip_stats": output_before_clip_stats,
|
||||
"output_after_clip_stats": output_after_clip_stats,
|
||||
"gray_target": gray_target,
|
||||
"white_target": white_target,
|
||||
"white_guard_max": white_guard_ch,
|
||||
"gray_p50": gray_p50,
|
||||
"scale_raw_gray": scale_raw,
|
||||
"scale_after_white_guard": scale_after_white_guard,
|
||||
"scale_before_clip_limits": scale_before_clip,
|
||||
"scale_final": scale,
|
||||
"would_clip_pct": would_clip_pct,
|
||||
"would_clip_high_pct": would_clip_high_pct,
|
||||
"would_clip_low_pct": would_clip_low_pct,
|
||||
}
|
||||
|
||||
if clip_output:
|
||||
out = np.clip(out, 0.0, 1.0)
|
||||
|
||||
result["tensor_stats_after"] = self._tensor_channel_stats(out, channel_names)
|
||||
result["summary"] = self._summarize_patch_normalization_result(result)
|
||||
|
||||
result["applied"] = True
|
||||
result["valid"] = bool(len(result["scales"]) == len(channel_names))
|
||||
result["clip_output"] = clip_output
|
||||
|
|
@ -867,6 +924,372 @@ class RawProcessorCore:
|
|||
self.last_patch_normalization_result = result
|
||||
return out.astype(np.float32, copy=False)
|
||||
|
||||
def _array01_stats(self, arr: np.ndarray) -> dict:
|
||||
"""
|
||||
Estatísticas compactas para debug radiométrico de arrays float.
|
||||
Mantém tudo serializável em JSON e leve o bastante para log por frame.
|
||||
"""
|
||||
if arr is None:
|
||||
return {}
|
||||
|
||||
vals = np.asarray(arr, dtype=np.float32).reshape(-1)
|
||||
if vals.size <= 0:
|
||||
return {}
|
||||
|
||||
finite = vals[np.isfinite(vals)]
|
||||
if finite.size <= 0:
|
||||
return {"count": int(vals.size), "finite_count": 0}
|
||||
|
||||
return {
|
||||
"count": int(vals.size),
|
||||
"finite_count": int(finite.size),
|
||||
"min": float(np.min(finite)),
|
||||
"p01": float(np.percentile(finite, 1)),
|
||||
"p05": float(np.percentile(finite, 5)),
|
||||
"p50": float(np.percentile(finite, 50)),
|
||||
"p95": float(np.percentile(finite, 95)),
|
||||
"p99": float(np.percentile(finite, 99)),
|
||||
"max": float(np.max(finite)),
|
||||
"mean": float(np.mean(finite)),
|
||||
"std": float(np.std(finite)),
|
||||
"sat_pct": float((finite >= 0.98).mean() * 100.0),
|
||||
"dark_pct": float((finite <= 0.02).mean() * 100.0),
|
||||
"over_1_pct": float((finite > 1.0).mean() * 100.0),
|
||||
"under_0_pct": float((finite < 0.0).mean() * 100.0),
|
||||
}
|
||||
|
||||
def _tensor_channel_stats(self, tensor: np.ndarray, channel_names: list) -> dict:
|
||||
if tensor is None or tensor.ndim != 3:
|
||||
return {}
|
||||
|
||||
stats = {}
|
||||
for i, ch_name in enumerate(channel_names):
|
||||
if i >= tensor.shape[0]:
|
||||
break
|
||||
stats[ch_name] = self._array01_stats(tensor[i])
|
||||
return stats
|
||||
|
||||
def _summarize_patch_normalization_result(self, result: dict) -> dict:
|
||||
scales = result.get("scales", {}) or {}
|
||||
patch_stats = result.get("patch_stats", {}) or {}
|
||||
|
||||
scale_values = [float(v.get("scale", 1.0)) for v in scales.values() if isinstance(v, dict)]
|
||||
clip_values = [float(v.get("would_clip_pct", 0.0)) for v in scales.values() if isinstance(v, dict)]
|
||||
|
||||
white_stats = patch_stats.get("white", {}) if isinstance(patch_stats, dict) else {}
|
||||
gray_stats = patch_stats.get("gray", {}) if isinstance(patch_stats, dict) else {}
|
||||
|
||||
max_white_sat = 0.0
|
||||
if isinstance(white_stats, dict):
|
||||
vals = [float(v.get("sat_pct", 0.0)) for v in white_stats.values() if isinstance(v, dict)]
|
||||
max_white_sat = max(vals) if vals else 0.0
|
||||
|
||||
max_gray_sat = 0.0
|
||||
if isinstance(gray_stats, dict):
|
||||
vals = [float(v.get("sat_pct", 0.0)) for v in gray_stats.values() if isinstance(v, dict)]
|
||||
max_gray_sat = max(vals) if vals else 0.0
|
||||
|
||||
return {
|
||||
"scale_min_applied": float(min(scale_values)) if scale_values else None,
|
||||
"scale_max_applied": float(max(scale_values)) if scale_values else None,
|
||||
"max_would_clip_pct": float(max(clip_values)) if clip_values else 0.0,
|
||||
"max_white_sat_pct": float(max_white_sat),
|
||||
"max_gray_sat_pct": float(max_gray_sat),
|
||||
"warning_count": int(len(result.get("warnings", []) or [])),
|
||||
"valid_channel_count": int(len(scales)),
|
||||
}
|
||||
|
||||
def evaluate_frame_quality(self, tensor: np.ndarray | None = None) -> dict:
|
||||
"""
|
||||
Avalia a qualidade do tensor final gerado pelo RawProcessorCore.
|
||||
|
||||
Esta avaliação pertence ao processamento do tensor, não à captura RAW_BRUTO.
|
||||
Ela combina:
|
||||
- estatísticas do tensor final por canal;
|
||||
- resumo do patch_normalization;
|
||||
- warnings radiométricos/normalização.
|
||||
|
||||
Status:
|
||||
good -> tensor utilizável normalmente;
|
||||
warning -> tensor utilizável, mas com cautela/auditoria;
|
||||
bad -> tensor não recomendado para treino/inferência crítica.
|
||||
"""
|
||||
channel_names = ["R", "G", "B", "RE", "NIR"]
|
||||
quality = {
|
||||
"status": "good",
|
||||
"usable_for_training": True,
|
||||
"usable_for_inference": True,
|
||||
"reasons": [],
|
||||
"metrics": {},
|
||||
"thresholds": {
|
||||
"tensor_sat_warning_pct": 0.5,
|
||||
"tensor_sat_bad_pct": 5.0,
|
||||
"tensor_dark_warning_pct": 45.0,
|
||||
"tensor_dark_bad_pct": 75.0,
|
||||
"patch_white_sat_warning_pct": 0.5,
|
||||
"patch_white_sat_bad_pct": 20.0,
|
||||
"patch_clip_warning_pct": 0.5,
|
||||
"patch_clip_bad_pct": 5.0,
|
||||
},
|
||||
}
|
||||
|
||||
def mark(level: str, reason: str):
|
||||
ranks = {"good": 0, "warning": 1, "bad": 2}
|
||||
if ranks.get(level, 0) > ranks.get(quality["status"], 0):
|
||||
quality["status"] = level
|
||||
if reason not in quality["reasons"]:
|
||||
quality["reasons"].append(reason)
|
||||
|
||||
if tensor is None or not isinstance(tensor, np.ndarray) or tensor.ndim != 3:
|
||||
mark("bad", "invalid_tensor")
|
||||
quality["usable_for_training"] = False
|
||||
quality["usable_for_inference"] = False
|
||||
return quality
|
||||
|
||||
stats = self._tensor_channel_stats(tensor, channel_names)
|
||||
quality["metrics"]["tensor_stats"] = stats
|
||||
|
||||
sat_values = [float(v.get("sat_pct", 0.0)) for v in stats.values() if isinstance(v, dict)]
|
||||
dark_values = [float(v.get("dark_pct", 0.0)) for v in stats.values() if isinstance(v, dict)]
|
||||
over_values = [float(v.get("over_1_pct", 0.0)) for v in stats.values() if isinstance(v, dict)]
|
||||
under_values = [float(v.get("under_0_pct", 0.0)) for v in stats.values() if isinstance(v, dict)]
|
||||
|
||||
max_tensor_sat = max(sat_values) if sat_values else 0.0
|
||||
max_tensor_dark = max(dark_values) if dark_values else 0.0
|
||||
max_tensor_over = max(over_values) if over_values else 0.0
|
||||
max_tensor_under = max(under_values) if under_values else 0.0
|
||||
|
||||
quality["metrics"]["max_tensor_sat_pct"] = float(max_tensor_sat)
|
||||
quality["metrics"]["max_tensor_dark_pct"] = float(max_tensor_dark)
|
||||
quality["metrics"]["max_tensor_over_1_pct"] = float(max_tensor_over)
|
||||
quality["metrics"]["max_tensor_under_0_pct"] = float(max_tensor_under)
|
||||
|
||||
th = quality["thresholds"]
|
||||
if max_tensor_sat >= th["tensor_sat_bad_pct"]:
|
||||
mark("bad", f"tensor_saturation_high:{max_tensor_sat:.2f}%")
|
||||
elif max_tensor_sat >= th["tensor_sat_warning_pct"]:
|
||||
mark("warning", f"tensor_saturation_warning:{max_tensor_sat:.2f}%")
|
||||
|
||||
if max_tensor_dark >= th["tensor_dark_bad_pct"]:
|
||||
mark("bad", f"tensor_too_dark:{max_tensor_dark:.2f}%")
|
||||
elif max_tensor_dark >= th["tensor_dark_warning_pct"]:
|
||||
mark("warning", f"tensor_dark_warning:{max_tensor_dark:.2f}%")
|
||||
|
||||
patch_result = self.last_patch_normalization_result or {}
|
||||
quality["metrics"]["patch_normalization_summary"] = patch_result.get("summary", {}) if isinstance(patch_result, dict) else {}
|
||||
|
||||
if isinstance(patch_result, dict):
|
||||
if patch_result.get("enabled", False) and not patch_result.get("applied", False):
|
||||
mark("bad", "patch_normalization_not_applied")
|
||||
|
||||
summary = patch_result.get("summary", {}) or {}
|
||||
max_white_sat = float(summary.get("max_white_sat_pct", 0.0) or 0.0)
|
||||
max_clip = float(summary.get("max_would_clip_pct", 0.0) or 0.0)
|
||||
warning_count = int(summary.get("warning_count", 0) or 0)
|
||||
valid_channel_count = int(summary.get("valid_channel_count", 0) or 0)
|
||||
|
||||
quality["metrics"]["max_patch_white_sat_pct"] = max_white_sat
|
||||
quality["metrics"]["max_patch_would_clip_pct"] = max_clip
|
||||
quality["metrics"]["patch_warning_count"] = warning_count
|
||||
quality["metrics"]["patch_valid_channel_count"] = valid_channel_count
|
||||
|
||||
if max_white_sat >= th["patch_white_sat_bad_pct"]:
|
||||
mark("bad", f"patch_white_saturation_high:{max_white_sat:.2f}%")
|
||||
elif max_white_sat >= th["patch_white_sat_warning_pct"]:
|
||||
mark("warning", f"patch_white_saturation_warning:{max_white_sat:.2f}%")
|
||||
|
||||
if max_clip >= th["patch_clip_bad_pct"]:
|
||||
mark("bad", f"patch_output_clip_high:{max_clip:.2f}%")
|
||||
elif max_clip >= th["patch_clip_warning_pct"]:
|
||||
mark("warning", f"patch_output_clip_warning:{max_clip:.2f}%")
|
||||
|
||||
if warning_count > 0:
|
||||
mark("warning", f"patch_warnings:{warning_count}")
|
||||
|
||||
if valid_channel_count not in (0, len(channel_names)):
|
||||
mark("bad", f"patch_valid_channels_incomplete:{valid_channel_count}")
|
||||
|
||||
warnings = patch_result.get("warnings", []) or []
|
||||
hard_warning_tokens = (
|
||||
"missing_or_invalid_gray_rois",
|
||||
"missing_gray_patch",
|
||||
"invalid_tensor_shape",
|
||||
"gray_p50_too_low",
|
||||
"empty_gray_rois",
|
||||
)
|
||||
for w in warnings:
|
||||
ws = str(w)
|
||||
if any(tok in ws for tok in hard_warning_tokens):
|
||||
mark("bad", f"patch_error:{ws}")
|
||||
|
||||
if quality["status"] == "bad":
|
||||
quality["usable_for_training"] = False
|
||||
quality["usable_for_inference"] = False
|
||||
elif quality["status"] == "warning":
|
||||
quality["usable_for_training"] = True
|
||||
quality["usable_for_inference"] = True
|
||||
quality["requires_review"] = True
|
||||
else:
|
||||
quality["requires_review"] = False
|
||||
|
||||
return quality
|
||||
|
||||
def _resolve_targets_by_patch_channel(self, cfg: dict, rad_cfg: dict) -> dict:
|
||||
"""
|
||||
Resolve os alvos radiométricos por tipo de referência e por canal CHW.
|
||||
|
||||
Contrato atual:
|
||||
patch_normalization.targets_by_patch_channel
|
||||
|
||||
Formato esperado:
|
||||
{
|
||||
"black": {"R": 0.06, "G": 0.06, "B": 0.06, "RE": 0.06, "NIR": 0.06},
|
||||
"gray": {"R": 0.34, "G": 0.34, "B": 0.34, "RE": 0.24, "NIR": 0.30},
|
||||
"white": {"R": 0.78, "G": 0.78, "B": 0.78, "RE": 0.78, "NIR": 0.78}
|
||||
}
|
||||
|
||||
Observação:
|
||||
Por enquanto, o método gray_scale_with_white_guard usa principalmente:
|
||||
- gray: alvo principal para calcular escala
|
||||
- white: alvo/guarda para limitar escala
|
||||
- black: reservado para futura correção com offset ou linearização
|
||||
"""
|
||||
channel_names = ["R", "G", "B", "RE", "NIR"]
|
||||
|
||||
# Valores seguros caso algo falte no JSON atual.
|
||||
default_targets = {
|
||||
"black": {
|
||||
"R": 0.06,
|
||||
"G": 0.06,
|
||||
"B": 0.06,
|
||||
"RE": 0.06,
|
||||
"NIR": 0.06,
|
||||
},
|
||||
"gray": {
|
||||
"R": 0.34,
|
||||
"G": 0.34,
|
||||
"B": 0.34,
|
||||
"RE": 0.24,
|
||||
"NIR": 0.30,
|
||||
},
|
||||
"white": {
|
||||
"R": 0.78,
|
||||
"G": 0.78,
|
||||
"B": 0.78,
|
||||
"RE": 0.78,
|
||||
"NIR": 0.78,
|
||||
},
|
||||
}
|
||||
|
||||
out = {
|
||||
patch_type: dict(values)
|
||||
for patch_type, values in default_targets.items()
|
||||
}
|
||||
|
||||
explicit = (cfg or {}).get("targets_by_patch_channel", {}) or {}
|
||||
|
||||
if isinstance(explicit, dict):
|
||||
for patch_type in ("black", "gray", "white"):
|
||||
patch_targets = explicit.get(patch_type, {}) or {}
|
||||
if not isinstance(patch_targets, dict):
|
||||
continue
|
||||
|
||||
for ch in channel_names:
|
||||
if ch not in patch_targets:
|
||||
continue
|
||||
try:
|
||||
out[patch_type][ch] = float(patch_targets[ch])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return out
|
||||
|
||||
def _resolve_patch_tensor_rois(self, patch: dict, w: int, h: int, reference_role: str = "rgb") -> list:
|
||||
"""
|
||||
Resolve as ROIs usadas pelo patch_normalization no tensor final.
|
||||
|
||||
Contrato atual escolhido:
|
||||
- O tensor final é alinhado no espaço do RGB.
|
||||
- Portanto, as ROIs dos cartões no tensor final vêm de:
|
||||
reference_patches[type].roi_list_by_role["rgb"]
|
||||
- Suporta N ROIs por cor/cartão.
|
||||
"""
|
||||
if not isinstance(patch, dict):
|
||||
return []
|
||||
|
||||
roi_list_by_role = patch.get("roi_list_by_role", {}) or {}
|
||||
roi_items = roi_list_by_role.get(reference_role, []) or []
|
||||
|
||||
if not isinstance(roi_items, list):
|
||||
roi_items = []
|
||||
|
||||
rois = []
|
||||
for idx, item in enumerate(roi_items):
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
|
||||
if not bool(item.get("enabled", True)):
|
||||
continue
|
||||
|
||||
roi_pct = item.get("roi_pct", {}) or {}
|
||||
if not isinstance(roi_pct, dict):
|
||||
continue
|
||||
|
||||
roi_px = self._roi_pct_to_pixels_from_patch(roi_pct, w, h)
|
||||
x0, y0, x1, y1 = roi_px
|
||||
|
||||
if x1 <= x0 or y1 <= y0:
|
||||
continue
|
||||
|
||||
rois.append({
|
||||
"name": str(item.get("name") or f"{reference_role}_roi_{idx + 1:02d}"),
|
||||
"roi_pct": dict(roi_pct),
|
||||
"roi_px": roi_px,
|
||||
})
|
||||
|
||||
return rois
|
||||
|
||||
def _measure_patch_rois_for_channel(self, ch: np.ndarray, rois: list) -> dict | None:
|
||||
"""
|
||||
Mede uma lista de ROIs no canal CHW já fusionado e reduz de forma robusta.
|
||||
|
||||
Com múltiplas ROIs, usamos mediana dos p50/p05/p95 e máximo de saturação/dark
|
||||
para manter a normalização estável sem ignorar ROI problemática.
|
||||
"""
|
||||
roi_results = []
|
||||
|
||||
for roi in rois or []:
|
||||
x0, y0, x1, y1 = roi["roi_px"]
|
||||
vals = ch[y0:y1, x0:x1].reshape(-1)
|
||||
|
||||
if vals.size <= 0:
|
||||
continue
|
||||
|
||||
stats = {
|
||||
"name": roi.get("name"),
|
||||
"roi_px": list(roi["roi_px"]),
|
||||
"p05": float(np.percentile(vals, 5)),
|
||||
"p50": float(np.percentile(vals, 50)),
|
||||
"p95": float(np.percentile(vals, 95)),
|
||||
"sat_pct": float((vals >= 0.98).mean() * 100.0),
|
||||
"dark_pct": float((vals <= 0.02).mean() * 100.0),
|
||||
}
|
||||
roi_results.append(stats)
|
||||
|
||||
if not roi_results:
|
||||
return None
|
||||
|
||||
return {
|
||||
"roi_count": len(rois or []),
|
||||
"valid_roi_count": len(roi_results),
|
||||
"p05": float(np.median([r["p05"] for r in roi_results])),
|
||||
"p50": float(np.median([r["p50"] for r in roi_results])),
|
||||
"p95": float(np.median([r["p95"] for r in roi_results])),
|
||||
"sat_pct": float(max(r["sat_pct"] for r in roi_results)),
|
||||
"dark_pct": float(max(r["dark_pct"] for r in roi_results)),
|
||||
"roi_results": roi_results,
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ class RawProcessorCore:
|
|||
self.sensor_width = sensor_width
|
||||
self.sensor_height = sensor_height
|
||||
self.bayer_pattern = bayer_pattern.upper()
|
||||
|
||||
self.fusion_config = {
|
||||
"alignment_mode": "manual_affine",
|
||||
"baseline_mm": 75.0,
|
||||
|
|
@ -26,6 +27,7 @@ class RawProcessorCore:
|
|||
"resize_after_crop": True,
|
||||
"target_size": None,
|
||||
}
|
||||
|
||||
self.rgb_calibration = {
|
||||
"enabled": False,
|
||||
"gains": {
|
||||
|
|
@ -34,8 +36,56 @@ class RawProcessorCore:
|
|||
"B": 1.0
|
||||
}
|
||||
}
|
||||
|
||||
self.calibration_json_path = calibration_json_path
|
||||
self.calibration_base_dir = os.path.dirname(os.path.abspath(calibration_json_path)) if calibration_json_path else os.getcwd()
|
||||
|
||||
self.flatfield_config = {
|
||||
"enabled": False,
|
||||
"npz_file": None,
|
||||
"apply_before_fusion": True,
|
||||
"apply_after_decode": True,
|
||||
"apply_space": "native_camera_space",
|
||||
"map_type": "gain",
|
||||
"channels": ["R", "G", "B", "RE", "NIR"],
|
||||
"channel_maps": {},
|
||||
"subtract_dark": False,
|
||||
"clip_output": True,
|
||||
}
|
||||
self.flatfield_maps = {}
|
||||
self.flatfield_loaded = False
|
||||
|
||||
self.radiometric_normalization_config = {
|
||||
"enabled": False,
|
||||
"method": "exposure_gain_reference",
|
||||
"reference_controls": {},
|
||||
"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 = {}
|
||||
|
||||
if calibration_json_path:
|
||||
self.load_fusion_config_json(calibration_json_path)
|
||||
self.load_config_json(calibration_json_path)
|
||||
|
||||
def unpack_raw10_packed(
|
||||
self,
|
||||
|
|
@ -231,107 +281,23 @@ class RawProcessorCore:
|
|||
raise RuntimeError("RGB obrigatório")
|
||||
|
||||
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.apply_patch_normalization_to_tensor(tensor)
|
||||
|
||||
return tensor, channel_names
|
||||
|
||||
def build_infer_tensor_from_stream_old(self, frame, meta, channels_expected):
|
||||
"""
|
||||
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)
|
||||
"""
|
||||
def build_infer_tensor_from_stream(self, frame, meta, channels_expected, target_size=None):
|
||||
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 not isinstance(frame, dict):
|
||||
raise RuntimeError("RAW_BRUTO esperado como dict de câmeras no modo multi")
|
||||
decoded = self.decode_stream_cameras(frame, meta)
|
||||
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 = []
|
||||
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 frame_type in ("RGB", "MULTISPEC"):
|
||||
if not isinstance(frame, np.ndarray):
|
||||
raise RuntimeError(f"Frame {frame_type} esperado como ndarray")
|
||||
|
||||
|
|
@ -348,26 +314,11 @@ class RawProcessorCore:
|
|||
raise RuntimeError(f"dtype {frame_type} não suportado: {dtype_str}")
|
||||
|
||||
if raw_np.shape[0] != channels_expected:
|
||||
raise RuntimeError(
|
||||
f"Frame {frame_type} com canais inesperados: {raw_np.shape[0]} | esperado={channels_expected}"
|
||||
)
|
||||
raise RuntimeError(f"Frame {frame_type} com canais inesperados: {raw_np.shape[0]} | esperado={channels_expected}")
|
||||
tensor = raw_np
|
||||
|
||||
return raw_np
|
||||
|
||||
|
||||
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)
|
||||
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}")
|
||||
|
||||
|
|
@ -450,7 +401,63 @@ class RawProcessorCore:
|
|||
|
||||
return decoded
|
||||
|
||||
def _decode_spectral_frame_to_float01(self, data, cam_meta):
|
||||
arr = data
|
||||
|
||||
if arr.ndim == 3 and arr.shape[2] == 1:
|
||||
arr = arr[:, :, 0]
|
||||
|
||||
bit_depth = int(cam_meta.get("bit_depth", 8))
|
||||
raw_format = str(cam_meta.get("raw_format", "")).upper()
|
||||
packed = bool(cam_meta.get("packed", False))
|
||||
channels = int(cam_meta.get("channels", 1)) if cam_meta.get("channels") is not None else 1
|
||||
|
||||
sensor_width = int(cam_meta.get("width", self.sensor_width))
|
||||
sensor_height = int(cam_meta.get("height", arr.shape[0]))
|
||||
packed_width = int(cam_meta.get("packed_width", 0) or 0)
|
||||
|
||||
looks_like_raw10_packed = (
|
||||
arr.ndim == 2
|
||||
and arr.dtype == np.uint8
|
||||
and arr.shape[0] == sensor_height
|
||||
and (
|
||||
raw_format == "RAW10_PACKED"
|
||||
or packed
|
||||
or bit_depth == 10
|
||||
or (packed_width > 0 and arr.shape[1] == packed_width and packed_width != sensor_width)
|
||||
or arr.shape[1] == int(sensor_width * 10 / 8)
|
||||
)
|
||||
)
|
||||
|
||||
if looks_like_raw10_packed:
|
||||
raw16 = self.unpack_raw10_packed(
|
||||
arr,
|
||||
sensor_width=sensor_width,
|
||||
sensor_height=sensor_height,
|
||||
)
|
||||
|
||||
max_val = float((1 << bit_depth) - 1)
|
||||
return np.clip(raw16.astype(np.float32) / max_val, 0.0, 1.0)
|
||||
|
||||
# Caso preview/processado: mono já vem uint8 normal.
|
||||
if arr.ndim == 2 and arr.dtype == np.uint8:
|
||||
return np.clip(arr.astype(np.float32) / 255.0, 0.0, 1.0)
|
||||
|
||||
if arr.ndim == 2 and arr.dtype == np.uint16:
|
||||
max_val = float((1 << bit_depth) - 1) if bit_depth > 0 and bit_depth <= 16 else 65535.0
|
||||
return np.clip(arr.astype(np.float32) / max_val, 0.0, 1.0)
|
||||
|
||||
arr01 = arr.astype(np.float32)
|
||||
if arr01.max() > 1.5:
|
||||
arr01 /= 255.0
|
||||
|
||||
return np.clip(arr01, 0.0, 1.0)
|
||||
|
||||
def fuse_multispec_cameras(self, decoded, meta, channels_expected):
|
||||
decoded = self.apply_dark_to_decoded(decoded)
|
||||
decoded = self.normalize_decoded_by_capture_controls(decoded, meta)
|
||||
decoded = self.apply_flat_gain_to_decoded(decoded)
|
||||
|
||||
rgb_cam_id = self._find_cam_by_role(decoded, "rgb")
|
||||
if rgb_cam_id is None:
|
||||
raise RuntimeError("Fusão requer câmera com role='rgb' como referência")
|
||||
|
|
@ -661,6 +668,218 @@ class RawProcessorCore:
|
|||
|
||||
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:
|
||||
cam_frames = meta_json.get("camera_frames", {}) or meta_json.get("stream_meta", {}).get("camera_frames", {})
|
||||
|
|
@ -778,7 +997,6 @@ class RawProcessorCore:
|
|||
|
||||
return arr.reshape(shape)
|
||||
|
||||
|
||||
def save_rgb_u8_file(self, path: str, arr: np.ndarray):
|
||||
arr.astype(np.uint8).tofile(path)
|
||||
|
||||
|
|
@ -830,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):
|
||||
raise FileNotFoundError(f"Arquivo de calibração não encontrado: {path}")
|
||||
|
||||
|
|
@ -839,57 +1057,433 @@ class RawProcessorCore:
|
|||
|
||||
fusion = data.get("fusion_config")
|
||||
if isinstance(fusion, dict):
|
||||
self.fusion_config = self._merge_fusion_config(self.fusion_config, fusion)
|
||||
self.fusion_config = self._merge_config(self.fusion_config, fusion)
|
||||
else:
|
||||
print("[WARN] JSON sem fusion_config. Mantendo config padrão.")
|
||||
|
||||
rgb_cal = data.get("rgb_calibration")
|
||||
if isinstance(rgb_cal, dict):
|
||||
self.rgb_calibration = self._merge_fusion_config(self.rgb_calibration, rgb_cal)
|
||||
self.rgb_calibration = self._merge_config(self.rgb_calibration, rgb_cal)
|
||||
|
||||
def _merge_fusion_config(self, default_cfg: dict, loaded_cfg: dict) -> dict:
|
||||
flatfield = data.get("flatfield_config")
|
||||
if isinstance(flatfield, dict):
|
||||
self.flatfield_config = self._merge_config(self.flatfield_config, flatfield)
|
||||
self.load_flatfield_maps()
|
||||
else:
|
||||
self.flatfield_config["enabled"] = False
|
||||
self.flatfield_maps = {}
|
||||
self.flatfield_loaded = False
|
||||
|
||||
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")
|
||||
if isinstance(rad_norm_config, dict):
|
||||
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")
|
||||
if isinstance(cam_set, dict):
|
||||
self.camera_settings = self._merge_config(self.camera_settings, cam_set)
|
||||
|
||||
def _merge_config(self, default_cfg: dict, loaded_cfg: dict) -> dict:
|
||||
cfg = json.loads(json.dumps(default_cfg))
|
||||
|
||||
for key, value in loaded_cfg.items():
|
||||
if isinstance(value, dict) and isinstance(cfg.get(key), dict):
|
||||
cfg[key].update(value)
|
||||
def merge(dst: dict, src: dict):
|
||||
for key, value in src.items():
|
||||
if isinstance(value, dict) and isinstance(dst.get(key), dict):
|
||||
merge(dst[key], value)
|
||||
else:
|
||||
cfg[key] = value
|
||||
dst[key] = value
|
||||
|
||||
if isinstance(loaded_cfg, dict):
|
||||
merge(cfg, loaded_cfg)
|
||||
|
||||
return cfg
|
||||
|
||||
def _decode_spectral_frame_to_float01(self, data, cam_meta):
|
||||
arr = data
|
||||
|
||||
if arr.ndim == 3 and arr.shape[2] == 1:
|
||||
arr = arr[:, :, 0]
|
||||
def _resolve_calibration_path(self, path: str) -> str:
|
||||
if not path:
|
||||
return ""
|
||||
|
||||
bit_depth = int(cam_meta.get("bit_depth", 8))
|
||||
channels = int(cam_meta.get("channels", 1)) if cam_meta.get("channels") is not None else 1
|
||||
path = str(path).replace("\\", "/")
|
||||
|
||||
# Caso preview/processado: mono já vem uint8/uint16 normal.
|
||||
if arr.ndim == 2 and arr.dtype == np.uint8:
|
||||
return np.clip(arr.astype(np.float32) / 255.0, 0.0, 1.0)
|
||||
if os.path.isabs(path):
|
||||
return path
|
||||
|
||||
if arr.ndim == 2 and arr.dtype == np.uint16 and bit_depth != 10:
|
||||
return np.clip(arr.astype(np.float32) / 65535.0, 0.0, 1.0)
|
||||
# Primeiro tenta relativo ao diretório de execução.
|
||||
if os.path.isfile(path):
|
||||
return path
|
||||
|
||||
if bit_depth == 10:
|
||||
sensor_width = int(cam_meta.get("width", self.sensor_width))
|
||||
sensor_height = int(cam_meta.get("height", arr.shape[0]))
|
||||
# Depois tenta relativo ao diretório do module_params.json.
|
||||
candidate = os.path.join(self.calibration_base_dir, path)
|
||||
if os.path.isfile(candidate):
|
||||
return candidate
|
||||
|
||||
raw16 = self.unpack_raw10_packed(
|
||||
arr,
|
||||
sensor_width=sensor_width,
|
||||
sensor_height=sensor_height,
|
||||
# Por fim, se o path já começa com "calibration/", tenta relativo ao pai da pasta calibration.
|
||||
base_parent = os.path.dirname(self.calibration_base_dir)
|
||||
candidate = os.path.join(base_parent, path)
|
||||
if os.path.isfile(candidate):
|
||||
return candidate
|
||||
|
||||
return path
|
||||
|
||||
def load_flatfield_maps(self):
|
||||
cfg = self.flatfield_config or {}
|
||||
|
||||
if not cfg.get("enabled", False):
|
||||
self.flatfield_maps = {}
|
||||
self.flatfield_loaded = False
|
||||
return False
|
||||
|
||||
npz_file = cfg.get("npz_file")
|
||||
if not npz_file:
|
||||
print("[WARN] flatfield_config habilitado, mas sem npz_file.")
|
||||
self.flatfield_maps = {}
|
||||
self.flatfield_loaded = False
|
||||
return False
|
||||
|
||||
npz_path = self._resolve_calibration_path(npz_file)
|
||||
|
||||
if not os.path.isfile(npz_path):
|
||||
print(f"[WARN] Arquivo flat-field não encontrado: {npz_file} -> {npz_path}")
|
||||
self.flatfield_maps = {}
|
||||
self.flatfield_loaded = False
|
||||
return False
|
||||
|
||||
data = np.load(npz_path)
|
||||
|
||||
maps = {}
|
||||
channel_maps = cfg.get("channel_maps", {}) or {}
|
||||
channels = cfg.get("channels", ["R", "G", "B", "RE", "NIR"])
|
||||
|
||||
for ch in channels:
|
||||
ch = str(ch).upper()
|
||||
ch_cfg = channel_maps.get(ch, {}) or {}
|
||||
|
||||
gain_key = ch_cfg.get("gain_key", f"gain_{ch}")
|
||||
dark_key = ch_cfg.get("dark_median_key", f"dark_median_{ch}")
|
||||
|
||||
if gain_key not in data:
|
||||
print(f"[WARN] Flat-field sem chave {gain_key} para canal {ch}.")
|
||||
continue
|
||||
|
||||
entry = {
|
||||
"gain": data[gain_key].astype(np.float32),
|
||||
"gain_key": gain_key,
|
||||
}
|
||||
|
||||
if dark_key and dark_key in data:
|
||||
entry["dark"] = data[dark_key].astype(np.float32)
|
||||
entry["dark_key"] = dark_key
|
||||
|
||||
maps[ch] = entry
|
||||
|
||||
self.flatfield_maps = maps
|
||||
self.flatfield_loaded = len(maps) > 0
|
||||
|
||||
if self.flatfield_loaded:
|
||||
print(f"[OK] Flat-field carregado: {npz_path} | canais={list(maps.keys())}")
|
||||
else:
|
||||
print(f"[WARN] Flat-field habilitado, mas nenhum mapa foi carregado: {npz_path}")
|
||||
|
||||
return self.flatfield_loaded
|
||||
|
||||
def apply_dark_to_decoded(self, decoded: dict) -> dict:
|
||||
cfg = self.flatfield_config or {}
|
||||
|
||||
if not cfg.get("enabled", False):
|
||||
return decoded
|
||||
|
||||
subtract_dark = bool(cfg.get("subtract_dark", True))
|
||||
if not subtract_dark:
|
||||
return decoded
|
||||
|
||||
if not self.flatfield_loaded:
|
||||
self.load_flatfield_maps()
|
||||
|
||||
if not self.flatfield_loaded:
|
||||
return decoded
|
||||
|
||||
corrected = {}
|
||||
|
||||
for cam_id, item in decoded.items():
|
||||
role = str(item.get("role") or item.get("meta", {}).get("role") or "").lower()
|
||||
img = item.get("image")
|
||||
|
||||
if img is None:
|
||||
corrected[cam_id] = item
|
||||
continue
|
||||
|
||||
new_item = dict(item)
|
||||
new_meta = dict(item.get("meta", {}) or {})
|
||||
|
||||
if role == "rgb":
|
||||
if img.ndim != 3 or img.shape[2] < 3:
|
||||
corrected[cam_id] = item
|
||||
continue
|
||||
|
||||
out = img.astype(np.float32).copy()
|
||||
|
||||
for idx, ch in enumerate(("R", "G", "B")):
|
||||
out[:, :, idx] = self._subtract_dark_single_channel(
|
||||
out[:, :, idx],
|
||||
ch,
|
||||
)
|
||||
|
||||
max_val = float((1 << bit_depth) - 1)
|
||||
return np.clip(raw16.astype(np.float32) / max_val, 0.0, 1.0)
|
||||
new_item["image"] = out
|
||||
|
||||
# fallback
|
||||
arr01 = arr.astype(np.float32)
|
||||
if arr01.max() > 1.5:
|
||||
arr01 /= 255.0
|
||||
elif role in ("re", "nir"):
|
||||
ch = "RE" if role == "re" else "NIR"
|
||||
|
||||
return np.clip(arr01, 0.0, 1.0)
|
||||
new_item["image"] = self._subtract_dark_single_channel(
|
||||
img.astype(np.float32),
|
||||
ch,
|
||||
)
|
||||
|
||||
else:
|
||||
corrected[cam_id] = item
|
||||
continue
|
||||
|
||||
new_meta["dark_applied"] = True
|
||||
new_item["meta"] = new_meta
|
||||
corrected[cam_id] = new_item
|
||||
|
||||
return corrected
|
||||
|
||||
def apply_flat_gain_to_decoded(self, decoded: dict) -> dict:
|
||||
cfg = self.flatfield_config or {}
|
||||
|
||||
if not cfg.get("enabled", False):
|
||||
return decoded
|
||||
|
||||
if not self.flatfield_loaded:
|
||||
self.load_flatfield_maps()
|
||||
|
||||
if not self.flatfield_loaded:
|
||||
return decoded
|
||||
|
||||
clip_output = bool(cfg.get("clip_output", True))
|
||||
corrected = {}
|
||||
|
||||
for cam_id, item in decoded.items():
|
||||
role = str(item.get("role") or item.get("meta", {}).get("role") or "").lower()
|
||||
img = item.get("image")
|
||||
|
||||
if img is None:
|
||||
corrected[cam_id] = item
|
||||
continue
|
||||
|
||||
new_item = dict(item)
|
||||
new_meta = dict(item.get("meta", {}) or {})
|
||||
|
||||
if role == "rgb":
|
||||
if img.ndim != 3 or img.shape[2] < 3:
|
||||
corrected[cam_id] = item
|
||||
continue
|
||||
|
||||
out = img.astype(np.float32).copy()
|
||||
|
||||
for idx, ch in enumerate(("R", "G", "B")):
|
||||
out[:, :, idx] = self._apply_flat_gain_single_channel(
|
||||
out[:, :, idx],
|
||||
ch,
|
||||
clip_output=clip_output,
|
||||
)
|
||||
|
||||
new_item["image"] = out
|
||||
|
||||
elif role in ("re", "nir"):
|
||||
ch = "RE" if role == "re" else "NIR"
|
||||
|
||||
new_item["image"] = self._apply_flat_gain_single_channel(
|
||||
img.astype(np.float32),
|
||||
ch,
|
||||
clip_output=clip_output,
|
||||
)
|
||||
|
||||
else:
|
||||
corrected[cam_id] = item
|
||||
continue
|
||||
|
||||
new_meta["flatfield_applied"] = True
|
||||
new_meta["flatfield_map_type"] = cfg.get("map_type", "gain")
|
||||
new_item["meta"] = new_meta
|
||||
corrected[cam_id] = new_item
|
||||
|
||||
return corrected
|
||||
|
||||
def _subtract_dark_single_channel(
|
||||
self,
|
||||
img: np.ndarray,
|
||||
channel_name: str,
|
||||
) -> np.ndarray:
|
||||
ch = str(channel_name).upper()
|
||||
entry = self.flatfield_maps.get(ch)
|
||||
|
||||
if not entry:
|
||||
return img.astype(np.float32, copy=False)
|
||||
|
||||
dark = entry.get("dark")
|
||||
if dark is None:
|
||||
return img.astype(np.float32, copy=False)
|
||||
|
||||
base = img.astype(np.float32)
|
||||
|
||||
dark = dark.astype(np.float32)
|
||||
if dark.shape[:2] != base.shape[:2]:
|
||||
dark = cv2.resize(
|
||||
dark,
|
||||
(base.shape[1], base.shape[0]),
|
||||
interpolation=cv2.INTER_LINEAR,
|
||||
)
|
||||
|
||||
out = np.maximum(base - dark, 0.0)
|
||||
return out.astype(np.float32, copy=False)
|
||||
|
||||
def _apply_flat_gain_single_channel(
|
||||
self,
|
||||
img: np.ndarray,
|
||||
channel_name: str,
|
||||
clip_output: bool = True,
|
||||
) -> np.ndarray:
|
||||
ch = str(channel_name).upper()
|
||||
entry = self.flatfield_maps.get(ch)
|
||||
|
||||
if not entry:
|
||||
return img.astype(np.float32, copy=False)
|
||||
|
||||
gain = entry.get("gain")
|
||||
if gain is None:
|
||||
return img.astype(np.float32, copy=False)
|
||||
|
||||
base = img.astype(np.float32)
|
||||
|
||||
gain = gain.astype(np.float32)
|
||||
if gain.shape[:2] != base.shape[:2]:
|
||||
gain = cv2.resize(
|
||||
gain,
|
||||
(base.shape[1], base.shape[0]),
|
||||
interpolation=cv2.INTER_LINEAR,
|
||||
)
|
||||
|
||||
out = base * gain
|
||||
|
||||
if clip_output:
|
||||
out = np.clip(out, 0.0, 1.0)
|
||||
|
||||
return out.astype(np.float32, copy=False)
|
||||
|
||||
|
||||
def normalize_decoded_by_capture_controls(self, decoded: dict, meta: dict | None = None) -> dict:
|
||||
cfg = self.radiometric_normalization_config or {}
|
||||
|
||||
if not cfg.get("enabled", False):
|
||||
return decoded
|
||||
|
||||
method = str(cfg.get("method", "exposure_gain_reference")).lower()
|
||||
if method != "exposure_gain_reference":
|
||||
return decoded
|
||||
|
||||
controls = self._extract_actual_controls_from_meta(meta)
|
||||
if not controls:
|
||||
return decoded
|
||||
|
||||
reference_controls = cfg.get("reference_controls", {}) or {}
|
||||
clip_output = bool(cfg.get("clip_output", False))
|
||||
|
||||
normalized = {}
|
||||
|
||||
for cam_id, item in decoded.items():
|
||||
role = str(item.get("role") or item.get("meta", {}).get("role") or "").lower()
|
||||
img = item.get("image")
|
||||
|
||||
if img is None or not role:
|
||||
normalized[cam_id] = item
|
||||
continue
|
||||
|
||||
actual_ctrl = controls.get(role, {}) or {}
|
||||
|
||||
ref_ctrl = (
|
||||
reference_controls.get(role)
|
||||
or self.camera_settings.get(role)
|
||||
or actual_ctrl
|
||||
or {}
|
||||
)
|
||||
|
||||
actual_factor = self._exposure_gain_factor(actual_ctrl)
|
||||
ref_factor = self._exposure_gain_factor(ref_ctrl)
|
||||
|
||||
if actual_factor <= 0 or ref_factor <= 0:
|
||||
normalized[cam_id] = item
|
||||
continue
|
||||
|
||||
scale = ref_factor / actual_factor
|
||||
|
||||
new_item = dict(item)
|
||||
new_meta = dict(item.get("meta", {}) or {})
|
||||
|
||||
out = img.astype(np.float32) * float(scale)
|
||||
|
||||
if clip_output:
|
||||
out = np.clip(out, 0.0, 1.0)
|
||||
|
||||
new_meta["radiometric_normalization_applied"] = True
|
||||
new_meta["radiometric_normalization_method"] = method
|
||||
new_meta["radiometric_normalization_scale"] = float(scale)
|
||||
new_meta["radiometric_actual_factor"] = float(actual_factor)
|
||||
new_meta["radiometric_reference_factor"] = float(ref_factor)
|
||||
|
||||
new_item["image"] = out.astype(np.float32, copy=False)
|
||||
new_item["meta"] = new_meta
|
||||
|
||||
normalized[cam_id] = new_item
|
||||
|
||||
return normalized
|
||||
|
||||
def _extract_actual_controls_from_meta(self, meta: dict | None) -> dict:
|
||||
if not meta:
|
||||
return {}
|
||||
|
||||
for key in ("actual_camera_controls", "camera_controls", "startup_camera_controls"):
|
||||
controls = meta.get(key)
|
||||
if isinstance(controls, dict) and controls:
|
||||
return controls
|
||||
|
||||
stream_meta = meta.get("stream_meta")
|
||||
if isinstance(stream_meta, dict):
|
||||
for key in ("actual_camera_controls", "camera_controls", "startup_camera_controls"):
|
||||
controls = stream_meta.get(key)
|
||||
if isinstance(controls, dict) and controls:
|
||||
return controls
|
||||
|
||||
return {}
|
||||
|
||||
def _exposure_gain_factor(self, ctrl: dict) -> float:
|
||||
if not isinstance(ctrl, dict):
|
||||
return 0.0
|
||||
|
||||
exp = ctrl.get("exposure_time_us", None)
|
||||
gain = ctrl.get("analogue_gain", None)
|
||||
|
||||
try:
|
||||
exp = float(exp)
|
||||
except Exception:
|
||||
exp = 0.0
|
||||
|
||||
try:
|
||||
gain = float(gain)
|
||||
except Exception:
|
||||
gain = 1.0
|
||||
|
||||
if exp <= 0:
|
||||
return 0.0
|
||||
|
||||
if gain <= 0:
|
||||
gain = 1.0
|
||||
|
||||
return float(exp * gain)
|
||||
|
|
|
|||
|
|
@ -245,7 +245,7 @@ def main():
|
|||
|
||||
if not isinstance(radiometric_normalization_config, dict):
|
||||
radiometric_normalization_config = {
|
||||
"enabled": True,
|
||||
"enabled": False,
|
||||
"method": "exposure_gain_reference",
|
||||
"apply_stage": "after_dark_before_flat_gain",
|
||||
"reference_controls": {
|
||||
|
|
@ -255,6 +255,10 @@ def main():
|
|||
},
|
||||
"clip_output": True,
|
||||
}
|
||||
else:
|
||||
radiometric_normalization_config["enabled"] = bool(
|
||||
radiometric_normalization_config.get("enabled", False)
|
||||
)
|
||||
|
||||
patch_normalization_config = radiometric_data.get("patch_normalization")
|
||||
if not isinstance(patch_normalization_config, dict):
|
||||
|
|
@ -262,17 +266,30 @@ def main():
|
|||
|
||||
if not isinstance(patch_normalization_config, dict):
|
||||
patch_normalization_config = {
|
||||
"enabled": False,
|
||||
"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,
|
||||
"targets_by_patch_channel": {
|
||||
"black": {
|
||||
"R": 0.06, "G": 0.06, "B": 0.06, "RE": 0.06, "NIR": 0.06
|
||||
},
|
||||
"gray": {
|
||||
"R": 0.34, "G": 0.34, "B": 0.34, "RE": 0.24, "NIR": 0.30
|
||||
},
|
||||
"white": {
|
||||
"R": 0.78, "G": 0.78, "B": 0.78, "RE": 0.78, "NIR": 0.78
|
||||
},
|
||||
},
|
||||
"white_guard_max": 0.92,
|
||||
"white_guard_max_by_channel": {
|
||||
"R": 0.92,
|
||||
"G": 0.92,
|
||||
"B": 0.92,
|
||||
"RE": 0.88,
|
||||
"NIR": 0.88,
|
||||
},
|
||||
"scale_min": 0.35,
|
||||
"scale_max": 2.50,
|
||||
"clip_output": True,
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ def save_multispec_tensor_from_raw_group(
|
|||
ensure_dir(out_dir)
|
||||
out_dir = Path(out_dir)
|
||||
|
||||
tensor, desc = build_multispec_from_raw_native_multi(group, meta)
|
||||
tensor, desc, processing_info = build_multispec_from_raw_native_multi(group, meta)
|
||||
|
||||
if tensor is None:
|
||||
raise RuntimeError(f"Falha ao gerar tensor MULTISPEC: {desc}")
|
||||
|
|
@ -85,6 +85,9 @@ def save_multispec_tensor_from_raw_group(
|
|||
"description": desc,
|
||||
"same_frame_as_raw_bruto": True,
|
||||
},
|
||||
"processing": processing_info or {},
|
||||
"frame_quality": (processing_info or {}).get("frame_quality"),
|
||||
"patch_normalization_result": (processing_info or {}).get("patch_normalization_result"),
|
||||
"source_capture_meta": {
|
||||
"ts": meta.get("ts"),
|
||||
"sensor_width": meta.get("sensor_width"),
|
||||
|
|
@ -124,9 +127,44 @@ def chw_to_hwc(arr: np.ndarray) -> np.ndarray:
|
|||
return np.transpose(arr, (1, 2, 0))
|
||||
|
||||
|
||||
def tensor_to_preview_panels(tensor: np.ndarray):
|
||||
def format_frame_quality_for_overlay(frame_quality: dict | None, patch_result: dict | None = None) -> str:
|
||||
"""
|
||||
Gera uma linha curta para mostrar no preview do tensor final.
|
||||
Exemplo:
|
||||
Q=good | sat=0.00% | dark=12.3% | clip=0.00% | scale=0.91-1.08
|
||||
"""
|
||||
if not isinstance(frame_quality, dict):
|
||||
return "Q=n/a"
|
||||
|
||||
status = frame_quality.get("status", "unknown")
|
||||
metrics = frame_quality.get("metrics", {}) or {}
|
||||
sat = float(metrics.get("max_tensor_sat_pct", 0.0) or 0.0)
|
||||
dark = float(metrics.get("max_tensor_dark_pct", 0.0) or 0.0)
|
||||
clip = float(metrics.get("max_patch_would_clip_pct", 0.0) or 0.0)
|
||||
|
||||
scale_txt = "scale=n/a"
|
||||
if isinstance(patch_result, dict):
|
||||
summary = patch_result.get("summary", {}) or {}
|
||||
smin = summary.get("scale_min_applied")
|
||||
smax = summary.get("scale_max_applied")
|
||||
if smin is not None and smax is not None:
|
||||
try:
|
||||
scale_txt = f"scale={float(smin):.2f}-{float(smax):.2f}"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
reasons = frame_quality.get("reasons", []) or []
|
||||
reason_txt = ""
|
||||
if status != "good" and reasons:
|
||||
reason_txt = f" | {str(reasons[0])[:38]}"
|
||||
|
||||
return f"Q={status} | sat={sat:.2f}% | dark={dark:.1f}% | clip={clip:.2f}% | {scale_txt}{reason_txt}"
|
||||
|
||||
|
||||
def tensor_to_preview_panels(tensor: np.ndarray, frame_quality: dict | None = None, patch_result: dict | None = None):
|
||||
"""
|
||||
Recebe tensor CHW [R,G,B,RE,NIR] float32 e devolve painéis visuais.
|
||||
Quando disponível, adiciona um resumo de qualidade no subtítulo do painel RGB final.
|
||||
"""
|
||||
if tensor.ndim != 3 or tensor.shape[0] < 5:
|
||||
raise RuntimeError(f"Tensor MULTISPEC inválido: shape={tensor.shape}")
|
||||
|
|
@ -147,8 +185,13 @@ def tensor_to_preview_panels(tensor: np.ndarray):
|
|||
cv2.COLOR_GRAY2BGR
|
||||
)
|
||||
|
||||
quality_subtitle = format_frame_quality_for_overlay(frame_quality, patch_result)
|
||||
rgb_subtitle = f"tensor {list(tensor.shape)} | canais 0,1,2"
|
||||
if quality_subtitle:
|
||||
rgb_subtitle = f"{rgb_subtitle} | {quality_subtitle}"
|
||||
|
||||
return [
|
||||
("MULTISPEC RGB final", rgb_bgr, f"tensor {list(tensor.shape)} | canais 0,1,2"),
|
||||
("MULTISPEC RGB final", rgb_bgr, rgb_subtitle),
|
||||
("MULTISPEC RE final", re_bgr, "tensor canal 3"),
|
||||
("MULTISPEC NIR final", nir_bgr, "tensor canal 4"),
|
||||
]
|
||||
|
|
@ -166,7 +209,7 @@ def build_multispec_from_raw_native_multi(group: dict, meta: dict):
|
|||
- camera_params_json/module_params.json
|
||||
"""
|
||||
if meta.get("saved_payload_type") != "raw_native_multi":
|
||||
return None, "captura não é raw_native_multi"
|
||||
return None, "captura não é raw_native_multi", {}
|
||||
|
||||
stream_meta = meta.get("stream_meta", {}) or {}
|
||||
camera_info = stream_meta.get("camera_info", {}) or {}
|
||||
|
|
@ -227,7 +270,12 @@ def build_multispec_from_raw_native_multi(group: dict, meta: dict):
|
|||
|
||||
tensor = core.build_infer_tensor_from_stream(frame, processing_meta, 5)
|
||||
|
||||
return tensor, f"MULTISPEC gerado offline do RAW_BRUTO | shape={list(tensor.shape)}"
|
||||
processing_info = {
|
||||
"patch_normalization_result": getattr(core, "last_patch_normalization_result", None),
|
||||
"frame_quality": getattr(core, "last_frame_quality_result", None),
|
||||
}
|
||||
|
||||
return tensor, f"MULTISPEC gerado offline do RAW_BRUTO | shape={list(tensor.shape)}", processing_info
|
||||
|
||||
|
||||
def build_visual_from_saved_payload(payload_path: Path, meta: dict, cam_id: str | None = None) -> tuple[np.ndarray, str]:
|
||||
|
|
@ -333,30 +381,26 @@ def build_visual_from_saved_payload(payload_path: Path, meta: dict, cam_id: str
|
|||
if arr.ndim != 3 or arr.shape[0] < 5:
|
||||
raise RuntimeError(f"Payload MULTISPEC inválido, shape={arr.shape}")
|
||||
|
||||
rgb_hwc = chw_to_hwc(arr[:3].astype(np.float32))
|
||||
rgb_bgr = normalize_float01_to_bgr(rgb_hwc)
|
||||
processing = meta.get("processing", {}) or {}
|
||||
frame_quality = meta.get("frame_quality") or processing.get("frame_quality")
|
||||
patch_result = meta.get("patch_normalization_result") or processing.get("patch_normalization_result")
|
||||
|
||||
re01 = arr[3].astype(np.float32)
|
||||
nir01 = arr[4].astype(np.float32)
|
||||
|
||||
re_bgr = cv2.cvtColor(
|
||||
np.clip(re01 * 255.0, 0, 255).astype(np.uint8),
|
||||
cv2.COLOR_GRAY2BGR
|
||||
panels = tensor_to_preview_panels(
|
||||
arr.astype(np.float32),
|
||||
frame_quality=frame_quality,
|
||||
patch_result=patch_result,
|
||||
)
|
||||
|
||||
nir_bgr = cv2.cvtColor(
|
||||
np.clip(nir01 * 255.0, 0, 255).astype(np.uint8),
|
||||
cv2.COLOR_GRAY2BGR
|
||||
)
|
||||
|
||||
panels = [
|
||||
("RGB reconstruido", rgb_bgr, "canais 0,1,2"),
|
||||
("RE reconstruido", re_bgr, "canal 3"),
|
||||
("NIR reconstruido", nir_bgr, "canal 4"),
|
||||
]
|
||||
# Renomeia os painéis para indicar que vieram de um MULTISPEC já salvo.
|
||||
renamed = []
|
||||
for title, img, subtitle in panels:
|
||||
title = title.replace("MULTISPEC RGB final", "RGB reconstruido")
|
||||
title = title.replace("MULTISPEC RE final", "RE reconstruido")
|
||||
title = title.replace("MULTISPEC NIR final", "NIR reconstruido")
|
||||
renamed.append((title, img, subtitle))
|
||||
|
||||
desc = f"Reconstruido de MULTISPEC | dtype={arr.dtype} | shape={arr.shape} | canais=[R,G,B,RE,NIR]"
|
||||
return panels, desc
|
||||
return renamed, desc
|
||||
|
||||
if saved_type == "raw_native_single":
|
||||
if arr.ndim == 3 and arr.shape[2] == 3 and arr.dtype == np.uint8:
|
||||
|
|
@ -445,13 +489,19 @@ def build_panels_from_group(group):
|
|||
# =========================================================
|
||||
if saved_type == "raw_native_multi":
|
||||
try:
|
||||
tensor, desc = build_multispec_from_raw_native_multi(group, meta)
|
||||
tensor_panels = tensor_to_preview_panels(tensor)
|
||||
tensor, desc, processing_info = build_multispec_from_raw_native_multi(group, meta)
|
||||
tensor_panels = tensor_to_preview_panels(
|
||||
tensor,
|
||||
frame_quality=(processing_info or {}).get("frame_quality"),
|
||||
patch_result=(processing_info or {}).get("patch_normalization_result"),
|
||||
)
|
||||
|
||||
# Aqui colocamos só o RGB final como painel principal,
|
||||
# para substituir o antigo PNG salvo.
|
||||
title, img, subtitle = tensor_panels[0]
|
||||
panels.append((title, img, desc))
|
||||
# Mostra a qualidade do tensor gerado offline a partir do RAW_BRUTO.
|
||||
# O desc completo continua sendo impresso no terminal/salvo no JSON offline.
|
||||
panels.append((title, img, subtitle))
|
||||
|
||||
# Opcional: se quiser também ver RE/NIR finais do tensor,
|
||||
# descomente estas duas linhas:
|
||||
|
|
@ -598,8 +648,24 @@ def put_label(img: np.ndarray, title: str, subtitle: str = "") -> np.ndarray:
|
|||
cv2.putText(out, title, (12, 28), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 255), 2, cv2.LINE_AA)
|
||||
|
||||
if subtitle:
|
||||
cv2.putText(out, subtitle, (12, 56), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (0, 0, 0), 3, cv2.LINE_AA)
|
||||
cv2.putText(out, subtitle, (12, 56), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (255, 255, 255), 1, cv2.LINE_AA)
|
||||
# Quebra visual simples para linhas longas de debug/qualidade.
|
||||
subtitle_lines = []
|
||||
current = ""
|
||||
for part in str(subtitle).split(" | "):
|
||||
candidate = part if not current else current + " | " + part
|
||||
if len(candidate) > 95 and current:
|
||||
subtitle_lines.append(current)
|
||||
current = part
|
||||
else:
|
||||
current = candidate
|
||||
if current:
|
||||
subtitle_lines.append(current)
|
||||
|
||||
y = 56
|
||||
for line in subtitle_lines[:3]:
|
||||
cv2.putText(out, line, (12, y), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (0, 0, 0), 3, cv2.LINE_AA)
|
||||
cv2.putText(out, line, (12, y), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (255, 255, 255), 1, cv2.LINE_AA)
|
||||
y += 22
|
||||
|
||||
return out
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -222,26 +222,26 @@ def base_ae_contract():
|
|||
"verbose": True,
|
||||
|
||||
"control_metric": "p50",
|
||||
"target_value": 0.40,
|
||||
"deadband": 0.04,
|
||||
"target_value": 0.34,
|
||||
"deadband": 0.035,
|
||||
|
||||
"p95_limit": 0.90,
|
||||
"p95_limit": 0.94,
|
||||
"saturation_limit_pct": 0.50,
|
||||
"dark_limit_pct": 35.0,
|
||||
|
||||
# Novo controle proporcional por razão
|
||||
"control_strategy": "ratio",
|
||||
"ratio_alpha": 0.55,
|
||||
"ratio_min": 0.55,
|
||||
"ratio_max": 1.85,
|
||||
"ratio_alpha": 0.35,
|
||||
"ratio_min": 0.65,
|
||||
"ratio_max": 1.35,
|
||||
|
||||
# Redução rápida quando satura
|
||||
"reduce_fast_factor": 0.75,
|
||||
"reduce_fast_factor": 0.80,
|
||||
|
||||
# Mantém compatibilidade com o modo antigo
|
||||
"alpha": 0.18,
|
||||
"exp_step_gain": 0.55,
|
||||
"factor_min": 0.72,
|
||||
"factor_min": 0.55,
|
||||
"factor_max": 1.28,
|
||||
|
||||
"prefer_exposure": True,
|
||||
|
|
@ -263,12 +263,12 @@ def base_ae_contract():
|
|||
|
||||
"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},
|
||||
"re": {"exp_min_us": 100, "exp_max_us": 2500, "gain_min": 1.0, "gain_max": 2.0},
|
||||
"nir": {"exp_min_us": 100, "exp_max_us": 3000, "gain_min": 1.0, "gain_max": 2.0},
|
||||
},
|
||||
|
||||
"exp_apply_threshold_us": 40,
|
||||
"gain_apply_threshold": 0.03,
|
||||
"exp_apply_threshold_us": 80,
|
||||
"gain_apply_threshold": 0.05,
|
||||
|
||||
"ready_required_cycles": 3,
|
||||
|
||||
|
|
@ -322,6 +322,12 @@ def make_default_patch(patch_type: str, target: float, weight: float):
|
|||
|
||||
def default_profile_patches():
|
||||
cfg = base_ae_contract()
|
||||
gray_patch = make_default_patch("gray", 0.34, 1.0)
|
||||
gray_patch["target_value_by_role"] = {
|
||||
"rgb": 0.34,
|
||||
"re": 0.24,
|
||||
"nir": 0.30,
|
||||
}
|
||||
cfg.update({
|
||||
"metering_mode": "reference_patches",
|
||||
"spectral_control_mode": "shared",
|
||||
|
|
@ -348,7 +354,7 @@ def default_profile_patches():
|
|||
|
||||
"reference_patches": [
|
||||
make_default_patch("black", 0.06, 0.25),
|
||||
make_default_patch("gray", 0.40, 1.0),
|
||||
gray_patch,
|
||||
make_default_patch("white", 0.78, 0.7),
|
||||
],
|
||||
})
|
||||
|
|
@ -399,12 +405,37 @@ def load_or_default_config(path: str):
|
|||
"apply_stage": "after_fusion",
|
||||
"method": "gray_scale_with_white_guard",
|
||||
"space": "multispec_tensor",
|
||||
"targets": {
|
||||
"black": 0.06,
|
||||
"gray": 0.40,
|
||||
"white": 0.78
|
||||
"targets_by_patch_channel": {
|
||||
"black": {
|
||||
"R": 0.06,
|
||||
"G": 0.06,
|
||||
"B": 0.06,
|
||||
"RE": 0.06,
|
||||
"NIR": 0.06
|
||||
},
|
||||
"gray": {
|
||||
"R": 0.34,
|
||||
"G": 0.34,
|
||||
"B": 0.34,
|
||||
"RE": 0.24,
|
||||
"NIR": 0.30
|
||||
},
|
||||
"white": {
|
||||
"R": 0.78,
|
||||
"G": 0.78,
|
||||
"B": 0.78,
|
||||
"RE": 0.78,
|
||||
"NIR": 0.78
|
||||
}
|
||||
},
|
||||
"white_guard_max": 0.92,
|
||||
"white_guard_max_by_channel": {
|
||||
"R": 0.92,
|
||||
"G": 0.92,
|
||||
"B": 0.92,
|
||||
"RE": 0.88,
|
||||
"NIR": 0.88
|
||||
},
|
||||
"scale_min": 0.35,
|
||||
"scale_max": 2.50,
|
||||
"clip_output": True,
|
||||
|
|
@ -1348,12 +1379,37 @@ def main():
|
|||
"apply_stage": "after_fusion",
|
||||
"method": "gray_scale_with_white_guard",
|
||||
"space": "multispec_tensor",
|
||||
"targets": {
|
||||
"black": 0.06,
|
||||
"gray": 0.40,
|
||||
"white": 0.78
|
||||
"targets_by_patch_channel": {
|
||||
"black": {
|
||||
"R": 0.06,
|
||||
"G": 0.06,
|
||||
"B": 0.06,
|
||||
"RE": 0.06,
|
||||
"NIR": 0.06
|
||||
},
|
||||
"gray": {
|
||||
"R": 0.34,
|
||||
"G": 0.34,
|
||||
"B": 0.34,
|
||||
"RE": 0.24,
|
||||
"NIR": 0.30
|
||||
},
|
||||
"white": {
|
||||
"R": 0.78,
|
||||
"G": 0.78,
|
||||
"B": 0.78,
|
||||
"RE": 0.78,
|
||||
"NIR": 0.78
|
||||
}
|
||||
},
|
||||
"white_guard_max": 0.92,
|
||||
"white_guard_max_by_channel": {
|
||||
"R": 0.92,
|
||||
"G": 0.92,
|
||||
"B": 0.92,
|
||||
"RE": 0.88,
|
||||
"NIR": 0.88
|
||||
},
|
||||
"scale_min": 0.35,
|
||||
"scale_max": 2.50,
|
||||
"clip_output": True,
|
||||
|
|
|
|||
Loading…
Reference in New Issue