OrientedBoxes3D
Represents a batch of rotated 3D bounding boxes: a center, a size, and an orientation quaternion per box.
Parameters
| Field | Type | Description |
|---|---|---|
data | np.ndarray | list | tuple | Batch of box coordinates, each row [x, y, z, w, h, l, qx, qy, qz, qw] (center, size, unit quaternion scalar-last); shape (N, 10), convertible to float32 via np.asarray. |
Raises
| Exception | Condition |
|---|---|
TypeError | data can't be converted into a uniform float32 array (e.g. ragged nested lists) |
ValueError | data is not rank-2 (e.g. a flat (10,) single box — wrap it as [[...]], or use OrientedBox3D) |
ValueError | The last axis is not exactly length 10 |
ValueError | Any element is non-finite (NaN/Inf) |
ValueError | Any box's w, h, or l (data[:, 3:6]) is negative |
ValueError | Any row's quaternion (data[:, 6:10]) has zero norm |
ValueError | Any row's quaternion norm deviates from 1.0 by more than 1e-3 (quat_norm_atol) |
A zero-sized w/h/l on any box is allowed but logs a warning (that box's volume will be 0).
Attributes
| Attribute | Type | Description |
|---|---|---|
data | np.ndarray | Defensive copy of the underlying (N, 10) array. Reading it returns a copy; writing re-validates the same way as construction (including per-row quaternion unit-norm checks). |
shape | tuple[int, ...] | (N, 10). |
ndim | int | Always 2. |
dtype | np.dtype | Always float32. |
size | int | N * 10. |
shape_spec | tuple[int | None, ...] | Class-level shape spec (None, 10) — None means variable batch size. |
single_cls | type | OrientedBox3D — the per-item class returned by integer indexing. |
width | np.ndarray, shape (N,) | Per-box width (data[:, 3]), before rotation. |
height | np.ndarray, shape (N,) | Per-box height (data[:, 4]), before rotation. |
depth | np.ndarray, shape (N,) | Per-box depth (data[:, 5]), before rotation. |
volume | np.ndarray, shape (N,) | Per-box width * height * depth (rotation-invariant). |
center | np.ndarray, shape (N, 3) | Per-box [cx, cy, cz] — data[:, :3] directly (already each center; no offset computation). |
theta | (not available) | Inherited property; accessing it raises ValueError (OrientedBoxes3D stores rotation as per-row quaternions, not angles — read data[:, 6:10] instead). |
quat_slice | slice | Class-level slice slice(6, 10) locating each row's quaternion on the last axis. |
quat_order | str | "xyzw" (scalar-last) — the project default; not overridden by this class. |
quat_norm_atol | float | 1e-3 — max allowed deviation of each quaternion's norm from 1.0. |
Methods
| Method | Description |
|---|---|
to_numpy(copy=True) | Returns the (N, 10) array. Pass copy=False for a zero-copy view — mutating it mutates the OrientedBoxes3D. |
copy() | Returns a new OrientedBoxes3D with an independent data buffer. |
translate(offset) | Returns a new OrientedBoxes3D with every box's [x, y, z] shifted by offset ([dx, dy, dz], applied to all boxes). Sizes and rotations are unchanged. |
scale(factor) | Returns a new OrientedBoxes3D with every box scaled around its own center. factor is a scalar (uniform) or a length-3 array-like ([fw, fh, fl], per-axis, applied to all boxes). Rotations are unchanged; each center stays fixed. |
rotate(rotation) | Returns a new OrientedBoxes3D composing rotation with every box's existing orientation as a world-frame delta, via scipy.spatial.transform.Rotation. rotation is a single quaternion (4,) (applied to all boxes) or a batch (N, 4) (one delta per box), in quat_order. Sizes and centers are unchanged. |
OrientedBoxes3D.to_xyzw(q) | Classmethod. Converts quaternion(s) from quat_order to scalar-last [x, y, z, w]. Since quat_order is already "xyzw" for this class, this is a no-op (returns q unchanged) — provided for API symmetry with subclasses that might use "wxyz". |
OrientedBoxes3D.from_xyzw(q) | Classmethod. Converts scalar-last quaternion(s) to quat_order. Also a no-op for this class, for the same reason. |
OrientedBoxes3D.coerce(value) | Classmethod. Returns value unchanged if it's already an OrientedBoxes3D; otherwise wraps an array-like into one (same validation as the constructor). Raises TypeError for any other input. |
Operators
| Operation | Behavior |
|---|---|
boxes == other | True only if other is also an OrientedBoxes3D with element-equal data (same N, same values). False for anything else. |
len(boxes) | Number of boxes, N. |
boxes[i] (int) | Returns an OrientedBox3D for row i. Negative indices count from the end; out-of-range raises IndexError. |
boxes[i:j] (slice) | Returns a new OrientedBoxes3D with the selected rows. |
boxes[mask] (boolean np.ndarray) | Returns a new OrientedBoxes3D with the rows where mask is True. |
for box in boxes | Iterates via indexed access (no explicit __iter__); yields one OrientedBox3D per row, stopping at the IndexError from an out-of-range index. |
np.asarray(boxes) | Returns a copy of data as an np.ndarray; NumPy functions accept an OrientedBoxes3D directly. |
hash(boxes) | Not supported — an OrientedBoxes3D can't be used as a dict key or set member. |
Visualization
datatypes.visualize(boxes, entity_path=...) logs the whole batch as one rr.Boxes3D(centers=data[:, :3], sizes=data[:, 3:6], quaternions=data[:, 6:10]) — rr.Boxes3D natively supports rotation, so no separate angle labels are auto-attached (unlike OrientedBoxes2D) — alongside a small 3D reference frame and an "origin" text label logged to {entity_path}/origin. Passing label=[...] (one string per box) attaches a floating text label per box, anchored at each box's center.
Example
python
"""Demonstrates the Telekinesis OrientedBoxes3D datatype."""
import itertools
import time
import numpy as np
from scipy.spatial.transform import Rotation
from loguru import logger
import rerun as rr
from telekinesis import datatypes
def oriented_boxes3d_example():
"""Demonstrate creation, access, visualization, update, translate/scale/rotate transforms, NumPy corner computation, volume ranking, and serialization."""
# ======================= Create ============================================
box3d_1 = [0.5, 0.5, 0.5, 1.0, 1.0, 1.0, 0.0, 0.0, 0.258819, 0.965926]
box3d_2 = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.382683, 0.923880]
boxes3d = datatypes.OrientedBoxes3D([box3d_1, box3d_2])
logger.info(f"Original OrientedBoxes3D: {boxes3d}")
# ======================= Inspect ===========================================
data = boxes3d.data
shape = boxes3d.shape
dtype = boxes3d.dtype
ndim = boxes3d.ndim
numpy_boxes3d = boxes3d.to_numpy()
center = boxes3d.center
volume = boxes3d.volume
width = boxes3d.width
height = boxes3d.height
depth = boxes3d.depth
quaternion = boxes3d.data[:, 6:10]
logger.info(f"shape={shape}, dtype={dtype}, ndim={ndim}")
logger.info(f"Underlying data: {data}")
logger.info(f"NumPy array: {numpy_boxes3d}")
logger.info(
f"center={center}, volume={volume}, width={width}, height={height}, depth={depth}"
)
logger.info(f"Quaternion: {quaternion}")
# ======================= Visualize =========================================
rr.init("oriented_box3d_example", spawn=True)
datatypes.visualize(
boxes3d,
entity_path="/OrientedBox3D/my_oriented_boxes3d",
label=["My Oriented Box3D 1", "My Oriented Box3D 2"],
)
# ======================= Update ============================================
updated_data = boxes3d.data
updated_data[0] = [2.0, 2.0, 2.0, 1.5, 1.0, 1.0, 0.0, 0.173648, 0.0, 0.984808]
boxes3d.data = updated_data
logger.info(f"Updated OrientedBoxes3D: {boxes3d}")
datatypes.visualize(
boxes3d,
entity_path="/OrientedBoxes3D/my_updated_oriented_box3d",
label=["Updated Oriented Box3D 1", "Updated Oriented Box3D 2"],
)
# ======================= Translate =========================================
translated = boxes3d.translate([1.0, 1.0, 1.0])
logger.info(f"Translated center: {translated.center} (was {boxes3d.center})")
datatypes.visualize(
translated,
entity_path="/OrientedBoxes3D/my_translated_oriented_box3d",
label=["Translated Oriented Box3D 1", "Translated Oriented Box3D 2"],
)
# ======================= Scale =============================================
scaled = boxes3d.scale(1.5)
logger.info(
f"Scaled width, height, depth: {scaled.width} x {scaled.height} x {scaled.depth} "
f"(was {boxes3d.width} x {boxes3d.height} x {boxes3d.depth})"
)
datatypes.visualize(
scaled,
entity_path="/OrientedBoxes3D/my_scaled_oriented_box3d",
label=["Scaled Oriented Box3D 1", "Scaled Oriented Box3D 2"],
)
# ======================= Rotate ============================================
delta_quat = [0.130526, 0.0, 0.0, 0.991445]
rotated = boxes3d.rotate(delta_quat)
logger.info(
f"Rotated quaternion: {rotated.data[:, 6:10]} (was {boxes3d.data[:, 6:10]})"
)
datatypes.visualize(
rotated,
entity_path="/OrientedBoxes3D/my_rotated_oriented_box3d",
label=["Rotated Oriented Box3D 1", "Rotated Oriented Box3D 2"],
)
# ======================= NumPy Interop =====================================
data = np.asarray(rotated)
centers = data[:, :3]
half_extents = data[:, 3:6] / 2
quats_xyzw = data[:, 6:10]
corner_signs = np.array(list(itertools.product([-1, 1], repeat=3)), dtype=np.float32)
local_corners = corner_signs[None, :, :] * half_extents[:, None, :]
rotation_matrices = Rotation.from_quat(quats_xyzw).as_matrix().astype(np.float32)
corners = local_corners @ rotation_matrices.transpose(0, 2, 1) + centers[:, None, :]
logger.info(f"Corners per box, world space, shape {corners.shape}:\n{corners}")
edge_d = np.linalg.norm(corners[:, 1] - corners[:, 0], axis=-1)
edge_h = np.linalg.norm(corners[:, 2] - corners[:, 0], axis=-1)
edge_w = np.linalg.norm(corners[:, 4] - corners[:, 0], axis=-1)
logger.info(
f"Volume from numpy corners: {edge_w * edge_h * edge_d} (matches .volume: {rotated.volume})"
)
# ======================= Rank by Volume ====================================
order = np.argsort(-rotated.volume)
largest_first = datatypes.OrientedBoxes3D(rotated.data[order])
logger.info(
f"Boxes ranked by volume (largest first): {largest_first.volume} (order: {order.tolist()})"
)
# ======================= Serialize / Deserialize ===========================
start = time.perf_counter()
serialized = datatypes.serialize(boxes3d)
serialization_ms = (time.perf_counter() - start) * 1000
start = time.perf_counter()
deserialized = datatypes.deserialize(serialized)["param_0"]
deserialization_ms = (time.perf_counter() - start) * 1000
logger.info(f"Deserialized OrientedBoxes3D: {deserialized}")
logger.info(f"Round-trip successful: {boxes3d == deserialized}")
logger.info(f"Serialization time: {serialization_ms:.3f} ms")
logger.info(f"Deserialization time: {deserialization_ms:.3f} ms")
if __name__ == "__main__":
oriented_boxes3d_example()
