Skip to content

OrientedBox2D

Represents a single rotated 2D bounding box: a center, a size, and a rotation angle.

Parameters

FieldTypeDescription
datanp.ndarray | list | tupleBox coordinates [cx, cy, w, h, theta] (center, size, rotation angle in radians), convertible to a (5,) 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, 5) batch — use OrientedBoxes2D instead)
ValueErrordata does not have exactly 5 elements
ValueErrorAny element is non-finite (NaN/Inf)
ValueErrorw or h (data[2], data[3]) is negative

A zero-sized w/h is allowed but logs a warning (area will be 0). theta is unconstrained (any finite radian value, including outside [0, 2π)).

Attributes

AttributeTypeDescription
datanp.ndarrayDefensive copy of the underlying (5,) array [cx, cy, w, h, theta]. Reading it returns a copy; writing re-validates the same way as construction.
shapetuple[int, ...]Always (5,).
ndimintAlways 1.
dtypenp.dtypeAlways float32.
sizeintAlways 5.
shape_spectuple[int, ...]Class-level shape spec (5,).
widthnp.ndarray (scalar)data[2], before rotation.
heightnp.ndarray (scalar)data[3], before rotation.
areanp.ndarray (scalar)width * height (rotation-invariant).
centernp.ndarray[cx, cy]data[:2] directly (already the center; no offset computation, unlike axis-aligned boxes).
thetanp.ndarray (scalar)Rotation angle in radians, data[4].

Methods

MethodDescription
to_numpy(copy=True)Returns the (5,) array. Pass copy=False for a zero-copy view — mutating it mutates the OrientedBox2D.
copy()Returns a new OrientedBox2D with an independent data buffer.
translate(offset)Returns a new OrientedBox2D with [cx, cy] shifted by offset ([dx, dy]). Size and rotation are unchanged.
scale(factor)Returns a new OrientedBox2D scaled around its center. factor is a scalar (uniform) or a length-2 array-like ([fw, fh], per-axis). Rotation is unchanged; the center stays fixed.
rotate(rotation)Returns a new OrientedBox2D with rotation (a scalar delta angle, radians) added to theta. Size and center are unchanged.
OrientedBox2D.coerce(value)Classmethod. Returns value unchanged if it's already an OrientedBox2D; 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 OrientedBox2D with element-equal data. False for anything else.
len(box)Returns 5 — the length of the coordinate vector, not a box count (a single box is conceptually one item).
box[i]Always raises ValueErrorOrientedBox2D 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 OrientedBox2D directly.
hash(box)Not supported — an OrientedBox2D can't be used as a dict key or set member.

Visualization

rr.Boxes2D has no rotation parameter, so datatypes.visualize(box, entity_path=...) draws an OrientedBox2D as its four rotated corners via rr.LineStrips2D, with the rotation angle attached to the polygon as a "θ=<value>" text label, alongside a small reference frame and an "origin" text label logged to {entity_path}/origin. Passing label="..." attaches an additional, independent floating text label anchored at the box's center — both the θ= label and the custom label are visible at once.

Example

python
"""Demonstrates the Telekinesis OrientedBox2D datatype."""

import time

import numpy as np
from loguru import logger
import rerun as rr

from telekinesis import datatypes

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

    # ======================= Create ============================================
    box = datatypes.OrientedBox2D([0.5, 0.5, 0.5, 0.5, 0.5])

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

    # ======================= Visualize =========================================
    rr.init("oriented_box2d_example", spawn=True)
    datatypes.visualize(
        box, entity_path="/OrientedBox2D/my_oriented_box2d", label="My Oriented Box2D"
    )

    # ======================= 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}, area={box.area}, width={box.width}, "
        f"height={box.height}, theta={box.theta}"
    )

    # ======================= Update ============================================
    box.data = [2.0, 2.0, 1.5, 1.0, 1.0]

    logger.info(f"Updated OrientedBox2D: {box}")
    datatypes.visualize(
        box, entity_path="/OrientedBox2D/my_updated_oriented_box2d", label="Updated Oriented Box2D"
    )

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

    logger.info(f"Translated center: {translated_box.center} (was {box.center})")
    datatypes.visualize(
        translated_box,
        entity_path="/OrientedBox2D/my_translated_oriented_box2d",
        label="Translated Oriented Box2D",
    )

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

    logger.info(
        f"Scaled width and height: {scaled_box.width} x {scaled_box.height} "
        f"(was {box.width} x {box.height})"
    )
    datatypes.visualize(
        scaled_box, entity_path="/OrientedBox2D/my_scaled_oriented_box2d", label="Scaled Oriented Box2D"
    )

    # ======================= Rotate ============================================
    rotated_box = box.rotate(0.25)

    logger.info(f"Rotated theta: {rotated_box.theta} (was {box.theta})")
    datatypes.visualize(
        rotated_box,
        entity_path="/OrientedBox2D/my_rotated_oriented_box2d",
        label="Rotated Oriented Box2D",
    )

    # ======================= NumPy Interop =====================================
    cx, cy, w, h, theta = np.asarray(rotated_box)
    local_corners = np.array(
        [[-w / 2, -h / 2], [w / 2, -h / 2], [w / 2, h / 2], [-w / 2, h / 2]], dtype=np.float32
    )
    cos_t, sin_t = np.cos(theta), np.sin(theta)
    rotation_matrix = np.array([[cos_t, -sin_t], [sin_t, cos_t]], dtype=np.float32)
    corners = local_corners @ rotation_matrix.T + np.array([cx, cy], dtype=np.float32)

    logger.info(f"Corners (world space, [x, y] per row):\n{corners}")

    edge_w = np.linalg.norm(corners[1] - corners[0])
    edge_h = np.linalg.norm(corners[2] - corners[1])

    logger.info(f"Area from corners: {edge_w * edge_h} (matches .area: {rotated_box.area})")

    # ======================= 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 OrientedBox2D: {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_box2d_example()