projector version

This commit is contained in:
awe
2026-09-15 19:06:05 +03:00
parent 62c0266a6b
commit b3339d101c
33 changed files with 2605 additions and 51 deletions
@@ -3,11 +3,13 @@
from python_app.gui.controllers.app_window_plot.bscan_plot_mixin import AppWindowBscanPlotMixin
from python_app.gui.controllers.app_window_plot.bscan_replay_mixin import AppWindowBscanReplayMixin
from python_app.gui.controllers.app_window_plot.gpr_plot_mixin import AppWindowGprPlotMixin
from python_app.gui.controllers.app_window_plot.sar3d_plot_mixin import AppWindowSar3dPlotMixin
from python_app.gui.controllers.app_window_plot.trace_plot_mixin import AppWindowTracePlotMixin
__all__ = [
"AppWindowBscanPlotMixin",
"AppWindowBscanReplayMixin",
"AppWindowGprPlotMixin",
"AppWindowSar3dPlotMixin",
"AppWindowTracePlotMixin",
]
@@ -0,0 +1,131 @@
"""3D SAR projection rendering helpers.
The sar3d processor publishes two normalized max-projections of the imaged
volume as collection-level ImageF32 payloads:
- ``sar3d_proj_xz`` — side view: X (m) horizontal, Z/range (m) vertical;
- ``sar3d_proj_xy`` — top view: X (m) horizontal, Y (m) vertical;
plus ``sar3d_peak`` (ScalarF32), the volume peak before normalization. Both
projections are drawn on their own PlotWidgets stacked in one plot-stack page
(side view on top), mirroring how the pass-through page stacks its two plots.
"""
from __future__ import annotations
from PyQt6.QtCore import QRectF
import numpy as np
import pyqtgraph as pg
from python_app.models.dataset_model import ResultCollection
class AppWindowSar3dPlotMixin:
"""Renders 3D SAR volume projections published by the sar3d processor."""
def _configure_sar3d_plot_axes(self) -> None:
"""Apply persistent axis labels and view settings to both projection plots."""
for plot, y_label in (
(self._sar3d_xz_plot, "Range Z"),
(self._sar3d_xy_plot, "Cross-range Y"),
):
plot_item = plot.getPlotItem()
plot_item.showAxis("left", show=True)
plot_item.showAxis("bottom", show=True)
plot_item.setClipToView(True)
plot.setLabel("bottom", "X", units="m")
plot.setLabel("left", y_label, units="m")
view_box = plot.getViewBox()
view_box.invertY(False)
view_box.enableAutoRange(x=False, y=False)
self._disable_gpr_plot_interaction(plot, view_box)
def _ensure_sar3d_plot_items(self) -> None:
"""Create persistent projection image items once and reuse them on redraw."""
if self._sar3d_xz_image_item is not None:
return
self._configure_sar3d_plot_axes()
if self._sar3d_lookup_table is None:
# Same palette as the GPR accumulator so both imagers read alike.
self._sar3d_lookup_table = self._build_lut(["#081c15", "#1b4332", "#ffd166", "#f94144"])
self._sar3d_xz_image_item = pg.ImageItem(axisOrder="row-major")
self._sar3d_xz_image_item.setZValue(0)
self._sar3d_xz_image_item.hide()
self._sar3d_xz_plot.addItem(self._sar3d_xz_image_item)
self._sar3d_xy_image_item = pg.ImageItem(axisOrder="row-major")
self._sar3d_xy_image_item.setZValue(0)
self._sar3d_xy_image_item.hide()
self._sar3d_xy_plot.addItem(self._sar3d_xy_image_item)
def _clear_sar3d_plot(self) -> None:
"""Clear both 3D SAR projection surfaces."""
if not hasattr(self, "_sar3d_xz_plot"):
return
self._configure_sar3d_plot_axes()
if self._sar3d_xz_image_item is not None:
self._sar3d_xz_image_item.hide()
if self._sar3d_xy_image_item is not None:
self._sar3d_xy_image_item.hide()
self._sar3d_xz_plot.setTitle("3D SAR side view (X-Z)")
self._sar3d_xy_plot.setTitle("3D SAR top view (X-Y)")
def _draw_sar3d_projection(self, plot: pg.PlotWidget, image_item: pg.ImageItem, payload) -> bool:
"""Draw one normalized projection payload onto its plot; return success."""
image = np.asarray(payload.image, dtype=np.float32)
x_axis = np.asarray(payload.image_x_axis, dtype=np.float32)
y_axis = np.asarray(payload.image_y_axis, dtype=np.float32)
if image.ndim != 2 or image.size == 0 or x_axis.size == 0 or y_axis.size == 0:
image_item.hide()
return False
x_min = float(x_axis[0])
x_max = float(x_axis[-1])
y_min = float(y_axis[0])
y_max = float(y_axis[-1])
rect = QRectF(x_min, y_min, max(x_max - x_min, 1e-6), max(y_max - y_min, 1e-6))
image_item.setImage(image, autoLevels=False)
image_item.setRect(rect)
image_item.setLookupTable(self._sar3d_lookup_table)
# Projections are normalized to [0, 1] by the processor.
image_item.setLevels((0.0, 1.0))
image_item.show()
plot.setXRange(x_min, x_max, padding=0.0)
plot.setYRange(y_min, y_max, padding=0.0)
return True
def _draw_sar3d_projections(self, collection: ResultCollection) -> bool:
"""Draw the latest collection-level 3D SAR side and top projections."""
xz_payload = self._collection_payload_by_name(collection, "sar3d_proj_xz", kind=3)
xy_payload = self._collection_payload_by_name(collection, "sar3d_proj_xy", kind=3)
if xz_payload is None and xy_payload is None:
self._clear_sar3d_plot()
return False
for plot in (self._sar3d_xz_plot, self._sar3d_xy_plot):
plot.setUpdatesEnabled(False)
try:
self._ensure_sar3d_plot_items()
xz_drawn = xz_payload is not None and self._draw_sar3d_projection(
self._sar3d_xz_plot, self._sar3d_xz_image_item, xz_payload
)
xy_drawn = xy_payload is not None and self._draw_sar3d_projection(
self._sar3d_xy_plot, self._sar3d_xy_image_item, xy_payload
)
if not xz_drawn and not xy_drawn:
self._clear_sar3d_plot()
return False
peak_payload = self._collection_payload_by_name(collection, "sar3d_peak", kind=2)
peak_suffix = ""
if peak_payload is not None:
peak_suffix = f" | peak={float(peak_payload.scalar_value):.3g}"
self._sar3d_xz_plot.setTitle(f"3D SAR side view (X-Z){peak_suffix}")
self._sar3d_xy_plot.setTitle("3D SAR top view (X-Y)")
finally:
for plot in (self._sar3d_xz_plot, self._sar3d_xy_plot):
plot.setUpdatesEnabled(True)
return True
@@ -78,7 +78,7 @@ class AppWindowTracePlotMixin:
def _on_trace_visibility_changed(self, *_args) -> None:
"""Redraw pass-through traces when magnitude/phase toggles changed."""
if self._processing_mode.currentText() in {"bscan", "gpr", "legacy_gpr"}:
if self._processing_mode.currentText() in {"bscan", "gpr", "legacy_gpr", "sar3d"}:
return
if self._result_history:
self._draw_results(self._result_history[-1])