Detection & Detection3D

Fixed-size object detection messages for zero-copy IPC. Detection holds a 2D bounding box result from models like YOLO or SSD. Detection3D holds a 3D oriented bounding box from point cloud detectors or depth-aware models. Both are fixed-size types — transferred via zero-copy shared memory (72 and 104 bytes respectively).

Rust: Detection and Detection3D live in the horus_robotics crate — use horus_robotics::prelude::*;

When to Use

Use Detection when your robot runs a 2D object detection model on camera images and needs to publish results to downstream nodes (tracking, planning, visualization). Use Detection3D when you have 3D detections from LiDAR-based or depth-aware models.

ROS2 Equivalent

  • Detection maps to vision_msgs/Detection2D
  • Detection3D maps to vision_msgs/Detection3D

Example

// simplified
use horus::prelude::*;
use horus_robotics::prelude::*;

// 2D detection from YOLO
let det = Detection::new("person", 0.92, 100.0, 50.0, 200.0, 400.0);
//                       class     conf  x      y      width   height

// Check confidence threshold
if det.is_confident(0.5) {
    println!("Found {} at ({}, {})", det.class_name(), det.bbox.x, det.bbox.y);
}

// 3D detection from point cloud
let bbox3d = BoundingBox3D::new(5.0, 2.0, 0.5, 4.5, 2.0, 1.5, 0.1);
//                              cx   cy   cz   len  wid  hgt  yaw
let det3d = Detection3D::new("car", 0.88, bbox3d)
    .with_velocity(10.0, 5.0, 0.0);

// Publish detections
let topic: Topic<Detection> = Topic::new("detections.2d")?;
topic.send(&det);

Detection Fields

FieldTypeUnitSizeDescription
bboxBoundingBox2Dpx16 BBounding box (x, y, width, height)
confidencef320--14 BDetection confidence
class_idu32--4 BNumeric class identifier
class_name[u8; 32]--32 BUTF-8 class label, null-padded (max 31 chars)
instance_idu32--4 BInstance ID for instance segmentation

Total size: 72 bytes (fixed-size, zero-copy)

Detection Methods

MethodSignatureDescription
new(name, conf, x, y, w, h)(&str, f32, f32, f32, f32, f32) -> SelfCreate with class name and bounding box
with_class_id(id, conf, bbox)(u32, f32, BoundingBox2D) -> SelfCreate with numeric class ID
class_name()-> &strGet class name as string
set_class_name(name)(&str) -> ()Set class name (truncates to 31 chars)
is_confident(threshold)(f32) -> boolTrue if confidence >= threshold

BoundingBox2D Methods

MethodSignatureDescription
new(x, y, w, h)(f32, f32, f32, f32) -> SelfCreate from top-left corner
from_center(cx, cy, w, h)(f32, f32, f32, f32) -> SelfCreate from center (YOLO format)
center_x(), center_y()-> f32Center coordinates
area()-> f32Box area in pixels
iou(other)(&BoundingBox2D) -> f32Intersection over Union

Detection3D Fields

FieldTypeUnitSizeDescription
bboxBoundingBox3Dm, rad48 B3D box: center, dimensions, rotation (roll/pitch/yaw)
confidencef320--14 BDetection confidence
class_idu32--4 BNumeric class identifier
class_name[u8; 32]--32 BUTF-8 class label
velocity_x/y/zf32m/s12 BObject velocity (for tracking-enabled detectors)
instance_idu32--4 BTracking/instance ID

Total size: 104 bytes (fixed-size, zero-copy)

Common Patterns

Camera-to-tracking pipeline:

Camera-to-tracking pipeline

Confidence filtering pattern:

// simplified
use horus::prelude::*;
use horus_robotics::prelude::*;

fn filter_detections(detections: &[Detection], min_conf: f32) -> Vec<&Detection> {
    detections.iter()
        .filter(|d| d.is_confident(min_conf))
        .collect()
}

NMS (Non-Maximum Suppression):

// simplified
use horus::prelude::*;
use horus_robotics::prelude::*;

fn nms(dets: &mut Vec<Detection>, iou_threshold: f32) {
    dets.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap());
    let mut keep = vec![true; dets.len()];
    for i in 0..dets.len() {
        if !keep[i] { continue; }
        for j in (i + 1)..dets.len() {
            if keep[j] && dets[i].bbox.iou(&dets[j].bbox) > iou_threshold {
                keep[j] = false;
            }
        }
    }
    let mut idx = 0;
    dets.retain(|_| { let k = keep[idx]; idx += 1; k });
}

Design Decisions

Why fixed-size 72/104 bytes instead of variable-length? Fixed-size enables zero-copy shared memory transport without serialization. Every Detection fits in the same ring buffer slot, so the scheduler can deliver detections at the same rate as IMU or odometry data -- no allocation jitter.

Why [u8; 32] for class_name instead of a String? Strings are heap-allocated and variable-length, making them incompatible with zero-copy transport. A 32-byte null-padded array fits in the fixed-size layout and accommodates class labels up to 31 characters (enough for COCO, ImageNet, and custom class names). If you need longer labels, use class_id and maintain a lookup table.

Why separate Detection and Detection3D? 2D and 3D detections have different data layouts and come from different pipelines (camera vs LiDAR/depth). Keeping them separate avoids wasted memory (no 3D fields when doing 2D detection) and makes topic typing unambiguous. A tracker node can subscribe to one or both.

Why no built-in NMS? Non-Maximum Suppression strategies vary by model and use case (soft-NMS, class-aware NMS, rotated NMS). HORUS provides the iou() primitive and lets you implement the NMS variant your pipeline needs, as shown in the Common Patterns section above.


See Also

Spotted an error on this page?