added GPR
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user