bevfusion-p150 / code /tt_bevfusion /host /preprocess.py
changh95's picture
tt-model push bevfusion-p150 (container)
5528e28 verified
Raw History Blame Contribute Delete
25.9 kB
# SPDX-License-Identifier: Apache-2.0
"""Host pre-processing of BEVFusion-L with Autoware's semantics (research/bevfusion/SPEC.md section 3; PLAN.md 2.6),
shared by the device path and the CPU reference so both see **identical** voxel, feature and rulebook tensors.
Input contract (the cloud is in the model frame, Autoware's ``base_link``; point features ``x, y, z, time_lag``,
``use_intensity: false``, so an ``intensity`` column is accepted and ignored):
1. ``points`` with a ``time_lag`` column: an already densified cloud (what Autoware's ``generateSweepPoints`` hands
to the voxelizer), used as is;
2. ``points`` (+ optional ``sweeps=[{"points", "time_lag_s", "T_current_from_sweep"}]``): client-side densification
(stateless), each past sweep through the float32 sweep kernel (``ttaw.pointcloud.densify_sweeps``);
3. ``points`` + ``stream={"id", "timestamp_s", "T_world_from_ego", "reset"}``: the optional server-side cache,
Autoware's ``PointCloudDensification`` per stream id (``densification_num_past_frames: 1``, cold start, capacity
``2,000,000`` with ``count + sweep > capacity`` dropping that sweep and every older one,
``voxel_generator.cpp:58-63``; ``ttaw.pointcloud.SweepDensifier``). A missing pose is Autoware's TF failure: the
frame is refused (HTTP 400) and not cached;
4. ``points`` alone: one sweep, ``time_lag = 0``.
Hygiene (documented deviations, as the other lidar ports): rows with a non-finite x / y / z never enter
(``ttaw.io.PointCloud``), all-zero x, y, z rows (organized-cloud fillers such as ``test.pcd``'s) are dropped.
Hard voxelization (:func:`hard_voxelize`; spconv ``Point2Voxel::point_to_voxel_hash_static`` as Autoware calls it,
``PKG/lib/preprocess/preprocess_kernel.cu:110-154``, S:bevfusion:152-161):
- per point ``floor((p - min) / size)`` in float32 for x, y, z; valid iff ``0 <= idx < grid`` with the float32 grid
(1440, 1440, 40); out-of-range points are dropped (Autoware has no other range crop);
- voxels in order of their first point (Autoware: hash-slot order, which no output depends on), the **first 10
points of a voxel in input order** (Autoware: the race of ``atomicAdd``; this deterministic rule is also the
training-time mmcv ``deterministic=True`` voxelization, S:bevfusion:191), ``num_points`` clamped to 10;
- at most 256,000 voxels (``bevfusion_trt.cpp:550-581``; which voxels survive an overflow is hash-order dependent in
Autoware, the first ones here; flagged);
- the ONNX inputs ``voxels [V, 10, 4]`` (zero-padded), ``coors [V, 3]`` int32 ``(z, y, x)``,
``num_points_per_voxel [V]`` int32, and the in-graph VFE mean ``ReduceSum(voxels, 1) / float(num)`` computed here
in float32 (the device input; ``/ReduceSum``, ``/Cast_2``, ``/Div``).
Sparse plan (:func:`build_sparse_plan`, C13 ``ttaw.sparse``): the voxel coordinates flipped to ``(x, y, z)`` (the
graph's ``/Slice`` with step -1) on the 1440 x 1440 x 41 grid, the four sub-manifold neighbour maps (shared by the
convs of a level: spconv recomputes identical rulebooks per layer), the three 3x3x3 stride-2 maps (padding 1, 1,
(1, 1, 0)), the 1x1x3 / (1, 1, 2) conv_out map, every level's active set, and the smallest capacity bucket
(``reference.config.CAPACITY_BUCKETS``) that holds every level's count (the TT trace variant). numpy only.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Dict, Mapping, Optional, Sequence, Tuple
import numpy as np
from .. import io as tio
from ..reference.config import (CAPACITY_BUCKETS, DROP_ZERO_POINTS, MAX_VOXELS, NUM_POINT_FEATURES, SPARSE_LEVELS,
SPARSE_SHAPE, BEVFusionConfig)
from ..ttaw import sparse as sp
from ..ttaw.geometry import as_transform
from ..ttaw.pointcloud import StreamDensifiers, densify_sweeps, nonzero_rows
__all__ = ["Prepared", "VoxelSet", "SparsePlan", "BEVFusionPreprocessor", "TIME_LAG_FIELD", "hard_voxelize",
"vfe_mean", "build_sparse_plan", "select_bucket", "LEVEL_NAMES", "SUBM_LAYERS", "STRIDED_LAYERS"]
TIME_LAG_FIELD = "time_lag" # a column with this name marks a pre-densified cloud (seconds)
LEVEL_NAMES = ("l1", "l2", "l3", "l4", "out")
# the strided layers between levels: name -> (kernel, stride, padding) (S:bevfusion:226-235)
STRIDED_LAYERS = {"down1": (3, 2, 1), "down2": (3, 2, 1), "down3": (3, 2, (1, 1, 0)),
"conv_out": ((1, 1, 3), (1, 1, 2), 0)}
SUBM_LAYERS = ("subm1", "subm2", "subm3", "subm4")
# ------------------------------------------------------------------------------------------------ voxels
@dataclass
class VoxelSet:
"""The hard-voxelized frame (module docstring) and its statistics."""
voxels: np.ndarray # (V, 10, 4) float32, unused slots zero
num_points: np.ndarray # (V,) int32, clamped to max_points_per_voxel
coors: np.ndarray # (V, 3) int32 (z, y, x)
stats: Dict[str, Any] = field(default_factory=dict)
@property
def num_voxels(self) -> int:
return int(len(self.coors))
def hard_voxelize(points: np.ndarray, point_cloud_range: Sequence[float], voxel_size: Sequence[float], *,
max_points: int = 10, max_voxels: int = MAX_VOXELS) -> VoxelSet:
"""(M, F) float32 points (x, y, z, ...) -> :class:`VoxelSet` (module docstring). Arithmetic and ordering are those
of ``research/bevfusion/scripts/bevfusion_ref.py voxelize`` (bit-identical, tested)."""
pts = np.asarray(points, dtype=np.float32)
if pts.ndim != 2 or pts.shape[1] < 3:
raise ValueError(f"points must be (M, >=3), got {pts.shape}")
rng_min = np.array(point_cloud_range[:3], dtype=np.float32)
rng_max = np.array(point_cloud_range[3:], dtype=np.float32)
vs = np.array(voxel_size, dtype=np.float32)
grid = ((rng_max - rng_min) / vs).astype(np.int64) # float32: (1440, 1440, 40)
with np.errstate(invalid="ignore"):
idx = np.floor((pts[:, :3] - rng_min) / vs).astype(np.int64) # (x, y, z)
valid = np.all((idx >= 0) & (idx < grid), axis=1)
pts_v = pts[valid]
idx_v = idx[valid]
F = pts.shape[1]
key = (idx_v[:, 2] * grid[1] + idx_v[:, 1]) * grid[0] + idx_v[:, 0] # z-major linear key
uniq, first_idx, inverse = np.unique(key, return_index=True, return_inverse=True)
inverse = inverse.reshape(-1)
order = np.argsort(first_idx, kind="stable") # first-appearance order
rank = np.empty_like(order)
rank[order] = np.arange(len(order))
vid = rank[inverse]
num_vox = min(len(uniq), int(max_voxels))
srt = np.argsort(vid, kind="stable")
vid_sorted = vid[srt]
starts = np.searchsorted(vid_sorted, np.arange(len(uniq)))
pos_sorted = np.arange(len(vid_sorted)) - starts[vid_sorted]
pos = np.empty_like(pos_sorted)
pos[srt] = pos_sorted
keep = (pos < max_points) & (vid < num_vox)
voxels = np.zeros((num_vox, max_points, F), dtype=np.float32)
voxels[vid[keep], pos[keep]] = pts_v[keep]
num_points = np.bincount(vid[keep], minlength=num_vox).astype(np.int32)[:num_vox]
vox_xyz = np.zeros((num_vox, 3), dtype=np.int64)
vox_xyz[vid[keep]] = idx_v[keep]
coors = np.ascontiguousarray(vox_xyz[:, ::-1].astype(np.int32))
stats = {"points_in": int(len(pts)), "points_in_range": int(valid.sum()), "voxels_total": int(len(uniq)),
"voxels": int(num_vox), "voxel_overflow": bool(len(uniq) > num_vox),
"points_dropped_by_cap": int((pos >= max_points).sum()),
"max_points_in_voxel": int(np.bincount(vid).max()) if len(vid) else 0}
return VoxelSet(voxels=voxels, num_points=num_points, coors=coors, stats=stats)
def vfe_mean(voxels: np.ndarray, num_points: np.ndarray) -> np.ndarray:
"""``HardSimpleVFE`` as exported: ``ReduceSum(voxels, axis=1) / float(num_points)`` in float32 -> (V, 4)."""
v = np.asarray(voxels, dtype=np.float32)
return (v.sum(axis=1) / np.asarray(num_points).astype(np.float32).reshape(-1, 1)).astype(np.float32)
# ------------------------------------------------------------------------------------------------ rulebooks
@dataclass
class SparsePlan:
"""Every level's active set and every layer's neighbour map (C13 gather form; ``-1`` = no input)."""
levels: Dict[str, sp.SparseLevel] # l1, l2, l3, l4, out
maps: Dict[str, np.ndarray] # subm1..4 (N_l, 27); down1..3 (N_l+1, 27); conv_out (N_out, 3)
bucket: str
overflow: bool
pair_counts: Optional[Dict[str, int]] = None # filled by the numba twin (host/fast.sparse_plan), else computed
# OPT_PLAN M0 (backend "levels", BEVFUSION_DEVICE_PLAN=1): no maps on the host; every level in ascending key,
# ``perm`` the l1 order (sorted row -> voxel id), the packed keys and x-row starts the device builds the maps
# from (``host/fast.sparse_levels``). ``pair_counts`` is then filled from the device after the frame ran.
perm: Optional[np.ndarray] = None
keys: Optional[Dict[str, np.ndarray]] = None
xstart: Optional[Dict[str, np.ndarray]] = None
def counts(self) -> Dict[str, int]:
return {name: lvl.num_active for name, lvl in self.levels.items()}
def pairs(self) -> Dict[str, int]:
if self.pair_counts is not None:
return dict(self.pair_counts)
return {name: sp.pairs_count(m) for name, m in self.maps.items()} # {} for a device plan before its run
@property
def device_fields(self) -> bool:
return self.keys is not None
def describe(self) -> Dict[str, Any]:
return {"counts": self.counts(), "pairs": self.pairs(), "bucket": self.bucket, "overflow": self.overflow}
def select_bucket(counts: Mapping[str, int], buckets: Mapping[str, Sequence[int]] = CAPACITY_BUCKETS
) -> Tuple[str, bool]:
"""The first bucket (in the mapping's order: small to large) whose capacities hold every level's count;
``(largest, True)`` when none does. Nothing is clipped: an overflowing plan is refused before the device runs
(``api.BEVFusion._prepare`` raises ``io.InputError``, HTTP 400; ``tt.sparse.frame_inputs`` refuses it too)."""
names = list(buckets)
for name in names:
if all(int(counts[lvl]) <= int(cap) for lvl, cap in zip(LEVEL_NAMES, buckets[name])):
return name, False
return names[-1], True
def build_sparse_plan(coors_zyx: np.ndarray, *, rule: str = "spconv",
buckets: Mapping[str, Sequence[int]] = CAPACITY_BUCKETS, backend: str = "numpy") -> SparsePlan:
"""The sparse encoder's rulebooks for one frame (module docstring). ``backend="numba"`` (OPT_PLAN H1,
``BEVFUSION_HOST_PLAN``): the hash-rulebook twin ``host/fast.sparse_plan`` (bit-identical maps and level
coordinates; both rules agree for in-grid voxels); ``backend="columns"`` (H1b) the column-table twin
``host/fast.sparse_plan_columns`` (same maps, levels and pair counts); both fall back to this numpy oracle without
numba or for out-of-grid voxels. ``backend="xcolumns"`` (H6a): the column twin with its serial passes in parallel
x partitions (same maps). ``backend="levels"`` (OPT_PLAN M0, ``BEVFUSION_DEVICE_PLAN=1``): the level sets only
(``host/fast.sparse_levels``: every level key-sorted, l1 too, with its ``perm``); the device builds the maps."""
if backend == "levels":
from . import fast
lv = fast.sparse_levels(coors_zyx, STRIDED_LAYERS, SPARSE_LEVELS, SPARSE_SHAPE)
if lv is not None:
levels_l, perm, keys, xstart = lv
bucket, overflow = select_bucket({k: v.num_active for k, v in levels_l.items()}, buckets)
return SparsePlan(levels=levels_l, maps={}, bucket=bucket, overflow=overflow, perm=perm, keys=keys,
xstart=xstart)
backend = "xcolumns" # no numba / out-of-grid voxels: a host plan (tt.sparse derives the fields)
if backend in ("numba", "columns", "xcolumns"):
from . import fast
res = None
if backend in ("columns", "xcolumns"): # H1b: per-level column tables (bit-identical to the hash twin)
res = fast.sparse_plan_columns(coors_zyx, STRIDED_LAYERS, SPARSE_LEVELS, SPARSE_SHAPE,
par=backend == "xcolumns")
if res is None:
res = fast.sparse_plan(coors_zyx, STRIDED_LAYERS, SPARSE_LEVELS, SPARSE_SHAPE)
if res is not None:
levels_f, maps_f, pairs_f = res
bucket, overflow = select_bucket({k: v.num_active for k, v in levels_f.items()}, buckets)
return SparsePlan(levels=levels_f, maps=maps_f, bucket=bucket, overflow=overflow, pair_counts=pairs_f)
elif backend != "numpy":
raise ValueError("backend must be 'numpy', 'numba', 'columns', 'xcolumns' or 'levels'")
c = np.asarray(coors_zyx, dtype=np.int64)
l1 = sp.SparseLevel(c[:, ::-1].copy(), SPARSE_SHAPE)
levels = {"l1": l1}
maps: Dict[str, np.ndarray] = {}
lvl = l1
for i, (down, nxt) in enumerate(zip(("down1", "down2", "down3", "conv_out"), ("l2", "l3", "l4", "out")), 1):
maps[f"subm{i}"] = sp.subm_neighbor_map(lvl, 3, rule=rule)
k, s, p = STRIDED_LAYERS[down]
lvl, maps[down] = sp.strided_neighbor_map(lvl, k, s, p)
levels[nxt] = lvl
for name, shape in SPARSE_LEVELS.items():
if levels[name].spatial_shape != shape:
raise AssertionError(f"level {name} grid {levels[name].spatial_shape} != {shape}")
bucket, overflow = select_bucket({k: v.num_active for k, v in levels.items()}, buckets)
return SparsePlan(levels=levels, maps=maps, bucket=bucket, overflow=overflow)
# ------------------------------------------------------------------------------------------------ prepared
@dataclass
class Prepared:
"""One frame, ready for the device or the CPU reference."""
points: np.ndarray # (M, 4) float32 x, y, z, time_lag: the densified cloud
voxel_set: VoxelSet
vfe: np.ndarray # (V, 4) float32 voxel features (the graph's mean VFE)
plan: Optional[SparsePlan] # None for an empty frame
frame_id: str = "base_link"
densify: Dict[str, Any] = field(default_factory=dict)
skipped: Optional[str] = None # Autoware's reason to skip the frame (no voxel: no inference, no objects)
@property
def num_voxels(self) -> int:
return self.voxel_set.num_voxels
@property
def coors(self) -> np.ndarray:
return self.voxel_set.coors
def onnx_inputs(self) -> Dict[str, np.ndarray]:
"""The deployed graph's inputs: ``voxels`` (V, 10, 4) float32, ``coors`` (V, 3) int32 (z, y, x),
``num_points_per_voxel`` (V,) int32."""
v = self.voxel_set
return {"voxels": v.voxels, "coors": v.coors, "num_points_per_voxel": v.num_points}
def stats(self) -> Dict[str, Any]:
"""Voxel and sparse statistics, the skip verdict and the densification info (``meta``)."""
s = dict(self.voxel_set.stats)
s["num_voxels"] = self.num_voxels
if self.plan is not None:
s["sparse"] = self.plan.describe()
s.update({"skipped": self.skipped, "densify": self.densify})
return s
def _cloud(source: Any, what: str) -> tio.PointCloud:
if source is None:
raise tio.InputError(f"{what} is missing")
return tio.load_points(source)
def _xyz(cloud: tio.PointCloud, what: str) -> np.ndarray:
if not {"x", "y", "z"} <= set(cloud.fields):
raise tio.InputError(f"{what} needs fields x, y, z (got {list(cloud.fields)})")
return np.ascontiguousarray(cloud.select(("x", "y", "z")), dtype=np.float32)
class BEVFusionPreprocessor:
"""Stateful only through the per-stream densification caches (``stream`` inputs)."""
def __init__(self, config: BEVFusionConfig, *, max_streams: int = 16, drop_zero_points: bool = DROP_ZERO_POINTS,
build_plan: bool = True, rule: str = "spconv", plan: str = "numpy", voxelizer: str = "numpy",
densify: str = "numpy"):
if plan not in ("numpy", "numba", "columns", "xcolumns", "levels") or voxelizer not in ("numpy", "numba", "lazy", "parallel"):
raise ValueError("plan / voxelizer must be 'numpy' or 'numba'")
if densify not in ("numpy", "numba"):
raise ValueError("densify must be 'numpy' or 'numba'")
self.densify = densify
# "numba": the host/fast.py twins (OPT_PLAN H1 BEVFUSION_HOST_PLAN, H3 BEVFUSION_HOST_VOXELIZER), bit-identical
# to the numpy oracles of this module, which stay the reference and the fallback
self.plan_backend = plan
self.voxelizer = voxelizer
self.config = config
self.drop_zero_points = drop_zero_points
self.build_plan = build_plan
self.rule = rule
self.capacity = int(config.node["cloud_capacity"])
self.streams = StreamDensifiers(num_past_frames=int(config.node["densification_num_past_frames"]),
cloud_capacity=self.capacity, max_streams=max_streams)
# ---------------------------------------------------------------------------------------- densification
def densified_points(self, points: Any = None, *, sweeps: Optional[Sequence[Mapping[str, Any]]] = None,
stream: Optional[Mapping[str, Any]] = None) -> Tuple[np.ndarray, Dict[str, Any], str]:
"""The (N, 4) float32 cloud (x, y, z, time_lag) of one request, its densification info and frame id."""
cloud = _cloud(points, "this model needs 'points'")
frame_id = cloud.frame_id
if TIME_LAG_FIELD in cloud.fields: # 1. pre-densified
if sweeps or stream is not None:
raise tio.InputError("a cloud with a 'time_lag' column is already densified: do not send 'sweeps' "
"or 'stream' with it")
fastpts = self._densify_fast(cloud, TIME_LAG_FIELD)
if fastpts is not None:
return fastpts, {"mode": "pre-densified", "num_points": int(len(fastpts))}, frame_id
xyz = _xyz(cloud, "points")
lag = np.asarray(cloud.select((TIME_LAG_FIELD,))[:, 0], dtype=np.float32)
if not np.isfinite(lag).all():
raise tio.InputError("time_lag holds non-finite values")
pts = np.concatenate([xyz, lag[:, None]], axis=1)
if self.drop_zero_points:
pts = pts[nonzero_rows(pts)]
if len(pts) > self.capacity:
raise tio.InputError(f"{len(pts)} points exceed cloud_capacity {self.capacity}")
return np.ascontiguousarray(pts), {"mode": "pre-densified", "num_points": int(len(pts))}, frame_id
xyz = None
if sweeps or stream is not None or not self._fast_densify_ok(cloud):
xyz = _xyz(cloud, "points")
if self.drop_zero_points:
xyz = xyz[nonzero_rows(xyz)]
if sweeps and stream is not None:
raise tio.InputError("give 'sweeps' (client-side densification) or 'stream' (server-side), not both")
if sweeps: # 2. client-side
past = []
for i, s in enumerate(sweeps):
if not isinstance(s, Mapping):
raise tio.InputError(f"sweeps[{i}] must be an object {{points, time_lag_s, T_current_from_sweep}}")
try:
lag = float(s.get("time_lag_s", 0.0))
except (TypeError, ValueError):
lag = float("nan")
if not np.isfinite(lag):
raise tio.InputError(f"sweeps[{i}].time_lag_s must be finite")
T = s.get("T_current_from_sweep")
T = None if T is None else as_transform(T, field=f"sweeps[{i}].T_current_from_sweep")
sx = _xyz(_cloud(s.get("points"), f"sweeps[{i}].points"), f"sweeps[{i}].points")
if self.drop_zero_points:
sx = sx[nonzero_rows(sx)]
past.append((sx, lag, T))
pts, info = densify_sweeps(xyz, past, point_feature_size=NUM_POINT_FEATURES,
cloud_capacity=self.capacity)
return pts, {"mode": "sweeps", **info.to_dict()}, frame_id
if stream is not None: # 3. server-side, per stream id
if not isinstance(stream, Mapping):
raise tio.InputError("stream must be an object {id, timestamp_s, T_world_from_ego, reset}")
sid = str(stream.get("id", "default"))
try:
stamp = float(stream["timestamp_s"])
except (KeyError, TypeError, ValueError):
stamp = float("nan")
if not np.isfinite(stamp):
raise tio.InputError("stream.timestamp_s (seconds) is required for server-side densification")
T = stream.get("T_world_from_ego")
T = None if T is None else as_transform(T, field="stream.T_world_from_ego")
d = self.streams.get(sid, reset=bool(stream.get("reset", False)))
if not d.enqueue(xyz, stamp, T):
raise tio.InputError(
f"stream.T_world_from_ego is required with densification_num_past_frames={d.num_past_frames}: "
"Autoware skips a frame without its map->base_link transform and does not cache it (nor did this "
"request)")
pts, info = d.sweep_points(point_feature_size=NUM_POINT_FEATURES)
return pts, {"mode": "stream", "stream_id": sid, **info.to_dict()}, frame_id
if not sweeps and stream is None and xyz is None: # 4. single sweep, H4 twin
pts = self._densify_fast(cloud, None)
return pts, {"mode": "single", "num_points": int(len(pts))}, frame_id
if xyz is None:
xyz = _xyz(cloud, "points")
if self.drop_zero_points:
xyz = xyz[nonzero_rows(xyz)]
if len(xyz) > self.capacity: # 4. single sweep
raise tio.InputError(f"{len(xyz)} points exceed cloud_capacity {self.capacity}")
pts = np.zeros((len(xyz), NUM_POINT_FEATURES), np.float32)
pts[:, :3] = xyz
return pts, {"mode": "single", "num_points": int(len(pts))}, frame_id
def _fast_densify_ok(self, cloud: tio.PointCloud) -> bool:
if self.densify != "numba" or not {"x", "y", "z"} <= set(cloud.fields):
return False
from . import fast
return fast.HAVE_NUMBA and cloud.points.dtype == np.float32 and cloud.points.ndim == 2
def _densify_fast(self, cloud: tio.PointCloud, lag_field: Optional[str]) -> Optional[np.ndarray]:
"""H4 (``BEVFUSION_HOST_DENSIFY=numba``): the single-sweep and pre-densified paths in one numba pass
(``host/fast.densify_rows``; the same rows, values, errors and capacity check), or None (numpy path)."""
if not self._fast_densify_ok(cloud):
return None
from . import fast
f = list(cloud.fields)
cols = (f.index("x"), f.index("y"), f.index("z"), f.index(lag_field) if lag_field else -1)
res = fast.densify_rows(cloud.points, cols, self.drop_zero_points)
if res is None:
return None
pts, bad = res
if bad:
raise tio.InputError("time_lag holds non-finite values")
if len(pts) > self.capacity:
raise tio.InputError(f"{len(pts)} points exceed cloud_capacity {self.capacity}")
return pts
# ------------------------------------------------------------------------------------------ voxelization
def voxelize(self, points4: np.ndarray) -> Tuple[VoxelSet, np.ndarray]:
cfg = self.config
if self.voxelizer in ("numba", "lazy", "parallel"):
from . import fast
res = fast.hard_voxelize(points4, cfg.point_cloud_range, cfg.voxel_size,
max_points=cfg.max_points_per_voxel, max_voxels=cfg.max_voxels,
lazy=self.voxelizer in ("lazy", "parallel"),
par=self.voxelizer == "parallel")
if res is not None:
return res
vs = hard_voxelize(points4, cfg.point_cloud_range, cfg.voxel_size, max_points=cfg.max_points_per_voxel,
max_voxels=cfg.max_voxels)
return vs, vfe_mean(vs.voxels, vs.num_points)
def __call__(self, points: Any = None, *, sweeps: Optional[Sequence[Mapping[str, Any]]] = None,
stream: Optional[Mapping[str, Any]] = None) -> Prepared:
pts, info, frame_id = self.densified_points(points, sweeps=sweeps, stream=stream)
vs, vfe = self.voxelize(pts)
skipped = "no_voxels" if vs.num_voxels < self.config.min_voxels else None # V < 1 aborts the frame
plan = None
if skipped is None and self.build_plan:
plan = build_sparse_plan(vs.coors, rule=self.rule, backend=self.plan_backend)
return Prepared(points=pts, voxel_set=vs, vfe=vfe, plan=plan, frame_id=frame_id, densify=info,
skipped=skipped)
def describe(self) -> Dict[str, Any]:
return {"max_voxels": self.config.max_voxels, "points_per_voxel": self.config.max_points_per_voxel,
"drop_zero_points": self.drop_zero_points, "cloud_capacity": self.capacity, "rule": self.rule,
"plan": self.plan_backend, "voxelizer": self.voxelizer, "densify": self.densify,
"capacity_buckets": {k: list(v) for k, v in CAPACITY_BUCKETS.items()},
"streams": self.streams.describe()}