Skip to content

OrientedBox3D

Represents a single rotated 3D bounding box: a center, a size, and an orientation quaternion.

Parameters

FieldTypeDescription
datanp.ndarray | list | tupleBox coordinates [x, y, z, w, h, l, qx, qy, qz, qw] (center, size, unit quaternion scalar-last), convertible to a (10,) float32 array via np.asarray.

Raises

ExceptionCondition
TypeErrordata can't be converted into a uniform float32 array (e.g. ragged nested lists)
ValueErrordata is not rank-1 (e.g. a (N, 10) batch — use OrientedBoxes3D instead)
ValueErrordata does not have exactly 10 elements
ValueErrorAny element is non-finite (NaN/Inf)
ValueErrorw, h, or l (data[3:6]) is negative
ValueErrorThe quaternion (data[6:10]) has zero norm
ValueErrorThe quaternion's norm deviates from 1.0 by more than 1e-3 (quat_norm_atol)

A zero-sized w/h/l is allowed but logs a warning (volume will be 0).

Attributes

AttributeTypeDescription
datanp.ndarrayDefensive copy of the underlying (10,) array. Reading it returns a copy; writing re-validates the same way as construction (including the quaternion unit-norm check).
shapetuple[int, ...]Always (10,).
ndimintAlways 1.
dtypenp.dtypeAlways float32.
sizeintAlways 10.
shape_spectuple[int, ...]Class-level shape spec (10,).
widthnp.ndarray (scalar)data[3], before rotation.
heightnp.ndarray (scalar)data[4], before rotation.
depthnp.ndarray (scalar)data[5], before rotation.
volumenp.ndarray (scalar)width * height * depth (rotation-invariant).
centernp.ndarray[cx, cy, cz]data[:3] directly (already the center; no offset computation, unlike axis-aligned boxes).
theta(not available)Inherited property; accessing it raises ValueError (OrientedBox3D stores rotation as a quaternion, not an angle — read data[6:10] instead).
quat_slicesliceClass-level slice slice(6, 10) locating the 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 the quaternion's norm from 1.0.

Methods

MethodDescription
to_numpy(copy=True)Returns the (10,) array. Pass copy=False for a zero-copy view — mutating it mutates the OrientedBox3D.
copy()Returns a new OrientedBox3D with an independent data buffer.
translate(offset)Returns a new OrientedBox3D with [x, y, z] shifted by offset ([dx, dy, dz]). Size and rotation are unchanged.
scale(factor)Returns a new OrientedBox3D scaled around its center. factor is a scalar (uniform) or a length-3 array-like ([fw, fh, fl], per-axis). Rotation is unchanged; the center stays fixed.
rotate(rotation)Returns a new OrientedBox3D composing rotation (a quaternion array-like of shape (4,), in quat_order) with the existing orientation as a world-frame delta (rotation applied on top of the existing rotation, via scipy.spatial.transform.Rotation). Size and center are unchanged.
OrientedBox3D.to_xyzw(q)Classmethod. Converts a quaternion 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".
OrientedBox3D.from_xyzw(q)Classmethod. Converts a scalar-last quaternion to quat_order. Also a no-op for this class, for the same reason.
OrientedBox3D.coerce(value)Classmethod. Returns value unchanged if it's already an OrientedBox3D; otherwise wraps an array-like into one (same validation as the constructor). Raises TypeError for any other input.

Operators

OperationBehavior
box == otherTrue only if other is also an OrientedBox3D with element-equal data. False for anything else.
len(box)Returns 10 — the length of the coordinate vector, not a box count (a single box is conceptually one item).
box[i]Always raises ValueErrorOrientedBox3D has no single_cls, so it isn't indexable/iterable like a batch. Use .data[i] for raw coordinate access instead.
for x in boxRaises ValueError immediately (falls back to box[0], which raises).
np.asarray(box)Returns a copy of data as an np.ndarray; NumPy functions accept an OrientedBox3D directly.
hash(box)Not supported — an OrientedBox3D can't be used as a dict key or set member.

Visualization

datatypes.visualize(box, entity_path=...) logs it as a single rr.Boxes3D(centers=[data[:3]], sizes=[data[3:6]], quaternions=[data[6:10]])rr.Boxes3D natively supports rotation, so no separate angle label is auto-attached (unlike OrientedBox2D) — alongside a small 3D reference frame and an "origin" text label logged to {entity_path}/origin. Passing label="..." attaches a floating text label anchored at the box's center.

Example

python
"""Demonstrates the Telekinesis OrientedBox3D datatype."""

import time

from loguru import logger
import rerun as rr

from telekinesis import datatypes

def oriented_box3d_example():
    """Demonstrate creation, access, visualization, translate/scale/rotate, and serialization."""

    # ======================= Create ============================================
    box = datatypes.OrientedBox3D([0.5, 0.5, 0.5, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0])

    logger.info(f"Created OrientedBox3D: {box}")

    # ======================= Visualize =========================================
    rr.init("oriented_box3d_example", spawn=True)
    datatypes.visualize(
        box, entity_path="/OrientedBox3D/my_oriented_box3d", label="My Oriented Box3D"
    )

    # ======================= Inspect ===========================================
    logger.info(f"shape={box.shape}, dtype={box.dtype}, ndim={box.ndim}")
    logger.info(f"NumPy array: {box.to_numpy()}")
    logger.info(
        f"center={box.center}, volume={box.volume}, width={box.width}, "
        f"height={box.height}, depth={box.depth}"
    )
    logger.info(f"Quaternion [qx, qy, qz, qw]: {box.data[6:]}")

    # ======================= Update ============================================
    box.data = [2.0, 2.0, 2.0, 3.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0]

    logger.info(f"Updated OrientedBox3D: {box}")
    datatypes.visualize(
        box, entity_path="/OrientedBox3D/my_updated_oriented_box3d", label="Updated Oriented Box3D"
    )

    # ======================= Translate =========================================
    translated_box = box.translate([1.0, 1.0, 1.0])

    logger.info(f"Translated center: {translated_box.center} (was {box.center})")
    datatypes.visualize(
        translated_box,
        entity_path="/OrientedBox3D/my_translated_oriented_box3d",
        label="Translated Oriented Box3D",
    )

    # ======================= Scale =============================================
    scaled_box = box.scale(1.5)

    logger.info(
        f"Scaled width, height, and depth: {scaled_box.width} x {scaled_box.height} x "
        f"{scaled_box.depth} (was {box.width} x {box.height} x {box.depth})"
    )
    datatypes.visualize(
        scaled_box, entity_path="/OrientedBox3D/my_scaled_oriented_box3d", label="Scaled Oriented Box3D"
    )

    # ======================= Rotate ============================================
    delta_quaternion = [0.0, 0.0, 0.70710678, 0.70710678]
    rotated_box = box.rotate(delta_quaternion)

    logger.info(f"Rotated quaternion: {rotated_box.data[6:]} (was {box.data[6:]})")
    rotated_box_display = rotated_box.translate([4.0, 0.0, 0.0])
    datatypes.visualize(
        rotated_box_display,
        entity_path="/OrientedBox3D/my_rotated_oriented_box3d",
        label="Rotated Oriented Box3D",
    )

    # ======================= Serialize / Deserialize ===========================
    start = time.perf_counter()
    serialized = datatypes.serialize(box)
    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 OrientedBox3D: {deserialized}")
    logger.info(f"Round-trip successful: {deserialized == box}")
    logger.info(f"Serialization time: {serialization_ms:.3f} ms")
    logger.info(f"Deserialization time: {deserialization_ms:.3f} ms")


if __name__ == "__main__":
    oriented_box3d_example()