Skill: moroiser/camera-yolo-operator (version 1.3.0)

--- name: Camera YOLO Operator | 摄像头 YOLO 操控者 description: Webcam YOLO and depth detection. 摄像头 YOLO 目标检测与 DA3Metric 深度估计；模型放 modules 缓存，输出放 projects。 --- # Camera YOLO Operator | 摄像头 YOLO 操控者 🐾 操作本地摄像头，运行 YOLO 目标检测和 DA3Metric 深度估计。 --- ## 目录 1. [概述](#概述) 2. [快速开始](#快速开始) 3. [运行模式](#运行模式) 4. [模型管理](#模型管理) 5. [输出目录](#输出目录) 6. [深度校正](#深度校正) 7. [环境变量](#环境变量) 8. [故障排查](#故障排查) --- ## 概述 ### 平台支持 | 平台 | 摄像头后端 | GPU | |------|-----------|-----| | Linux | V4L2 | NVIDIA CUDA | | Windows | DirectShow | NVIDIA CUDA | | macOS | AVFoundation | Apple MPS / CPU | ### 模型 - **YOLO26s 系列**: 目标检测、分割、姿态、旋转框、分类 - **DA3Metric-Large**: HuggingFace 深度估计模型，首次运行自动下载 --- ## 快速开始 ### 1. 安装依赖 ```bash cd ~/.openclaw/workspace/skills/camera-yolo-operator pip install -r requirements.txt ``` ### 2. 下载 YOLO 模型 ```bash bash scripts/download_models.sh ``` ### 3. 运行 ```bash # 测试摄像头 python3 scripts/capture_webcam.py --count 3 # YOLO 检测 python3 scripts/yolo_detection.py --seconds 30 # 深度距离检测 python3 scripts/yolo_depth_distance.py --seconds 30 ``` --- ## 运行模式 ### 模式一: 纯摄像头抓拍 不运行任何 AI 模型，只抓拍图片。用于测试摄像头是否正常工作。 ```bash python3 scripts/capture_webcam.py \ --count 3 \ --interval 0.5 \ --camera-index 0 ``` **参数说明**: - `--count`: 抓拍数量（默认 3） - `--interval`: 每次抓拍间隔秒数（默认 0.5） - `--camera-index`: 摄像头索引（默认 0） - `--warmup`: 预热帧数（默认 5） --- ### 模式二: YOLO 纯检测 使用 YOLO 进行目标检测，可选多种模型（检测/分割/姿态等）。 ```bash python3 scripts/yolo_detection.py \ --seconds 30 \ --save-every 10 \ --max-saves 3 \ --yolo-model yolo26s.pt \ --conf 0.5 ``` **参数说明**: - `--seconds`: 运行时间（默认 30） - `--save-every`: 每隔多少秒保存一张（默认 10） - `--max-saves`: 最大保存数量（默认 3） - `--yolo-model`: YOLO 模型路径或名称 - `--conf`: 置信度阈值（默认 0.5） - `--device`: 设备，"0"=GPU，"cpu"=CPU（默认 "0"） **可用 YOLO 模型**: | 模型 | 用途 | 命令示例 | |------|------|---------| | `yolo26s.pt` | 目标检测（默认） | `--yolo-model yolo26s.pt` | | `yolo26s-seg.pt` | 实例分割 | `--yolo-model yolo26s-seg.pt` | | `yolo26s-pose.pt` | 姿态估计 | `--yolo-model yolo26s-pose.pt` | | `yolo26s-obb.pt` | 旋转框检测 | `--yolo-model yolo26s-obb.pt` | | `yolo26s-cls.pt` | 图像分类 | `--yolo-model yolo26s-cls.pt` | --- ### 模式三: YOLO 检测 + 深度距离叠加 将 YOLO 检测框与 DA3Metric 深度图融合，在每个检测框上标注物体类别、置信度和到摄像头的距离（米）。 ```bash python3 scripts/yolo_depth_distance.py \ --seconds 30 \ --save-every 5 \ --max-saves 6 \ --yolo-model yolo26s.pt \ --depth-scale 1.0 \ --output-dir ~/.openclaw/workspace/projects/camera-yolo-operator/depth_distance ``` **参数说明**: - `--depth-scale`: 深度校正系数（详见下文） - `--depth-model`: 深度模型，默认 `depth-anything/DA3Metric-Large` - `--device`: 深度模型运行设备（默认 `cuda:0`） **输出内容**: - 绿色检测框 + 标签格式：`{类别} {置信度} | {距离}m` - 背景叠加伪彩色深度图（INFERNO 配色，暖色=近，冷色=远） --- ### 模式四: 通用目标轨迹跟踪 使用 YOLO 检测任意目标 + ByteTrack 分配持续 ID + Supervision 绘制轨迹线。行人、车辆、鸟类——只要 YOLO 能检测的类别都能跟踪。统计输出为**独立目标数量**（去重后的车辆数、行人数），而非帧次出现次数。 ```bash # 摄像头实时检测（默认纯检测，画框不画轨迹） python3 scripts/yolo_pedestrian_tracker.py --source 0 --seconds 30 # 摄像头实时跟踪（开启轨迹线） python3 scripts/yolo_pedestrian_tracker.py --source 0 --seconds 30 --track # 视频文件：标注并输出完整带轨迹的视频 python3 scripts/yolo_pedestrian_tracker.py --source /path/to/video.mp4 --track --output-video out.mp4 # 指定类别跟踪 python3 scripts/yolo_pedestrian_tracker.py --source 0 --seconds 30 --track --classes 0 # 仅行人 python3 scripts/yolo_pedestrian_tracker.py --source 0 --seconds 30 --track --classes 2 3 # 车+摩托车 # 调轨迹线长度 python3 scripts/yolo_pedestrian_tracker.py --source 0 --trail-length 50 ``` **参数说明**: - `--track`: 开启轨迹跟踪模式（默认关闭） - `--classes`: 要检测的目标类别，默认全部检测。`0`=人，`2`=车，`3`=摩托车，`15`=猫，`16`=狗等 - `--source`: 视频源，`0`=摄像头，或本地视频文件路径 - `--seconds`: 运行时间（默认 30，仅摄像头模式有效） - `--trail-length`: 轨迹线长度，按帧计（默认 30） - `--conf`: 置信度阈值（默认 0.5） - `--device`: 设备，`0`=GPU，`cpu`=CPU - `--output-video`: 输出带标注的完整视频路径（推荐使用） - `--save-every`: 截图保存间隔秒数（默认 5，仅视频文件模式） - `--max-saves`: 最大截图数量（默认 10） **输出**: - 带标注的视频（`--output-video` 指定路径） - 控制台输出独立目标统计（独立车辆数、独立行人数等） **新增依赖**（supervision 已内置 ByteTrack）： ```bash pip install supervision ``` **输出目录**: ``` projects/camera-yolo-operator/pedestrian_track/ ├── annotated_video.mp4 # 带标注的视频（--output-video 指定） └── frame_*.jpg # 截图 ``` --- ## 模式五: 纯深度图（无 YOLO） 单独输出深度图，不叠加检测结果。 ```python from depth_anything_3.api import DepthAnything3 import cv2 import numpy as np model = DepthAnything3.from_pretrained('depth-anything/DA3Metric-Large') cap = cv2.VideoCapture(0) ret, frame = cap.read() cap.release() pred = model.inference([frame]) depth = pred.depth[0] # [H, W] 米制深度 # 灰度版 gray = (depth / depth.max() * 255).astype(np.uint8) cv2.imwrite('depth_gray.jpg', gray) # 伪彩色版 color = cv2.applyColorMap( cv2.convertScaleAbs((depth / depth.max() * 255).astype(np.uint8)), cv2.COLORMAP_INFERNO) cv2.imwrite('depth_color.jpg', color) print(f"深度范围: {depth.min():.2f}~{depth.max():.2f}m") ``` --- ## 模型管理 ### 模型存放位置 ⚠️ 大型模型文件（>50MB）禁止直接放入技能文件夹，统一存放在： ``` ~/.cache/huggingface/modules/camera-yolo-operator/ ├── yolo26s.pt # YOLO 目标检测（ Ultralytics 官方下载） └── DA3Metric-Large → ~/.cache/huggingface/hub/models--depth-anything--DA3Metric-Large/ ``` DA3Metric-Large 由 `depth_anything_3` 包调用 HuggingFace 官方下载，存放在 hub 缓存，通过符号链接在模块目录对外保持路径统一。 技能文件夹本身（`~/.openclaw/workspace/skills/camera-yolo-operator/`）仅含技能代码，不存放任何大型模型文件。 ### 下载模型 ```bash # YOLO26s（下载后移动到模块缓存） mkdir -p ~/.cache/huggingface/modules/camera-yolo-operator/ python3 -c "from ultralytics import YOLO; YOLO('yolo26s.pt')" mv yolo26s.pt ~/.cache/huggingface/modules/camera-yolo-operator/ # DA3Metric-Large（自动下载到 hub 缓存，再做符号链接） python3 -c "from depth_anything_3.api import DepthAnything3; DepthAnything3.from_pretrained('depth-anything/DA3Metric-Large')" ln -s ~/.cache/huggingface/hub/models--depth-anything--DA3Metric-Large \ ~/.cache/huggingface/modules/camera-yolo-operator/DA3Metric-Large ``` ### 模型搜索路径 脚本按以下顺序查找 YOLO 模型： 1. 环境变量 `YOLO_MODEL_PATH` 2. `~/.cache/huggingface/modules/camera-yolo-operator/` 3. `~/.openclaw/workspace/yolo/`（兼容旧路径） 4. 当前目录 ### 指定模型路径 ```bash # 通过命令行 python3 scripts/yolo_detection.py --yolo-model /path/to/custom/model.pt # 或通过环境变量 export YOLO_MODEL_PATH=/path/to/custom/model.pt python3 scripts/yolo_detection.py ``` --- ## 输出目录 运行结果统一保存在工作区的 `projects/camera-yolo-operator/` 目录下： ``` ~/.openclaw/workspace/projects/camera-yolo-operator/ ├── captures/ # capture_webcam.py 的原始抓拍 ├── detection/ # yolo_detection.py 的检测结果 ├── depth_distance/ # yolo_depth_distance.py 的深度+检测结果 └── pedestrian_track/ # yolo_pedestrian_tracker.py 的目标轨迹标注结果 └── track_*.jpg ``` 可使用 `--output-dir` 参数指定其他目录。 --- ## 深度校正 ### 问题现象 深度模型输出的距离值与实际距离不符。 ### 校正方法 1. 在摄像头前放置物体，记录模型显示的距离 `D_model` 2. 用尺子测量实际距离 `D_real` 3. 校正系数 = `D_real / D_model` 4. 使用 `--depth-scale {系数}` 运行脚本 **示例**: - 物体实际距离: 3m - 模型显示距离: 1.9m - 校正系数: `3.0 / 1.9 ≈ 1.574` - 运行命令: `python3 scripts/yolo_depth_distance.py --depth-scale 1.574` --- ## 环境变量 | 变量名 | 默认值 | 说明 | |--------|--------|------| | `OPENCLAW_WORKSPACE` | `~/.openclaw/workspace` | 工作区根目录 | | `YOLO_MODEL_PATH` | `~/.cache/huggingface/modules/camera-yolo-operator/yolo26s.pt` | YOLO 模型路径 | | `HF_ENDPOINT` | (官方) | HuggingFace 镜像，解决下载问题 | **设置镜像加速下载**: ```bash export HF_ENDPOINT=https://hf-mirror.com ``` --- ## 故障排查 ### 摄像头无法打开 ```bash # 检查摄像头设备 ls -la /dev/video* # Linux # 或设备管理器 # Windows # 测试摄像头是否被占用 python3 scripts/capture_webcam.py --count 1 ``` **解决方案**: - 关闭其他占用摄像头的应用（微信、QQ、Zoom 等） - 尝试不同的 `--camera-index` - Linux: 检查用户权限 `sudo usermod -a -G video $USER` ### HuggingFace 模型下载失败 ```bash # 方法一: 清除代理 unset all_proxy ALL_PROXY http_proxy https_proxy # 方法二: 使用镜像 export HF_ENDPOINT=https://hf-mirror.com # 方法三: 手动下载 git lfs install git clone https://huggingface.co/depth-anything/DA3Metric-Large ~/.cache/huggingface/modules/depth_anything_3/ddp/ ``` ### GPU 未被使用 ```bash # 检查 PyTorch CUDA python3 -c "import torch; print(f'CUDA: {torch.cuda.is_available()}')" # 检查 NVIDIA 驱动 nvidia-smi ``` **解决方案**: - 确认 CUDA 版本匹配（当前环境 CUDA 12.1） - 重新安装 PyTorch: `pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121` ### 依赖安装失败 ```bash # 升级 pip pip install --upgrade pip # 使用清华镜像 pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple ``` --- ## 目录结构 ``` camera-yolo-operator/ ├── SKILL.md # 本文档 ├── requirements.txt # Python 依赖 ├── scripts/ │ ├── capture_webcam.py # 纯摄像头抓拍 │ ├── yolo_detection.py # YOLO 目标检测 │ ├── yolo_depth_distance.py # YOLO + 深度距离 │ ├── yolo_pedestrian_tracker.py # 行人轨迹跟踪 │ └── download_models.sh # 模型下载脚本 ├── models/ │ └── .gitkeep # YOLO 模型目录（需单独下载） └── references/ ├── platform.md # 平台假设与维护笔记 └── deployment.md # 详细部署指南 ``` --- ## 依赖 ``` # requirements.txt ultralytics>=8.0.0 torch>=2.0.0 torchvision>=0.15.0 depth-anything-3>=0.1.0 opencv-python>=4.8.0 numpy>=1.24.0 ``` --- ## 维护笔记 - **模型更新**: 定期检查 Ultralytics 官网获取新版本 YOLO - **深度模型**: DA3Metric-Large 由 HuggingFace 托管，需网络访问 - **GPU 兼容性**: 确认 PyTorch CUDA 版本与 NVIDIA 驱动兼容

path: _meta.json
content: {
  "ownerId": "kn75k53tcdhqwka2fra5x817p9821zpb",
  "slug": "camera-yolo-operator",
  "version": "1.3.0",
  "publishedAt": 1775240252543,
  "changelog": [
    {
      "version": "1.3.0",
      "changes": [
        "Standardize model and output paths: models now live under ~/.cache/huggingface/modules/camera-yolo-operator, outputs under ~/.openclaw/workspace/projects/camera-yolo-operator, and the skill folder stays code-only. 统一模型与输出路径：模型放 modules 缓存，运行结果放 projects，技能文件夹仅保留代码。",
        "Refresh the bilingual ClawHub Summary and default script paths for cleaner publishing and runtime behavior. 更新双语 ClawHub Summary 与脚本默认路径，提升发布与运行目录规范性。"
      ]
    }
  ]
}


path: references/deployment.md
content: # YOLO Skill 部署指南

本文档说明如何在全新机器上部署 YOLO Skill，实现零配置或最小配置运行。

---

## 目录

1. [系统要求](#系统要求)
2. [快速部署](#快速部署)
3. [手动部署步骤](#手动部署步骤)
4. [模型下载](#模型下载)
5. [常见问题](#常见问题)
6. [平台特定说明](#平台特定说明)

---

## 系统要求

### 硬件
- **摄像头**: 内置或外置 USB 摄像头
- **GPU**: NVIDIA GPU（推荐，CUDA 加速）
  - 如无 GPU，脚本会自动降级到 CPU（速度较慢）
- **内存**: 8GB+ RAM

### 软件
- **Python**: 3.8+
- **CUDA**: 11.8+ (GPU 加速)
- **操作系统**: Linux / Windows / macOS

---

## 快速部署

### Linux / macOS

```bash
# 1. 进入 skill 目录
cd ~/.openclaw/workspace/skills/camera-yolo-operator

# 2. 安装依赖
pip install -r requirements.txt

# 3. 下载模型
bash scripts/download_models.sh

# 4. 运行测试
python3 scripts/capture_webcam.py  # 测试摄像头
python3 scripts/yolo_detection.py  # 测试 YOLO 检测
```

### Windows

```powershell
# 1. 进入 skill 目录
cd $env:USERPROFILE\.openclaw\workspace\skills\camera-yolo-operator

# 2. 安装依赖
pip install -r requirements.txt

# 3. 下载模型（使用 Python 下载）
python scripts\download_models.py

# 4. 运行测试
python scripts/capture_webcam.py
```

---

## 手动部署步骤

### Step 1: 创建工作区目录

```bash
mkdir -p ~/.openclaw/workspace/yolo
mkdir -p ~/.openclaw/workspace/projects/yolo/captures
mkdir -p ~/.openclaw/workspace/projects/yolo/detection
mkdir -p ~/.openclaw/workspace/projects/yolo/depth_distance
```

### Step 2: 安装 Python 依赖

```bash
pip install ultralytics torch torchvision --index-url https://download.pytorch.org/whl/cu121
pip install depth-anything-3
pip install opencv-python numpy
```

### Step 3: 下载 YOLO 模型

从 [Ultralytics](https://docs.ultralytics.com/models/yolo11) 下载以下模型到 `~/.openclaw/workspace/yolo/`：

| 模型文件 | 用途 |
|---------|------|
| `yolo26s.pt` | 目标检测（默认）|
| `yolo26s-seg.pt` | 实例分割 |
| `yolo26s-pose.pt` | 姿态估计 |
| `yolo26s-obb.pt` | 旋转框检测 |
| `yolo26s-cls.pt` | 图像分类 |

或使用命令行：

```bash
python3 -c "from ultralytics import YOLO; YOLO('yolo26s.pt')"
```

### Step 4: 验证安装

```bash
# 测试摄像头
python3 scripts/capture_webcam.py --count 3

# 测试 YOLO
python3 scripts/yolo_detection.py --seconds 10

# 测试深度检测
python3 scripts/yolo_depth_distance.py --seconds 10
```

---

## 模型下载

### 方式一: 自动下载脚本

```bash
cd skills/camera-yolo-operator
bash scripts/download_models.sh
```

### 方式二: 手动下载

从以下来源下载：

1. **Ultralytics 官方**: https://docs.ultralytics.com/models/yolo11
2. **HuggingFace**: https://huggingface.co/ultralytics/ (如可用)

### 方式三: Python 下载

```python
from ultralytics import YOLO
model = YOLO('yolo26s.pt')  # 自动下载
```

---

## 常见问题

### Q: HuggingFace 模型下载失败

**原因**: 网络问题或代理干扰

**解决方案**:

1. 清除代理:
```bash
unset all_proxy ALL_PROXY http_proxy https_proxy HTTP_PROXY HTTPS_PROXY
```

2. 使用镜像:
```bash
export HF_ENDPOINT=https://hf-mirror.com
```

3. 或手动下载 DA3Metric-Large:
```bash
git clone https://huggingface.co/depth-anything/DA3Metric-Large ~/.cache/huggingface/modules/depth_anything_3/ddp/
```

### Q: 摄像头无法打开

**解决方案**:

1. 检查摄像头是否被其他应用占用
2. 尝试不同的 camera index:
```bash
python3 scripts/capture_webcam.py --camera-index 0
python3 scripts/capture_webcam.py --camera-index 1
```

3. Linux: 检查设备权限:
```bash
ls -la /dev/video*
# 如果权限不够:
sudo chmod 666 /dev/video0
# 或将用户加入 video 组:
sudo usermod -a -G video $USER
```

### Q: GPU 未被使用

**检查**:

```bash
python3 -c "import torch; print(f'CUDA: {torch.cuda.is_available()}')"
```

**解决方案**:

1. 确认 NVIDIA 驱动已安装: `nvidia-smi`
2. 确认 CUDA 版本匹配: `nvcc --version`
3. 重新安装 PyTorch CUDA 版本:
```bash
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121
```

### Q: 模型文件找不到

**原因**: 模型未下载或路径错误

**解决**:

1. 运行下载脚本:
```bash
bash scripts/download_models.sh
```

2. 或设置环境变量:
```bash
export YOLO_MODEL_PATH=/path/to/your/yolo26s.pt
```

---

## 平台特定说明

### Linux

- **摄像头后端**: V4L2 (Video4Linux2)
- **设备路径**: `/dev/video0`, `/dev/video1`, ...
- **权限**: 用户需要在 `video` 组中
- **GPU**: 需要 NVIDIA driver + CUDA

### Windows

- **摄像头后端**: DirectShow (DSHOW)
- **设备索引**: 通常 `0` 是默认摄像头
- **GPU**: 需要 NVIDIA driver + CUDA

### macOS

- **摄像头后端**: AVFoundation
- **GPU**: MPS (Apple Silicon) 或 CPU

---

## 环境变量

| 变量名 | 默认值 | 说明 |
|--------|--------|------|
| `OPENCLAW_WORKSPACE` | `~/.openclaw/workspace` | 工作区根目录 |
| `YOLO_MODEL_PATH` | `~/.../workspace/yolo/yolo26s.pt` | YOLO 模型路径 |
| `HF_ENDPOINT` | (官方) | HuggingFace 镜像 |

---

## 目录结构

部署后的完整目录结构：

```
~/.openclaw/workspace/
├── yolo/                          # YOLO 模型文件夹
│   ├── yolo26s.pt
│   ├── yolo26s-seg.pt
│   └── ...
└── projects/yolo/                 # 运行结果
    ├── captures/                  # 原始摄像头抓拍
    ├── detection/                 # YOLO 检测结果
    └── depth_distance/            # 深度+检测结果
```

---

## 故障排查命令

```bash
# 检查 Python 版本
python3 --version

# 检查 PyTorch + CUDA
python3 -c "import torch; print(f'PyTorch: {torch.__version__}'); print(f'CUDA: {torch.cuda.is_available()}')"

# 检查摄像头
python3 -c "import cv2; cap = cv2.VideoCapture(0); print(f'Camera OK: {cap.isOpened()}'); cap.release()"

# 检查模型文件
ls -lh ~/.openclaw/workspace/yolo/*.pt

# 检查依赖
pip list | grep -E "ultralytics|torch|depth-anything|cv2"
```


path: references/platform.md
content: # Platform Notes

## Tested environments

### Windows
- OS: Windows 11
- GPU: NVIDIA GeForce GTX 1060 6GB
- OpenClaw install mode: installed directly on Windows
- Camera backend: DirectShow

### Linux
- OS: Linux (generic)
- GPU: varies
- OpenClaw install mode: installed directly on Linux
- Camera backend: V4L2

## Model location

Store the YOLO model in `workspace/assets/yolo/` or configure the path in `TOOLS.md`.

## Camera device detection

### Linux
Check available camera devices:
```bash
ls /dev/video*
```
Typical laptop webcam appears as `/dev/video0` or `/dev/video1`.

Make sure the user has read access to the device, or add the user to the `video` group.

### Windows
Camera index `0` is typically the built-in notebook webcam. If multiple cameras exist, test indices starting from `0`.

## Maintenance notes

If the webcam index changes or multiple cameras appear, test camera indices manually.

If the GPU stack changes, verify:
- `torch.cuda.is_available()`
- model inference still works
- saved images still land under `workspace/tmp/`

## Suggested output folders

- Raw webcam captures: `workspace/tmp/camshots/`
- Annotated YOLO captures: `workspace/tmp/yolo-captures/`


path: requirements.txt
content: # YOLO Skill - Python Dependencies
# Install: pip install -r requirements.txt

# Core
ultralytics>=8.0.0
torch>=2.0.0
torchvision>=0.15.0

# Depth estimation
depth-anything-3>=0.1.0

# Pedestrian tracking (supervision 自带 ByteTrack)
supervision>=0.20.0

opencv-python>=4.8.0
numpy>=1.24.0


path: scripts/capture_webcam.py
content: #!/usr/bin/env python3
"""
Pure webcam capture script - no AI models, just take pictures.
Cross-platform: Linux (V4L2), Windows (DirectShow/DShow), macOS (AVFoundation).
"""
import argparse
import os
import sys
import time

import cv2

# ── Platform Detection ───────────────────────────────────────────────────────
IS_WINDOWS = sys.platform == "win32" or sys.platform == "cygwin"
IS_LINUX = sys.platform.startswith("linux")
IS_MACOS = sys.platform == "darwin"

# ── Default Paths ─────────────────────────────────────────────────────────────
# 统一使用环境变量或默认值，不硬编码用户目录
def get_default_output_dir():
    """Get default output directory in workspace projects folder."""
    workspace = os.environ.get("OPENCLAW_WORKSPACE", os.path.join(os.path.expanduser("~"), ".openclaw", "workspace"))
    return os.path.join(workspace, "projects", "camera-yolo-operator", "captures")


def get_skill_dir():
    """Get the skill directory based on this script's location."""
    return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


# ── Camera Opening ────────────────────────────────────────────────────────────
def open_camera(index: int):
    """
    Try multiple backends to open the camera.
    Order: platform-specific first, then fallback.
    """
    if IS_WINDOWS:
        # DirectShow (DSHOW) is Windows-specific, fastest for Windows
        cap = cv2.VideoCapture(index, cv2.CAP_DSHOW)
        if cap.isOpened():
            return cap
        cap.release()

    if IS_LINUX:
        # Try V4L2 first (most common on Linux)
        cap = cv2.VideoCapture(index, cv2.CAP_V4L2)
        if cap.isOpened():
            return cap
        cap.release()

    if IS_MACOS:
        # AVFoundation on macOS
        cap = cv2.VideoCapture(index, cv2.CAP_AVFOUNDATION)
        if cap.isOpened():
            return cap
        cap.release()

    # Fallback: default backend
    cap = cv2.VideoCapture(index)
    return cap


# ── Main ──────────────────────────────────────────────────────────────────────
def main():
    parser = argparse.ArgumentParser(
        description="Capture photos from local webcam. No AI models - pure camera capture."
    )
    parser.add_argument("--camera-index", type=int, default=0, help="Camera index (default: 0)")
    parser.add_argument("--count", type=int, default=3, help="Number of photos to take (default: 3)")
    parser.add_argument("--interval", type=float, default=0.5,
                        help="Interval between shots in seconds (default: 0.5)")
    parser.add_argument("--output-dir", default=None,
                        help=f"Output directory (default: ~/.../projects/camera-yolo-operator/captures)")
    parser.add_argument("--warmup", type=int, default=5,
                        help="Number of warmup frames (default: 5)")
    parser.add_argument("--prefix", default="shot",
                        help="Filename prefix (default: 'shot')")
    args = parser.parse_args()

    # Setup output directory
    output_dir = args.output_dir or get_default_output_dir()
    os.makedirs(output_dir, exist_ok=True)

    print(f"[Camera Capture] Starting...")
    print(f"  Platform:   {sys.platform}")
    print(f"  Camera:     {args.camera_index}")
    print(f"  Count:      {args.count}")
    print(f"  Interval:   {args.interval}s")
    print(f"  Output:     {output_dir}")
    print()

    # Open camera
    cap = open_camera(args.camera_index)
    if not cap.isOpened():
        print(f"ERROR: Cannot open camera {args.camera_index}", file=sys.stderr)
        sys.exit(1)

    # Warmup - discard initial frames for auto-exposure to settle
    print(f"Warming up camera ({args.warmup} frames)...")
    for i in range(args.warmup):
        cap.read()
        time.sleep(0.05)

    # Capture
    saved_paths = []
    for i in range(1, args.count + 1):
        ok, frame = cap.read()
        if not ok:
            print(f"ERROR: Failed to read frame {i}", file=sys.stderr)
            cap.release()
            sys.exit(2)

        filename = f"{args.prefix}{i}.jpg"
        path = os.path.join(output_dir, filename)

        if not cv2.imwrite(path, frame):
            print(f"ERROR: Failed to write {path}", file=sys.stderr)
            cap.release()
            sys.exit(3)

        saved_paths.append(path)
        print(f"  [{i}/{args.count}] Saved: {path}")

        if i < args.count:
            time.sleep(args.interval)

    cap.release()

    print()
    print(f"Done! Captured {len(saved_paths)} photos.")
    for p in saved_paths:
        print(f"  {p}")


if __name__ == "__main__":
    main()


path: scripts/download_models.sh
content: #!/bin/bash
#==============================================================================
# YOLO Model Downloader
# 自动下载 YOLO 模型到 HuggingFace modules 缓存目录
#==============================================================================

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
MODELS_DIR="${HOME}/.cache/huggingface/modules/camera-yolo-operator"

echo "=========================================="
echo " YOLO Model Downloader"
echo "=========================================="
echo ""
echo "Skill 目录: ${SKILL_DIR}"
echo "模型目录:   ${MODELS_DIR}"
echo "说明: 模型禁止放入 skill 文件夹本身；统一放到 HuggingFace modules 缓存。"
echo ""

# Create directory
mkdir -p "${MODELS_DIR}"

# Ultralytics YOLO26s models
MODELS=(
    "yolo26s.pt"
    "yolo26s-seg.pt"
    "yolo26s-pose.pt"
    "yolo26s-obb.pt"
    "yolo26s-cls.pt"
)

echo "开始下载 YOLO 模型..."
echo ""

# Download each model using Python/ultralytics
for model in "${MODELS[@]}"; do
    target="${MODELS_DIR}/${model}"

    if [ -f "${target}" ]; then
        echo "  [SKIP] ${model} already exists"
        continue
    fi

    echo "  [DOWNLOAD] ${model}..."

    python3 -c "
from ultralytics import YOLO
import sys
try:
    model = YOLO('${model}')
    print('    [OK] Downloaded: ${model}')
except Exception as e:
    print(f'    [ERROR] Failed: {e}', file=sys.stderr)
    sys.exit(1)
" || {
        echo "  [ERROR] Failed to download ${model}"
        continue
    }

    # Move downloaded model to target directory.
    # Ultralytics may save to the current directory or ~/.cache/ultralytics/.
    if [ -f "${model}" ]; then
        mv "${model}" "${target}"
        echo "    [MOVED] to ${target}"
        continue
    fi
    cache_dir="$HOME/.cache/ultralytics/"
    if [ -f "${cache_dir}${model}" ]; then
        cp "${cache_dir}${model}" "${target}"
        echo "    [MOVED] to ${target}"
    elif [ -f "${cache_dir}/yolo${model:4}" ]; then
        # Handle yolo26s.pt -> yolo26s.pt naming
        cp "${cache_dir}/yolo${model:4}" "${target}" 2>/dev/null || true
    fi
done

echo ""
echo "=========================================="
echo " Model download complete!"
echo "=========================================="
echo ""
echo "Model location: ${MODELS_DIR}"
ls -lh "${MODELS_DIR}"/*.pt 2>/dev/null || echo "  (no .pt files)"
echo ""
echo "Depth model (DA3Metric-Large) will be downloaded automatically on first run"
echo "Tip: Run 'unset all_proxy ALL_PROXY' if HuggingFace download is slow"


path: scripts/yolo_depth_distance.py
content: #!/usr/bin/env python3
"""
YOLO Detection + DA3Metric Depth Estimation with Distance Overlay.
Combines YOLO bounding boxes with metric depth estimation.
Each detection box shows: class name + confidence + distance in meters.
"""
import argparse
import os
import sys
import time
from pathlib import Path

import cv2
import numpy as np
import torch

from depth_anything_3.api import DepthAnything3
from ultralytics import YOLO

# ── Platform Detection ───────────────────────────────────────────────────────
IS_WINDOWS = sys.platform == "win32" or sys.platform == "cygwin"
IS_LINUX = sys.platform.startswith("linux")
IS_MACOS = sys.platform == "darwin"

# ── Default Paths ─────────────────────────────────────────────────────────────
def get_default_yolo_model_path():
    """Get default YOLO model path from the shared model cache."""
    return os.path.join(os.path.expanduser("~"), ".cache", "huggingface", "modules",
                        "camera-yolo-operator", "yolo26s.pt")


def get_default_output_dir():
    workspace = os.environ.get("OPENCLAW_WORKSPACE",
                               os.path.join(os.path.expanduser("~"), ".openclaw", "workspace"))
    return os.path.join(workspace, "projects", "camera-yolo-operator", "depth_distance")


def get_skill_dir():
    return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


def get_model_search_paths():
    workspace = os.environ.get("OPENCLAW_WORKSPACE",
                               os.path.join(os.path.expanduser("~"), ".openclaw", "workspace"))
    return [
        os.environ.get("YOLO_MODEL_PATH", ""),
        os.path.join(os.path.expanduser("~"), ".cache", "huggingface", "modules",
                     "camera-yolo-operator"),
        os.path.join(workspace, "yolo"),
        ".",
    ]


def find_model(model_name_or_path):
    path = Path(model_name_or_path)
    if path.is_absolute() and path.exists():
        return str(path)
    if path.suffix.lower() in ['.pt', '.pth', '.yolo']:
        for search_dir in get_model_search_paths():
            if not search_dir:
                continue
            candidate = Path(search_dir) / path.name
            if candidate.exists():
                return str(candidate)
    if path.exists():
        return str(path)
    return model_name_or_path


# ── Camera Opening ────────────────────────────────────────────────────────────
def open_camera(index: int):
    if IS_WINDOWS:
        cap = cv2.VideoCapture(index, cv2.CAP_DSHOW)
        if cap.isOpened():
            return cap
        cap.release()

    if IS_LINUX:
        cap = cv2.VideoCapture(index, cv2.CAP_V4L2)
        if cap.isOpened():
            return cap
        cap.release()

    if IS_MACOS:
        cap = cv2.VideoCapture(index, cv2.CAP_AVFOUNDATION)
        if cap.isOpened():
            return cap
        cap.release()

    cap = cv2.VideoCapture(index)
    return cap


# ── Main ──────────────────────────────────────────────────────────────────────
def main():
    parser = argparse.ArgumentParser(
        description="YOLO detection + DA3Metric depth estimation with distance overlay."
    )
    parser.add_argument("--camera-index", type=int, default=0, help="Camera index (default: 0)")
    parser.add_argument("--seconds", type=int, default=30, help="Runtime in seconds (default: 30)")
    parser.add_argument("--save-every", type=float, default=5.0,
                        help="Save snapshot every N seconds (default: 5.0)")
    parser.add_argument("--max-saves", type=int, default=6,
                        help="Maximum number of saves (default: 6)")
    parser.add_argument("--conf", type=float, default=0.5,
                        help="YOLO confidence threshold (default: 0.5)")
    parser.add_argument("--depth-scale", type=float, default=1.0,
                        help="Depth calibration scale factor. "
                             "E.g. real=3m, reads=1.9m → scale=3.0/1.9≈1.574 (default: 1.0)")
    parser.add_argument("--yolo-model", default=None,
                        help="YOLO model path (default: yolo26s.pt)")
    parser.add_argument("--depth-model", default="depth-anything/DA3Metric-Large",
                        help="Depth model (default: depth-anything/DA3Metric-Large)")
    parser.add_argument("--output-dir", default=None,
                        help=f"Output directory (default: ~/.../projects/camera-yolo-operator/depth_distance)")
    parser.add_argument("--device", default="cuda:0",
                        help="Device for depth model (default: cuda:0)")
    parser.add_argument("--no-display", action="store_true", help="Headless mode")
    args = parser.parse_args()

    # Resolve paths
    yolo_model_path = find_model(args.yolo_model or get_default_yolo_model_path())
    output_dir = args.output_dir or get_default_output_dir()
    os.makedirs(output_dir, exist_ok=True)

    # Check YOLO model
    if not os.path.exists(yolo_model_path):
        print(f"ERROR: YOLO model not found: {yolo_model_path}", file=sys.stderr)
        print(f"\nRun scripts/download_models.sh to download models.", file=sys.stderr)
        sys.exit(1)

    # Load models
    print(f"[YOLO + Depth] Starting...")
    print(f"  YOLO Model:   {yolo_model_path}")
    print(f"  Depth Model:  {args.depth_model}")
    print(f"  Camera:       {args.camera_index}")
    print(f"  Runtime:      {args.seconds}s")
    print(f"  Depth Scale:  {args.depth_scale}")
    print(f"  Output:       {output_dir}")
    print()

    # Load YOLO
    print("Loading YOLO model...")
    yolo = YOLO(yolo_model_path)

    # Load Depth model (auto-downloads from HuggingFace on first run)
    print("Loading depth model (may download from HuggingFace on first run)...")
    print("  TIP: Run 'unset all_proxy ALL_PROXY' if download is slow/fails")
    print("  Or:  export HF_ENDPOINT=https://hf-mirror.com")

    # Check for proxy issues
    proxy_vars = ['http_proxy', 'https_proxy', 'HTTP_PROXY', 'HTTPS_PROXY',
                  'all_proxy', 'ALL_PROXY']
    active_proxies = [v for v in proxy_vars if os.environ.get(v)]
    if active_proxies:
        print(f"  [WARN] Active proxy env vars detected: {active_proxies}")
        print(f"         These may interfere with HuggingFace downloads.")

    depth_model = DepthAnything3.from_pretrained(args.depth_model)
    device = torch.device(args.device if torch.cuda.is_available() else "cpu")
    depth_model = depth_model.to(device=device)
    print(f"  Depth model loaded on: {device}")
    print(f"  PyTorch: {torch.__version__}, CUDA: {torch.cuda.is_available()}")

    # Open camera
    cap = open_camera(args.camera_index)
    if not cap.isOpened():
        print(f"ERROR: Cannot open camera {args.camera_index}", file=sys.stderr)
        sys.exit(2)

    # Warmup
    print("Warming up camera and models...")
    for _ in range(5):
        cap.read()

    print(f"\nRunning for {args.seconds}s, saving every {args.save_every}s...")
    print("(Press 'q' in window or wait for timeout)")
    print()

    start = time.time()
    last_save_at = start
    save_count = 0
    saved_paths = []
    window_name = "YOLO + Depth Distance (Press 'q' to quit)"

    while time.time() - start < args.seconds:
        ok, frame = cap.read()
        if not ok:
            print("Camera read error.")
            break

        # ── YOLO Detection ──────────────────────────────────────────────
        det_results = yolo(frame, conf=args.conf, verbose=False, device=0)
        det = det_results[0]

        # ── Depth Estimation ─────────────────────────────────────────────
        depth_pred = depth_model.inference([frame])
        depth_map = depth_pred.depth[0]  # [H, W] in meters

        # ── Draw Annotations ─────────────────────────────────────────────
        annotated = frame.copy()

        if det.boxes is not None and len(det.boxes) > 0:
            for i in range(len(det.boxes)):
                cls_id = int(det.boxes.cls[i].item())
                conf = float(det.boxes.conf[i].item())
                name = det.names[cls_id]

                # Bounding box
                x1, y1, x2, y2 = det.boxes.xyxy[i].cpu().numpy().astype(int)

                # Center point
                cx = int((x1 + x2) / 2)
                cy = int((y1 + y2) / 2)

                # Get depth at center
                h, w = depth_map.shape
                cx_clip = np.clip(cx, 0, w - 1)
                cy_clip = np.clip(cy, 0, h - 1)
                depth_m = float(depth_map[cy_clip, cx_clip]) * args.depth_scale

                # Label: class + confidence + distance
                label = f"{name} {conf:.2f} | {depth_m:.1f}m"

                # Draw box and label
                cv2.rectangle(annotated, (x1, y1), (x2, y2), (0, 255, 0), 2)
                cv2.putText(annotated, label, (x1, y1 - 8),
                            cv2.FONT_HERSHEY_SIMPLEX, 0.55, (0, 255, 0), 2)

        # ── Depth Colormap Overlay ──────────────────────────────────────
        depth_calibrated = depth_map * args.depth_scale
        depth_colormap = cv2.applyColorMap(
            cv2.convertScaleAbs((depth_calibrated / max(depth_calibrated.max(), 1e-6) * 255).astype(np.uint8)),
            cv2.COLORMAP_INFERNO
        )
        depth_colormap = cv2.resize(depth_colormap, (frame.shape[1], frame.shape[0]))
        alpha = 0.35
        annotated = cv2.addWeighted(annotated, 1, depth_colormap, alpha, 0)

        # Display
        if not args.no_display:
            cv2.imshow(window_name, annotated)
            if cv2.waitKey(1) & 0xFF == ord('q'):
                print("User pressed 'q'.")
                break

        # Save periodically
        now = time.time()
        if save_count < args.max_saves and now - last_save_at >= args.save_every:
            save_count += 1
            path = os.path.join(output_dir, f"yolo_depth_{save_count}.jpg")
            cv2.imwrite(path, annotated)
            saved_paths.append(path)
            print(f"  [{save_count}/{args.max_saves}] Saved: {path}")
            last_save_at = now

    cap.release()
    if not args.no_display:
        cv2.destroyAllWindows()

    print()
    print(f"Done! Saved {len(saved_paths)} images to {output_dir}")
    if saved_paths:
        print("Output files:")
        for p in saved_paths:
            print(f"  {p}")


if __name__ == "__main__":
    main()


path: scripts/yolo_detection.py
content: #!/usr/bin/env python3
"""
YOLO Detection - Webcam + YOLO model for object detection.
Supports all YOLO models (detection, segmentation, pose, OBB, classification).
"""
import argparse
import os
import sys
import time
from pathlib import Path

import cv2
import torch
from ultralytics import YOLO

# ── Platform Detection ───────────────────────────────────────────────────────
IS_WINDOWS = sys.platform == "win32" or sys.platform == "cygwin"
IS_LINUX = sys.platform.startswith("linux")
IS_MACOS = sys.platform == "darwin"

# ── Default Paths ─────────────────────────────────────────────────────────────
def get_default_model_path():
    """Get default YOLO model path from the shared model cache."""
    return os.path.join(os.path.expanduser("~"), ".cache", "huggingface", "modules",
                        "camera-yolo-operator", "yolo26s.pt")


def get_default_output_dir():
    """Get default output directory in workspace projects folder."""
    workspace = os.environ.get("OPENCLAW_WORKSPACE",
                               os.path.join(os.path.expanduser("~"), ".openclaw", "workspace"))
    return os.path.join(workspace, "projects", "camera-yolo-operator", "detection")


def get_skill_dir():
    """Get the skill directory based on this script's location."""
    return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


def get_model_search_paths():
    """Get search paths for YOLO models, in priority order."""
    workspace = os.environ.get("OPENCLAW_WORKSPACE",
                               os.path.join(os.path.expanduser("~"), ".openclaw", "workspace"))
    return [
        # 1. Explicit path from env or default
        os.environ.get("YOLO_MODEL_PATH", ""),
        # 2. Shared model cache (skill folders must not contain model files)
        os.path.join(os.path.expanduser("~"), ".cache", "huggingface", "modules",
                     "camera-yolo-operator"),
        # 3. Legacy workspace yolo folder
        os.path.join(workspace, "yolo"),
        # 4. Current working directory
        ".",
    ]


def find_model(model_name_or_path):
    """
    Find model file given a name or path.
    Searches in standard locations.
    """
    path = Path(model_name_or_path)

    # If it's an absolute path and exists, use it
    if path.is_absolute() and path.exists():
        return str(path)

    # If it's just a filename, search in standard locations
    if path.suffix.lower() in ['.pt', '.pth', '.yolo']:
        for search_dir in get_model_search_paths():
            if not search_dir:
                continue
            candidate = Path(search_dir) / path.name
            if candidate.exists():
                return str(candidate)

    # Try as-is path
    if path.exists():
        return str(path)

    return model_name_or_path  # Return original, let YOLO handle error


# ── Camera Opening ────────────────────────────────────────────────────────────
def open_camera(index: int):
    """Try multiple backends to open the camera."""
    if IS_WINDOWS:
        cap = cv2.VideoCapture(index, cv2.CAP_DSHOW)
        if cap.isOpened():
            return cap
        cap.release()

    if IS_LINUX:
        cap = cv2.VideoCapture(index, cv2.CAP_V4L2)
        if cap.isOpened():
            return cap
        cap.release()

    if IS_MACOS:
        cap = cv2.VideoCapture(index, cv2.CAP_AVFOUNDATION)
        if cap.isOpened():
            return cap
        cap.release()

    cap = cv2.VideoCapture(index)
    return cap


# ── Main ──────────────────────────────────────────────────────────────────────
def main():
    parser = argparse.ArgumentParser(
        description="YOLO object detection on webcam feed with periodic snapshots."
    )
    parser.add_argument("--camera-index", type=int, default=0, help="Camera index (default: 0)")
    parser.add_argument("--seconds", type=int, default=30, help="Runtime in seconds (default: 30)")
    parser.add_argument("--save-every", type=float, default=10.0,
                        help="Save snapshot every N seconds (default: 10.0)")
    parser.add_argument("--max-saves", type=int, default=3,
                        help="Maximum number of saves (default: 3)")
    parser.add_argument("--conf", type=float, default=0.5,
                        help="YOLO confidence threshold (default: 0.5)")
    parser.add_argument("--yolo-model", default=None,
                        help=f"YOLO model path or name (default: yolo26s.pt in model cache)")
    parser.add_argument("--output-dir", default=None,
                        help=f"Output directory (default: ~/.../projects/camera-yolo-operator/detection)")
    parser.add_argument("--device", default="0",
                        help="Device: '0' for GPU, 'cpu' for CPU (default: '0')")
    parser.add_argument("--no-display", action="store_true",
                        help="Don't display window (headless mode)")
    args = parser.parse_args()

    # Resolve model path
    model_path = args.yolo_model or get_default_model_path()
    model_path = find_model(model_path)

    # Setup output
    output_dir = args.output_dir or get_default_output_dir()
    os.makedirs(output_dir, exist_ok=True)

    # Check model exists
    if not os.path.exists(model_path):
        print(f"ERROR: Model not found: {model_path}", file=sys.stderr)
        print(f"Search paths: {get_model_search_paths()}", file=sys.stderr)
        print(f"\nRun scripts/download_models.sh to download models.", file=sys.stderr)
        sys.exit(1)

    # Load model
    print(f"[YOLO Detection] Starting...")
    print(f"  Model:      {model_path}")
    print(f"  Camera:     {args.camera_index}")
    print(f"  Runtime:    {args.seconds}s")
    print(f"  Device:     {args.device}")
    print(f"  Output:     {output_dir}")
    print(f"  PyTorch:    {torch.__version__}")
    print(f"  CUDA:       {torch.cuda.is_available()}")
    print()

    yolo = YOLO(model_path)

    # Open camera
    cap = open_camera(args.camera_index)
    if not cap.isOpened():
        print(f"ERROR: Cannot open camera {args.camera_index}", file=sys.stderr)
        sys.exit(2)

    # Warmup
    print("Warming up...")
    for _ in range(5):
        cap.read()

    # Run detection
    start = time.time()
    last_save_at = start
    save_count = 0
    detected_labels = set()
    saved_paths = []

    print(f"Running for {args.seconds}s, saving every {args.save_every}s...")
    print()

    window_name = "YOLO Detection (Press 'q' to quit)"

    while time.time() - start < args.seconds:
        ok, frame = cap.read()
        if not ok:
            print("Camera read error, exiting.")
            break

        # Run detection
        results = yolo(frame, conf=args.conf, verbose=False, device=args.device)
        annotated = results[0].plot()

        # Collect labels
        if results and results[0].boxes is not None and len(results[0].boxes) > 0:
            for cls in results[0].boxes.cls.cpu().numpy():
                detected_labels.add(results[0].names[int(cls)])

        # Display
        if not args.no_display:
            cv2.imshow(window_name, annotated)
            if cv2.waitKey(1) & 0xFF == ord('q'):
                print("User pressed 'q', stopping.")
                break

        # Save periodically
        now = time.time()
        if save_count < args.max_saves and now - last_save_at >= args.save_every:
            save_count += 1
            filename = f"yolo_det_{save_count}.jpg"
            path = os.path.join(output_dir, filename)
            cv2.imwrite(path, annotated)
            saved_paths.append(path)
            print(f"  [{save_count}/{args.max_saves}] Saved: {path}")
            last_save_at = now

    cap.release()
    if not args.no_display:
        cv2.destroyAllWindows()

    print()
    print(f"Done! Captured {len(saved_paths)} images.")
    print(f"Detected objects: {', '.join(sorted(detected_labels)) if detected_labels else 'none'}")
    print()
    for p in saved_paths:
        print(f"  {p}")


if __name__ == "__main__":
    main()


path: scripts/yolo_pedestrian_tracker.py
content: #!/usr/bin/env python3
"""
YOLO Object Tracker — 通用目标轨迹跟踪

Usage:
    python3 scripts/yolo_pedestrian_tracker.py --source 0 --seconds 30
    python3 scripts/yolo_pedestrian_tracker.py --source video.mp4 --track --output-video out.mp4
"""

import argparse
import cv2
import numpy as np
import os
import sys
from collections import defaultdict
from datetime import datetime

WORKSPACE = os.path.expanduser("~/.openclaw/workspace")
OUTPUT_DIR = os.path.join(WORKSPACE, "projects", "camera-yolo-operator", "pedestrian_track")

YOLO_MODEL_PATH = os.environ.get(
    "YOLO_MODEL_PATH",
    os.path.join(os.path.expanduser("~"), ".cache", "huggingface", "modules", "camera-yolo-operator", "yolo26s.pt")
)


def parse_args():
    parser = argparse.ArgumentParser(description="YOLO Object Tracker — 通用目标轨迹跟踪")
    parser.add_argument("--source", default="0", help="视频源: 0=摄像头, 或视频文件路径")
    parser.add_argument("--seconds", type=int, default=30, help="运行秒数（仅摄像头模式有效）")
    parser.add_argument("--yolo-model", default=YOLO_MODEL_PATH, help="YOLO 模型路径")
    parser.add_argument("--classes", nargs="+", type=int, default=None,
                        help="要检测的目标类别（默认全部检测）")
    parser.add_argument("--conf", type=float, default=0.5, help="检测置信度")
    parser.add_argument("--device", default="0", help="设备: 0=GPU, cpu=CPU")
    parser.add_argument("--save-every", type=int, default=5, help="截图保存间隔秒数（仅视频文件模式）")
    parser.add_argument("--max-saves", type=int, default=10, help="最大截图数量")
    parser.add_argument("--output-dir", default=OUTPUT_DIR, help="输出目录")
    parser.add_argument("--trail-length", type=int, default=30, help="轨迹线长度（帧数）")
    parser.add_argument("--track", action="store_true", help="开启轨迹跟踪模式（默认关闭）")
    parser.add_argument("--output-video", default=None, help="输出视频路径（不填则不保存视频）")
    return parser.parse_args()


def main():
    args = parse_args()
    os.makedirs(args.output_dir, exist_ok=True)

    # 初始化 YOLO
    from ultralytics import YOLO
    print(f"[INFO] Loading YOLO: {args.yolo_model}")
    model = YOLO(args.yolo_model)
    names = model.names  # 类别名称映射

    # 初始化 Supervision 标注器
    import supervision as sv
    box_annotator = sv.BoxAnnotator()
    label_annotator = sv.LabelAnnotator()

    byte_tracker = None
    trace_annotator = None
    if args.track:
        trace_annotator = sv.TraceAnnotator(
            trace_length=args.trail_length,
            position=sv.Position.CENTER,
        )
        byte_tracker = sv.ByteTrack(
            track_activation_threshold=0.25,
            lost_track_buffer=30,
            minimum_matching_threshold=0.8,
            frame_rate=30,
        )

    # 打开视频源
    source = int(args.source) if args.source.isdigit() else args.source
    print(f"[INFO] Opening source: {source}")
    cap = cv2.VideoCapture(source)
    if not cap.isOpened():
        print(f"[ERROR] Cannot open source: {source}")
        sys.exit(1)

    fps = cap.get(cv2.CAP_PROP_FPS) or 30.0
    width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
    height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
    is_video_file = not args.source.isdigit()
    total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) if is_video_file else None
    print(f"[INFO] Resolution: {width}x{height}, FPS: {fps}")
    if is_video_file:
        print(f"[INFO] Video file, total frames: {total_frames}")

    # 输出视频写入器
    writer = None
    if args.output_video:
        writer = cv2.VideoWriter(
            args.output_video,
            cv2.VideoWriter_fourcc(*"mp4v"),
            fps,
            (width, height),
        )
        print(f"[INFO] Writing annotated video to: {args.output_video}")

    # 统计变量
    start_time = cv2.getTickCount()
    frame_idx = 0
    save_count = 0
    last_save_tick = start_time
    last_save_frame = 0

    # 独立目标统计
    unique_tracks = {}       # track_id -> class_id（去重后的独立目标）
    track_history = defaultdict(list)  # track_id -> [(frame, cx, cy), ...]

    print(f"[INFO] Mode: {'tracking ON' if args.track else 'detection only'}, classes: all, starting...")

    while True:
        ret, frame = cap.read()
        if not ret:
            print("[INFO] Video ended or read error")
            break

        frame_idx += 1
        elapsed = (cv2.getTickCount() - start_time) / cv2.getTickFrequency()

        # YOLO 检测
        results = model(frame, classes=args.classes, conf=args.conf, device=args.device, verbose=False)
        detections = results[0]

        if detections.boxes is None or len(detections.boxes) == 0:
            det_obj = sv.Detections(
                xyxy=np.empty((0, 4)),
                confidence=np.empty((0,)),
                class_id=np.empty((0,), dtype=int),
            )
        else:
            det_obj = sv.Detections(
                xyxy=detections.boxes.xyxy.cpu().numpy(),
                confidence=detections.boxes.conf.cpu().numpy(),
                class_id=detections.boxes.cls.cpu().numpy().astype(int),
            )

        # 跟踪模式：ByteTrack 更新 ID
        if args.track and byte_tracker is not None:
            det_obj = byte_tracker.update_with_detections(det_obj)

            # 记录独立目标和轨迹
            if det_obj.tracker_id is not None:
                for tid, (x1, y1, x2, y2), cid in zip(
                        det_obj.tracker_id, det_obj.xyxy, det_obj.class_id):
                    tid_int = int(tid)
                    unique_tracks[tid_int] = int(cid)
                    cx, cy = int((x1 + x2) / 2), int((y1 + y2) / 2)
                    track_history[tid_int].append((frame_idx, cx, cy))

        # 标注
        annotated = frame.copy()
        if args.track and trace_annotator is not None:
            annotated = trace_annotator.annotate(annotated, det_obj)

        annotated = box_annotator.annotate(annotated, det_obj)

        if args.track and det_obj.tracker_id is not None:
            labels = [f"#{int(tid)}" for tid in det_obj.tracker_id]
            annotated = label_annotator.annotate(annotated, det_obj, labels)

        # 写入输出视频
        if writer is not None:
            writer.write(annotated)

        # 实时显示
        cv2.imshow("Tracker [q=quit]" if args.track else "Detector [q=quit]", annotated)

        # 保存截图（视频文件模式，每隔 save_every 秒保存一张）
        if is_video_file and args.output_video:
            save_interval_frames = int(fps * args.save_every)
            frames_since_last_save = frame_idx - last_save_frame
            if frames_since_last_save >= save_interval_frames and save_count < args.max_saves:
                ts = datetime.now().strftime("%Y%m%d_%H%M%S")
                out_path = os.path.join(args.output_dir, f"frame_{ts}.jpg")
                cv2.imwrite(out_path, annotated)
                print(f"[SAVE] {out_path}")
                save_count += 1
                last_save_frame = frame_idx

        # 超时退出（仅摄像头模式）
        if not is_video_file and elapsed >= args.seconds:
            print(f"[INFO] Time limit ({args.seconds}s) reached")
            break

        key = cv2.waitKey(1) & 0xFF
        if key in (ord('q'), 27):
            print("[INFO] Quit by user")
            break

    cap.release()
    if writer:
        writer.release()
    cv2.destroyAllWindows()

    # 打印统计摘要（独立目标数量）
    print(f"\n========== Video Summary ==========")
    print(f"Frames: {frame_idx}, Duration: {frame_idx / fps:.1f}s")

    if args.track and unique_tracks:
        # 按类别汇总独立目标数量
        class_track_counts = defaultdict(list)
        for tid, cid in unique_tracks.items():
            class_track_counts[names[cid]].append(tid)

        total_unique = len(unique_tracks)
        print(f"\n独立目标数量（共 {total_unique} 个）：")
        for cls_name, tids in sorted(class_track_counts.items(), key=lambda x: -len(x[1])):
            print(f"  {cls_name}: {len(tids)} 个")

        # 跟踪最久的 Top 5
        if track_history:
            print(f"\n跟踪最久的目标 Top 5：")
            for tid, history in sorted(track_history.items(), key=lambda x: -len(x[1]))[:5]:
                cid = unique_tracks.get(tid, '?')
                name = names.get(cid, str(cid))
                print(f"  #{tid} ({name}): {len(history)} 帧")

    print(f"\nOutput video: {args.output_video}" if args.output_video else "(no video saved)")
    if save_count > 0:
        print(f"Screenshots: {save_count} frames saved to {args.output_dir}")


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)