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). -
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_iou3d–Pairwise oriented 3D box IoU.
-
nms3d–Greedy 3D non-maximum suppression.
-
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
¶
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
¶
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
¶
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)\)
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.
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
¶
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)\).
Returns:
-
Tensor–Pairwise BEV IoU in \([0, 1]\), shape \((N, M)\).
Shape
- boxes_a: \((N, 7)\)
- boxes_b: \((M, 7)\)
- output: \((N, M)\)
boxes_iou3d
¶
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)\).
Returns:
-
Tensor–Pairwise 3D IoU in \([0, 1]\), shape \((N, M)\).
Shape
- boxes_a: \((N, 7)\)
- boxes_b: \((M, 7)\)
- output: \((N, M)\)
nms3d
¶
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,)\)
count_points_in_boxes
¶
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
¶
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 average_precision3d), 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])