cmvr_edge_ai/detect_server/README.md
2026-07-21 12:17:13 +08:00

368 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# PPE 检测流水线
`configs/edge_ai.yaml` 中的 `detection` Pipeline 是当前可运行的园区施工安全装备检测
链路。它与 `talk` Pipeline 共用一个部署 YAML当前只启用 Construction PPE 模型和
8081 告警平台;六类模型与 8082 模拟平台的实现保留但不实例化:
```text
cmvr-es CameraService
-> H264/H265 ImageFrame/v1
-> media.video_decoder.pyav@1
-> BGR8 ImageFrame/v1
-> construction-ppe-yolov8@1 -> DetectionResult/v1
-> detection.repeat_gate@1 -> DetectionAlert/v1
-> POST 127.0.0.1:8081/v1/detection-alerts
```
## 安装与启动
从仓库根目录执行一键安装。默认 profile 安装锁定的 CPU 检测环境、生成 cmvr-es
bindings并验证最小测试 fixture 与统一部署配置中的检测链路:
```bash
cd /home/xtkuang/Projects/cmvr/cmvr_edge_ai
bash scripts/bootstrap.sh
```
cmvr-es 不在相邻的 `../cmvr-es` 时指定实际路径:
```bash
bash scripts/bootstrap.sh \
--cmvr-es-root /home/xtkuang/Projects/cmvr/cmvr-es
```
环境固定 Python 3.10、Ultralytics 8.4.31、PyTorch 2.7.0 CPU 和 torchvision
0.22.0,并通过 `image` extra 安装 Pillow 以生成告警图片;所有具体包版本记录在
仓库的 `uv.lock` 中。GPU/Jetson 需要单独适配与驱动或 JetPack 匹配的 PyTorch
wheel不能直接复用 `detection-cpu` profile。手动组合依赖时必须显式增加
`--extra image`,不能只依赖 YOLO 间接安装 Pillow。
直接编辑 `configs/edge_ai.yaml``detection` Pipeline 的部署参数:
```yaml
endpoints:
cmvr_es:
target: 127.0.0.1:50052
ppe_alert_platform:
base_url: http://127.0.0.1:8081
pipelines:
detection:
nodes:
camera:
with:
device_id: right_hand_cam
stream_log_interval_s: 5
detector:
with:
attach_frame: true
inference_log_interval_s: 5
model_options:
weights: models/detection/construction-ppe-yolov8/v1/best.pt
device: cpu
repeat_gate:
with:
alert_image:
enabled: true
jpeg_quality: 85
alert_platform:
with:
endpoint: ppe_alert_platform
failure_mode: log_and_drop
```
上述相对权重路径按进程启动时的当前工作目录(`cwd`)解析,不是按
`configs/edge_ai.yaml` 所在目录解析。下面的 validate、run 和 Viewer 命令都应先
`cd /home/xtkuang/Projects/cmvr/cmvr_edge_ai`;如果必须在其他 `cwd` 启动,请在
YAML 中使用正确的绝对权重路径。
完成配置后启动,不需要再通过 shell `export` 传入这些值:
```bash
uv run --no-sync cmvr-edge-ai models
uv run --no-sync cmvr-edge-ai validate \
-c configs/edge_ai.yaml \
--pipeline detection
uv run --no-sync cmvr-edge-ai run -c configs/edge_ai.yaml \
--pipeline detection \
--log-level INFO \
--log-format json
```
运行时会看到类似下面两类 JSON 日志:
```json
{"level":"INFO","logger":"cmvr_edge_ai.detection.operator","message":"detection model loaded node=detector model=construction-ppe-yolov8@1 ..."}
{"level":"INFO","logger":"cmvr_edge_ai.detection.operator","message":"detection inference node=detector model=construction-ppe-yolov8@1 total_frames=1 window_frames=1 window_detections=2 hit_labels=No-Helmet:2 ..."}
```
- `detection model loaded`checkpoint 已成功加载并完成标签顺序校验;当前 detector
应出现一条;
- `detection inference`:模型确实收到解码帧并执行了 `predict`;第一帧立即输出,
后续按配置周期聚合;
- `window_detections=0 hit_labels=none`:模型在工作,但本周期没有高于配置阈值的命中;
- 只有 loaded、长期没有 inference优先检查 cmvr-es 相机流、decoder 和关键帧;
- 短时观察可把 `inference_log_interval_s` 改为 `1` 秒,长期运行建议 `30``60` 秒,
省略该字段会关闭周期推理日志。
运行前确认 cmvr-es 已启用 `right_hand_cam`、Construction PPE 权重存在,并且 8081
平台接受 `POST /v1/detection-alerts`。默认 profile 下 `model_options.device` 应设为
`cpu`;只有完成设备专用的 CUDA/Jetson PyTorch 环境适配后,才能改为 `cuda:0` 等值。
## 实时画框 Demo
`configs/debug/detection_viewer.yaml``show_detections.py` 提供一个不访问 HTTP 平台的
独立调试链路:
```text
cmvr-es CameraService -> PyAV decoder -> YOLO detector -> OpenCV window
```
先编辑 `configs/debug/detection_viewer.yaml` 中的远端 cmvr-es 地址、相机 ID 和权重路径:
```yaml
endpoints:
cmvr_es:
target: 192.168.0.119:50052
pipelines:
detection_show:
nodes:
camera:
with:
device_id: wrist_cam
detector:
with:
max_fps: 10
model_options:
weights: /absolute/path/to/best.pt
device: cpu
```
`max_fps: 10` 表示最多每秒执行 10 次推理;视频流更快时,中间的已解码帧通过
`realtime_latest + drop_oldest` 丢弃,以保持低延迟。`attach_frame: true` 已在 Demo 配置
中启用viewer 因而能拿到与本次推理严格对应的原图并绘制 bounding box。
只检查配置和插件连线,不连接相机、不加载模型、也不创建窗口:
```bash
uv run --no-sync python detect_server/show_detections.py \
--config configs/debug/detection_viewer.yaml \
--validate-only
```
启动实时显示:
```bash
uv run --no-sync python detect_server/show_detections.py \
--config configs/debug/detection_viewer.yaml \
--pipeline detection_show \
--log-level INFO \
--log-format json
```
相机 Source 会先调用 `StartCamera`,成功后再建立 gRPC 视频流。窗口显示每个实际推理
结果,即使本帧没有检测框也会刷新;按 `q`、`Q`、`Esc` 或关闭窗口可安全退出。这个
Demo 直接订阅 detector 输出,刻意绕过 `repeat_gate` 和 HTTP Sink因此只用于观察模型
效果,不代表某条告警规则已满足。
OpenCV 窗口出现在运行命令的机器上。无桌面的边缘设备不能直接显示;通过 SSH 运行时
需要启用 X11 转发并确保 `DISPLAY` 可用,否则程序会给出明确错误并退出。依赖缺失时先
执行 `bash scripts/bootstrap.sh`
## 模型与标签
`detection.model@1` 不绑定某一个框架;它通过 `DetectionModelRegistry` 查找配置中的
`model`。每个 `DetectionModelSpec` 注册版本化模型 ID、模型名称、backend 和有序
`supported_labels`。注册表当前包含两个内置模型,但 Pipeline 只引用第一个:
- [`construction-ppe-yolov8@1`](../models/detection/construction-ppe-yolov8/v1/README.md)
对应制品目录 `v1`19 类,包含用于违规告警的 `No-*` 标签;
- [`ppe-6classes-yolov8n@1`](../models/detection/ppe-6classes-yolov8n/v1/README.md)
对应制品目录 `v1`,包含 `Gloves`、`Vest`、`goggles`、`helmet`、`mask`、
`safety_shoe` 六个正向装备标签。
模型 ID 尾部的 `@1` 与制品目录的 `v1` 对应;运行时不会根据 ID 自动拼接文件
路径,仍由 YAML 中的 `model_options.weights` 显式指定。训练信息、完整标签
顺序、性能、限制和许可信息请查看各自的 model card。
可用 `cmvr-edge-ai models` 核对 ID、名称、backend 和标签顺序。六类模型只表达
“检测到某件装备”,不包含 `Person``No-*` 类,也不执行人员/PPE 关联;所以它
不能直接判断某个人缺少装备。需要这种语义时,仍应增加人员检测、空间关联和缺失
判定节点,不能把“没有检测到 helmet”直接当成“人员未戴安全帽”。
检测节点的关键参数:
| 参数 | 语义 |
|---|---|
| `model` | 必须是已注册的精确模型 ID |
| `detect_labels` | 本次部署需要的标签;省略表示模型的全部注册标签 |
| `confidence` | 所有选中标签的默认置信度阈值 |
| `label_confidence` | 可选的逐标签阈值覆盖 |
| `max_fps` | 推理启动频率上限;过密的解码帧会被跳过 |
| `inference_log_interval_s` | 可选推理摘要周期(秒);首帧立即输出,省略表示关闭周期日志 |
| `attach_frame` | 是否让 `DetectionResult` 临时附带对应解码帧;生成告警图片时必须为 `true` |
| `model_options` | backend 私有选项;当前 YOLO 支持 weights、device、imgsz、iou、half、max_det、agnostic_nms |
配置阶段会拒绝未知模型、模型不支持的 `detect_labels`、重复标签和非法阈值。YOLO 加载 checkpoint 时还会严格比较 checkpoint 的实际标签及顺序与注册信息,避免类别编号静默错位。
## 六类逐推理结果分支
这一分支当前默认关闭。恢复后,六类 detector 不连接 repeat gate而是把每次完成推理产生的
`DetectionResult/v1` 直接交给第二个 HTTP Sink。其 POST Envelope 的
`schema``DetectionResult/v1``payload` 示例为:
```json
{
"detections": [
{
"label": "helmet",
"confidence": 0.91,
"box": {"x_min": 120.0, "y_min": 60.0, "x_max": 250.0, "y_max": 220.0},
"track_id": null
}
],
"model_id": "ppe-6classes-yolov8n@1",
"inference_ms": 34.2,
"model_name": "PPE Detection YOLOv8n (6 Classes)"
}
```
该结构刻意不同于原分支的 `DetectionAlert/v1`:它没有 `rule_id`、`hit_count`、
`event_id``image`,空检测帧也会以空 `detections` 数组上报。`cooldown_ms` 是
repeat gate 的内部规则配置,不属于 `DetectionAlert` payload。由于
payload 没有 event IDHTTP Sink 的 `Idempotency-Key` 回退使用 Envelope
`trace_id`。这只是模拟的逐结果接口;若平台要求另一套字段命名或嵌套,需要增加
平台专用转换节点/Sink而不是仅修改 endpoint。
## 重复触发规则
`detection.repeat_gate@1` 按规则维护滑动时间窗口。规则字段如下:
| 参数 | 语义 |
|---|---|
| `id` | 唯一规则 ID写入告警 |
| `labels` | 任一标签匹配即视为该规则在当前帧命中 |
| `min_confidence` | 规则侧最低置信度,可高于 detector 阈值 |
| `min_hits` | 触发所需的不同帧数 |
| `window_ms` | 上述命中必须落入的时间窗口 |
| `cooldown_ms` | 告警后的静默期;期间不累计,结束后重新计数 |
| `scope` | `source` 按相机统计;`track` 按相机内的 `track_id` 分别统计 |
一次命中按 `(source_id, sequence)` 的不同视频帧计算:同一帧出现多个相同标签框只算一次,重复投递同一帧也不会增加计数。一个规则即使配置多个标签,同一 scope 在一帧内仍只增加一次。`time_source` 可选 `captured`、`received` 或优先采集时间的 `auto`;当前 cmvr-es 成功帧通常没有 `header.timestamp`,示例显式使用 `received`
`alert_image.enabled` 默认为 `false`;启用后要求 detector 同时配置
`attach_frame: true`。`jpeg_quality` 是 `1..95` 的整数,默认 85。repeat gate
只在规则达到 `min_hits`、准备输出告警时,才用 Pillow 在当前阈值帧上绘制匹配的
bounding boxes、标签和置信度并编码 JPEG不会为每个推理结果都渲染。告警里的
`detections` 和图片框均来自阈值帧;窗口内更早的命中只参与次数、时间范围和
`max_confidence` 统计。
同一阈值帧同时触发多条规则或多个 track 时repeat gate 对这些告警相关框取并集,
只画框并编码一次,然后让本帧产生的告警共享该 JPEG避免在边缘端重复编码。
非有限坐标、反向/退化框、非法置信度等 detection 会在规则计数前被忽略,并记录在
`invalid_detections` 健康指标中,避免 `NaN/Inf` 令整个 HTTP JSON 告警无法发送。
当前 YOLO adapter 是逐帧检测,不执行人员跟踪,输出的 `track_id``None`。因此示例使用 `scope: source`;如果业务要求“同一个人连续多次违规”,必须在 detector 和 repeat gate 之间加入 tracker 及人员/PPE 关联节点,之后才能使用 `scope: track`
## 相机启动与视频流日志
cmvr-es 的 `StartCamera``GetRGBImageStream` 是两个不同阶段:前者打开物理相机,
后者只启动编码和流式传输。连接器在每个首次连接或重连 session 中严格执行:
```text
StartCamera -> 检查 feedback.header.success -> GetRGBImageStream -> 等待首帧
```
`StartCamera` 使用 `cmvr_es.timeout_s`RPC 异常或 `header.success=false` 都进入同一套
指数退避重连。正常关闭只取消当前 stream不自动调用设备级 `StopCamera`,避免影响
同一相机的其他客户端。
使用 `--log-level INFO --log-format json` 时,按顺序关注:
- `camera source configured`endpoint、device 和超时配置完成;
- `camera start requested/succeeded`StartCamera 已被调用并成功;
- `camera stream opening`:流请求已创建,正在等待第一帧;
- `camera stream first frame`:已经收到真实编码帧,包含 codec、尺寸、字节数和远程序号
- `camera stream progress`:每 `stream_log_interval_s` 秒输出帧数、FPS、bitrate、关键帧、
最后序号和 `last_frame_age_s`;即使完全没帧也会输出 `first_frame_received=false`
- `camera stream disconnected`:包含错误类型、重连次数和退避时间;
- `camera stream closed`、`camera source stopped`:当前 session 和本地 Source 已清理。
INFO 日志不会打印帧二进制。示例设置 `stream_log_interval_s: 5` 便于联调,正式部署
可改为 3060 秒。
## 视频连续性和背压
H264/H265 是有参考关系的编码流,解码前不能任意丢包:
- `camera -> decoder` 使用 `video_contiguous + block`,保持 edge-ai 内部已经接受的数据连续;
- decoder 在新 session、编码参数变化、sequence gap 或解码错误后释放上下文,等待下一个关键帧;
- `decoder -> detector` 已经是完整 BGR 图像,使用 `realtime_latest + drop_oldest` 和容量 1以有限内存换取较低实时延迟
- `detector -> repeat_gate``attach_frame: true` 时携带未压缩图像,示例把队列容量限制为 2避免排队的 `DetectionResult` 长时间占用大量内存。
该边使用 `drop_oldest` 偏向低延迟;持续过载时,被丢弃的推理结果不会计入
`min_hits`。如果业务更重视每次推理结果都参与计数,可改为 `overflow: block`,但要
接受延迟向上游传播,并继续保持很小的队列容量。
恢复第二模型时,应从 decoder 输出端口 fan-out以避免第二次相机订阅和第二次
H264/H265 解码;这不会复用模型计算,两个 YOLO 实例仍会分别加载权重并共享应用的
有界线程池。
这个保证只覆盖 edge-ai 内部。当前 cmvr-es 服务端通过 `getLatestEncodedFrame` 读取最新编码数据;如果它在负载或时序竞争下已经跳过参考包,`video_contiguous` 无法恢复丢失内容,而且当前 edge-ai 的本地 sequence 不能可靠暴露这种上游跳包。此时 PyAV 可能报错decoder 会重置并等待关键帧。上线前必须用真实摄像头长时间验证连续性;更稳妥的方案是让 cmvr-es 提供连续 access unit 流,或额外提供原始/JPEG/带显式 discontinuity 的 AI 接口。
相机 Source 默认启用指数退避重连;每次重新订阅生成新的 `session_id`,使 decoder 主动重置。`reconnect_initial_s`、`reconnect_max_s` 和可选 `max_reconnect_attempts` 可配置;省略最大次数表示持续重连直到进程关闭。
## HTTP 输出可靠性
每次规则触发会产生带唯一 `event_id``DetectionAlert/v1`。HTTP Sink 优先把该
`event_id` 放入 `Idempotency-Key`,没有事件 ID 时才回退到 `trace_id`。它对连接/超时
错误和配置的临时 HTTP 状态执行有限指数退避,示例最多尝试 3 次。
`failure_mode` 支持两种明确语义:默认 `raise` 在最终投递失败时让 Pipeline 失败;当前
检测配置使用 `log_and_drop`,最终连接失败、超时或非 2xx 时输出
`HTTP report dropped ... action=drop` WARNING丢弃当前告警并继续处理后续视频帧。
`CancelledError`、序列化错误和未知本地程序异常不会被吞掉。
当前唯一 HTTP 输出将告警发送到
`http://127.0.0.1:8081/v1/detection-alerts`。8082 六类逐推理结果 endpoint 当前未配置。
启用告警图片后POST JSON 的 `payload.image` 结构如下:
```json
{
"media_type": "image/jpeg",
"width": 1280,
"height": 720,
"encoding": "base64",
"data": "/9j/4AAQSk..."
}
```
`width/height` 是 JPEG 的像素尺寸HTTP JSON Sink 将内部 JPEG `bytes` 特判为
上述扁平对象,`encoding` 固定为 `base64`。关闭 `alert_image`,或运行时因第三方
结果未附带帧、坏帧等原因渲染失败时,告警仍会发送且该字段为 `null`。Base64 会
额外增加约三分之一的体积,平台和反向代理需要配置足够的请求体上限。
这不是持久化 outbox`log_and_drop` WARNING 表示该告警已经永久丢失,不会自动补发;
每次告警在放弃前仍会完成有限重试,因此平台离线时 Sink 队列会短暂阻塞。进程崩溃、
断电或告警仍在内存队列中时也可能丢失。平台必须按幂等键去重;对“不可丢告警”的
部署,还需要后续增加有界磁盘 outbox、投递确认和恢复发送。
## 部署风险
- 模型 README 报告某些小样本类别存在漏检,实际园区需要按相机角度、光照、遮挡和距离重新标定置信度与 `min_hits`;规则降噪不能补偿模型系统性漏检。
- 权重目录对权重许可的描述与 Ultralytics runtime/checkpoint 中的 AGPL 信息需要在商业部署前核对;同时确认训练数据来源和权重再分发权利。
- `.pt` 使用 PyTorch checkpoint loader只加载可信构建和受控分发的权重并固定、验证实际 Ultralytics 8.x 版本。
如果检测结果还要驱动机器人,控制路径必须保持:
```text
DetectionResult -> policy -> RobotCommand/v1
-> safety.robot_command_gate@1
-> ApprovedRobotCommand/v1
-> cmvr.grpc.agv_command_sink@1
```
编译器要求 actuator 的每个直接前驱都是安全门,安全门和执行器之间不能插入普通变换节点,也不能存在绕过路径。