Streaming RGB-D detection

This example expects a corresponding RGB and depth topic pair. It streams RGB frames through YOLOX, parses any detection’s bounding box and uses the olo.vision.sample_depth_region() helper to sample the distance to the object. Any detections are annotated on the frames and published to a /annotated topic.

"""Continuously run RGB-D YOLOX detection and publish annotated frames."""

import queue
import threading
import time

import cv2

from olo import Client
from olo.ai.yolo import YoloDetector
from olo.core.images import Image, image_message_to_frame
from olo.vision import sample_depth_region
# @olo-playground: autofill-sim-topics

RGB_TOPIC = "wrist_rgbd/color_image"
DEPTH_TOPIC = "wrist_rgbd/depth_image"
ANNOTATED_TOPIC = "wrist_rgbd/annotated"
CONFIDENCE = 0.4
MAX_FPS = 10


def annotate(rgb: Image, depth: Image, detections) -> Image:
    canvas = rgb.array.copy()

    for detection in detections:
        x_min, y_min, x_max, y_max = map(int, detection.box.to_xyxy())
        try:
            depth_m = sample_depth_region(depth, detection.box)
            caption = f"{detection.label} {detection.score:.2f} {depth_m:.2f}m"
        except ValueError:
            caption = f"{detection.label} {detection.score:.2f}"

        cv2.rectangle(canvas, (x_min, y_min), (x_max, y_max), (0, 255, 0), 2)
        cv2.putText(
            canvas,
            caption,
            (x_min, max(y_min - 6, 14)),
            cv2.FONT_HERSHEY_SIMPLEX,
            0.5,
            (0, 255, 0),
            1,
            cv2.LINE_AA,
        )

    return Image(array=canvas, encoding="bgr8", header=rgb.header)


def receive_latest(robot, topic, frames, errors):
    try:
        with robot.core.subscribe(topic) as sub:
            for message in sub:
                try:
                    frames.get_nowait()
                except queue.Empty:
                    pass
                frames.put_nowait(message)
    except Exception as error:
        errors.put(error)


with Client() as client:
    robot = client.robot()
    rgb_topic = robot.core.topic(RGB_TOPIC, "sensor_msgs/msg/Image")
    depth_topic = robot.core.topic(DEPTH_TOPIC, "sensor_msgs/msg/Image")
    annotated_topic = robot.core.topic(ANNOTATED_TOPIC, "sensor_msgs/msg/Image")
    publisher = robot.core.publisher(annotated_topic)
    rgb_frames = queue.Queue(maxsize=1)
    depth_frames = queue.Queue(maxsize=1)
    errors = queue.Queue()
    for topic, frames in ((rgb_topic, rgb_frames), (depth_topic, depth_frames)):
        threading.Thread(
            target=receive_latest,
            args=(robot, topic, frames, errors),
            daemon=True,
        ).start()

    with client.ai.load_model("yolox_s.onnx") as model:
        detector = YoloDetector(model)
        print("Running continuously; press Ctrl+C to stop.")
        next_frame = time.monotonic()

        try:
            while True:
                if not errors.empty():
                    raise errors.get()

                now = time.monotonic()
                if now < next_frame:
                    time.sleep(min(next_frame - now, 0.01))
                    continue

                try:
                    rgb_message = rgb_frames.get(timeout=0.1)
                    depth_message = depth_frames.get(timeout=0.1)
                except queue.Empty:
                    continue
                next_frame = now + 1 / MAX_FPS

                rgb = image_message_to_frame(rgb_message)
                depth = image_message_to_frame(depth_message)
                detections = []
                try:
                    detections = detector.detect(rgb, conf=CONFIDENCE)
                except Exception:
                    pass
                publisher.publish(
                    annotate(rgb, depth, detections) if detections else rgb
                )
        except KeyboardInterrupt:
            print("Stopping...")
/** Continuously run RGB-D YOLOX detection and publish annotated frames. */

import { createCanvas, ImageData } from "@napi-rs/canvas";
import { YoloDetector } from "olo/ai";
import { imageMessageToFrame, type Image } from "olo/core";
import { connect } from "olo/web";
import { sampleDepthRegion } from "olo/vision";
// @olo-playground: autofill-sim-topics

const RGB_TOPIC = "wrist_rgbd/color_image";
const DEPTH_TOPIC = "wrist_rgbd/depth_image";
const ANNOTATED_TOPIC = "wrist_rgbd/annotated";
const CONFIDENCE = 0.4;
const MAX_FPS = 10;

const sleep = (milliseconds: number): Promise<void> =>
  new Promise((resolve) => setTimeout(resolve, milliseconds));

function annotate(rgb: Image, depth: Image, detections: Awaited<ReturnType<YoloDetector["detect"]>>): Image {
  const { data, width, height, step } = rgb;
  const rgba = new Uint8ClampedArray(width * height * 4);
  const rgbOrder = rgb.encoding.toLowerCase() === "rgb8";

  for (let y = 0; y < height; y += 1) {
    for (let x = 0; x < width; x += 1) {
      const source = y * step + x * 3;
      const target = (y * width + x) * 4;
      rgba[target] = data[source + (rgbOrder ? 0 : 2)]!;
      rgba[target + 1] = data[source + 1]!;
      rgba[target + 2] = data[source + (rgbOrder ? 2 : 0)]!;
      rgba[target + 3] = 255;
    }
  }

  const canvas = createCanvas(width, height);
  const context = canvas.getContext("2d");
  context.putImageData(new ImageData(rgba, width, height), 0, 0);
  context.strokeStyle = "#00ff00";
  context.fillStyle = "#00ff00";
  context.lineWidth = 2;
  context.font = "14px sans-serif";

  for (const detection of detections) {
    const [xMin, yMin, xMax, yMax] = detection.box.toXyxy();
    let caption = `${detection.label} ${detection.score.toFixed(2)}`;
    try {
      caption += ` ${sampleDepthRegion(depth, detection.box).toFixed(2)}m`;
    } catch {
      // Keep the label and score when no valid depth is available.
    }

    context.strokeRect(xMin, yMin, xMax - xMin, yMax - yMin);
    context.fillText(caption, xMin, Math.max(yMin - 4, 14));
  }

  const annotatedRgba = context.getImageData(0, 0, width, height).data;
  // Preserve the source channel order because no-detection frames are
  // published unchanged on the same topic.
  const annotatedInSourceOrder = new Uint8Array(width * height * 3);
  for (let pixel = 0; pixel < width * height; pixel += 1) {
    annotatedInSourceOrder[pixel * 3] =
      annotatedRgba[pixel * 4 + (rgbOrder ? 0 : 2)]!;
    annotatedInSourceOrder[pixel * 3 + 1] = annotatedRgba[pixel * 4 + 1]!;
    annotatedInSourceOrder[pixel * 3 + 2] =
      annotatedRgba[pixel * 4 + (rgbOrder ? 2 : 0)]!;
  }

  return {
    data: annotatedInSourceOrder,
    // The byte order above matches the source, so do not hardcode "bgr8".
    encoding: rgb.encoding || "bgr8",
    format: "",
    width,
    height,
    step: width * 3,
    header: rgb.header,
  };
}

const client = connect();
const robot = await client.robot();
const rgbTopic = robot.core.topic(RGB_TOPIC, "sensor_msgs/msg/Image");
const depthTopic = robot.core.topic(DEPTH_TOPIC, "sensor_msgs/msg/Image");
const annotatedTopic = robot.core.topic(ANNOTATED_TOPIC, "sensor_msgs/msg/Image");
const publisher = robot.core.publisher(annotatedTopic);
let latestRgb: Image | undefined;
let latestDepth: Image | undefined;
let subscriptionError: unknown;

const rgbSubscription = robot.core.subscribe(rgbTopic);
const depthSubscription = robot.core.subscribe(depthTopic);
void (async () => {
  try {
    for await (const message of rgbSubscription) {
      latestRgb = imageMessageToFrame(message);
    }
  } catch (error) {
    subscriptionError = error;
  }
})();
void (async () => {
  try {
    for await (const message of depthSubscription) {
      latestDepth = imageMessageToFrame(message);
    }
  } catch (error) {
    subscriptionError = error;
  }
})();

const model = await client.ai.loadModel("yolox_s.onnx");
try {
  const detector = new YoloDetector(model);
  console.log("Running continuously; press Ctrl+C to stop.");
  let nextFrame = Date.now();

  while (true) {
    if (subscriptionError !== undefined) throw subscriptionError;

    const now = Date.now();
    if (latestRgb === undefined || latestDepth === undefined || now < nextFrame) {
      await sleep(10);
      continue;
    }

    const rgb = latestRgb;
    const depth = latestDepth;
    latestRgb = undefined;
    latestDepth = undefined;
    nextFrame = now + 1_000 / MAX_FPS;

    let detections: Awaited<ReturnType<YoloDetector["detect"]>> = [];
    try {
      detections = await detector.detect(rgb, { conf: CONFIDENCE });
    } catch {
      // Publish the unannotated frame if detection fails.
    }
    await publisher.publish(detections.length > 0 ? annotate(rgb, depth, detections) : rgb);
  }
} finally {
  rgbSubscription.close();
  depthSubscription.close();
  await model.unload();
}