projector version

This commit is contained in:
awe
2026-09-15 19:06:05 +03:00
parent 62c0266a6b
commit b3339d101c
33 changed files with 2605 additions and 51 deletions
+3
View File
@@ -371,6 +371,9 @@ class AppWindow(
self._gpr_region_contours = []
self._gpr_geometry_signature = None
self._gpr_selected_geometry = None
self._sar3d_lookup_table = None
self._sar3d_xz_image_item = None
self._sar3d_xy_image_item = None
self._phase_viewbox = None
self._history_run_signature = None
self._processor_run_signature = None
@@ -16,10 +16,9 @@ from python_app.gui.runtime.constraints import validate_processing_mode_constrai
from python_app.orchestration.live_processing_config import ProcessingLiveConfig
_GPR_MODES = ("gpr", "legacy_gpr")
def _is_legacy_gpr(window) -> bool:
return window._processing_mode.currentText() == "legacy_gpr"
# Modes that consume the shared gpr_* live-config fields (band, depth gate, antenna
# selection, motion): both GPR imagers plus the 3D SAR processor.
_GPR_LIKE_MODES = ("gpr", "legacy_gpr", "sar3d")
def _attr(name: str):
@@ -27,9 +26,16 @@ def _attr(name: str):
return lambda window: getattr(window, name)
def _dual(gpr_attr: str, legacy_attr: str):
"""Widget getter for a gpr_* field with separate gpr / legacy_gpr widgets."""
return lambda window: getattr(window, legacy_attr if _is_legacy_gpr(window) else gpr_attr)
def _dual(default_attr: str, mode_attrs: dict[str, str] | None = None):
"""Widget getter for a field with per-mode widgets (gpr / legacy_gpr / sar3d).
`mode_attrs` maps a processor mode to its dedicated widget attribute; any
other mode (including inactive ones) falls back to `default_attr`.
"""
attrs = dict(mode_attrs or {})
return lambda window: getattr(
window, attrs.get(window._processing_mode.currentText(), default_attr)
)
# The ONE authoritative field <-> widget map (inverse of `_live_processing_config`),
@@ -50,17 +56,17 @@ _WEB_LIVE_SCHEMA = [
("bscan_start_freq_mhz", "B-scan", ("bscan",), _attr("_bscan_start_freq_mhz")),
("bscan_stop_freq_mhz", "B-scan", ("bscan",), _attr("_bscan_stop_freq_mhz")),
("legacy_gpr_mode", "Mode", ("legacy_gpr",), _attr("_legacy_gpr_config_mode")),
("gpr_input_positions", "Geometry & depth", _GPR_MODES, _dual("_gpr_input_positions_input", "_legacy_gpr_input_positions_input")),
("gpr_output_positions", "Geometry & depth", _GPR_MODES, _dual("_gpr_output_positions_input", "_legacy_gpr_output_positions_input")),
("gpr_min_depth_m", "Geometry & depth", _GPR_MODES, _dual("_gpr_min_depth_m", "_legacy_gpr_min_depth_m")),
("gpr_max_depth_m", "Geometry & depth", _GPR_MODES, _dual("_gpr_max_depth_m", "_legacy_gpr_max_depth_m")),
("gpr_start_freq_mhz", "Geometry & depth", _GPR_MODES, _dual("_gpr_start_freq_mhz", "_legacy_gpr_start_freq_mhz")),
("gpr_stop_freq_mhz", "Geometry & depth", _GPR_MODES, _dual("_gpr_stop_freq_mhz", "_legacy_gpr_stop_freq_mhz")),
("gpr_input_positions", "Geometry & depth", _GPR_LIKE_MODES, _dual("_gpr_input_positions_input", {"legacy_gpr": "_legacy_gpr_input_positions_input", "sar3d": "_sar3d_input_positions_input"})),
("gpr_output_positions", "Geometry & depth", _GPR_LIKE_MODES, _dual("_gpr_output_positions_input", {"legacy_gpr": "_legacy_gpr_output_positions_input", "sar3d": "_sar3d_output_positions_input"})),
("gpr_min_depth_m", "Geometry & depth", _GPR_LIKE_MODES, _dual("_gpr_min_depth_m", {"legacy_gpr": "_legacy_gpr_min_depth_m", "sar3d": "_sar3d_min_depth_m"})),
("gpr_max_depth_m", "Geometry & depth", _GPR_LIKE_MODES, _dual("_gpr_max_depth_m", {"legacy_gpr": "_legacy_gpr_max_depth_m", "sar3d": "_sar3d_max_depth_m"})),
("gpr_start_freq_mhz", "Geometry & depth", _GPR_LIKE_MODES, _dual("_gpr_start_freq_mhz", {"legacy_gpr": "_legacy_gpr_start_freq_mhz", "sar3d": "_sar3d_start_freq_mhz"})),
("gpr_stop_freq_mhz", "Geometry & depth", _GPR_LIKE_MODES, _dual("_gpr_stop_freq_mhz", {"legacy_gpr": "_legacy_gpr_stop_freq_mhz", "sar3d": "_sar3d_stop_freq_mhz"})),
("gpr_imaging_plane_y_m", "Geometry & depth", ("gpr",), _attr("_gpr_imaging_plane_y_m")),
("gpr_range_comp_power", "Imaging", ("gpr",), _attr("_gpr_range_comp_power")),
("gpr_score_mode", "Imaging", ("gpr",), _attr("_gpr_score_mode")),
("gpr_background_subtract_enabled", "Imaging", _GPR_MODES, _dual("_gpr_background_subtract_enabled", "_legacy_gpr_background_subtract_enabled")),
("gpr_background_mean_count", "Imaging", _GPR_MODES, _dual("_gpr_background_mean_count", "_legacy_gpr_background_mean_count")),
("gpr_background_subtract_enabled", "Imaging", _GPR_MODES, _dual("_gpr_background_subtract_enabled", {"legacy_gpr": "_legacy_gpr_background_subtract_enabled"})),
("gpr_background_mean_count", "Imaging", _GPR_MODES, _dual("_gpr_background_mean_count", {"legacy_gpr": "_legacy_gpr_background_mean_count"})),
("gpr_remove_sidelobe_objects_enabled", "Imaging", ("gpr",), _attr("_gpr_remove_sidelobe_objects_enabled")),
("gpr_object_min_frac", "Detection", ("gpr",), _attr("_gpr_object_min_frac")),
("gpr_max_detected_objects_to_draw", "Detection", ("gpr",), _attr("_gpr_max_detected_objects_to_draw")),
@@ -70,17 +76,29 @@ _WEB_LIVE_SCHEMA = [
("gpr_snr_thresh", "Detection", ("legacy_gpr",), _attr("_legacy_gpr_snr_thresh")),
("gpr_snr_comp_max", "Detection", ("legacy_gpr",), _attr("_legacy_gpr_snr_comp_max")),
("legacy_gpr_min_visible_pair_count", "Detection", ("legacy_gpr",), _attr("_legacy_gpr_min_visible_pair_count")),
("gpr_visible_x_min_m", "Visible window", _GPR_MODES, _dual("_gpr_visible_x_min_m", "_legacy_gpr_visible_x_min_m")),
("gpr_visible_x_max_m", "Visible window", _GPR_MODES, _dual("_gpr_visible_x_max_m", "_legacy_gpr_visible_x_max_m")),
("gpr_visible_z_min_m", "Visible window", _GPR_MODES, _dual("_gpr_visible_z_min_m", "_legacy_gpr_visible_z_min_m")),
("gpr_visible_z_max_m", "Visible window", _GPR_MODES, _dual("_gpr_visible_z_max_m", "_legacy_gpr_visible_z_max_m")),
("gpr_visible_x_min_m", "Visible window", _GPR_MODES, _dual("_gpr_visible_x_min_m", {"legacy_gpr": "_legacy_gpr_visible_x_min_m"})),
("gpr_visible_x_max_m", "Visible window", _GPR_MODES, _dual("_gpr_visible_x_max_m", {"legacy_gpr": "_legacy_gpr_visible_x_max_m"})),
("gpr_visible_z_min_m", "Visible window", _GPR_MODES, _dual("_gpr_visible_z_min_m", {"legacy_gpr": "_legacy_gpr_visible_z_min_m"})),
("gpr_visible_z_max_m", "Visible window", _GPR_MODES, _dual("_gpr_visible_z_max_m", {"legacy_gpr": "_legacy_gpr_visible_z_max_m"})),
("gpr_motion_mode", "Motion", ("gpr",), _attr("_gpr_motion_mode")),
("gpr_look_angle_deg", "Motion", _GPR_MODES, _dual("_gpr_look_angle_deg", "_legacy_gpr_look_angle_deg")),
("gpr_direction_sign", "Motion", ("gpr",), _attr("_gpr_direction_sign")),
("gpr_apply_freq_phase_correction", "Motion", ("legacy_gpr",), _attr("_legacy_gpr_apply_freq_phase_correction")),
("gpr_look_angle_deg", "Motion", _GPR_LIKE_MODES, _dual("_gpr_look_angle_deg", {"legacy_gpr": "_legacy_gpr_look_angle_deg", "sar3d": "_sar3d_look_angle_deg"})),
("gpr_direction_sign", "Motion", ("gpr", "sar3d"), _dual("_gpr_direction_sign", {"sar3d": "_sar3d_direction_sign"})),
("gpr_apply_freq_phase_correction", "Motion", ("legacy_gpr", "sar3d"), _dual("_legacy_gpr_apply_freq_phase_correction", {"sar3d": "_sar3d_apply_freq_phase_correction"})),
("gpr_reference_mode", "Motion", ("legacy_gpr",), _attr("_legacy_gpr_reference_mode")),
("ignore_socket_speed", "Motion", _GPR_MODES, _dual("_gpr_ignore_socket_speed_enabled", "_legacy_gpr_ignore_socket_speed_enabled")),
("gpr_speed_m_s", "Motion", _GPR_MODES, _dual("_gpr_speed_m_s", "_legacy_gpr_speed_m_s")),
("ignore_socket_speed", "Motion", _GPR_LIKE_MODES, _dual("_gpr_ignore_socket_speed_enabled", {"legacy_gpr": "_legacy_gpr_ignore_socket_speed_enabled", "sar3d": "_sar3d_ignore_socket_speed_enabled"})),
("gpr_speed_m_s", "Motion", _GPR_LIKE_MODES, _dual("_gpr_speed_m_s", {"legacy_gpr": "_legacy_gpr_speed_m_s", "sar3d": "_sar3d_speed_m_s"})),
("sar3d_frame_radius", "3D SAR", ("sar3d",), _attr("_sar3d_frame_radius")),
("sar3d_edge_frame_weight", "3D SAR", ("sar3d",), _attr("_sar3d_edge_frame_weight")),
("sar3d_pitch_deg", "3D SAR", ("sar3d",), _attr("_sar3d_pitch_deg")),
("sar3d_center_depth_m", "3D SAR", ("sar3d",), _attr("_sar3d_center_depth_m")),
("sar3d_half_width_x_m", "3D SAR", ("sar3d",), _attr("_sar3d_half_width_x_m")),
("sar3d_half_width_y_m", "3D SAR", ("sar3d",), _attr("_sar3d_half_width_y_m")),
("sar3d_half_width_z_m", "3D SAR", ("sar3d",), _attr("_sar3d_half_width_z_m")),
("sar3d_grid_nx", "3D SAR", ("sar3d",), _attr("_sar3d_grid_nx")),
("sar3d_grid_ny", "3D SAR", ("sar3d",), _attr("_sar3d_grid_ny")),
("sar3d_grid_nz", "3D SAR", ("sar3d",), _attr("_sar3d_grid_nz")),
("sar3d_use_compensation", "3D SAR", ("sar3d",), _attr("_sar3d_use_compensation")),
("sar3d_normalize_pairs", "3D SAR", ("sar3d",), _attr("_sar3d_normalize_pairs")),
]
_WEB_LIVE_GETTERS = {field: getter for field, _group, _modes, getter in _WEB_LIVE_SCHEMA}
@@ -101,7 +119,7 @@ _NON_WIDGET_LIVE_FIELDS = frozenset({
# config). They apply immediately through the same redraw as a live edit but write
# nothing to processing_live.json.
_WEB_DISPLAY_SCHEMA = [
("render_mode", "Display", _GPR_MODES, _dual("_gpr_render_mode", "_legacy_gpr_render_mode")),
("render_mode", "Display", _GPR_MODES, _dual("_gpr_render_mode", {"legacy_gpr": "_legacy_gpr_render_mode"})),
("subtract_mean_ascan", "Display", ("bscan",), _attr("_bscan_subtract_mean_ascan")),
("combo_filter", "Display", ("pass_through",), _attr("_pass_through_combo_filter_input")),
]
@@ -113,8 +131,8 @@ _WEB_STABLE_SCHEMA = [
# Coherent BP fixes the medium to eps_r = 1 (Horns_motion_3libre.py), so relative
# permittivity is a legacy-GPR-only knob; BP ignores it.
("relative_permittivity", "Geometry & medium", ("legacy_gpr",), _attr("_gpr_relative_permittivity")),
("tx_geometry", "Geometry & medium", _GPR_MODES, _attr("_gpr_tx_geometry_input")),
("rx_geometry", "Geometry & medium", _GPR_MODES, _attr("_gpr_rx_geometry_input")),
("tx_geometry", "Geometry & medium", _GPR_LIKE_MODES, _attr("_gpr_tx_geometry_input")),
("rx_geometry", "Geometry & medium", _GPR_LIKE_MODES, _attr("_gpr_rx_geometry_input")),
]
# Every field the web form renders and may write back, across all three categories.
@@ -222,6 +240,17 @@ class AppWindowLiveProcessingMixin:
"""Return whether a mode uses GPR result payloads and plot surface."""
return mode in {"gpr", "legacy_gpr"}
@staticmethod
def _is_gpr_like_processing_mode(mode: str) -> bool:
"""Return whether a mode consumes the shared gpr_* fields and Tx/Rx geometry.
Covers both GPR imagers and the 3D SAR processor. The latter renders on its
own plot surface, so GPR-object/locator branches must keep using
:meth:`_is_gpr_processing_mode`; this predicate gates only the shared
behaviors (result drain on settings changes, geometry page visibility).
"""
return mode in set(_GPR_LIKE_MODES)
def _validate_processing_mode_selection(self, mode: str) -> None:
"""Validate requested processing mode against current stable/live GUI state."""
validate_processing_mode_constraints(
@@ -352,7 +381,7 @@ class AppWindowLiveProcessingMixin:
return
try:
current_mode = self._processing_mode.currentText()
if self._is_gpr_processing_mode(current_mode):
if self._is_gpr_like_processing_mode(current_mode):
self._drain_results_until_quiet(timeout_s=0.05, poll_s=0.005)
self._write_live_processing_config()
@@ -364,16 +393,16 @@ class AppWindowLiveProcessingMixin:
# recompute of everything on screen. Debounced, so dragging a spin box
# sends one burst instead of one per step.
self._schedule_bscan_history_reprocess()
elif self._is_gpr_processing_mode(current_mode):
elif self._is_gpr_like_processing_mode(current_mode):
latest = self._drain_results_until_quiet(timeout_s=0.8, poll_s=0.02)
collection = latest
if collection is None and self._result_history:
collection = self._result_history[-1]
if collection is not None:
if not self._draw_results(collection):
if collection is None or not self._draw_results(collection):
if current_mode == "sar3d":
self._clear_sar3d_plot()
else:
self._clear_gpr_plot()
else:
self._clear_gpr_plot()
elif self._result_history:
self._draw_results(self._result_history[-1])
else:
@@ -427,9 +456,10 @@ class AppWindowLiveProcessingMixin:
"bscan": 1,
"gpr": 2,
"legacy_gpr": 3,
"sar3d": 4,
}
self._processing_mode_pages.setCurrentIndex(mode_to_page.get(mode, 0))
self._gpr_common_page.setVisible(self._is_gpr_processing_mode(mode))
self._gpr_common_page.setVisible(self._is_gpr_like_processing_mode(mode))
current_page = self._processing_mode_pages.currentWidget()
if current_page is not None:
self._processing_mode_pages.setFixedHeight(current_page.sizeHint().height())
@@ -509,9 +539,21 @@ class AppWindowLiveProcessingMixin:
f"render_mode={self._legacy_gpr_render_mode.currentText()}, "
f"min_pairs={self._legacy_gpr_min_visible_pair_count.value()})"
)
elif mode == "sar3d":
self._log(
"Processing mode selected: sar3d "
f"(inputs={self._sar3d_input_positions_input.text().strip() or '<all>'}, "
f"outputs={self._sar3d_output_positions_input.text().strip() or '<all>'}, "
f"depth={self._sar3d_min_depth_m.value():g}..{self._sar3d_max_depth_m.value():g} m, "
f"freq={self._sar3d_start_freq_mhz.value():g}..{self._sar3d_stop_freq_mhz.value():g} MHz, "
f"frame_radius={self._sar3d_frame_radius.value()}, "
f"grid={self._sar3d_grid_nx.value()}x{self._sar3d_grid_ny.value()}x{self._sar3d_grid_nz.value()}, "
f"center_depth={self._sar3d_center_depth_m.value():g} m, "
f"pitch={self._sar3d_pitch_deg.value():g} deg)"
)
def _clear_history_mode_caches(self) -> None:
"""Drop cached render state for pass-through, B-scan, and GPR views."""
"""Drop cached render state for pass-through, B-scan, GPR, and 3D SAR views."""
self._clear_bscan_plot_history()
if hasattr(self, "_bscan_plot"):
self._bscan_plot.clear()
@@ -519,6 +561,8 @@ class AppWindowLiveProcessingMixin:
self._clear_trace_plots()
if hasattr(self, "_gpr_plot"):
self._clear_gpr_plot()
if hasattr(self, "_sar3d_xz_plot"):
self._clear_sar3d_plot()
def _redraw_after_history_deletion(self) -> None:
"""Refresh plot immediately after destructive history deletion."""
@@ -534,6 +578,11 @@ class AppWindowLiveProcessingMixin:
return
self._clear_gpr_plot()
return
if self._processing_mode.currentText() == "sar3d":
if self._result_history and self._draw_results(self._result_history[-1]):
return
self._clear_sar3d_plot()
return
if self._result_history:
self._draw_results(self._result_history[-1])
return
@@ -336,6 +336,29 @@ class AppWindowConfigProfileIOMixin:
self._legacy_gpr_visible_x_max_m,
self._legacy_gpr_visible_z_min_m,
self._legacy_gpr_visible_z_max_m,
self._sar3d_input_positions_input,
self._sar3d_output_positions_input,
self._sar3d_min_depth_m,
self._sar3d_max_depth_m,
self._sar3d_start_freq_mhz,
self._sar3d_stop_freq_mhz,
self._sar3d_speed_m_s,
self._sar3d_ignore_socket_speed_enabled,
self._sar3d_look_angle_deg,
self._sar3d_direction_sign,
self._sar3d_apply_freq_phase_correction,
self._sar3d_frame_radius,
self._sar3d_edge_frame_weight,
self._sar3d_pitch_deg,
self._sar3d_center_depth_m,
self._sar3d_half_width_x_m,
self._sar3d_half_width_y_m,
self._sar3d_half_width_z_m,
self._sar3d_grid_nx,
self._sar3d_grid_ny,
self._sar3d_grid_nz,
self._sar3d_use_compensation,
self._sar3d_normalize_pairs,
self._save_count,
self._record_count,
self._save_path_input,
@@ -524,6 +547,31 @@ class AppWindowConfigProfileIOMixin:
self._legacy_gpr_visible_z_min_m.setValue(float(gui_state.processing.legacy_gpr.visible_z_min_m))
self._legacy_gpr_visible_z_max_m.setValue(float(gui_state.processing.legacy_gpr.visible_z_max_m))
sar3d_state = gui_state.processing.sar3d
self._sar3d_input_positions_input.setText(str(sar3d_state.input_positions))
self._sar3d_output_positions_input.setText(str(sar3d_state.output_positions))
self._sar3d_min_depth_m.setValue(float(sar3d_state.min_depth_m))
self._sar3d_max_depth_m.setValue(float(sar3d_state.max_depth_m))
self._sar3d_start_freq_mhz.setValue(float(sar3d_state.start_freq_mhz))
self._sar3d_stop_freq_mhz.setValue(float(sar3d_state.stop_freq_mhz))
self._sar3d_speed_m_s.setValue(float(sar3d_state.speed_m_s))
self._sar3d_ignore_socket_speed_enabled.setChecked(bool(sar3d_state.ignore_socket_speed_enabled))
self._sar3d_look_angle_deg.setValue(float(sar3d_state.look_angle_deg))
self._sar3d_direction_sign.setValue(float(sar3d_state.direction_sign))
self._sar3d_apply_freq_phase_correction.setChecked(bool(sar3d_state.apply_freq_phase_correction))
self._sar3d_frame_radius.setValue(int(sar3d_state.frame_radius))
self._sar3d_edge_frame_weight.setValue(float(sar3d_state.edge_frame_weight))
self._sar3d_pitch_deg.setValue(float(sar3d_state.pitch_deg))
self._sar3d_center_depth_m.setValue(float(sar3d_state.center_depth_m))
self._sar3d_half_width_x_m.setValue(float(sar3d_state.half_width_x_m))
self._sar3d_half_width_y_m.setValue(float(sar3d_state.half_width_y_m))
self._sar3d_half_width_z_m.setValue(float(sar3d_state.half_width_z_m))
self._sar3d_grid_nx.setValue(int(sar3d_state.grid_nx))
self._sar3d_grid_ny.setValue(int(sar3d_state.grid_ny))
self._sar3d_grid_nz.setValue(int(sar3d_state.grid_nz))
self._sar3d_use_compensation.setChecked(bool(sar3d_state.use_compensation))
self._sar3d_normalize_pairs.setChecked(bool(sar3d_state.normalize_pairs))
self._save_count.setValue(int(gui_state.data_actions.save_count))
self._record_count.setValue(int(gui_state.data_actions.record_count))
self._save_path_input.setText(str(gui_state.data_actions.save_path))
@@ -182,6 +182,8 @@ class AppWindowRadarLimitsMixin:
"_gpr_stop_freq_mhz": "GPR Stop MHz",
"_legacy_gpr_start_freq_mhz": "Legacy GPR Start MHz",
"_legacy_gpr_stop_freq_mhz": "Legacy GPR Stop MHz",
"_sar3d_start_freq_mhz": "3D SAR Start MHz",
"_sar3d_stop_freq_mhz": "3D SAR Stop MHz",
}
widgets = [getattr(self, widget_name) for widget_name in widget_names]
previous_values = {widget_name: getattr(self, widget_name).value() for widget_name in widget_names}
@@ -232,5 +234,7 @@ class AppWindowRadarLimitsMixin:
"_gpr_stop_freq_mhz",
"_legacy_gpr_start_freq_mhz",
"_legacy_gpr_stop_freq_mhz",
"_sar3d_start_freq_mhz",
"_sar3d_stop_freq_mhz",
)
)
@@ -13,6 +13,7 @@ from python_app.models.gui_profile_model import (
GuiPreprocessDialogStateModel,
GuiProcessingStateModel,
GuiProfileModel,
GuiSar3dStateModel,
GuiStateModel,
GuiSwitchStateModel,
)
@@ -285,6 +286,10 @@ class AppWindowConfigStateBuildersMixin:
visible_z_min_m=0.0,
visible_z_max_m=14.0,
),
sar3d=GuiSar3dStateModel(
input_positions=self._default_gpr_input_positions_from_config(config),
output_positions=self._default_gpr_output_positions_from_config(config),
),
),
data_actions=GuiDataActionsStateModel(
save_count=10,
@@ -418,6 +423,31 @@ class AppWindowConfigStateBuildersMixin:
visible_z_min_m=float(self._legacy_gpr_visible_z_min_m.value()),
visible_z_max_m=float(self._legacy_gpr_visible_z_max_m.value()),
),
sar3d=GuiSar3dStateModel(
input_positions=self._sar3d_input_positions_input.text().strip(),
output_positions=self._sar3d_output_positions_input.text().strip(),
min_depth_m=float(self._sar3d_min_depth_m.value()),
max_depth_m=float(self._sar3d_max_depth_m.value()),
start_freq_mhz=float(self._sar3d_start_freq_mhz.value()),
stop_freq_mhz=float(self._sar3d_stop_freq_mhz.value()),
speed_m_s=float(self._sar3d_speed_m_s.value()),
ignore_socket_speed_enabled=bool(self._sar3d_ignore_socket_speed_enabled.isChecked()),
look_angle_deg=float(self._sar3d_look_angle_deg.value()),
direction_sign=float(self._sar3d_direction_sign.value()),
apply_freq_phase_correction=bool(self._sar3d_apply_freq_phase_correction.isChecked()),
frame_radius=int(self._sar3d_frame_radius.value()),
edge_frame_weight=float(self._sar3d_edge_frame_weight.value()),
pitch_deg=float(self._sar3d_pitch_deg.value()),
center_depth_m=float(self._sar3d_center_depth_m.value()),
half_width_x_m=float(self._sar3d_half_width_x_m.value()),
half_width_y_m=float(self._sar3d_half_width_y_m.value()),
half_width_z_m=float(self._sar3d_half_width_z_m.value()),
grid_nx=int(self._sar3d_grid_nx.value()),
grid_ny=int(self._sar3d_grid_ny.value()),
grid_nz=int(self._sar3d_grid_nz.value()),
use_compensation=bool(self._sar3d_use_compensation.isChecked()),
normalize_pairs=bool(self._sar3d_normalize_pairs.isChecked()),
),
),
data_actions=GuiDataActionsStateModel(
save_count=int(self._save_count.value()),
@@ -472,7 +502,7 @@ class AppWindowConfigStateBuildersMixin:
combo_text = self._combos_text.text()
config.combos = parse_combos_from_text(combo_text)
config.ensure_combos()
if self._processing_mode.currentText() not in {"gpr", "legacy_gpr"} and self._switches_are_effectively_static(config):
if self._processing_mode.currentText() not in {"gpr", "legacy_gpr", "sar3d"} and self._switches_are_effectively_static(config):
config.combos = [ComboModel(input=0, output=0)]
for key in PREPROCESS_ASSET_KEYS:
@@ -3,11 +3,13 @@
from python_app.gui.controllers.app_window_plot.bscan_plot_mixin import AppWindowBscanPlotMixin
from python_app.gui.controllers.app_window_plot.bscan_replay_mixin import AppWindowBscanReplayMixin
from python_app.gui.controllers.app_window_plot.gpr_plot_mixin import AppWindowGprPlotMixin
from python_app.gui.controllers.app_window_plot.sar3d_plot_mixin import AppWindowSar3dPlotMixin
from python_app.gui.controllers.app_window_plot.trace_plot_mixin import AppWindowTracePlotMixin
__all__ = [
"AppWindowBscanPlotMixin",
"AppWindowBscanReplayMixin",
"AppWindowGprPlotMixin",
"AppWindowSar3dPlotMixin",
"AppWindowTracePlotMixin",
]
@@ -0,0 +1,131 @@
"""3D SAR projection rendering helpers.
The sar3d processor publishes two normalized max-projections of the imaged
volume as collection-level ImageF32 payloads:
- ``sar3d_proj_xz`` — side view: X (m) horizontal, Z/range (m) vertical;
- ``sar3d_proj_xy`` — top view: X (m) horizontal, Y (m) vertical;
plus ``sar3d_peak`` (ScalarF32), the volume peak before normalization. Both
projections are drawn on their own PlotWidgets stacked in one plot-stack page
(side view on top), mirroring how the pass-through page stacks its two plots.
"""
from __future__ import annotations
from PyQt6.QtCore import QRectF
import numpy as np
import pyqtgraph as pg
from python_app.models.dataset_model import ResultCollection
class AppWindowSar3dPlotMixin:
"""Renders 3D SAR volume projections published by the sar3d processor."""
def _configure_sar3d_plot_axes(self) -> None:
"""Apply persistent axis labels and view settings to both projection plots."""
for plot, y_label in (
(self._sar3d_xz_plot, "Range Z"),
(self._sar3d_xy_plot, "Cross-range Y"),
):
plot_item = plot.getPlotItem()
plot_item.showAxis("left", show=True)
plot_item.showAxis("bottom", show=True)
plot_item.setClipToView(True)
plot.setLabel("bottom", "X", units="m")
plot.setLabel("left", y_label, units="m")
view_box = plot.getViewBox()
view_box.invertY(False)
view_box.enableAutoRange(x=False, y=False)
self._disable_gpr_plot_interaction(plot, view_box)
def _ensure_sar3d_plot_items(self) -> None:
"""Create persistent projection image items once and reuse them on redraw."""
if self._sar3d_xz_image_item is not None:
return
self._configure_sar3d_plot_axes()
if self._sar3d_lookup_table is None:
# Same palette as the GPR accumulator so both imagers read alike.
self._sar3d_lookup_table = self._build_lut(["#081c15", "#1b4332", "#ffd166", "#f94144"])
self._sar3d_xz_image_item = pg.ImageItem(axisOrder="row-major")
self._sar3d_xz_image_item.setZValue(0)
self._sar3d_xz_image_item.hide()
self._sar3d_xz_plot.addItem(self._sar3d_xz_image_item)
self._sar3d_xy_image_item = pg.ImageItem(axisOrder="row-major")
self._sar3d_xy_image_item.setZValue(0)
self._sar3d_xy_image_item.hide()
self._sar3d_xy_plot.addItem(self._sar3d_xy_image_item)
def _clear_sar3d_plot(self) -> None:
"""Clear both 3D SAR projection surfaces."""
if not hasattr(self, "_sar3d_xz_plot"):
return
self._configure_sar3d_plot_axes()
if self._sar3d_xz_image_item is not None:
self._sar3d_xz_image_item.hide()
if self._sar3d_xy_image_item is not None:
self._sar3d_xy_image_item.hide()
self._sar3d_xz_plot.setTitle("3D SAR side view (X-Z)")
self._sar3d_xy_plot.setTitle("3D SAR top view (X-Y)")
def _draw_sar3d_projection(self, plot: pg.PlotWidget, image_item: pg.ImageItem, payload) -> bool:
"""Draw one normalized projection payload onto its plot; return success."""
image = np.asarray(payload.image, dtype=np.float32)
x_axis = np.asarray(payload.image_x_axis, dtype=np.float32)
y_axis = np.asarray(payload.image_y_axis, dtype=np.float32)
if image.ndim != 2 or image.size == 0 or x_axis.size == 0 or y_axis.size == 0:
image_item.hide()
return False
x_min = float(x_axis[0])
x_max = float(x_axis[-1])
y_min = float(y_axis[0])
y_max = float(y_axis[-1])
rect = QRectF(x_min, y_min, max(x_max - x_min, 1e-6), max(y_max - y_min, 1e-6))
image_item.setImage(image, autoLevels=False)
image_item.setRect(rect)
image_item.setLookupTable(self._sar3d_lookup_table)
# Projections are normalized to [0, 1] by the processor.
image_item.setLevels((0.0, 1.0))
image_item.show()
plot.setXRange(x_min, x_max, padding=0.0)
plot.setYRange(y_min, y_max, padding=0.0)
return True
def _draw_sar3d_projections(self, collection: ResultCollection) -> bool:
"""Draw the latest collection-level 3D SAR side and top projections."""
xz_payload = self._collection_payload_by_name(collection, "sar3d_proj_xz", kind=3)
xy_payload = self._collection_payload_by_name(collection, "sar3d_proj_xy", kind=3)
if xz_payload is None and xy_payload is None:
self._clear_sar3d_plot()
return False
for plot in (self._sar3d_xz_plot, self._sar3d_xy_plot):
plot.setUpdatesEnabled(False)
try:
self._ensure_sar3d_plot_items()
xz_drawn = xz_payload is not None and self._draw_sar3d_projection(
self._sar3d_xz_plot, self._sar3d_xz_image_item, xz_payload
)
xy_drawn = xy_payload is not None and self._draw_sar3d_projection(
self._sar3d_xy_plot, self._sar3d_xy_image_item, xy_payload
)
if not xz_drawn and not xy_drawn:
self._clear_sar3d_plot()
return False
peak_payload = self._collection_payload_by_name(collection, "sar3d_peak", kind=2)
peak_suffix = ""
if peak_payload is not None:
peak_suffix = f" | peak={float(peak_payload.scalar_value):.3g}"
self._sar3d_xz_plot.setTitle(f"3D SAR side view (X-Z){peak_suffix}")
self._sar3d_xy_plot.setTitle("3D SAR top view (X-Y)")
finally:
for plot in (self._sar3d_xz_plot, self._sar3d_xy_plot):
plot.setUpdatesEnabled(True)
return True
@@ -78,7 +78,7 @@ class AppWindowTracePlotMixin:
def _on_trace_visibility_changed(self, *_args) -> None:
"""Redraw pass-through traces when magnitude/phase toggles changed."""
if self._processing_mode.currentText() in {"bscan", "gpr", "legacy_gpr"}:
if self._processing_mode.currentText() in {"bscan", "gpr", "legacy_gpr", "sar3d"}:
return
if self._result_history:
self._draw_results(self._result_history[-1])
@@ -6,6 +6,7 @@ from python_app.gui.controllers.app_window_plot import (
AppWindowBscanPlotMixin,
AppWindowBscanReplayMixin,
AppWindowGprPlotMixin,
AppWindowSar3dPlotMixin,
AppWindowTracePlotMixin,
)
from python_app.models.dataset_model import ResultCollection
@@ -16,8 +17,9 @@ class AppWindowPlotMixin(
AppWindowBscanPlotMixin,
AppWindowBscanReplayMixin,
AppWindowGprPlotMixin,
AppWindowSar3dPlotMixin,
):
"""Routes plotting to trace, B-scan, or GPR-specific mixins."""
"""Routes plotting to trace, B-scan, GPR, or 3D SAR-specific mixins."""
def _draw_preferred_collection(
self,
@@ -35,6 +37,8 @@ class AppWindowPlotMixin(
return self._draw_bscan_heatmap(collection)
if self._processing_mode.currentText() in {"gpr", "legacy_gpr"}:
return self._draw_gpr_map(collection)
if self._processing_mode.currentText() == "sar3d":
return self._draw_sar3d_projections(collection)
return self._draw_trace_lines(collection)
def _result_collection_has_trace(self, collection: ResultCollection) -> bool:
@@ -74,6 +74,7 @@ class AppWindowUiMixin:
self._plot_stack = QStackedWidget(root)
self._build_bscan_plot_page()
self._build_gpr_plot_page()
self._build_sar3d_plot_page()
self._build_trace_plot_page()
# Default view on startup is pass-through traces.
@@ -139,6 +140,27 @@ class AppWindowUiMixin:
self._configure_gpr_plot_axes()
self._plot_stack.addWidget(self._gpr_plot)
def _build_sar3d_plot_page(self) -> None:
"""Create 3D SAR page with stacked side (X-Z) and top (X-Y) projection plots."""
# 3D SAR surface: container with two projection heatmaps, mirroring the
# pass-through page layout (two stacked PlotWidgets on one stack page).
self._sar3d_plots_container = QWidget()
sar3d_layout = QVBoxLayout(self._sar3d_plots_container)
sar3d_layout.setContentsMargins(0, 0, 0, 0)
sar3d_layout.setSpacing(6)
self._sar3d_xz_plot = self._create_plot_widget(background="#0f141c")
self._sar3d_xz_plot.showGrid(x=True, y=True, alpha=0.2)
sar3d_layout.addWidget(self._sar3d_xz_plot, stretch=1)
self._sar3d_xy_plot = self._create_plot_widget(background="#0f141c")
self._sar3d_xy_plot.showGrid(x=True, y=True, alpha=0.2)
sar3d_layout.addWidget(self._sar3d_xy_plot, stretch=1)
self._configure_sar3d_plot_axes()
self._clear_sar3d_plot()
self._plot_stack.addWidget(self._sar3d_plots_container)
def _build_settings_toggle(self, root_layout: QHBoxLayout) -> None:
"""Create narrow button used to collapse or show settings panel."""
self._settings_toggle_button = QPushButton("<")
@@ -286,6 +308,7 @@ class AppWindowUiMixin:
`bscan` -> show `self._bscan_plot`
`gpr` and `legacy_gpr` -> show `self._gpr_plot`
`sar3d` -> show `self._sar3d_plots_container`
otherwise -> show `self._trace_plots_container`
"""
if mode == "bscan":
@@ -294,6 +317,9 @@ class AppWindowUiMixin:
if mode in {"gpr", "legacy_gpr"}:
self._plot_stack.setCurrentWidget(self._gpr_plot)
return
if mode == "sar3d":
self._plot_stack.setCurrentWidget(self._sar3d_plots_container)
return
self._plot_stack.setCurrentWidget(self._trace_plots_container)
@staticmethod
@@ -59,7 +59,7 @@ def _build_processing_mode_page(parent: QStackedWidget, rows: list[FormRow], *,
def build_processing_group(owner) -> QGroupBox:
"""Create processing mode section with pass-through, B-scan, and GPR pages."""
"""Create processing mode section with pass-through, B-scan, GPR, and 3D SAR pages."""
group = QGroupBox("Processing")
form = QFormLayout(group)
form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
@@ -68,9 +68,10 @@ def build_processing_group(owner) -> QGroupBox:
bscan_defaults = processing_defaults.bscan
gpr_live_defaults = processing_defaults.gpr
legacy_gpr_defaults = processing_defaults.legacy_gpr
sar3d_defaults = processing_defaults.sar3d
owner._processing_mode = QComboBox()
owner._processing_mode.addItems(["pass_through", "bscan", "gpr", "legacy_gpr"])
owner._processing_mode.addItems(["pass_through", "bscan", "gpr", "legacy_gpr", "sar3d"])
owner._set_combo_current_text(owner._processing_mode, processing_defaults.selected_mode)
owner._processing_mode_pages = QStackedWidget(group)
@@ -570,6 +571,172 @@ def build_processing_group(owner) -> QGroupBox:
)
owner._processing_mode_pages.addWidget(legacy_gpr_page)
# 3D SAR page: shared gpr_* knobs get their own widget set (like legacy_gpr)
# plus the sar3d-specific aperture/volume knobs consumed by the C++ processor.
owner._sar3d_input_positions_input = QLineEdit(str(sar3d_defaults.input_positions))
owner._sar3d_input_positions_input.setPlaceholderText("0,1,2")
owner._sar3d_output_positions_input = QLineEdit(str(sar3d_defaults.output_positions))
owner._sar3d_output_positions_input.setPlaceholderText("0,1")
owner._sar3d_min_depth_m = QDoubleSpinBox()
owner._sar3d_min_depth_m.setDecimals(2)
owner._sar3d_min_depth_m.setRange(0.0, 50.0)
owner._sar3d_min_depth_m.setSingleStep(0.1)
owner._sar3d_min_depth_m.setValue(float(sar3d_defaults.min_depth_m))
owner._sar3d_max_depth_m = QDoubleSpinBox()
owner._sar3d_max_depth_m.setDecimals(2)
owner._sar3d_max_depth_m.setRange(0.1, 50.0)
owner._sar3d_max_depth_m.setSingleStep(0.1)
owner._sar3d_max_depth_m.setValue(float(sar3d_defaults.max_depth_m))
owner._sar3d_start_freq_mhz = QDoubleSpinBox()
owner._sar3d_start_freq_mhz.setDecimals(1)
owner._sar3d_start_freq_mhz.setRange(100.0, 8800.0)
owner._sar3d_start_freq_mhz.setSingleStep(10.0)
owner._sar3d_start_freq_mhz.setValue(float(sar3d_defaults.start_freq_mhz))
owner._sar3d_stop_freq_mhz = QDoubleSpinBox()
owner._sar3d_stop_freq_mhz.setDecimals(1)
owner._sar3d_stop_freq_mhz.setRange(100.0, 8800.0)
owner._sar3d_stop_freq_mhz.setSingleStep(10.0)
owner._sar3d_stop_freq_mhz.setValue(float(sar3d_defaults.stop_freq_mhz))
owner._sar3d_look_angle_deg = QDoubleSpinBox()
owner._sar3d_look_angle_deg.setDecimals(2)
owner._sar3d_look_angle_deg.setRange(-90.0, 90.0)
owner._sar3d_look_angle_deg.setSingleStep(0.5)
owner._sar3d_look_angle_deg.setValue(float(sar3d_defaults.look_angle_deg))
owner._sar3d_look_angle_deg.setToolTip(
"Radar look angle vs the range axis (deg). Used by intra-sweep motion correction."
)
owner._sar3d_direction_sign = QDoubleSpinBox()
owner._sar3d_direction_sign.setDecimals(0)
owner._sar3d_direction_sign.setRange(-1.0, 1.0)
owner._sar3d_direction_sign.setSingleStep(2.0)
owner._sar3d_direction_sign.setValue(float(sar3d_defaults.direction_sign))
owner._sar3d_direction_sign.setToolTip(
"Motion direction along range: +1 (later frequencies deeper) or -1 (shallower)."
)
owner._sar3d_ignore_socket_speed_enabled = QCheckBox("Ignore socket speed")
owner._sar3d_ignore_socket_speed_enabled.setChecked(bool(sar3d_defaults.ignore_socket_speed_enabled))
owner._sar3d_ignore_socket_speed_enabled.setToolTip(
"When checked, use the speed below for motion correction instead of the "
"value streamed over the locator socket."
)
owner._sar3d_speed_m_s = QDoubleSpinBox()
owner._sar3d_speed_m_s.setDecimals(3)
owner._sar3d_speed_m_s.setRange(-100.0, 100.0)
owner._sar3d_speed_m_s.setSingleStep(0.05)
owner._sar3d_speed_m_s.setValue(float(sar3d_defaults.speed_m_s))
owner._sar3d_speed_m_s.setToolTip(
"Radar speed (m/s) used by intra-sweep motion correction when 'Ignore socket speed' is on."
)
owner._sar3d_apply_freq_phase_correction = QCheckBox("Apply intra-sweep phase correction")
owner._sar3d_apply_freq_phase_correction.setChecked(bool(sar3d_defaults.apply_freq_phase_correction))
owner._sar3d_frame_radius = QSpinBox()
owner._sar3d_frame_radius.setRange(0, 25)
owner._sar3d_frame_radius.setValue(int(sar3d_defaults.frame_radius))
owner._sar3d_frame_radius.setToolTip(
"Synthetic-aperture half width in frames: 2*radius+1 consecutive frames form one aperture."
)
owner._sar3d_edge_frame_weight = QDoubleSpinBox()
owner._sar3d_edge_frame_weight.setDecimals(2)
owner._sar3d_edge_frame_weight.setRange(0.0, 1.0)
owner._sar3d_edge_frame_weight.setSingleStep(0.05)
owner._sar3d_edge_frame_weight.setValue(float(sar3d_defaults.edge_frame_weight))
owner._sar3d_edge_frame_weight.setToolTip(
"Weight of the outermost aperture frames (1 = flat window, 0 = strong taper)."
)
owner._sar3d_pitch_deg = QDoubleSpinBox()
owner._sar3d_pitch_deg.setDecimals(2)
owner._sar3d_pitch_deg.setRange(-90.0, 90.0)
owner._sar3d_pitch_deg.setSingleStep(0.5)
owner._sar3d_pitch_deg.setValue(float(sar3d_defaults.pitch_deg))
owner._sar3d_pitch_deg.setToolTip("Antenna pitch above the horizon (deg); tilts the imaged volume.")
owner._sar3d_center_depth_m = QDoubleSpinBox()
owner._sar3d_center_depth_m.setDecimals(2)
owner._sar3d_center_depth_m.setRange(0.0, 50.0)
owner._sar3d_center_depth_m.setSingleStep(0.1)
owner._sar3d_center_depth_m.setValue(float(sar3d_defaults.center_depth_m))
owner._sar3d_center_depth_m.setToolTip("Range distance to the center of the imaged volume (m).")
owner._sar3d_half_width_x_m = QDoubleSpinBox()
owner._sar3d_half_width_x_m.setDecimals(2)
owner._sar3d_half_width_x_m.setRange(0.01, 50.0)
owner._sar3d_half_width_x_m.setSingleStep(0.1)
owner._sar3d_half_width_x_m.setValue(float(sar3d_defaults.half_width_x_m))
owner._sar3d_half_width_y_m = QDoubleSpinBox()
owner._sar3d_half_width_y_m.setDecimals(2)
owner._sar3d_half_width_y_m.setRange(0.01, 50.0)
owner._sar3d_half_width_y_m.setSingleStep(0.1)
owner._sar3d_half_width_y_m.setValue(float(sar3d_defaults.half_width_y_m))
owner._sar3d_half_width_z_m = QDoubleSpinBox()
owner._sar3d_half_width_z_m.setDecimals(2)
owner._sar3d_half_width_z_m.setRange(0.01, 50.0)
owner._sar3d_half_width_z_m.setSingleStep(0.1)
owner._sar3d_half_width_z_m.setValue(float(sar3d_defaults.half_width_z_m))
owner._sar3d_grid_nx = QSpinBox()
owner._sar3d_grid_nx.setRange(2, 1024)
owner._sar3d_grid_nx.setValue(int(sar3d_defaults.grid_nx))
owner._sar3d_grid_ny = QSpinBox()
owner._sar3d_grid_ny.setRange(2, 1024)
owner._sar3d_grid_ny.setValue(int(sar3d_defaults.grid_ny))
owner._sar3d_grid_nz = QSpinBox()
owner._sar3d_grid_nz.setRange(2, 1024)
owner._sar3d_grid_nz.setValue(int(sar3d_defaults.grid_nz))
owner._sar3d_use_compensation = QCheckBox("Use per-voxel compensation")
owner._sar3d_use_compensation.setChecked(bool(sar3d_defaults.use_compensation))
owner._sar3d_normalize_pairs = QCheckBox("Normalize Tx/Rx pairs")
owner._sar3d_normalize_pairs.setChecked(bool(sar3d_defaults.normalize_pairs))
sar3d_page = _build_processing_mode_page(
owner._processing_mode_pages,
[
("Input positions", owner._sar3d_input_positions_input),
("Output positions", owner._sar3d_output_positions_input),
("Min depth m", owner._sar3d_min_depth_m),
("Max depth m", owner._sar3d_max_depth_m),
("Start MHz", owner._sar3d_start_freq_mhz),
("Stop MHz", owner._sar3d_stop_freq_mhz),
("Look angle deg", owner._sar3d_look_angle_deg),
("Direction sign", owner._sar3d_direction_sign),
owner._sar3d_ignore_socket_speed_enabled,
("Speed m/s", owner._sar3d_speed_m_s),
owner._sar3d_apply_freq_phase_correction,
("Frame radius", owner._sar3d_frame_radius),
("Edge frame weight", owner._sar3d_edge_frame_weight),
("Pitch deg", owner._sar3d_pitch_deg),
("Center depth m", owner._sar3d_center_depth_m),
("Half width X m", owner._sar3d_half_width_x_m),
("Half width Y m", owner._sar3d_half_width_y_m),
("Half width Z m", owner._sar3d_half_width_z_m),
("Grid Nx", owner._sar3d_grid_nx),
("Grid Ny", owner._sar3d_grid_ny),
("Grid Nz", owner._sar3d_grid_nz),
owner._sar3d_use_compensation,
owner._sar3d_normalize_pairs,
],
split_index=11,
)
owner._processing_mode_pages.addWidget(sar3d_page)
owner._processing_mode.currentTextChanged.connect(owner._on_processing_mode_changed)
owner._show_magnitude_checkbox.toggled.connect(owner._on_trace_visibility_changed)
owner._show_phase_checkbox.toggled.connect(owner._on_trace_visibility_changed)
@@ -642,6 +809,29 @@ def build_processing_group(owner) -> QGroupBox:
owner._legacy_gpr_visible_x_max_m.valueChanged.connect(owner._on_gpr_locator_window_changed)
owner._legacy_gpr_visible_z_min_m.valueChanged.connect(owner._on_gpr_locator_window_changed)
owner._legacy_gpr_visible_z_max_m.valueChanged.connect(owner._on_gpr_locator_window_changed)
owner._sar3d_input_positions_input.editingFinished.connect(owner._on_processing_live_settings_changed)
owner._sar3d_output_positions_input.editingFinished.connect(owner._on_processing_live_settings_changed)
owner._sar3d_min_depth_m.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_max_depth_m.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_start_freq_mhz.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_stop_freq_mhz.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_look_angle_deg.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_direction_sign.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_ignore_socket_speed_enabled.toggled.connect(owner._on_processing_live_settings_changed)
owner._sar3d_speed_m_s.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_apply_freq_phase_correction.toggled.connect(owner._on_processing_live_settings_changed)
owner._sar3d_frame_radius.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_edge_frame_weight.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_pitch_deg.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_center_depth_m.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_half_width_x_m.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_half_width_y_m.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_half_width_z_m.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_grid_nx.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_grid_ny.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_grid_nz.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_use_compensation.toggled.connect(owner._on_processing_live_settings_changed)
owner._sar3d_normalize_pairs.toggled.connect(owner._on_processing_live_settings_changed)
owner._set_processing_mode_page(owner._processing_mode.currentText())
return group
+3 -1
View File
@@ -24,7 +24,9 @@ def validate_processing_mode_constraints(
)
return
if processing_mode not in {"gpr", "legacy_gpr"}:
# sar3d shares the GPR combo/geometry requirements: it images from the same
# multi-Tx/Rx pair set selected through the gpr_* live-config fields.
if processing_mode not in {"gpr", "legacy_gpr", "sar3d"}:
return
configured_combos = {(int(combo.input), int(combo.output)) for combo in config.combos}