Update GPR and SAR3D processing

This commit is contained in:
Ayzen
2026-10-08 16:24:01 +03:00
parent 390015ec39
commit 3575ca68f5
67 changed files with 1187 additions and 2581 deletions
+14 -15
View File
@@ -10,7 +10,7 @@ import math
from pathlib import Path
from PyQt6.QtCore import QSignalBlocker
from PyQt6.QtWidgets import QCheckBox, QComboBox, QDoubleSpinBox, QFormLayout, QLabel, QSpinBox
from PyQt6.QtWidgets import QCheckBox, QComboBox, QDoubleSpinBox, QFormLayout, QLabel, QSlider, QSpinBox
from python_app.storage.npz.paths import radar_config_filename_prefix
from python_app.gui.antenna_geometry_editor import AntennaGeometryEditor
@@ -61,7 +61,6 @@ _ADC_FIELDS = (
("radar.laser_control.variation.delay_time", "_optical_delay_time_input"),
)
_GEOMETRY_FIELDS = {
"relative_permittivity": "_gpr_relative_permittivity",
"tx_geometry": "_gpr_tx_geometry_input",
"rx_geometry": "_gpr_rx_geometry_input",
}
@@ -97,15 +96,15 @@ def _set_web_widget(widget: SettingWidget, value: object) -> None:
if isinstance(widget, QCheckBox):
if not isinstance(value, bool):
raise ValueError("Checkbox value must be true or false")
elif isinstance(widget, (QSpinBox, QDoubleSpinBox)):
elif isinstance(widget, (QSpinBox, QDoubleSpinBox, QSlider)):
if isinstance(value, bool) or not isinstance(value, (int, float, str)):
raise ValueError("Numeric setting requires a number")
numeric = float(value)
if not math.isfinite(numeric) or not widget.minimum() <= numeric <= widget.maximum():
raise ValueError("Numeric setting is outside its allowed range")
if isinstance(widget, QSpinBox) and numeric != int(numeric):
if isinstance(widget, (QSpinBox, QSlider)) and numeric != int(numeric):
raise ValueError("Setting requires an integer")
value = int(numeric) if isinstance(widget, QSpinBox) else numeric
value = int(numeric) if isinstance(widget, (QSpinBox, QSlider)) else numeric
elif isinstance(widget, QComboBox):
if not isinstance(value, str) or widget.findText(value) < 0:
raise ValueError(f"Unknown setting option: {value}")
@@ -192,7 +191,7 @@ class ConfigController:
config.input_switch.positions <= 1 and config.output_switch.positions <= 1
or config.input_switch.driver_mode == config.output_switch.driver_mode == "mock"
)
if self._scalar("_processing_mode") not in {"gpr", "legacy_gpr", "sar3d"} and static:
if self._scalar("_processing_mode") not in {"gpr", "sar3d"} and static:
config.combos = [ComboModel(0, 0)]
for key in PREPROCESS_ASSET_NAMES:
asset = preprocess_asset_model(config, key)
@@ -224,7 +223,6 @@ class ConfigController:
def geometry(self) -> GprModel:
return GprModel(
relative_permittivity=float(self._scalar("_gpr_relative_permittivity")),
tx_geometry=[GprTxGeometryModel(**entry) for entry in self._geometry("tx_geometry").records()],
rx_geometry=[GprRxGeometryModel(**entry) for entry in self._geometry("rx_geometry").records()],
)
@@ -242,9 +240,10 @@ class ConfigController:
if binding.spec.live in {"gpr_input_positions", "gpr_output_positions"}:
value = parse_position_csv(str(value))
setattr(config, binding.spec.live, value)
config.relative_permittivity = float(self._scalar("_gpr_relative_permittivity"))
config.tx_geometry = self._geometry("tx_geometry").records()
config.rx_geometry = self._geometry("rx_geometry").records()
if config.sar3d_y_max_m <= config.sar3d_y_min_m:
raise ValueError("SAR_Y_RANGE_M max must be above min")
for axis in ("x", "z"):
lower_name, upper_name = f"gpr_visible_{axis}_min_m", f"gpr_visible_{axis}_max_m"
lower, upper = sorted((getattr(config, lower_name), getattr(config, upper_name)))
@@ -279,7 +278,6 @@ class ConfigController:
if binding.spec.widget:
write_setting(_scalar_widget(self.fields[binding.spec.widget]), binding.read(gui))
write_setting(_scalar_widget(self.fields["_log_level_combo"]), config.logging.level.capitalize())
write_setting(_scalar_widget(self.fields["_gpr_relative_permittivity"]), config.gpr.relative_permittivity)
for name, count, records in (
("tx_geometry", config.output_switch.positions, config.gpr.tx_geometry),
("rx_geometry", config.input_switch.positions, config.gpr.rx_geometry),
@@ -292,8 +290,8 @@ class ConfigController:
widgets = {binding.spec.web: (binding.spec.group, self.fields[binding.spec.widget])
for binding in active_web_fields(mode)}
for name, widget_id in _GEOMETRY_FIELDS.items():
if mode in ({"legacy_gpr"} if name == "relative_permittivity" else {"gpr", "legacy_gpr", "sar3d"}):
widgets[name] = ("Geometry & medium", self.fields[widget_id])
if mode in {"gpr", "sar3d"}:
widgets[name] = ("Geometry", self.fields[widget_id])
return widgets
def web_schema(self) -> list[dict[str, object]]:
@@ -315,9 +313,10 @@ class ConfigController:
entry.update(kind="select", options=[widget.itemText(i) for i in range(widget.count())])
elif isinstance(widget, QCheckBox):
entry.update(kind="bool")
elif isinstance(widget, (QSpinBox, QDoubleSpinBox)):
entry.update(kind="int" if isinstance(widget, QSpinBox) else "float",
min=widget.minimum(), max=widget.maximum(), step=widget.singleStep())
elif isinstance(widget, (QSpinBox, QDoubleSpinBox, QSlider)):
entry.update(kind="int" if isinstance(widget, (QSpinBox, QSlider)) else "float",
min=widget.minimum(), max=widget.maximum(), step=widget.singleStep(),
slider=isinstance(widget, QSlider))
if isinstance(widget, QDoubleSpinBox):
entry["decimals"] = widget.decimals()
elif isinstance(widget, AntennaGeometryEditor):
@@ -357,7 +356,7 @@ class ConfigController:
geometry_snapshots.append((widget, widget.records()))
else:
scalar_snapshots.append((widget, read_setting(widget)))
if isinstance(widget, (QSpinBox, QDoubleSpinBox)):
if isinstance(widget, (QSpinBox, QDoubleSpinBox, QSlider)):
ranges.append((widget, widget.minimum(), widget.maximum()))
try:
if "processor_mode" in fields:
@@ -17,8 +17,7 @@ from python_app.models.run_config_schema import RunConfigModel
from python_app.models.setting_fields import active_live_fields
from python_app.orchestration.live_processing_config import ProcessingLiveConfig, ProcessingLiveConfigWriter
_GPR_MODES = {"gpr", "legacy_gpr"}
_GEOMETRY_MODES = _GPR_MODES | {"sar3d"}
_GEOMETRY_MODES = {"gpr", "sar3d"}
class LiveSettingsController:
@@ -84,7 +83,7 @@ class LiveSettingsController:
def commit() -> None:
self.validate_mode(self._mode.currentText())
live_fields = {binding.spec.web for binding in active_live_fields(self._mode.currentText())}
if settings.keys() & (live_fields | {"tx_geometry", "rx_geometry", "relative_permittivity"}):
if settings.keys() & (live_fields | {"tx_geometry", "rx_geometry"}):
self.publish()
self.config.apply_web_fields(settings, commit)
@@ -171,12 +170,12 @@ class LiveSettingsController:
self._log.show_exception("Failed to update plot display settings", exc)
def locator_changed(self, *_args: object) -> None:
if self._mode.currentText() in _GPR_MODES and self._publish_changes():
if self._mode.currentText() == "gpr" and self._publish_changes():
self.visual_changed()
def set_mode_page(self, mode: str) -> None:
pages = self._processing.pages
pages.setCurrentIndex({"pass_through": 0, "bscan": 1, "gpr": 2, "legacy_gpr": 3, "sar3d": 4}.get(mode, 0))
pages.setCurrentIndex({"pass_through": 0, "bscan": 1, "gpr": 2, "sar3d": 3}.get(mode, 0))
self._processing.geometry_page.setVisible(mode in _GEOMETRY_MODES)
page = pages.currentWidget()
if page is not None:
@@ -55,7 +55,7 @@ class PlotController:
return self.sar3d.grid if current is self.sar3d.widget else current
def set_mode(self, mode: str) -> None:
renderer = {"bscan": self.bscan, "gpr": self.gpr, "legacy_gpr": self.gpr, "sar3d": self.sar3d}.get(mode, self.trace)
renderer = {"bscan": self.bscan, "gpr": self.gpr, "sar3d": self.sar3d}.get(mode, self.trace)
self.stack.setCurrentWidget(renderer.widget)
self.sync_display_combos()
@@ -114,15 +114,15 @@ class PlotController:
def draw(self, collection: ResultCollection) -> bool:
settings = self._settings()
mode = settings.selected_mode
if mode in {"gpr", "legacy_gpr", "sar3d"} and self.required_revision is not None:
if mode in {"gpr", "sar3d"} and self.required_revision is not None:
if collection.settings_revision != self.required_revision:
return False
if mode == "bscan":
return self.draw_bscan()
if mode in {"gpr", "legacy_gpr"}:
return self.gpr.draw(collection, settings.legacy_gpr if mode == "legacy_gpr" else settings.gpr, self._geometry())
if mode == "gpr":
return self.gpr.draw(collection, settings.gpr, self._geometry())
if mode == "sar3d":
return self.sar3d.draw(collection)
return self.sar3d.draw(collection, settings.sar3d, self._geometry())
return self.trace.draw(collection, settings.pass_through)
def redraw(self, *_args: object) -> None:
@@ -144,14 +144,13 @@ class PlotController:
self.sync_display_combos()
def clear_gpr(self) -> None:
settings = self._settings()
self.gpr.clear(settings.legacy_gpr if settings.selected_mode == "legacy_gpr" else settings.gpr)
self.gpr.clear()
def clear_current(self) -> None:
mode = self._settings().selected_mode
if mode == "bscan":
self.clear_bscan()
elif mode in {"gpr", "legacy_gpr"}:
elif mode == "gpr":
self.clear_gpr()
elif mode == "sar3d":
self.sar3d.clear()
@@ -3,6 +3,8 @@
import numpy as np
import pyqtgraph as pg
from python_app.models.run_config_schema import GprModel
from python_app.models.run_config_validation import parse_position_csv
from python_app.gui.theme import PLOT_BACKGROUND
@@ -22,3 +24,16 @@ def build_lut(stops: list[str], *, size: int = 256) -> np.ndarray:
for channel in range(3):
lut[:, channel] = np.interp(sample_positions, stop_positions, stop_colors[:, channel]).astype(np.uint8)
return lut
# Same Tx/Rx markers on ground BP and the three planar SAR views.
ANTENNA_STYLES = (("t", "#ff595e", "#ffca3a"), ("t1", "#4cc9f0", "#e0fbfc"))
def selected_antenna_coordinates(geometry: GprModel, inputs: str, outputs: str) -> tuple[np.ndarray, np.ndarray]:
selected_inputs, selected_outputs = parse_position_csv(inputs), parse_position_csv(outputs)
tx = [[row.x_m, row.y_m, row.z_m] for row in geometry.tx_geometry
if not selected_outputs or row.output_pos in selected_outputs]
rx = [[row.x_m, row.y_m, row.z_m] for row in geometry.rx_geometry
if not selected_inputs or row.input_pos in selected_inputs]
return np.asarray(tx, dtype=float).reshape(-1, 3), np.asarray(rx, dtype=float).reshape(-1, 3)
+28 -89
View File
@@ -6,18 +6,14 @@ from PyQt6.QtCore import QRectF
import numpy as np
import pyqtgraph as pg
from python_app.gui.controllers.renderers.common import build_lut, create_plot
from python_app.gui.controllers.renderers.common import ANTENNA_STYLES, build_lut, create_plot, selected_antenna_coordinates
from python_app.gui.result_payloads import (
collection_payload_by_name, collection_payloads_by_prefix,
filter_object_rows, gpr_object_rows,
collection_payload_by_name, gpr_object_rows,
)
from python_app.models.run_config_validation import parse_position_csv
from python_app.models.dataset_model import ResultCollection, ResultPayload
from python_app.models.gui_profile_schema import GuiGprStateModel, GuiLegacyGprStateModel
from python_app.models.gui_profile_schema import GuiGprStateModel
from python_app.models.run_config_schema import GprModel
GprSettings = GuiGprStateModel | GuiLegacyGprStateModel
def normalized_display_range(start: float, stop: float, *, minimum_span: float = 0.1) -> tuple[float, float]:
"""Order display bounds and expand an empty interval around its center."""
@@ -54,8 +50,8 @@ def object_label_candidates(
]
def _title(settings: GprSettings, *, objects_only: bool = False) -> str:
title = f"Legacy GPR {settings.mode}" if isinstance(settings, GuiLegacyGprStateModel) else "GPR coherent BP"
def _title(*, objects_only: bool = False) -> str:
title = "GPR coherent BP — ground plane"
return f"{title} Objects Only" if objects_only else title
@@ -63,7 +59,7 @@ class GprRenderer:
"""Draw one collection from explicit display settings and antenna geometry."""
def __init__(self) -> None:
self.widget = create_plot("GPR coherent BP")
self.widget = create_plot("GPR coherent BP — ground plane")
plot = self.widget.getPlotItem()
assert plot is not None
self._plot = plot
@@ -75,17 +71,14 @@ class GprRenderer:
self._tx = pg.ScatterPlotItem()
self._rx = pg.ScatterPlotItem()
self._points = pg.ScatterPlotItem()
self._region_centers = pg.ScatterPlotItem()
self._markers = (self._tx, self._rx, self._points, self._region_centers)
for item, z_value in zip((self._image, *self._markers), (0, 20, 20, 30, 30)):
self._markers = (self._tx, self._rx, self._points)
for item, z_value in zip((self._image, *self._markers), (0, 20, 20, 30)):
item.setZValue(z_value)
item.hide()
self._plot.addItem(item)
self._labels: list[pg.TextItem] = []
self._masks: list[pg.ImageItem] = []
self._contours: list[pg.IsocurveItem] = []
self._geometry_signature: tuple[object, ...] | None = None
self._selected_geometry = (np.zeros(0, dtype=np.float32), np.zeros(0, dtype=np.float32))
self._selected_geometry = (np.empty((0, 3)), np.empty((0, 3)))
def _configure_axes(self) -> None:
plot = self._plot
@@ -103,13 +96,11 @@ class GprRenderer:
view.setMenuEnabled(False)
def _clear_annotations(self) -> None:
for item in (*self._labels, *self._masks):
for item in self._labels:
self._plot.removeItem(item)
self._labels.clear()
self._masks.clear()
self._contours.clear()
def clear(self, settings: GprSettings) -> None:
def clear(self) -> None:
"""Clear displayed data while retaining reusable graphics items."""
self._configure_axes()
self._clear_annotations()
@@ -117,41 +108,25 @@ class GprRenderer:
for marker in self._markers:
marker.setData(x=[], y=[])
marker.hide()
self._plot.setTitle(_title(settings))
self._plot.setTitle(_title())
def _geometry(self, settings: GprSettings, geometry: GprModel) -> tuple[np.ndarray, np.ndarray]:
inputs = tuple(parse_position_csv(settings.input_positions))
outputs = tuple(parse_position_csv(settings.output_positions))
def _geometry(self, settings: GuiGprStateModel, geometry: GprModel) -> tuple[np.ndarray, np.ndarray]:
signature = (
tuple((entry.output_pos, entry.x_m, entry.y_m, entry.z_m) for entry in geometry.tx_geometry),
tuple((entry.input_pos, entry.x_m, entry.y_m, entry.z_m) for entry in geometry.rx_geometry),
inputs, outputs,
settings.input_positions, settings.output_positions,
)
if signature != self._geometry_signature:
self._selected_geometry = (
np.asarray([
entry.x_m for entry in sorted(geometry.tx_geometry, key=lambda entry: entry.output_pos)
if not outputs or entry.output_pos in outputs
], dtype=np.float32),
np.asarray([
entry.x_m for entry in sorted(geometry.rx_geometry, key=lambda entry: entry.input_pos)
if not inputs or entry.input_pos in inputs
], dtype=np.float32),
)
self._selected_geometry = selected_antenna_coordinates(geometry, settings.input_positions, settings.output_positions)
self._geometry_signature = signature
return self._selected_geometry
def _draw_geometry(self, settings: GprSettings, geometry: GprModel) -> None:
def _draw_geometry(self, settings: GuiGprStateModel, geometry: GprModel) -> None:
tx, rx = self._geometry(settings, geometry)
for marker, xs, symbol, brush, pen in (
(self._tx, tx, "t", "#ff595e", "#ffca3a"),
(self._rx, rx, "t1", "#4cc9f0", "#e0fbfc"),
):
marker.setData(
x=xs, y=np.zeros_like(xs), symbol=symbol, size=13,
brush=pg.mkBrush(brush), pen=pg.mkPen(pen, width=1.0),
)
marker.setVisible(xs.size > 0)
for marker, coordinates, (symbol, brush, pen) in zip((self._tx, self._rx), (tx, rx), ANTENNA_STYLES):
marker.setData(x=coordinates[:, 0], y=coordinates[:, 2], symbol=symbol, size=13,
brush=pg.mkBrush(brush), pen=pg.mkPen(pen, width=1.0))
marker.setVisible(len(coordinates) > 0)
def _label(self, text: str, x: float, y: float, color: str) -> pg.TextItem:
label = pg.TextItem(text=text, color=color, anchor=(0.0, 1.0))
@@ -161,34 +136,6 @@ class GprRenderer:
self._labels.append(label)
return label
def _draw_regions(self, collection: ResultCollection, rect: QRectF) -> None:
centers_payload = collection_payload_by_name(collection, "gpr_region_centers", kind=4)
if centers_payload is not None and np.asarray(centers_payload.table).size > 0:
centers = np.asarray(centers_payload.table, dtype=np.float32)
self._region_centers.setData(
x=centers[:, 0], y=centers[:, 1], symbol="o", size=10,
brush=pg.mkBrush("#80ed99"), pen=pg.mkPen("#081c15", width=1.1),
)
self._region_centers.show()
for row in centers:
self._label(f"{float(row[2]):.0f}", float(row[0]), float(row[1]), "#d8f3dc")
for payload in collection_payloads_by_prefix(collection, "gpr_region_mask_", kind=3):
mask = np.asarray(payload.image, dtype=np.float32)
if mask.ndim != 2 or mask.size == 0:
continue
mask_image = pg.ImageItem(axisOrder="row-major")
mask_image.setZValue(5)
mask_image.setImage(mask, autoLevels=False)
mask_image.setRect(rect)
mask_image.setOpacity(0.0)
self._plot.addItem(mask_image)
contour = pg.IsocurveItem(data=mask)
contour.setLevel(0.5)
contour.setPen("#4cc9f0", width=1.3)
contour.setParentItem(mask_image)
self._masks.append(mask_image)
self._contours.append(contour)
@staticmethod
def _place_object_label(
label: pg.TextItem, x: float, z: float, x_span: float, z_span: float, occupied: list[QRectF],
@@ -203,7 +150,7 @@ class GprRenderer:
occupied.append(label.sceneBoundingRect().adjusted(-4.0, -4.0, 4.0, 4.0))
def _draw_points(
self, rows: np.ndarray, settings: GprSettings, objects_only: bool, x_span: float, z_span: float,
self, rows: np.ndarray, objects_only: bool, x_span: float, z_span: float,
) -> None:
if rows.size == 0:
return
@@ -217,39 +164,33 @@ class GprRenderer:
occupied: list[QRectF] = []
for x_value, z_value, score in rows:
x, z = float(x_value), float(z_value)
text = f"{float(score):.0f}" if isinstance(settings, GuiLegacyGprStateModel) else f"{float(score):.2f}"
text = f"{float(score):.2f}"
if objects_only:
text += f" | x={x:.1f} | z={z:.1f}"
label = self._label(text, x, z, "#ffd6d9" if objects_only else "#ffffff")
if objects_only:
self._place_object_label(label, x, z, x_span, z_span, occupied)
def draw(self, collection: ResultCollection, settings: GprSettings, geometry: GprModel) -> bool:
def draw(self, collection: ResultCollection, settings: GuiGprStateModel, geometry: GprModel) -> bool:
"""Render a heatmap or object-only view, returning whether data is present."""
objects_only = settings.render_mode == "objects_only"
accumulator = collection_payload_by_name(collection, "gpr_accumulator", kind=3)
x_min, x_max = normalized_display_range(settings.visible_x_min_m, settings.visible_x_max_m)
z_min, z_max = normalized_display_range(settings.visible_z_min_m, settings.visible_z_max_m)
rows = gpr_object_rows(collection)
# Coherent BP rows are finalized in C++; only legacy GPR uses GUI filtering.
if isinstance(settings, GuiLegacyGprStateModel):
rows = filter_object_rows(
rows, min_score=float(settings.min_visible_pair_count),
x_bounds=(x_min, x_max), z_bounds=(z_min, z_max),
)
if objects_only:
if accumulator is None and rows.size == 0:
self.clear(settings)
self.clear()
return False
elif accumulator is None or not self._valid_accumulator(accumulator):
self.clear(settings)
self.clear()
return False
self.widget.setUpdatesEnabled(False)
try:
self._clear_annotations()
self._image.hide()
for marker in (self._points, self._region_centers):
for marker in (self._points,):
marker.setData(x=[], y=[])
marker.hide()
self._view.invertY(False)
@@ -274,13 +215,11 @@ class GprRenderer:
points = collection_payload_by_name(collection, "gpr_points", kind=4)
if points is None or np.asarray(points.table).size == 0:
rows = np.zeros((0, 3), dtype=np.float32)
self._draw_points(rows, settings, objects_only, x_max - x_min, z_max - z_min)
if rect is not None:
self._draw_regions(collection, rect)
self._draw_points(rows, objects_only, x_max - x_min, z_max - z_min)
if objects_only:
self._view.setXRange(x_min, x_max, padding=0.0)
self._view.setYRange(display_y_min(z_min, z_max), z_max, padding=0.0)
self._plot.setTitle(_title(settings, objects_only=objects_only))
self._plot.setTitle(_title(objects_only=objects_only))
finally:
self.widget.setUpdatesEnabled(True)
return True
+52 -18
View File
@@ -9,6 +9,7 @@ away from the Y boundaries:
- ``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_target_width``: axial FWHM [X, Y, Z] in metres through the joint peak.
- ``sar3d_peak``: [level, X, Y, Z] of that maximum before normalization.
- ``sar3d_status``: [code, UTF-8 bytes...] with ready CUDA (0), no signal (1),
processing error (2), or CUDA unavailable (3), including the explanation.
@@ -23,13 +24,11 @@ import pyqtgraph as pg
from PyQt6.QtCore import QRectF, Qt
from PyQt6.QtWidgets import QGridLayout, QLabel, QVBoxLayout, QWidget
from python_app.gui.controllers.renderers.common import build_lut, create_plot
from python_app.gui.controllers.renderers.common import ANTENNA_STYLES, build_lut, create_plot, selected_antenna_coordinates
from python_app.gui.result_payloads import collection_payload_by_name
from python_app.models.dataset_model import ResultCollection, ResultPayload
# Colours start at this fraction of the normalization reference; weaker voxels stay transparent,
# like those under the notebook's point-cloud threshold (SAR_CLOUD_THRESHOLD).
DISPLAY_THRESHOLD = 0.70
from python_app.models.gui_profile_schema import GuiSar3dStateModel
from python_app.models.run_config_schema import GprModel
# Status codes published by the sar3d processor in its "sar3d_status" table.
SAR3D_STATES = {0: "ready", 1: "no signal", 2: "error", 3: "warning"}
@@ -93,11 +92,19 @@ class Sar3dRenderer:
self.plots[name] = plot
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
lut[0, 3] = 0 # below SAR_CLOUD_THRESHOLD
self.antennas: dict[str, tuple[pg.ScatterPlotItem, pg.ScatterPlotItem]] = {}
self.images: dict[str, pg.ImageItem] = {}
self.peaks: dict[str, pg.ScatterPlotItem] = {}
self.targets: dict[str, pg.PlotCurveItem] = {}
for name, (_title, x_label, y_label, _axes) in SAR3D_VIEWS.items():
plot = self.plots[name]
plot.setAspectLocked(True)
if _axes is not None:
markers = (pg.ScatterPlotItem(), pg.ScatterPlotItem())
for marker in markers:
marker.setZValue(4)
plot.addItem(marker)
self.antennas[name] = markers
item = plot.getPlotItem()
assert item is not None
view = item.getViewBox()
@@ -108,11 +115,11 @@ class Sar3dRenderer:
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 = pg.PlotCurveItem(pen=pg.mkPen("w", width=1.5))
peak.setZValue(3)
plot.addItem(peak)
self.images[name] = image
self.peaks[name] = peak
self.targets[name] = peak
if x_label:
plot.showGrid(x=True, y=True, alpha=0.2)
plot.setLabel("bottom", x_label, units="m")
@@ -146,15 +153,18 @@ class Sar3dRenderer:
"""Clear all four 3D SAR views."""
for name, (title, *_rest) in SAR3D_VIEWS.items():
self.images[name].hide()
self.peaks[name].setData([], [])
self.targets[name].setData([], [])
self.plots[name].setTitle(title)
for markers in self.antennas.values():
for marker in markers:
marker.setData([], [])
self.box.setData([], [])
self.ground.setData([], [])
for label in self.axis_labels.values():
label.hide()
self.status_label.setText(message)
def draw(self, collection: ResultCollection) -> bool:
def draw(self, collection: ResultCollection, settings: GuiSar3dStateModel, geometry: GprModel) -> bool:
"""Draw four maximum projections of the volume; return success."""
status = sar3d_status(collection)
if status is not None and status[0] != 0:
@@ -174,20 +184,44 @@ class Sar3dRenderer:
peak = collection_payload_by_name(collection, "sar3d_peak", kind=4)
level, *peak_xyz = peak.table[0] if peak is not None else (None,)
antennas = selected_antenna_coordinates(geometry, settings.input_positions, settings.output_positions)
pitch = np.radians(settings.look_angle_deg)
for coordinates in antennas:
y, z = coordinates[:, 1].copy(), coordinates[:, 2].copy()
coordinates[:, 1] = settings.bp_ground_depth_m + y*np.cos(pitch) - z*np.sin(pitch)
coordinates[:, 2] = y*np.sin(pitch) + z*np.cos(pitch)
width_payload = collection_payload_by_name(collection, "sar3d_target_width", kind=4)
widths = width_payload.table[0] if width_payload is not None else np.full(3, settings.voxel_m)
zoom = settings.zoom_percent / 100.0
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.images[name]
image.setImage(payload.image, autoLevels=False, levels=(DISPLAY_THRESHOLD, 1.0))
image.setImage(np.where(payload.image >= settings.cloud_threshold, payload.image, 0),
autoLevels=False, levels=(0, 1))
image.setRect(rect)
image.show()
plot = self.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.peaks[name].setData([position[0]], [position[1]])
position = (basis.table @ np.asarray(peak_xyz) if axes is None else np.asarray(peak_xyz)[list(axes)]) if peak_xyz else None
if axes is not None:
for marker, coordinates, (symbol, brush, pen) in zip(self.antennas[name], antennas, ANTENNA_STYLES):
xx, yy = coordinates[:, axes[0]], coordinates[:, axes[1]]
marker.setData(x=xx, y=yy, symbol=symbol, size=13, brush=pg.mkBrush(brush), pen=pg.mkPen(pen))
if len(xx):
pad = settings.voxel_m
rect = rect.united(QRectF(xx.min()-pad, yy.min()-pad, np.ptp(xx)+2*pad, np.ptp(yy)+2*pad))
if position is not None:
extent = np.abs(basis.table) @ widths if axes is None else widths[list(axes)]
half = .5 * max(extent)
self.targets[name].setData(position[0]+np.array([-half, half, half, -half, -half]),
position[1]+np.array([-half, -half, half, half, -half]))
if zoom > 1.0:
center = position + (np.array([rect.center().x(), rect.center().y()])-position) / zoom**2
rect = QRectF(center[0]-rect.width()/(2*zoom), center[1]-rect.height()/(2*zoom),
rect.width()/zoom, rect.height()/zoom)
else:
self.peaks[name].setData([], [])
self.targets[name].setData([], [])
plot.setRange(rect, padding=0.04)
plot.setTitle(title)
# Scene box and ground on the oblique view, from the extents of the other views.
@@ -213,5 +247,5 @@ class Sar3dRenderer:
if level is not None:
x, y, z = peak_xyz
summary += f" | max X {x:+.2f}, Y {y:+.2f}, Z {z:.2f} m | level {level:.3g}"
self.status_label.setText(summary)
self.status_label.setText(f"{summary} | zoom {settings.zoom_percent}%")
return True
@@ -2,7 +2,7 @@
from __future__ import annotations
from PyQt6.QtCore import QRegularExpression
from PyQt6.QtCore import QRegularExpression, Qt
from PyQt6.QtGui import QRegularExpressionValidator
from PyQt6.QtWidgets import (
QCheckBox,
@@ -10,6 +10,7 @@ from PyQt6.QtWidgets import (
QFormLayout,
QGroupBox,
QLineEdit,
QSlider,
QSizePolicy,
QStackedWidget,
QVBoxLayout,
@@ -51,7 +52,7 @@ def build_processing_group(config: RunConfigModel, gui: GuiStateModel) -> Proces
group = view.group
form = QFormLayout(group)
form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
processing_mode = view.add("_processing_mode", choice(['pass_through', 'bscan', 'gpr', 'legacy_gpr', 'sar3d']))
processing_mode = view.add("_processing_mode", choice(['pass_through', 'bscan', 'gpr', 'sar3d']))
processing_mode_pages = QStackedWidget(group)
processing_mode_pages.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
show_magnitude_checkbox = view.add("_show_magnitude_checkbox", QCheckBox("Show magnitude"))
@@ -112,10 +113,6 @@ def build_processing_group(config: RunConfigModel, gui: GuiStateModel) -> Proces
gpr_defaults = config.gpr
# Medium permittivity is a legacy-GPR-only knob (shown on the legacy page below).
# Coherent BP fixes the medium to eps_r = 1, so it is not offered there.
gpr_relative_permittivity = view.add("_gpr_relative_permittivity", double_spin(0.0001, 1000.0, 4, 0.05))
gpr_relative_permittivity.setValue(float(gpr_defaults.relative_permittivity))
gpr_tx_geometry_input = view.add("_gpr_tx_geometry_input", AntennaGeometryEditor(
"output_pos", gpr_defaults.tx_geometry, position_count=config.output_switch.positions,
))
@@ -178,10 +175,9 @@ def build_processing_group(config: RunConfigModel, gui: GuiStateModel) -> Proces
gpr_visible_x_max_m = view.add("_gpr_visible_x_max_m", double_spin(-100.0, 100.0, 2, 0.1))
gpr_visible_z_min_m = view.add("_gpr_visible_z_min_m", double_spin(0.0, 100.0, 2, 0.1))
gpr_visible_z_max_m = view.add("_gpr_visible_z_max_m", double_spin(0.0, 100.0, 2, 0.1))
gpr_imaging_plane_y_m = view.add("_gpr_imaging_plane_y_m", double_spin(-50.0, 50.0, 3, 0.05))
gpr_imaging_plane_y_m.setToolTip(
"Y coordinate of the BP imaging slice (m). Use 0 for legacy 1D antenna layouts."
)
gpr_bp_ground_depth_m = view.add("_gpr_bp_ground_depth_m", double_spin(0.0, 50.0, 3, 0.05))
gpr_bp_ground_depth_m.setToolTip("Height of the radar origin above the ground; the BP slice follows the ground and radar tilt.")
gpr_pair_depth_equalize = view.add("_gpr_pair_depth_equalize", QCheckBox("PAIR_DEPTH_EQUALIZE"))
gpr_page = _build_processing_mode_page(
processing_mode_pages,
@@ -204,89 +200,22 @@ def build_processing_group(config: RunConfigModel, gui: GuiStateModel) -> Proces
("Approach min frames", gpr_object_approach_min_frames),
("Start (MHz)", gpr_start_freq_mhz),
("Stop (MHz)", gpr_stop_freq_mhz),
("Imaging plane Y (m)", gpr_imaging_plane_y_m),
("Radar height (m)", gpr_bp_ground_depth_m),
("Visible X min (m)", gpr_visible_x_min_m),
("Visible X max (m)", gpr_visible_x_max_m),
("Visible Z min (m)", gpr_visible_z_min_m),
("Visible Z max (m)", gpr_visible_z_max_m),
gpr_background_subtract_enabled,
("Mean count", gpr_background_mean_count),
gpr_pair_depth_equalize,
gpr_remove_sidelobe_objects_enabled,
],
split_index=8,
)
processing_mode_pages.addWidget(gpr_page)
legacy_gpr_config_mode = view.add("_legacy_gpr_config_mode", choice(['point', 'extended']))
legacy_gpr_input_positions_input = view.add("_legacy_gpr_input_positions_input", QLineEdit())
legacy_gpr_input_positions_input.setPlaceholderText("0,1,2")
legacy_gpr_output_positions_input = view.add("_legacy_gpr_output_positions_input", QLineEdit())
legacy_gpr_output_positions_input.setPlaceholderText("0,1")
legacy_gpr_min_depth_m = view.add("_legacy_gpr_min_depth_m", double_spin(0.0, 50.0, 2, 0.1))
legacy_gpr_max_depth_m = view.add("_legacy_gpr_max_depth_m", double_spin(0.1, 50.0, 2, 0.1))
legacy_gpr_comp_power = view.add("_legacy_gpr_comp_power", double_spin(0.0, 5.0, 3, 0.05))
legacy_gpr_snr_thresh = view.add("_legacy_gpr_snr_thresh", double_spin(0.0, 1000.0, 2, 0.1))
legacy_gpr_snr_comp_max = view.add("_legacy_gpr_snr_comp_max", double_spin(0.0, 1000.0, 2, 0.5))
legacy_gpr_start_freq_mhz = view.add("_legacy_gpr_start_freq_mhz", double_spin(100.0, 8800.0, 1, 10.0))
legacy_gpr_stop_freq_mhz = view.add("_legacy_gpr_stop_freq_mhz", double_spin(100.0, 8800.0, 1, 10.0))
legacy_gpr_speed_m_s = view.add("_legacy_gpr_speed_m_s", double_spin(-100.0, 100.0, 3, 0.01))
legacy_gpr_ignore_socket_speed_enabled = view.add("_legacy_gpr_ignore_socket_speed_enabled", QCheckBox("Ignore socket speed"))
legacy_gpr_ignore_socket_speed_enabled.setToolTip(_IGNORE_SOCKET_SPEED_TOOLTIP)
legacy_gpr_look_angle_deg = view.add("_legacy_gpr_look_angle_deg", double_spin(-90.0, 90.0, 2, 0.1))
# Intra-sweep phase compensation toggle. When checked, S21 phase is
# corrected per frequency point before the IFFT so the radar's motion
# during one sweep is removed at the spectrum stage. Only matters when
# speed != 0; otherwise has no effect by construction.
legacy_gpr_apply_freq_phase_correction = view.add("_legacy_gpr_apply_freq_phase_correction", QCheckBox("Apply intra-sweep phase correction"))
# Anchor point for the motion model's per-event `dt_ref`. `frame_center`
# spreads motion symmetrically around the frame midpoint; `first_tx_event`
# accumulates it forward from the first event center.
legacy_gpr_reference_mode = view.add("_legacy_gpr_reference_mode", choice(['frame_center', 'first_tx_event']))
legacy_gpr_background_subtract_enabled = view.add("_legacy_gpr_background_subtract_enabled", QCheckBox("Subtract mean of previous collections"))
legacy_gpr_background_mean_count = view.add("_legacy_gpr_background_mean_count", integer_spin(0, 10000))
legacy_gpr_render_mode = view.add("_legacy_gpr_render_mode", choice(['heatmap', 'objects_only']))
legacy_gpr_min_visible_pair_count = view.add("_legacy_gpr_min_visible_pair_count", integer_spin(1, 10000))
legacy_gpr_visible_x_min_m = view.add("_legacy_gpr_visible_x_min_m", double_spin(-100.0, 100.0, 2, 0.1))
legacy_gpr_visible_x_max_m = view.add("_legacy_gpr_visible_x_max_m", double_spin(-100.0, 100.0, 2, 0.1))
legacy_gpr_visible_z_min_m = view.add("_legacy_gpr_visible_z_min_m", double_spin(0.0, 100.0, 2, 0.1))
legacy_gpr_visible_z_max_m = view.add("_legacy_gpr_visible_z_max_m", double_spin(0.0, 100.0, 2, 0.1))
legacy_gpr_page = _build_processing_mode_page(
processing_mode_pages,
[
("Config mode", legacy_gpr_config_mode),
("Relative permittivity", gpr_relative_permittivity),
("Input positions", legacy_gpr_input_positions_input),
("Output positions", legacy_gpr_output_positions_input),
("Min depth (m)", legacy_gpr_min_depth_m),
("Max depth (m)", legacy_gpr_max_depth_m),
("Compensation power", legacy_gpr_comp_power),
("SNR threshold", legacy_gpr_snr_thresh),
("Max SNR compensation", legacy_gpr_snr_comp_max),
("Speed (m/s)", legacy_gpr_speed_m_s),
legacy_gpr_ignore_socket_speed_enabled,
("Render mode", legacy_gpr_render_mode),
("Min visible pairs", legacy_gpr_min_visible_pair_count),
("Start (MHz)", legacy_gpr_start_freq_mhz),
("Stop (MHz)", legacy_gpr_stop_freq_mhz),
("Look angle (°)", legacy_gpr_look_angle_deg),
("Reference mode", legacy_gpr_reference_mode),
legacy_gpr_apply_freq_phase_correction,
("Visible X min (m)", legacy_gpr_visible_x_min_m),
("Visible X max (m)", legacy_gpr_visible_x_max_m),
("Visible Z min (m)", legacy_gpr_visible_z_min_m),
("Visible Z max (m)", legacy_gpr_visible_z_max_m),
legacy_gpr_background_subtract_enabled,
("Mean count", legacy_gpr_background_mean_count),
],
split_index=10,
)
processing_mode_pages.addWidget(legacy_gpr_page)
# 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.
# Multi_antenna_demonstrate_depthnorm.ipynb; the shared gpr_* fields get their own
# widgets here, like on the GPR page.
sar3d_input_positions_input = view.add("_sar3d_input_positions_input", QLineEdit())
sar3d_input_positions_input.setPlaceholderText("0,1,2")
sar3d_output_positions_input = view.add("_sar3d_output_positions_input", QLineEdit())
@@ -311,8 +240,8 @@ def build_processing_group(config: RunConfigModel, gui: GuiStateModel) -> Proces
sar3d_speed_m_s = view.add("_sar3d_speed_m_s", double_spin(-100.0, 100.0, 3, 0.05))
sar3d_frame_radius = view.add("_sar3d_frame_radius", integer_spin(0, 24))
sar3d_frame_radius.setToolTip(
"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."
"Causal aperture: current and r previous frames. 0: the current frame only (needs a vertical antenna "
"baseline); >0 adds aperture from motion and needs an accurate speed. The scene follows the current frame."
)
sar3d_bp_ground_depth_m = view.add("_sar3d_bp_ground_depth_m", double_spin(0.0, 10.0, 2, 0.05))
sar3d_bp_ground_depth_m.setToolTip("Height of the lower antenna row above the ground (m); scene Y = 0 is the ground.")
@@ -321,6 +250,17 @@ def build_processing_group(config: RunConfigModel, gui: GuiStateModel) -> Proces
sar3d_use_compensation = view.add("_sar3d_use_compensation", QCheckBox("SAR_USE_COMPENSATION"))
sar3d_normalize_each_frame = view.add("_sar3d_normalize_each_frame", QCheckBox("SAR_NORMALIZE_EACH_FRAME"))
sar3d_normalize_each_frame.setToolTip("Scale every Tx/Rx A-scan by its median inside the depth gate.")
sar3d_pair_depth_equalize = view.add("_sar3d_pair_depth_equalize", QCheckBox("PAIR_DEPTH_EQUALIZE"))
sar3d_background_subtract_enabled = view.add("_sar3d_background_subtract_enabled", QCheckBox("Subtract mean of previous collections"))
sar3d_background_mean_count = view.add("_sar3d_background_mean_count", integer_spin(0, 10000))
sar3d_x_size_m = view.add("_sar3d_x_size_m", double_spin(0.0, 100.0, 2, 0.1))
sar3d_y_min_m = view.add("_sar3d_y_min_m", double_spin(-50.0, 50.0, 2, 0.1))
sar3d_y_max_m = view.add("_sar3d_y_max_m", double_spin(-50.0, 50.0, 2, 0.1))
sar3d_z_size_m = view.add("_sar3d_z_size_m", double_spin(0.0, 100.0, 2, 0.1))
sar3d_cloud_threshold = view.add("_sar3d_cloud_threshold", double_spin(0.0, 1.0, 2, 0.05))
sar3d_zoom_percent = view.add("_sar3d_zoom_percent", QSlider(Qt.Orientation.Horizontal))
sar3d_zoom_percent.setRange(100, 1000)
sar3d_zoom_percent.setToolTip("100% shows the full scene; higher values zoom towards its brightest object.")
sar3d_page = _build_processing_mode_page(
processing_mode_pages,
@@ -340,6 +280,15 @@ def build_processing_group(config: RunConfigModel, gui: GuiStateModel) -> Proces
("COMP_RANGE_POWER", sar3d_comp_range_power),
sar3d_use_compensation,
sar3d_normalize_each_frame,
sar3d_pair_depth_equalize,
sar3d_background_subtract_enabled,
("Mean count", sar3d_background_mean_count),
("SAR_X_SIZE_M (0 = auto)", sar3d_x_size_m),
("SAR_Y_RANGE_M min", sar3d_y_min_m),
("SAR_Y_RANGE_M max", sar3d_y_max_m),
("SAR_Z_SIZE_M (0 = auto)", sar3d_z_size_m),
("SAR_CLOUD_THRESHOLD", sar3d_cloud_threshold),
("Zoom (%)", sar3d_zoom_percent),
],
split_index=8,
)
@@ -367,17 +316,14 @@ def build_processing_group(config: RunConfigModel, gui: GuiStateModel) -> Proces
geometry_page=gpr_common_page, geometries=(gpr_tx_geometry_input, gpr_rx_geometry_input),
fixed_y=pass_through_fixed_y_enabled, y_bounds=(pass_through_y_min_db, pass_through_y_max_db),
speed_controls=((gpr_ignore_socket_speed_enabled, gpr_speed_m_s),
(legacy_gpr_ignore_socket_speed_enabled, legacy_gpr_speed_m_s),
(sar3d_ignore_socket_speed_enabled, sar3d_speed_m_s)),
trace_controls=(show_magnitude_checkbox, show_phase_checkbox, unwrap_phase_checkbox,
pass_through_combo_filter_input, pass_through_fixed_y_enabled,
pass_through_y_min_db, pass_through_y_max_db),
visual_controls=(gpr_render_mode, legacy_gpr_render_mode),
visual_controls=(gpr_render_mode, sar3d_cloud_threshold, sar3d_zoom_percent),
locator_controls=(gpr_max_detected_objects_to_draw, gpr_draw_top_m_objects,
legacy_gpr_min_visible_pair_count, gpr_visible_x_min_m, gpr_visible_x_max_m,
gpr_visible_z_min_m, gpr_visible_z_max_m, legacy_gpr_visible_x_min_m,
legacy_gpr_visible_x_max_m, legacy_gpr_visible_z_min_m, legacy_gpr_visible_z_max_m),
gpr_visible_x_min_m, gpr_visible_x_max_m,
gpr_visible_z_min_m, gpr_visible_z_max_m),
frequency_controls=(bscan_start_freq_mhz, bscan_stop_freq_mhz, gpr_start_freq_mhz, gpr_stop_freq_mhz,
legacy_gpr_start_freq_mhz, legacy_gpr_stop_freq_mhz,
sar3d_start_freq_mhz, sar3d_stop_freq_mhz),
)
+5 -5
View File
@@ -6,13 +6,13 @@ from typing import TypeAlias, TypeVar
from PyQt6.QtWidgets import (
QCheckBox, QComboBox, QDoubleSpinBox, QGroupBox, QLineEdit, QPlainTextEdit,
QSpinBox, QWidget, QLabel, QPushButton, QStackedWidget,
QSpinBox, QSlider, QWidget, QLabel, QPushButton, QStackedWidget,
)
from python_app.gui.antenna_geometry_editor import AntennaGeometryEditor
from python_app.models.setting_fields import Scalar
ScalarWidget: TypeAlias = QLineEdit | QPlainTextEdit | QComboBox | QCheckBox | QSpinBox | QDoubleSpinBox
ScalarWidget: TypeAlias = QLineEdit | QPlainTextEdit | QComboBox | QCheckBox | QSpinBox | QDoubleSpinBox | QSlider
SettingWidget: TypeAlias = ScalarWidget | AntennaGeometryEditor
_Widget = TypeVar("_Widget", bound=SettingWidget)
@@ -102,7 +102,7 @@ class RadarView(SectionView):
def read_setting(widget: ScalarWidget) -> Scalar:
if isinstance(widget, QCheckBox):
return widget.isChecked()
if isinstance(widget, (QSpinBox, QDoubleSpinBox)):
if isinstance(widget, (QSpinBox, QDoubleSpinBox, QSlider)):
return widget.value()
if isinstance(widget, QComboBox):
return widget.currentText()
@@ -114,7 +114,7 @@ def read_setting(widget: ScalarWidget) -> Scalar:
def write_setting(widget: ScalarWidget, value: Scalar) -> None:
if isinstance(widget, QCheckBox):
widget.setChecked(bool(value))
elif isinstance(widget, QSpinBox):
elif isinstance(widget, (QSpinBox, QSlider)):
widget.setValue(int(value))
elif isinstance(widget, QDoubleSpinBox):
widget.setValue(float(value))
@@ -134,7 +134,7 @@ def write_setting(widget: ScalarWidget, value: Scalar) -> None:
def connect_setting(widget: ScalarWidget, callback: Callable[..., object]) -> None:
if isinstance(widget, QCheckBox):
signal = widget.toggled
elif isinstance(widget, (QSpinBox, QDoubleSpinBox)):
elif isinstance(widget, (QSpinBox, QDoubleSpinBox, QSlider)):
signal = widget.valueChanged
elif isinstance(widget, QComboBox):
signal = widget.currentTextChanged
+1 -46
View File
@@ -1,4 +1,4 @@
"""Result payload lookup and GPR display filtering."""
"""Result payload lookup and coherent-GPR objects."""
from __future__ import annotations
@@ -22,22 +22,6 @@ def collection_payload_by_name(
return None
def collection_payloads_by_prefix(
collection: ResultCollection,
prefix: str,
kind: int | None = None,
) -> list[ResultPayload]:
"""Return collection payloads matching a processing-name prefix."""
payloads: list[ResultPayload] = []
for payload in collection.collection_payloads:
if not str(payload.processing_name).startswith(prefix):
continue
if kind is not None and int(payload.kind) != int(kind):
continue
payloads.append(payload)
return payloads
def gpr_object_rows(collection: ResultCollection) -> np.ndarray:
"""Return object rows as `[x_m, z_m, score]` from a GPR collection."""
points_payload = collection_payload_by_name(collection, "gpr_points", kind=4)
@@ -46,33 +30,4 @@ def gpr_object_rows(collection: ResultCollection) -> np.ndarray:
if points.ndim == 2 and points.shape[1] >= 3:
return points[:, :3]
centers_payload = collection_payload_by_name(collection, "gpr_region_centers", kind=4)
if centers_payload is not None:
centers = np.asarray(centers_payload.table, dtype=np.float32)
if centers.ndim == 2 and centers.shape[1] >= 3:
return centers[:, :3]
return np.zeros((0, 3), dtype=np.float32)
def filter_object_rows(
rows: np.ndarray,
*,
min_score: float,
x_bounds: tuple[float, float],
z_bounds: tuple[float, float],
) -> np.ndarray:
"""Keep finite objects inside the visible window and above the score threshold."""
if rows.size == 0:
return rows
x_min, x_max = x_bounds
z_min, z_max = z_bounds
visible_mask = (
np.all(np.isfinite(rows[:, :3]), axis=1)
& (rows[:, 2] >= min_score)
& (rows[:, 0] >= x_min)
& (rows[:, 0] <= x_max)
& (rows[:, 1] >= z_min)
& (rows[:, 1] <= z_max)
)
return rows[visible_mask]
+1 -1
View File
@@ -26,7 +26,7 @@ def validate_processing_mode_constraints(
# 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"}:
if processing_mode not in {"gpr", "sar3d"}:
return
configured_combos = {(int(combo.input), int(combo.output)) for combo in config.combos}
+1 -7
View File
@@ -5,7 +5,7 @@ from pathlib import Path
from python_app.models.run_config_schema import RunConfigModel, ComboModel
from python_app.models.gui_profile_schema import (
GuiStateModel, GuiSwitchStateModel, GuiProcessingStateModel, GuiGprStateModel,
GuiLegacyGprStateModel, GuiSar3dStateModel, GuiDataActionsStateModel,
GuiSar3dStateModel, GuiDataActionsStateModel,
)
GUI_SAVE_HISTORY_LIMIT = 1000
@@ -91,12 +91,6 @@ def default_gui_state_for_config(config: RunConfigModel, project_root: Path) ->
visible_x_min_m=default_gpr_x_min_m,
visible_x_max_m=default_gpr_x_max_m,
),
legacy_gpr=GuiLegacyGprStateModel(
input_positions=inputs,
output_positions=outputs,
visible_x_min_m=default_gpr_x_min_m,
visible_x_max_m=default_gpr_x_max_m,
),
sar3d=GuiSar3dStateModel(
input_positions=inputs,
output_positions=outputs,