test new version
This commit is contained in:
@@ -13,6 +13,7 @@ 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")
|
||||
@@ -99,6 +100,7 @@ _WEB_LIVE_SCHEMA = [
|
||||
("sar3d_grid_nz", "3D SAR", ("sar3d",), _attr("_sar3d_grid_nz")),
|
||||
("sar3d_use_compensation", "3D SAR", ("sar3d",), _attr("_sar3d_use_compensation")),
|
||||
("sar3d_normalize_pairs", "3D SAR", ("sar3d",), _attr("_sar3d_normalize_pairs")),
|
||||
("sar3d_bottom_view_deg", "3D SAR", ("sar3d",), _attr("_sar3d_bottom_view")),
|
||||
]
|
||||
_WEB_LIVE_GETTERS = {field: getter for field, _group, _modes, getter in _WEB_LIVE_SCHEMA}
|
||||
|
||||
@@ -285,6 +287,10 @@ 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)
|
||||
|
||||
@@ -10,6 +10,7 @@ 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 (
|
||||
@@ -571,6 +572,10 @@ class AppWindowConfigProfileIOMixin:
|
||||
self._sar3d_grid_nz.setValue(int(sar3d_state.grid_nz))
|
||||
self._sar3d_use_compensation.setChecked(bool(sar3d_state.use_compensation))
|
||||
self._sar3d_normalize_pairs.setChecked(bool(sar3d_state.normalize_pairs))
|
||||
self._set_combo_current_text(
|
||||
self._sar3d_bottom_view,
|
||||
sar3d_bottom_view_label_from_deg(float(sar3d_state.bottom_view_deg)),
|
||||
)
|
||||
|
||||
self._save_count.setValue(int(gui_state.data_actions.save_count))
|
||||
self._record_count.setValue(int(gui_state.data_actions.record_count))
|
||||
|
||||
@@ -25,6 +25,7 @@ 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
|
||||
|
||||
|
||||
@@ -447,6 +448,9 @@ class AppWindowConfigStateBuildersMixin:
|
||||
grid_nz=int(self._sar3d_grid_nz.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()
|
||||
),
|
||||
),
|
||||
),
|
||||
data_actions=GuiDataActionsStateModel(
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
"""3D SAR projection rendering helpers.
|
||||
|
||||
The sar3d processor publishes two normalized max-projections of the imaged
|
||||
volume as collection-level ImageF32 payloads:
|
||||
volume as collection-level ImageF32 payloads, matching the legacy render
|
||||
conventions (X across, Z range/"вдаль", Y up):
|
||||
|
||||
- ``sar3d_proj_xz`` — side view: X (m) horizontal, Z/range (m) vertical;
|
||||
- ``sar3d_proj_xy`` — top view: X (m) horizontal, Y (m) vertical;
|
||||
- ``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
|
||||
(side view on top), mirroring how the pass-through page stacks its two plots.
|
||||
(top view on top), mirroring how the pass-through page stacks its two plots.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -17,23 +21,39 @@ from PyQt6.QtCore import QRectF
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
|
||||
from python_app.gui.runtime.sar3d_views import sar3d_bottom_view_deg_from_label
|
||||
from python_app.models.dataset_model import ResultCollection
|
||||
|
||||
|
||||
class AppWindowSar3dPlotMixin:
|
||||
"""Renders 3D SAR volume projections published by the sar3d processor."""
|
||||
|
||||
def _sar3d_bottom_view_labels(self) -> tuple[str, str]:
|
||||
"""Return (plot title, horizontal axis label) for the active bottom view.
|
||||
|
||||
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."""
|
||||
for plot, y_label in (
|
||||
(self._sar3d_xz_plot, "Range Z"),
|
||||
(self._sar3d_xy_plot, "Cross-range Y"),
|
||||
_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", units="m")
|
||||
plot.setLabel("bottom", x_label, units="m")
|
||||
plot.setLabel("left", y_label, units="m")
|
||||
view_box = plot.getViewBox()
|
||||
view_box.invertY(False)
|
||||
@@ -69,8 +89,9 @@ class AppWindowSar3dPlotMixin:
|
||||
self._sar3d_xz_image_item.hide()
|
||||
if self._sar3d_xy_image_item is not None:
|
||||
self._sar3d_xy_image_item.hide()
|
||||
self._sar3d_xz_plot.setTitle("3D SAR side view (X-Z)")
|
||||
self._sar3d_xy_plot.setTitle("3D SAR top view (X-Y)")
|
||||
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."""
|
||||
@@ -98,10 +119,10 @@ class AppWindowSar3dPlotMixin:
|
||||
return True
|
||||
|
||||
def _draw_sar3d_projections(self, collection: ResultCollection) -> bool:
|
||||
"""Draw the latest collection-level 3D SAR side and top projections."""
|
||||
"""Draw the latest collection-level 3D SAR top and bottom projections."""
|
||||
xz_payload = self._collection_payload_by_name(collection, "sar3d_proj_xz", kind=3)
|
||||
xy_payload = self._collection_payload_by_name(collection, "sar3d_proj_xy", kind=3)
|
||||
if xz_payload is None and xy_payload is None:
|
||||
bottom_payload = self._collection_payload_by_name(collection, "sar3d_proj_bottom", kind=3)
|
||||
if xz_payload is None and bottom_payload is None:
|
||||
self._clear_sar3d_plot()
|
||||
return False
|
||||
|
||||
@@ -109,13 +130,15 @@ class AppWindowSar3dPlotMixin:
|
||||
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
|
||||
)
|
||||
xy_drawn = xy_payload is not None and self._draw_sar3d_projection(
|
||||
self._sar3d_xy_plot, self._sar3d_xy_image_item, xy_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 xy_drawn:
|
||||
if not xz_drawn and not bottom_drawn:
|
||||
self._clear_sar3d_plot()
|
||||
return False
|
||||
|
||||
@@ -123,8 +146,9 @@ class AppWindowSar3dPlotMixin:
|
||||
peak_suffix = ""
|
||||
if peak_payload is not None:
|
||||
peak_suffix = f" | peak={float(peak_payload.scalar_value):.3g}"
|
||||
self._sar3d_xz_plot.setTitle(f"3D SAR side view (X-Z){peak_suffix}")
|
||||
self._sar3d_xy_plot.setTitle("3D SAR top view (X-Y)")
|
||||
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)
|
||||
|
||||
@@ -20,6 +20,10 @@ 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,
|
||||
)
|
||||
|
||||
|
||||
def _format_geometry_row(position: int, x_m: float, y_m: float, z_m: float) -> str:
|
||||
@@ -706,6 +710,18 @@ def build_processing_group(owner) -> QGroupBox:
|
||||
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."
|
||||
)
|
||||
|
||||
sar3d_page = _build_processing_mode_page(
|
||||
owner._processing_mode_pages,
|
||||
[
|
||||
@@ -730,6 +746,7 @@ def build_processing_group(owner) -> QGroupBox:
|
||||
("Grid Nx", owner._sar3d_grid_nx),
|
||||
("Grid Ny", owner._sar3d_grid_ny),
|
||||
("Grid Nz", owner._sar3d_grid_nz),
|
||||
("Bottom view", owner._sar3d_bottom_view),
|
||||
owner._sar3d_use_compensation,
|
||||
owner._sar3d_normalize_pairs,
|
||||
],
|
||||
@@ -832,6 +849,7 @@ def build_processing_group(owner) -> QGroupBox:
|
||||
owner._sar3d_grid_nz.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||
owner._sar3d_use_compensation.toggled.connect(owner._on_processing_live_settings_changed)
|
||||
owner._sar3d_normalize_pairs.toggled.connect(owner._on_processing_live_settings_changed)
|
||||
owner._sar3d_bottom_view.currentTextChanged.connect(owner._on_processing_live_settings_changed)
|
||||
|
||||
owner._set_processing_mode_page(owner._processing_mode.currentText())
|
||||
return group
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
"""Shared mapping for the sar3d bottom-view projection choices.
|
||||
|
||||
The bottom plot of the sar3d mode projects the imaged volume along a
|
||||
horizontal direction rotated around the vertical Y axis: 0° = front view
|
||||
(X horizontal), 90° = side view (Z horizontal). One shared mapping keeps the
|
||||
combo box, the live-config build, the profile persistence and the plot labels
|
||||
consistent.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
SAR3D_BOTTOM_VIEW_CHOICES: tuple[tuple[str, float], ...] = (
|
||||
("front (0°)", 0.0),
|
||||
("30°", 30.0),
|
||||
("60°", 60.0),
|
||||
("side (90°)", 90.0),
|
||||
)
|
||||
|
||||
|
||||
def sar3d_bottom_view_deg_from_label(label: str) -> float:
|
||||
"""Map a bottom-view combo label to its projection angle in degrees."""
|
||||
for choice_label, degrees in SAR3D_BOTTOM_VIEW_CHOICES:
|
||||
if choice_label == label:
|
||||
return degrees
|
||||
return 0.0
|
||||
|
||||
|
||||
def sar3d_bottom_view_label_from_deg(degrees: float) -> str:
|
||||
"""Map a projection angle to the nearest bottom-view combo label."""
|
||||
return min(SAR3D_BOTTOM_VIEW_CHOICES, key=lambda choice: abs(choice[1] - float(degrees)))[0]
|
||||
@@ -578,6 +578,12 @@ def gui_profile_from_dict(payload: dict[str, Any]) -> GuiProfileModel:
|
||||
gui.processing.sar3d.normalize_pairs,
|
||||
"gui.processing.sar3d",
|
||||
),
|
||||
bottom_view_deg=_optional_float(
|
||||
sar3d_object,
|
||||
"bottom_view_deg",
|
||||
gui.processing.sar3d.bottom_view_deg,
|
||||
"gui.processing.sar3d",
|
||||
),
|
||||
),
|
||||
)
|
||||
if gui.processing.selected_mode not in {"pass_through", "bscan", "gpr", "legacy_gpr", "sar3d"}:
|
||||
@@ -799,6 +805,7 @@ def gui_profile_to_dict(model: GuiProfileModel) -> dict[str, Any]:
|
||||
"grid_nz": gui.processing.sar3d.grid_nz,
|
||||
"use_compensation": gui.processing.sar3d.use_compensation,
|
||||
"normalize_pairs": gui.processing.sar3d.normalize_pairs,
|
||||
"bottom_view_deg": gui.processing.sar3d.bottom_view_deg,
|
||||
},
|
||||
},
|
||||
"data_actions": {
|
||||
|
||||
@@ -155,6 +155,9 @@ class GuiSar3dStateModel:
|
||||
grid_nz: int = 160
|
||||
use_compensation: bool = True
|
||||
normalize_pairs: bool = True
|
||||
# Bottom-view projection angle around the vertical Y axis, degrees:
|
||||
# 0 = front view (X-Y), 90 = side view (Z-Y).
|
||||
bottom_view_deg: float = 0.0
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
|
||||
@@ -87,6 +87,9 @@ class ProcessingLiveConfig:
|
||||
sar3d_grid_nz: int = 160
|
||||
sar3d_use_compensation: bool = True
|
||||
sar3d_normalize_pairs: bool = True
|
||||
# Bottom-view projection angle around the vertical Y axis, degrees:
|
||||
# 0 = front view (X horizontal), 90 = side view (Z horizontal).
|
||||
sar3d_bottom_view_deg: float = 0.0
|
||||
# When true, the C++ data_processor ignores socket-supplied vlc updates
|
||||
# and keeps using `gpr_speed_m_s` from this file.
|
||||
ignore_socket_speed: bool = False
|
||||
@@ -163,6 +166,7 @@ class ProcessingLiveConfig:
|
||||
"sar3d_grid_nz": int(self.sar3d_grid_nz),
|
||||
"sar3d_use_compensation": bool(self.sar3d_use_compensation),
|
||||
"sar3d_normalize_pairs": bool(self.sar3d_normalize_pairs),
|
||||
"sar3d_bottom_view_deg": float(self.sar3d_bottom_view_deg),
|
||||
"ignore_socket_speed": bool(self.ignore_socket_speed),
|
||||
"reprocess_current_result": bool(self.reprocess_current_result),
|
||||
"history_command_seq": int(self.history_command_seq),
|
||||
|
||||
@@ -94,6 +94,7 @@ def _sar3d_live_config(profile_path: Path) -> ProcessingLiveConfig:
|
||||
live.sar3d_grid_nz = int(sar3d.get("grid_nz", live.sar3d_grid_nz))
|
||||
live.sar3d_use_compensation = bool(sar3d.get("use_compensation", live.sar3d_use_compensation))
|
||||
live.sar3d_normalize_pairs = bool(sar3d.get("normalize_pairs", live.sar3d_normalize_pairs))
|
||||
live.sar3d_bottom_view_deg = float(sar3d.get("bottom_view_deg", live.sar3d_bottom_view_deg))
|
||||
return live
|
||||
|
||||
|
||||
@@ -207,7 +208,7 @@ def main() -> int:
|
||||
ids_monotonic = False
|
||||
last_id = result.collection_id
|
||||
names = {p.processing_name: p for p in result.collection_payloads}
|
||||
if "sar3d_proj_xz" in names and "sar3d_proj_xy" in names:
|
||||
if "sar3d_proj_xz" in names and "sar3d_proj_bottom" in names:
|
||||
sar3d_results += 1
|
||||
peak_payload = names.get("sar3d_peak")
|
||||
if peak_payload is not None:
|
||||
|
||||
Reference in New Issue
Block a user