Skip to content

Box2D

Represents a single axis-aligned 2D bounding box: a minimum corner plus a width and height.

Parameters

FieldTypeDescription
datanp.ndarray | list | tupleBox coordinates [x, y, width, height] (min corner + size), convertible to a (4,) 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, 4) batch — use Boxes2D instead)
ValueErrordata does not have exactly 4 elements
ValueErrorAny element is non-finite (NaN/Inf)
ValueErrorwidth or height (data[2], data[3]) is negative

A zero-sized width/height is allowed but logs a warning (area will be 0).

Attributes

AttributeTypeDescription
datanp.ndarrayDefensive copy of the underlying (4,) array [x, y, width, height]. Reading it returns a copy; writing re-validates the same way as construction.
shapetuple[int, ...]Always (4,).
ndimintAlways 1.
dtypenp.dtypeAlways float32.
sizeintAlways 4.
shape_spectuple[int, ...]Class-level shape spec (4,).
widthnp.ndarray (scalar)data[2].
heightnp.ndarray (scalar)data[3].
areanp.ndarray (scalar)width * height.
centernp.ndarray[cx, cy], computed as the min corner (data[:2]) plus half the size — data[:2] itself is the min corner, not the center.

Methods

MethodDescription
to_numpy(copy=True)Returns the (4,) array. Pass copy=False for a zero-copy view — mutating it mutates the Box2D.
copy()Returns a new Box2D with an independent data buffer.
translate(offset)Returns a new Box2D with [x, y] shifted by offset ([dx, dy]). Size is unchanged.
scale(factor)Returns a new Box2D scaled around its center. factor is a scalar (uniform) or a length-2 array-like ([fw, fh], per-axis). The min corner is recomputed so the center stays fixed.
convert_box_format(target_format)Returns this box re-expressed as a (4,) array in target_format: "xywh" (native), "xyxy" (min corner, max corner), or "cxcywh" (center + size).
Box2D.from_format(data, source_format)Classmethod. Inverse of convert_box_format — builds a Box2D from data already given in source_format ("xywh"/"xyxy"/"cxcywh").
Box2D.coerce(value)Classmethod. Returns value unchanged if it's already a Box2D; 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 a Box2D with element-equal data. False for anything else.
len(box)Returns 4 — the length of the coordinate vector, not a box count (a single box is conceptually one item).
box[i]Always raises ValueErrorBox2D 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 a Box2D directly.
hash(box)Not supported — a Box2D can't be used as a dict key or set member.

Visualization

datatypes.visualize(box2d, entity_path=...) logs it as a single rr.Boxes2D(mins=[[x, y]], sizes=[[w, h]]), alongside a small reference frame and an "origin" text label logged to {entity_path}/origin. Passing label="..." attaches a floating text label anchored at the box's mins vertex ([x, y]).

Example

python
"""Demonstrates the Telekinesis Box2D datatype."""

import time

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

from telekinesis import datatypes

def box2d_example():
    """Demonstrate creation, access, update, translation, scaling, NumPy interop, and serialization."""

    # ======================= Create ============================================
    coords = [1, 2.5, 3, 3]
    box2d = datatypes.Box2D(coords)
    logger.info(f"Original Box2D: {box2d}")

    # ======================= Inspect ===========================================
    logger.info(f"Box2D data: {box2d.data}")
    logger.info(
        f"shape={box2d.shape}, "
        f"width={box2d.width}, "
        f"height={box2d.height}, "
        f"area={box2d.area}, "
        f"center={box2d.center}"
    )

    # ======================= Visualize =========================================
    rr.init("box2d_example", spawn=True)
    datatypes.visualize(box2d, entity_path="/Box2D/my_box2d", label="Original Box2D")

    # ======================= Update ============================================
    updated_coords = [3, 4, 3, 5]
    box2d.data = updated_coords
    logger.info(f"Updated Box2D: {box2d}")
    datatypes.visualize(box2d, entity_path="/Box2D/my_updated_box2d", label="Updated Box2D")

    # ======================= Translate =========================================
    translation = [2, 3]
    translated_box2d = box2d.translate(translation)
    logger.info(f"Translated Box2D: {translated_box2d}")
    datatypes.visualize(
        translated_box2d, entity_path="/Box2D/my_translated_box2d", label="Translated Box2D"
    )

    # ======================= Scale =============================================
    scale_factors = [2, 0.5]
    scaled_box2d = box2d.scale(scale_factors)
    logger.info(f"Scaled Box2D: {scaled_box2d}")
    datatypes.visualize(scaled_box2d, entity_path="/Box2D/my_scaled_box2d", label="Scaled Box2D")

    # ======================= NumPy Interop =====================================
    box2d_xyxy = box2d.convert_box_format(target_format="xyxy")
    scaled_xyxy = scaled_box2d.convert_box_format(target_format="xyxy")
    inter_min = np.maximum(box2d_xyxy[:2], scaled_xyxy.data[:2])
    inter_max = np.minimum(box2d_xyxy[2:], scaled_xyxy.data[2:])
    inter_wh = np.clip(inter_max - inter_min, 0, None)
    intersection = inter_wh[0] * inter_wh[1]
    union = box2d.area + scaled_box2d.area - intersection
    iou = intersection / union if union > 0 else 0.0
    logger.info(f"IoU between box2d and scaled_box2d: {iou}")

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


if __name__ == "__main__":
    box2d_example()