Streaming RGB detectionΒΆ
Stream RGB frames from a camera topic through YOLOX. Any detections are annotated on the frames and published to a /annotated topic.
"""Continuously run YOLOX on a camera stream."""
import queue
import threading
import time
import cv2
from olo import Client
from olo.ai.yolo import YoloDetector
from olo.core.images import Image, compressed_message_to_frame
# @olo-playground: autofill-sim-topics
BASE_CAMERA_TOPIC = "wrist_rgbd"
RGB_TOPIC = f"{BASE_CAMERA_TOPIC}/compressed_image"
OUTPUT_TOPIC = f"{BASE_CAMERA_TOPIC}/annotated"
CONFIDENCE = 0.4
MAX_FPS = 10
def annotate(rgb: 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())
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/CompressedImage")
output_topic = robot.core.topic(OUTPUT_TOPIC, "sensor_msgs/msg/Image")
publisher = robot.core.publisher(output_topic)
frames = queue.Queue(maxsize=1)
errors = queue.Queue(maxsize=1)
threading.Thread(
target=receive_latest,
args=(robot, rgb_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:
try:
message = frames.get(timeout=0.1)
except queue.Empty:
if not errors.empty():
raise errors.get()
continue
now = time.monotonic()
if now < next_frame:
continue
next_frame = now + 1 / MAX_FPS
rgb = compressed_message_to_frame(message)
detections = []
try:
detections = detector.detect(rgb, conf=CONFIDENCE)
except Exception:
pass
publisher.publish(annotate(rgb, detections) if detections else rgb)
except KeyboardInterrupt:
print("Stopping...")
/** Continuously run YOLOX on a camera stream. */
import { createCanvas, ImageData } from "@napi-rs/canvas";
import { YoloDetector } from "olo/ai";
import { imageMessageToFrame, type Image } from "olo/core";
import { connect } from "olo/web";
// @olo-playground: autofill-sim-topics
const RGB_TOPIC = "wrist_rgbd/color_image";
const OUTPUT_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, 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();
context.strokeRect(xMin, yMin, xMax - xMin, yMax - yMin);
context.fillText(
`${detection.label} ${detection.score.toFixed(2)}`,
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 outputTopic = robot.core.topic(OUTPUT_TOPIC, "sensor_msgs/msg/Image");
const publisher = robot.core.publisher(outputTopic);
let latestRgb: Image | undefined;
let subscriptionError: unknown;
const subscription = robot.core.subscribe(rgbTopic);
void (async () => {
try {
for await (const message of subscription) {
latestRgb = 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 || now < nextFrame) {
await sleep(10);
continue;
}
const rgb = latestRgb;
latestRgb = 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, detections) : rgb);
}
} finally {
subscription.close();
await model.unload();
}