Files
radar_system/python_app/orchestration/gpr_locator.py

117 lines
4.1 KiB
Python

"""Helpers for extracting GPR objects from result collections.
Locator TCP delivery now lives in the C++ data_processor. This module retains
only the inspection helpers that the GUI uses for plotting.
"""
from __future__ import annotations
import numpy as np
from python_app.models.dataset_model import ResultCollection, ResultPayload
def collection_payload_by_name(
collection: ResultCollection,
name: str,
kind: int | None = None,
) -> ResultPayload | None:
"""Return the 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
def collection_payloads_by_prefix(
collection: ResultCollection,
prefix: str,
kind: int | None = None,
) -> list[ResultPayload]:
"""Return collection payloads matching a processing-name prefix."""
payloads: list[ResultPayload] = []
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 collection_has_gpr_payloads(collection: ResultCollection) -> bool:
"""Return whether collection carries GPR-specific collection payloads."""
return any(
str(payload.processing_name).startswith("gpr_")
for payload in collection.collection_payloads
)
def gpr_object_rows(collection: ResultCollection) -> np.ndarray:
"""Return object rows as `[x_m, z_m, score]` from a GPR collection."""
points_payload = 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 = 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 apply_object_draw_limits(
rows: np.ndarray,
limits: tuple[int, int] | None,
) -> np.ndarray:
"""Apply the object count/top-M drawing rules to already-filtered `[x, z, score]` rows.
`limits` is `(max_detected_objects, draw_top_objects)`, or `None` to disable
(legacy GPR). When more than ``max_detected_objects`` survive, ALL are hidden
(the scene is too cluttered to be meaningful); otherwise the top ``draw_top_objects``
rows are kept (rows arrive already sorted by score descending).
"""
if limits is None or rows.size == 0:
return rows
max_detected_objects, draw_top_objects = limits
if rows.shape[0] > int(max_detected_objects):
return np.zeros((0, rows.shape[1]), dtype=rows.dtype)
return rows[: max(0, int(draw_top_objects))]
def filter_object_rows(
rows: np.ndarray,
*,
min_score: float,
x_bounds: tuple[float, float],
z_bounds: tuple[float, float],
draw_limits: tuple[int, int] | None,
) -> np.ndarray:
"""Filter `[x_m, z_m, score]` object rows for display/broadcast.
Drops non-finite rows, rows below ``min_score`` (the caller passes the mode's own
threshold — a normalized float for coherent GPR, a pair count for legacy GPR; the
comparison is identical either way), and rows outside the visible X/Z window, then
applies ``draw_limits``. Mode-agnostic: all semantics enter through the parameters.
"""
if rows.size == 0:
return rows
x_min, x_max = x_bounds
z_min, z_max = z_bounds
visible_mask = (
np.all(np.isfinite(rows[:, :3]), axis=1)
& (rows[:, 2] >= min_score)
& (rows[:, 0] >= x_min)
& (rows[:, 0] <= x_max)
& (rows[:, 1] >= z_min)
& (rows[:, 1] <= z_max)
)
return apply_object_draw_limits(rows[visible_mask], draw_limits)