Update GPR and SAR3D processing

This commit is contained in:
Ayzen
2026-10-08 16:24:01 +03:00
parent 390015ec39
commit 3575ca68f5
67 changed files with 1187 additions and 2581 deletions
+4 -21
View File
@@ -10,7 +10,7 @@ from python_app.models.json_fields import finite_number, json_object
from python_app.models.run_config_schema import RunConfigModel
def _decode_state(model: object, payload: dict[str, Any], context: str, *, check_constraints: bool = True) -> None:
def _decode_state(model: object, payload: dict[str, Any], context: str) -> None:
"""Read known fields recursively; unknown fields do not survive re-encoding."""
if not is_dataclass(model) or isinstance(model, type):
raise TypeError("GUI state must be a dataclass instance")
@@ -19,14 +19,14 @@ def _decode_state(model: object, payload: dict[str, Any], context: str, *, check
value = payload.get(member.name, default)
name = f"{context}.{member.name}"
if is_dataclass(default):
_decode_state(default, json_object(payload.get(member.name), name), name, check_constraints=check_constraints)
_decode_state(default, json_object(payload.get(member.name), name), name)
continue
if isinstance(default, float):
value = finite_number(value, name)
elif type(value) is not type(default):
raise ValueError(f"{name} must be a JSON {type(default).__name__}")
spec = member.metadata.get("setting")
if check_constraints and isinstance(spec, Setting):
if isinstance(spec, Setting):
if spec.choices and value not in spec.choices:
raise ValueError(f"{name} must be one of: {', '.join(spec.choices)}")
if spec.minimum is not None:
@@ -39,33 +39,16 @@ def _decode_state(model: object, payload: dict[str, Any], context: str, *, check
def gui_profile_from_dict(payload: dict[str, Any]) -> GuiProfileModel:
"""Read the pipeline config and optional GUI state, preserving old GPR profiles."""
"""Read the pipeline config and optional GUI state; ignore unknown fields."""
profile = GuiProfileModel(run_config=RunConfigModel.from_dict(payload))
if payload.get("gui") is None:
return profile
gui_object = json_object(payload["gui"], "gui")
processing = json_object(gui_object.get("processing"), "gui.processing")
old_gpr = json_object(processing.get("gpr"), "gui.processing.gpr")
gui = GuiStateModel()
# Older profiles kept both algorithms under gpr. Root mode and the short-lived
# algorithm selector choose the legacy page; explicit legacy values win.
root_gpr = payload.get("gpr")
root_mode = root_gpr.get("mode") if isinstance(root_gpr, dict) else None
algorithm_mode = {"legacy_point": "point", "legacy_extended": "extended"}.get(
str(old_gpr.get("algorithm", "")))
if root_mode in ("point", "extended"):
gui.processing.legacy_gpr.mode = root_mode
if algorithm_mode is not None:
gui.processing.legacy_gpr.mode = algorithm_mode
_decode_state(gui.processing.legacy_gpr, old_gpr, "gui.processing.gpr", check_constraints=False)
_decode_state(gui, gui_object, "gui")
if gui.version != 1:
raise ValueError(f"Unsupported gui.version: {gui.version}")
if gui.processing.selected_mode == "gpr" and (
algorithm_mode is not None or root_mode in ("point", "extended")
):
gui.processing.selected_mode = "legacy_gpr"
gui.preprocess_dialog.median_sweep_count = max(1, gui.preprocess_dialog.median_sweep_count)
profile.gui = gui
return profile
+13 -41
View File
@@ -121,7 +121,8 @@ class GuiGprStateModel:
remove_sidelobe_objects_enabled: bool = setting(
True, '_gpr_remove_sidelobe_objects_enabled', live='gpr_remove_sidelobe_objects_enabled', group='Imaging',
)
imaging_plane_y_m: float = setting(0.0, '_gpr_imaging_plane_y_m', live='gpr_imaging_plane_y_m', group='Geometry & depth')
bp_ground_depth_m: float = setting(1.25, '_gpr_bp_ground_depth_m', live='gpr_bp_ground_depth_m', group='Geometry & depth', minimum=0)
pair_depth_equalize: bool = setting(True, '_gpr_pair_depth_equalize', live='pair_depth_equalize', group='Imaging')
render_mode: str = setting("heatmap", '_gpr_render_mode', web='render_mode', group='Display', choices=('heatmap', 'objects_only'))
visible_x_min_m: float = setting(-2.0, '_gpr_visible_x_min_m', live='gpr_visible_x_min_m', group='Visible window')
visible_x_max_m: float = setting(2.0, '_gpr_visible_x_max_m', live='gpr_visible_x_max_m', group='Visible window')
@@ -129,48 +130,11 @@ class GuiGprStateModel:
visible_z_max_m: float = setting(14.0, '_gpr_visible_z_max_m', live='gpr_visible_z_max_m', group='Visible window')
@dataclass(slots=True)
class GuiLegacyGprStateModel:
"""UI-only defaults for legacy point/extended GPR live settings."""
mode: str = setting("point", '_legacy_gpr_config_mode', live='legacy_gpr_mode', group='Mode', choices=('point', 'extended'))
input_positions: str = setting("", '_legacy_gpr_input_positions_input', live='gpr_input_positions', group='Geometry & depth')
output_positions: str = setting("", '_legacy_gpr_output_positions_input', live='gpr_output_positions', group='Geometry & depth')
min_depth_m: float = setting(2.0, '_legacy_gpr_min_depth_m', live='gpr_min_depth_m', group='Geometry & depth')
max_depth_m: float = setting(14.0, '_legacy_gpr_max_depth_m', live='gpr_max_depth_m', group='Geometry & depth')
comp_power: float = setting(0.2, '_legacy_gpr_comp_power', live='gpr_comp_power', group='Detection', minimum=0)
start_freq_mhz: float = setting(3000.0, '_legacy_gpr_start_freq_mhz', live='gpr_start_freq_mhz', group='Geometry & depth')
stop_freq_mhz: float = setting(6000.0, '_legacy_gpr_stop_freq_mhz', live='gpr_stop_freq_mhz', group='Geometry & depth')
speed_m_s: float = setting(0.0, '_legacy_gpr_speed_m_s', live='gpr_speed_m_s', group='Motion')
ignore_socket_speed_enabled: bool = setting(False, '_legacy_gpr_ignore_socket_speed_enabled', live='ignore_socket_speed', group='Motion')
look_angle_deg: float = setting(0.0, '_legacy_gpr_look_angle_deg', live='gpr_look_angle_deg', group='Motion')
apply_freq_phase_correction: bool = setting(
True, '_legacy_gpr_apply_freq_phase_correction', live='gpr_apply_freq_phase_correction', group='Motion',
)
reference_mode: str = setting(
"frame_center", '_legacy_gpr_reference_mode', live='gpr_reference_mode', group='Motion', choices=('frame_center', 'first_tx_event'),
)
snr_thresh: float = setting(4.5, '_legacy_gpr_snr_thresh', live='gpr_snr_thresh', group='Detection', minimum=0)
snr_comp_max: float = setting(25.0, '_legacy_gpr_snr_comp_max', live='gpr_snr_comp_max', group='Detection', minimum=0)
background_subtract_enabled: bool = setting(
True, '_legacy_gpr_background_subtract_enabled', live='gpr_background_subtract_enabled', group='Imaging',
)
background_mean_count: int = setting(10, '_legacy_gpr_background_mean_count', live='gpr_background_mean_count', group='Imaging')
render_mode: str = setting("heatmap", '_legacy_gpr_render_mode', web='render_mode', group='Display', choices=('heatmap', 'objects_only'))
min_visible_pair_count: int = setting(
1, '_legacy_gpr_min_visible_pair_count', live='legacy_gpr_min_visible_pair_count', group='Detection', minimum=1,
)
visible_x_min_m: float = setting(-2.0, '_legacy_gpr_visible_x_min_m', live='gpr_visible_x_min_m', group='Visible window')
visible_x_max_m: float = setting(2.0, '_legacy_gpr_visible_x_max_m', live='gpr_visible_x_max_m', group='Visible window')
visible_z_min_m: float = setting(0.0, '_legacy_gpr_visible_z_min_m', live='gpr_visible_z_min_m', group='Visible window')
visible_z_max_m: float = setting(14.0, '_legacy_gpr_visible_z_max_m', live='gpr_visible_z_max_m', group='Visible window')
@dataclass(slots=True)
class GuiSar3dStateModel:
"""UI-only defaults for the 3D SAR (sar3d) live settings.
Values are the reference notebook's (Multi_antenna_3D_clean_v1.ipynb), whose
Values are the reference notebook's (Multi_antenna_demonstrate_depthnorm.ipynb), whose
names the GUI shows: INPUT_IDX, F_START, MIN_DEPTH, LOOK_ANGLE_DEG, ...
"""
@@ -189,6 +153,15 @@ class GuiSar3dStateModel:
comp_range_power: float = setting(0.3, '_sar3d_comp_range_power', live='sar3d_comp_range_power', group='3D SAR')
use_compensation: bool = setting(True, '_sar3d_use_compensation', live='sar3d_use_compensation', group='3D SAR')
normalize_each_frame: bool = setting(True, '_sar3d_normalize_each_frame', live='sar3d_normalize_each_frame', group='3D SAR')
pair_depth_equalize: bool = setting(True, '_sar3d_pair_depth_equalize', live='pair_depth_equalize', group='3D SAR')
background_subtract_enabled: bool = setting(True, '_sar3d_background_subtract_enabled', live='gpr_background_subtract_enabled', group='3D SAR')
background_mean_count: int = setting(10, '_sar3d_background_mean_count', live='gpr_background_mean_count', group='3D SAR', minimum=0)
x_size_m: float = setting(0.0, '_sar3d_x_size_m', live='sar3d_x_size_m', group='Scene', minimum=0)
y_min_m: float = setting(-0.5, '_sar3d_y_min_m', live='sar3d_y_min_m', group='Scene')
y_max_m: float = setting(2.0, '_sar3d_y_max_m', live='sar3d_y_max_m', group='Scene')
z_size_m: float = setting(0.0, '_sar3d_z_size_m', live='sar3d_z_size_m', group='Scene', minimum=0)
cloud_threshold: float = setting(0.65, '_sar3d_cloud_threshold', web='sar3d_cloud_threshold', group='Display', minimum=0, maximum=1)
zoom_percent: int = setting(100, '_sar3d_zoom_percent', web='sar3d_zoom_percent', group='Display', minimum=100, maximum=1000)
@dataclass(slots=True)
@@ -196,12 +169,11 @@ class GuiProcessingStateModel:
"""UI-only processing-section defaults."""
selected_mode: str = setting(
"pass_through", '_processing_mode', live='processor_mode', group='Processor', choices=('pass_through', 'bscan', 'gpr', 'legacy_gpr', 'sar3d'),
"pass_through", '_processing_mode', live='processor_mode', group='Processor', choices=('pass_through', 'bscan', 'gpr', 'sar3d'),
)
pass_through: GuiPassThroughStateModel = field(default_factory=GuiPassThroughStateModel)
bscan: GuiBscanStateModel = field(default_factory=GuiBscanStateModel)
gpr: GuiGprStateModel = field(default_factory=GuiGprStateModel)
legacy_gpr: GuiLegacyGprStateModel = field(default_factory=GuiLegacyGprStateModel)
sar3d: GuiSar3dStateModel = field(default_factory=GuiSar3dStateModel)
-3
View File
@@ -343,9 +343,6 @@ def run_config_from_dict(payload: object) -> RunConfigModel:
if model.preprocess.notch.taper_type not in ("cosine", "hard"):
raise ValueError("preprocess.notch.taper_type must be cosine or hard")
model.gpr.relative_permittivity = _read_float(
gpr_payload, "relative_permittivity", model.gpr.relative_permittivity
)
model.gpr.tx_geometry = []
for entry in _as_list(gpr_payload.get("tx_geometry"), "gpr.tx_geometry"):
entry_payload = _as_dict(entry, "gpr.tx_geometry[]")
-1
View File
@@ -304,7 +304,6 @@ class GprRxGeometryModel:
class GprModel:
"""Stable GPR configuration saved in run_config.json."""
relative_permittivity: float = 1.0
tx_geometry: list[GprTxGeometryModel] = field(default_factory=list)
rx_geometry: list[GprRxGeometryModel] = field(default_factory=list)
+1 -5
View File
@@ -288,8 +288,7 @@ def validate_gpr_model(
) -> None:
"""Validate stable GPR config against current switch dimensions.
Checks that the relative permittivity is positive and that every tx/rx
geometry entry indexes a valid, non-duplicate switch position. When ``sweep``
Checks that every tx/rx geometry entry indexes a valid, non-duplicate switch position. When ``sweep``
is supplied (the load and GUI/save paths share this chokepoint), its bounds
are validated here too so sweep failures surface alongside the GPR checks
instead of as a C++ boot crash.
@@ -297,9 +296,6 @@ def validate_gpr_model(
if sweep is not None:
validate_sweep_model(sweep)
if not math.isfinite(gpr.relative_permittivity) or float(gpr.relative_permittivity) <= 0.0:
raise ValueError("gpr.relative_permittivity must be > 0")
normalize_antenna_geometry(gpr.tx_geometry, "output_pos", output_switch_positions)
normalize_antenna_geometry(gpr.rx_geometry, "input_pos", input_switch_positions)