sar3d: GPU port of the notebook with four views

The sar3d processor is rewritten as a checked port of Multi_antenna_3D_clean_v1.ipynb:
NVRTC kernels, timing from the trace stamps, notebook parameter names and one cubic
SAR_VOXEL_M. It adds front, side, top and 3D views, drops gpr_cpp and the parity tools,
and the GUI speed now applies only while the socket speed is ignored.
This commit is contained in:
Ayzen
2026-10-02 16:05:22 +03:00
parent 61ef8720e1
commit 45861f4047
50 changed files with 1737 additions and 8144 deletions
@@ -13,7 +13,6 @@ from PyQt6.QtWidgets import (
)
from python_app.gui.runtime.constraints import validate_processing_mode_constraints
from python_app.gui.runtime.sar3d_views import sar3d_bottom_view_deg_from_label
from python_app.orchestration.live_processing_config import ProcessingLiveConfig
_GPR_MODES = ("gpr", "legacy_gpr")
@@ -83,32 +82,17 @@ _WEB_LIVE_SCHEMA = [
("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_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_direction_sign", "Motion", ("gpr",), _attr("_gpr_direction_sign")),
("gpr_apply_freq_phase_correction", "Motion", ("legacy_gpr",), _attr("_legacy_gpr_apply_freq_phase_correction")),
("gpr_reference_mode", "Motion", ("legacy_gpr",), _attr("_legacy_gpr_reference_mode")),
("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_override_enabled", "3D SAR", ("sar3d",), _attr("_sar3d_pitch_override_enabled")),
("sar3d_pitch_deg", "3D SAR", ("sar3d",), _attr("_sar3d_pitch_deg")),
("sar3d_ground_depth_m", "3D SAR", ("sar3d",), _attr("_sar3d_ground_depth_m")),
("sar3d_x_min_m", "3D SAR", ("sar3d",), _attr("_sar3d_x_min_m")),
("sar3d_x_max_m", "3D SAR", ("sar3d",), _attr("_sar3d_x_max_m")),
("sar3d_y_min_m", "3D SAR", ("sar3d",), _attr("_sar3d_y_min_m")),
("sar3d_y_max_m", "3D SAR", ("sar3d",), _attr("_sar3d_y_max_m")),
("sar3d_z_min_m", "3D SAR", ("sar3d",), _attr("_sar3d_z_min_m")),
("sar3d_z_max_m", "3D SAR", ("sar3d",), _attr("_sar3d_z_max_m")),
("sar3d_voxel_x_m", "3D SAR", ("sar3d",), _attr("_sar3d_voxel_x_m")),
("sar3d_voxel_y_m", "3D SAR", ("sar3d",), _attr("_sar3d_voxel_y_m")),
("sar3d_voxel_z_m", "3D SAR", ("sar3d",), _attr("_sar3d_voxel_z_m")),
("sar3d_norm_edge_margin_m", "3D SAR", ("sar3d",), _attr("_sar3d_norm_edge_margin_m")),
("sar3d_smooth_sigma", "3D SAR", ("sar3d",), _attr("_sar3d_smooth_sigma")),
("sar3d_voxel_m", "3D SAR", ("sar3d",), _attr("_sar3d_voxel_m")),
("sar3d_bp_ground_depth_m", "3D SAR", ("sar3d",), _attr("_sar3d_bp_ground_depth_m")),
("sar3d_comp_range_power", "3D SAR", ("sar3d",), _attr("_sar3d_comp_range_power")),
("sar3d_tx_per_segment", "3D SAR", ("sar3d",), _attr("_sar3d_tx_per_segment")),
("sar3d_use_compensation", "3D SAR", ("sar3d",), _attr("_sar3d_use_compensation")),
("sar3d_normalize_pairs", "3D SAR", ("sar3d",), _attr("_sar3d_normalize_pairs")),
("sar3d_bottom_view_deg", "3D SAR", ("sar3d",), _attr("_sar3d_bottom_view")),
("sar3d_normalize_each_frame", "3D SAR", ("sar3d",), _attr("_sar3d_normalize_each_frame")),
]
_WEB_LIVE_GETTERS = {field: getter for field, _group, _modes, getter in _WEB_LIVE_SCHEMA}
@@ -259,7 +243,7 @@ class AppWindowLiveProcessingMixin:
: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)
return mode in _GPR_LIKE_MODES
def _validate_processing_mode_selection(self, mode: str) -> None:
"""Validate requested processing mode against current stable/live GUI state."""
@@ -295,10 +279,6 @@ class AppWindowLiveProcessingMixin:
# Switch-position CSV text -> integer lists.
values["gpr_input_positions"] = self._parse_csv_int_list(values["gpr_input_positions"])
values["gpr_output_positions"] = self._parse_csv_int_list(values["gpr_output_positions"])
# Bottom-view combo label -> projection angle in degrees.
values["sar3d_bottom_view_deg"] = sar3d_bottom_view_deg_from_label(
str(values["sar3d_bottom_view_deg"])
)
# Order-normalize the pass-through Y range so a reversed entry still works.
y_min_db, y_max_db = values["pass_through_y_min_db"], values["pass_through_y_max_db"]
values["pass_through_y_min_db"] = min(y_min_db, y_max_db)
@@ -424,13 +404,23 @@ class AppWindowLiveProcessingMixin:
except Exception as exc: # noqa: BLE001
self._show_exception("Failed to update live processing settings", exc)
def _on_legacy_gpr_ignore_socket_speed_toggled(self, ignore_socket_speed: bool) -> None:
"""Reflect socket/manual speed authority in the GUI and live config."""
try:
self._legacy_gpr_speed_m_s.setEnabled(bool(ignore_socket_speed))
self._write_live_processing_config()
except Exception as exc: # noqa: BLE001
self._show_exception("Failed to update socket-speed mode", exc)
def _sync_socket_speed_controls(self) -> None:
"""Enable each GPR-like speed field only while its page ignores the socket speed.
Unchecked, the speed a locator client streams drives the processing and the
field only stands in while no client sends one, so it must not look editable.
"""
for checkbox, speed in (
(self._gpr_ignore_socket_speed_enabled, self._gpr_speed_m_s),
(self._legacy_gpr_ignore_socket_speed_enabled, self._legacy_gpr_speed_m_s),
(self._sar3d_ignore_socket_speed_enabled, self._sar3d_speed_m_s),
):
speed.setEnabled(checkbox.isChecked())
def _on_ignore_socket_speed_toggled(self, *_args) -> None:
"""Switch the speed authority between the socket client and the GUI field."""
self._sync_socket_speed_controls()
self._on_processing_live_settings_changed()
def _on_gpr_visual_settings_changed(self, *_args) -> None:
"""Redraw current GPR result using updated GUI-only render settings."""
@@ -556,13 +546,13 @@ class AppWindowLiveProcessingMixin:
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"voxel={self._sar3d_voxel_x_m.value():g}x{self._sar3d_voxel_y_m.value():g}x{self._sar3d_voxel_z_m.value():g} m, "
f"ground_depth={self._sar3d_ground_depth_m.value():g} m)"
f"(INPUT_IDX={self._sar3d_input_positions_input.text().strip() or '<all>'}, "
f"OUTPUT_IDX={self._sar3d_output_positions_input.text().strip() or '<all>'}, "
f"F={self._sar3d_start_freq_mhz.value():g}..{self._sar3d_stop_freq_mhz.value():g} MHz, "
f"DEPTH={self._sar3d_min_depth_m.value():g}..{self._sar3d_max_depth_m.value():g} m, "
f"SAR_VOXEL_M={self._sar3d_voxel_m.value():g}, "
f"LOOK_ANGLE_DEG={self._sar3d_look_angle_deg.value():g}, "
f"SAR_FRAME_RADIUS={self._sar3d_frame_radius.value()})"
)
def _clear_history_mode_caches(self) -> None:
@@ -574,7 +564,7 @@ class AppWindowLiveProcessingMixin:
self._clear_trace_plots()
if hasattr(self, "_gpr_plot"):
self._clear_gpr_plot()
if hasattr(self, "_sar3d_xz_plot"):
if hasattr(self, "_sar3d_plots"):
self._clear_sar3d_plot()
def _redraw_after_history_deletion(self) -> None:
@@ -10,7 +10,6 @@ from pathlib import Path
from PyQt6.QtCore import QSignalBlocker
from PyQt6.QtWidgets import QFileDialog
from python_app.gui.runtime.sar3d_views import sar3d_bottom_view_label_from_deg
from python_app.models.gui_profile_model import GuiProfileModel
from python_app.models.run_config_model import RunConfigModel
from python_app.orchestration.preprocess_assets import (
@@ -346,28 +345,12 @@ class AppWindowConfigProfileIOMixin:
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_override_enabled,
self._sar3d_pitch_deg,
self._sar3d_ground_depth_m,
self._sar3d_x_min_m,
self._sar3d_x_max_m,
self._sar3d_y_min_m,
self._sar3d_y_max_m,
self._sar3d_z_min_m,
self._sar3d_z_max_m,
self._sar3d_voxel_x_m,
self._sar3d_voxel_y_m,
self._sar3d_voxel_z_m,
self._sar3d_norm_edge_margin_m,
self._sar3d_smooth_sigma,
self._sar3d_voxel_m,
self._sar3d_bp_ground_depth_m,
self._sar3d_comp_range_power,
self._sar3d_tx_per_segment,
self._sar3d_use_compensation,
self._sar3d_normalize_pairs,
self._sar3d_normalize_each_frame,
self._save_count,
self._record_count,
self._save_path_input,
@@ -532,11 +515,9 @@ class AppWindowConfigProfileIOMixin:
self._legacy_gpr_start_freq_mhz.setValue(float(gui_state.processing.legacy_gpr.start_freq_mhz))
self._legacy_gpr_stop_freq_mhz.setValue(float(gui_state.processing.legacy_gpr.stop_freq_mhz))
self._legacy_gpr_speed_m_s.setValue(float(gui_state.processing.legacy_gpr.speed_m_s))
ignore_socket_speed_enabled = bool(
gui_state.processing.legacy_gpr.ignore_socket_speed_enabled
self._legacy_gpr_ignore_socket_speed_enabled.setChecked(
bool(gui_state.processing.legacy_gpr.ignore_socket_speed_enabled)
)
self._legacy_gpr_ignore_socket_speed_enabled.setChecked(ignore_socket_speed_enabled)
self._legacy_gpr_speed_m_s.setEnabled(ignore_socket_speed_enabled)
self._legacy_gpr_look_angle_deg.setValue(float(gui_state.processing.legacy_gpr.look_angle_deg))
self._legacy_gpr_apply_freq_phase_correction.setChecked(
bool(gui_state.processing.legacy_gpr.apply_freq_phase_correction)
@@ -566,32 +547,13 @@ class AppWindowConfigProfileIOMixin:
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_override_enabled.setChecked(bool(sar3d_state.pitch_override_enabled))
self._sar3d_pitch_deg.setValue(float(sar3d_state.pitch_deg))
self._sar3d_ground_depth_m.setValue(float(sar3d_state.ground_depth_m))
self._sar3d_x_min_m.setValue(float(sar3d_state.x_min_m))
self._sar3d_x_max_m.setValue(float(sar3d_state.x_max_m))
self._sar3d_y_min_m.setValue(float(sar3d_state.y_min_m))
self._sar3d_y_max_m.setValue(float(sar3d_state.y_max_m))
self._sar3d_z_min_m.setValue(float(sar3d_state.z_min_m))
self._sar3d_z_max_m.setValue(float(sar3d_state.z_max_m))
self._sar3d_voxel_x_m.setValue(float(sar3d_state.voxel_x_m))
self._sar3d_voxel_y_m.setValue(float(sar3d_state.voxel_y_m))
self._sar3d_voxel_z_m.setValue(float(sar3d_state.voxel_z_m))
self._sar3d_norm_edge_margin_m.setValue(float(sar3d_state.norm_edge_margin_m))
self._sar3d_smooth_sigma.setValue(float(sar3d_state.smooth_sigma))
self._sar3d_voxel_m.setValue(float(sar3d_state.voxel_m))
self._sar3d_bp_ground_depth_m.setValue(float(sar3d_state.bp_ground_depth_m))
self._sar3d_comp_range_power.setValue(float(sar3d_state.comp_range_power))
self._sar3d_tx_per_segment.setValue(int(sar3d_state.tx_per_segment))
self._sar3d_use_compensation.setChecked(bool(sar3d_state.use_compensation))
self._sar3d_normalize_pairs.setChecked(bool(sar3d_state.normalize_pairs))
self._set_combo_current_text(
self._sar3d_bottom_view,
sar3d_bottom_view_label_from_deg(float(sar3d_state.bottom_view_deg)),
)
self._sar3d_normalize_each_frame.setChecked(bool(sar3d_state.normalize_each_frame))
self._sync_socket_speed_controls()
self._save_count.setValue(int(gui_state.data_actions.save_count))
self._record_count.setValue(int(gui_state.data_actions.record_count))
@@ -25,7 +25,6 @@ from python_app.orchestration.preprocess_assets import (
VISIBLE_PREPROCESS_ASSET_KEYS,
preprocess_asset_model,
)
from python_app.gui.runtime.sar3d_views import sar3d_bottom_view_deg_from_label
from python_app.storage.npz_store import radar_key_from_config
@@ -434,31 +433,12 @@ class AppWindowConfigStateBuildersMixin:
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_override_enabled=bool(self._sar3d_pitch_override_enabled.isChecked()),
pitch_deg=float(self._sar3d_pitch_deg.value()),
ground_depth_m=float(self._sar3d_ground_depth_m.value()),
x_min_m=float(self._sar3d_x_min_m.value()),
x_max_m=float(self._sar3d_x_max_m.value()),
y_min_m=float(self._sar3d_y_min_m.value()),
y_max_m=float(self._sar3d_y_max_m.value()),
z_min_m=float(self._sar3d_z_min_m.value()),
z_max_m=float(self._sar3d_z_max_m.value()),
voxel_x_m=float(self._sar3d_voxel_x_m.value()),
voxel_y_m=float(self._sar3d_voxel_y_m.value()),
voxel_z_m=float(self._sar3d_voxel_z_m.value()),
norm_edge_margin_m=float(self._sar3d_norm_edge_margin_m.value()),
smooth_sigma=float(self._sar3d_smooth_sigma.value()),
voxel_m=float(self._sar3d_voxel_m.value()),
bp_ground_depth_m=float(self._sar3d_bp_ground_depth_m.value()),
comp_range_power=float(self._sar3d_comp_range_power.value()),
tx_per_segment=int(self._sar3d_tx_per_segment.value()),
use_compensation=bool(self._sar3d_use_compensation.isChecked()),
normalize_pairs=bool(self._sar3d_normalize_pairs.isChecked()),
bottom_view_deg=sar3d_bottom_view_deg_from_label(
self._sar3d_bottom_view.currentText()
),
normalize_each_frame=bool(self._sar3d_normalize_each_frame.isChecked()),
),
),
data_actions=GuiDataActionsStateModel(
@@ -1,162 +1,158 @@
"""3D SAR projection rendering helpers.
"""3D SAR views of the volume published by the sar3d processor.
The sar3d processor publishes two normalized max-projections of the imaged
volume as collection-level ImageF32 payloads, matching the legacy render
conventions (X across, Z range/"вдаль", Y up):
Scene axes: X across, Y height above the ground (Y = 0 is the ground), Z along
the ground away from the radar. Payloads, the volume normalized to a peak of 1:
- ``sar3d_proj_xz`` — top view: X (m) horizontal, Z/range (m) vertical
(max over the height Y), like the classic GPR heatmap;
- ``sar3d_proj_bottom`` — switchable bottom view: height Y (m) vertical, the
horizontal axis is the ground direction rotated by ``sar3d_bottom_view_deg``
around Y (0° = front view X, 90° = side view Z, 30°/60° in between);
plus ``sar3d_peak`` (ScalarF32), the volume peak before normalization. Both
projections are drawn on their own PlotWidgets stacked in one plot-stack page
(top view on top), mirroring how the pass-through page stacks its two plots.
- ``sar3d_front``, ``sar3d_side``, ``sar3d_top``: maximum projections onto X-Y
(looking ahead from the radar), Z-Y (from the side) and X-Z (from above);
- ``sar3d_oblique``: orthographic projection along the camera of the notebook's
3D scene; ``sar3d_oblique_basis`` (2x3) gives its image axes u and v as scene
directions, which places the scene box, the ground and the maximum on it;
- ``sar3d_peak``: [level, X, Y, Z] of the volume maximum before normalization.
"""
from __future__ import annotations
from PyQt6.QtCore import QRectF
import itertools
import numpy as np
import pyqtgraph as pg
from PyQt6.QtCore import QRectF, Qt
from python_app.gui.runtime.sar3d_views import sar3d_bottom_view_deg_from_label
from python_app.gui.controllers.app_window_plot.bscan_plot_mixin import build_lut
from python_app.models.dataset_model import ResultCollection
# Fraction of the projection maximum below which the heatmap is not highlighted
# (same as cloud-threshold=0.75 in the VNA_AlGo 3D SAR animation): the color
# scale is stretched over [frac, 1], everything below falls into the darkest
# LUT color.
HEATMAP_DISPLAY_THRESHOLD_FRAC = 0.75
# Colours start at this fraction of the maximum; weaker voxels stay transparent,
# like those under the threshold of the notebook's point cloud.
DISPLAY_THRESHOLD = 0.75
# payload -> (title, x label, y label, scene axes (0 = X, 1 = Y, 2 = Z) along the plot axes)
SAR3D_VIEWS: dict[str, tuple[str, str, str, tuple[int, int] | None]] = {
"sar3d_front": ("Front (X-Y)", "X", "Y", (0, 1)),
"sar3d_side": ("Side (Z-Y)", "Z", "Y", (2, 1)),
"sar3d_top": ("Top (X-Z)", "X", "Z", (0, 2)),
"sar3d_oblique": ("3D", "", "", None),
}
# Corners of the scene box indexed by bits (x, y, z); an edge joins corners one bit apart.
_BOX_EDGES = [(a, b) for a, b in itertools.combinations(range(8), 2) if bin(a ^ b).count("1") == 1]
# Axis names go on the edges meeting at the bottom corner nearest the camera (X max, Y min, Z min).
_AXIS_EDGES = {"X": (0, 1), "Y": (1, 3), "Z": (1, 5)}
def _image_rect(x_axis: np.ndarray, y_axis: np.ndarray) -> QRectF:
"""Rectangle covering the pixels centred on the axis samples."""
dx = (x_axis[-1] - x_axis[0]) / max(len(x_axis) - 1, 1)
dy = (y_axis[-1] - y_axis[0]) / max(len(y_axis) - 1, 1)
return QRectF(x_axis[0] - dx / 2, y_axis[0] - dy / 2, x_axis[-1] - x_axis[0] + dx, y_axis[-1] - y_axis[0] + dy)
class AppWindowSar3dPlotMixin:
"""Renders 3D SAR volume projections published by the sar3d processor."""
"""Draws the 3D SAR views."""
def _sar3d_bottom_view_labels(self) -> tuple[str, str]:
"""Return (plot title, horizontal axis label) for the active bottom view.
def _init_sar3d_views(self) -> None:
"""Create the persistent items of the four 3D SAR plots."""
lut = build_lut(["#081c15", "#1b4332", "#ffd166", "#f94144"]) # the GPR palette
lut = np.column_stack([lut, np.full(len(lut), 255, dtype=np.uint8)])
lut[0, 3] = 0 # below DISPLAY_THRESHOLD
self._sar3d_images = {}
self._sar3d_peaks = {}
for name, (_title, x_label, y_label, _axes) in SAR3D_VIEWS.items():
plot = self._sar3d_plots[name]
self._disable_gpr_plot_interaction(plot, plot.getViewBox())
plot.getViewBox().enableAutoRange(x=False, y=False)
image = pg.ImageItem(axisOrder="row-major", lut=lut)
plot.addItem(image)
# Hollow, so the response at the maximum stays visible.
peak = pg.ScatterPlotItem(symbol="star", size=18, pen=pg.mkPen("w", width=1.5), brush=None)
peak.setZValue(3)
plot.addItem(peak)
self._sar3d_images[name] = image
self._sar3d_peaks[name] = peak
if x_label:
plot.showGrid(x=True, y=True, alpha=0.2)
plot.setLabel("bottom", x_label, units="m")
plot.setLabel("left", y_label, units="m")
if y_label == "Y":
ground = pg.InfiniteLine(pos=0.0, angle=0, pen=pg.mkPen("w", width=1, style=Qt.PenStyle.DashLine))
ground.setZValue(2)
plot.addItem(ground)
The plot surfaces are created before the processing section builds the
bottom-view combo, so fall back to the front view until it exists.
"""
combo = getattr(self, "_sar3d_bottom_view", None)
degrees = sar3d_bottom_view_deg_from_label(combo.currentText()) if combo is not None else 0.0
if degrees <= 1e-9:
return "3D SAR front view (X-Y)", "X"
if degrees >= 90.0 - 1e-9:
return "3D SAR side view (Z-Y)", "Range Z"
return f"3D SAR view @ {degrees:g}\N{DEGREE SIGN}", f"u({degrees:g}\N{DEGREE SIGN})"
def _configure_sar3d_plot_axes(self) -> None:
"""Apply persistent axis labels and view settings to both projection plots."""
_bottom_title, bottom_x_label = self._sar3d_bottom_view_labels()
for plot, x_label, y_label in (
(self._sar3d_xz_plot, "X", "Range Z"),
(self._sar3d_xy_plot, bottom_x_label, "Height 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_label, 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)
oblique = self._sar3d_plots["sar3d_oblique"]
oblique.hideAxis("bottom")
oblique.hideAxis("left")
oblique.setAspectLocked(True)
# The notebook scene's ground, under the image so it shows through weak voxels.
self._sar3d_ground = pg.PlotCurveItem(
pen=pg.mkPen(115, 105, 88), brush=pg.mkBrush(115, 105, 88, 110), fillLevel="enclosed"
)
self._sar3d_ground.setZValue(-1)
oblique.addItem(self._sar3d_ground)
self._sar3d_box = pg.PlotCurveItem(pen=pg.mkPen(190, 190, 190, 170), connect="pairs")
self._sar3d_box.setZValue(2)
oblique.addItem(self._sar3d_box)
self._sar3d_axis_labels = {}
for axis_name in _AXIS_EDGES:
label = pg.TextItem(axis_name, color=(190, 190, 190), anchor=(0.5, 0.5))
oblique.addItem(label)
self._sar3d_axis_labels[axis_name] = label
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()
bottom_title, _bottom_x_label = self._sar3d_bottom_view_labels()
self._sar3d_xz_plot.setTitle("3D SAR top view (X-Z)")
self._sar3d_xy_plot.setTitle(bottom_title)
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; stretch the
# colors over [threshold, 1] like the 3D animation cloud cutoff.
image_item.setLevels((HEATMAP_DISPLAY_THRESHOLD_FRAC, 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
"""Clear all four 3D SAR views."""
for name, (title, *_rest) in SAR3D_VIEWS.items():
self._sar3d_images[name].hide()
self._sar3d_peaks[name].setData([], [])
self._sar3d_plots[name].setTitle(title)
self._sar3d_box.setData([], [])
self._sar3d_ground.setData([], [])
def _draw_sar3d_projections(self, collection: ResultCollection) -> bool:
"""Draw the latest collection-level 3D SAR top and bottom projections."""
xz_payload = self._collection_payload_by_name(collection, "sar3d_proj_xz", kind=3)
bottom_payload = self._collection_payload_by_name(collection, "sar3d_proj_bottom", kind=3)
if xz_payload is None and bottom_payload is None:
"""Draw the 3D SAR views of a result collection; return success."""
payloads = {name: self._collection_payload_by_name(collection, name, kind=3) for name in SAR3D_VIEWS}
basis = self._collection_payload_by_name(collection, "sar3d_oblique_basis", kind=4)
if basis is None or any(payload is None or payload.image.size == 0 for payload in payloads.values()):
self._clear_sar3d_plot()
return False
peak = self._collection_payload_by_name(collection, "sar3d_peak", kind=4)
level, *peak_xyz = peak.table[0] if peak is not None else (None,)
for plot in (self._sar3d_xz_plot, self._sar3d_xy_plot):
plot.setUpdatesEnabled(False)
try:
self._ensure_sar3d_plot_items()
# Axis labels track the bottom-view selector (payload axes change with it).
self._configure_sar3d_plot_axes()
xz_drawn = xz_payload is not None and self._draw_sar3d_projection(
self._sar3d_xz_plot, self._sar3d_xz_image_item, xz_payload
)
bottom_drawn = bottom_payload is not None and self._draw_sar3d_projection(
self._sar3d_xy_plot, self._sar3d_xy_image_item, bottom_payload
)
if not xz_drawn and not bottom_drawn:
self._clear_sar3d_plot()
return False
for name, payload in payloads.items():
title, _x_label, _y_label, axes = SAR3D_VIEWS[name]
rect = _image_rect(payload.image_x_axis, payload.image_y_axis)
image = self._sar3d_images[name]
image.setImage(payload.image, autoLevels=False, levels=(DISPLAY_THRESHOLD, 1.0))
image.setRect(rect)
image.show()
plot = self._sar3d_plots[name]
plot.setRange(rect, padding=0.0)
if peak_xyz:
position = basis.table @ np.asarray(peak_xyz) if axes is None else [peak_xyz[axis] for axis in axes]
self._sar3d_peaks[name].setData([position[0]], [position[1]])
else:
self._sar3d_peaks[name].setData([], [])
plot.setTitle(title)
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}"
bottom_title, _bottom_x_label = self._sar3d_bottom_view_labels()
self._sar3d_xz_plot.setTitle(f"3D SAR top view (X-Z){peak_suffix}")
self._sar3d_xy_plot.setTitle(bottom_title)
finally:
for plot in (self._sar3d_xz_plot, self._sar3d_xy_plot):
plot.setUpdatesEnabled(True)
# Scene box and ground on the oblique view, from the extents of the other views.
front, top = payloads["sar3d_front"], payloads["sar3d_top"]
bounds = [
(front.image_x_axis[0], front.image_x_axis[-1]),
(front.image_y_axis[0], front.image_y_axis[-1]),
(top.image_y_axis[0], top.image_y_axis[-1]),
]
corners = np.array([[bounds[axis][(index >> axis) & 1] for axis in range(3)] for index in range(8)])
projected = corners @ basis.table.T
box = projected[np.ravel(_BOX_EDGES)]
self._sar3d_box.setData(box[:, 0], box[:, 1])
centre = projected.mean(axis=0)
for axis_name, (start, end) in _AXIS_EDGES.items():
middle = (projected[start] + projected[end]) / 2
self._sar3d_axis_labels[axis_name].setPos(*(middle + 0.08 * (middle - centre))) # just outside
(x0, x1), _, (z0, z1) = bounds
ground = (np.array([[x0, 0.0, z0], [x1, 0.0, z0], [x1, 0.0, z1], [x0, 0.0, z1], [x0, 0.0, z0]]) @ basis.table.T).T
self._sar3d_ground.setData(ground[0], ground[1])
if level is not None:
x, y, z = peak_xyz
self._sar3d_plots["sar3d_oblique"].setTitle(
f"3D | max X {x:+.2f}, Y {y:+.2f}, Z {z:.2f} m | level {level:.3g}"
)
return True
@@ -13,6 +13,7 @@ from __future__ import annotations
from PyQt6.QtWidgets import (
QComboBox,
QFrame,
QGridLayout,
QGroupBox,
QHBoxLayout,
QLabel,
@@ -25,6 +26,7 @@ from PyQt6.QtWidgets import (
)
import pyqtgraph as pg
from python_app.gui.controllers.app_window_plot.sar3d_plot_mixin import SAR3D_VIEWS
from python_app.gui.controllers.sections import (
build_data_actions_group,
build_primary_actions_group,
@@ -141,23 +143,16 @@ class AppWindowUiMixin:
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).
"""Create the 3D SAR page: front, side, top and 3D views in a 2x2 grid."""
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()
layout = QGridLayout(self._sar3d_plots_container)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(6)
self._sar3d_plots = {}
for index, name in enumerate(SAR3D_VIEWS):
self._sar3d_plots[name] = self._create_plot_widget(background="#0f141c")
layout.addWidget(self._sar3d_plots[name], index // 2, index % 2)
self._init_sar3d_views()
self._clear_sar3d_plot()
self._plot_stack.addWidget(self._sar3d_plots_container)
@@ -20,9 +20,13 @@ from PyQt6.QtWidgets import (
from python_app.gui.controllers.app_window_config.state_builders import GUI_SAVE_HISTORY_LIMIT
from python_app.gui.controllers.sections.layout_helpers import FormRow, build_two_column_form_widget
from python_app.gui.runtime.sar3d_views import (
SAR3D_BOTTOM_VIEW_CHOICES,
sar3d_bottom_view_label_from_deg,
# Same semantics on every GPR-like page; the data_processor applies them.
_IGNORE_SOCKET_SPEED_TOOLTIP = (
"Checked: the speed below drives the motion correction. Unchecked: the speed a locator "
"socket client streams ('vlc') does, and the field below only stands in while no client "
"has sent one in the last 5 s."
)
@@ -290,10 +294,7 @@ def build_processing_group(owner) -> QGroupBox:
owner._gpr_ignore_socket_speed_enabled = QCheckBox("Ignore socket speed")
owner._gpr_ignore_socket_speed_enabled.setChecked(bool(gpr_live_defaults.ignore_socket_speed_enabled))
owner._gpr_ignore_socket_speed_enabled.setToolTip(
"When checked, use the speed below for motion correction instead of the "
"value streamed over the locator socket."
)
owner._gpr_ignore_socket_speed_enabled.setToolTip(_IGNORE_SOCKET_SPEED_TOOLTIP)
owner._gpr_speed_m_s = QDoubleSpinBox()
owner._gpr_speed_m_s.setDecimals(3)
@@ -475,12 +476,7 @@ def build_processing_group(owner) -> QGroupBox:
owner._legacy_gpr_ignore_socket_speed_enabled.setChecked(
bool(legacy_gpr_defaults.ignore_socket_speed_enabled)
)
# When the box is unchecked, an external TCP client controls the speed
# through the C++ locator server; disable the spinner so the GUI value
# cannot silently win against the live socket value.
owner._legacy_gpr_speed_m_s.setEnabled(
bool(legacy_gpr_defaults.ignore_socket_speed_enabled)
)
owner._legacy_gpr_ignore_socket_speed_enabled.setToolTip(_IGNORE_SOCKET_SPEED_TOOLTIP)
owner._legacy_gpr_look_angle_deg = QDoubleSpinBox()
owner._legacy_gpr_look_angle_deg.setDecimals(2)
@@ -575,26 +571,15 @@ 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.
# 3D SAR page. The labels are the names of the reference notebook
# Multi_antenna_3D_clean_v1.ipynb; the shared gpr_* fields get their own
# widgets here, like on the legacy page.
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)
@@ -607,223 +592,98 @@ def build_processing_group(owner) -> QGroupBox:
owner._sar3d_stop_freq_mhz.setSingleStep(10.0)
owner._sar3d_stop_freq_mhz.setValue(float(sar3d_defaults.stop_freq_mhz))
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_min_depth_m.setToolTip("Radar depth gate start (m); the scene grid along Z follows the gate.")
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_max_depth_m.setToolTip("Radar depth gate end (m); the scene grid along Z follows the gate.")
owner._sar3d_voxel_m = QDoubleSpinBox()
owner._sar3d_voxel_m.setDecimals(3)
owner._sar3d_voxel_m.setRange(0.01, 0.1)
owner._sar3d_voxel_m.setSingleStep(0.005)
owner._sar3d_voxel_m.setValue(float(sar3d_defaults.voxel_m))
owner._sar3d_voxel_m.setToolTip(
"Edge of the cubic voxel (m), the same along X, Y and Z. Smaller is finer and slower: "
"halving it makes 8 times more voxels."
)
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)."
"Downward tilt of the radar (deg): motion correction along range and the ground scene axes."
)
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_ignore_socket_speed_enabled.setToolTip(_IGNORE_SOCKET_SPEED_TOOLTIP)
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.setRange(0, 24)
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."
"Aperture of 2*r+1 consecutive frames. 0: the current frame only (needs a vertical antenna "
"baseline); >0 adds aperture from motion and needs an accurate speed. The image lags by r frames."
)
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_override_enabled = QCheckBox("Override pitch")
owner._sar3d_pitch_override_enabled.setChecked(bool(sar3d_defaults.pitch_override_enabled))
owner._sar3d_pitch_override_enabled.setToolTip(
"When unchecked, the frame pitch follows the look angle (notebook "
"BP_GROUND_PITCH_DEG = None); check to set it separately below."
)
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(
"Frame pitch (deg) used for ground coordinates and aperture offsets when the override is on."
)
owner._sar3d_ground_depth_m = QDoubleSpinBox()
owner._sar3d_ground_depth_m.setDecimals(2)
owner._sar3d_ground_depth_m.setRange(0.0, 10.0)
owner._sar3d_ground_depth_m.setSingleStep(0.05)
owner._sar3d_ground_depth_m.setValue(float(sar3d_defaults.ground_depth_m))
owner._sar3d_ground_depth_m.setToolTip(
"Height of the lower antenna row above the ground (m); scene Y=0 is the ground."
)
owner._sar3d_x_min_m = QDoubleSpinBox()
owner._sar3d_x_min_m.setDecimals(2)
owner._sar3d_x_min_m.setRange(-50.0, 50.0)
owner._sar3d_x_min_m.setSingleStep(0.1)
owner._sar3d_x_min_m.setValue(float(sar3d_defaults.x_min_m))
owner._sar3d_x_max_m = QDoubleSpinBox()
owner._sar3d_x_max_m.setDecimals(2)
owner._sar3d_x_max_m.setRange(-50.0, 50.0)
owner._sar3d_x_max_m.setSingleStep(0.1)
owner._sar3d_x_max_m.setValue(float(sar3d_defaults.x_max_m))
owner._sar3d_z_min_m = QDoubleSpinBox()
owner._sar3d_z_min_m.setDecimals(2)
owner._sar3d_z_min_m.setRange(-50.0, 50.0)
owner._sar3d_z_min_m.setSingleStep(0.1)
owner._sar3d_z_min_m.setValue(float(sar3d_defaults.z_min_m))
owner._sar3d_z_max_m = QDoubleSpinBox()
owner._sar3d_z_max_m.setDecimals(2)
owner._sar3d_z_max_m.setRange(-50.0, 50.0)
owner._sar3d_z_max_m.setSingleStep(0.1)
owner._sar3d_z_max_m.setValue(float(sar3d_defaults.z_max_m))
owner._sar3d_y_min_m = QDoubleSpinBox()
owner._sar3d_y_min_m.setDecimals(2)
owner._sar3d_y_min_m.setRange(-10.0, 10.0)
owner._sar3d_y_min_m.setSingleStep(0.1)
owner._sar3d_y_min_m.setValue(float(sar3d_defaults.y_min_m))
owner._sar3d_y_max_m = QDoubleSpinBox()
owner._sar3d_y_max_m.setDecimals(2)
owner._sar3d_y_max_m.setRange(-10.0, 10.0)
owner._sar3d_y_max_m.setSingleStep(0.1)
owner._sar3d_y_max_m.setValue(float(sar3d_defaults.y_max_m))
owner._sar3d_voxel_x_m = QDoubleSpinBox()
owner._sar3d_voxel_x_m.setDecimals(3)
owner._sar3d_voxel_x_m.setRange(0.005, 1.0)
owner._sar3d_voxel_x_m.setSingleStep(0.005)
owner._sar3d_voxel_x_m.setValue(float(sar3d_defaults.voxel_x_m))
owner._sar3d_voxel_y_m = QDoubleSpinBox()
owner._sar3d_voxel_y_m.setDecimals(3)
owner._sar3d_voxel_y_m.setRange(0.005, 1.0)
owner._sar3d_voxel_y_m.setSingleStep(0.005)
owner._sar3d_voxel_y_m.setValue(float(sar3d_defaults.voxel_y_m))
owner._sar3d_voxel_z_m = QDoubleSpinBox()
owner._sar3d_voxel_z_m.setDecimals(3)
owner._sar3d_voxel_z_m.setRange(0.005, 1.0)
owner._sar3d_voxel_z_m.setSingleStep(0.005)
owner._sar3d_voxel_z_m.setValue(float(sar3d_defaults.voxel_z_m))
owner._sar3d_norm_edge_margin_m = QDoubleSpinBox()
owner._sar3d_norm_edge_margin_m.setDecimals(2)
owner._sar3d_norm_edge_margin_m.setRange(0.0, 5.0)
owner._sar3d_norm_edge_margin_m.setSingleStep(0.05)
owner._sar3d_norm_edge_margin_m.setValue(float(sar3d_defaults.norm_edge_margin_m))
owner._sar3d_norm_edge_margin_m.setToolTip(
"Grid Y edges excluded from volume normalization (constant-range ridge)."
)
owner._sar3d_smooth_sigma = QDoubleSpinBox()
owner._sar3d_smooth_sigma.setDecimals(2)
owner._sar3d_smooth_sigma.setRange(0.0, 10.0)
owner._sar3d_smooth_sigma.setSingleStep(0.25)
owner._sar3d_smooth_sigma.setValue(float(sar3d_defaults.smooth_sigma))
owner._sar3d_smooth_sigma.setToolTip("Gaussian smoothing sigma over (Z, X) planes, voxels.")
owner._sar3d_bp_ground_depth_m = QDoubleSpinBox()
owner._sar3d_bp_ground_depth_m.setDecimals(2)
owner._sar3d_bp_ground_depth_m.setRange(0.0, 10.0)
owner._sar3d_bp_ground_depth_m.setSingleStep(0.05)
owner._sar3d_bp_ground_depth_m.setValue(float(sar3d_defaults.bp_ground_depth_m))
owner._sar3d_bp_ground_depth_m.setToolTip("Height of the lower antenna row above the ground (m); scene Y = 0 is the ground.")
owner._sar3d_comp_range_power = QDoubleSpinBox()
owner._sar3d_comp_range_power.setDecimals(2)
owner._sar3d_comp_range_power.setRange(0.0, 2.0)
owner._sar3d_comp_range_power.setSingleStep(0.05)
owner._sar3d_comp_range_power.setValue(float(sar3d_defaults.comp_range_power))
owner._sar3d_comp_range_power.setToolTip("Geometric spreading compensation power.")
owner._sar3d_comp_range_power.setToolTip("Geometric spreading compensation (Rtx*Rrx)^power, capped at 5.")
owner._sar3d_tx_per_segment = QSpinBox()
owner._sar3d_tx_per_segment.setRange(1, 64)
owner._sar3d_tx_per_segment.setValue(int(sar3d_defaults.tx_per_segment))
owner._sar3d_tx_per_segment.setToolTip(
"Fallback Tx-per-segment split of a frame when per-trace capture stamps are absent."
)
owner._sar3d_use_compensation = QCheckBox("Use per-voxel compensation")
owner._sar3d_use_compensation = QCheckBox("SAR_USE_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))
owner._sar3d_bottom_view = QComboBox()
owner._sar3d_bottom_view.addItems([label for label, _deg in SAR3D_BOTTOM_VIEW_CHOICES])
owner._set_combo_current_text(
owner._sar3d_bottom_view,
sar3d_bottom_view_label_from_deg(float(sar3d_defaults.bottom_view_deg)),
)
owner._sar3d_bottom_view.setToolTip(
"Bottom plot projection: front view (X-Y), side view (Z-Y) or an "
"intermediate rotation around the vertical axis. The top plot always "
"shows the X-Z top view."
)
owner._sar3d_normalize_each_frame = QCheckBox("SAR_NORMALIZE_EACH_FRAME")
owner._sar3d_normalize_each_frame.setChecked(bool(sar3d_defaults.normalize_each_frame))
owner._sar3d_normalize_each_frame.setToolTip("Scale every Tx/Rx A-scan by its median inside the depth gate.")
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),
("INPUT_IDX", owner._sar3d_input_positions_input),
("OUTPUT_IDX", owner._sar3d_output_positions_input),
("F_START MHz", owner._sar3d_start_freq_mhz),
("F_STOP MHz", owner._sar3d_stop_freq_mhz),
("MIN_DEPTH", owner._sar3d_min_depth_m),
("MAX_DEPTH", owner._sar3d_max_depth_m),
("SAR_VOXEL_M", owner._sar3d_voxel_m),
("LOOK_ANGLE_DEG", owner._sar3d_look_angle_deg),
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),
owner._sar3d_pitch_override_enabled,
("Pitch deg", owner._sar3d_pitch_deg),
("Ground depth m", owner._sar3d_ground_depth_m),
("X min m", owner._sar3d_x_min_m),
("X max m", owner._sar3d_x_max_m),
("Y min m", owner._sar3d_y_min_m),
("Y max m", owner._sar3d_y_max_m),
("Z min m", owner._sar3d_z_min_m),
("Z max m", owner._sar3d_z_max_m),
("Voxel X m", owner._sar3d_voxel_x_m),
("Voxel Y m", owner._sar3d_voxel_y_m),
("Voxel Z m", owner._sar3d_voxel_z_m),
("Norm edge margin m", owner._sar3d_norm_edge_margin_m),
("Smooth sigma", owner._sar3d_smooth_sigma),
("Comp range power", owner._sar3d_comp_range_power),
("Tx per segment", owner._sar3d_tx_per_segment),
("Bottom view", owner._sar3d_bottom_view),
("SPEED_M_S", owner._sar3d_speed_m_s),
("SAR_FRAME_RADIUS", owner._sar3d_frame_radius),
("BP_GROUND_DEPTH_M", owner._sar3d_bp_ground_depth_m),
("COMP_RANGE_POWER", owner._sar3d_comp_range_power),
owner._sar3d_use_compensation,
owner._sar3d_normalize_pairs,
owner._sar3d_normalize_each_frame,
],
split_index=13,
split_index=8,
)
owner._processing_mode_pages.addWidget(sar3d_page)
@@ -858,7 +718,7 @@ def build_processing_group(owner) -> QGroupBox:
owner._gpr_motion_mode.currentTextChanged.connect(owner._on_processing_live_settings_changed)
owner._gpr_look_angle_deg.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._gpr_direction_sign.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._gpr_ignore_socket_speed_enabled.toggled.connect(owner._on_processing_live_settings_changed)
owner._gpr_ignore_socket_speed_enabled.toggled.connect(owner._on_ignore_socket_speed_toggled)
owner._gpr_speed_m_s.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._gpr_start_freq_mhz.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._gpr_stop_freq_mhz.valueChanged.connect(owner._on_processing_live_settings_changed)
@@ -885,9 +745,7 @@ def build_processing_group(owner) -> QGroupBox:
owner._legacy_gpr_start_freq_mhz.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._legacy_gpr_stop_freq_mhz.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._legacy_gpr_speed_m_s.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._legacy_gpr_ignore_socket_speed_enabled.toggled.connect(
owner._on_legacy_gpr_ignore_socket_speed_toggled
)
owner._legacy_gpr_ignore_socket_speed_enabled.toggled.connect(owner._on_ignore_socket_speed_toggled)
owner._legacy_gpr_look_angle_deg.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._legacy_gpr_apply_freq_phase_correction.toggled.connect(owner._on_processing_live_settings_changed)
owner._legacy_gpr_reference_mode.currentTextChanged.connect(owner._on_processing_live_settings_changed)
@@ -901,36 +759,23 @@ def build_processing_group(owner) -> QGroupBox:
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_override_enabled.toggled.connect(owner._on_processing_live_settings_changed)
owner._sar3d_pitch_deg.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_ground_depth_m.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_x_min_m.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_x_max_m.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_z_min_m.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_z_max_m.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_y_min_m.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_y_max_m.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_voxel_x_m.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_voxel_y_m.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_voxel_z_m.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_norm_edge_margin_m.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_smooth_sigma.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_comp_range_power.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_tx_per_segment.valueChanged.connect(owner._on_processing_live_settings_changed)
for widget in (
owner._sar3d_start_freq_mhz,
owner._sar3d_stop_freq_mhz,
owner._sar3d_min_depth_m,
owner._sar3d_max_depth_m,
owner._sar3d_voxel_m,
owner._sar3d_look_angle_deg,
owner._sar3d_speed_m_s,
owner._sar3d_frame_radius,
owner._sar3d_bp_ground_depth_m,
owner._sar3d_comp_range_power,
):
widget.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_ignore_socket_speed_enabled.toggled.connect(owner._on_ignore_socket_speed_toggled)
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._sar3d_bottom_view.currentTextChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_normalize_each_frame.toggled.connect(owner._on_processing_live_settings_changed)
owner._sync_socket_speed_controls()
owner._set_processing_mode_page(owner._processing_mode.currentText())
return group