diff --git a/CMakeLists.txt b/CMakeLists.txt index 7e7ea93..8728443 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,12 +20,20 @@ target_include_directories(camera_risk_core SYSTEM PRIVATE third_party/zlg) target_compile_options(camera_risk_core PRIVATE $<$:-Wall;-Wextra;-Wpedantic>) add_library(camera_risk_visualizer src/visualizer.cpp) target_link_libraries(camera_risk_visualizer PUBLIC camera_risk_core) +add_library(camera_risk_mask_annotator src/door_mask_annotator.cpp) +target_include_directories(camera_risk_mask_annotator PUBLIC include third_party ${OpenCV_INCLUDE_DIRS}) +target_link_libraries(camera_risk_mask_annotator PUBLIC ${OpenCV_LIBS}) +target_compile_options(camera_risk_mask_annotator PRIVATE -Wall -Wextra -Wpedantic) add_executable(camera_risk src/main.cpp) target_link_libraries(camera_risk PRIVATE camera_risk_visualizer) +add_executable(door_mask_tool src/door_mask_tool.cpp) +target_link_libraries(door_mask_tool PRIVATE camera_risk_core camera_risk_mask_annotator) +target_compile_options(door_mask_tool PRIVATE -Wall -Wextra -Wpedantic) # Reference USBCANFD SDK is shipped with the standalone Orin package. target_link_libraries(camera_risk_core PUBLIC "${CMAKE_SOURCE_DIR}/orin/lib/libusbcanfd.so" "-Wl,--no-as-needed" "${CMAKE_SOURCE_DIR}/orin/lib/libusb-1.0.so" "-Wl,--as-needed") -set_target_properties(camera_risk PROPERTIES BUILD_WITH_INSTALL_RPATH TRUE INSTALL_RPATH "$ORIGIN/../lib;$ORIGIN/../orin/lib;/usr/local/cuda/lib64") +set_target_properties(camera_risk door_mask_tool PROPERTIES BUILD_WITH_INSTALL_RPATH TRUE INSTALL_RPATH "$ORIGIN/../lib;$ORIGIN/../orin/lib;/usr/local/cuda/lib64") # Keep the bundled SDK/libusb pair together even with a desktop LD_LIBRARY_PATH. target_link_options(camera_risk PRIVATE "-Wl,--disable-new-dtags") +target_link_options(door_mask_tool PRIVATE "-Wl,--disable-new-dtags") diff --git a/README.md b/README.md index cdf4832..6ff5f67 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,38 @@ Ctrl+C / SIGTERM 停止程序。 摄像头为 /dev/video5,1920×1536 UYVY,30 FPS。 当前 door mask 是指定的 constant_door_mask.json 副本,标定状态为 false;装车后应更换与实际画面匹配的门区并更新标定状态。 +## Door mask 标注工具 + +先停止正在占用摄像头的推理程序,再运行: + +```bash +./annotate_door_mask.sh --camera --output config/door_masks/driver-side.json +``` + +工具按 `config/camera.json` 抓取一张参考画面并打开标注窗口。左键依次添加门区多边形顶点,右键或 `U` 撤销一个点,`R` 清空,`S` 或 Enter 保存,Esc 或 `Q` 取消。保存时生成 x-anylabeling JSON,并在同一目录保存 `<名称>_reference.jpg`;已有文件默认不会覆盖,需要覆盖时显式添加 `--force`。 + +也可以使用已有图片,不打开摄像头: + +```bash +./annotate_door_mask.sh --image /path/to/frame.jpg --output config/door_masks/driver-side.json +``` + +输入已经采集的视频时,工具会先打开帧选择窗口。可拖动滑块,使用 `A/D` 逐帧移动,或使用 `J/L` 前后跳过 30 帧;按 `S` 或 Enter 选择当前帧并进入标注: + +```bash +./annotate_door_mask.sh --video /path/to/video.mp4 --output config/door_masks/driver-side.json +``` + +已知帧号时可以跳过帧选择窗口,帧号从 0 开始: + +```bash +./annotate_door_mask.sh --video /path/to/video.mp4 --frame 244 --output config/door_masks/driver-side.json +``` + +`--image` 和 `--video` 模式不会打开或检查摄像头。视频生成的 JSON 会额外记录 `sourceVideo` 和 `sourceFrameIndex`。 + +该工具仅生成 mask 文件,暂不改变 `camera.json` 或推理程序当前使用的默认 mask。 + ## 下游获取结果 main.cpp 注释中也包含以下示例。下游通过自己的调用位置或线程读取: @@ -138,7 +170,7 @@ Ctrl+C / SIGTERM 会封装已写入的视频,`CameraPipeline::join()` 返回 ## CAN/CANFD 收发与四门位置显示 -实时运行默认打开参考工程 `/home/orin/vision-anti-pinch` 使用的 USBCANFD(设备类型 33、设备索引 0),仅启用 CH0:60 MHz 时钟、仲裁域 500 kbps、数据域 2 Mbps。一个接收线程读取 CH0,默认启用终端电阻。可在 `config/camera.json` 的 `can` 中设置 `enabled`、`device_index`、`timeout_ms`(默认 1000 ms)和 `termination`;`--no-can` 显式关闭实时 CAN。设备不可用时显示 UNAVAILABLE,摄像头推理继续;接收 API 失败显示 ERROR,需检查设备后重启程序。 +实时运行默认打开参考工程 `/home/orin/vision-anti-pinch` 使用的 USBCANFD(设备类型 33、设备索引 0),启用 CH0 和 CH1:60 MHz 时钟、仲裁域 500 kbps、数据域 2 Mbps。一个接收线程只接收 CH0,所有发送只使用 CH1;两个通道默认启用终端电阻。可在 `config/camera.json` 的 `can` 中设置 `enabled`、`device_index`、`timeout_ms`(默认 1000 ms)和 `termination`;`--no-can` 显式关闭实时 CAN。设备不可用时显示 UNAVAILABLE,摄像头推理继续;接收 API 失败显示 ERROR,需检查设备后重启程序。 首次使用 USB-CAN 时,若节点为 root:root 且普通用户不可写,可运行 `./scripts/enable_can_access.sh`(需要 sudo)。它只为 `3068:0009` 适配器安装 `plugdev` 组读写规则并刷新设备权限;运行用户须属于 `plugdev`,新加组后需重新登录。无需以 root 运行整个推理程序。 @@ -153,13 +185,13 @@ Ctrl+C / SIGTERM 会封装已写入的视频,`CameraPipeline::join()` 返回 显示值是无符号原始字节 0–255,尚无物理单位、缩放或无效值定义,因此不标为百分比、不把 255 擅自视为无效。CH0 接收 CAN/CANFD 数据帧,过滤远程帧、错误帧和发送回显;少于 4 字节的 `0x391` 不更新信号。修正了参考代码 `get_bit_value(0/1/2/3, 8)` 导致的位域重叠,字节起始位应为 0/8/16/24。 -默认预览显示 CH0 四门实际位置反馈、LIVE/STALE/WAIT、新鲜度及帧数,并显示总 RX/TX 和错误计数。尚未接收到值时显示 `--`,超时保留最后值但标记 STALE;CH1 不初始化、不收发,实时预览不显示 CH1。即使相机正在重试或暂无新推理帧,CAN 预览仍约每 100 ms 刷新。CAN 只提供状态显示和下游数据,本次没有新增基于门位置的推理启停或风险门控。 +默认预览显示 CH0 四门实际位置反馈、LIVE/STALE/WAIT、新鲜度及帧数,并显示总 RX/TX 和错误计数。尚未接收到值时显示 `--`,超时保留最后值但标记 STALE;CH1 专用于发送,不接收也不在实时预览中显示。即使相机正在重试或暂无新推理帧,CAN 预览仍约每 100 ms 刷新。CAN 只提供状态显示和下游数据,本次没有新增基于门位置的推理启停或风险门控。 下游通过 `pipeline.canStatus()` 读取当前状态;`pipeline.latest()->can` 是该推理结果生成时的快照。按需发送使用线程安全的 `pipeline.sendCan()`: ```cpp cms::CanFrame frame; -frame.channel = 0; // 仅 CH0 +frame.channel = 1; // 发送仅使用 CH1 frame.id = 0x123; // 按实际协议设置 frame.data = {0x01, 0x02}; // 示例数据,应替换为实际报文 bool sent = pipeline.sendCan(frame); @@ -169,11 +201,11 @@ bool sent = pipeline.sendCan(frame); // 标准 ID 最大 0x7FF,扩展 ID 最大 0x1FFFFFFF。 ``` -非法通道/ID/长度/标志抛出 `std::invalid_argument`;设备未就绪或 SDK 发送失败返回 false,发送计数以 SDK 返回为准。`sendCan()` 接口不自动填充/截断数据。发送调用与设备关闭互斥;CH0 接收保持独立线程运行。 +发送通道不是 CH1,或 ID/长度/标志非法时抛出 `std::invalid_argument`;设备未就绪或 SDK 发送失败返回 false,发送计数以 SDK 返回为准。`sendCan()` 接口不自动填充/截断数据。发送调用与设备关闭互斥;CH0 接收保持独立线程运行。 -实时推理每产生一个新结果,会将风险提交给后台 CAN 发送线程,由它通过 CH0 发送一帧标准 CAN 数据报文 `0x1F6`,DLC 固定为 8。`data[0]` 的低四位(bit0–3)和高四位(bit4–7)同时填写相同的二值风险:`risk_level == "clear"` 时各为 0,其余状态(包括 `watch`、`warning`、`danger`、`unavailable`)各为 1;后七字节全部为 0。即 `clear` 发送 `00 00 00 00 00 00 00 00`,其余发送 `11 00 00 00 00 00 00 00`(十六进制)。无人或证据不足导致的 `unavailable` 也在高、低四位各发送 1。 +实时推理首次取得风险状态时发送一次,此后仅在二值风险发生变化时,由后台 CAN 发送线程通过 CH1 发送一帧标准 CAN 数据报文 `0x1F6`,DLC 固定为 8。`data[0]` 的低四位(bit0–3)和高四位(bit4–7)同时填写相同的二值风险:`risk_level` 为 `clear`、`watch` 或 `unavailable` 时各为 0,`warning`、`danger` 以及未知状态时各为 1;后七字节全部为 0。即 `clear`、`watch` 和无人或证据不足导致的 `unavailable` 发送 `00 00 00 00 00 00 00 00`,其余发送 `11 00 00 00 00 00 00 00`(十六进制)。 -发送随推理结果触发,没有独立周期;无新推理结果时不重发。SDK 发送等待不占用推理线程;后台最多保留一个待发结果,发送变慢时用最新结果替换尚未发送的结果,避免积压旧风险,并非保证每个推理帧都上总线。CH0 发送失败通过现有 TX 错误计数呈现;结果中的 CAN 快照记录采样时已完成的发送计数,不保证包含当前帧的发送。停止时丢弃待发结果,并等待已经进入 SDK 的调用返回后释放设备。`--no-can` 或 `can.enabled=false` 禁用自动发送,视频回灌始终不发送。按需 `sendCan()` 仍为同步 API。 +发送按报文二值状态变化触发:`clear/watch/unavailable ↔ warning/danger/未知状态` 才产生新报文;相同编码的等级切换不重复发送。没有独立周期,风险不变时不重发;发送失败计数后也不因后续相同状态自动重试,等待新的二值状态变化。SDK 发送等待不占用推理线程;后台最多保留一个待发结果,发送变慢时用最新结果替换尚未发送的结果,避免积压旧风险;等待期间若从 A 变为 B 又回到正在发送的 A,会取消冗余待发报文,因此不保证每个短暂中间状态都上总线。CH1 发送失败通过现有 TX 错误计数呈现;结果中的 CAN 快照记录采样时已完成的发送计数,不保证包含当前帧的发送。停止时丢弃待发结果,并等待已经进入 SDK 的调用返回后释放设备。`--no-can` 或 `can.enabled=false` 禁用自动发送,视频回灌始终不发送。按需 `sendCan()` 仍为同步 API。 结果 JSON 和录制会话自动包含每帧 `can` 快照。视频回灌不打开 USB-CAN,不发送报文,直接恢复录制时的状态并标记 `source=replay`;旧会话没有 CAN 字段时显示 NOT_RECORDED。回灌调用 `sendCan()` 会明确拒绝。 @@ -182,3 +214,7 @@ bool sent = pipeline.sendCan(frame); 本次检查范围与延期项目见 [CAN 实现记录](tests/ORIN_CAN_REPORT.md);CH0 实机排查与单通道修复见 [0x391 实际位置反馈检查](tests/ORIN_391_REPORT.md)。 新增 `0x1F6` 二值风险发送的检查范围及部署状态见 [风险发送检查记录](tests/RISK_CAN_REPORT.md)。 + +风险发送现已改为首次发送及二值变化触发,验证结果见 [风险变化触发发送检查](tests/ORIN_RISK_CHANGE_REPORT.md)。 + +CAN 通道现为 CH0 接收、CH1 发送,板端检查与未覆盖项目见 [CH1 发送切换检查](tests/ORIN_CH1_TX_REPORT.md)。 diff --git a/annotate_door_mask.sh b/annotate_door_mask.sh new file mode 100755 index 0000000..ce0cae8 --- /dev/null +++ b/annotate_door_mask.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +exec "$root/build/door_mask_tool" --config "$root/config/camera.json" "$@" diff --git a/config/door_mask.json b/config/door_mask.json index 135b2a1..3fbef92 100644 --- a/config/door_mask.json +++ b/config/door_mask.json @@ -1,77 +1,51 @@ -{ - "version": "x-anylabeling", - "flags": {}, - "shapes": [ - { - "label": "door", - "points": [ - [ - 212.86, - 145.919 - ], - [ - 378.891, - 851.857 - ], - [ - 468.293, - 996.446 - ], - [ - 536.408, - 1294.131 - ], - [ - 553.437, - 1404.7 - ], - [ - 561.951, - 1532.279 - ], - [ - 51.086, - 1532.279 - ], - [ - 8.514, - 1515.268 - ], - [ - 0.0, - 1468.489 - ], - [ - 8.514, - 26.845 - ], - [ - 25.543, - 5.582 - ], - [ - 131.973, - -2.924 - ], - [ - 191.574, - 1.329 - ], - [ - 208.603, - 145.919 - ] - ], - "group_id": null, - "shape_type": "polygon", - "flags": {} - } - ], - "imagePath": "constant_door_mask_reference.jpg", - "imageData": null, - "imageHeight": 1536, - "imageWidth": 1920, - "sourceVideo": "D:\\huang.xuewei\\CMS\\video\\20260921104502\\segment_009.mp4", - "sourceFrameIndex": 244, - "constantDoorMask": true -} \ No newline at end of file +{ + "version": "x-anylabeling", + "flags": {}, + "shapes": [ + { + "label": "door", + "points": [ + [ + 1908, + 235 + ], + [ + 1874, + 267 + ], + [ + 1787, + 803 + ], + [ + 1735, + 873 + ], + [ + 1716, + 944 + ], + [ + 1682, + 1275 + ], + [ + 1654, + 1531 + ], + [ + 1919, + 1533 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + } + ], + "imagePath": "driver-side-2_reference.jpg", + "imageData": null, + "imageHeight": 1536, + "imageWidth": 1920, + "constantDoorMask": true +} diff --git a/config/door_masks/driver-side-1.json b/config/door_masks/driver-side-1.json new file mode 100644 index 0000000..af0e5df --- /dev/null +++ b/config/door_masks/driver-side-1.json @@ -0,0 +1,51 @@ +{ + "version": "x-anylabeling", + "flags": {}, + "shapes": [ + { + "label": "door", + "points": [ + [ + 314, + 4 + ], + [ + 425, + 572 + ], + [ + 495, + 713 + ], + [ + 534, + 822 + ], + [ + 542, + 1138 + ], + [ + 538, + 1522 + ], + [ + 6, + 1535 + ], + [ + 0, + 9 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + } + ], + "imagePath": "driver-side-1_reference.jpg", + "imageData": null, + "imageHeight": 1536, + "imageWidth": 1920, + "constantDoorMask": true +} diff --git a/config/door_masks/driver-side-1_reference.jpg b/config/door_masks/driver-side-1_reference.jpg new file mode 100644 index 0000000..bc7ce39 Binary files /dev/null and b/config/door_masks/driver-side-1_reference.jpg differ diff --git a/config/door_masks/driver-side-2.json b/config/door_masks/driver-side-2.json new file mode 100644 index 0000000..3fbef92 --- /dev/null +++ b/config/door_masks/driver-side-2.json @@ -0,0 +1,51 @@ +{ + "version": "x-anylabeling", + "flags": {}, + "shapes": [ + { + "label": "door", + "points": [ + [ + 1908, + 235 + ], + [ + 1874, + 267 + ], + [ + 1787, + 803 + ], + [ + 1735, + 873 + ], + [ + 1716, + 944 + ], + [ + 1682, + 1275 + ], + [ + 1654, + 1531 + ], + [ + 1919, + 1533 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + } + ], + "imagePath": "driver-side-2_reference.jpg", + "imageData": null, + "imageHeight": 1536, + "imageWidth": 1920, + "constantDoorMask": true +} diff --git a/config/door_masks/driver-side-2_reference.jpg b/config/door_masks/driver-side-2_reference.jpg new file mode 100644 index 0000000..0d6a5dc Binary files /dev/null and b/config/door_masks/driver-side-2_reference.jpg differ diff --git a/config/door_masks/driver-side.json b/config/door_masks/driver-side.json new file mode 100644 index 0000000..8ebca2e --- /dev/null +++ b/config/door_masks/driver-side.json @@ -0,0 +1,55 @@ +{ + "version": "x-anylabeling", + "flags": {}, + "shapes": [ + { + "label": "door", + "points": [ + [ + 1426, + 0 + ], + [ + 1257, + 489 + ], + [ + 1195, + 615 + ], + [ + 1155, + 636 + ], + [ + 1108, + 813 + ], + [ + 1106, + 1097 + ], + [ + 1097, + 1529 + ], + [ + 1917, + 1535 + ], + [ + 1915, + 9 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + } + ], + "imagePath": "driver-side1_reference.jpg", + "imageData": null, + "imageHeight": 1536, + "imageWidth": 1920, + "constantDoorMask": true +} diff --git a/config/door_masks/driver-side1_reference.jpg b/config/door_masks/driver-side1_reference.jpg new file mode 100644 index 0000000..0dd0496 Binary files /dev/null and b/config/door_masks/driver-side1_reference.jpg differ diff --git a/config/door_masks/driver-side_reference.jpg b/config/door_masks/driver-side_reference.jpg new file mode 100644 index 0000000..bc7ce39 Binary files /dev/null and b/config/door_masks/driver-side_reference.jpg differ diff --git a/include/can_bus.hpp b/include/can_bus.hpp index 0fda9f0..5c691dd 100644 --- a/include/can_bus.hpp +++ b/include/can_bus.hpp @@ -20,14 +20,15 @@ public: // Thread-safe on-demand data-frame send; invalid frames throw, SDK failure // returns false. Never generates periodic or inference-triggered messages. bool send(CanFrame const& frame); - // Send one standard 8-byte 0x1F6 frame on CH0. Both nibbles of - // data[0] contain 0 for "clear", 1 for every other level (including - // unavailable): 0x00 or 0x11. Bytes 1-7 are zero. Returns {CH0 result, false}; - // the second slot is retained for API compatibility, CH1 is never sent. + // Send one standard 8-byte 0x1F6 frame on CH1. Both nibbles of + // data[0] contain 0 for "clear", "watch" and "unavailable", 1 for every + // other level: 0x00 or 0x11. Bytes 1-7 are zero. Returns {false, CH1 result}. std::array sendRisk(std::string_view riskLevel); - // Nonblocking live-inference path. Keeps at most one pending risk result; - // a newer result replaces one waiting behind a slow SDK send. Returns - // queue acceptance, not delivery. No timer or retransmission of old risk. + // Nonblocking live-inference path. Attempts the initial binary risk once, + // then only encoded 0/1 changes. Latest pending state wins while busy; + // returning to the in-flight state cancels a redundant pending transition. + // True means accepted (including unchanged), not delivered. No periodic + // retransmission or retry of a failed state without a new binary change. bool queueRisk(std::string_view riskLevel); private: struct Impl; diff --git a/include/can_types.hpp b/include/can_types.hpp index 8179283..4611c8d 100644 --- a/include/can_types.hpp +++ b/include/can_types.hpp @@ -5,7 +5,10 @@ #include namespace cms { -inline constexpr unsigned liveCanChannelCount=1; // CH0 only; CH1 is not used. +inline constexpr unsigned canReceiveChannel=0; +inline constexpr unsigned canTransmitChannel=1; +inline constexpr unsigned canHardwareChannelCount=2; +inline constexpr unsigned liveCanChannelCount=1; // Only CH0 contributes live receive data. struct CanConfig { bool enabled{true}; int device_index{0}; diff --git a/include/door_mask_annotator.hpp b/include/door_mask_annotator.hpp new file mode 100644 index 0000000..86e6818 --- /dev/null +++ b/include/door_mask_annotator.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace cms { + +struct DoorMaskAnnotationOptions { + std::filesystem::path output_json; + std::filesystem::path reference_image; + bool overwrite{}; + std::optional source_video; + std::optional source_frame_index; +}; + +// Returns false when the user cancels without writing any annotation files. +bool annotateDoorMask(cv::Mat const& image, DoorMaskAnnotationOptions const& options); + +} diff --git a/orin/README.md b/orin/README.md index 39b2c23..8ee153e 100644 --- a/orin/README.md +++ b/orin/README.md @@ -24,6 +24,26 @@ sha256sum -c SHA256SUMS bin/drivers 中 GMSL 模块对应内核 5.10.120-tegra。初始化脚本检查内核版本;加载模块需要 root 或现有 sudo 权限。 门区保留当前室内联调设置,door.calibration_verified=false。 +## Door mask 标注工具 + +停止正在占用摄像头的推理程序后运行: + +```bash +./annotate_door_mask.sh --camera --output config/door_masks/driver-side.json +``` + +左键添加多边形顶点,右键或 `U` 撤销,`R` 清空,`S` 或 Enter 保存,Esc 或 `Q` 取消。工具输出 x-anylabeling JSON 及同目录参考 JPEG。 + +已有图像和视频模式不会打开摄像头: + +```bash +./annotate_door_mask.sh --image /path/to/frame.jpg --output config/door_masks/driver-side.json +./annotate_door_mask.sh --video /path/to/video.mp4 --output config/door_masks/driver-side.json +./annotate_door_mask.sh --video /path/to/video.mp4 --frame 244 --output config/door_masks/driver-side.json +``` + +视频未指定 `--frame` 时先通过滑块或 `A/D`、`J/L` 选择参考帧;帧号从 0 开始。 + ## 可选保存(默认全部关闭) 三个独立参数可以单独使用,也可以组合: @@ -54,10 +74,10 @@ bin/drivers 中 GMSL 模块对应内核 5.10.120-tegra。初始化脚本检查 ## CAN 状态与发送接口 -实时运行默认仅接收 USBCANFD CH0(500 kbps / 2 Mbps)。预览新增 `0x391` 的 LF/RF/LR/RR 四门实际位置反馈原始值,仅显示 CH0,并标记新鲜度和收发计数。`--no-can` 可关闭;设备缺失不停止推理。`config/camera.json` 的 `can.termination` 控制终端电阻,默认沿用参考工程的开启设置。 +实时运行默认从 USBCANFD CH0 接收、从 CH1 发送(两个通道均为 500 kbps / 2 Mbps)。预览显示 `0x391` 的 LF/RF/LR/RR 四门实际位置反馈原始值,仅显示 CH0,并标记新鲜度和收发计数。`--no-can` 可关闭;设备缺失不停止推理。`config/camera.json` 的 `can.termination` 同时控制两个通道的终端电阻,默认沿用参考工程的开启设置。 `lib/` 包含 SDK 与 libusb 及其 SONAME 链接。回灌使用录制的 CAN 快照,不访问总线;旧会话显示 NOT_RECORDED。按需发送通过 C++ `CameraPipeline::sendCan()`,参数与完整示例见工程 README。 -每个新的实时推理结果提交到后台 CAN 发送线程,仅在 CH0 发送一帧标准 CAN `0x1F6`,长度 8 字节:首字节高、低四位同时填写相同风险值,`clear` 时各为 0,其余状态(包括 `unavailable`)各为 1,即首字节为 `0x00` 或 `0x11`,后七字节清零。CH1 不初始化、不收发。发送等待不阻塞推理,最多保留一个待发结果,忙时由最新结果替换;发送失败计入 TX 错误。无新结果时不重发,`--no-can` 和视频回灌禁用发送。停止时清空待发结果,等待正在执行的 SDK 调用结束后释放设备。 +首个实时推理风险状态发送一次,此后仅在二值风险变化时,由后台线程在 CH1 发送一帧标准 CAN `0x1F6`,长度 8 字节:首字节高、低四位同时填写相同风险值,`clear`、`watch` 和 `unavailable` 时各为 0,`warning`、`danger` 及未知状态各为 1,即首字节为 `0x00` 或 `0x11`,后七字节清零。CH0 仅接收,CH1 仅发送。发送等待不阻塞推理,最多保留一个待发结果,忙时由最新结果替换;发送失败计入 TX 错误。仅在报文编码 0 与 1 之间变化时触发发送,相同编码的等级切换不重发。状态不变及发送失败时均不自动重试,等待新的二值变化。等待期间回到正在发送的状态会取消冗余待发报文。`--no-can` 和视频回灌禁用发送。停止时清空待发结果,等待正在执行的 SDK 调用结束后释放设备。 USB-CAN 首次接入如出现权限不足,运行 `./scripts/enable_can_access.sh`(需要 sudo,运行用户属于 plugdev),仅为 `3068:0009` 适配器安装设备访问规则。 diff --git a/orin/SHA256SUMS b/orin/SHA256SUMS index 35cd798..b324ede 100644 --- a/orin/SHA256SUMS +++ b/orin/SHA256SUMS @@ -1,16 +1,18 @@ +6e54ad1474a8a00a93bb5e1ffe9362eb31beab41a5b9c4b2e17baa5051932e27 ./annotate_door_mask.sh 6d5e874fd27fcfd75d303d7e35d9672028af71041f52dcc82a1e83676faefa65 ./bin/drivers/max9295.ko 8a78d04ef8be44e9b62a5e4ee315d3d6d679e34aedff02a234bbf4d40e222933 ./bin/drivers/max9296.ko 3da628587477cc35d5e3f8aee86e718e2ddf16808e893e00ba6e164b2307e843 ./bin/drivers/sgx-yuv-gmsl2.ko -418637a35f6ad05441578db1333e15714262a9c0ee088f10c2dd84923471cd0f ./build/camera_risk +1744ec8a2c584aa5306cdf975fb7f01ec9ee6cec524a447dfc85e179b270ffc3 ./build/camera_risk +12ebf1d21a967cc4d7620636352fc5779a697a0fb9feaa8bdb339927292a3517 ./build/door_mask_tool d44dcf10df43c7bcd73b7f2b64e4117d207435a5218c2146bfb659566cb320ba ./config/camera.json -9ba768d559fd3b64e833b50c62c70f2adfd6c169620aea5fba32cfb1b4c5b0e9 ./config/door_mask.json +7808f4ae27de49a02fe51f8e6b60ae8454ff775ea3e98fa0a7c633ee85027fb4 ./config/door_mask.json ebcd09e8c2e35008adbb0c1d222e580b17e0df1c8093df3481ba3374cd9a8174 ./engines/rtmw_l_384.engine f6a65b926ac1144484f4aa93cd26d7997d32d62310ab93fe867f2bd4412222e9 ./engines/yolo11s.engine 8dae8c557deadbe6bd7241c0dcd7025cb007077ee39a79b115e953a2b22cd9c1 ./lib/libusb-1.0.so 8dae8c557deadbe6bd7241c0dcd7025cb007077ee39a79b115e953a2b22cd9c1 ./lib/libusb-1.0.so.0 ab8b7f7249d5fb73d1d33d14fa7532a522088ffb6c25f37ba334daf4c6b4d313 ./lib/libusbcanfd.so ab8b7f7249d5fb73d1d33d14fa7532a522088ffb6c25f37ba334daf4c6b4d313 ./lib/libusbcanfd.so.1.0.12 -584875c5a52f969da7ae50514fd627749123f3db7140058052055982a6c01f2f ./README.md +73e6be03ec983fe6c14b0923c69cad7ec816ed4260257e6aecaf7fe341686cfc ./README.md 7f42b0e7f9396e0d82313a892fc62c153261dab1206a206a466bce6376f58a36 ./run.sh 41ebe2dcf57674232ca137d024e99447b467049cb1c63a284daae274296cb78c ./scripts/99-usbcanfd.rules 920102fc5e91bde9b78f6f7c5cb2ff8761a0c39b63c7a28b3347d5de5fe7c60a ./scripts/enable_can_access.sh diff --git a/orin/annotate_door_mask.sh b/orin/annotate_door_mask.sh new file mode 100755 index 0000000..ce0cae8 --- /dev/null +++ b/orin/annotate_door_mask.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +exec "$root/build/door_mask_tool" --config "$root/config/camera.json" "$@" diff --git a/orin/build/camera_risk b/orin/build/camera_risk index baf97c7..ad41ec8 100755 Binary files a/orin/build/camera_risk and b/orin/build/camera_risk differ diff --git a/orin/build/door_mask_tool b/orin/build/door_mask_tool new file mode 100755 index 0000000..3003ba9 Binary files /dev/null and b/orin/build/door_mask_tool differ diff --git a/orin/config/door_mask.json b/orin/config/door_mask.json index 135b2a1..8ebca2e 100644 --- a/orin/config/door_mask.json +++ b/orin/config/door_mask.json @@ -1,77 +1,55 @@ -{ - "version": "x-anylabeling", - "flags": {}, - "shapes": [ - { - "label": "door", - "points": [ - [ - 212.86, - 145.919 - ], - [ - 378.891, - 851.857 - ], - [ - 468.293, - 996.446 - ], - [ - 536.408, - 1294.131 - ], - [ - 553.437, - 1404.7 - ], - [ - 561.951, - 1532.279 - ], - [ - 51.086, - 1532.279 - ], - [ - 8.514, - 1515.268 - ], - [ - 0.0, - 1468.489 - ], - [ - 8.514, - 26.845 - ], - [ - 25.543, - 5.582 - ], - [ - 131.973, - -2.924 - ], - [ - 191.574, - 1.329 - ], - [ - 208.603, - 145.919 - ] - ], - "group_id": null, - "shape_type": "polygon", - "flags": {} - } - ], - "imagePath": "constant_door_mask_reference.jpg", - "imageData": null, - "imageHeight": 1536, - "imageWidth": 1920, - "sourceVideo": "D:\\huang.xuewei\\CMS\\video\\20260921104502\\segment_009.mp4", - "sourceFrameIndex": 244, - "constantDoorMask": true -} \ No newline at end of file +{ + "version": "x-anylabeling", + "flags": {}, + "shapes": [ + { + "label": "door", + "points": [ + [ + 1426, + 0 + ], + [ + 1257, + 489 + ], + [ + 1195, + 615 + ], + [ + 1155, + 636 + ], + [ + 1108, + 813 + ], + [ + 1106, + 1097 + ], + [ + 1097, + 1529 + ], + [ + 1917, + 1535 + ], + [ + 1915, + 9 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + } + ], + "imagePath": "driver-side1_reference.jpg", + "imageData": null, + "imageHeight": 1536, + "imageWidth": 1920, + "constantDoorMask": true +} diff --git a/scripts/package_orin.sh b/scripts/package_orin.sh index 75fd56b..89ccfdc 100755 --- a/scripts/package_orin.sh +++ b/scripts/package_orin.sh @@ -1,8 +1,10 @@ #!/usr/bin/env bash set -euo pipefail root="$(cd "$(dirname "$0")/.." && pwd)" -[[ -x "$root/build/camera_risk" ]] || { echo 'Build on Orin first.' >&2; exit 1; } +[[ -x "$root/build/camera_risk" && -x "$root/build/door_mask_tool" ]] || { echo 'Build on Orin first.' >&2; exit 1; } cp -p "$root/build/camera_risk" "$root/orin/build/" +cp -p "$root/build/door_mask_tool" "$root/orin/build/" +cp -p "$root/annotate_door_mask.sh" "$root/orin/" cp -p "$root/config/door_mask.json" "$root/orin/config/" sed 's|../orin/engines|../engines|' "$root/config/camera.json" > "$root/orin/config/camera.json" ( diff --git a/src/can_bus.cpp b/src/can_bus.cpp index 28ebd08..2a6c3ce 100644 --- a/src/can_bus.cpp +++ b/src/can_bus.cpp @@ -19,8 +19,11 @@ std::set owners; std::uint64_t nowNs() { return std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); } +bool assertedRisk(std::string_view level) { + return level!="clear"&&level!="watch"&&level!="unavailable"; +} void validate(CanFrame const& f) { - if(f.channel<0||unsigned(f.channel)>=liveCanChannelCount||f.id>(f.extended?0x1fffffffu:0x7ffu)||(!f.fd&&f.bitrate_switch)) + if(f.channel!=static_cast(canTransmitChannel)||f.id>(f.extended?0x1fffffffu:0x7ffu)||(!f.fd&&f.bitrate_switch)) throw std::invalid_argument("Invalid CAN channel, identifier or BRS flag"); auto size=f.data.size(); if((!f.fd&&size>8)||(f.fd&&!(size<=8||size==12||size==16||size==20||size==24||size==32||size==48||size==64))) @@ -32,7 +35,7 @@ struct CanBus::Impl { mutable std::mutex stateMutex; std::mutex lifecycleMutex,waitMutex,riskMutex; std::condition_variable wake,riskWake; - std::optional pendingRisk; + std::optional pendingRisk,lastQueuedRisk; CanSnapshot state; std::atomic stopped{false}; bool started{},opened{},owned{}; @@ -46,7 +49,7 @@ struct CanBus::Impl { void setStatus(char const* value) {std::lock_guard lock(stateMutex);state.status=value;} void releaseDevice() noexcept { if(opened) { - for(unsigned i=0;i frames{}; while(!stopped) { - auto count=VCI_ReceiveData(deviceType,config.device_index,0,frames.data(),receiveCapacity,100); + auto count=VCI_ReceiveData(deviceType,config.device_index,canReceiveChannel,frames.data(),receiveCapacity,100); if(count>receiveCapacity) { {std::lock_guard lock(stateMutex);++state.receive_errors;state.status="ERROR";} std::cerr<<"CAN receive failed; inference continues without live CAN"< stateLock(stateMutex); if(stopped||state.status!="OPEN")return false; } - pendingRisk=level!="clear";riskWake.notify_one();return true; + bool risk=assertedRisk(level); + if(lastQueuedRisk&&*lastQueuedRisk==risk)return true; + lastQueuedRisk=risk;pendingRisk=risk;riskWake.notify_one();return true; } void riskLoop() noexcept { try { CanFrame frame;frame.id=0x1f6;frame.data.assign(8,0); + std::optional lastAttemptedRisk; while(!stopped) { { std::unique_lock lock(riskMutex); riskWake.wait(lock,[this]{return stopped||pendingRisk.has_value();}); if(stopped)break; - frame.data[0]=*pendingRisk?0x11:0x00;pendingRisk.reset(); - } - for(unsigned channel=0;channel B -> A while A is being sent. Repeated + // inference results and failed sends do not trigger retries. + if(lastAttemptedRisk&&*lastAttemptedRisk==risk)continue; + lastAttemptedRisk=risk; + frame.data[0]=risk?0x11:0x00; } + if(!stopped){frame.channel=canTransmitChannel;send(frame);} } }catch(...) {setStatus("ERROR");} } @@ -208,11 +217,9 @@ bool CanBus::send(CanFrame const& f){return impl_->send(f);} bool CanBus::queueRisk(std::string_view level){return impl_->queueRisk(level);} std::array CanBus::sendRisk(std::string_view riskLevel) { CanFrame frame;frame.id=0x1f6;frame.data.assign(8,0); - frame.data[0]=riskLevel=="clear"?0x00:0x11; + frame.data[0]=assertedRisk(riskLevel)?0x11:0x00; std::array sent{}; - for(unsigned channel=0;channel +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cms { +namespace { +using Json = nlohmann::ordered_json; +constexpr char windowName[]="Door Mask Annotator"; +constexpr int maxDisplayWidth=1280; +constexpr int maxDisplayHeight=720; +constexpr int footerHeight=72; + +struct EditorState { + cv::Mat image; + std::vector points; + int displayWidth{}; + int displayHeight{}; + std::string message="Left click: add point Right click/U: undo R: reset S/Enter: save Esc/Q: cancel"; + + cv::Point toOriginal(int x,int y) const { + auto ox=static_cast(std::lround(double(x)*(image.cols-1)/std::max(displayWidth-1,1))); + auto oy=static_cast(std::lround(double(y)*(image.rows-1)/std::max(displayHeight-1,1))); + return {std::clamp(ox,0,image.cols-1),std::clamp(oy,0,image.rows-1)}; + } + cv::Point toDisplay(cv::Point const& p) const { + return {static_cast(std::lround(double(p.x)*(displayWidth-1)/std::max(image.cols-1,1))), + static_cast(std::lround(double(p.y)*(displayHeight-1)/std::max(image.rows-1,1)))}; + } +}; + +void onMouse(int event,int x,int y,int,void* opaque) { + auto& state=*static_cast(opaque); + if(x<0||y<0||x>=state.displayWidth||y>=state.displayHeight)return; + if(event==cv::EVENT_LBUTTONDOWN) { + state.points.push_back(state.toOriginal(x,y)); + state.message="Point added. Add at least 3 points, then press S or Enter."; + } else if(event==cv::EVENT_RBUTTONDOWN&&!state.points.empty()) { + state.points.pop_back();state.message="Last point removed."; + } +} + +cv::Mat render(EditorState const& state) { + cv::Mat scaled; + cv::resize(state.image,scaled,{state.displayWidth,state.displayHeight},0,0,cv::INTER_AREA); + std::vector displayed; + displayed.reserve(state.points.size()); + for(auto const& point:state.points)displayed.push_back(state.toDisplay(point)); + if(displayed.size()>=3) { + auto overlay=scaled.clone(); + cv::fillPoly(overlay,std::vector>{displayed},cv::Scalar(30,180,255)); + cv::addWeighted(overlay,0.25,scaled,0.75,0,scaled); + cv::polylines(scaled,std::vector>{displayed},true,cv::Scalar(20,230,255),2,cv::LINE_AA); + } else if(displayed.size()>=2) { + cv::polylines(scaled,std::vector>{displayed},false,cv::Scalar(20,230,255),2,cv::LINE_AA); + } + for(std::size_t i=0;i const& points) { + return points.size()>=3&&std::abs(cv::contourArea(points))>=10.0; +} + +void writeFiles(EditorState const& state,DoorMaskAnnotationOptions const& options) { + auto const output=std::filesystem::absolute(options.output_json); + auto const reference=std::filesystem::absolute(options.reference_image); + if(output.extension()!=".json")throw std::invalid_argument("Door mask output must use the .json extension"); + if(!options.overwrite&&(std::filesystem::exists(output)||std::filesystem::exists(reference))) + throw std::runtime_error("Output already exists; use --force to replace it: "+output.string()); + if(!output.parent_path().empty())std::filesystem::create_directories(output.parent_path()); + if(!reference.parent_path().empty())std::filesystem::create_directories(reference.parent_path()); + + auto imageTmp=reference;imageTmp.replace_filename(reference.stem().string()+".tmp"+reference.extension().string()); + if(!cv::imwrite(imageTmp.string(),state.image,{cv::IMWRITE_JPEG_QUALITY,95})) + throw std::runtime_error("Cannot write reference image: "+reference.string()); + std::error_code error; + if(options.overwrite)std::filesystem::remove(reference,error); + std::filesystem::rename(imageTmp,reference); + + Json shape={{"label","door"},{"points",Json::array()},{"group_id",nullptr},{"shape_type","polygon"},{"flags",Json::object()}}; + for(auto const& point:state.points)shape["points"].push_back({point.x,point.y}); + Json document={{"version","x-anylabeling"},{"flags",Json::object()},{"shapes",Json::array({shape})}, + {"imagePath",reference.filename().string()},{"imageData",nullptr},{"imageHeight",state.image.rows},{"imageWidth",state.image.cols}, + {"constantDoorMask",true}}; + if(options.source_video)document["sourceVideo"]=*options.source_video; + if(options.source_frame_index)document["sourceFrameIndex"]=*options.source_frame_index; + auto text=document.dump(2)+"\n"; + auto jsonTmp=output;jsonTmp+=".tmp"; + std::ofstream stream;stream.exceptions(std::ios::failbit|std::ios::badbit); + stream.open(jsonTmp,std::ios::binary|std::ios::trunc);stream.write(text.data(),static_cast(text.size()));stream.close(); + if(options.overwrite)std::filesystem::remove(output,error); + std::filesystem::rename(jsonTmp,output); +} +} + +bool annotateDoorMask(cv::Mat const& image,DoorMaskAnnotationOptions const& options) { + if(image.empty()||image.type()!=CV_8UC3)throw std::invalid_argument("Door mask annotation requires a non-empty BGR image"); + if(options.output_json.empty()||options.reference_image.empty())throw std::invalid_argument("Door mask output paths cannot be empty"); + auto scale=std::min({1.0,double(maxDisplayWidth)/image.cols,double(maxDisplayHeight)/image.rows}); + EditorState state{image.clone(),{},std::max(1,static_cast(std::lround(image.cols*scale))), + std::max(1,static_cast(std::lround(image.rows*scale)))}; + cv::namedWindow(windowName,cv::WINDOW_AUTOSIZE); + cv::setMouseCallback(windowName,onMouse,&state); + try { + while(true) { + cv::imshow(windowName,render(state)); + auto raw=cv::waitKeyEx(20),key=raw&0xff; + if(cvGetWindowHandle(windowName)==nullptr) {cv::destroyWindow(windowName);return false;} + if(key==27||key=='q'||key=='Q') {cv::destroyWindow(windowName);return false;} + if(key=='r'||key=='R') {state.points.clear();state.message="Polygon cleared.";continue;} + if((key=='u'||key=='U'||key==8||key==127)&&!state.points.empty()) {state.points.pop_back();state.message="Last point removed.";continue;} + if(key=='s'||key=='S'||key==10||key==13) { + if(!usablePolygon(state.points)) {state.message="A valid door polygon needs at least 3 non-collinear points.";continue;} + writeFiles(state,options);cv::destroyWindow(windowName);return true; + } + } + } catch(...) {cv::destroyWindow(windowName);throw;} +} + +} diff --git a/src/door_mask_tool.cpp b/src/door_mask_tool.cpp new file mode 100644 index 0000000..32fc72b --- /dev/null +++ b/src/door_mask_tool.cpp @@ -0,0 +1,160 @@ +#include "camera.hpp" +#include "config.hpp" +#include "door_mask_annotator.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { +constexpr char videoWindowName[]="Select Door Mask Reference Frame"; + +cv::Mat captureReference(std::filesystem::path const& configPath) { + auto config=cms::loadConfig(configPath); + cms::Camera camera(config.camera); + std::optional frame; + // Let exposure and the camera link settle while retaining the newest frame. + for(int i=0;i<5;++i)frame=camera.read(); + camera.requestStop(); + if(!frame||!frame->image)throw std::runtime_error("Camera stopped before an annotation frame was captured"); + auto const& image=*frame->image; + return cv::Mat(image.height,image.width,CV_8UC3,const_cast(image.pixels.data()),image.stride).clone(); +} + +cv::Mat readVideoFrame(cv::VideoCapture& video,int index) { + if(!video.set(cv::CAP_PROP_POS_FRAMES,index))throw std::runtime_error("Cannot seek to video frame "+std::to_string(index)); + cv::Mat frame; + if(!video.read(frame)||frame.empty())throw std::runtime_error("Cannot decode video frame "+std::to_string(index)); + return frame; +} + +std::optional> selectVideoFrame(std::filesystem::path const& path,std::optional requested) { + cv::VideoCapture video(path.string(),cv::CAP_FFMPEG); + if(!video.isOpened())throw std::runtime_error("Cannot open annotation video: "+path.string()); + auto countValue=video.get(cv::CAP_PROP_FRAME_COUNT); + if(!std::isfinite(countValue)||countValue<1||countValue>std::numeric_limits::max()) + throw std::runtime_error("Annotation video does not report a usable frame count: "+path.string()); + auto count=static_cast(std::llround(countValue)); + if(requested) { + if(*requested<0||*requested>=count)throw std::out_of_range("--frame is outside the video: "+std::to_string(*requested)); + return std::pair{readVideoFrame(video,static_cast(*requested)),*requested}; + } + if(count==1)return std::pair{readVideoFrame(video,0),0}; + + int position=0,current=-1; + cv::Mat selected; + cv::namedWindow(videoWindowName,cv::WINDOW_AUTOSIZE); + cv::createTrackbar("Frame",videoWindowName,&position,count-1,[](int,void*){},nullptr); + try { + while(true) { + if(position!=current) {selected=readVideoFrame(video,position);current=position;} + auto scale=std::min({1.0,1280.0/selected.cols,680.0/selected.rows}); + auto width=std::max(1,static_cast(std::lround(selected.cols*scale))); + auto height=std::max(1,static_cast(std::lround(selected.rows*scale))); + cv::Mat preview,canvas(height+58,width,CV_8UC3,cv::Scalar(24,27,32)); + cv::resize(selected,preview,{width,height},0,0,cv::INTER_AREA); + preview.copyTo(canvas(cv::Rect(0,0,width,height))); + auto timeMs=video.get(cv::CAP_PROP_POS_MSEC); + cv::putText(canvas,"Frame "+std::to_string(position)+" / "+std::to_string(count-1)+" "+ + std::to_string(static_cast(std::llround(timeMs)))+" ms",{12,height+23},cv::FONT_HERSHEY_SIMPLEX,.6,cv::Scalar(235,235,235),1,cv::LINE_AA); + cv::putText(canvas,"Trackbar or A/D: previous/next J/L: -/+30 S/Enter: select Esc/Q: cancel", + {12,height+47},cv::FONT_HERSHEY_SIMPLEX,.45,cv::Scalar(180,220,255),1,cv::LINE_AA); + cv::imshow(videoWindowName,canvas); + auto raw=cv::waitKeyEx(20),key=raw&0xff,next=position; + if(cvGetWindowHandle(videoWindowName)==nullptr) {cv::destroyWindow(videoWindowName);return std::nullopt;} + if(key==27||key=='q'||key=='Q') {cv::destroyWindow(videoWindowName);return std::nullopt;} + if(key=='a'||key=='A'||raw==65361||raw==2424832)next=position-1; + else if(key=='d'||key=='D'||raw==65363||raw==2555904)next=position+1; + else if(key=='j'||key=='J')next=position-30; + else if(key=='l'||key=='L')next=position+30; + else if(key=='s'||key=='S'||key==10||key==13) { + auto result=std::pair{selected.clone(),position}; + cv::destroyWindow(videoWindowName);return result; + } + next=std::clamp(next,0,count-1); + if(next!=position) {position=next;cv::setTrackbarPos("Frame",videoWindowName,position);} + } + } catch(...) {cv::destroyWindow(videoWindowName);throw;} +} + +void printHelp() { + std::cout<< + "door_mask_tool --output MASK.json [--camera | --image IMAGE | --video VIDEO [--frame N]] [--config CAMERA.json] [--force]\n" + "\n" + "With no input option, --camera is used. Image and video modes never open the camera.\n" + "Video mode opens a frame selector unless --frame selects a zero-based frame directly.\n" + "The reference JPEG is stored beside MASK.json. Existing files are preserved unless --force is used.\n" + "\n" + "Mouse/keys: left click adds a point; right click or U undoes; R resets;\n" + "S or Enter saves; Esc or Q cancels without writing files.\n"; +} +} + +int main(int argc,char** argv)try { + auto root=std::filesystem::canonical("/proc/self/exe").parent_path().parent_path(); + std::filesystem::path configPath=root/"config/camera.json",imagePath,videoPath,outputPath; + std::optional requestedFrame; + bool force=false,cameraInput=false; + for(int i=1;i=argc)throw std::invalid_argument(std::string(name)+" needs a path");return std::filesystem::path(argv[i]);}; + if(argument=="--output")outputPath=value("--output"); + else if(argument=="--camera")cameraInput=true; + else if(argument=="--image")imagePath=value("--image"); + else if(argument=="--video")videoPath=value("--video"); + else if(argument=="--frame") { + if(++i>=argc)throw std::invalid_argument("--frame needs a zero-based frame number"); + std::size_t consumed{};auto text=std::string(argv[i]);auto parsed=std::stoll(text,&consumed); + if(consumed!=text.size()||parsed<0)throw std::invalid_argument("--frame must be a non-negative integer"); + requestedFrame=parsed; + } + else if(argument=="--config")configPath=value("--config"); + else if(argument=="--force")force=true; + else if(argument=="--help"||argument=="-h") {printHelp();return 0;} + else throw std::invalid_argument("Unknown option: "+argument); + } + if(outputPath.empty())throw std::invalid_argument("--output MASK.json is required"); + auto inputs=int(cameraInput)+int(!imagePath.empty())+int(!videoPath.empty()); + if(inputs>1)throw std::invalid_argument("Choose exactly one input: --camera, --image, or --video"); + if(inputs==0)cameraInput=true; + if(requestedFrame&&videoPath.empty())throw std::invalid_argument("--frame can only be used with --video"); + if(outputPath.extension()!=".json")throw std::invalid_argument("Door mask output must use the .json extension"); + auto reference=outputPath.parent_path()/(outputPath.stem().string()+"_reference.jpg"); + if(!force&&(std::filesystem::exists(outputPath)||std::filesystem::exists(reference))) + throw std::runtime_error("Output already exists; use --force to replace it: "+outputPath.string()); + auto display=std::getenv("DISPLAY"),wayland=std::getenv("WAYLAND_DISPLAY"); + if((!display||!*display)&&(!wayland||!*wayland))throw std::runtime_error("No graphical display is available for annotation"); + + cv::Mat image;std::optional selectedFrame; + if(cameraInput) { + std::cout<<"Capturing annotation frame using "<first);selectedFrame=selected->second; + } + cms::DoorMaskAnnotationOptions options;options.output_json=outputPath;options.reference_image=reference;options.overwrite=force; + if(!videoPath.empty()) {options.source_video=std::filesystem::absolute(videoPath).string();options.source_frame_index=selectedFrame;} + if(!cms::annotateDoorMask(image,options)) {std::cout<<"Annotation cancelled; no files written.\n";return 2;} + std::cout<<"Door mask saved: "<=argc)throw std::invalid_argument("--config needs a file");configPath=argv[i];}}, - {"--help",[](int&){std::cout<<"camera_risk [--record-session NEW_DIR] [--replay-session DIR] [--save-original] [--save-annotated] [--save-results] [--config FILE] [--no-can] [--visualize|--no-visualize]\nLive CAN/CANFD reception and per-result 0x1F6 binary risk transmission on CH0 only are enabled by default.\nRisk: clear=0, all other levels=1; --no-can disables both reception and transmission.\nReplay uses recorded CAN only and never transmits.\nVisualization is enabled by default for live inference, recording and replay.\nContinuous camera inference; replay runs to EOF without a camera.\nH.264 sessions require a new recording directory. Stop with Ctrl+C or SIGTERM to finalize video.\n";std::exit(0);}} + {"--help",[](int&){std::cout<<"camera_risk [--record-session NEW_DIR] [--replay-session DIR] [--save-original] [--save-annotated] [--save-results] [--config FILE] [--no-can] [--visualize|--no-visualize]\nLive CAN/CANFD reception on CH0 and change-triggered 0x1F6 binary risk transmission on CH1 are enabled by default.\nRisk: clear/watch/unavailable=0, warning/danger/unknown=1; send initial state once, then only binary changes. No automatic retries. --no-can disables reception and transmission.\nReplay uses recorded CAN only and never transmits.\nVisualization is enabled by default for live inference, recording and replay.\nContinuous camera inference; replay runs to EOF without a camera.\nH.264 sessions require a new recording directory. Stop with Ctrl+C or SIGTERM to finalize video.\n";std::exit(0);}} }; for(int i=1;isecond(i);} diff --git a/tests/ORIN_CH1_TX_REPORT.md b/tests/ORIN_CH1_TX_REPORT.md new file mode 100644 index 0000000..05d233d --- /dev/null +++ b/tests/ORIN_CH1_TX_REPORT.md @@ -0,0 +1,25 @@ +# CAN CH1 发送切换(2026-09-29) + +实时 CAN 改为 CH0 只接收、CH1 只发送。CH0 与 CH1 都使用 60 MHz 时钟、仲裁域 500 kbps、数据域 2 Mbps,并随设备生命周期一同初始化、启动和复位;合并接收仍由 CH0 的单一接收线程执行,收到的 CH1 数据被忽略。自动风险报文和按需 `sendCan()` 均只允许 CH1,CH0 发送参数会抛出 `std::invalid_argument`。 + +风险协议未改变:标准 CAN `0x1F6`、DLC 8,首字节 `clear=0x00`,其余状态 `0x11`,后七字节清零;首次状态发送一次,之后仅在二值状态变化时发送。摄像头采集、停止恢复、0x391 解析和实时画面只显示 CH0 的行为未改动。 + +## 本机模拟回归 + +`tests/run_can.sh` 通过(模拟 SDK + UBSan)。测试桩强制要求 CH0/CH1 均完成初始化、终端电阻配置、启动和复位,所有经典 CAN/CANFD 发送调用必须使用 CH1;覆盖 CH0 接收及 CH1 输入忽略、按需发送校验、风险变化触发、发送失败、阻塞合并和停止清理。`tests/run_session.sh` 通过,会话兼容性未受影响;完整 CMake 构建和 `git diff --check` 通过。 + +## Orin 真实硬件验证 + +使用当前 Orin 的 `/dev/video5`、TensorRT 引擎、USBCANFD-200U 和原 `config/camera.json`,以 `./run_camera.sh --no-visualize --save-results` 运行 12 秒。SDK 识别到双通道适配器并打印 `CH0 receive, CH1 transmit`;产生 260 个结果 JSON,末次快照为 CAN `OPEN`、CH1 成功发送 1 帧、发送错误 0,CH0 的 `0x391` 计数为 11,证明摄像头/TensorRT 推理、CH0 接收和 CH1 SDK 发送可在同一进程继续运行。 + +本次未进行板端故障注入或物理拔插,没有人为触发风险 0/1 变化,也未使用独立总线分析仪或 CH1 接收节点核验线上 `0x1F6` 的电气载荷与 ACK。因此“CH1 线上报文已被对端接收”仍未覆盖,不能由 SDK 成功计数替代。测试前无常驻 `camera_risk`,测试结束恢复为无常驻进程;原配置和启动脚本未修改。 + +详细计数与版本见 [实机结果](orin_ch1_tx_results.json)。 + +## 部署核对 + +源码构建 `build/camera_risk` 与独立包 `orin/build/camera_risk` SHA256 一致: + +`feca0b7f476171f45489c22f11a51ba1fe0dabe2d9a17bfcdc50110d1afdb276` + +独立包已运行 `sha256sum -c orin/SHA256SUMS` 并全部通过。当前检出分支为 `inference-only`,HEAD `f87e5aad40478824da16eb42c76b40383de3a9a1`;修改位于未提交工作区。联网只读核对远端 `inference-only` 为 `0192fccbf06cb928eac098b3dfad32abf0fc370d`,本次未创建提交或推送远端。 diff --git a/tests/ORIN_RISK_CHANGE_REPORT.md b/tests/ORIN_RISK_CHANGE_REPORT.md new file mode 100644 index 0000000..cd89fcb --- /dev/null +++ b/tests/ORIN_RISK_CHANGE_REPORT.md @@ -0,0 +1,27 @@ +# CAN 风险变化触发发送(2026-09-28) + +实时自动发送仍仅使用 CH0、标准 CAN `0x1F6`、8 字节;首字节 `clear=0x00`,其余状态 `0x11`,后七字节清零。首个风险状态尝试发送一次,之后仅在报文的二值风险变化时发送。`watch/warning/danger/unavailable` 间切换不重复发送。 + +后台最多保留最新待发状态。发送 A 期间若状态 A→B→A,取消冗余的待发 A;短暂中间状态可能被合并。发送失败计入错误,不随后续相同状态重试,也无周期重发;发生新的二值变化才触发后续发送。显式按需 `sendCan()` / `sendRisk()` 保持调用即尝试发送的接口语义。摄像头采集、停止恢复和 0x391 解析未改动。 + +## 本机模拟回归 + +`tests/run_can.sh` 通过(模拟 SDK + UBSan)。新增首次 clear、首次非 clear、1000 次相同状态去重、不同非 clear 等级去重、0/1 往返、失败后相同状态不重试、新变化恢复发送、发送阻塞期间回到原状态不重复发帧,以及后续 clear 不丢失。原有收发、CH0 限制、过期恢复、最新状态合并和停止回归通过。构建与 `git diff --check` 通过。 + +## 真实硬件验证 + +使用 Orin 现有摄像头、TensorRT 引擎、USBCANFD 和原配置。12 秒产生 357 个推理结果,观测风险始终为 `unavailable`(二值 1),CH0 成功发送 1 帧,发送错误为 0;0x391 接收计数持续增长至 15。证明稳定风险不会随每个推理结果重复发送,推理与位置反馈接收持续工作。 + +本次未进行板端故障注入或物理拔插,未人为向实车注入伪造风险状态。实机场景没有发生风险 0/1 切换,变化载荷和失败路径由本机模拟回归覆盖;尚未以独立总线分析仪核验线上报文,未覆盖长期运行或接收端重启后状态重新同步。不能把模拟切换测试当作真实场景风险切换验证。 + +采样源码 `/tmp/vap_risk_change_probe.cpp`,原始日志 `/tmp/vap_risk_change_probe.log`。详细计数与版本见 [实机结果](orin_risk_change_results.json)。 + +## 部署核对 + +另按原命令 `./run_camera.sh` 启动正式程序;SIGTERM 后约 0.919 秒退出,退出码 0。测试前无常驻 camera_risk,测试结束恢复无常驻进程状态,未修改启动参数。 + +运行中 `/proc//exe`、源码构建 `build/camera_risk`、独立包 `orin/build/camera_risk` SHA256 一致: + +`5fa01c08baf73b1e044288b5e85f753c10f10d7cc9559ff25952bc4618f01899` + +独立包已更新并刷新 `orin/SHA256SUMS`。硬件验证时本机即 Orin,检出 `inference-only`,HEAD 为 `f87e5aad40478824da16eb42c76b40383de3a9a1`,当时修复位于未提交工作区。经用户要求,最终修改和验证记录纳入本报告所在本地提交。验证时联网核对远端 `inference-only` 为 `0192fccbf06cb928eac098b3dfad32abf0fc370d`,未推送本次修改。 diff --git a/tests/can_bus_test.cpp b/tests/can_bus_test.cpp index d63a3eb..cfd713b 100644 --- a/tests/can_bus_test.cpp +++ b/tests/can_bus_test.cpp @@ -15,7 +15,7 @@ using namespace std::chrono_literals; std::mutex mutex; std::condition_variable changed; std::deque incoming; -std::atomic opens{0},closes{0},resets{0},mergeCalls{0},txCalls{0},rxCalls{0}; +std::atomic opens{0},closes{0},initializations{0},starts{0},resets{0},terminationCalls{0},mergeCalls{0},txCalls{0},rxCalls{0}; std::atomic unavailable{false},initFailure{false},receiveFailure{false},txFailure{false}; ZCAN_20_MSG sentCan{};ZCAN_FD_MSG sentFd{}; std::vector sentCanFrames; @@ -30,7 +30,7 @@ template void waitFor(F f) { } template void rejects(F f){bool failed=false;try{f();}catch(std::exception const&){failed=true;}check(failed,"Expected rejection");} void reset() { - opens=closes=resets=mergeCalls=txCalls=rxCalls=0; + opens=closes=initializations=starts=resets=terminationCalls=mergeCalls=txCalls=rxCalls=0; unavailable=initFailure=receiveFailure=txFailure=false; sentCanFrames.clear();failedTxChannel=-1; blockTx=false;txBlocked=false; @@ -49,16 +49,19 @@ extern "C" { U32 VCI_OpenDevice(U32 type,U32,U32){check(type==33,"Device type changed");++opens;return unavailable?0:1;} U32 VCI_CloseDevice(U32,U32){++closes;return 1;} U32 VCI_InitCAN(U32,U32,U32 port,ZCAN_INIT* init){ - check(port==0,"CH1 initialized"); + check(port<2,"Unexpected CAN channel initialized");++initializations; check(init->clk==60000000&&init->mode==0&&init->aset.brp==5&&init->dset.brp==1,"Reference timing not retained"); return initFailure?0:1; } U32 VCI_SetReference(U32,U32,U32 port,U32 ref,void* data){ - check(port==0,"CH1 configured"); - if(ref==ZCAN_CMD_SET_CHNL_RECV_MERGE){check(port==0&&*static_cast(data)==1,"Invalid merged receive configuration");++mergeCalls;}return 1; + check(port<2,"Unexpected CAN channel configured"); + if(ref==CMD_CAN_TRES){check(*static_cast(data)==1,"Invalid termination configuration");++terminationCalls;} + else if(ref==ZCAN_CMD_SET_CHNL_RECV_MERGE){check(port==0&&*static_cast(data)==1,"Invalid merged receive configuration");++mergeCalls;} + else check(false,"Unexpected CAN reference configuration"); + return 1; } -U32 VCI_StartCAN(U32,U32,U32 port){check(port==0,"CH1 started");return 1;} -U32 VCI_ResetCAN(U32,U32,U32 port){check(port==0,"CH1 reset");++resets;return 1;} +U32 VCI_StartCAN(U32,U32,U32 port){check(port<2,"Unexpected CAN channel started");++starts;return 1;} +U32 VCI_ResetCAN(U32,U32,U32 port){check(port<2,"Unexpected CAN channel reset");++resets;return 1;} U32 VCI_ReceiveData(U32,U32,U32 port,ZCANDataObj* out,U32 capacity,U32){ check(port==0,"Multiple merged receive readers");++rxCalls; if(receiveFailure)return std::numeric_limits::max(); @@ -67,7 +70,7 @@ U32 VCI_ReceiveData(U32,U32,U32 port,ZCANDataObj* out,U32 capacity,U32){ U32 count=0;while(!incoming.empty()&&counthdr.chn,"Invalid CAN send call");++txCalls;sentCan=*message; + check(port==1&&count==1&&port==message->hdr.chn,"CAN send did not use CH1");++txCalls;sentCan=*message; { std::unique_lock lock(txMutex);sentCanFrames.push_back(*message); if(blockTx) { @@ -78,14 +81,14 @@ U32 VCI_Transmit(U32,U32,U32 port,ZCAN_20_MSG* message,U32 count){ return txFailure||int(port)==failedTxChannel?0:1; } U32 VCI_TransmitFD(U32,U32,U32 port,ZCAN_FD_MSG* message,U32 count){ - check(port==0&&count==1&&port==message->hdr.chn,"Invalid CANFD send call");++txCalls;sentFd=*message;return txFailure?0:1; + check(port==1&&count==1&&port==message->hdr.chn,"CANFD send did not use CH1");++txCalls;sentFd=*message;return txFailure?0:1; } } int main()try { using namespace cms; reset();{ CanConfig c;c.enabled=false;CanBus bus(c);bus.start(); - check(bus.snapshot().status=="DISABLED"&&!bus.send({0,0x123,false,false,false,{1,2}}),"Disabled CAN used hardware"); + check(bus.snapshot().status=="DISABLED"&&!bus.send({1,0x123,false,false,false,{1,2}}),"Disabled CAN used hardware"); }check(opens==0,"Disabled CAN opened device"); reset();unavailable=true;{ CanBus bus({});bus.start();check(bus.snapshot().status=="UNAVAILABLE","Missing adapter not reported"); @@ -95,7 +98,8 @@ int main()try { }check(closes==1&&resets==0,"Partial initialization leaked device"); reset();{ CanConfig c;c.timeout_ms=150;CanBus bus(c);bus.start(); - check(mergeCalls==1,"Merged receive configured more than once"); + check(initializations==2&&starts==2&&terminationCalls==2&&mergeCalls==1, + "CH0/CH1 initialization or merged receive configuration is wrong"); CanBus duplicate(c);rejects([&]{duplicate.start();}); inject(0,false,0x391,{1,37,128,255},123); waitFor([&]{return bus.snapshot().received==1;}); @@ -113,41 +117,42 @@ int main()try { s=bus.snapshot();check(s.channels[0].received_monotonic_ns==timestamp&&s.receive_errors==1,"Invalid input refreshed signal"); waitFor([&]{return !bus.snapshot().channels[0].valid;}); inject(0,false,0x391,{0,1,2,3});waitFor([&]{return bus.snapshot().channels[0].valid;}); - check(bus.send({0,0x321,false,false,false,{1,2,3}}),"CAN send failed"); + check(bus.send({1,0x321,false,false,false,{1,2,3}}),"CAN send failed"); check(sentCan.hdr.len==3&&!sentCan.hdr.inf.sef&&sentCan.dat[2]==3,"CAN encoding incorrect"); - CanFrame fd{0,0x12345,true,true,true,std::vector(64,0xa5)}; + CanFrame fd{1,0x12345,true,true,true,std::vector(64,0xa5)}; check(bus.send(fd)&&sentFd.hdr.len==64&&sentFd.hdr.inf.brs&&sentFd.hdr.inf.sef&&sentFd.dat[63]==0xa5,"CANFD encoding incorrect"); fd.data.resize(9);rejects([&]{bus.send(fd);}); - rejects([&]{bus.send({1,0x391,false,false,false,{}});}); - rejects([&]{bus.send({0,0x800,false,false,false,{}});}); - rejects([&]{bus.send({0,0x391,false,false,true,{}});}); - txFailure=true;check(!bus.send({}),"SDK send failure ignored"); + rejects([&]{bus.send({0,0x391,false,false,false,{}});}); + rejects([&]{bus.send({1,0x800,false,false,false,{}});}); + rejects([&]{bus.send({1,0x391,false,false,true,{}});}); + txFailure=true;check(!bus.send({1,0,false,false,false,{}}),"SDK send failure ignored"); check(bus.snapshot().transmitted==2&&bus.snapshot().transmit_errors==1,"TX counters wrong"); receiveFailure=true;waitFor([&]{return bus.snapshot().status=="ERROR";}); - check(!bus.snapshot().channels[0].valid&&!bus.send({}),"Receive error left bus live"); - bus.requestStop();bus.join();check(closes==1&&resets==1,"Shutdown did not release CH0"); - check(!bus.send({}),"Transmission after stop"); + check(!bus.snapshot().channels[0].valid&&!bus.send({1,0,false,false,false,{}}),"Receive error left bus live"); + bus.requestStop();bus.join();check(closes==1&&resets==2,"Shutdown did not release CH0 and CH1"); + check(!bus.send({1,0,false,false,false,{}}),"Transmission after stop"); }check(closes==1,"Device closed twice"); reset();{ CanBus bus({});bus.start(); for(auto level:{"clear","watch","warning","danger","unavailable","unknown","","clear"}) { auto sent=bus.sendRisk(level); - check(sent==std::array{true,false},"Risk must use CH0 only"); + check(sent==std::array{false,true},"Risk must use CH1 only"); } check(sentCanFrames.size()==8,"Risk must send exactly once per channel per result"); for(std::size_t i=0;i{false,false},"CH0 failure reported success"); + failedTxChannel=1; + check(bus.sendRisk("danger")==std::array{false,false},"CH1 failure reported success"); failedTxChannel=-1; - check(bus.sendRisk("clear")==std::array{true,false},"CH0 did not recover after send failure"); + check(bus.sendRisk("clear")==std::array{false,true},"CH1 did not recover after send failure"); check(bus.snapshot().transmitted==9&&bus.snapshot().transmit_errors==1,"Risk TX counters wrong"); auto calls=txCalls.load();bus.requestStop(); check(bus.sendRisk("danger")==std::array{false,false}&&txCalls==calls,"Risk sent after stop"); @@ -172,11 +177,11 @@ int main()try { { std::lock_guard lock(txMutex); check(sentCanFrames.size()==2,"Stale pending results accumulated"); - for(int i=0;i<2;++i)check(sentCanFrames[i].hdr.chn==0&& + for(int i=0;i<2;++i)check(sentCanFrames[i].hdr.chn==1&& sentCanFrames[i].dat[0]==(i==0?0x11:0x00),"Async risk or latest result incorrect"); } - failedTxChannel=0; - check(bus.queueRisk("unavailable"),"Ready queue rejected unavailable risk"); + failedTxChannel=1; + check(bus.queueRisk("danger"),"Ready queue rejected changed risk"); waitFor([&]{auto s=bus.snapshot();return s.transmitted==2&&s.transmit_errors==1;}); failedTxChannel=-1;txBlocked=false;blockTx=true; check(bus.queueRisk("clear"),"Ready queue rejected clear risk");waitFor([]{return txBlocked.load();}); @@ -184,8 +189,61 @@ int main()try { auto calls=txCalls.load();bus.requestStop(); check(!bus.queueRisk("danger"),"Stopped bus accepted risk"); {std::lock_guard lock(txMutex);blockTx=false;}txWake.notify_all(); - bus.join();check(txCalls==calls,"Stop sent a pending result or second channel"); - check(closes==1&&resets==1,"Async shutdown leaked hardware"); + bus.join();check(txCalls==calls,"Stop sent a pending result"); + check(closes==1&&resets==2,"Async shutdown leaked hardware"); } - std::cout<<"PASS CAN/CANFD RX decoding, CH0-only reception, expiry/recovery, TX validation, binary risk 0x1F6 on CH0 only, errors and lifecycle cleanup\n"; + reset();{ + CanBus bus({});bus.start(); + check(bus.queueRisk("clear"),"Initial clear rejected"); + waitFor([&]{return bus.snapshot().transmitted==1;}); + for(int i=0;i<1000;++i)check(bus.queueRisk("clear"),"Unchanged risk rejected"); + std::this_thread::sleep_for(30ms); + check(txCalls==1,"Repeated clear sent redundant frames"); + check(bus.queueRisk("watch"),"Watch risk rejected"); + std::this_thread::sleep_for(30ms); + check(txCalls==1,"Watch resent clear's zero value"); + check(bus.queueRisk("warning"),"Risk change rejected"); + waitFor([&]{return bus.snapshot().transmitted==2;}); + for(auto level:{"warning","danger","unknown",""}) + check(bus.queueRisk(level),"Equivalent binary risk rejected"); + std::this_thread::sleep_for(30ms); + check(txCalls==2,"Equivalent non-clear levels resent binary risk"); + check(bus.queueRisk("unavailable"),"Unavailable transition rejected"); + waitFor([&]{return bus.snapshot().transmitted==3;}); + check(sentCanFrames.back().dat[0]==0,"Unavailable risk must send zero"); + check(bus.queueRisk("clear")&&bus.queueRisk("watch"),"Zero-risk transition rejected"); + std::this_thread::sleep_for(30ms); + check(bus.snapshot().transmitted==3,"Clear/watch resent unavailable's zero value"); + failedTxChannel=1; + check(bus.queueRisk("danger"),"Failed-send fixture rejected"); + waitFor([&]{return bus.snapshot().transmit_errors==1;}); + for(int i=0;i<1000;++i)check(bus.queueRisk("warning"),"Unchanged failed state rejected"); + std::this_thread::sleep_for(30ms); + check(txCalls==4,"Unchanged failed state retried automatically"); + failedTxChannel=-1; + check(bus.queueRisk("clear"),"Recovery transition rejected"); + waitFor([&]{return bus.snapshot().transmitted==4;}); + check(bus.queueRisk("danger"),"Second danger transition rejected"); + waitFor([&]{return bus.snapshot().transmitted==5;}); + bus.join(); + check(sentCanFrames.size()==6,"Unexpected change-triggered frame count"); + for(std::size_t i=0;i lock(txMutex);blockTx=false;}txWake.notify_all(); + waitFor([&]{return bus.snapshot().transmitted==1;}); + std::this_thread::sleep_for(30ms); + check(txCalls==1,"Returning to in-flight risk sent redundant frame"); + check(bus.queueRisk("clear"),"Later clear change lost"); + waitFor([&]{return bus.snapshot().transmitted==2;}); + bus.join(); + check(sentCanFrames.size()==2&&sentCanFrames[0].dat[0]==0x11&&sentCanFrames[1].dat[0]==0, + "Latest-state coalescing lost final clear"); + } + std::cout<<"PASS CAN/CANFD CH0 reception, CH1 transmission, expiry/recovery, TX validation, change-triggered binary risk 0x1F6, errors and lifecycle cleanup\n"; }catch(std::exception const& e){std::cerr<