changh95's picture
tt-model push diffusion-planner-p150 (container)
4d9b003 verified
Raw History Blame Contribute Delete
12.5 kB
# SPDX-License-Identifier: Apache-2.0
"""C12 voxelization: deterministic pillar builders (vectorised numpy), Autoware's pillar feature decoration, and the
two canvas forms of the PointPillars scatter (dense scatter for references, gather index for the device).
Autoware's CUDA voxelizer (``autoware_lidar_centerpoint/lib/preprocess/preprocess_kernel.cu``) is
non-deterministic: slots inside a pillar come from ``atomicAdd`` over a randomly shuffled point buffer and pillar
ids from ``atomicAdd`` over a 2-D thread grid (S:centerpoint:177-186). The ports replace it by a deterministic
policy with the same statistics (PLAN.md D1, S:centerpoint:381-389):
1. optional seeded permutation of the input rows (:func:`ttaw.pointcloud.seeded_permutation`);
2. the half-open range test and the cell index ``floor((v - min) / voxel)`` (a float32 *division*, clamped; the
kernel's ``:135-145``);
3. the first ``max_points_per_pillar`` points of every cell in (permuted) input order;
4. pillar ids in ascending cell order, keeping the first ``max_pillars``. The cell order is ``"flipped_x"``
(CenterPoint: ``cell = (grid_x - 1 - ix) * grid_y + iy``, front rows first, ``:144``) or ``"raster"``
(PointPainting: ``cell = iy * grid_x + ix``, S:pointpainting:229-232).
The result (:class:`PillarSet`) holds the raw rows of every kept slot (zero-padded), the point counts and the
``(z, y, x)`` = ``(0, iy, ix)`` coordinates of mmdet3d / the CUDA ``uint3 {0, idy, idx}`` (``:189-190``).
:func:`decorate_pillar_features` is ``generateFeatures_kernel`` (``:211-343``) for 9 / 10 / 11 encoder features
(the cluster mean is summed slot by slot in float32, a fixed order standing in for the kernel's float atomics; the
pillar centre is ``voxel / 2 + coord * voxel + min`` in float32; padded slots are all zero).
Canvas: :func:`scatter_canvas` is ``scatterFeatures_kernel`` (``scatter_kernel.cu:25-48``: ``canvas[c, iy, ix]``).
The device builds the same canvas as a *gather* (C19, probe P4): :func:`canvas_gather_index` gives, for every cell
``iy * grid_x + ix`` of the NHWC canvas, the row of its pillar or a sentinel row (an explicit zero row of the
feature table), so the canvas is ``table[index]`` with no scatter, no clear and a static shape.
numpy only; no side effects on import.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Dict, Optional, Sequence, Tuple
import numpy as np
from .pointcloud import seeded_permutation
__all__ = [
"PillarGridSpec",
"PillarSet",
"assign_pillars",
"decorate_pillar_features",
"scatter_canvas",
"canvas_gather_index",
"pillar_cell_index",
]
_ORDERS = ("flipped_x", "raster")
@dataclass(frozen=True)
class PillarGridSpec:
"""A pillar grid and its capacities. Ranges and sizes are float32, as the CUDA config stores them
(``centerpoint_config.hpp:41-56``); ``grid_x = int((max_x - min_x) / voxel_x)`` in float32 (``:95-97``)."""
range_min: Tuple[float, float, float]
range_max: Tuple[float, float, float]
voxel_size: Tuple[float, float, float]
max_points_per_pillar: int = 32
max_pillars: int = 40000
order: str = "flipped_x"
def __post_init__(self) -> None:
if self.order not in _ORDERS:
raise ValueError(f"order must be one of {_ORDERS}, got {self.order!r}")
if self.max_points_per_pillar < 1 or self.max_pillars < 1:
raise ValueError("capacities must be >= 1")
@property
def min_f32(self) -> np.ndarray:
return np.asarray(self.range_min, dtype=np.float32)
@property
def max_f32(self) -> np.ndarray:
return np.asarray(self.range_max, dtype=np.float32)
@property
def voxel_f32(self) -> np.ndarray:
return np.asarray(self.voxel_size, dtype=np.float32)
def _grid(self, axis: int) -> int:
return int(np.float32(self.max_f32[axis] - self.min_f32[axis]) / self.voxel_f32[axis])
@property
def grid_x(self) -> int:
return self._grid(0)
@property
def grid_y(self) -> int:
return self._grid(1)
@property
def grid_z(self) -> int:
return self._grid(2)
@property
def num_cells(self) -> int:
return self.grid_x * self.grid_y
@dataclass
class PillarSet:
"""The deterministic voxelization of one cloud (fixed capacity ``max_pillars``; rows past ``num_pillars`` are
zero)."""
points: np.ndarray # (max_pillars, K, F) float32: raw rows of the kept slots, zero-padded
num_points: np.ndarray # (max_pillars,) int32: min(count, K), 0 for unused rows
coords: np.ndarray # (max_pillars, 3) int32: (0, iy, ix)
num_pillars: int # kept pillars (<= max_pillars)
num_pillars_total: int # non-empty cells before the cap (Autoware's num_voxels counter)
num_points_in_range: int
max_points_in_pillar: int # largest per-cell count before the slot cap
pillars_over_cap: int # cells holding more than K points
num_points_input: int
@property
def overflow(self) -> bool:
"""More non-empty cells than ``max_pillars``: the farthest-back pillars were dropped."""
return self.num_pillars_total > self.points.shape[0]
def stats(self) -> Dict[str, Any]:
return {"num_points": self.num_points_input, "num_points_in_range": self.num_points_in_range,
"num_pillars": self.num_pillars, "num_pillars_total": self.num_pillars_total,
"max_points_in_pillar": self.max_points_in_pillar, "pillars_over_cap": self.pillars_over_cap,
"pillar_overflow": self.overflow}
def pillar_cell_index(points: np.ndarray, spec: PillarGridSpec) -> Tuple[np.ndarray, np.ndarray]:
"""``(ix, iy)`` int64 of each row: ``floor((v - min) / voxel)`` evaluated in float32, clamped to the grid
(``preprocess_kernel.cu:140-143``). The caller has already applied the range test."""
p = np.asarray(points, dtype=np.float32)
mn, vs = spec.min_f32, spec.voxel_f32
ix = np.floor((p[:, 0] - mn[0]) / vs[0]).astype(np.int64)
iy = np.floor((p[:, 1] - mn[1]) / vs[1]).astype(np.int64)
return np.clip(ix, 0, spec.grid_x - 1), np.clip(iy, 0, spec.grid_y - 1)
def _cell_key(ix: np.ndarray, iy: np.ndarray, spec: PillarGridSpec) -> np.ndarray:
if spec.order == "flipped_x":
return (spec.grid_x - 1 - ix) * spec.grid_y + iy
return iy * spec.grid_x + ix
def _key_to_ixy(key: np.ndarray, spec: PillarGridSpec) -> Tuple[np.ndarray, np.ndarray]:
if spec.order == "flipped_x":
return (spec.grid_x - 1) - key // spec.grid_y, key % spec.grid_y
return key % spec.grid_x, key // spec.grid_x
def assign_pillars(points: np.ndarray, spec: PillarGridSpec, *, shuffle_seed: Optional[int] = None) -> PillarSet:
"""Deterministic voxelization (module docstring). ``points``: (N, F) float32 in the model frame, x, y, z in
columns 0-2 (the remaining columns, e.g. time_lag, are copied into the slots). Rows with a non-finite x, y or z
never enter a pillar."""
pts = np.ascontiguousarray(np.asarray(points, dtype=np.float32))
if pts.ndim != 2 or pts.shape[1] < 3:
raise ValueError(f"points must be (N, >=3), got {pts.shape}")
n_in = len(pts)
perm = seeded_permutation(n_in, shuffle_seed)
if perm is not None:
pts = pts[perm]
mn, mx = spec.min_f32, spec.max_f32
keep = np.ones(len(pts), dtype=bool)
for a in range(3): # Autoware: reject x < min || x >= max (NaN would pass there; here it fails)
keep &= (pts[:, a] >= mn[a]) & (pts[:, a] < mx[a])
pts = pts[keep]
ix, iy = pillar_cell_index(pts, spec)
key = _cell_key(ix, iy, spec)
order = np.argsort(key, kind="stable") # stable: input order inside each cell
key_sorted = key[order]
uniq, first, counts = np.unique(key_sorted, return_index=True, return_counts=True)
slot = np.arange(len(key_sorted)) - np.repeat(first, counts)
K, P = spec.max_points_per_pillar, spec.max_pillars
n_total = int(len(uniq))
n_keep = min(n_total, P)
pid = np.repeat(np.arange(n_total), counts) # pillar id of every sorted row (ascending cell order)
sel = (slot < K) & (pid < n_keep)
F = pts.shape[1]
raw = np.zeros((P, K, F), dtype=np.float32)
raw[pid[sel], slot[sel]] = pts[order][sel]
num = np.zeros(P, dtype=np.int32)
num[:n_keep] = np.minimum(counts[:n_keep], K)
cx, cy = _key_to_ixy(uniq[:n_keep], spec)
coords = np.zeros((P, 3), dtype=np.int32)
coords[:n_keep, 1] = cy
coords[:n_keep, 2] = cx
return PillarSet(points=raw, num_points=num, coords=coords, num_pillars=n_keep, num_pillars_total=n_total,
num_points_in_range=int(keep.sum()), max_points_in_pillar=int(counts.max()) if n_total else 0,
pillars_over_cap=int((counts > K).sum()), num_points_input=n_in)
def decorate_pillar_features(pillars: PillarSet, spec: PillarGridSpec, *, encoder_in_feature_size: int = 9,
point_dim: Optional[int] = None) -> np.ndarray:
"""``generateFeatures_kernel`` (``preprocess_kernel.cu:211-343``) -> (max_pillars, K, encoder_in_feature_size)
float32. ``point_dim`` (columns copied from the raw rows) is 5 (x, y, z, intensity, time_lag) for 11 features
and 4 (x, y, z, time_lag) otherwise, as the kernel's ``point_dim``. Layouts:
- 9: x, y, z, t, x-x̄, y-ȳ, z-z̄, x-x_c, y-y_c
- 10: x, y, z, t, x-x̄, y-ȳ, z-z̄, x-x_c, y-y_c, z-z_c
- 11: x, y, z, i, t, x-x̄, y-ȳ, z-z̄, x-x_c, y-y_c, z-z_c
x̄ is the mean of the pillar's kept points (summed slot by slot in float32, divided by the float count);
``x_c = voxel_x / 2 + ix * voxel_x + min_x`` in float32 (``:269-276``). Padded slots and unused pillars are 0."""
F_enc = int(encoder_in_feature_size)
if F_enc not in (9, 10, 11):
raise ValueError("encoder_in_feature_size must be 9, 10 or 11")
pd = (5 if F_enc >= 11 else 4) if point_dim is None else int(point_dim)
raw = pillars.points
P, K, F = raw.shape
if F < pd:
raise ValueError(f"pillar rows have {F} columns, decoration needs {pd}")
n = pillars.num_pillars
out = np.zeros((P, K, F_enc), dtype=np.float32)
if n == 0:
return out
r = raw[:n]
cnt = pillars.num_points[:n].astype(np.float32)
s = np.zeros((n, 3), dtype=np.float32)
for k in range(K): # fixed slot order (the kernel's float atomicAdd order is unspecified)
s += r[:, k, :3]
mean = s / cnt[:, None]
vs, mn = spec.voxel_f32, spec.min_f32
half = vs / np.float32(2)
xo = (half[0] + pillars.coords[:n, 2].astype(np.float32) * vs[0]) + mn[0]
yo = (half[1] + pillars.coords[:n, 1].astype(np.float32) * vs[1]) + mn[1]
zo = (half[2] + pillars.coords[:n, 0].astype(np.float32) * vs[2]) + mn[2]
f = out[:n]
f[:, :, :pd] = r[:, :, :pd]
f[:, :, pd:pd + 3] = r[:, :, :3] - mean[:, None, :]
f[:, :, pd + 3] = r[:, :, 0] - xo[:, None]
f[:, :, pd + 4] = r[:, :, 1] - yo[:, None]
if F_enc in (10, 11):
f[:, :, pd + 5] = r[:, :, 2] - zo[:, None]
valid = np.arange(K)[None, :] < pillars.num_points[:n, None]
f[~valid] = 0.0
return out
def scatter_canvas(features: np.ndarray, coords: np.ndarray, num_pillars: int, spec: PillarGridSpec) -> np.ndarray:
"""``scatterFeatures_kernel``: ``canvas[c, iy, ix] = features[p, c]`` for the first ``num_pillars`` rows; the
canvas is (C, grid_y, grid_x) float32 and zero elsewhere (NCHW without the batch axis)."""
f = np.asarray(features, dtype=np.float32).reshape(np.shape(features)[0], -1)
C = f.shape[1]
canvas = np.zeros((C, spec.grid_y, spec.grid_x), dtype=np.float32)
c = np.asarray(coords)[:num_pillars]
canvas[:, c[:, 1], c[:, 2]] = f[:num_pillars].T
return canvas
def canvas_gather_index(coords: np.ndarray, num_pillars: int, spec: PillarGridSpec, *, sentinel: int,
dtype: Any = np.uint32) -> np.ndarray:
"""Gather form of the scatter (C19): for every cell ``iy * grid_x + ix`` of the row-major (NHWC) canvas, the row
of its pillar, or ``sentinel`` (an all-zero row of the feature table) for an empty cell. Then
``canvas_nhwc = table[index]`` equals :func:`scatter_canvas` transposed to (grid_y, grid_x, C)."""
idx = np.full(spec.num_cells, sentinel, dtype=np.int64)
c = np.asarray(coords)[:num_pillars]
idx[c[:, 1].astype(np.int64) * spec.grid_x + c[:, 2].astype(np.int64)] = np.arange(num_pillars)
return idx.astype(dtype)