box3d
Generic 3D oriented-box geometry shared by detection models and their evaluation.
Boxes are parameterized as \((c_x, c_y, c_z, d_x, d_y, d_z, \theta)\): gravity-aligned center, full extents, and heading \(\theta\) (radians) counter-clockwise about \(+z\) from \(+x\). Axis-aligned boxes set \(\theta = 0\). Corners are \((\ldots, 8, 3)\) with the top face (max \(z\)) first; IoU is frame-invariant so these work in any right-handed frame.
Functions:
-
box_cornersβConvert parameterized boxes to their 8 corners.
-
decode_box_residualsβDecode predicted box residuals against anchors (OpenPCDet's
ResidualCoder). -
encode_box_residualsβEncode ground-truth boxes into anchor-relative residuals (inverse of
decode_box_residuals). -
angle_to_classβConvert continuous heading angles to discrete bin classes and residuals.
-
class_to_angleβInvert
angle_to_class: recover continuous heading angles from bin classes and residuals. -
class_to_sizeβRecover full box edge lengths from a size class index and residual (inverse of the size encoding).
-
limit_periodβWrap an angle to \([-\text{offset} \cdot \text{period}, (1 - \text{offset}) \cdot \text{period})\).
-
box3d_overlapβPairwise 3D intersection volume and IoU of two sets of boxes given as corners.
-
boxes_iou_bevβPairwise bird's-eye (top-down) rotated-box IoU.
-
boxes_iou_nearest_bevβPairwise bird's-eye IoU of boxes snapped to their nearest axis-aligned orientation.
-
boxes_iou3dβPairwise oriented 3D box IoU.
-
nms3dβGreedy 3D non-maximum suppression.
-
points_in_boxesβTest every point against every oriented box.
-
count_points_in_boxesβCount how many points fall inside each oriented box.
-
projected_ignore_maskβFlag boxes whose image projection is shorter than
min_heightpixels (the KITTI difficulty rule).
box_corners [source] ΒΆ
Convert parameterized boxes to their 8 corners.
The heading is counter-clockwise about \(+z\) from \(+x\); boxes are \((c_x, c_y, c_z, d_x, d_y, d_z, \theta)\) with full extents.
Parameters:
-
boxes(Tensor) βBoxes \((c_x, c_y, c_z, d_x, d_y, d_z, \theta)\), shape \((\ldots, 7)\).
Returns:
-
TensorβCorner coordinates, shape \((\ldots, 8, 3)\), with the top face (max \(z\)) as corners \(0..3\).
Shape
- boxes: \((\ldots, 7)\)
- output: \((\ldots, 8, 3)\)
decode_box_residuals [source] ΒΆ
decode_box_residuals(
encodings: Tensor,
anchors: Tensor,
*,
angle_by_sincos: bool = False,
) -> Tensor
Decode predicted box residuals against anchors (OpenPCDet's ResidualCoder).
Residuals encode the center offset normalized by the anchor base diagonal, log-size ratios, and an angle term: a plain delta by default, or a \((\cos, \sin)\) pair when angle_by_sincos (one extra channel). Trailing channels (e.g. nuScenes velocity) decode as plain deltas.
Parameters:
-
encodings(Tensor) βPredicted residuals, shape \((\ldots, 7 + C)\), or \((\ldots, 8 + C)\) with
angle_by_sincos. -
anchors(Tensor) βMatching anchors \((x, y, z, d_x, d_y, d_z, \theta, \ldots)\), shape \((\ldots, 7 + C)\).
-
angle_by_sincos(bool, default:False) βWhether the heading residual is encoded as \((\cos, \sin)\).
Returns:
-
TensorβDecoded boxes \((c_x, c_y, c_z, d_x, d_y, d_z, \theta, \ldots)\), shape \((\ldots, 7 + C)\).
Shape
- encodings: \((\ldots, 7 + C)\) or \((\ldots, 8 + C)\)
- anchors: \((\ldots, 7 + C)\)
- output: \((\ldots, 7 + C)\)
encode_box_residuals [source] ΒΆ
Encode ground-truth boxes into anchor-relative residuals (inverse of decode_box_residuals).
The exact inverse of decode_box_residuals: the center offset is normalized by the anchor base diagonal, sizes become log ratios, and the heading becomes a plain delta or a \((\cos, \sin)\) pair. Extents are clamped to \(10^{-5}\) before the log so a degenerate box does not produce a non-finite target.
Parameters:
-
boxes(Tensor) βGround-truth boxes \((c_x, c_y, c_z, d_x, d_y, d_z, \theta, \ldots)\), shape \((\ldots, 7 + C)\).
-
anchors(Tensor) βMatching anchors \((x, y, z, d_x, d_y, d_z, \theta, \ldots)\), shape \((\ldots, 7 + C)\).
-
angle_by_sincos(bool, default:False) βWhether to encode the heading residual as \((\cos, \sin)\) (one extra channel).
Returns:
-
TensorβResidual encodings, shape \((\ldots, 7 + C)\), or \((\ldots, 8 + C)\) with
angle_by_sincos.
Shape
- boxes: \((\ldots, 7 + C)\)
- anchors: \((\ldots, 7 + C)\)
- output: \((\ldots, 7 + C)\) or \((\ldots, 8 + C)\)
angle_to_class [source] ΒΆ
Convert continuous heading angles to discrete bin classes and residuals.
The range \([0, 2\pi)\) is split into num_heading_bins equal bins centered at \(0, 1 \cdot (2\pi / N), \ldots, (N - 1) \cdot (2\pi / N)\). The returned class and residual satisfy \(\text{class} \cdot (2\pi / N) + \text{residual} = \text{angle}\).
Parameters:
-
angle(Tensor) βHeading angles in radians of shape \((K,)\).
-
num_heading_bins(int) βNumber of heading bins \(N\).
Returns:
-
Tuple[Tensor, Tensor]βA tuple of the per-angle class indices (long, shape \((K,)\)) and residual angles (shape \((K,)\)).
class_to_angle [source] ΒΆ
Invert angle_to_class: recover continuous heading angles from bin classes and residuals.
A single bin (num_heading_bins == 1, axis-aligned boxes) always decodes to a heading of \(0\).
Parameters:
-
heading_class(Tensor) βBin class indices (long) of shape \((K,)\).
-
heading_residual(Tensor) βPer-angle residuals of shape \((K,)\).
-
num_heading_bins(int) βNumber of heading bins \(N\).
Returns:
-
TensorβThe recovered heading angles of shape \((K,)\).
class_to_size [source] ΒΆ
Recover full box edge lengths from a size class index and residual (inverse of the size encoding).
Parameters:
-
size_class(Tensor) βSize class indices (long) of shape \((K,)\).
-
size_residual(Tensor) βPer-axis residuals of shape \((K, 3)\).
-
mean_sizes(Tensor) βTemplate sizes of shape \((C, 3)\) holding full edge lengths per class.
Returns:
-
TensorβThe recovered full edge lengths of shape \((K, 3)\).
limit_period [source] ΒΆ
Wrap an angle to \([-\text{offset} \cdot \text{period}, (1 - \text{offset}) \cdot \text{period})\).
box3d_overlap [source] ΒΆ
Pairwise 3D intersection volume and IoU of two sets of boxes given as corners.
Signature-compatible with pytorch3d.ops.box3d_overlap, so the exact CUDA implementation can be swapped in behind this interface. Boxes are assumed gravity-aligned (as produced by box_corners): the bird's-eye polygons are intersected exactly (corner containment plus edge crossings) and scaled by the vertical overlap, entirely on the input device.
Parameters:
-
boxes1(Tensor) βCorners of the first set, shape \((M, 8, 3)\) (see
box_corners). -
boxes2(Tensor) βCorners of the second set, shape \((N, 8, 3)\).
Returns:
-
Tuple[Tensor, Tensor]βA tuple
(intersection_vol, iou), each shape \((M, N)\).
Shape
- boxes1: \((M, 8, 3)\)
- boxes2: \((N, 8, 3)\)
- output: \((M, N)\), \((M, N)\)
boxes_iou_bev [source] ΒΆ
Pairwise bird's-eye (top-down) rotated-box IoU.
Projects both box sets onto the ground plane (ignoring \(z\)) and intersects the oriented rectangles. The heading is counter-clockwise (angle increases \(x \to y\)). Runs entirely in torch, so it stays on CUDA tensors without a custom extension; the cost is \(O(N \cdot M)\).
Parameters:
-
boxes_a(Tensor) βBoxes \((c_x, c_y, c_z, d_x, d_y, d_z, \theta)\) with full extents, shape \((N, 7)\).
-
boxes_b(Tensor) βBoxes in the same layout, shape \((M, 7)\).
-
aligned(bool, default:False) βWith
True,boxes_aandboxes_bpair up row by row (\(N = M\)) and the result is \((N,)\).
Returns:
-
TensorβPairwise BEV IoU in \([0, 1]\), shape \((N, M)\).
Shape
- boxes_a: \((N, 7)\)
- boxes_b: \((M, 7)\)
- output: \((N, M)\)
boxes_iou_nearest_bev [source] ΒΆ
Pairwise bird's-eye IoU of boxes snapped to their nearest axis-aligned orientation.
Each box keeps its center and takes the footprint of its heading rounded to the nearest multiple of \(\pi / 2\), so \(d_x\) and \(d_y\) swap when the heading is closer to \(\pm \pi / 2\) than to \(0\); the IoU is then the plain axis-aligned rectangle IoU. The anchor-based detectors assign their anchors by this measure rather than by the exact rotated overlap of boxes_iou_bev.
Parameters:
-
boxes_a(Tensor) βBoxes \((c_x, c_y, c_z, d_x, d_y, d_z, \theta)\) with full extents, shape \((N, 7)\).
-
boxes_b(Tensor) βBoxes in the same layout, shape \((M, 7)\).
Returns:
-
TensorβPairwise IoU in \([0, 1]\), shape \((N, M)\).
Shape
- boxes_a: \((N, 7)\)
- boxes_b: \((M, 7)\)
- output: \((N, M)\)
boxes_iou3d [source] ΒΆ
Pairwise oriented 3D box IoU.
The BEV intersection area (rotated rectangles, ignoring \(z\)) is multiplied by the vertical overlap of the height intervals \([c_z - d_z/2, c_z + d_z/2]\) to give the intersection volume, then divided by the union. The heading is counter-clockwise about \(+z\) from \(+x\); boxes are \((c_x, c_y, c_z, d_x, d_y, d_z, \theta)\) with full extents. Runs entirely in torch (CUDA-capable, no custom extension); the cost is \(O(N \cdot M)\).
Parameters:
-
boxes_a(Tensor) βBoxes \((c_x, c_y, c_z, d_x, d_y, d_z, \theta)\) with full extents, shape \((N, 7)\).
-
boxes_b(Tensor) βBoxes in the same layout, shape \((M, 7)\).
-
aligned(bool, default:False) βWith
True,boxes_aandboxes_bpair up row by row (\(N = M\)) and the result is \((N,)\).
Returns:
-
TensorβPairwise 3D IoU in \([0, 1]\), shape \((N, M)\).
Shape
- boxes_a: \((N, 7)\)
- boxes_b: \((M, 7)\)
- output: \((N, M)\)
nms3d [source] ΒΆ
nms3d(
boxes: Tensor,
scores: Tensor,
iou_threshold: float,
*,
labels: OptTensor = None,
batch: OptTensor = None,
rotated: bool = False,
max_keep: Optional[int] = None,
) -> Tensor
Greedy 3D non-maximum suppression.
Keeps the highest-scoring box of each overlapping cluster. By default the suppression criterion is the 3D IoU of the boxes' axis-aligned bounding boxes (cheap corner min / max); with rotated=True it is the exact rotated bird's-eye IoU of boxes_iou_bev (the KITTI outdoor protocol, where the axis-aligned surrogate over-suppresses angled neighbors at low thresholds). Pass labels to restrict suppression to boxes of the same class, and batch (PyG-style per-box scene index) to run NMS independently per scene and return a single index tensor over the concatenated input. The heading is counter-clockwise about \(+z\) from \(+x\); boxes are \((c_x, c_y, c_z, d_x, d_y, d_z, \theta)\) with full extents.
Parameters:
-
boxes(Tensor) βBoxes \((N, 7)\) (see
box_corners). -
scores(Tensor) βPer-box confidence, shape \((N,)\).
-
iou_threshold(float) βIoU above which a lower-scoring box is removed.
-
labels(OptTensor, default:None) βOptional per-box class, shape \((N,)\); when given, only same-class boxes suppress each other.
-
batch(OptTensor, default:None) βOptional per-box scene index, shape \((N,)\); when given, NMS runs independently per scene.
-
rotated(bool, default:False) βSuppress on the exact rotated BEV IoU (
boxes_iou_bev) instead of the axis-aligned 3D IoU. -
max_keep(Optional[int], default:None) βOptional cap on the boxes kept per scene; suppression stops once it is reached, so the kept set equals the first
max_keepentries of the uncapped result.
Returns:
-
TensorβIndices of the kept boxes (into the input), highest score first within each scene, shape \((K,)\) long.
Shape
- boxes: \((N, 7)\)
- output: \((K,)\)
points_in_boxes [source] ΒΆ
Test every point against every oriented box.
The point offsets to each box center are rotated into the box frame by \(-\theta\) about \(+z\) and compared with the half extents. The heading is counter-clockwise about \(+z\) from \(+x\); boxes are \((c_x, c_y, c_z, d_x, d_y, d_z, \theta)\) with full extents. Pairs are materialized at once, so restrict large scenes to their own boxes (or use count_points_in_boxes, which chunks).
Parameters:
-
pos(Tensor) βPoint coordinates, shape \((N, 3)\).
-
boxes(Tensor) βBoxes \((c_x, c_y, c_z, d_x, d_y, d_z, \theta)\) with full extents, shape \((K, 7)\).
Returns:
-
TensorβBoolean containment mask, shape \((N, K)\).
Shape
- pos: \((N, 3)\)
- boxes: \((K, 7)\)
- output: \((N, K)\)
count_points_in_boxes [source] ΒΆ
count_points_in_boxes(
pos: Tensor,
boxes: Tensor,
*,
pos_batch: OptTensor = None,
box_batch: OptTensor = None,
) -> Tensor
Count how many points fall inside each oriented box.
Pass pos_batch and box_batch (PyG-style per-point / per-box scene indices) to restrict each box's count to points from its own scene, so boxes of different scenes never share points. The heading is counter-clockwise about \(+z\) from \(+x\); boxes are \((c_x, c_y, c_z, d_x, d_y, d_z, \theta)\) with full extents.
Parameters:
-
pos(Tensor) βPoint coordinates, shape \((N, 3)\).
-
boxes(Tensor) βBoxes \((c_x, c_y, c_z, d_x, d_y, d_z, \theta)\) with full extents, shape \((K, 7)\).
-
pos_batch(OptTensor, default:None) βOptional per-point scene index, shape \((N,)\).
-
box_batch(OptTensor, default:None) βOptional per-box scene index, shape \((K,)\).
Returns:
-
TensorβPer-box point count, shape \((K,)\) long.
Shape
- pos: \((N, 3)\)
- boxes: \((K, 7)\)
- output: \((K,)\)
projected_ignore_mask [source] ΒΆ
projected_ignore_mask(
boxes: Tensor,
calib: Tensor,
image_shape: Tensor,
*,
min_height: float = 25.0,
) -> Tensor
Flag boxes whose image projection is shorter than min_height pixels (the KITTI difficulty rule).
Each box's 8 corners are projected through the \((3, 4)\) homogeneous LiDAR-to-image matrix (rows \(0\) and \(1\) divided by the perspective depth of row \(2\)), the vertical pixel coordinates are clipped to the image rows \([0, \text{height} - 1]\), and a box is flagged when its clipped pixel height is strictly below min_height. Only the vertical extent is used; the width entry of image_shape keeps the dataset's \((\text{height}, \text{width})\) contract. The KITTI protocol excludes such predictions from scoring (the prediction-side ignore_mask of box_matches), with min_height at \(40\) / \(25\) / \(25\) px for the easy / moderate / hard difficulties. For KITTI, compose the calib as \(P_2 \cdot [R_0 T_\text{velo}; 0\ 0\ 0\ 1]\) with the third row taken from \(R_0 T_\text{velo}\), so the perspective divide is by the rectified depth.
calib and image_shape broadcast on a leading box dimension: pass a single \((3, 4)\) / \((2,)\) frame for all boxes, or per-box \((N, 3, 4)\) / \((N, 2)\) rows (e.g. a stacked per-frame calib indexed by the boxes' scene index) to score a multi-frame batch in one call.
Parameters:
-
boxes(Tensor) βBoxes \((c_x, c_y, c_z, d_x, d_y, d_z, \theta)\) with full extents, shape \((N, 7)\).
-
calib(Tensor) βHomogeneous projection from LiDAR coordinates to image pixels, shape \((3, 4)\) or per-box \((N, 3, 4)\).
-
image_shape(Tensor) βImage \((\text{height}, \text{width})\) in pixels, shape \((2,)\) or per-box \((N, 2)\).
-
min_height(float, default:25.0) βPixel height below which a box is flagged.
Returns:
-
TensorβBoolean ignore mask, shape \((N,)\).
Shape
- boxes: \((N, 7)\)
- calib: \((3, 4)\) or \((N, 3, 4)\)
- image_shape: \((2,)\) or \((N, 2)\)
- output: \((N,)\)
Example
>>> calib = torch.tensor([[50.0, -100.0, 0.0, 0.0], [50.0, 0.0, -100.0, 0.0], [1.0, 0.0, 0.0, 0.0]])
>>> boxes = torch.tensor([[10.0, 0.0, 0.0, 2.0, 2.0, 1.0, 0.0]])
>>> projected_ignore_mask(boxes, calib, torch.tensor([100, 200]))
tensor([True])
>>> boxes = torch.tensor([[10.0, 0.0, 0.0, 2.0, 2.0, 1.0, 0.0], [2.0, 0.0, 0.0, 2.0, 2.0, 1.0, 0.0]])
>>> projected_ignore_mask(boxes, calib.expand(2, 3, 4), torch.tensor([[100, 200], [100, 200]]))
tensor([ True, False])