diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index e69de29..0000000 diff --git a/.gitignore b/.gitignore index 7bcf26b..2d33480 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ __pycache__/ *.py[cod] *.egg-info/ +/build/ +/dist/ diff --git a/README.md b/README.md index 277549f..b5fe327 100644 --- a/README.md +++ b/README.md @@ -11,19 +11,28 @@ - cmvr-es RGB 相机 gRPC Source(带退避上限的指数重连)、AGV gRPC Sink; - PyAV H264/H265 有状态解码、通用检测模型节点和重复命中规则节点; - Construction PPE、六类 PPE YOLOv8n、People-Talking YOLOv8x 与 YOLOv8n Mobile Phone 模型注册,以及按标签、置信度和最大 FPS 的部署配置; -- 平台 HTTP JSON Sink,支持有限重试、`raise/log_and_drop` 失败策略,并使用告警 - `event_id` 作为幂等键; +- 平台 HTTP JSON Sink,支持有限重试、`raise/log_and_drop` 失败策略,使用告警 + `event_id` 作为幂等键,并可把已配置的 gRPC endpoint IP 作为 `grpc_ip` 上报; - `RobotCommand -> ApprovedRobotCommand` 安全门和无重试的类型化 AGV 命令映射; - 文本/JSON 日志、共享 gRPC Channel 与 HTTP Client; -- 可直接执行的最小测试 fixture,以及包含 PPE/电话使用检测和对话占位链路的统一部署配置。 +- `serve` 被动调用入口、版本化推理契约和能力目录,以及只按稳定业务 `category` + 选择服务端路由的 Detect Client SDK; +- `gauge.analog` 被动调用适配:主服务保持 Python 3.10,ETHZ Analog Gauge Reader + 通过独立的 Python 3.8 uv 环境和常驻二进制协议 worker 运行; +- 纯 ONNX Runtime 的 YOLOv8 推理 backend 与受控导出工具;原 `.pt` `@1` 版本作为 + 导出/回滚源保留,三个部署配置已使用带 manifest/SHA256 校验的 `@2` ONNX 制品; +- 可直接执行的最小测试 fixture,以及包含 PPE/电话使用检测的主动推送部署配置。 -`configs/edge_ai.yaml` 是统一部署配置,其中同时定义 `detection` 和 `talk` 两个 -Pipeline。`detection` 从同一个相机 decoder 并行运行 Construction PPE 与 +`configs/active_detection.yaml` 是主动检测部署配置。它从同一个相机 decoder 并行运行 Construction PPE 与 People-Talking 两个 detector,各自经过独立时间窗口规则后向同一个告警平台上报; `model_id` 和 `rule_id` 用于区分事件。六类 PPE 模型仍保留在注册表中,但不在当前 Pipeline 图中实例化。 -VAD/ASR/LLM/TTS 尚未内置;`talk` 仍使用模拟音频数据,等待 cmvr-es 音频双向流 -proto 落地。 +VAD/ASR/LLM/TTS 尚未内置,因此仓库不再保留不能提供真实能力的 Talk 占位 Pipeline。 + +主动推送与被动调用可以在同一份配置、同一进程中并存:原有相机 Pipeline 持续向平台 +推送事件;被动 Detect Server 则接收单次 HTTP 请求,经服务端拥有的 Pipeline 完成推理并 +返回响应。两者共用版本化契约和能力注册表,但不会把模型 ID、Pipeline ID 或权重路径 +暴露为客户端可选参数。 ## 架构概览 @@ -48,36 +57,44 @@ cmvr_edge_ai/ ├── .python-version # uv 默认 Python 3.10 ├── uv.lock # 所有 profile 的可复现依赖锁 ├── configs/ -│ ├── edge_ai.yaml # detection + talk 统一部署配置 -│ └── debug/ -│ └── detection_viewer.yaml # 远端相机 -> PPE+电话检测 -> 合并画框窗口 -├── detect_server/ -│ ├── README.md # PPE/电话检测链路与 Viewer 使用说明 -│ └── show_detections.py # OpenCV 实时检测结果 Demo +│ ├── README.md # 配置命名、归属与路径规则 +│ ├── active_detection.yaml # 主动检测与平台告警 +│ ├── server_detect.yaml # 被动 PPE/手机检测 HTTP 服务 +│ ├── server_gauge.yaml # 被动 Analog Gauge HTTP 服务 +│ └── detection_viewer.yaml # 远端相机 -> 双模型 -> OpenCV 窗口 +├── server/ +│ ├── detect/ # 被动检测服务说明与 OpenCV Viewer +│ ├── gauge/ # 仪表读数服务及独立 Python 3.8 uv worker +│ └── talk/ # 对话能力边界说明(当前未实现) +├── client/ +│ └── detect/ # 不依赖 Pipeline 的 HTTP Detect Client 示例 ├── models/ │ └── detection/ # 按模型 ID/版本组织的检测模型制品库 -│ ├── construction-ppe-yolov8/v1/ # best.pt + 独立 model card -│ ├── ppe-6classes-yolov8n/v1/ # best.pt + 独立 model card -│ ├── people-talking-yolov8x/v1/ # best.pt + 独立 model card -│ └── yolov8n-mobile-phone/ # Viewer 手机目标检测权重 + model card -├── talk_server/ -│ └── nodes/ # 对话插件预留目录 +│ ├── construction-ppe-yolov8/v1,v2/ # PT 源 + ONNX/manifest/model card +│ ├── ppe-6classes-yolov8n/v1,v2/ # PT 源 + ONNX/manifest/model card +│ ├── people-talking-yolov8x/v1,v2/ # PT 源 + ONNX/manifest/model card +│ └── yolov8n-mobile-phone/v2/ # ONNX;v1 PT 保留在父目录 ├── scripts/ │ ├── bootstrap.sh # 一键创建 uv 环境、生成 bindings 并自检 +│ ├── export_detection_onnx.py # 受控导出检测模型 @2 ONNX 制品与 manifest │ └── generate_cmvr_stubs.py # 从 cmvr-es proto 生成 Python bindings ├── src/cmvr_edge_ai/ │ ├── config/ # 配置模型、加载与环境变量展开 │ ├── core/ # Envelope、组件接口、队列和 DAG 运行时 -│ ├── contracts/ # 协议无关的图像、音频、AI 和控制契约 +│ ├── contracts/ # 内部消息及 inference/catalog 公共 wire 契约 +│ ├── capabilities/ # 稳定 category、服务模式与模型能力注册表 +│ ├── client/ # 远程调用 SDK;当前包含 Detect HTTP Client +│ ├── server/ # 请求 broker、边界节点、能力目录与 HTTP API │ ├── detection/ # 模型注册、视频解码、推理与时间窗口规则 +│ ├── gauge/ # Analog Gauge 主进程 adapter 与 Python 3.8 worker │ ├── plugins/ # 插件注册、发现和内置基础插件 │ ├── connectors/ # cmvr-es 与平台边界连接器 -│ ├── transports/ # gRPC/HTTP 连接池;UDP/QUIC 扩展位置 +│ ├── transports/ # 已实现的 gRPC/HTTP 连接池 │ ├── workers/ # 显式线程 offload 与常驻进程 Worker 工具 │ ├── observability/ # 低开销文本/JSON 日志 │ ├── application.py # 多 Pipeline 与共享网络客户端的所有者 │ ├── compiler.py # 配置到可执行 DAG 的编译器 -│ └── cli.py # validate/run/plugins/models +│ └── cli.py # validate/run/serve/plugins/models └── tests/ └── fixtures/ └── minimal_pipeline.yaml # 不依赖外部服务的框架/CLI 自检配置 @@ -99,7 +116,7 @@ uv --version curl -LsSf https://astral.sh/uv/install.sh | sh ``` -只验证框架和模拟对话链路时,一条命令创建最小环境并自检: +只验证框架核心时,一条命令创建最小环境并自检: ```bash cd /home/xtkuang/Projects/cmvr/cmvr_edge_ai @@ -131,9 +148,9 @@ bootstrap 支持以下环境: | Profile | 安装内容 | 命令 | |---|---|---| -| `core` | 框架核心、最小测试 fixture 和模拟 talk 链路 | `bash scripts/bootstrap.sh --profile core` | -| `detection-cpu` | gRPC、HTTP、PyAV、Pillow 告警图片和固定版本 CPU YOLO;默认值 | `bash scripts/bootstrap.sh` | -| `dev` | `detection-cpu` 加测试和 protobuf codegen 工具,并运行完整测试 | `bash scripts/bootstrap.sh --profile dev` | +| `core` | 框架核心与最小测试 fixture | `bash scripts/bootstrap.sh --profile core` | +| `detection-cpu` | gRPC、HTTP、PyAV、Pillow 和 CPU ONNX Runtime;默认值 | `bash scripts/bootstrap.sh` | +| `dev` | `detection-cpu` 加锁定的 ONNX exporter、测试和 protobuf codegen | `bash scripts/bootstrap.sh --profile dev` | 如果只希望安装、不执行自检,可加 `--skip-check`。完整参数通过以下命令查看: @@ -141,27 +158,133 @@ bootstrap 支持以下环境: bash scripts/bootstrap.sh --help ``` -不要使用 `uv sync --all-extras`:`yolo` 与 `yolo-cpu` 是为不同 PyTorch 来源准备的 -互斥环境。请使用 bootstrap profile,或显式只选择其中一个 extra。 +不要使用 `uv sync --all-extras`:旧 PT adapter 的 `yolo`/`yolo-cpu` 与构建期 +`onnx-export-cpu` 具有互斥依赖来源。请使用 bootstrap profile 或只选择目标 extra。 告警图片的画框和 JPEG 编码由独立的 `image` extra 提供;默认的 `detection-cpu`/`dev` profile 已安装它,手动组合检测环境时也必须选择 `--extra image`。 -CLI 的四个子命令如下: +启动被动 HTTP 服务需要 `server` extra,使用仓库提供的 Detect Client 还需要 `http` +extra。CPU ONNX 检测服务可显式安装以下组合: + +```bash +uv sync --locked --extra server --extra http --extra onnx-cpu +``` + +CLI 的五个子命令如下: | 命令 | 用途 | |---|---| | `validate -c FILE [--pipeline ID]` | 加载配置、展开环境变量、构造插件并校验 DAG;不启动 Pipeline | | `run -c FILE [--pipeline ID]` | 启动选中的 Pipeline;未指定时启动所有 `enabled: true` 的 Pipeline | +| `serve -c FILE` | 启动所有已启用 Pipeline 和远程推理 HTTP API;要求 `server.enabled: true` | | `plugins` | 列出内置插件和已安装 entry point 插件 | | `models` | 列出检测模型 ID、名称、backend 和注册的全部标签 | -`--pipeline` 可以重复传入。`run` 还支持 `--log-level` 和 `--log-format text|json`。配置错误退出码为 `2`,运行错误为 `1`,键盘中断为 `130`。 +`--pipeline` 可以重复传给 `validate` 和 `run`。`run`、`serve` 还支持 `--log-level` 和 +`--log-format text|json`;`serve` 按配置启动全部已启用 Pipeline,不接受客户端或命令行 +覆盖单条 route 的模型选择。配置错误退出码为 `2`,运行错误为 `1`,键盘中断为 `130`。 -生产配置 `configs/edge_ai.yaml` 在一个 YAML 中同时定义 `detection` 和 `talk`。部署时 -建议显式传 `--pipeline detection` 或 `--pipeline talk`,这样进程只加载并运行选中的 -链路;需要同进程运行两条链路时,可以重复传两个 `--pipeline`。如果完全省略 -`--pipeline`,运行时会启动配置中所有 `enabled: true` 的 Pipeline。 +主动推送配置为 `configs/active_detection.yaml`,当前只包含 `detection` Pipeline。 +部署时可以显式传 `--pipeline detection`;省略时会启动配置中所有已启用 Pipeline。 + +## 被动检测 HTTP 服务 + +被动调用使用统一的 `InferenceRequest/v1`、`InferenceResponse/v1` 和能力目录契约。 +阶段一提供两个入口: + +| HTTP 入口 | 用途 | +|---|---| +| `GET /v1/models` | 返回 `cmvr.model-catalog/v1`,分别列出 `active_push` 与 `passive_invoke` 能力及部署状态 | +| `POST /v1/inference` | 接收 `cmvr.inference-request/v1` JSON,并返回 `cmvr.inference-response/v1` 或稳定的错误响应 | + +客户端只提交稳定业务 category,例如 `detect.ppe` 或 `detect.mobile_phone`。客户端不允许 +指定 `model_id`、Pipeline ID 或权重路径;`server.routes.` 与对应 Pipeline 配置 +决定实际模型、制品、provider 和队列容量。这样服务端可以升级实现而不改变调用方协议。 + +从仓库根目录校验并启动示例服务: + +```bash +uv run --no-sync cmvr-edge-ai validate \ + --config configs/server_detect.yaml +uv run --no-sync cmvr-edge-ai serve \ + --config configs/server_detect.yaml \ + --log-level INFO \ + --log-format text +``` + +服务启动后,可在另一终端查询能力并通过 Detect Client 调用 `POST /v1/inference`: + +```bash +curl -sS http://127.0.0.1:8081/v1/models +CMVR_DETECT_BASE_URL=http://127.0.0.1:8081 \ + uv run --no-sync python client/detect/example.py \ + /path/to/image.jpg detect.ppe +``` + +存活与就绪探针分别是 `GET /health/live` 和 `GET /health/ready`。完整服务端配置和链路见 +[server/detect/README.md](server/detect/README.md),SDK 契约与错误分类见 +[client/detect/README.md](client/detect/README.md)。 + +默认示例仅监听 loopback。监听局域网或其他非 loopback 地址时,配置必须包含 +`server.http.bearer_token`,并默认要求 `tls_certfile` 与 `tls_keyfile`;只有受信隔离网络 +才能显式设置 `allow_insecure_remote: true` 使用明文 HTTP。除 `/health/live` 和 +`/health/ready` 外,所有 HTTP 入口都要求 `Authorization: Bearer `。 + +被动调用链采用 `request/block` 单线性 DAG。每个 Operator 必须声明并在运行时满足 +`exactly_one` 输出;单条请求的解码/模型异常会返回 500,但不会终止可复用 Pipeline。 +broker 使用每次提交生成的 invocation token 防止迟到结果串到复用的 `request_id`;已经 +进入模型的超时任务会继续占用该 route 容量,直到真实执行结束,避免超时请求堆积。 + +## Analog Gauge 被动服务 + +[ethz-asl/analog_gauge_reader](https://github.com/ethz-asl/analog_gauge_reader) 使用同一个 +`POST /v1/inference` URL,客户端只需选择 `category: gauge.analog`。它返回通用 +`scalar` 输出,并可按请求返回 `original`、`annotated` 和 `diagnostics` artifact。 + +该上游项目依赖 Python 3.8、Torch 2.0、MMCV/MMOCR,与主项目 Python 3.10 环境隔离。 +首次部署从仓库根目录执行: + +```bash +bash server/gauge/worker/bootstrap.sh +uv run --no-sync cmvr-edge-ai validate \ + --config configs/server_gauge.yaml +uv run --no-sync cmvr-edge-ai serve \ + --config configs/server_gauge.yaml \ + --log-level INFO +``` + +worker 在进程内只串行处理一个请求,模型只加载一次;请求超时或协议失配时会终止被污染 +的子进程,并在下一次请求时重新创建。项目路径和三份权重路径只存在于服务端配置,调用方 +不能覆盖。完整准备步骤、请求 JSON 和当前 Linux x86_64 限制见 +[server/gauge/README.md](server/gauge/README.md)。 + +## ONNX 检测迁移 + +四个检测模型都保留 `ultralytics-yolo` 的 `@1` 注册,同时提供 +`onnxruntime-yolov8` 的 `@2` 注册。`@2` backend 直接执行静态 batch=1、NMS 外置的 +YOLOv8 ONNX 图,运行环境只需 NumPy、Pillow 和 ONNX Runtime,不导入 Torch 或 +Ultralytics: + +```bash +uv sync --locked --extra onnx-cpu +``` + +导出是单独的构建步骤,必须使用锁定的 exporter 环境;以下命令以 Mobile Phone 模型为例: + +```bash +uv sync --locked --extra onnx-export-cpu +uv run --no-sync python scripts/export_detection_onnx.py \ + --model-id yolov8n-mobile-phone@2 +``` + +脚本会校验来源标签、静态输入输出和 ONNX metadata,清除训练机路径/时间戳,并在 +`v2/` 写入 `model.onnx` 与 SHA256 `manifest.json`。四份制品已经生成,三个部署 YAML +已切换到 `@2`;运行时会在 session 创建前验证 manifest、模型身份与制品哈希。当前已 +完成真实 ORT smoke,但正式现场使用仍要用授权图片完成 `.pt(rect=False)`/ONNX 框与 +置信度一致性、延迟、峰值内存和长期稳定性验收。YOLOv8x People-Talking 计算量较大, +格式转换本身不会把它变成轻量网络。 +具体状态与普通 Git 制品规则见 [models/detection/README.md](models/detection/README.md)。 ## 运行视觉检测链路 @@ -196,29 +319,32 @@ uv sync --locked --only-group codegen 生成后应再次执行目标 profile 的 `uv sync --locked`,让可编辑安装识别新包;bootstrap 已经按这个顺序处理。 -在 `configs/edge_ai.yaml` 的 `detection` Pipeline 中配置部署参数: +在 `configs/active_detection.yaml` 的 `detection` Pipeline 中配置部署参数: ```yaml endpoints: cmvr_es: - target: 127.0.0.1:50052 + target: 192.168.0.119:50052 ppe_alert_platform: - base_url: http://127.0.0.1:8081 + base_url: http://192.168.0.222:13080 pipelines: detection: nodes: camera: with: - device_id: right_hand_cam + device_id: wrist_cam stream_log_interval_s: 5 detector: with: + model: construction-ppe-yolov8@2 attach_frame: true inference_log_interval_s: 5 model_options: - weights: models/detection/construction-ppe-yolov8/v1/best.pt - device: cpu + weights: models/detection/construction-ppe-yolov8/v2/model.onnx + providers: [CPUExecutionProvider] + intra_op_threads: 1 + inter_op_threads: 1 repeat_gate: with: alert_image: @@ -226,13 +352,15 @@ pipelines: jpeg_quality: 85 phone_detector: with: - model: people-talking-yolov8x@1 + model: people-talking-yolov8x@2 detect_labels: [talking on phone] max_fps: 5 attach_frame: true model_options: - weights: models/detection/people-talking-yolov8x/v1/best.pt - device: cpu + weights: models/detection/people-talking-yolov8x/v2/model.onnx + providers: [CPUExecutionProvider] + intra_op_threads: 1 + inter_op_threads: 1 phone_repeat_gate: with: alert_image: @@ -241,6 +369,7 @@ pipelines: alert_platform: with: endpoint: ppe_alert_platform + grpc_endpoint: cmvr_es failure_mode: log_and_drop ``` @@ -249,9 +378,9 @@ pipelines: ```bash uv run --no-sync cmvr-edge-ai models uv run --no-sync cmvr-edge-ai validate \ - --config configs/edge_ai.yaml \ + --config configs/active_detection.yaml \ --pipeline detection -uv run --no-sync cmvr-edge-ai run --config configs/edge_ai.yaml \ +uv run --no-sync cmvr-edge-ai run --config configs/active_detection.yaml \ --pipeline detection \ --log-level INFO \ --log-format json @@ -271,16 +400,16 @@ cmvr-es gRPC CameraService,共享解码帧并行执行 PPE 与 YOLOv8n Mobile 合并到一个窗口;它不经过重复触发规则,也不会访问 HTTP 平台: ```bash -uv run --no-sync python detect_server/show_detections.py \ - --config configs/debug/detection_viewer.yaml \ +uv run --no-sync python server/detect/show_detections.py \ + --config configs/detection_viewer.yaml \ --pipeline detection_show \ --log-level INFO ``` -运行前在 `configs/debug/detection_viewer.yaml` 中配置远端地址、`device_id` 和两份模型 +运行前在 `configs/detection_viewer.yaml` 中配置远端地址、`device_id` 和两份模型 权重;按 `q` 或 `Esc` 退出。框标签使用 `PPE:`/`PHONE:` 前缀,手机框标签为 `PHONE:mobile_phone`,窗口刷新率由较慢的模型分支决定。详细说明见 -[detect_server/README.md](detect_server/README.md#实时画框-demo)。 +[server/detect/README.md](server/detect/README.md#opencv-实时画框-viewer)。 相机连接器会在每次首次连接或重连时先发 `CameraService.StartCamera`,收到成功反馈后 才建立 `GetRGBImageStream`。终端会依次出现 `camera start requested/succeeded`、 @@ -288,33 +417,28 @@ uv run --no-sync python detect_server/show_detections.py \ 如果只有 opening 而没有 first frame,progress 中会持续显示 `first_frame_received=false window_frames=0`,用于区分“RPC 已建立但相机没有出帧”。 -运行前需要确认 Construction PPE 与 People-Talking 权重存在、cmvr-es 已启用 `right_hand_cam`,8081 平台的 -`/v1/detection-alerts` 可访问。默认 `yolo-cpu` profile 将 PyTorch 2.7.0 和 -torchvision 0.22.0 绑定到官方 CPU wheel,并固定 checkpoint 记录的 Ultralytics -8.4.31;同时显式安装 People-Talking checkpoint 反序列化所需的 `dill`。 -`model_options.device` 在该环境中应保持 `cpu`,`half` 应保持 `false`。 - -x86 CUDA 和 Jetson/JetPack 的 PyTorch wheel 与驱动强绑定,不能复用这个 CPU -profile。项目保留了不绑定 CPU index 的 `yolo` extra 作为设备专用环境的基础,但 -GPU 部署前应为目标设备建立单独的 uv source/lock(或使用 NVIDIA 容器),再把 YAML -中的 `device` 改为 `cuda:0`;不要只改 YAML 就认为 CUDA 环境已经就绪。 +运行前需要确认 Construction PPE 与 People-Talking 的 `model.onnx`、`manifest.json` +存在,cmvr-es 已启用配置相机,并且告警平台 `/v1/detection-alerts` 可访问。默认 +`detection-cpu` profile 只安装 NumPy、Pillow 和 ONNX Runtime,不安装 Torch 或 +Ultralytics。CUDA/TensorRT、Jetson 和其他 provider 必须建立与目标设备匹配的独立 +依赖/锁文件并重新验收,不能只修改 `providers` 就认为硬件环境已经就绪。 `detection.model@1` 根据 `model` 从 `DetectionModelRegistry` 解析模型。`detect_labels` 只选择需要检测的标签,省略时检测注册模型的全部标签;`confidence` 是全局阈值,也可以用 `label_confidence` 为个别标签覆盖。当前四个内置模型都可以通过 `cmvr-edge-ai models` 查看,模型制品和独立 model card 位于仓库内: -- [Construction PPE YOLOv8 v1](models/detection/construction-ppe-yolov8/v1/README.md):包含正向 PPE、`No-*` 违规类和施工现场设备类; -- [PPE YOLOv8n 6 Classes v1](models/detection/ppe-6classes-yolov8n/v1/README.md):轻量的六类正向装备检测模型。 -- [People Talking YOLOv8x v1](models/detection/people-talking-yolov8x/v1/README.md):电话使用行为检测;Pipeline 只选择 `talking on phone` 类。 -- [YOLOv8n Mobile Phone v1](models/detection/yolov8n-mobile-phone/README.md):单类手机目标检测;Debug Viewer 使用 `mobile_phone` 类。 +- [Construction PPE ONNX v2](models/detection/construction-ppe-yolov8/v2/README.md):包含正向 PPE、`No-*` 违规类和施工现场设备类; +- [PPE YOLOv8n 6 Classes ONNX v2](models/detection/ppe-6classes-yolov8n/v2/README.md):六类正向装备检测模型,当前未实例化; +- [People Talking ONNX v2](models/detection/people-talking-yolov8x/v2/README.md):电话使用行为检测,只选择 `talking on phone`; +- [YOLOv8n Mobile Phone ONNX v2](models/detection/yolov8n-mobile-phone/v2/README.md):Viewer 和被动服务使用的单类手机检测。 -注册 ID 中的 `@1` 表示版本 1;多数制品使用 `v1` 子目录,当前 YOLOv8n Mobile -Phone 保留导入时的扁平目录和上游文件名。实际 `weights` 路径仍由部署 YAML 显式 -指定。标签顺序、训练指标、局限和许可声明以上述 model card 为准。 +注册 ID 中 `@N` 与版本目录 `vN` 对应;Mobile Phone 的 v1 PT 是历史扁平布局,v2 已 +恢复版本目录。实际 `weights` 路径仍由部署 YAML 显式指定。标签顺序、训练指标、局限 +和许可声明以各版本 model card 为准。 `attach_frame: true` 让检测结果临时携带对应的解码帧,供后续告警节点使用;因此原 detector 到 repeat gate 的队列应保持较小,避免堆积未压缩图像。 六类模型的标签是 `Gloves`、`Vest`、`goggles`、`helmet`、`mask` 和 `safety_shoe`, 语义是“画面中检测到了该装备”,不是“人员缺少该装备”。它没有 `Person` 或 `No-*` 类,也没有人员与装备关联能力,因此不能只靠配置推断某个人未佩戴 PPE。该模型当前仅 -注册、未被 `configs/edge_ai.yaml` 的 `detection` Pipeline 引用;需要恢复第二分支时,应同时配置 detector、 +注册、未被 `configs/active_detection.yaml` 的 `detection` Pipeline 引用;需要恢复第二分支时,应同时配置 detector、 8082 endpoint、HTTP Sink 和两条关联 edge。 当前 Construction PPE 和 People-Talking 分支从 decoder 输出端口 fan-out,共享同一个 @@ -323,7 +447,25 @@ People-Talking 是视觉行为判断,不能证明电话已接通,也不能 `detection.repeat_gate@1` 只在一个规则的 `window_ms` 内看到至少 `min_hits` 个不同帧后生成 `DetectionAlert/v1`。同一帧有多个同类框仍只算一次;触发后进入 `cooldown_ms`,冷却期间不累计,结束后必须重新满足次数。`scope: source` 按相机统计;`scope: track` 按 `track_id` 统计,但当前 YOLO adapter 只做逐帧检测,不产生 `track_id`,因此使用 track 规则前必须增加跟踪/关联节点。 -启用 `alert_image` 后,repeat gate 只在规则真正触发时使用 Pillow 对阈值帧画框并编码 JPEG,不会给每一帧都生成图片。告警的 `detections` 和图片中的 bounding boxes 都来自达到 `min_hits` 的阈值帧;窗口内更早帧只参与 `hit_count`、时间范围和最大置信度统计。HTTP JSON 中图片位于 `payload.image`: +启用 `alert_image` 后,repeat gate 只在规则真正触发时使用 Pillow 对阈值帧画框并编码 JPEG,不会给每一帧都生成图片。告警的 `detections` 和图片中的 bounding boxes 都来自达到 `min_hits` 的阈值帧;窗口内更早帧只参与 `hit_count`、时间范围和最大置信度统计。 + +`alert_platform.with.grpc_endpoint: cmvr_es` 会读取 `endpoints.cmvr_es.target`,从 +`192.168.0.119:50052` 这样的字面 IP target 中去掉端口,并把结果放在 HTTP JSON +顶层。`source_id` 仍是相机 ID,二者不会互相覆盖: + +```json +{ + "schema": "DetectionAlert/v1", + "source_id": "wrist_cam", + "grpc_ip": "192.168.0.119", + "payload": {"event_id": "...", "image": {"encoding": "base64", "data": "..."}} +} +``` + +被引用 target 必须是单个字面 IPv4/IPv6 加端口;hostname、Unix socket 和多地址 +target 会在 Sink setup 时被拒绝,避免 DNS 地址变化造成边缘端身份漂移。 + +HTTP JSON 中完整图片对象位于 `payload.image`: ```json { @@ -343,16 +485,8 @@ People-Talking 是视觉行为判断,不能证明电话已接通,也不能 同一阈值帧若同时触发多条规则或多个 track,只编码一次相关框的并集,并让这些 告警共享同一个不可变 JPEG 对象,以限制边缘端瞬时 CPU 和内存开销。 -对话占位链路不依赖音频 proto: - -```bash -uv run --no-sync cmvr-edge-ai validate \ - --config configs/edge_ai.yaml \ - --pipeline talk -uv run --no-sync cmvr-edge-ai run \ - --config configs/edge_ai.yaml \ - --pipeline talk -``` +Talk Server 当前只保留能力边界说明;在真实音频 proto、VAD/ASR/LLM/TTS 节点和远程 +调用契约落地前,不提供容易被误认为可部署服务的占位配置。 ## 配置最小示例 @@ -480,7 +614,7 @@ AGV Sink 必须绑定固定的非空 `device_id`。`set_velocity` 默认禁用 - 音频内部契约 `AudioChunk/v1` 已定义,但 cmvr-es 麦克风/扬声器双向流 proto 和连接器尚未落地。 - UDP 与 QUIC 目录目前是扩展占位,没有可用传输实现。 - 运行时 v1 只支持 `execution.mode: async|inline`,两者当前都是单 task、单并发执行。`thread`、`process`、`model_worker` 是保留值;`concurrency != 1`、非空 `max_in_flight/timeout_s`、`ordered: false` 也是保留配置,都会在编译期被拒绝。插件仍可在组件内部显式使用 `workers.run_blocking()`、`PersistentProcessWorker` 或自有模型 Worker,但不能把 `execution` 声明误当作自动调度。 -- v1 对未实现的声明采取 fail-closed:显式设置 `runtime.max_processes/process_start_method/health_bind/reserved_memory_mb`、非默认 pipeline `priority` 或任何非空 `resources` 都会在编译期被拒绝。健康状态和队列统计目前只能通过 Python API 获取。 +- v1 对未实现的声明采取 fail-closed:显式设置 `runtime.max_processes/process_start_method/health_bind/reserved_memory_mb`、非默认 pipeline `priority` 或任何非空 `resources` 都会在编译期被拒绝。`serve` 已提供 `/health/live` 和 `/health/ready`;详细节点健康信息与队列统计仍只能通过 Python API 获取。 - 当前没有配置热更新、配置 overlay、持久化 outbox/spool 或共享内存图像池。HTTP Sink 只有当前进程内的有限重试;默认 `failure_mode: raise` 会终止 Pipeline,当前检测配置的 `log_and_drop` 则在重试耗尽后记录 WARNING 并丢弃该告警。两种模式在进程退出或断电时 diff --git a/client/detect/README.md b/client/detect/README.md new file mode 100644 index 0000000..7450a1a --- /dev/null +++ b/client/detect/README.md @@ -0,0 +1,31 @@ +# Detect Client + +这里放置不依赖 pipeline 的 Detect HTTP SDK 使用示例。SDK 只负责: + +- `list_models()` 查询 `active_push` 与 `passive_invoke` 能力; +- `infer(category, image, media_type, parameters, image_roles)` 调用 + `POST /v1/inference`; +- 严格校验版本化响应,并把连接、HTTP 状态和协议错误分成不同异常。 + +客户端不得传入 `model_id`、权重路径或 pipeline 配置。模型选择和权重生命周期属于 +`server/detect`;客户端只使用服务端公开的稳定业务 `category`。 + +安装 HTTP extra 后运行 [example.py](example.py): + +```bash +uv sync --extra http +CMVR_DETECT_BASE_URL=http://127.0.0.1:8081 \ + uv run --no-sync python client/detect/example.py ./sample.jpg detect.ppe +``` + +远程服务配置了 `server.http.bearer_token` 时,通过环境变量传入同一 token;TLS 部署应把 +base URL 改为 `https://`: + +```bash +CMVR_DETECT_BASE_URL=https://edge.example.com:8081 \ +CMVR_DETECT_BEARER_TOKEN='deployment-secret' \ + uv run --no-sync python client/detect/example.py ./sample.jpg detect.ppe +``` + +SDK 的生产代码位于 `cmvr_edge_ai.client.detect`。base URL、认证 header 和 timeout +属于调用应用配置;本目录不复制服务端 category/model/weights 配置。 diff --git a/client/detect/example.py b/client/detect/example.py new file mode 100644 index 0000000..2fd6d14 --- /dev/null +++ b/client/detect/example.py @@ -0,0 +1,40 @@ +"""Call a Detect Server without constructing an edge-AI pipeline.""" + +from __future__ import annotations + +import asyncio +import os +import sys +from pathlib import Path + +from cmvr_edge_ai.client.detect import DetectClient + + +async def main() -> None: + if len(sys.argv) != 3: + raise SystemExit("usage: example.py IMAGE_PATH CATEGORY") + image_path = Path(sys.argv[1]) + category = sys.argv[2] + base_url = os.environ.get("CMVR_DETECT_BASE_URL", "http://127.0.0.1:8081") + bearer_token = os.environ.get("CMVR_DETECT_BEARER_TOKEN") + headers = ( + {} + if bearer_token is None + else {"Authorization": f"Bearer {bearer_token}"} + ) + + async with DetectClient(base_url, headers=headers) as client: + models = await client.list_models() + print(models) + result = await client.infer( + category, + image_path.read_bytes(), + media_type="image/jpeg", + parameters={}, + image_roles=("annotated", "original"), + ) + print(result) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/configs/README.md b/configs/README.md new file mode 100644 index 0000000..dfadc9b --- /dev/null +++ b/configs/README.md @@ -0,0 +1,23 @@ +# 配置目录 + +所有可部署、可直接传给 `cmvr-edge-ai --config` 的配置统一保存在本目录: + +| 文件 | 模式 | 用途 | +|---|---|---| +| `active_detection.yaml` | 主动推送 | 相机检测告警链路 | +| `server_detect.yaml` | 被动调用 | PPE 与手机检测 HTTP 服务 | +| `server_gauge.yaml` | 被动调用 | Analog Gauge HTTP 服务 | +| `detection_viewer.yaml` | 本地调试 | OpenCV 双模型实时画框 | + +三份检测配置均使用 `@2` `onnxruntime-yolov8` 制品;`model_options` 必须使用 +`providers/intra_op_threads/inter_op_threads`,不能保留 PT adapter 的 `device/half`。 + +所有仓库内命令都以项目根目录为当前工作目录。配置中的模型权重路径也按项目根目录 +解析,而不是按 YAML 文件所在目录解析。 + +`tests/fixtures/minimal_pipeline.yaml` 是自动化测试夹具,不是部署配置; +`server/gauge/worker/pyproject.toml` 与 `uv.lock` 是隔离 Worker 的环境清单,也不属于 +运行时 YAML 配置。 + +Talk Server 尚未实现,因此不保留不能运行的占位 YAML。实现真实音频协议与模型节点后, +再新增 `configs/server_talk.yaml`。 diff --git a/configs/edge_ai.yaml b/configs/active_detection.yaml similarity index 67% rename from configs/edge_ai.yaml rename to configs/active_detection.yaml index 2024fc9..ac54a8b 100644 --- a/configs/edge_ai.yaml +++ b/configs/active_detection.yaml @@ -12,7 +12,7 @@ endpoints: target: 192.168.0.119:50052 tls: false timeout_s: 5 - options: + options: max_receive_mb: 32 ppe_alert_platform: @@ -45,7 +45,7 @@ pipelines: detector: uses: detection.model@1 with: - model: construction-ppe-yolov8@1 + model: construction-ppe-yolov8@2 # Omitting detect_labels means all registered labels. This example # asks the backend to return only PPE violations used by the rules. detect_labels: @@ -66,14 +66,15 @@ pipelines: # annotated alert image can be rendered only when a rule triggers. attach_frame: true model_options: - # Model artifact and inference device are deployment configuration, + # Model artifact and inference provider are deployment configuration, # not process environment requirements. This repository-relative # path requires launching cmvr-edge-ai from the repository root. - weights: models/detection/construction-ppe-yolov8/v1/best.pt - device: cpu + weights: models/detection/construction-ppe-yolov8/v2/model.onnx + providers: [CPUExecutionProvider] + intra_op_threads: 1 + inter_op_threads: 1 imgsz: 640 iou: 0.70 - half: false max_det: 100 repeat_gate: @@ -136,50 +137,54 @@ pipelines: min_confidence: 0.50 scope: source - # phone_detector: - # uses: detection.model@1 - # with: - # model: people-talking-yolov8x@1 - # # The checkpoint also contains a generic class named "label". It is - # # retained in model registration for class-ID safety but is not an - # # actionable phone-use event, so this branch selects only class 1. - # detect_labels: - # - talking on phone - # confidence: 0.50 - # # YOLOv8x is substantially heavier than the PPE model. Start with a - # # conservative CPU rate and tune only after measuring target hardware. - # max_fps: 5 - # inference_log_interval_s: 5 - # attach_frame: true - # model_options: - # weights: models/detection/people-talking-yolov8x/v1/best.pt - # device: cpu - # imgsz: 640 - # iou: 0.70 - # half: false - # max_det: 100 + phone_detector: + uses: detection.model@1 + with: + model: people-talking-yolov8x@2 + # The source model also contains a generic class named "label". It is + # retained in model registration for class-ID safety but is not an + # actionable phone-use event, so this branch selects only class 1. + detect_labels: + - talking on phone + confidence: 0.50 + # YOLOv8x is substantially heavier than the PPE model. Start with a + # conservative CPU rate and tune only after measuring target hardware. + max_fps: 5 + inference_log_interval_s: 5 + attach_frame: true + model_options: + weights: models/detection/people-talking-yolov8x/v2/model.onnx + providers: [CPUExecutionProvider] + intra_op_threads: 1 + inter_op_threads: 1 + imgsz: 640 + iou: 0.70 + max_det: 100 - # phone_repeat_gate: - # uses: detection.repeat_gate@1 - # with: - # time_source: received - # alert_image: - # enabled: true - # jpeg_quality: 85 - # rules: - # - id: talking-on-phone - # labels: [talking on phone] - # min_hits: 3 - # window_ms: 2000 - # cooldown_ms: 30000 - # min_confidence: 0.50 - # scope: source + phone_repeat_gate: + uses: detection.repeat_gate@1 + with: + time_source: received + alert_image: + enabled: true + jpeg_quality: 85 + rules: + - id: talking-on-phone + labels: [talking on phone] + min_hits: 3 + window_ms: 2000 + cooldown_ms: 30000 + min_confidence: 0.50 + scope: source alert_platform: uses: platform.http_json_sink@1 with: endpoint: ppe_alert_platform path: /v1/detection-alerts + # Include the IP from endpoints.cmvr_es.target in every platform + # report so the platform can identify the originating edge device. + grpc_endpoint: cmvr_es # Platform outages must not stop camera capture or inference. After # bounded retries, log a WARNING and drop only this report. failure_mode: log_and_drop @@ -223,13 +228,13 @@ pipelines: capacity: 2 overflow: drop_oldest - # - from: phone_detector.detections - # to: phone_repeat_gate.detections - # qos: - # profile: telemetry - # # attach_frame carries the decoded image until the rule is evaluated. - # capacity: 2 - # overflow: drop_oldest + - from: phone_detector.detections + to: phone_repeat_gate.detections + qos: + profile: telemetry + # attach_frame carries the decoded image until the rule is evaluated. + capacity: 2 + overflow: drop_oldest - from: repeat_gate.alerts to: alert_platform.input @@ -238,45 +243,9 @@ pipelines: capacity: 64 overflow: block - # - from: phone_repeat_gate.alerts - # to: alert_platform.input - # qos: - # profile: telemetry - # capacity: 64 - # overflow: block - - talk: - enabled: true - nodes: - audio_stream_placeholder: - # Replace with cmvr.grpc.microphone_audio_stream@1 when its proto lands. - uses: core.sequence_source@1 - with: - items: - - simulated-audio-chunk - schema_name: AudioChunk - schema_version: 1 - - dialogue_placeholder: - # The real chain will be VAD -> ASR -> dialogue -> TTS. - uses: core.passthrough@1 - - output: - uses: core.log_sink@1 - with: - logger: cmvr_edge_ai.talk - - edges: - - from: audio_stream_placeholder.output - to: dialogue_placeholder.input + - from: phone_repeat_gate.alerts + to: alert_platform.input qos: - profile: audio_contiguous - capacity: 16 - overflow: block - - - from: dialogue_placeholder.output - to: output.input - qos: - profile: request - capacity: 8 + profile: telemetry + capacity: 64 overflow: block diff --git a/configs/debug/detection_viewer.yaml b/configs/detection_viewer.yaml similarity index 89% rename from configs/debug/detection_viewer.yaml rename to configs/detection_viewer.yaml index 90668f7..7241289 100644 --- a/configs/debug/detection_viewer.yaml +++ b/configs/detection_viewer.yaml @@ -41,7 +41,7 @@ pipelines: detector: uses: detection.model@1 with: - model: construction-ppe-yolov8@1 + model: construction-ppe-yolov8@2 detect_labels: - No-Boots - No-Ear-Protection @@ -56,28 +56,30 @@ pipelines: attach_frame: true model_options: # Repository-relative path; launch the viewer from the repository root. - weights: models/detection/construction-ppe-yolov8/v1/best.pt - device: cpu + weights: models/detection/construction-ppe-yolov8/v2/model.onnx + providers: [CPUExecutionProvider] + intra_op_threads: 1 + inter_op_threads: 1 imgsz: 640 iou: 0.70 - half: false max_det: 100 phone_detector: uses: detection.model@1 with: - model: yolov8n-mobile-phone@1 + model: yolov8n-mobile-phone@2 detect_labels: - mobile_phone confidence: 0.50 inference_log_interval_s: 5 attach_frame: true model_options: - weights: models/detection/yolov8n-mobile-phone/yolov8n-mobile-phone.pt - device: cpu + weights: models/detection/yolov8n-mobile-phone/v2/model.onnx + providers: [CPUExecutionProvider] + intra_op_threads: 1 + inter_op_threads: 1 imgsz: 640 iou: 0.70 - half: false max_det: 100 viewer: diff --git a/configs/server_detect.yaml b/configs/server_detect.yaml new file mode 100644 index 0000000..f46b9ce --- /dev/null +++ b/configs/server_detect.yaml @@ -0,0 +1,148 @@ +# Passive Detect Server deployment example. +api_version: cmvr.edge.ai/v1 + +runtime: + thread_workers: 2 + +server: + enabled: true + http: + bind: 127.0.0.1 + port: 8081 + # 绑定局域网地址时必须设置 bearer_token,并配置下面两项 TLS;只有受信隔离网络 + # 才应改用 allow_insecure_remote: true 明文传输 token。 + # bearer_token: env://CMVR_EDGE_AI_BEARER_TOKEN + # tls_certfile: /etc/cmvr-edge-ai/tls/server.crt + # tls_keyfile: /etc/cmvr-edge-ai/tls/server.key + request_timeout_s: 30 + max_request_bytes: 16777216 + max_image_bytes: 10485760 + routes: + detect.ppe: + pipeline: detect_ppe + model_id: construction-ppe-yolov8@2 + queue_capacity: 4 + detect.mobile_phone: + pipeline: detect_mobile_phone + model_id: yolov8n-mobile-phone@2 + queue_capacity: 4 + +# Public clients know only the route categories above. Model IDs, weights and +# devices remain in this server-owned configuration. +pipelines: + detect_ppe: + nodes: + request_source: + uses: server.request_source@1 + with: + category: detect.ppe + + image_decoder: + uses: media.image_decoder.pillow@1 + with: + input_name: image + pixel_format: BGR8 + max_pixels: 25000000 + accepted_media_types: [image/jpeg, image/png] + + detector: + uses: detection.model@1 + with: + model: construction-ppe-yolov8@2 + confidence: 0.50 + # Passive requests must all produce a response, so this pipeline does + # not configure max_fps or any frame-dropping edge. + attach_frame: true + model_options: + weights: models/detection/construction-ppe-yolov8/v2/model.onnx + providers: [CPUExecutionProvider] + intra_op_threads: 1 + inter_op_threads: 1 + imgsz: 640 + iou: 0.70 + max_det: 100 + + response: + uses: server.detection_response@1 + with: + category: detect.ppe + backend: onnxruntime-yolov8 + jpeg_quality: 85 + + response_sink: + uses: server.response_sink@1 + + edges: + - from: request_source.requests + to: image_decoder.requests + qos: &ppe_request_qos + profile: request + capacity: 4 + overflow: block + - from: image_decoder.frames + to: detector.frames + qos: *ppe_request_qos + - from: detector.detections + to: response.detections + qos: *ppe_request_qos + - from: response.responses + to: response_sink.responses + qos: *ppe_request_qos + + detect_mobile_phone: + nodes: + request_source: + uses: server.request_source@1 + with: + category: detect.mobile_phone + + image_decoder: + uses: media.image_decoder.pillow@1 + with: + input_name: image + pixel_format: BGR8 + max_pixels: 25000000 + accepted_media_types: [image/jpeg, image/png] + + detector: + uses: detection.model@1 + with: + model: yolov8n-mobile-phone@2 + detect_labels: [mobile_phone] + confidence: 0.50 + attach_frame: true + model_options: + weights: models/detection/yolov8n-mobile-phone/v2/model.onnx + providers: [CPUExecutionProvider] + intra_op_threads: 1 + inter_op_threads: 1 + imgsz: 640 + iou: 0.70 + max_det: 100 + + response: + uses: server.detection_response@1 + with: + category: detect.mobile_phone + backend: onnxruntime-yolov8 + jpeg_quality: 85 + + response_sink: + uses: server.response_sink@1 + + edges: + - from: request_source.requests + to: image_decoder.requests + qos: &phone_request_qos + profile: request + capacity: 4 + overflow: block + - from: image_decoder.frames + to: detector.frames + qos: *phone_request_qos + - from: detector.detections + to: response.detections + qos: *phone_request_qos + - from: response.responses + to: response_sink.responses + qos: *phone_request_qos diff --git a/configs/server_gauge.yaml b/configs/server_gauge.yaml new file mode 100644 index 0000000..1b61adf --- /dev/null +++ b/configs/server_gauge.yaml @@ -0,0 +1,66 @@ +api_version: cmvr.edge.ai/v1 + +runtime: + thread_workers: 1 + shutdown_timeout_s: 15 + +server: + enabled: true + http: + bind: 127.0.0.1 + port: 8081 + # 绑定局域网地址时必须设置 bearer_token,并配置下面两项 TLS;只有受信隔离网络 + # 才应改用 allow_insecure_remote: true 明文传输 token。 + # bearer_token: env://CMVR_EDGE_AI_BEARER_TOKEN + # tls_certfile: /etc/cmvr-edge-ai/tls/server.crt + # tls_keyfile: /etc/cmvr-edge-ai/tls/server.key + # 首次模型初始化和 OCR 都比较慢;HTTP timeout 必须大于 worker timeout。 + request_timeout_s: 360 + max_request_bytes: 33554432 + max_image_bytes: 25165824 + routes: + gauge.analog: + pipeline: gauge_analog + model_id: ethz-analog-gauge-reader@1 + queue_capacity: 1 + +pipelines: + gauge_analog: + nodes: + request_source: + uses: server.request_source@1 + with: + category: gauge.analog + + gauge_reader: + uses: gauge.analog_reader@1 + with: + category: gauge.analog + model_id: ethz-analog-gauge-reader@1 + # 这些路径由服务端拥有,远程调用方不能覆盖。 + python_executable: /home/xtkuang/Projects/cmvr/cmvr_edge_ai/server/gauge/worker/.venv/bin/python + project_root: /home/xtkuang/Projects/cmvr/changan_robot/analog_gauge_reader + detection_model_path: models/gauge_detection_model.pt + key_point_model_path: models/key_point_model.pt + segmentation_model_path: models/segmentation_model.pt + startup_timeout_s: 300 + request_timeout_s: 300 + shutdown_timeout_s: 10 + max_header_bytes: 1048576 + max_blob_bytes: 67108864 + # 先读取 JPEG/PNG 头检查像素数,再交给 Pillow/OpenCV 和上游模型。 + max_pixels: 25000000 + + response_sink: + uses: server.response_sink@1 + + edges: + - from: request_source.requests + to: gauge_reader.requests + qos: &gauge_request_qos + profile: request + capacity: 1 + overflow: block + - from: gauge_reader.responses + to: response_sink.responses + qos: *gauge_request_qos diff --git a/detect_server/README.md b/detect_server/README.md deleted file mode 100644 index 6b24f97..0000000 --- a/detect_server/README.md +++ /dev/null @@ -1,410 +0,0 @@ -# PPE 与电话使用检测流水线 - -`configs/edge_ai.yaml` 中的 `detection` Pipeline 是当前可运行的园区视觉告警链路。 -它与 `talk` Pipeline 共用一个部署 YAML,并从同一个 decoder 并行运行 Construction -PPE 与 People-Talking 两个模型。两个 detector 使用独立 repeat gate,再把带有不同 -`model_id`/`rule_id` 的告警交给同一个平台 Sink: - -```text -cmvr-es CameraService - -> H264/H265 ImageFrame/v1 - -> media.video_decoder.pyav@1 - -> BGR8 ImageFrame/v1 - |-> construction-ppe-yolov8@1 -> PPE repeat gate --| - `-> people-talking-yolov8x@1 -> phone repeat gate --+-> DetectionAlert/v1 - `-> POST /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 以生成告警图片,并显式安装 People-Talking -checkpoint 反序列化所需的 `dill`;所有具体包版本记录在仓库的 `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 - phone_detector: - with: - model: people-talking-yolov8x@1 - detect_labels: [talking on phone] - max_fps: 5 - attach_frame: true - model_options: - weights: models/detection/people-talking-yolov8x/v1/best.pt - device: cpu - phone_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`、两份生产权重存在,并且 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 -> latest decoded frame - |-> Construction PPE --| - `-> YOLOv8n Mobile ----+-> same-frame join - -> 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: - model_options: - weights: models/detection/construction-ppe-yolov8/v1/best.pt - device: cpu - phone_detector: - with: - model: yolov8n-mobile-phone@1 - detect_labels: [mobile_phone] - model_options: - weights: models/detection/yolov8n-mobile-phone/yolov8n-mobile-phone.pt - device: cpu -``` - -Demo 只在共享 fan-out 之前通过 `realtime_latest + drop_oldest` 选择最新解码帧,之后用 -两条 `block` 边把同一个 `ImageFrame` 交给两个 detector。两个 detector 都不设置独立 -`max_fps`,避免各自跳过不同帧。Viewer 按 frame identity 等待两个结果配齐,再在同一 -张图上绘制;因此预览吞吐由较慢的模型分支决定。 - -只检查配置和插件连线,不连接相机、不加载模型、也不创建窗口: - -```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 视频流。窗口只在同一帧的两个 -推理结果都到达后刷新;PPE 框显示为 `PPE: