AI and Camera Subsystems¶
Simplified, high-level APIs for accessing AI inference and camera streaming on the PIB robot.
Overview¶
The subsystem APIs (robot.ai and robot.camera) provide a simpler alternative to manual subscription management. They automatically handle subscription lifecycle and provide typed results.
| Approach | When to Use |
|---|---|
Subsystem API (robot.ai, robot.camera) |
Most use cases—simple, auto-managed |
Raw subscriptions (subscribe_ai_detections(model, callback)) |
Custom buffering, multiple callbacks, advanced scenarios |
from pib3 import Robot, AIModel
with Robot(host="172.26.34.149") as robot:
# Subsystem API - simple and direct
robot.ai.set_model(AIModel.HAND)
for hand in robot.ai.get_hand_landmarks():
print(f"{hand.handedness}: index={hand.finger_angles.index:.0f}°")
AISubsystem (robot.ai)¶
Run AI models on the OAK-D Lite and read their results as typed objects.
The camera runs the models of the robot's model store (pib-backend). A client asks for a model with /start_model and names itself as an owner; the model runs as long as any owner holds it. That is why a script, cerebra and a neighbouring group do not switch each other's models away. robot.ai starts and stops models under this client's owner name (pib3-<user>@<computer>, or Robot(ai_owner="group-3")) and keeps one receiver per model.
Starting or stopping a model restarts the camera pipeline
Video and the IMU pause for a few seconds, unless another owner already runs the model. Depth is gone while any model runs (see Depth).
Quick Start¶
from pib3 import Robot, AIModel
with Robot(host="172.26.34.149") as robot:
# Run a model (returns when the robot reports it running)
robot.ai.set_model(AIModel.YOLO26S)
# Get detections (waits automatically for results)
for det in robot.ai.get_detections(latest_only=True):
print(f"{det.label}: {det.confidence:.0%} at {det.bbox}")
# Check performance
print(f"FPS: {robot.ai.fps:.1f}")
Methods¶
set_model()¶
Run one model and none of the others this client started.
| Parameter | Type | Default | Description |
|---|---|---|---|
model |
AIModel or str |
required | Model to run (enum, or any model id the robot lists) |
timeout |
float |
90.0 |
Max seconds for each answer of the robot; rosbridge is asked to wait as long |
Returns: True if the robot reports the model running; False if it refused. The reason is logged together with the models the robot offers.
set_model stops the models this client started earlier, then starts the new one. Each start and stop rebuilds the camera pipeline and takes a few seconds, except when another owner already runs that model. A model that is already running costs nothing.
from pib3 import AIModel
robot.ai.set_model(AIModel.HAND)
robot.ai.set_model(AIModel.YOLO26S) # stops the hand model first
robot.ai.set_model("yolov6n_coco_640x640") # any id from get_available_ai_models()
start_model() / stop_model()¶
Run several models at once. The camera has 16 processing cores; each model uses a fixed number (AIModelInfo.shaves, 4 for the YOLO models, 9 for the hand chain), so two or three models fit together.
robot.ai.start_model(AIModel.YOLO26S)
robot.ai.start_model(AIModel.HAND) # both run; HAND is now the current model
dets = robot.ai.get_detections(latest_only=True, model=AIModel.YOLO26S)
hands = robot.ai.get_hand_landmarks(latest_only=True) # the current model
robot.ai.stop_model(AIModel.HAND)
stop_model() only releases this client's hold; the model keeps running while another owner holds it.
get_detections()¶
Get every box a model reports (objects, but also the persons of a pose model, hands, faces, QR codes).
Waits automatically for results if buffer is empty. Raises RuntimeError if no model has been started.
robot.ai.set_model(AIModel.YOLO26S)
for det in robot.ai.get_detections(latest_only=True):
print(f"Found {det.label} ({det.confidence:.0%})")
print(f" BBox: {det.bbox.center}")
Models of the face family attach more to det.keypoints and det.scalars:
robot.ai.set_model(AIModel.HEAD_POSE)
for det in robot.ai.get_detections(latest_only=True):
print(det.scalars) # {"yaw": -12.0, "pitch": 3.5, "roll": 0.8}
get_hand_landmarks()¶
Get hand tracking results with finger angles.
def get_hand_landmarks(timeout: float = 5.0, latest_only: bool = False, model=None) -> List[HandLandmarks]
robot.ai.set_model(AIModel.HAND)
for hand in robot.ai.get_hand_landmarks(latest_only=True):
print(f"{hand.handedness}: index={hand.finger_angles.index:.0f}°")
# Convert to servo values for robot hand control
servos = hand.finger_angles.to_servo_values()
robot.set_joints({
"index_left_stretch": servos["index"],
"middle_left_stretch": servos["middle"],
})
The list is empty while no hand is in view. Finger angles are measured in pixel space, so the 16:9 frame does not skew them.
get_poses()¶
Get body pose estimation results.
robot.ai.set_model(AIModel.POSE_YOLO)
for pose in robot.ai.get_poses(latest_only=True):
if pose.nose:
print(f"Nose at: ({pose.nose.x:.2f}, {pose.nose.y:.2f})")
Properties¶
| Property | Type | Description |
|---|---|---|
model |
Optional[str] |
Id of the current model: the one the getters read by default |
models |
Tuple[str, ...] |
Ids of all models this client started |
fps |
float |
Results per second of the current model |
avg_latency_ms |
float |
Average age of a result on arrival, in milliseconds |
avg_latency_ms compares the robot's timestamp with this computer's clock, so it includes the network and is only meaningful when both clocks are synchronised (NTP). Implausible values are ignored. In the simulation it is the inference time.
Other Methods¶
| Method | Description |
|---|---|
available_models() |
The models the robot offers, as AIModelInfo (available, active, shaves, task, ...) |
clear() |
Clear buffered results |
stop() |
Release every model this client started and unsubscribe (also runs on disconnect) |
CameraSubsystem (robot.camera)¶
Access raw camera frames from the OAK-D Lite with automatic subscription management.
Quick Start¶
from pib3 import Robot
with Robot(host="172.26.34.149") as robot:
# Get latest frame
frame = robot.camera.get_frame()
if frame:
print(f"Frame {frame.frame_id}, {len(frame.jpeg_bytes)} bytes")
# Decode to numpy (requires OpenCV)
img = frame.to_numpy()
Methods¶
get_frame()¶
Get the latest camera frame.
frame = robot.camera.get_frame()
if frame:
# Save JPEG directly
with open("capture.jpg", "wb") as f:
f.write(frame.jpeg_bytes)
# Or decode to numpy for processing
img = frame.to_numpy() # Requires OpenCV
get_frames()¶
Get all buffered frames and clear the buffer.
configure()¶
Configure camera settings.
def configure(
fps: Optional[int] = None,
quality: Optional[int] = None,
resolution: Optional[tuple] = None,
) -> None
| Parameter | Type | Description |
|---|---|---|
fps |
int |
Frames per second (e.g., 30) |
quality |
int |
JPEG quality 1-100 (e.g., 80) |
resolution |
tuple |
(width, height) e.g., (1280, 720) |
Keep the frame 16:9
The camera publishes 1280×720 by default. AI models read the same frame, and the camera refuses to feed them one of another aspect, so use 16:9 sizes such as 1280×720 or 640×360. Changing the resolution restarts the camera pipeline; quality and frame rate do not.
Properties¶
| Property | Type | Description |
|---|---|---|
frame_count |
int |
Total number of frames received |
Other Methods¶
| Method | Description |
|---|---|
stop() |
Stop camera streaming (unsubscribe) |
Depth¶
robot.camera.get_depth_frame() and get_distance_at_px() read the camera's stereo depth. Depth is the camera's resting state: it runs while no AI model runs and is gone while one does, because the stereo pair and a model share the camera's cores. After robot.ai.stop() it returns within a few seconds. So a script cannot look up the distance of a detected object while the detector runs; measure first, or stop the model.
The OAK-D Lite has no time-of-flight sensor; depth comes from comparing the two black-and-white cameras (about 0.8 m to 12 m).
AIModel Enum¶
Type-safe names for the models of the robot's model store (pib-backend, models/manifest.yaml). The enum value is the model id that /list_models reports and that cerebra shows.
| Enum value | Model id | Task | Cores |
|---|---|---|---|
AIModel.YOLO26S |
yolo26s_coco_512x288 |
Object detection, 80 COCO classes (default) | 4 |
AIModel.YOLO26N |
yolo26n_coco_512x288 |
Object detection, faster and less accurate | 4 |
AIModel.POSE_YOLO (also POSE_YOLO26S) |
yolo26s_pose_coco_512x288 |
Body pose, 17 COCO keypoints (default) | 4 |
AIModel.POSE_YOLO26N |
yolo26n_pose_coco_512x288 |
Body pose, faster and less accurate | 4 |
AIModel.YOLO26M, AIModel.POSE_YOLO26M |
yolo26m_coco_512x288, yolo26m_pose_coco_512x288 |
More accurate, laptop (simulation) only | - |
AIModel.HAND |
hand_tracking_fast |
Hand landmarks, 21 points per hand | 9 |
AIModel.FACE |
face_detection_yunet_160x120 |
Face boxes | 4 |
AIModel.EMOTION |
emotion_recognition_crop |
Emotion (one probability per emotion in det.scalars) |
8 |
AIModel.HEAD_POSE |
head_pose_estimation_crop |
Head yaw, pitch, roll in det.scalars |
8 |
AIModel.QR_CODE |
qr_code_detection_384x384 |
QR code boxes | 4 |
AIModel.YOLO26M and POSE_YOLO26M are in LAPTOP_ONLY_MODELS: on the OAK-D YOLO26m reached 4 results/s, so the robot's store does not have it, and robot.ai refuses them without asking the robot.
pib-backend withdrew the face mesh (facemesh_crop) and the 68 facial landmarks (facial_landmarks_68_crop) from its camera model list (PR-1957), so AIModel no longer names them.
Hand tracking speed is unknown
AIModel.HAND is hand_tracking_fast, which replaced the backend's two older hand chains (pib-backend PR-1961). It computes the hand crops on the camera, so no frame waits for the robot's computer; that wait is what held the oldest chain, hand_tracking, to 1.0 result/s on a robot. Nobody has measured hand_tracking_fast on a camera yet, pib-backend included; its first version could not have run there at all (PR-1970 fixed the tensor access on the device). A hand-mirroring program needs several results per second: check robot.ai.fps on your robot before relying on it.
The older chains "hand_tracking_mp" and "hand_tracking" still start as strings and publish on the shared topic detections/hand_tracking. The robot no longer lists them, so available_models() does not show them.
Any other model id works as a plain string; robot.ai.available_models() lists what the robot has. The model ids and core counts come from the backend's manifest and were not all measured by pib3; available_models() reports the numbers the robot itself uses.
The YOLO26 models come with pib-backend's b3 fork
The four YOLO26 models are Ultralytics builds (AGPL-3.0) at 512×288, the camera's 16:9. pib-backend's b3 fork (mamrehn/pib-backend, branch b3-develop) lists them and provisions them from its model release (setup/setup-pib.sh --models). On a robot without them, set_model returns False and logs the models the robot does offer, for example yolov6n_coco_640x640 (pib-rocks' detector, 640×640, same COCO classes).
Speed and accuracy on the OAK-D Lite
Measured with the camera node's own chain on an OAK-D Lite (laptop, USB 3); COCO accuracy (mAP50-95) is Ultralytics' figure at 640×640.
| Model | Accuracy | Results/s | Age of a result |
|---|---|---|---|
YOLO26S |
48.6 | 12.6 | 0.22 s |
YOLO26N |
40.9 | 25.2 | 0.15 s |
POSE_YOLO |
63.0 | 11.6 | 0.25 s |
POSE_YOLO26N |
57.2 | 21.9 | 0.16 s |
The camera itself parses the results, so the robot's Raspberry Pi only forwards them; these rates are the ceiling there. They were not measured on a robot.
Hidden keypoints
A pose model also places keypoints it cannot see, such as a wrist behind the back, and gives them a low keypoint.confidence. Check it before using a point: if wrist.confidence > 0.5: .... Robots on an older backend send no confidences; pib3 then reports 1.0.
Retired names¶
These were exposed by earlier versions of this SDK. set_model() remaps them with a DeprecationWarning, on the robot and in the simulation:
| Old name | Now uses |
|---|---|
"yolo26n", "yolov6n", "yolov10n", "yolov8n", "yolo11n" |
AIModel.YOLO26N |
"yolo26s", "yolo11s", "mobilenet-ssd" |
AIModel.YOLO26S |
"pose_yolo", "pose_hrnet", "pose" |
AIModel.POSE_YOLO |
"pose_yolov8" |
AIModel.POSE_YOLO26N |
"hand" |
AIModel.HAND |
"face" |
AIModel.FACE |
"yolov8n-seg", "deeplabv3", "fastsam" |
"segmentation" (simulation only) |
The robot never had segmentation, gaze, line or person-only models in this store; they were part of an earlier backend branch.
Simulation
The Webots backend (sim.ai) has the same methods and runs ultralytics or MediaPipe on your laptop instead of the OAK-D: YOLO26S → yolo26s.pt, YOLO26N → yolo26n.pt, YOLO26M → yolo26m.pt, POSE_YOLO → yolo26s-pose.pt, POSE_YOLO26N / POSE_YOLO26M → yolo26n-pose.pt / yolo26m-pose.pt, HAND → MediaPipe, "segmentation" → yolo26n-seg.pt (simulation only, with det.mask_rle). Use s by default; n on a laptop that is too slow for it, m when you want more accuracy. On a laptop CPU one frame took 38 / 86 / 212 ms (n / s / m); pib3.backends.sim_ai.inference_ms(model) measures it on yours. Weights are loaded from $PIB3_WEIGHTS_DIR, the running script's folder or the working directory before ultralytics downloads them. It also takes a weights file name directly ("yolo26l.pt") and "recognition" for Webots ground truth. Faces, emotion, head pose and QR codes have no simulated equivalent. Models run together, each inferring on every rendered frame.
Type Reference¶
Detection¶
One box a model reports.
@dataclass
class Detection:
label_id: int # COCO class id, or -1 for labels outside COCO
confidence: float # 0.0 to 1.0
bbox: BoundingBox # normalized coordinates
label: str # class name, e.g. "person", "hand"
mask_rle: Optional[Dict] # RLE mask (simulation only)
keypoints: List[Keypoint] # named landmarks, in normalized coordinates
scalars: Dict[str, float] # named values: yaw, pitch, handedness, ...
The robot sends pixel coordinates together with the frame size (DetectionArray); pib3 normalizes them, so bbox and keypoints are in [0, 1] whatever the camera's resolution.
Properties:
det.label # "person"
det.confidence # 0.92
det.bbox.center # (0.5, 0.3) - center point
det.bbox.width # 0.2
det.bbox.height # 0.4
det.bbox.to_pixels(640, 480) # (x1, y1, x2, y2) in pixels
det.keypoints[0].name # "nose" (pose and face models)
det.scalars["yaw"] # head pose, in degrees
HandLandmarks¶
Hand tracking result with 21 landmarks and calculated finger angles.
@dataclass
class HandLandmarks:
landmarks: np.ndarray # Shape (21, 2) normalized coordinates
keypoints: List[Keypoint] # 21 named Keypoint objects
handedness: Handedness # LEFT, RIGHT, or UNKNOWN
confidence: float # Landmark score of the hand
finger_angles: FingerAngles # Calculated finger bend angles
aspect_ratio: float # Frame width / height the landmarks come from
handedness is the model's probability of "right" (above 0.5) as MediaPipe labels it, from the camera's point of view.
Properties:
hand.handedness # Handedness.LEFT
hand.wrist_position # (x, y) tuple
hand.finger_angles.index # Index finger bend angle in degrees
FingerAngles¶
Finger bend angles calculated from hand landmarks.
@dataclass
class FingerAngles:
thumb_bend: float # Thumb flexion angle
thumb_rotation: float # Thumb rotation relative to palm
index: float # Index finger bend angle (0° = straight, 90° = bent)
middle: float # Middle finger bend angle
ring: float # Ring finger bend angle
pinky: float # Pinky finger bend angle
Methods:
# Convert to list
angles = finger_angles.to_list() # [thumb_bend, thumb_rot, index, middle, ring, pinky]
# Convert to servo percentages (for robot hand control)
servos = finger_angles.to_servo_values() # {"index": 75.0, "middle": 80.0, ...}
# Use directly with robot
robot.set_joints({
"index_left_stretch": servos["index"],
"middle_left_stretch": servos["middle"],
})
PoseKeypoints¶
Body pose estimation with 17 COCO keypoints.
@dataclass
class PoseKeypoints:
keypoints: List[Keypoint] # 17 keypoints, in COCO order
confidence: float # Confidence of the person detection
bbox: Optional[BoundingBox] # Bounding box around person
Convenience properties:
pose.nose # Keypoint or None
pose.left_shoulder # Keypoint or None
pose.right_shoulder # Keypoint or None
pose.get_keypoint(5) # Get by COCO index (0-16)
CameraFrame¶
Single camera frame with metadata.
@dataclass
class CameraFrame:
jpeg_bytes: bytes # Raw JPEG image data
frame_id: int # Sequential frame number
timestamp_ns: int # Timestamp in nanoseconds
Properties and Methods:
frame.timestamp # Timestamp as float seconds
frame.to_numpy() # Decode to BGR numpy array (requires OpenCV)
Complete Example¶
Hand tracking with robot hand mirroring:
from pib3 import Robot, AIModel
import time
with Robot(host="172.26.34.149") as robot:
# Enable hand tracking
if not robot.ai.set_model(AIModel.HAND):
print("Failed to set model")
exit(1)
print("Tracking hands... (Ctrl+C to stop)")
try:
while True:
# Newest frame only (waits automatically)
hands = robot.ai.get_hand_landmarks(timeout=1.0, latest_only=True)
for hand in hands:
print(f"\n{hand.handedness}:")
print(f" Index: {hand.finger_angles.index:.0f}°")
print(f" Middle: {hand.finger_angles.middle:.0f}°")
# Mirror to robot hand
servos = hand.finger_angles.to_servo_values()
if hand.handedness.value == "left":
robot.set_joints({
"index_left_stretch": servos["index"],
"middle_left_stretch": servos["middle"],
"ring_left_stretch": servos["ring"],
"pinky_left_stretch": servos["pinky"],
})
# Show stats
print(f"FPS: {robot.ai.fps:.1f}")
except KeyboardInterrupt:
print("\nStopping...")
finally:
robot.ai.stop() # release the model; depth comes back
See Also¶
- Camera, AI, and IMU Tutorial - Step-by-step guide
- RealRobotBackend - Low-level subscription methods
- Types Reference - All pib3 types