Python PointCloud

A 3D point cloud backed by shared memory for zero-copy inter-process communication. Supports XYZ, XYZI (intensity), and XYZRGB (color) point formats with direct conversion to NumPy, PyTorch, and JAX.

When to Use

Use PointCloud when your robot has a LiDAR, depth camera, or stereo vision system producing 3D point data. A 100K-point cloud transfers between nodes in microseconds via shared memory — no serialization.

ROS2 equivalent: sensor_msgs/PointCloud2 — same concept, but HORUS uses shared memory pools instead of serialized byte arrays.

Constructor

from horus import PointCloud

# PointCloud(num_points, fields, dtype)
cloud = PointCloud(num_points=10000, fields=3, dtype="float32")

Parameters:

  • num_points: int — Number of points to allocate
  • fields: int — Floats per point: 3 = XYZ, 4 = XYZI, 6 = XYZRGB (default: 3)
  • dtype: str — Data type (default: "float32")

Factory Methods

# From NumPy — shape (N, F) where F = fields per point
import numpy as np
points = np.random.randn(10000, 3).astype(np.float32)
cloud = PointCloud.from_numpy(points)

# From PyTorch tensor
import torch
tensor = torch.randn(10000, 3)
cloud = PointCloud.from_torch(tensor)
FactoryParametersUse case
PointCloud(n, f, dtype)count, fields, dtypePre-allocate for filling
PointCloud.from_numpy(arr)ndarray shape (N, F)LiDAR driver output
PointCloud.from_torch(tensor)Tensor shape (N, F)ML model output

Properties

PropertyTypeDescription
point_countintNumber of points
fields_per_pointintFloats per point (3, 4, or 6)
dtypestrData type string (e.g., "float32")
nbytesintTotal data size in bytes
frame_idstrSensor coordinate frame (e.g., "lidar_front")
timestamp_nsintTimestamp in nanoseconds

Point Format Queries

cloud.is_xyz()          # True if 3 fields (XYZ only)
cloud.has_intensity()   # True if 4+ fields (XYZI)
cloud.has_color()       # True if 6+ fields (XYZRGB)

Methods

Point Access

# Get i-th point as list of floats
point = cloud.point_at(0)  # e.g., [1.0, 2.0, 3.0]

Framework Conversions

# To NumPy — zero-copy, shape (N, F)
np_points = cloud.to_numpy()

# To PyTorch — zero-copy via DLPack
torch_points = cloud.to_torch()

# To JAX — zero-copy via DLPack
jax_points = cloud.to_jax()

Metadata

cloud.set_frame_id("lidar_front")
cloud.set_timestamp_ns(horus.timestamp_ns())

Complete Example

import horus
from horus import PointCloud, Topic
import numpy as np

scan_topic = Topic(PointCloud)

def lidar_tick(node):
    # Simulate LiDAR scan (10000 XYZ points)
    points = np.random.randn(10000, 3).astype(np.float32)
    cloud = PointCloud.from_numpy(points)
    cloud.set_frame_id("lidar_front")
    scan_topic.send(cloud)

def obstacle_tick(node):
    cloud = scan_topic.recv()
    if cloud:
        pts = cloud.to_numpy()  # Zero-copy (N, 3)
        # Find points within 2m
        distances = np.linalg.norm(pts, axis=1)
        nearby = np.sum(distances < 2.0)
        if nearby > 100:
            node.log_warning(f"{nearby} points within 2m!")

lidar = horus.Node(name="lidar", tick=lidar_tick, rate=10, order=0, pubs=["scan"])
detector = horus.Node(name="obstacle", tick=obstacle_tick, rate=10, order=1, subs=["scan"])
horus.run(lidar, detector)

Design Decisions

Why fields_per_point instead of named fields? Point clouds in robotics use a small set of layouts: XYZ (3), XYZI (4), XYZRGB (6). A flat float array with a known field count is the fastest representation — no per-point struct overhead, direct memcpy to GPU, and trivial NumPy reshaping. Named fields would add indirection and prevent zero-copy to ML frameworks.

Why pool-backed? Same as Image — shared memory pools enable zero-copy IPC. A 100K-point XYZ cloud is 1.2 MB. Serializing it takes milliseconds; sharing a 64-byte descriptor takes microseconds.


See Also