OrientedBox2D
Represents a single rotated 2D bounding box: a center, a size, and a rotation angle.
Parameters
| Field | Type | Description |
|---|---|---|
data | np.ndarray | list | tuple | Box coordinates [cx, cy, w, h, theta] (center, size, rotation angle in radians), convertible to a (5,) 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, 5) batch — use OrientedBoxes2D instead) |
ValueError | data does not have exactly 5 elements |
ValueError | Any element is non-finite (NaN/Inf) |
ValueError | w 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
| Attribute | Type | Description |
|---|---|---|
data | np.ndarray | Defensive copy of the underlying (5,) array [cx, cy, w, h, theta]. Reading it returns a copy; writing re-validates the same way as construction. |
shape | tuple[int, ...] | Always (5,). |
ndim | int | Always 1. |
dtype | np.dtype | Always float32. |
size | int | Always 5. |
shape_spec | tuple[int, ...] | Class-level shape spec (5,). |
width | np.ndarray (scalar) | data[2], before rotation. |
height | np.ndarray (scalar) | data[3], before rotation. |
area | np.ndarray (scalar) | width * height (rotation-invariant). |
center | np.ndarray | [cx, cy] — data[:2] directly (already the center; no offset computation, unlike axis-aligned boxes). |
theta | np.ndarray (scalar) | Rotation angle in radians, data[4]. |
Methods
| Method | Description |
|---|---|
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
| Operation | Behavior |
|---|---|
box == other | True 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 ValueError — OrientedBox2D 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 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()
