Update GPR and SAR3D processing
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user