changh95's picture
tt-model push diffusion-planner-p150 (container)
4d9b003 verified
Raw History Blame Contribute Delete
11.4 kB
# SPDX-License-Identifier: Apache-2.0
"""C15 NMS family of the Autoware LiDAR detectors: circle NMS, rotated IoU-BEV NMS with perception_utils semantics,
and the area-based class remapper.
- :func:`circle_nms` is ``circleNMS`` of ``autoware_lidar_centerpoint/lib/postprocess/circle_nms_kernel.cu:38-142``:
boxes in descending score order; a kept box suppresses every *later* box whose squared BEV centre distance is
strictly below ``threshold^2`` (float32: ``powf(dx, 2) + powf(dy, 2) < powf(thr, 2)``); greedy, class-agnostic.
A threshold <= 0 means 1.5 m in Autoware's config (``centerpoint_config.hpp:83-85,139``): the caller applies that.
- :func:`iou_bev_nms` is ``perception_utils::IouBevNms::apply`` (``perception_utils/src/iou_bev_nms.cpp:132-171``,
universe main 9ceaccf). Object ``t`` is compared with **every** earlier object ``s < t``, kept or suppressed
(S:centerpoint:291-295); pairs with different labels where one is PEDESTRIAN are skipped; pairs whose centre
distance squared exceeds ``search_distance_2d^2`` are skipped; the loop over ``s`` breaks at the first
``iou > threshold``; ``t`` is kept iff the largest IoU seen is ``<= threshold``. ``sort=False`` (the CenterPoint
node's call) keeps the input order. IoU: the BEV rectangles of ``autoware_utils::toPolygon2d`` intersected
exactly (Sutherland-Hodgman on two convex quadrilaterals, float64), with the area guards 1e-6 (each box and the
intersection) and 0.01 (union).
- :class:`ClassRemapper` is ``perception_utils::DetectionClassRemapper::mapClasses``
(``perception_utils/src/detection_class_remapper.cpp:54-70``): with ``bev_area = float(length * width)``, the first
destination label (in label order) whose matrix entry allows it and whose ``[min, max]`` contains the area wins.
numpy only; no side effects on import.
"""
from __future__ import annotations
import math
from dataclasses import dataclass
from typing import Any, List, Mapping, Optional, Sequence
import numpy as np
__all__ = [
"PEDESTRIAN_LABEL",
"circle_nms",
"bev_box_corners",
"polygon_area",
"clip_convex",
"iou_bev",
"iou_bev_nms",
"ClassRemapper",
]
PEDESTRIAN_LABEL = 7 # autoware_perception_msgs/ObjectClassification PEDESTRIAN
_MIN_AREA = 1.0e-6 # iou_bev_nms.cpp compute2dIoU kMinArea
_MIN_UNION_AREA = 0.01 # kMinUnionArea
def circle_nms(x: Any, y: Any, dist_threshold: float) -> np.ndarray:
"""Indices (ascending) of the boxes kept by Autoware's circle NMS. The input must already be in descending
score order (``thrust::sort``); see the module docstring."""
xs = np.asarray(x, dtype=np.float32)
ys = np.asarray(y, dtype=np.float32)
n = len(xs)
thr2 = np.float32(dist_threshold) * np.float32(dist_threshold)
removed = np.zeros(n, dtype=bool)
keep: List[int] = []
for i in range(n):
if removed[i]:
continue
keep.append(i)
dx = xs[i + 1:] - xs[i]
dy = ys[i + 1:] - ys[i]
removed[i + 1:] |= (dx * dx + dy * dy) < thr2
return np.asarray(keep, dtype=np.int64)
def bev_box_corners(x: float, y: float, length: float, width: float, yaw: float) -> np.ndarray:
"""(4, 2) float64 corners in ``toPolygon2d`` order: (+l/2, +w/2), (-l/2, +w/2), (-l/2, -w/2), (+l/2, -w/2) in
the object frame, rotated by ``yaw`` about the centre (counter-clockwise for positive sizes)."""
c, s = math.cos(yaw), math.sin(yaw)
hl, hw = length / 2.0, width / 2.0
local = ((hl, hw), (-hl, hw), (-hl, -hw), (hl, -hw))
return np.array([(x + c * px - s * py, y + s * px + c * py) for px, py in local], dtype=np.float64)
def polygon_area(poly: Any) -> float:
"""Absolute shoelace area of a polygon (k, 2)."""
p = np.asarray(poly, dtype=np.float64)
if len(p) < 3:
return 0.0
x, y = p[:, 0], p[:, 1]
return abs(float(np.dot(x, np.roll(y, -1)) - np.dot(np.roll(x, -1), y))) / 2.0
def _signed_area(p: np.ndarray) -> float:
x, y = p[:, 0], p[:, 1]
return float(np.dot(x, np.roll(y, -1)) - np.dot(np.roll(x, -1), y)) / 2.0
def clip_convex(subject: Any, clipper: Any) -> np.ndarray:
"""Sutherland-Hodgman: ``subject`` polygon clipped by the convex polygon ``clipper`` (either orientation) ->
(k, 2) float64 intersection polygon (k may be 0)."""
clip = np.asarray(clipper, dtype=np.float64)
if _signed_area(clip) < 0:
clip = clip[::-1]
out = [tuple(p) for p in np.asarray(subject, dtype=np.float64)]
for i in range(len(clip)):
if not out:
break
ax, ay = clip[i]
bx, by = clip[(i + 1) % len(clip)]
inp, out = out, []
def inside(p):
return (bx - ax) * (p[1] - ay) - (by - ay) * (p[0] - ax) >= 0.0
def cross_point(p, q):
x1, y1, x2, y2 = p[0], p[1], q[0], q[1]
den = (x1 - x2) * (ay - by) - (y1 - y2) * (ax - bx)
if den == 0.0:
return q
t = ((x1 - ax) * (ay - by) - (y1 - ay) * (ax - bx)) / den
return (x1 + t * (x2 - x1), y1 + t * (y2 - y1))
prev = inp[-1]
for cur in inp:
if inside(cur):
if not inside(prev):
out.append(cross_point(prev, cur))
out.append(cur)
elif inside(prev):
out.append(cross_point(prev, cur))
prev = cur
return np.asarray(out, dtype=np.float64).reshape(-1, 2)
def _iou_from(pa: np.ndarray, area_a: float, env_a: np.ndarray, pb: np.ndarray, area_b: float,
env_b: np.ndarray) -> float:
if area_a < _MIN_AREA or area_b < _MIN_AREA:
return 0.0
if env_a[0] > env_b[2] or env_b[0] > env_a[2] or env_a[1] > env_b[3] or env_b[1] > env_a[3]:
return 0.0 # disjoint envelopes (a speed guard in perception_utils; the intersection would be empty)
inter = polygon_area(clip_convex(pa, pb))
if inter < _MIN_AREA:
return 0.0
union = area_a + area_b - inter
if union < _MIN_UNION_AREA:
return 0.0
return min(1.0, inter / union)
def _envelope(p: np.ndarray) -> np.ndarray:
return np.array([p[:, 0].min(), p[:, 1].min(), p[:, 0].max(), p[:, 1].max()])
def iou_bev(a: Sequence[float], b: Sequence[float]) -> float:
"""Rotated BEV IoU of two boxes ``(x, y, length, width, yaw)`` with perception_utils' ``compute2dIoU`` guards."""
pa, pb = bev_box_corners(*a), bev_box_corners(*b)
return _iou_from(pa, polygon_area(pa), _envelope(pa), pb, polygon_area(pb), _envelope(pb))
def iou_bev_nms(x: Any, y: Any, length: Any, width: Any, yaw: Any, labels: Any, *, search_distance_2d: float = 10.0,
iou_threshold: float = 0.1, scores: Optional[Any] = None, sort: bool = False,
pedestrian_label: int = PEDESTRIAN_LABEL) -> np.ndarray:
"""Indices of the objects kept by ``IouBevNms::apply`` (module docstring), in processing order. ``labels`` are
ObjectClassification labels; ``yaw`` is the ROS yaw. ``sort=True`` first orders by descending ``scores``
(``std::stable_sort``)."""
if not math.isfinite(search_distance_2d) or search_distance_2d < 0.0:
raise ValueError("search_distance_2d must be a finite non-negative value")
if not math.isfinite(iou_threshold) or not 0.0 <= iou_threshold <= 1.0:
raise ValueError("iou_threshold must be a finite value between 0 and 1")
xs = np.asarray(x, dtype=np.float64)
n = len(xs)
order = np.arange(n)
if sort:
if scores is None:
raise ValueError("sort=True needs scores")
order = np.argsort(-np.asarray(scores, dtype=np.float64), kind="stable")
ys = np.asarray(y, dtype=np.float64)[order]
xs = xs[order]
ls = np.asarray(length, dtype=np.float64)[order]
ws = np.asarray(width, dtype=np.float64)[order]
yw = np.asarray(yaw, dtype=np.float64)[order]
lab = np.asarray(labels, dtype=np.int64)[order]
polys = [bev_box_corners(xs[i], ys[i], ls[i], ws[i], yw[i]) for i in range(n)]
areas = [polygon_area(p) for p in polys]
envs = [_envelope(p) for p in polys]
sd2 = float(search_distance_2d) * float(search_distance_2d)
keep: List[int] = []
for t in range(n):
max_iou = 0.0
for s in range(t):
if lab[t] != lab[s] and (lab[t] == pedestrian_label or lab[s] == pedestrian_label):
continue
dx, dy = xs[t] - xs[s], ys[t] - ys[s]
if not dx * dx + dy * dy <= sd2:
continue
iou = _iou_from(polys[t], areas[t], envs[t], polys[s], areas[s], envs[s])
max_iou = max(max_iou, iou)
if iou > iou_threshold:
break
if max_iou <= iou_threshold:
keep.append(int(order[t]))
return np.asarray(keep, dtype=np.int64)
@dataclass(frozen=True)
class ClassRemapper:
"""``DetectionClassRemapper`` with square ``allow`` (bool), ``min_area`` and ``max_area`` matrices indexed
``[source_label, destination_label]`` over the ObjectClassification labels. Non-finite bounds become
``DBL_MAX`` (``detection_class_remapper.cpp:43-48``)."""
allow: np.ndarray
min_area: np.ndarray
max_area: np.ndarray
@classmethod
def from_lists(cls, allow_remapping_by_area_matrix: Sequence[int], min_area_matrix: Sequence[float],
max_area_matrix: Sequence[float]) -> "ClassRemapper":
"""The three flat lists of ``detection_class_remapper.param.yaml`` (row = source, column = destination)."""
if not (len(allow_remapping_by_area_matrix) == len(min_area_matrix) == len(max_area_matrix)):
raise ValueError("Class remapping matrices must have equal sizes.")
n = int(math.isqrt(len(min_area_matrix)))
if n == 0 or n * n != len(min_area_matrix):
raise ValueError("Class remapping matrices must be non-empty and square.")
def finite(v: Sequence[float]) -> np.ndarray:
a = np.asarray(v, dtype=np.float64)
return np.where(np.isfinite(a), a, np.finfo(np.float64).max).reshape(n, n)
return cls(np.asarray(allow_remapping_by_area_matrix, dtype=np.int64).reshape(n, n) != 0,
finite(min_area_matrix), finite(max_area_matrix))
@classmethod
def from_params(cls, params: Mapping[str, Any]) -> "ClassRemapper":
"""From the ``ros__parameters`` mapping of ``detection_class_remapper.param.yaml``."""
return cls.from_lists(params["allow_remapping_by_area_matrix"], params["min_area_matrix"],
params["max_area_matrix"])
@property
def num_labels(self) -> int:
return int(self.allow.shape[0])
def apply(self, labels: Any, length: Any, width: Any) -> np.ndarray:
"""New labels (uint8) for objects with ``labels`` and BEV ``length`` x ``width``."""
lab = np.asarray(labels, dtype=np.int64).copy()
area = (np.asarray(length, dtype=np.float64) * np.asarray(width, dtype=np.float64)).astype(np.float32)
area = area.astype(np.float64)
for i in range(len(lab)):
src = int(lab[i])
if src < 0 or src >= self.num_labels:
continue
for dst in range(self.num_labels):
if self.allow[src, dst] and self.min_area[src, dst] <= area[i] <= self.max_area[src, dst]:
lab[i] = dst
break
return lab.astype(np.uint8)