Rework radar system

This commit is contained in:
Ayzen
2026-10-06 12:32:26 +03:00
parent 552cdc688a
commit 5bb86b7165
299 changed files with 13537 additions and 27763 deletions
+148 -96
View File
@@ -7,120 +7,163 @@ from dataclasses import dataclass, field
import json
import logging
from pathlib import Path
from typing import Any
from typing import Protocol
from typing import Any, TypeVar
from python_app.models.run_config_model import RunConfigModel
from python_app.models.run_config_schema import RunConfigModel
logger = logging.getLogger(__name__)
Scalar = str | int | float | bool
_ScalarT = TypeVar("_ScalarT", str, int, float, bool)
@dataclass(frozen=True, slots=True)
class Setting:
"""Qt-free binding and validation metadata for one persistent GUI field."""
widget: str = ""
live: str = ""
web: str = ""
group: str = ""
choices: tuple[str, ...] = ()
minimum: float | None = None
maximum: float | None = None
exclusive_minimum: bool = False
def setting(
default: _ScalarT, widget: str = "", *, live: str = "", web: str = "",
group: str = "", choices: tuple[str, ...] = (), minimum: float | None = None,
maximum: float | None = None, exclusive_minimum: bool = False,
) -> _ScalarT:
"""Declare a typed dataclass field and its single profile/live/web binding."""
return field(default=default, metadata={"setting": Setting(
widget, live, web or live, group, choices, minimum, maximum, exclusive_minimum,
)})
@dataclass(slots=True)
class GuiSwitchStateModel:
"""UI-only state for switch selection controls."""
combo_mode: str = "text"
combos_text: str = ""
single_input: str = "0"
single_output: str = "0"
combo_mode: str = setting("text", choices=('text', 'single'))
combos_text: str = setting("", '_combos_text')
single_input: str = setting("0", '_single_combo_input')
single_output: str = setting("0", '_single_combo_output')
@dataclass(slots=True)
class GuiPassThroughStateModel:
"""UI-only defaults for pass-through rendering."""
show_magnitude: bool = True
show_phase: bool = True
unwrap_phase: bool = False
combo_filter: str = ""
fixed_y_enabled: bool = False
y_min_db: float = -100.0
y_max_db: float = 0.0
show_magnitude: bool = setting(True, '_show_magnitude_checkbox')
show_phase: bool = setting(True, '_show_phase_checkbox')
unwrap_phase: bool = setting(False, '_unwrap_phase_checkbox')
combo_filter: str = setting("", '_pass_through_combo_filter_input', web='combo_filter', group='Display')
fixed_y_enabled: bool = setting(False, '_pass_through_fixed_y_enabled', web='pass_through_fixed_y_enabled', group='Pass-through')
y_min_db: float = setting(-100.0, '_pass_through_y_min_db', web='pass_through_y_min_db', group='Pass-through')
y_max_db: float = setting(0.0, '_pass_through_y_max_db', web='pass_through_y_max_db', group='Pass-through')
@dataclass(slots=True)
class GuiBscanStateModel:
"""UI-only defaults for B-scan live settings."""
axis: str = "abs"
cut_m: float = 0.0
max_depth_m: float = 8.0
gain: float = 1.0
start_freq_mhz: float = 100.0
stop_freq_mhz: float = 8800.0
subtract_mean_ascan: bool = False
axis: str = setting("abs", '_bscan_axis', live='bscan_axis', group='B-scan', choices=('abs', 'real', 'phase'))
cut_m: float = setting(0.824, '_bscan_cut_m', live='bscan_cut_m', group='B-scan')
max_depth_m: float = setting(1.0, '_bscan_max_depth_m', live='bscan_max_depth_m', group='B-scan')
gain: float = setting(1.0, '_bscan_gain', live='bscan_gain', group='B-scan')
start_freq_mhz: float = setting(100.0, '_bscan_start_freq_mhz', live='bscan_start_freq_mhz', group='B-scan')
stop_freq_mhz: float = setting(8800.0, '_bscan_stop_freq_mhz', live='bscan_stop_freq_mhz', group='B-scan')
subtract_mean_ascan: bool = setting(False, '_bscan_subtract_mean_ascan', web='subtract_mean_ascan', group='Display')
# How many past sweeps the B-scan heatmap renders once acquisition is stopped.
# While running the window stays at the C++ replay window (see
# `_cpp_bscan_replay_window_for_config`); this only widens the stopped-mode view.
history_window_scans: int = 50
history_window_scans: int = setting(50, '_bscan_history_window')
@dataclass(slots=True)
class GuiGprStateModel:
"""UI-only defaults for coherent backprojection GPR live settings."""
input_positions: str = ""
output_positions: str = ""
min_depth_m: float = 2.0
max_depth_m: float = 14.0
range_comp_power: float = 0.1
# BP object-detection stop level, fraction of the global peak (Horns_motion_3libre.py
# BP_OBJECT_MIN_FRAC). Angle compensation and permittivity are fixed for coherent BP
# (Python 0.3 block), so they are not exposed here.
object_min_frac: float = 0.7
score_mode: str = "combined"
motion_mode: str = "int_minus"
input_positions: str = setting("", '_gpr_input_positions_input', live='gpr_input_positions', group='Geometry & depth')
output_positions: str = setting("", '_gpr_output_positions_input', live='gpr_output_positions', group='Geometry & depth')
min_depth_m: float = setting(2.0, '_gpr_min_depth_m', live='gpr_min_depth_m', group='Geometry & depth')
max_depth_m: float = setting(14.0, '_gpr_max_depth_m', live='gpr_max_depth_m', group='Geometry & depth')
range_comp_power: float = setting(0.1, '_gpr_range_comp_power', live='gpr_range_comp_power', group='Imaging', minimum=0)
# BP object-detection stop level, fraction of the global peak. Angle compensation and
# permittivity are fixed for coherent BP, so they are not exposed here.
object_min_frac: float = setting(0.7, '_gpr_object_min_frac', live='gpr_object_min_frac', group='Detection', minimum=0, maximum=1)
score_mode: str = setting("combined", '_gpr_score_mode', live='gpr_score_mode', group='Imaging', choices=('peak', 'combined'))
motion_mode: str = setting("int_minus", '_gpr_motion_mode', live='gpr_motion_mode', group='Motion', choices=('int_minus', 'int_focus'))
# Intra-sweep motion-correction inputs. Sweep time is derived from acquisition
# metadata; speed comes from the socket unless `ignore_socket_speed_enabled`,
# in which case `speed_m_s` from here is used.
look_angle_deg: float = 0.0
direction_sign: float = 1.0
speed_m_s: float = 0.0
ignore_socket_speed_enabled: bool = False
max_detected_objects_to_draw: int = 5
draw_top_m_objects: int = 2
look_angle_deg: float = setting(0.0, '_gpr_look_angle_deg', live='gpr_look_angle_deg', group='Motion')
direction_sign: float = setting(1.0, '_gpr_direction_sign', live='gpr_direction_sign', group='Motion')
speed_m_s: float = setting(0.0, '_gpr_speed_m_s', live='gpr_speed_m_s', group='Motion')
ignore_socket_speed_enabled: bool = setting(False, '_gpr_ignore_socket_speed_enabled', live='ignore_socket_speed', group='Motion')
max_detected_objects_to_draw: int = setting(
5, '_gpr_max_detected_objects_to_draw', live='gpr_max_detected_objects_to_draw', group='Detection', minimum=0,
)
draw_top_m_objects: int = setting(2, '_gpr_draw_top_m_objects', live='gpr_draw_top_m_objects', group='Detection', minimum=0)
# Cross-frame approach filter: show an object only after it persists as a
# motion-consistent track this many consecutive frames (<= 1 disables it).
object_approach_min_frames: int = 3
start_freq_mhz: float = 3000.0
stop_freq_mhz: float = 6000.0
background_subtract_enabled: bool = True
background_mean_count: int = 10
remove_sidelobe_objects_enabled: bool = True
imaging_plane_y_m: float = 0.0
render_mode: str = "heatmap"
visible_x_min_m: float = -2.0
visible_x_max_m: float = 2.0
visible_z_min_m: float = 0.0
visible_z_max_m: float = 14.0
object_approach_min_frames: int = setting(
3, '_gpr_object_approach_min_frames', live='gpr_object_approach_min_frames', group='Detection', minimum=1,
)
start_freq_mhz: float = setting(3000.0, '_gpr_start_freq_mhz', live='gpr_start_freq_mhz', group='Geometry & depth')
stop_freq_mhz: float = setting(6000.0, '_gpr_stop_freq_mhz', live='gpr_stop_freq_mhz', group='Geometry & depth')
background_subtract_enabled: bool = setting(True, '_gpr_background_subtract_enabled', live='gpr_background_subtract_enabled', group='Imaging')
background_mean_count: int = setting(10, '_gpr_background_mean_count', live='gpr_background_mean_count', group='Imaging')
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')
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')
visible_z_min_m: float = setting(0.0, '_gpr_visible_z_min_m', live='gpr_visible_z_min_m', group='Visible window')
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 = "point"
input_positions: str = ""
output_positions: str = ""
min_depth_m: float = 2.0
max_depth_m: float = 14.0
comp_power: float = 0.2
start_freq_mhz: float = 3000.0
stop_freq_mhz: float = 6000.0
speed_m_s: float = 0.0
ignore_socket_speed_enabled: bool = False
look_angle_deg: float = 0.0
apply_freq_phase_correction: bool = True
reference_mode: str = "frame_center"
snr_thresh: float = 4.5
snr_comp_max: float = 25.0
background_subtract_enabled: bool = True
background_mean_count: int = 10
render_mode: str = "heatmap"
min_visible_pair_count: int = 1
visible_x_min_m: float = -2.0
visible_x_max_m: float = 2.0
visible_z_min_m: float = 0.0
visible_z_max_m: float = 14.0
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)
@@ -131,28 +174,30 @@ class GuiSar3dStateModel:
names the GUI shows: INPUT_IDX, F_START, MIN_DEPTH, LOOK_ANGLE_DEG, ...
"""
input_positions: str = ""
output_positions: str = ""
min_depth_m: float = 2.0
max_depth_m: float = 8.0
start_freq_mhz: float = 1500.0
stop_freq_mhz: float = 6000.0
speed_m_s: float = 0.0
ignore_socket_speed_enabled: bool = False
look_angle_deg: float = 19.5
frame_radius: int = 0
voxel_m: float = 0.02
bp_ground_depth_m: float = 1.25
comp_range_power: float = 0.3
use_compensation: bool = True
normalize_each_frame: bool = True
input_positions: str = setting("", '_sar3d_input_positions_input', live='gpr_input_positions', group='Geometry & depth')
output_positions: str = setting("", '_sar3d_output_positions_input', live='gpr_output_positions', group='Geometry & depth')
min_depth_m: float = setting(2.0, '_sar3d_min_depth_m', live='gpr_min_depth_m', group='Geometry & depth')
max_depth_m: float = setting(8.0, '_sar3d_max_depth_m', live='gpr_max_depth_m', group='Geometry & depth')
start_freq_mhz: float = setting(1500.0, '_sar3d_start_freq_mhz', live='gpr_start_freq_mhz', group='Geometry & depth')
stop_freq_mhz: float = setting(6000.0, '_sar3d_stop_freq_mhz', live='gpr_stop_freq_mhz', group='Geometry & depth')
speed_m_s: float = setting(0.0, '_sar3d_speed_m_s', live='gpr_speed_m_s', group='Motion')
ignore_socket_speed_enabled: bool = setting(False, '_sar3d_ignore_socket_speed_enabled', live='ignore_socket_speed', group='Motion')
look_angle_deg: float = setting(19.5, '_sar3d_look_angle_deg', live='gpr_look_angle_deg', group='Motion')
frame_radius: int = setting(0, '_sar3d_frame_radius', live='sar3d_frame_radius', group='3D SAR', minimum=0)
voxel_m: float = setting(0.02, '_sar3d_voxel_m', live='sar3d_voxel_m', group='3D SAR', minimum=0, exclusive_minimum=True)
bp_ground_depth_m: float = setting(1.25, '_sar3d_bp_ground_depth_m', live='sar3d_bp_ground_depth_m', group='3D SAR')
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')
@dataclass(slots=True)
class GuiProcessingStateModel:
"""UI-only processing-section defaults."""
selected_mode: str = "pass_through"
selected_mode: str = setting(
"pass_through", '_processing_mode', live='processor_mode', group='Processor', choices=('pass_through', 'bscan', 'gpr', 'legacy_gpr', 'sar3d'),
)
pass_through: GuiPassThroughStateModel = field(default_factory=GuiPassThroughStateModel)
bscan: GuiBscanStateModel = field(default_factory=GuiBscanStateModel)
gpr: GuiGprStateModel = field(default_factory=GuiGprStateModel)
@@ -164,21 +209,21 @@ class GuiProcessingStateModel:
class GuiDataActionsStateModel:
"""UI-only defaults for snapshot/export controls."""
save_path: str = ""
save_name: str = "snapshot_manual"
save_path: str = setting("", '_save_path_input')
save_name: str = setting("snapshot_manual", '_save_name_input')
# How many freshly acquired measurements the "Start + Record" action writes to
# disk before it stops recording (acquisition keeps running).
record_count: int = 100
record_count: int = setting(100, '_record_count')
@dataclass(slots=True)
class GuiPreprocessDialogStateModel:
"""UI-only defaults for preprocessing dialog controls."""
set_name: str = "set_001"
radar_config_dir: str = ""
use_all_radar_configs: bool = False
median_sweep_count: int = 5
set_name: str = setting("set_001")
radar_config_dir: str = setting("")
use_all_radar_configs: bool = setting(False)
median_sweep_count: int = setting(5)
@dataclass(slots=True)
@@ -227,3 +272,10 @@ class GuiProfileModel:
from python_app.models.gui_profile_codec import gui_profile_to_dict
return gui_profile_to_dict(self)
class ProfileWriter(Protocol):
"""Publish the current profile, optionally using the acquisition provenance."""
def __call__(self, path: Path, /, *, allow_overwrite: bool = True,
dataset: bool = False) -> GuiProfileModel: ...