Skip to content

OrientedBoxes3D

Represents a batch of rotated 3D bounding boxes: a center, a size, and an orientation quaternion per box.

Parameters

FieldTypeDescription
datanp.ndarray | list | tupleBatch 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

ExceptionCondition
TypeErrordata can't be converted into a uniform float32 array (e.g. ragged nested lists)
ValueErrordata is not rank-2 (e.g. a flat (10,) single box — wrap it as [[...]], or use OrientedBox3D)
ValueErrorThe last axis is not exactly length 10
ValueErrorAny element is non-finite (NaN/Inf)
ValueErrorAny box's w, h, or l (data[:, 3:6]) is negative
ValueErrorAny row's quaternion (data[:, 6:10]) has zero norm
ValueErrorAny 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

AttributeTypeDescription
datanp.ndarrayDefensive 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).
shapetuple[int, ...](N, 10).
ndimintAlways 2.
dtypenp.dtypeAlways float32.
sizeintN * 10.
shape_spectuple[int | None, ...]Class-level shape spec (None, 10)None means variable batch size.
single_clstypeOrientedBox3D — the per-item class returned by integer indexing.
widthnp.ndarray, shape (N,)Per-box width (data[:, 3]), before rotation.
heightnp.ndarray, shape (N,)Per-box height (data[:, 4]), before rotation.
depthnp.ndarray, shape (N,)Per-box depth (data[:, 5]), before rotation.
volumenp.ndarray, shape (N,)Per-box width * height * depth (rotation-invariant).
centernp.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_slicesliceClass-level slice slice(6, 10) locating each row's quaternion on the last axis.
quat_orderstr"xyzw" (scalar-last) — the project default; not overridden by this class.
quat_norm_atolfloat1e-3 — max allowed deviation of each quaternion's norm from 1.0.

Methods

MethodDescription
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

OperationBehavior
boxes == otherTrue 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 boxesIterates 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()