new GPR parameters and some UI fixes
This commit is contained in:
@@ -359,6 +359,249 @@ class AppWindowPlotMixin:
|
||||
self._sync_bscan_history_from_results()
|
||||
return self._draw_bscan_heatmap_from_history()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def _configure_bscan_plot_axes(self) -> None:
|
||||
"""Apply persistent B-scan plot axis labels and base view settings."""
|
||||
plot = self._bscan_plot
|
||||
plot_item = plot.getPlotItem()
|
||||
plot_item.showAxis("left", show=True)
|
||||
plot_item.showAxis("bottom", show=True)
|
||||
plot.setLabel("bottom", "Sweep #")
|
||||
plot.setLabel("left", "Range", units="m")
|
||||
view_box = plot.getViewBox()
|
||||
view_box.invertY(False)
|
||||
view_box.enableAutoRange(x=False, y=False)
|
||||
|
||||
def _draw_bscan_heatmap_from_history(self) -> bool:
|
||||
"""Render B-scan heatmap from currently cached history arrays."""
|
||||
display_key = self._pick_bscan_display_key()
|
||||
@@ -391,13 +634,7 @@ class AppWindowPlotMixin:
|
||||
image_item.setLevels(self._bscan_levels(sweeps, axis_mode))
|
||||
|
||||
self._bscan_plot.clear()
|
||||
view_box = self._bscan_plot.getViewBox()
|
||||
view_box.invertY(True)
|
||||
view_box.enableAutoRange(x=False, y=False)
|
||||
self._bscan_plot.getPlotItem().showAxis("left", show=True)
|
||||
self._bscan_plot.getPlotItem().showAxis("bottom", show=True)
|
||||
self._bscan_plot.setLabel("bottom", "Sweep #")
|
||||
self._bscan_plot.setLabel("left", "Depth", units="m")
|
||||
self._configure_bscan_plot_axes()
|
||||
self._bscan_plot.addItem(image_item)
|
||||
self._bscan_plot.setXRange(x_min, x_max, padding=0.02)
|
||||
self._bscan_plot.setYRange(depth_min, depth_max, padding=0.02)
|
||||
@@ -538,6 +775,7 @@ class AppWindowPlotMixin:
|
||||
"""Clear latest GPR plot surface."""
|
||||
if not hasattr(self, "_gpr_plot"):
|
||||
return
|
||||
self._configure_gpr_plot_axes()
|
||||
self._clear_gpr_point_labels()
|
||||
self._clear_gpr_region_labels()
|
||||
self._clear_gpr_region_masks()
|
||||
@@ -557,23 +795,27 @@ class AppWindowPlotMixin:
|
||||
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
|
||||
|
||||
def _configure_gpr_plot_axes(self) -> None:
|
||||
"""Apply persistent GPR plot axis labels and base view settings."""
|
||||
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")
|
||||
|
||||
plot.setLabel("left", "Range", units="m")
|
||||
view_box = plot.getViewBox()
|
||||
view_box.invertY(False)
|
||||
view_box.enableAutoRange(x=False, y=False)
|
||||
|
||||
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
|
||||
self._configure_gpr_plot_axes()
|
||||
|
||||
if self._gpr_lookup_table is None:
|
||||
self._gpr_lookup_table = self._build_lut(["#081c15", "#1b4332", "#ffd166", "#f94144"])
|
||||
|
||||
@@ -694,6 +936,12 @@ class AppWindowPlotMixin:
|
||||
return self._gpr_selected_geometry
|
||||
|
||||
def _draw_gpr_map(self, collection: ResultCollection) -> bool:
|
||||
"""Draw latest collection-level GPR plot according to current render mode."""
|
||||
if self._gpr_render_mode.currentText() == "objects_only":
|
||||
return self._draw_gpr_objects_only(collection)
|
||||
return self._draw_gpr_heatmap(collection)
|
||||
|
||||
def _draw_gpr_heatmap(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:
|
||||
@@ -729,36 +977,9 @@ class AppWindowPlotMixin:
|
||||
self._gpr_image_item.show()
|
||||
|
||||
plot.setXRange(x_min, x_max, padding=0.02)
|
||||
plot.setYRange(min(0.0, y_min), y_max, padding=0.02)
|
||||
plot.setYRange(self._gpr_display_y_min(y_min, y_max), 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()
|
||||
self._draw_gpr_geometry_markers()
|
||||
|
||||
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:
|
||||
@@ -824,6 +1045,242 @@ class AppWindowPlotMixin:
|
||||
plot.setUpdatesEnabled(True)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _normalized_display_range(start: float, stop: float, *, minimum_span: float = 0.1) -> tuple[float, float]:
|
||||
"""Return ordered display bounds with a non-zero span."""
|
||||
lower = min(float(start), float(stop))
|
||||
upper = max(float(start), float(stop))
|
||||
if upper - lower >= minimum_span:
|
||||
return lower, upper
|
||||
center = 0.5 * (lower + upper)
|
||||
half_span = 0.5 * minimum_span
|
||||
return center - half_span, center + half_span
|
||||
|
||||
def _gpr_visible_object_bounds(self) -> tuple[float, float, float, float]:
|
||||
"""Return normalized object-only visible X/Z bounds from GUI controls."""
|
||||
x_min, x_max = self._normalized_display_range(
|
||||
float(self._gpr_visible_x_min_m.value()),
|
||||
float(self._gpr_visible_x_max_m.value()),
|
||||
minimum_span=0.1,
|
||||
)
|
||||
z_min, z_max = self._normalized_display_range(
|
||||
float(self._gpr_visible_z_min_m.value()),
|
||||
float(self._gpr_visible_z_max_m.value()),
|
||||
minimum_span=0.1,
|
||||
)
|
||||
return x_min, x_max, z_min, z_max
|
||||
|
||||
@staticmethod
|
||||
def _gpr_display_y_min(z_min: float, z_max: float) -> float:
|
||||
"""Return lower GPR display bound with a small negative margin for antenna markers."""
|
||||
lower = min(0.0, float(z_min))
|
||||
span = max(float(z_max) - float(z_min), 1e-6)
|
||||
marker_margin = max(span * 0.03, 0.06)
|
||||
return lower - marker_margin
|
||||
|
||||
def _draw_gpr_geometry_markers(self) -> None:
|
||||
"""Render selected Tx/Rx geometry markers on current GPR plot."""
|
||||
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()
|
||||
|
||||
@staticmethod
|
||||
def _format_gpr_object_label(x_m: float, z_m: float, pair_count: float) -> str:
|
||||
"""Format object-only annotation text with pair count and coordinates."""
|
||||
return f"{int(round(pair_count))} | x={x_m:.1f} | z={z_m:.1f}"
|
||||
|
||||
@staticmethod
|
||||
def _expanded_scene_rect(rect: QRectF, *, padding_px: float = 4.0) -> QRectF:
|
||||
"""Return scene rect padded to keep labels visually separated."""
|
||||
return rect.adjusted(-padding_px, -padding_px, padding_px, padding_px)
|
||||
|
||||
@staticmethod
|
||||
def _scene_rect_intersects_any(rect: QRectF, occupied_rects: list[QRectF]) -> bool:
|
||||
"""Return whether candidate label rect intersects any already placed label."""
|
||||
return any(rect.intersects(occupied_rect) for occupied_rect in occupied_rects)
|
||||
|
||||
@staticmethod
|
||||
def _gpr_object_label_candidates(
|
||||
x_m: float,
|
||||
z_m: float,
|
||||
*,
|
||||
x_span: float,
|
||||
z_span: float,
|
||||
) -> list[tuple[float, float, tuple[float, float]]]:
|
||||
"""Return candidate label placements around one object."""
|
||||
x_offset = max(x_span * 0.015, 0.02)
|
||||
z_offset = max(z_span * 0.02, 0.02)
|
||||
return [
|
||||
(x_m + x_offset, z_m - z_offset, (0.0, 1.0)),
|
||||
(x_m + x_offset, z_m + z_offset, (0.0, 0.0)),
|
||||
(x_m - x_offset, z_m - z_offset, (1.0, 1.0)),
|
||||
(x_m - x_offset, z_m + z_offset, (1.0, 0.0)),
|
||||
(x_m, z_m - (z_offset * 1.35), (0.5, 1.0)),
|
||||
(x_m, z_m + (z_offset * 1.35), (0.5, 0.0)),
|
||||
(x_m + (x_offset * 2.2), z_m - (z_offset * 1.5), (0.0, 1.0)),
|
||||
(x_m + (x_offset * 2.2), z_m + (z_offset * 1.5), (0.0, 0.0)),
|
||||
(x_m - (x_offset * 2.2), z_m - (z_offset * 1.5), (1.0, 1.0)),
|
||||
(x_m - (x_offset * 2.2), z_m + (z_offset * 1.5), (1.0, 0.0)),
|
||||
]
|
||||
|
||||
def _place_gpr_object_label(
|
||||
self,
|
||||
*,
|
||||
label: pg.TextItem,
|
||||
x_m: float,
|
||||
z_m: float,
|
||||
x_span: float,
|
||||
z_span: float,
|
||||
occupied_scene_rects: list[QRectF],
|
||||
) -> None:
|
||||
"""Place one object label using the first non-overlapping candidate position."""
|
||||
last_rect: QRectF | None = None
|
||||
for label_x, label_z, anchor in self._gpr_object_label_candidates(
|
||||
x_m,
|
||||
z_m,
|
||||
x_span=x_span,
|
||||
z_span=z_span,
|
||||
):
|
||||
label.setAnchor(anchor)
|
||||
label.setPos(label_x, label_z)
|
||||
candidate_rect = self._expanded_scene_rect(label.sceneBoundingRect())
|
||||
last_rect = candidate_rect
|
||||
if not self._scene_rect_intersects_any(candidate_rect, occupied_scene_rects):
|
||||
occupied_scene_rects.append(candidate_rect)
|
||||
return
|
||||
|
||||
if last_rect is not None:
|
||||
occupied_scene_rects.append(last_rect)
|
||||
|
||||
def _gpr_object_rows(self, collection: ResultCollection) -> np.ndarray:
|
||||
"""Return object rows as `[x_m, z_m, pair_count]` from current GPR result payload."""
|
||||
points_payload = self._collection_payload_by_name(collection, "gpr_points", kind=4)
|
||||
if points_payload is not None:
|
||||
points = np.asarray(points_payload.table, dtype=np.float32)
|
||||
if points.ndim == 2 and points.shape[1] >= 3:
|
||||
return points[:, :3]
|
||||
|
||||
centers_payload = self._collection_payload_by_name(collection, "gpr_region_centers", kind=4)
|
||||
if centers_payload is not None:
|
||||
centers = np.asarray(centers_payload.table, dtype=np.float32)
|
||||
if centers.ndim == 2 and centers.shape[1] >= 3:
|
||||
return centers[:, :3]
|
||||
|
||||
return np.zeros((0, 3), dtype=np.float32)
|
||||
|
||||
def _filtered_gpr_object_rows(self, collection: ResultCollection) -> np.ndarray:
|
||||
"""Return object rows filtered by minimum pair count and visible X/Z bounds."""
|
||||
rows = self._gpr_object_rows(collection)
|
||||
if rows.size == 0:
|
||||
return rows
|
||||
|
||||
x_min, x_max, z_min, z_max = self._gpr_visible_object_bounds()
|
||||
min_pair_count = float(self._gpr_min_visible_pair_count.value())
|
||||
finite_mask = np.all(np.isfinite(rows[:, :3]), axis=1)
|
||||
visible_mask = (
|
||||
finite_mask
|
||||
& (rows[:, 2] >= min_pair_count)
|
||||
& (rows[:, 0] >= x_min)
|
||||
& (rows[:, 0] <= x_max)
|
||||
& (rows[:, 1] >= z_min)
|
||||
& (rows[:, 1] <= z_max)
|
||||
)
|
||||
return rows[visible_mask]
|
||||
|
||||
def _draw_gpr_objects_only(self, collection: ResultCollection) -> bool:
|
||||
"""Draw only detected GPR objects inside configured X/Z bounds."""
|
||||
accumulator_payload = self._collection_payload_by_name(collection, "gpr_accumulator", kind=3)
|
||||
object_rows = self._filtered_gpr_object_rows(collection)
|
||||
if accumulator_payload is None and object_rows.size == 0:
|
||||
self._clear_gpr_plot()
|
||||
return False
|
||||
|
||||
x_min, x_max, z_min, z_max = self._gpr_visible_object_bounds()
|
||||
plot = self._gpr_plot
|
||||
plot.setUpdatesEnabled(False)
|
||||
try:
|
||||
self._ensure_gpr_plot_items()
|
||||
plot.getViewBox().invertY(False)
|
||||
self._clear_gpr_point_labels()
|
||||
self._clear_gpr_region_labels()
|
||||
self._clear_gpr_region_masks()
|
||||
|
||||
self._gpr_image_item.hide()
|
||||
self._draw_gpr_geometry_markers()
|
||||
self._gpr_region_centers_item.setData(x=[], y=[])
|
||||
self._gpr_region_centers_item.hide()
|
||||
|
||||
if object_rows.size > 0:
|
||||
self._gpr_points_item.setData(
|
||||
x=object_rows[:, 0],
|
||||
y=object_rows[:, 1],
|
||||
symbol="o",
|
||||
size=18,
|
||||
brush=pg.mkBrush("#ff4d4f"),
|
||||
pen=pg.mkPen("#ff4d4f", width=1.6),
|
||||
)
|
||||
self._gpr_points_item.show()
|
||||
|
||||
occupied_scene_rects: list[QRectF] = []
|
||||
x_span = x_max - x_min
|
||||
z_span = z_max - z_min
|
||||
for x_value, z_value, pair_count in object_rows:
|
||||
label = pg.TextItem(
|
||||
text=self._format_gpr_object_label(
|
||||
float(x_value),
|
||||
float(z_value),
|
||||
float(pair_count),
|
||||
),
|
||||
color="#ffd6d9",
|
||||
anchor=(0.0, 1.0),
|
||||
)
|
||||
label.setZValue(40)
|
||||
plot.addItem(label)
|
||||
self._place_gpr_object_label(
|
||||
label=label,
|
||||
x_m=float(x_value),
|
||||
z_m=float(z_value),
|
||||
x_span=x_span,
|
||||
z_span=z_span,
|
||||
occupied_scene_rects=occupied_scene_rects,
|
||||
)
|
||||
self._gpr_point_labels.append(label)
|
||||
else:
|
||||
self._gpr_points_item.setData(x=[], y=[])
|
||||
self._gpr_points_item.hide()
|
||||
|
||||
plot.setXRange(x_min, x_max, padding=0.0)
|
||||
plot.setYRange(self._gpr_display_y_min(z_min, z_max), z_max, padding=0.0)
|
||||
plot.setTitle(f"GPR {self._gpr_config_mode.currentText()} Objects Only")
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user