added GPR

This commit is contained in:
Ayzen
2026-03-19 19:29:08 +03:00
parent bdefe3f581
commit 9581730e41
39 changed files with 3830 additions and 201 deletions
@@ -36,6 +36,8 @@ class AppWindowPlotMixin:
"""Draw collection based on currently selected processing mode."""
if self._processing_mode.currentText() == "bscan":
return self._draw_bscan_heatmap(collection)
if self._processing_mode.currentText() == "gpr":
return self._draw_gpr_map(collection)
return self._draw_trace_lines(collection)
def _show_magnitude_curves(self) -> bool:
@@ -46,9 +48,24 @@ class AppWindowPlotMixin:
"""Return whether phase curves should be rendered."""
return self._show_phase_checkbox.isChecked()
def _pass_through_fixed_y_range(self) -> tuple[bool, float, float]:
"""Return normalized magnitude Y-range override for pass-through mode."""
y_min = float(self._pass_through_y_min_db.value())
y_max = float(self._pass_through_y_max_db.value())
return bool(self._pass_through_fixed_y_enabled.isChecked()), min(y_min, y_max), max(y_min, y_max)
def _configure_pass_through_magnitude_axis(self, plot: pg.PlotWidget) -> None:
"""Apply pass-through magnitude-axis autorange or fixed Y window."""
fixed_y_enabled, y_min, y_max = self._pass_through_fixed_y_range()
view_box = plot.getViewBox()
view_box.invertY(False)
view_box.enableAutoRange(x=True, y=not fixed_y_enabled)
if fixed_y_enabled:
plot.setYRange(y_min, y_max, padding=0.0)
def _on_trace_visibility_changed(self, *_args) -> None:
"""Redraw pass-through traces when magnitude/phase toggles changed."""
if self._processing_mode.currentText() == "bscan":
if self._processing_mode.currentText() in {"bscan", "gpr"}:
return
if self._result_history:
self._draw_results(self._result_history[-1])
@@ -98,8 +115,7 @@ class AppWindowPlotMixin:
if show_magnitude:
mag_item = magnitude_plot.getPlotItem()
magnitude_plot.getViewBox().invertY(False)
magnitude_plot.getViewBox().enableAutoRange(x=True, y=True)
self._configure_pass_through_magnitude_axis(magnitude_plot)
mag_item.showAxis("left", show=True)
mag_item.showAxis("bottom", show=not show_phase)
magnitude_plot.setLabel("left", "Magnitude", units="dB")
@@ -219,6 +235,8 @@ class AppWindowPlotMixin:
phase_plot.setXRange(x_min, x_max, padding=0.02)
if show_phase:
phase_plot.setYRange(-180.0, 180.0, padding=0.02)
if show_magnitude:
self._configure_pass_through_magnitude_axis(magnitude_plot)
return has_data
@staticmethod
@@ -481,8 +499,290 @@ class AppWindowPlotMixin:
"""Hide right axis and clear phase overlay when phase is not rendered."""
self._clear_phase_overlay()
def _clear_gpr_plot(self) -> None:
"""Clear latest GPR plot surface."""
if not hasattr(self, "_gpr_plot"):
return
self._clear_gpr_point_labels()
self._clear_gpr_region_labels()
self._clear_gpr_region_masks()
if self._gpr_image_item is not None:
self._gpr_image_item.hide()
if self._gpr_tx_item is not None:
self._gpr_tx_item.setData(x=[], y=[])
self._gpr_tx_item.hide()
if self._gpr_rx_item is not None:
self._gpr_rx_item.setData(x=[], y=[])
self._gpr_rx_item.hide()
if self._gpr_points_item is not None:
self._gpr_points_item.setData(x=[], y=[])
self._gpr_points_item.hide()
if self._gpr_region_centers_item is not None:
self._gpr_region_centers_item.setData(x=[], y=[])
self._gpr_region_centers_item.hide()
self._gpr_plot.setTitle(f"GPR {self._gpr_config_mode.currentText()}")
def _ensure_gpr_plot_items(self) -> None:
"""Create persistent GPR plot items once and reuse them on redraw."""
if self._gpr_image_item is not None:
return
plot = self._gpr_plot
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", "Depth", units="m")
view_box = plot.getViewBox()
view_box.invertY(True)
view_box.enableAutoRange(x=False, y=False)
if self._gpr_lookup_table is None:
self._gpr_lookup_table = self._build_lut(["#081c15", "#1b4332", "#ffd166", "#f94144"])
self._gpr_image_item = pg.ImageItem(axisOrder="row-major")
self._gpr_image_item.setZValue(0)
self._gpr_image_item.hide()
plot.addItem(self._gpr_image_item)
self._gpr_tx_item = pg.ScatterPlotItem()
self._gpr_tx_item.setZValue(20)
self._gpr_tx_item.hide()
plot.addItem(self._gpr_tx_item)
self._gpr_rx_item = pg.ScatterPlotItem()
self._gpr_rx_item.setZValue(20)
self._gpr_rx_item.hide()
plot.addItem(self._gpr_rx_item)
self._gpr_points_item = pg.ScatterPlotItem()
self._gpr_points_item.setZValue(30)
self._gpr_points_item.hide()
plot.addItem(self._gpr_points_item)
self._gpr_region_centers_item = pg.ScatterPlotItem()
self._gpr_region_centers_item.setZValue(30)
self._gpr_region_centers_item.hide()
plot.addItem(self._gpr_region_centers_item)
def _clear_gpr_point_labels(self) -> None:
"""Remove dynamic point-score labels from GPR plot."""
for item in self._gpr_point_labels:
try:
self._gpr_plot.removeItem(item)
except Exception: # noqa: BLE001
pass
self._gpr_point_labels.clear()
def _clear_gpr_region_labels(self) -> None:
"""Remove dynamic region labels from GPR plot."""
for item in self._gpr_region_center_labels:
try:
self._gpr_plot.removeItem(item)
except Exception: # noqa: BLE001
pass
self._gpr_region_center_labels.clear()
def _clear_gpr_region_masks(self) -> None:
"""Remove dynamic region contour carriers from GPR plot."""
for item in self._gpr_region_mask_items:
try:
self._gpr_plot.removeItem(item)
except Exception: # noqa: BLE001
pass
self._gpr_region_mask_items.clear()
self._gpr_region_contours.clear()
@staticmethod
def _collection_payload_by_name(collection: ResultCollection, name: str, kind: int | None = None):
"""Return first collection payload matching name and optional kind."""
for payload in collection.collection_payloads:
if payload.processing_name != name:
continue
if kind is not None and int(payload.kind) != int(kind):
continue
return payload
return None
@staticmethod
def _collection_payloads_by_prefix(collection: ResultCollection, prefix: str, kind: int | None = None):
"""Return collection payloads matching processing-name prefix."""
payloads = []
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 _selected_gpr_geometry(self) -> tuple[np.ndarray, np.ndarray]:
"""Resolve selected Tx/Rx geometry arrays for current GPR selection."""
requested_inputs = tuple(self._parse_csv_int_list(self._gpr_input_positions_input.text()))
requested_outputs = tuple(self._parse_csv_int_list(self._gpr_output_positions_input.text()))
signature = (
self._gpr_tx_geometry_input.toPlainText(),
self._gpr_rx_geometry_input.toPlainText(),
requested_inputs,
requested_outputs,
)
if signature == self._gpr_geometry_signature and self._gpr_selected_geometry is not None:
return self._gpr_selected_geometry
tx_entries = self._parse_gpr_tx_geometry_text(signature[0])
rx_entries = self._parse_gpr_rx_geometry_text(signature[1])
requested_input_set = set(requested_inputs)
requested_output_set = set(requested_outputs)
rx_entries = sorted(rx_entries, key=lambda entry: int(entry.input_pos))
tx_entries = sorted(tx_entries, key=lambda entry: int(entry.output_pos))
if requested_input_set:
rx_entries = [entry for entry in rx_entries if int(entry.input_pos) in requested_input_set]
if requested_output_set:
tx_entries = [entry for entry in tx_entries if int(entry.output_pos) in requested_output_set]
x_tx = np.asarray([float(entry.x_m) for entry in tx_entries], dtype=np.float32)
x_rx = np.asarray([float(entry.x_m) for entry in rx_entries], dtype=np.float32)
self._gpr_geometry_signature = signature
self._gpr_selected_geometry = (x_tx, x_rx)
return self._gpr_selected_geometry
def _draw_gpr_map(self, collection: ResultCollection) -> bool:
"""Draw latest collection-level GPR accumulator and annotations."""
accumulator_payload = self._collection_payload_by_name(collection, "gpr_accumulator", kind=3)
if accumulator_payload is None:
self._clear_gpr_plot()
return False
image = np.asarray(accumulator_payload.image, dtype=np.float32)
x_axis = np.asarray(accumulator_payload.image_x_axis, dtype=np.float32)
y_axis = np.asarray(accumulator_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:
self._clear_gpr_plot()
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))
plot = self._gpr_plot
plot.setUpdatesEnabled(False)
try:
self._ensure_gpr_plot_items()
self._clear_gpr_point_labels()
self._clear_gpr_region_labels()
self._clear_gpr_region_masks()
self._gpr_image_item.setImage(image, autoLevels=False)
self._gpr_image_item.setRect(rect)
self._gpr_image_item.setLookupTable(self._gpr_lookup_table)
self._gpr_image_item.setLevels((float(np.min(image)), float(np.max(image) + 1e-6)))
self._gpr_image_item.show()
plot.setXRange(x_min, x_max, padding=0.02)
plot.setYRange(y_min, y_max, padding=0.02)
x_tx, x_rx = self._selected_gpr_geometry()
if x_tx.size > 0:
self._gpr_tx_item.setData(
x=x_tx,
y=np.zeros_like(x_tx),
symbol="t",
size=13,
brush=pg.mkBrush("#ff595e"),
pen=pg.mkPen("#ffca3a", width=1.0),
)
self._gpr_tx_item.show()
else:
self._gpr_tx_item.setData(x=[], y=[])
self._gpr_tx_item.hide()
if x_rx.size > 0:
self._gpr_rx_item.setData(
x=x_rx,
y=np.zeros_like(x_rx),
symbol="t1",
size=13,
brush=pg.mkBrush("#4cc9f0"),
pen=pg.mkPen("#e0fbfc", width=1.0),
)
self._gpr_rx_item.show()
else:
self._gpr_rx_item.setData(x=[], y=[])
self._gpr_rx_item.hide()
points_payload = self._collection_payload_by_name(collection, "gpr_points", kind=4)
if points_payload is not None and np.asarray(points_payload.table).size > 0:
points = np.asarray(points_payload.table, dtype=np.float32)
self._gpr_points_item.setData(
x=points[:, 0],
y=points[:, 1],
symbol="d",
size=11,
brush=pg.mkBrush("#ffffff"),
pen=pg.mkPen("#111111", width=1.1),
)
self._gpr_points_item.show()
for x_value, y_value, score in points:
label = pg.TextItem(text=f"{float(score):.0f}", color="#ffffff", anchor=(0.0, 1.0))
label.setZValue(40)
label.setPos(float(x_value), float(y_value))
plot.addItem(label)
self._gpr_point_labels.append(label)
else:
self._gpr_points_item.setData(x=[], y=[])
self._gpr_points_item.hide()
region_centers_payload = self._collection_payload_by_name(collection, "gpr_region_centers", kind=4)
if region_centers_payload is not None and np.asarray(region_centers_payload.table).size > 0:
centers = np.asarray(region_centers_payload.table, dtype=np.float32)
self._gpr_region_centers_item.setData(
x=centers[:, 0],
y=centers[:, 1],
symbol="o",
size=10,
brush=pg.mkBrush("#80ed99"),
pen=pg.mkPen("#081c15", width=1.1),
)
self._gpr_region_centers_item.show()
for row in centers:
label = pg.TextItem(text=f"{float(row[2]):.0f}", color="#d8f3dc", anchor=(0.0, 1.0))
label.setZValue(40)
label.setPos(float(row[0]), float(row[1]))
plot.addItem(label)
self._gpr_region_center_labels.append(label)
else:
self._gpr_region_centers_item.setData(x=[], y=[])
self._gpr_region_centers_item.hide()
for payload in self._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)
plot.addItem(mask_image)
contour = pg.IsocurveItem(data=mask, level=0.5, pen=pg.mkPen("#4cc9f0", width=1.3))
contour.setParentItem(mask_image)
self._gpr_region_mask_items.append(mask_image)
self._gpr_region_contours.append(contour)
plot.setTitle(f"GPR {self._gpr_config_mode.currentText()}")
finally:
plot.setUpdatesEnabled(True)
return True
def _result_collection_has_trace(self, collection: ResultCollection) -> bool:
"""Return `True` when collection contains at least one trace payload."""
if collection.collection_payloads:
return True
for block in collection.blocks:
for payload in block.payloads:
if payload.kind == 1 and payload.trace.size > 0:
@@ -503,8 +803,7 @@ class AppWindowPlotMixin:
return
if show_magnitude:
magnitude_plot.getViewBox().invertY(False)
magnitude_plot.getViewBox().enableAutoRange(x=True, y=True)
self._configure_pass_through_magnitude_axis(magnitude_plot)
magnitude_plot.getPlotItem().showAxis("bottom", show=not show_phase)
magnitude_plot.setLabel("left", "Magnitude", units="dB")
magnitude_plot.setTitle(title)
@@ -548,5 +847,6 @@ class AppWindowPlotMixin:
x_max = float(np.max(trace.frequency_hz))
if show_magnitude:
magnitude_plot.setXRange(x_min, x_max, padding=0.02)
self._configure_pass_through_magnitude_axis(magnitude_plot)
if show_phase:
phase_plot.setXRange(x_min, x_max, padding=0.02)