490 lines
14 KiB
Markdown
490 lines
14 KiB
Markdown
# 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)
|
|
|
|
```bash
|
|
cd ~/Robotics_workspace/AI/Saqr
|
|
conda activate AI_MSI_yolo
|
|
python train.py --dataset dataset --epochs 100 --batch 16
|
|
```
|
|
|
|
Verify model exists:
|
|
```bash
|
|
ls -lh models/saqr_best.pt
|
|
# Expected: ~5.3 MB
|
|
```
|
|
|
|
---
|
|
|
|
## Step 2: Deploy to Robot (Dev Machine)
|
|
|
|
### Option A: Auto deploy
|
|
```bash
|
|
cd ~/Robotics_workspace/AI/Saqr
|
|
./deploy.sh
|
|
```
|
|
|
|
### Option B: Manual SCP
|
|
```bash
|
|
# 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)
|
|
|
|
```bash
|
|
ssh unitree@192.168.123.164
|
|
```
|
|
|
|
### Fix system clock (required for SSL/pip):
|
|
```bash
|
|
sudo date -s "2026-04-10 15:00:00"
|
|
```
|
|
|
|
### Install into teleimager conda env:
|
|
```bash
|
|
conda activate teleimager
|
|
python -m pip install ultralytics opencv-python-headless numpy PyYAML
|
|
```
|
|
|
|
If pip fails (SSL errors), install offline from dev machine:
|
|
```bash
|
|
# 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):
|
|
```bash
|
|
# 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):**
|
|
```bash
|
|
xhost +local:
|
|
export DISPLAY=:0
|
|
export QT_QPA_PLATFORM=xcb
|
|
```
|
|
|
|
**B) Via SSH with X11 forwarding:**
|
|
```bash
|
|
# From dev machine:
|
|
ssh -X unitree@192.168.123.164
|
|
export QT_QPA_PLATFORM=xcb
|
|
```
|
|
|
|
**C) Headless / no display (SSH without -X):**
|
|
```bash
|
|
export QT_QPA_PLATFORM=offscreen
|
|
# Always add --headless flag when running saqr.py
|
|
```
|
|
|
|
**Make permanent:**
|
|
```bash
|
|
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):
|
|
```bash
|
|
sudo date -s "2026-04-10 16:00:00"
|
|
```
|
|
|
|
### Verify install:
|
|
```bash
|
|
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):
|
|
```bash
|
|
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):
|
|
```bash
|
|
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):
|
|
```bash
|
|
# 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:
|
|
```bash
|
|
python saqr.py --source 0 --model models/saqr_best.pt --headless
|
|
```
|
|
|
|
### With V4L2 device path:
|
|
```bash
|
|
python saqr.py --source /dev/video0 --model models/saqr_best.pt --headless
|
|
```
|
|
|
|
### With GUI (if display connected):
|
|
```bash
|
|
python gui.py --source realsense --model models/saqr_best.pt
|
|
```
|
|
|
|
### Simple detection (no tracking):
|
|
```bash
|
|
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):
|
|
```bash
|
|
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):
|
|
```bash
|
|
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):
|
|
```bash
|
|
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:
|
|
```bash
|
|
# 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:
|
|
```bash
|
|
cat ~/Saqr/captures/result.csv
|
|
```
|
|
|
|
### Event history (audit log):
|
|
```bash
|
|
cat ~/Saqr/captures/events.csv
|
|
```
|
|
|
|
### Captured photos:
|
|
```bash
|
|
ls ~/Saqr/captures/SAFE/
|
|
ls ~/Saqr/captures/PARTIAL/
|
|
ls ~/Saqr/captures/UNSAFE/
|
|
```
|
|
|
|
### Export CSV report:
|
|
```bash
|
|
cd ~/Saqr
|
|
python manager.py --export
|
|
```
|
|
|
|
### Download results to dev machine:
|
|
```bash
|
|
# 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:
|
|
```bash
|
|
# 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:
|
|
```bash
|
|
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
|
|
```bash
|
|
# 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
|
|
```bash
|
|
# 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
|
|
```bash
|
|
# 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)
|
|
```bash
|
|
sudo date -s "2026-04-10 15:00:00"
|
|
```
|
|
|
|
### Model not found
|
|
```bash
|
|
ls ~/Saqr/models/
|
|
# Should show: saqr_best.pt (~5.3 MB)
|
|
```
|
|
|
|
### Low FPS on Jetson
|
|
```bash
|
|
# 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
|
|
```bash
|
|
# Increase tolerance
|
|
python saqr.py --source realsense --max-missing 150 --match-distance 300 --headless
|
|
```
|