Skip to content

OrientedBoxes2D

Represents a batch of rotated 2D bounding boxes: a center, a size, and a rotation angle per box.

Parameters

FieldTypeDescription
datanp.ndarray | list | tupleBatch of box coordinates, each row [cx, cy, w, h, theta] (center, size, rotation angle in radians); shape (N, 5), 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 (5,) single box — wrap it as [[...]], or use OrientedBox2D)
ValueErrorThe last axis is not exactly length 5
ValueErrorAny element is non-finite (NaN/Inf)
ValueErrorAny box's w or h (data[:, 2], data[:, 3]) is negative

A zero-sized w/h on any box is allowed but logs a warning (that box's area will be 0). theta is unconstrained per box.

Attributes

AttributeTypeDescription
datanp.ndarrayDefensive copy of the underlying (N, 5) array. Reading it returns a copy; writing re-validates the same way as construction.
shapetuple[int, ...](N, 5).
ndimintAlways 2.
dtypenp.dtypeAlways float32.
sizeintN * 5.
shape_spectuple[int | None, ...]Class-level shape spec (None, 5)None means variable batch size.
single_clstypeOrientedBox2D — the per-item class returned by integer indexing.
widthnp.ndarray, shape (N,)Per-box width (data[:, 2]), before rotation.
heightnp.ndarray, shape (N,)Per-box height (data[:, 3]), before rotation.
areanp.ndarray, shape (N,)Per-box width * height (rotation-invariant).
centernp.ndarray, shape (N, 2)Per-box [cx, cy]data[:, :2] directly (already each center; no offset computation).
thetanp.ndarray, shape (N,)Per-box rotation angle in radians, data[:, 4].

Methods

MethodDescription
to_numpy(copy=True)Returns the (N, 5) array. Pass copy=False for a zero-copy view — mutating it mutates the OrientedBoxes2D.
copy()Returns a new OrientedBoxes2D with an independent data buffer.
translate(offset)Returns a new OrientedBoxes2D with every box's [cx, cy] shifted by offset ([dx, dy], applied to all boxes). Sizes and rotations are unchanged.
scale(factor)Returns a new OrientedBoxes2D with every box scaled around its own center. factor is a scalar (uniform) or a length-2 array-like ([fw, fh], per-axis, applied to all boxes). Rotations are unchanged; each center stays fixed.
rotate(rotation)Returns a new OrientedBoxes2D with rotation added to every box's theta. rotation is a scalar (added to all boxes) or a length-N array-like (one delta angle per box). Sizes and centers are unchanged.
OrientedBoxes2D.coerce(value)Classmethod. Returns value unchanged if it's already an OrientedBoxes2D; 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 OrientedBoxes2D with element-equal data (same N, same values). False for anything else.
len(boxes)Number of boxes, N.
boxes[i] (int)Returns an OrientedBox2D for row i. Negative indices count from the end; out-of-range raises IndexError.
boxes[i:j] (slice)Returns a new OrientedBoxes2D with the selected rows.
boxes[mask] (boolean np.ndarray)Returns a new OrientedBoxes2D with the rows where mask is True.
for box in boxesIterates via indexed access (no explicit __iter__); yields one OrientedBox2D 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 OrientedBoxes2D directly.
hash(boxes)Not supported — an OrientedBoxes2D can't be used as a dict key or set member.

Visualization

rr.Boxes2D has no rotation parameter, so datatypes.visualize(boxes, entity_path=...) draws each box in the batch as its own four rotated corners via rr.LineStrips2D, each carrying a "θ=<value>" text label, alongside a small reference frame and an "origin" text label logged to {entity_path}/origin. Passing label=[...] (one string per box) attaches an additional, independent floating text label per box, anchored at each box's center.

Example

python
"""Demonstrates the Telekinesis OrientedBoxes2D datatype."""

import time

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

from telekinesis import datatypes

def oriented_boxes2d_example():
    """Demonstrate creation, access, visualization, update, translate/rotate transforms, NumPy corner computation, area ranking, and serialization."""

    # ======================= Create ============================================
    box2d_1 = [0.5, 0.5, 0.5, 0.5, 0.5]
    box2d_2 = [1.0, 1.0, 1.0, 1.0, 0.25]
    boxes2d = datatypes.OrientedBoxes2D([box2d_1, box2d_2])

    logger.info(f"Original OrientedBoxes2D: {boxes2d}")

    # ======================= Inspect ===========================================
    data = boxes2d.data
    shape = boxes2d.shape
    dtype = boxes2d.dtype
    ndim = boxes2d.ndim
    numpy_boxes2d = boxes2d.to_numpy()
    center = boxes2d.center
    area = boxes2d.area
    width = boxes2d.width
    height = boxes2d.height
    theta = boxes2d.theta

    logger.info(f"shape={shape}, dtype={dtype}, ndim={ndim}")
    logger.info(f"Underlying data: {data}")
    logger.info(f"NumPy array: {numpy_boxes2d}")
    logger.info(
        f"center={center}, area={area}, width={width}, height={height}, theta={theta}"
    )

    # ======================= Visualize =========================================
    rr.init("oriented_box2d_example", spawn=True)
    datatypes.visualize(
        boxes2d,
        entity_path="/OrientedBox2D/my_oriented_boxes2d",
        label=["My Oriented Box2D 1", "My Oriented Box2D 2"],
    )

    # ======================= Update ============================================
    boxes2d.data = [
        [2.0, 2.0, 1.5, 1.0, 1.0],
        [3.0, 3.0, 2.0, 1.5, 0.5],
    ]
    logger.info(f"Updated OrientedBoxes2D: {boxes2d}")
    datatypes.visualize(
        boxes2d,
        entity_path="/OrientedBoxes2D/my_updated_oriented_box2d",
        label=["Updated Oriented Box2D 1", "Updated Oriented Box2D 2"],
    )

    # ======================= Translate =========================================
    translated = boxes2d.translate([3.0, 3.0])
    logger.info(f"Translated center: {translated.center} (was {boxes2d.center})")
    datatypes.visualize(
        translated,
        entity_path="/OrientedBoxes2D/my_translated_oriented_box2d",
        label=["Translated Oriented Box2D 1", "Translated Oriented Box2D 2"],
    )

    # ======================= Rotate ============================================
    rotated = boxes2d.rotate(0.25)
    logger.info(f"Rotated theta: {rotated.theta} (was {boxes2d.theta})")
    datatypes.visualize(
        rotated,
        entity_path="/OrientedBoxes2D/my_rotated_oriented_box2d",
        label=["Rotated Oriented Box2D 1", "Rotated Oriented Box2D 2"],
    )

    # ======================= NumPy Interop =====================================
    data = np.asarray(rotated)
    centers = data[:, :2]
    half_extents = data[:, 2:4] / 2
    angles = data[:, 4]

    corner_signs = np.array([[-1, -1], [1, -1], [1, 1], [-1, 1]], dtype=np.float32)
    local_corners = corner_signs[None, :, :] * half_extents[:, None, :]

    cos_t, sin_t = np.cos(angles), np.sin(angles)
    rotation_matrices = np.stack(
        [np.stack([cos_t, -sin_t], axis=-1), np.stack([sin_t, cos_t], axis=-1)], axis=1
    )

    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_w = np.linalg.norm(corners[:, 1] - corners[:, 0], axis=-1)
    edge_h = np.linalg.norm(corners[:, 2] - corners[:, 1], axis=-1)
    logger.info(f"Area from numpy corners: {edge_w * edge_h} (matches .area: {rotated.area})")

    # ======================= Rank by Area ======================================
    order = np.argsort(-rotated.area)
    largest_first = datatypes.OrientedBoxes2D(rotated.data[order])
    logger.info(
        f"Boxes ranked by area (largest first): {largest_first.area} (order: {order.tolist()})"
    )

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


if __name__ == "__main__":
    oriented_boxes2d_example()