Box2D
Represents a single axis-aligned 2D bounding box: a minimum corner plus a width and height.
Parameters
| Field | Type | Description |
|---|---|---|
data | np.ndarray | list | tuple | Box coordinates [x, y, width, height] (min corner + size), convertible to a (4,) float32 array via np.asarray. |
Raises
| Exception | Condition |
|---|---|
TypeError | data can't be converted into a uniform float32 array (e.g. ragged nested lists) |
ValueError | data is not rank-1 (e.g. a (N, 4) batch — use Boxes2D instead) |
ValueError | data does not have exactly 4 elements |
ValueError | Any element is non-finite (NaN/Inf) |
ValueError | width or height (data[2], data[3]) is negative |
A zero-sized width/height is allowed but logs a warning (area will be 0).
Attributes
| Attribute | Type | Description |
|---|---|---|
data | np.ndarray | Defensive copy of the underlying (4,) array [x, y, width, height]. Reading it returns a copy; writing re-validates the same way as construction. |
shape | tuple[int, ...] | Always (4,). |
ndim | int | Always 1. |
dtype | np.dtype | Always float32. |
size | int | Always 4. |
shape_spec | tuple[int, ...] | Class-level shape spec (4,). |
width | np.ndarray (scalar) | data[2]. |
height | np.ndarray (scalar) | data[3]. |
area | np.ndarray (scalar) | width * height. |
center | np.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
| Method | Description |
|---|---|
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
| Operation | Behavior |
|---|---|
box == other | True 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 ValueError — Box2D has no single_cls, so it isn't indexable/iterable like a batch. Use .data[i] for raw coordinate access instead. |
for x in box | Raises 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()
