cmvr_edge_ai/detect_server/README.md
2026-07-20 16:59:37 +08:00

15 KiB
Raw Blame History

PPE 检测流水线

pipeline.yaml 是当前可运行的园区施工安全装备检测链路。当前只启用 Construction PPE 模型和 8081 告警平台;六类模型与 8082 模拟平台的实现保留但不实例化:

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并验证 smoke 与本检测配置:

cd /home/xtkuang/Projects/cmvr/cmvr_edge_ai
bash scripts/bootstrap.sh

cmvr-es 不在相邻的 ../cmvr-es 时指定实际路径:

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。

直接编辑 detect_server/pipeline.yaml 中的部署参数:

endpoints:
  cmvr_es:
    target: 127.0.0.1:50052
  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: /home/xtkuang/Projects/cmvr/changan_robot/construction-ppe-yolov8/best.pt
            device: cpu
      repeat_gate:
        with:
          alert_image:
            enabled: true
            jpeg_quality: 85
      platform:
        with:
          failure_mode: log_and_drop

完成配置后启动,不需要再通过 shell export 传入这些值:

uv run --no-sync cmvr-edge-ai models
uv run --no-sync cmvr-edge-ai validate \
  -c detect_server/pipeline.yaml \
  --pipeline detection
uv run --no-sync cmvr-edge-ai run -c detect_server/pipeline.yaml \
  --pipeline detection \
  --log-level INFO \
  --log-format 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 loadedcheckpoint 已成功加载并完成标签顺序校验;当前 detector 应出现一条;
  • detection inference:模型确实收到解码帧并执行了 predict;第一帧立即输出, 后续按配置周期聚合;
  • window_detections=0 hit_labels=none:模型在工作,但本周期没有高于配置阈值的命中;
  • 只有 loaded、长期没有 inference优先检查 cmvr-es 相机流、decoder 和关键帧;
  • 短时观察可把 inference_log_interval_s 改为 1 秒,长期运行建议 3060 秒, 省略该字段会关闭周期推理日志。

运行前确认 cmvr-es 已启用 right_hand_cam、Construction PPE 权重存在,并且 8081 平台接受 POST /v1/detection-alerts。默认 profile 下 model_options.device 应设为 cpu;只有完成设备专用的 CUDA/Jetson PyTorch 环境适配后,才能改为 cuda:0 等值。

模型与标签

detection.model@1 不绑定某一个框架;它通过 DetectionModelRegistry 查找配置中的 model。每个 DetectionModelSpec 注册版本化模型 ID、模型名称、backend 和有序 supported_labels。注册表当前包含两个内置模型,但 Pipeline 只引用第一个:

  • construction-ppe-yolov8@119 类,包含原分支用于违规告警的 No-* 标签;
  • ppe-6classes-yolov8n@1GlovesVestgoggleshelmetmasksafety_shoe 六个正向装备标签。

可用 cmvr-edge-ai models 核对 ID、名称、backend 和标签顺序。六类模型只表达 “检测到某件装备”,不包含 PersonNo-* 类,也不执行人员/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 的 schemaDetectionResult/v1payload 示例为:

{
  "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_idhit_countevent_idimage,空检测帧也会以空 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 可选 capturedreceived 或优先采集时间的 auto;当前 cmvr-es 成功帧通常没有 header.timestamp,示例显式使用 received

alert_image.enabled 默认为 false;启用后要求 detector 同时配置 attach_frame: truejpeg_quality1..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_idNone。因此示例使用 scope: source;如果业务要求“同一个人连续多次违规”,必须在 detector 和 repeat gate 之间加入 tracker 及人员/PPE 关联节点,之后才能使用 scope: track

相机启动与视频流日志

cmvr-es 的 StartCameraGetRGBImageStream 是两个不同阶段:前者打开物理相机, 后者只启动编码和流式传输。连接器在每个首次连接或重连 session 中严格执行:

StartCamera -> 检查 feedback.header.success -> GetRGBImageStream -> 等待首帧

StartCamera 使用 cmvr_es.timeout_sRPC 异常或 header.success=false 都进入同一套 指数退避重连。正常关闭只取消当前 stream不自动调用设备级 StopCamera,避免影响 同一相机的其他客户端。

使用 --log-level INFO --log-format json 时,按顺序关注:

  • camera source configuredendpoint、device 和超时配置完成;
  • camera start requested/succeededStartCamera 已被调用并成功;
  • 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 closedcamera 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_gateattach_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_sreconnect_max_s 和可选 max_reconnect_attempts 可配置;省略最大次数表示持续重连直到进程关闭。

HTTP 输出可靠性

每次规则触发会产生带唯一 event_idDetectionAlert/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 结构如下:

{
  "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 会 额外增加约三分之一的体积,平台和反向代理需要配置足够的请求体上限。

这不是持久化 outboxlog_and_drop WARNING 表示该告警已经永久丢失,不会自动补发; 每次告警在放弃前仍会完成有限重试,因此平台离线时 Sink 队列会短暂阻塞。进程崩溃、 断电或告警仍在内存队列中时也可能丢失。平台必须按幂等键去重;对“不可丢告警”的 部署,还需要后续增加有界磁盘 outbox、投递确认和恢复发送。

部署风险

  • 模型 README 报告某些小样本类别存在漏检,实际园区需要按相机角度、光照、遮挡和距离重新标定置信度与 min_hits;规则降噪不能补偿模型系统性漏检。
  • 权重目录对权重许可的描述与 Ultralytics runtime/checkpoint 中的 AGPL 信息需要在商业部署前核对;同时确认训练数据来源和权重再分发权利。
  • .pt 使用 PyTorch checkpoint loader只加载可信构建和受控分发的权重并固定、验证实际 Ultralytics 8.x 版本。

如果检测结果还要驱动机器人,控制路径必须保持:

DetectionResult -> policy -> RobotCommand/v1
                -> safety.robot_command_gate@1
                -> ApprovedRobotCommand/v1
                -> cmvr.grpc.agv_command_sink@1

编译器要求 actuator 的每个直接前驱都是安全门,安全门和执行器之间不能插入普通变换节点,也不能存在绕过路径。