14 KiB
Saqr PPE Detection - Deployment Guide
Unitree G1 Robot + Intel RealSense D435I
Robot Details
| Item | Value |
|---|---|
| Robot | Unitree G1 Humanoid |
| IP | 192.168.123.164 |
| User | unitree |
| OS | Ubuntu 20.04 (aarch64 / Jetson) |
| Python | 3.10 (conda env: teleimager) |
| Camera | Intel RealSense D435I |
| Serial | 243622073459 |
| Port | USB 3.2 @ /dev/video0 |
Step 1: Train the Model (Dev Machine)
cd ~/Robotics_workspace/AI/Saqr
conda activate AI_MSI_yolo
python train.py --dataset dataset --epochs 100 --batch 16
Verify model exists:
ls -lh models/saqr_best.pt
# Expected: ~5.3 MB
Step 2: Deploy to Robot (Dev Machine)
Option A: Auto deploy
cd ~/Robotics_workspace/AI/Saqr
./deploy.sh
Option B: Manual SCP
# Create folders
ssh unitree@192.168.123.164 "mkdir -p ~/Saqr/{models,captures/{SAFE,PARTIAL,UNSAFE},Config,Logs}"
# Copy project files
scp saqr.py saqr_g1_bridge.py detect.py manager.py logger.py gui.py requirements.txt deploy.sh DEPLOY.md \
unitree@192.168.123.164:~/Saqr/
# Copy config
scp Config/logging.json unitree@192.168.123.164:~/Saqr/Config/
# Copy trained model (5.3 MB)
scp models/saqr_best.pt unitree@192.168.123.164:~/Saqr/models/
Step 3: Install Dependencies (Robot)
ssh unitree@192.168.123.164
Fix system clock (required for SSL/pip):
sudo date -s "2026-04-10 15:00:00"
Install into teleimager conda env:
conda activate teleimager
python -m pip install ultralytics opencv-python-headless numpy PyYAML
If pip fails (SSL errors), install offline from dev machine:
# On dev machine:
mkdir -p /tmp/saqr_pkgs
pip download ultralytics opencv-python-headless numpy PyYAML \
-d /tmp/saqr_pkgs --python-version 3.10 --platform manylinux2014_aarch64 --only-binary=:all:
scp -r /tmp/saqr_pkgs unitree@192.168.123.164:/tmp/saqr_pkgs
# On robot:
conda activate teleimager
python -m pip install --no-index --find-links=/tmp/saqr_pkgs ultralytics opencv-python-headless numpy PyYAML
Install Jetson GPU PyTorch (for CUDA acceleration):
# Remove pip PyTorch (wrong CUDA version)
python -m pip uninstall torch torchvision -y
# Install Jetson-specific PyTorch for JetPack 5.1 / CUDA 11.4
python -m pip install --no-cache-dir \
https://developer.download.nvidia.com/compute/redist/jp/v51/pytorch/torch-2.1.0a0+41361538.nv23.06-cp310-cp310-linux_aarch64.whl
python -m pip install --no-cache-dir \
https://developer.download.nvidia.com/compute/redist/jp/v51/pytorch/torchvision-0.16.1a0+5e8e2f1-cp310-cp310-linux_aarch64.whl
Fix Qt / Display (choose one):
A) At the robot's physical terminal (monitor connected):
xhost +local:
export DISPLAY=:0
export QT_QPA_PLATFORM=xcb
B) Via SSH with X11 forwarding:
# From dev machine:
ssh -X unitree@192.168.123.164
export QT_QPA_PLATFORM=xcb
C) Headless / no display (SSH without -X):
export QT_QPA_PLATFORM=offscreen
# Always add --headless flag when running saqr.py
Make permanent:
echo 'export QT_QPA_PLATFORM=offscreen' >> ~/.bashrc
source ~/.bashrc
Common error: Invalid MIT-MAGIC-COOKIE-1 key or could not connect to display :0
This means you're in SSH without X11 auth. Either use ssh -X, run xhost +local: on the physical terminal, or switch to headless mode.
Fix system clock (required for pip/SSL):
sudo date -s "2026-04-10 16:00:00"
Verify install:
python -c "from ultralytics import YOLO; print('ultralytics OK')"
python -c "import torch; print('CUDA:', torch.cuda.is_available())"
python -c "import cv2; print('opencv OK')"
Step 4: Run PPE Detection (Robot)
Option A: OpenCV + RealSense RGB (recommended, no pyrealsense2 needed):
conda activate teleimager
cd ~/Saqr
# === WITH DISPLAY (physical monitor on robot) ===
xhost +
export DISPLAY=:0
python saqr.py --source /dev/video2 --model models/saqr_best.pt
# === HEADLESS via SSH (no display, saves captures + CSV) ===
export QT_QPA_PLATFORM=offscreen
python saqr.py --source /dev/video2 --model models/saqr_best.pt --headless
Note: /dev/video2 is the RealSense D435I RGB camera accessed directly via OpenCV V4L2.
No pyrealsense2 SDK needed. Pure OpenCV frames (640x480 BGR).
Option B: RealSense SDK (pyrealsense2):
python saqr.py --source realsense --model models/saqr_best.pt --headless
python saqr.py --source realsense:243622073459 --model models/saqr_best.pt --headless
Option C: GUI (dev machine only, not on robot):
# On your dev machine (not the robot):
python gui.py --source 0 --model models/saqr_best.pt
Note: gui.py requires PySide6 and a display. It will NOT work on the headless Jetson robot.
With OpenCV camera index:
python saqr.py --source 0 --model models/saqr_best.pt --headless
With V4L2 device path:
python saqr.py --source /dev/video0 --model models/saqr_best.pt --headless
With GUI (if display connected):
python gui.py --source realsense --model models/saqr_best.pt
Simple detection (no tracking):
python detect.py --source realsense --model models/saqr_best.pt
Step 4b: Run with G1 TTS + Reject Action (Bridge)
saqr_g1_bridge.py spawns saqr.py, parses its event stream, and drives the
G1 onboard TTS and the G1 arm action client on each per-person status
transition:
| Transition | TTS (speaker_id=2, English) | Arm action |
|---|---|---|
| → UNSAFE | "Not safe! Please wear your protective equipment." | reject (id=13) + auto release arm |
| → SAFE | "Safe." | — |
| → PARTIAL | — | — |
Requires unitree_sdk2py installed on the robot and a reachable DDS bus on
eth0. The bridge uses a single ChannelFactoryInitialize for both clients.
Headless + MJPEG stream (recommended over SSH):
conda activate marcus # or teleimager — whichever env has unitree_sdk2py
cd ~/Saqr
python3 saqr_g1_bridge.py --iface eth0 --source realsense --headless -- --stream 8080
Then open http://192.168.123.164:8080 in your laptop browser.
With live OpenCV window (physical monitor on robot):
xhost +local: >/dev/null 2>&1
DISPLAY=:0 python3 saqr_g1_bridge.py --iface eth0 --source realsense
q in the window quits; Ctrl+C in the terminal is also forwarded to Saqr.
Dry run (no TTS, no motion — just see decisions):
python3 saqr_g1_bridge.py --dry-run --source realsense --headless
Bridge CLI flags:
| Flag | Default | Description |
|---|---|---|
--iface |
(default DDS) | DDS network interface, e.g. eth0 |
--timeout |
10.0 |
Arm/Audio client timeout (seconds) |
--cooldown |
8.0 |
Per-(id, status) seconds before re-triggering |
--release-after |
2.0 |
Seconds before auto release arm (0 = never) |
--speaker-id |
2 |
G1 TtsMaker speaker_id (2 = English on current firmware) |
--dry-run |
off | Parse events but never call the SDK |
--source |
— | Pass through to saqr (0 / realsense / /dev/video2 / path) |
--headless |
off | Pass --headless to saqr |
--saqr-conf |
— | Pass --conf to saqr |
--imgsz |
— | Pass --imgsz to saqr |
--device |
— | Pass --device to saqr (cpu / 0 / cuda:0) |
-- <extra> |
— | Everything after -- is forwarded raw to saqr |
Speaker-id reference
speaker_ids are locked to a language — they do NOT auto-detect input text.
On current G1 firmware, speaker_id=0 is Chinese regardless of what you feed
it. Speaker 2 was confirmed English by running Sanad mode 6
(voice_example.py 6). If the robot's firmware changes, re-scan:
# On the robot (in a conda env with unitree_sdk2py):
python3 ~/Sanad/voice_example.py 6
and pass the new id with --speaker-id N.
What successful output looks like:
[BRIDGE] G1ArmActionClient ready (iface=eth0)
[BRIDGE] G1 AudioClient ready (speaker_id=2)
[BRIDGE] launching: /.../python3 -u /home/unitree/Saqr/saqr.py --source realsense --headless
...
ID 0001 | NEW | SAFE | wearing: helmet, vest | missing: none | ...
[BRIDGE] tts -> 'Safe.'
ID 0002 | NEW | UNSAFE | wearing: none | missing: vest | ...
[BRIDGE] tts -> 'Not safe! Please wear your protective equipment.'
[BRIDGE] -> reject
[BRIDGE] -> release arm
Step 5: Check Results (Robot)
Live status:
cat ~/Saqr/captures/result.csv
Event history (audit log):
cat ~/Saqr/captures/events.csv
Captured photos:
ls ~/Saqr/captures/SAFE/
ls ~/Saqr/captures/PARTIAL/
ls ~/Saqr/captures/UNSAFE/
Export CSV report:
cd ~/Saqr
python manager.py --export
Download results to dev machine:
# From dev machine
scp -r unitree@192.168.123.164:~/Saqr/captures/ ./captures_from_robot/
scp unitree@192.168.123.164:~/Saqr/captures/events.csv ./events_robot.csv
Camera Source Options
| Source | Command | Description |
|---|---|---|
/dev/video2 |
--source /dev/video2 |
RGB camera via OpenCV (recommended) |
realsense |
--source realsense |
RealSense D435I via pyrealsense2 SDK |
realsense:SERIAL |
--source realsense:243622073459 |
Specific RealSense by serial |
/dev/video4 |
--source /dev/video4 |
Second RGB stream (if available) |
0 |
--source 0 |
First OpenCV camera index |
video.mp4 |
--source video.mp4 |
Video file |
image.jpg |
--source image.jpg |
Single image |
G1 Robot V4L2 Device Map (RealSense D435I):
/dev/video0 - Stereo module (infrared) - won't open with OpenCV
/dev/video1 - Stereo metadata
/dev/video2 - RGB camera (640x480) ← USE THIS
/dev/video3 - RGB metadata
/dev/video4 - RGB camera (secondary stream)
Detect cameras on robot:
# Find working RGB cameras
python -c "
import cv2
for i in range(10):
cap = cv2.VideoCapture(f'/dev/video{i}', cv2.CAP_V4L2)
if cap.isOpened():
ret, frame = cap.read()
if ret and frame is not None:
print(f'/dev/video{i}: {frame.shape} OK')
else:
print(f'/dev/video{i}: opened but no frame')
cap.release()
"
# RealSense devices
rs-enumerate-devices | grep "Serial Number"
Tuning Parameters
| Parameter | Default | Flag | Description |
|---|---|---|---|
| Confidence | 0.35 | --conf 0.35 |
Lower = more detections, higher = fewer false positives |
| Max Missing | 90 | --max-missing 90 |
Frames before track deleted (~3s at 30fps) |
| Match Distance | 250 | --match-distance 250 |
Pixels for track matching |
| Confirm Frames | 5 | --status-confirm-frames 5 |
Frames to confirm a status change |
Recommended for G1 patrol:
python saqr.py --source realsense --model models/saqr_best.pt --headless \
--conf 0.30 --max-missing 120 --match-distance 300 --status-confirm-frames 7
Compliance Rules
| Status | Condition | Color |
|---|---|---|
| SAFE | Helmet AND vest detected, no violations | Green |
| PARTIAL | Only helmet OR only vest detected | Yellow |
| UNSAFE | no-helmet or no-vest detected, or nothing detected |
Red |
Output Files
| File | Location | Description |
|---|---|---|
result.csv |
captures/result.csv |
Current state of all tracked persons |
events.csv |
captures/events.csv |
Audit log (NEW / STATUS_CHANGE events) |
| Person crops | captures/SAFE/*.jpg |
Cropped images of compliant workers |
| Person crops | captures/PARTIAL/*.jpg |
Workers with incomplete PPE |
| Person crops | captures/UNSAFE/*.jpg |
Workers violating PPE rules |
| Logs | Logs/Inference/saqr.log |
Runtime log |
Project Files
| File | Purpose |
|---|---|
saqr.py |
Main PPE tracking + detection (RealSense + OpenCV) |
saqr_g1_bridge.py |
Saqr → G1 bridge (onboard TTS + reject arm action on UNSAFE/SAFE transitions) |
detect.py |
Simple detection without tracking |
gui.py |
PySide6 desktop GUI |
manager.py |
Photo management CLI + CSV export |
train.py |
YOLO model training |
logger.py |
Centralized logging |
deploy.sh |
One-command deploy to robot |
Config/logging.json |
Log settings |
Troubleshooting
RealSense not detected
# Check USB connection
lsusb | grep Intel
# Re-enumerate
rs-enumerate-devices | head -10
# Reset USB (if needed)
sudo usbreset /dev/bus/usb/002/002
Camera not opening
# Test RealSense directly
python -c "
import pyrealsense2 as rs
pipe = rs.pipeline()
cfg = rs.config()
cfg.enable_stream(rs.stream.color, 640, 480, rs.format.bgr8, 30)
pipe.start(cfg)
frames = pipe.wait_for_frames()
print('Frame:', frames.get_color_frame().get_width(), 'x', frames.get_color_frame().get_height())
pipe.stop()
"
# Test OpenCV fallback
python -c "import cv2; c=cv2.VideoCapture(0); print('OK' if c.isOpened() else 'FAIL'); c.release()"
# Try different source
python saqr.py --source /dev/video0 --model models/saqr_best.pt --headless
ModuleNotFoundError: ultralytics
# Check you're in the right conda env
which python
# Should show: /home/unitree/miniconda3/envs/teleimager/bin/python
# Install to the correct env
python -m pip install ultralytics
System clock wrong (SSL errors)
sudo date -s "2026-04-10 15:00:00"
Model not found
ls ~/Saqr/models/
# Should show: saqr_best.pt (~5.3 MB)
Low FPS on Jetson
# Use smaller confidence to reduce load
python saqr.py --source realsense --conf 0.5 --headless
# Or use headless opencv
export DISPLAY=
python saqr.py --source realsense --headless
Too many duplicate track IDs
# Increase tolerance
python saqr.py --source realsense --max-missing 150 --match-distance 300 --headless