2 Commits

25 changed files with 939 additions and 127 deletions
+3
View File
@@ -2,3 +2,6 @@
/engines/
/logs/
*.tmp
# Optional per-frame output, including standalone Orin runs
output/
+7
View File
@@ -0,0 +1,7 @@
# 工程交付约定
- 共享采集、停止与恢复逻辑的修改,应同步本地 `inference-only`、`use-can`、`lazy-inference` 三个分支,并完成 Git 远端与 Orin 部署同步。
- 本机模拟测试仅作为回归检查,不得用它代替 Orin 上的真实摄像头和 TensorRT 验证。报告须区分本机模拟、板端故障注入、真实硬件操作,明确未覆盖项目。
- 短暂取流故障的重点是期限内恢复后在同一进程继续输出新的推理结果,并重新计数;不能只验证超时后退出。
- 发布前核对本地提交、Git 远端、Orin 检出版本及实际运行二进制的哈希。更新 Orin 可执行文件后同时刷新独立包 SHA256SUMS。
- 更新测试期间保留原启动参数,测试结束后恢复运行部署版本。不要把旧进程仍在运行当作新版本已部署。
+1 -1
View File
@@ -13,7 +13,7 @@ find_path(TENSORRT_INCLUDE_DIR NvInfer.h PATH_SUFFIXES aarch64-linux-gnu x86_64-
find_library(TENSORRT_LIBRARY nvinfer REQUIRED)
add_library(camera_risk_core
src/trt_engine.cpp src/pose_engine.cu src/gpu_preprocess.cu src/uyvy_converter.cu
src/detector.cpp src/door_risk.cpp src/camera.cpp src/camera_pipeline.cpp src/config.cpp)
src/detector.cpp src/door_risk.cpp src/camera.cpp src/camera_pipeline.cpp src/serialization.cpp src/output_writer.cpp src/result_renderer.cpp src/config.cpp)
target_include_directories(camera_risk_core PUBLIC include third_party ${TENSORRT_INCLUDE_DIR} ${OpenCV_INCLUDE_DIRS})
target_link_libraries(camera_risk_core PUBLIC ${TENSORRT_LIBRARY} ${OpenCV_LIBS} Threads::Threads CUDA::cudart CUDA::nppc CUDA::nppig CUDA::nppidei CUDA::nppicc)
target_compile_options(camera_risk_core PRIVATE $<$<COMPILE_LANGUAGE:CXX>:-Wall;-Wextra;-Wpedantic>)
+25
View File
@@ -3,6 +3,8 @@
C++ / TensorRT 实时摄像头风险推理,算法来自最新 run_risk.py。
持续取图、推理并更新内存中的 FrameResult;可视化可选。
相机每次取帧默认等待 **2000 ms**,连续超时 **6 次**才停止流水线(纯超时约 **12 秒**);成功取得有效帧后清零超时计数。单次等待时间由 `config/camera.json` 的 `camera.timeout_ms` 配置。运行中的坏帧、取流错误共用该重试期限;设备失效时尝试重新打开相机,恢复有效帧后继续推理。GPU 错误仍按原有异常处理退出。
## Orin 直接运行
适用环境:Jetson AGX Orin、JetPack 5.1.2 / L4T R35.4.1、CUDA 11.4、TensorRT 8.5.2、OpenCV 4.5.4。
@@ -71,3 +73,26 @@ NPP 用于 UYVY 转换、YOLO 缩放填充、RTMW 仿射裁剪;CUDA 用于通
当前摄像头驱动启动时偶发首帧 ERROR,应用会丢弃并计数;持续错误、采集超时或推理错误会明确报错并退出。
现有 GMSL 驱动停止采集时可产生 videobuf2 缓冲区告警,v4l2-ctl 同样复现;需在驱动侧完成量产修复和验证。
## 可选保存(默认全部关闭)
三个独立参数可以单独使用,也可以组合:
```bash
./run_camera.sh --save-original --save-annotated --save-results
# 独立包:./orin/run.sh --save-original --save-annotated --save-results
```
开启任一项时,程序自动创建可执行文件所属工程的 `output/` 及三个子目录:
- `original/`:`--save-original` 保存未叠加的原图,JPEG 质量 95。
- `annotated/`:`--save-annotated` 保存带框、关键点及风险文字的 JPEG,无需开启可视化窗口。
- `results/`:`--save-results` 每个完成推理的帧保存一个 JSON,包含帧号、检测框、跟踪 ID、关键点及置信度、风险和耗时;有 CAN 的分支还包含相应状态。
同一帧三类文件同名(扩展名不同),文件名包含运行标识和帧号,多次运行不覆盖。JSON 的 `images` 提供相对于 `output/` 的图片路径,未选择保存的图片为 null。源码入口写入工程 `output/`;独立包入口写入 `orin/output/`。默认不创建目录、不保存文件。
保存对象是完成推理的帧;惰性分支暂停时不新增文件。开启保存会增加编码和磁盘 I/O 开销。写入失败时报告错误并停止,不静默丢失文件。
相机超时回归测试:`./tests/run_camera_timeout.sh`;加 `--wall-clock` 验证默认 6 次超时约 12 秒。测试直接编译实际相机代码,使用模拟采集和转换器,无需摄像头、GPU 或 OpenCV;不覆盖视频回放路径。
板端真实摄像头 / TensorRT 恢复与停止测试记录见 [Orin 测试报告](tests/ORIN_TEST_REPORT.md)。
+2
View File
@@ -1,12 +1,14 @@
#pragma once
#include "camera.hpp"
#include "frame_result.hpp"
#include "output_writer.hpp"
#include <filesystem>
#include <memory>
namespace cms {
struct PipelineConfig {
CameraConfig camera;
OutputOptions output;
std::filesystem::path engines,door_mask;
double detector_confidence{0.30};
int max_people{3};
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include "frame_result.hpp"
#include <filesystem>
#include <string>
namespace cms {
struct OutputOptions {
bool original{},annotated{},results{};
std::filesystem::path directory;
bool enabled() const {return original||annotated||results;}
};
// Called by the inference worker; one matching filename stem per completed frame.
class OutputWriter {
public:
explicit OutputWriter(OutputOptions options);
void write(FrameResult const& result);
private:
OutputOptions options_;
std::string runId_;
};
}
+4
View File
@@ -0,0 +1,4 @@
#pragma once
#include "frame_result.hpp"
#include "door_risk.hpp"
namespace cms { Json serializeResult(FrameResult const& result); }
+21
View File
@@ -1,5 +1,8 @@
# Orin 可直接运行目录
相机每次等待默认 2 秒,连续 6 次超时才退出(约 12 秒);有效帧恢复后重新计数。Ctrl+C / SIGTERM 主动停止不需要等待 6 次超时。
本目录包含实际在 Jetson AGX Orin 上编译的 aarch64 程序和 TensorRT FP16 引擎。
目标环境:L4T R35.4.1 / JetPack 5.1.2,CUDA 11.4,TensorRT 8.5.2,OpenCV 4.5.4。
使用系统安装的 CUDA、TensorRT、OpenCV 共享库;不适用于任意 JetPack 版本或 x86 主机。
@@ -20,3 +23,21 @@ sha256sum -c SHA256SUMS
bin/drivers 中 GMSL 模块对应内核 5.10.120-tegra。初始化脚本检查内核版本;加载模块需要 root 或现有 sudo 权限。
门区保留当前室内联调设置,door.calibration_verified=false。
## 可选保存(默认全部关闭)
三个独立参数可以单独使用,也可以组合:
```bash
./run.sh --save-original --save-annotated --save-results
```
开启任一项时,程序自动创建可执行文件所属工程的 `output/` 及三个子目录:
- `original/`:`--save-original` 保存未叠加的原图,JPEG 质量 95。
- `annotated/`:`--save-annotated` 保存带框、关键点及风险文字的 JPEG,无需开启可视化窗口。
- `results/`:`--save-results` 每个完成推理的帧保存一个 JSON,包含帧号、检测框、跟踪 ID、关键点及置信度、风险和耗时;有 CAN 的分支还包含相应状态。
同一帧三类文件同名(扩展名不同),文件名包含运行标识和帧号,多次运行不覆盖。JSON 的 `images` 提供相对于 `output/` 的图片路径,未选择保存的图片为 null。源码入口写入工程 `output/`;独立包入口写入 `orin/output/`。默认不创建目录、不保存文件。
保存对象是完成推理的帧;惰性分支暂停时不新增文件。开启保存会增加编码和磁盘 I/O 开销。写入失败时报告错误并停止,不静默丢失文件。
+11 -11
View File
@@ -1,11 +1,11 @@
926a5165a4ebffe9568cf694018afb097cfb69b6e696a19939e5d364a9e3dfa5 README.md
6d5e874fd27fcfd75d303d7e35d9672028af71041f52dcc82a1e83676faefa65 bin/drivers/max9295.ko
8a78d04ef8be44e9b62a5e4ee315d3d6d679e34aedff02a234bbf4d40e222933 bin/drivers/max9296.ko
3da628587477cc35d5e3f8aee86e718e2ddf16808e893e00ba6e164b2307e843 bin/drivers/sgx-yuv-gmsl2.ko
b5102fe0a529349bb8e9700ae1d8e8ddeaa7c4279cb28e7826c5cc32dce2bb1d build/camera_risk
f2334c43e6a48480260c770d46a3ab0edb855f6f52bc8b91d1559e69edfad043 config/camera.json
9ba768d559fd3b64e833b50c62c70f2adfd6c169620aea5fba32cfb1b4c5b0e9 config/door_mask.json
ebcd09e8c2e35008adbb0c1d222e580b17e0df1c8093df3481ba3374cd9a8174 engines/rtmw_l_384.engine
f6a65b926ac1144484f4aa93cd26d7997d32d62310ab93fe867f2bd4412222e9 engines/yolo11s.engine
7f42b0e7f9396e0d82313a892fc62c153261dab1206a206a466bce6376f58a36 run.sh
54e1d63e3aa72ad9b056a224d55968d4947ca6703a8d1c405570de5707a7e7ef scripts/initialize_camera.sh
49a272525082edf5ef4619efc57bc2509c06653af262fc8a8d257d138f9206fb ./README.md
6d5e874fd27fcfd75d303d7e35d9672028af71041f52dcc82a1e83676faefa65 ./bin/drivers/max9295.ko
8a78d04ef8be44e9b62a5e4ee315d3d6d679e34aedff02a234bbf4d40e222933 ./bin/drivers/max9296.ko
3da628587477cc35d5e3f8aee86e718e2ddf16808e893e00ba6e164b2307e843 ./bin/drivers/sgx-yuv-gmsl2.ko
3a984e1e4c0fa12285967085e3359a5b7c698d744e9c2deb2480a312eb637c1c ./build/camera_risk
8a572f661e056e3916758a442e73212842bfe89361dd1b07b87de8f6ae523cf8 ./config/camera.json
9ba768d559fd3b64e833b50c62c70f2adfd6c169620aea5fba32cfb1b4c5b0e9 ./config/door_mask.json
ebcd09e8c2e35008adbb0c1d222e580b17e0df1c8093df3481ba3374cd9a8174 ./engines/rtmw_l_384.engine
f6a65b926ac1144484f4aa93cd26d7997d32d62310ab93fe867f2bd4412222e9 ./engines/yolo11s.engine
7f42b0e7f9396e0d82313a892fc62c153261dab1206a206a466bce6376f58a36 ./run.sh
54e1d63e3aa72ad9b056a224d55968d4947ca6703a8d1c405570de5707a7e7ef ./scripts/initialize_camera.sh
Binary file not shown.
+2 -2
View File
@@ -9,13 +9,13 @@
},
"inference": {
"engine_directory": "../engines",
"detector_confidence": 0.30,
"detector_confidence": 0.3,
"max_people": 3,
"queue_capacity": 2
},
"door": {
"mask": "door_mask.json",
"warning_relative": 0.30,
"warning_relative": 0.3,
"release_relative": 0.45,
"calibration_verified": false
}
+12
View File
@@ -0,0 +1,12 @@
#!/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; }
cp -p "$root/build/camera_risk" "$root/orin/build/"
cp -p "$root/config/door_mask.json" "$root/orin/config/"
sed 's|../orin/engines|../engines|' "$root/config/camera.json" > "$root/orin/config/camera.json"
(
cd "$root/orin"
find . -type f ! -path './output/*' ! -path './outputs/*' ! -path './logs/*' ! -name SHA256SUMS -print0 |
sort -z | xargs -0 sha256sum > SHA256SUMS
)
+78 -34
View File
@@ -9,6 +9,7 @@
#include <unistd.h>
#include <time.h>
#include <cerrno>
#include <algorithm>
#include <cstring>
#include <limits>
#include <stdexcept>
@@ -42,6 +43,11 @@ struct Camera::Impl {
try {
stopFd=eventfd(0,EFD_CLOEXEC|EFD_NONBLOCK);
if(stopFd<0)throw std::runtime_error("Cannot create camera stop event");
openCapture();
} catch(...) {release();throw;}
}
void openCapture() {
try {
fd=open(config.device.c_str(),O_RDWR|O_NONBLOCK|O_CLOEXEC);
if(fd<0)throw std::runtime_error("Cannot open "+config.device+": "+std::strerror(errno));
v4l2_capability capability{};checkedIoctl(fd,VIDIOC_QUERYCAP,&capability,"VIDIOC_QUERYCAP");
@@ -77,16 +83,19 @@ struct Camera::Impl {
if(mapping==MAP_FAILED)throw std::runtime_error("Camera mmap failed");
buffers[i].data=mapping;checkedIoctl(fd,VIDIOC_QBUF,&buffer,"VIDIOC_QBUF");
}
converter=std::make_unique<UyvyConverter>(config.width,config.height);
if(!converter)converter=std::make_unique<UyvyConverter>(config.width,config.height);
auto type=V4L2_BUF_TYPE_VIDEO_CAPTURE;checkedIoctl(fd,VIDIOC_STREAMON,&type,"VIDIOC_STREAMON");streaming=true;
} catch(...) {release();throw;}
} catch(...) {releaseCapture();throw;}
}
~Impl(){release();}
void release() noexcept {
void releaseCapture() noexcept {
if(streaming){auto type=V4L2_BUF_TYPE_VIDEO_CAPTURE;ioctl(fd,VIDIOC_STREAMOFF,&type);streaming=false;}
for(auto const& b:buffers)if(b.data)munmap(b.data,b.size);
buffers.clear();
if(fd>=0){close(fd);fd=-1;}
}
void release() noexcept {
releaseCapture();
if(stopFd>=0){close(stopFd);stopFd=-1;}
}
void stop() noexcept {
@@ -94,47 +103,82 @@ struct Camera::Impl {
if(stopFd>=0){auto ignored=write(stopFd,&one,sizeof(one));(void)ignored;}
}
std::optional<CapturedFrame> readFrame() {
auto windowStart=monotonicNs();unsigned failedWindows=0;
auto windowNs=std::uint64_t(config.timeout_ms)*1000000;
std::string lastError="no camera frame";
auto expired=[&](bool timedOut=false) {
auto now=monotonicNs();
if(timedOut||now-windowStart>=windowNs) {
if(++failedWindows>=6)
throw std::runtime_error("Camera capture failed 6 consecutive times: "+config.device+": "+lastError);
windowStart=now;
}
};
auto pause=[&] {
pollfd stopEvent{stopFd,POLLIN,0};
// Back off transient driver errors while retaining immediate cancellation.
poll(&stopEvent,1,20);
};
auto reconnect=[&] {
releaseCapture();
if(haveSequence){++index;haveSequence=false;}
};
while(!stopped.load()) {
expired();
if(fd<0) {
try {openCapture();}
catch(std::exception const& e){lastError=e.what();pause();continue;}
}
if(stopped.load())return {};
auto elapsed=monotonicNs()-windowStart;
auto remaining=elapsed<windowNs?windowNs-elapsed:1;
int waitMs=static_cast<int>((remaining+999999)/1000000);
pollfd descriptors[2]={{fd,POLLIN,0},{stopFd,POLLIN,0}};
int ready=poll(descriptors,2,config.timeout_ms);
int ready=poll(descriptors,2,waitMs);
if(ready<0&&errno==EINTR)continue;
if(stopped.load()||(descriptors[1].revents&POLLIN))return {};
if(ready==0)throw std::runtime_error("Camera capture timeout: "+config.device);
if(ready<0||(descriptors[0].revents&(POLLERR|POLLHUP|POLLNVAL)))throw std::runtime_error("Camera disconnected or poll failed");
if(ready==0){expired(true);continue;}
if(ready<0||(descriptors[0].revents&(POLLERR|POLLHUP|POLLNVAL))) {
lastError="camera disconnected or poll failed";reconnect();pause();continue;
}
v4l2_buffer buffer{};buffer.type=V4L2_BUF_TYPE_VIDEO_CAPTURE;buffer.memory=V4L2_MEMORY_MMAP;
int status=ioctl(fd,VIDIOC_DQBUF,&buffer);
if(status<0&&(errno==EAGAIN||errno==EINTR))continue;
if(status<0)throw std::runtime_error("VIDIOC_DQBUF: "+std::string(std::strerror(errno)));
if(buffer.index>=buffers.size())throw std::runtime_error("Invalid V4L2 buffer index");
// The GMSL driver can mark its startup buffer as corrupt. Requeue
// it without conversion or inference; persistent corruption is fatal.
if(buffer.flags&V4L2_BUF_FLAG_ERROR) {
checkedIoctl(fd,VIDIOC_QBUF,&buffer,"VIDIOC_QBUF(error frame)");
if(++consecutiveErrors>=static_cast<unsigned>(config.fps))
throw std::runtime_error("Camera produced one second of consecutive error frames");
continue;
if(status<0) {
lastError="VIDIOC_DQBUF: "+std::string(std::strerror(errno));
// EIO can be a transient corrupt frame; a lost device needs reopening.
if(errno!=EIO)reconnect();
pause();continue;
}
if(buffer.index>=buffers.size()){lastError="invalid V4L2 buffer index";reconnect();pause();continue;}
bool invalid=(buffer.flags&V4L2_BUF_FLAG_ERROR)||buffer.bytesused<std::size_t(stride)*config.height||buffer.bytesused>buffers[buffer.index].size;
if(invalid) {
++consecutiveErrors;lastError="corrupt or incomplete camera frame";
if(ioctl(fd,VIDIOC_QBUF,&buffer)<0)reconnect();
pause();continue;
}
CapturedFrame frame;
try {
if(buffer.bytesused<std::size_t(stride)*config.height||buffer.bytesused>buffers[buffer.index].size)
throw std::runtime_error("Invalid camera frame: flags="+std::to_string(buffer.flags)+", used="+std::to_string(buffer.bytesused)+", required="+std::to_string(std::size_t(stride)*config.height)+", allocated="+std::to_string(buffers[buffer.index].size));
auto& m=frame.metadata;
m.discarded_error_frames=consecutiveErrors;consecutiveErrors=0;
m.driver_sequence=buffer.sequence;m.received_monotonic_ns=monotonicNs();
m.driver_timestamp_ns=std::uint64_t(buffer.timestamp.tv_sec)*1000000000ULL+std::uint64_t(buffer.timestamp.tv_usec)*1000;
m.driver_timestamp_monotonic=(buffer.flags&V4L2_BUF_FLAG_TIMESTAMP_MASK)==V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC;
m.capture_monotonic_ns=m.driver_timestamp_monotonic&&m.driver_timestamp_ns>0&&m.driver_timestamp_ns<=m.received_monotonic_ns?m.driver_timestamp_ns:m.received_monotonic_ns;
if(haveSequence) {
auto delta=static_cast<std::uint32_t>(buffer.sequence-lastSequence);
if(delta==0||delta>0x7fffffffU)throw std::runtime_error("Camera sequence reset or repeated frame");
m.skipped_driver_frames=delta-1;index+=delta;
auto& m=frame.metadata;
m.discarded_error_frames=consecutiveErrors;
m.driver_sequence=buffer.sequence;m.received_monotonic_ns=monotonicNs();
m.driver_timestamp_ns=std::uint64_t(buffer.timestamp.tv_sec)*1000000000ULL+std::uint64_t(buffer.timestamp.tv_usec)*1000;
m.driver_timestamp_monotonic=(buffer.flags&V4L2_BUF_FLAG_TIMESTAMP_MASK)==V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC;
m.capture_monotonic_ns=m.received_monotonic_ns;
if(haveSequence) {
auto delta=static_cast<std::uint32_t>(buffer.sequence-lastSequence);
if(delta==0||delta>0x7fffffffU) {
lastError="camera sequence reset or repeated frame";
reconnect();pause();continue;
}
m.frame_index=index;
lastSequence=buffer.sequence;haveSequence=true;
// Convert before requeuing. No published result references MMAP memory.
frame.image=converter->convert(buffers[buffer.index].data,stride);
} catch(...) {ioctl(fd,VIDIOC_QBUF,&buffer);throw;}
checkedIoctl(fd,VIDIOC_QBUF,&buffer,"VIDIOC_QBUF");return frame;
m.skipped_driver_frames=delta-1;index+=delta;
}
m.frame_index=index;
// GPU failures remain fatal; only capture failures consume the retry budget.
try {frame.image=converter->convert(buffers[buffer.index].data,stride);}
catch(...) {ioctl(fd,VIDIOC_QBUF,&buffer);throw;}
if(ioctl(fd,VIDIOC_QBUF,&buffer)<0){lastError="VIDIOC_QBUF failed";reconnect();pause();continue;}
lastSequence=buffer.sequence;haveSequence=true;consecutiveErrors=0;
return frame; // The next read starts a fresh six-window retry budget.
}
return {};
}
+3
View File
@@ -50,6 +50,7 @@ struct CameraPipeline::Impl {
BoundedQueue<CapturedFrame> captures;
BoundedQueue<Detected> detections;
LatestResult results;
std::unique_ptr<OutputWriter> outputWriter;
fangjia::TrtLogger logger;
std::unique_ptr<Detector> detector;
std::unique_ptr<fangjia::RtmwEngine> pose;
@@ -93,6 +94,7 @@ struct CameraPipeline::Impl {
void start() {
{std::lock_guard<std::mutex> lock(stateMutex);if(started||stopped)throw std::logic_error("CameraPipeline can only be started once");started=true;}
try {
outputWriter=std::make_unique<OutputWriter>(config.output);
detector=std::make_unique<Detector>(config.engines/"yolo11s.engine",logger,config.detector_confidence,config.max_people);
pose=std::make_unique<fangjia::RtmwEngine>(config.engines/"rtmw_l_384.engine",logger,config.camera.width,config.camera.height,config.max_people);
DoorOptions options;options.constant=config.door_mask;options.warning=config.warning_relative;options.release=config.release_relative;
@@ -160,6 +162,7 @@ struct CameraPipeline::Impl {
FrameResultPtr immutable=result;
results.publish(std::move(immutable));
{std::lock_guard<std::mutex> lock(stateMutex);totalLatency+=result->timing.capture_to_result_ms;++published;}
outputWriter->write(*result);
}
active=false;
}
+7 -2
View File
@@ -9,15 +9,20 @@
int main(int argc,char** argv)try {
auto root=std::filesystem::canonical("/proc/self/exe").parent_path().parent_path();
auto configPath=root/"config/camera.json";
cms::OutputOptions output;output.directory=root/"output";
bool visualize=false;
std::map<std::string,std::function<void(int&)>> arguments{
{"--save-original",[&](int&){output.original=true;}},
{"--save-annotated",[&](int&){output.annotated=true;}},
{"--save-results",[&](int&){output.results=true;}},
{"--visualize",[&](int&){visualize=true;}},
{"--config",[&](int&i){if(++i>=argc)throw std::invalid_argument("--config needs a file");configPath=argv[i];}},
{"--help",[](int&){std::cout<<"camera_risk [--config FILE] [--visualize]\nContinuous camera inference; latest FrameResult stays in memory. Stop with Ctrl+C or SIGTERM.\n";std::exit(0);}}
{"--help",[](int&){std::cout<<"camera_risk [--save-original] [--save-annotated] [--save-results] [--config FILE] [--visualize]\nContinuous camera inference; latest FrameResult stays in memory. Stop with Ctrl+C or SIGTERM.\n";std::exit(0);}}
};
for(int i=1;i<argc;++i){auto handler=arguments.find(argv[i]);if(handler==arguments.end())throw std::invalid_argument("Unknown option: "+std::string(argv[i]));handler->second(i);}
cms::CameraPipeline pipeline(cms::loadConfig(configPath));
auto config=cms::loadConfig(configPath);config.output=output;
cms::CameraPipeline pipeline(config);
cms::StopSignals signals(pipeline);
pipeline.start();
/*
+69
View File
@@ -0,0 +1,69 @@
#include "output_writer.hpp"
#include "serialization.hpp"
#include "visualizer.hpp"
#include <opencv2/imgcodecs.hpp>
#include <chrono>
#include <ctime>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <unistd.h>
namespace cms {
namespace {
void writeFile(std::filesystem::path const& path,char const* data,std::size_t size) {
auto temporary=path;temporary+=".tmp";
try {
std::ofstream file;file.exceptions(std::ios::failbit|std::ios::badbit);
file.open(temporary,std::ios::binary|std::ios::trunc);
file.write(data,static_cast<std::streamsize>(size));file.close();
std::filesystem::rename(temporary,path);
} catch(...) {
std::error_code ignored;std::filesystem::remove(temporary,ignored);
throw std::runtime_error("Cannot save output file: "+path.string());
}
}
void writeJpeg(std::filesystem::path const& path,cv::Mat const& image) {
std::vector<unsigned char> encoded;
if(!cv::imencode(".jpg",image,encoded,{cv::IMWRITE_JPEG_QUALITY,95}))
throw std::runtime_error("JPEG encoding failed: "+path.string());
writeFile(path,reinterpret_cast<char const*>(encoded.data()),encoded.size());
}
}
OutputWriter::OutputWriter(OutputOptions options):options_(std::move(options)) {
if(!options_.enabled())return;
if(options_.directory.empty())options_.directory=std::filesystem::current_path()/"output";
for(auto name:{"original","annotated","results"})std::filesystem::create_directories(options_.directory/name);
// Nanosecond wall-clock + PID prevents a later run from overwriting earlier frames.
auto now=std::chrono::system_clock::now();auto time=std::chrono::system_clock::to_time_t(now);
std::tm utc{};gmtime_r(&time,&utc);
auto ns=std::chrono::duration_cast<std::chrono::nanoseconds>(now.time_since_epoch()).count()%1000000000;
std::ostringstream id;id<<std::put_time(&utc,"%Y%m%dT%H%M%S")<<'_'<<std::setw(9)<<std::setfill('0')<<ns<<"Z_"<<getpid();
runId_=id.str();
std::cout<<"Saving selected outputs to "<<options_.directory<<std::endl;
}
void OutputWriter::write(FrameResult const& result) {
if(!options_.enabled())return;
std::ostringstream name;name<<runId_<<"_frame_"<<std::setw(10)<<std::setfill('0')<<result.frame_index;
auto stem=name.str();Json original=nullptr,annotated=nullptr;
if(options_.original) {
if(!result.image)throw std::runtime_error("Cannot save original image: frame is missing");
auto const& image=*result.image;
if(image.width<=0||image.height<=0||image.stride<image.width*3||image.pixels.size()<std::size_t(image.stride)*image.height)
throw std::runtime_error("Cannot save malformed original image");
cv::Mat view(image.height,image.width,CV_8UC3,const_cast<std::uint8_t*>(image.pixels.data()),image.stride);
auto path=std::filesystem::path("original")/(stem+".jpg");writeJpeg(options_.directory/path,view);original=path.generic_string();
}
if(options_.annotated) {
auto path=std::filesystem::path("annotated")/(stem+".jpg");
writeJpeg(options_.directory/path,renderResult(result));annotated=path.generic_string();
}
if(options_.results) {
auto document=serializeResult(result);document["schema_version"]=1;document["run_id"]=runId_;
document["images"]={{"original",original},{"annotated",annotated}};
auto contents=document.dump(2)+"\n";
writeFile(options_.directory/"results"/(stem+".json"),contents.data(),contents.size());
}
}
}
+80
View File
@@ -0,0 +1,80 @@
#include "visualizer.hpp"
#include <opencv2/imgproc.hpp>
#include <algorithm>
#include <cmath>
#include <iomanip>
#include <sstream>
#include <stdexcept>
namespace cms {
namespace {
cv::Point point(PosePoint const& p){return {int(std::nearbyint(p.x)),int(std::nearbyint(p.y))};}
cv::Scalar color(std::string const& level) {
if(level=="danger")return {40,40,255};
if(level=="warning")return {0,160,255};
if(level=="watch")return {0,240,240};
if(level=="clear")return {80,220,90};
return {190,190,190};
}
void segment(cv::Mat& image,cv::Point a,cv::Point b,cv::Scalar c,int thickness,bool dashed) {
if(!dashed){cv::line(image,a,b,c,thickness,cv::LINE_AA);return;}
cv::Point2d d=b-a;double length=cv::norm(d);if(length<1)return;d*=1.0/length;
for(double x=0;x<length;x+=14)cv::line(image,cv::Point2d(a)+d*x,cv::Point2d(a)+d*std::min(x+8,length),c,thickness,cv::LINE_AA);
}
}
cv::Mat renderResult(FrameResult const& r) {
if(!r.image)throw std::invalid_argument("Result has no frame image");
auto const& image=*r.image;
if(image.width<=0||image.height<=0||image.stride<image.width*3||image.pixels.size()<std::size_t(image.stride)*image.height)
throw std::invalid_argument("Malformed result image");
cv::Mat view(image.height,image.width,CV_8UC3,const_cast<std::uint8_t*>(image.pixels.data()),image.stride);
auto canvas=view.clone();
if(r.door_mask_pixels) {
auto const& m=*r.door_mask_pixels;
if(m.width!=image.width||m.height!=image.height||m.stride<m.width||m.pixels.size()<std::size_t(m.stride)*m.height)
throw std::invalid_argument("Result mask does not match image");
cv::Mat mask(m.height,m.width,CV_8U,const_cast<std::uint8_t*>(m.pixels.data()),m.stride);
auto tint=canvas.clone();tint.setTo(cv::Scalar(255,180,0),mask);
cv::addWeighted(canvas,0.75,tint,0.25,0,canvas);
std::vector<std::vector<cv::Point>> contours;cv::findContours(mask.clone(),contours,cv::RETR_EXTERNAL,cv::CHAIN_APPROX_SIMPLE);
cv::drawContours(canvas,contours,-1,{255,180,0},2);
}
for(auto const& person:r.persons) {
auto const& b=person.box;
cv::rectangle(canvas,{int(b[0]),int(b[1])},{int(b[2]),int(b[3])},{0,220,255},2);
std::ostringstream text;text<<"ID "<<person.track_id<<" "<<std::fixed<<std::setprecision(2)<<person.confidence;
cv::putText(canvas,text.str(),{int(b[0])+4,std::max(180,int(b[1])+25)},cv::FONT_HERSHEY_SIMPLEX,.8,{0,220,255},2,cv::LINE_AA);
for(int s=0;s<2;++s) {
auto const& state=person.hands[s];auto const& k=person.selected;
cv::Scalar arm=s==0?cv::Scalar(40,40,255):cv::Scalar(30,230,30);
for(auto e:std::vector<std::pair<int,int>>{{5+s,7+s},{7+s,9+s}}) {
segment(canvas,point(k[e.first]),point(k[e.second]),state.limb_consensus?arm:cv::Scalar(0,140,255),3,!state.limb_consensus);
cv::circle(canvas,point(k[e.second]),4,arm,-1,cv::LINE_AA);
}
if(state.state=="rejected")continue;
int start=s==0?91:112;
auto hand=state.distance_eligible?(s==0?cv::Scalar(255,80,220):cv::Scalar(255,190,40)):cv::Scalar(0,140,255);
for(int f=0;f<5;++f)for(int j=0;j<4;++j){int a=j==0?start:start+f*4+j;int b=start+1+f*4+j;segment(canvas,point(k[a]),point(k[b]),hand,2,!state.distance_eligible);}
for(int j=0;j<21;++j)cv::circle(canvas,point(k[start+j]),3,hand,-1,cv::LINE_AA);
}
}
for(auto const& h:r.hand_regions) {
std::vector<cv::Point> points,hull;for(auto const& p:h.keypoints)points.push_back(point(p));cv::convexHull(points,hull);
auto risk=std::find_if(r.risk.regions.begin(),r.risk.regions.end(),[&](auto const& item){return item.track_id==h.track_id&&item.side==h.side;});
auto c=color(risk==r.risk.regions.end()?"unavailable":risk->risk_level);
cv::polylines(canvas,std::vector<std::vector<cv::Point>>{hull},true,c,2,cv::LINE_AA);
}
for(auto const& region:r.risk.regions)if(region.source=="YOLO_BODY_BOX_FALLBACK") {
auto it=std::find(r.track_ids.begin(),r.track_ids.end(),region.track_id);
if(it!=r.track_ids.end()){auto const& b=r.boxes.at(it-r.track_ids.begin());cv::putText(canvas,"BODY FALLBACK",{int(b[0])+4,int(b[1])+50},cv::FONT_HERSHEY_SIMPLEX,.65,{0,160,255},2,cv::LINE_AA);}
}
cv::rectangle(canvas,{0,0},{canvas.cols,148},{20,24,30},-1);
std::ostringstream headline;headline<<"RISK ";if(r.risk.risk_score)headline<<std::fixed<<std::setprecision(3)<<*r.risk.risk_score;else headline<<"N/A";headline<<" "<<r.risk.risk_level;
cv::putText(canvas,headline.str(),{24,40},cv::FONT_HERSHEY_SIMPLEX,1.0,color(r.risk.risk_level),2,cv::LINE_AA);
std::ostringstream status;status<<"camera frame "<<r.frame_index<<" | seq "<<r.capture.driver_sequence<<" | latency "<<std::fixed<<std::setprecision(1)<<r.timing.capture_to_result_ms<<" ms";
cv::putText(canvas,status.str(),{24,82},cv::FONT_HERSHEY_SIMPLEX,.72,{235,235,235},2,cv::LINE_AA);
std::string calibration=r.door_mask_calibration_verified?"door calibration verified":"door calibration NOT verified (bring-up)";
cv::putText(canvas,calibration+" | q/Esc: close preview",{24,123},cv::FONT_HERSHEY_SIMPLEX,.70,{0,190,255},2,cv::LINE_AA);
return canvas;
}
}
+45
View File
@@ -0,0 +1,45 @@
#include "serialization.hpp"
namespace cms {
namespace {
template<class T> Json optional(std::optional<T> const& x){return x?Json(*x):Json(nullptr);}
Json riskJson(RiskResult const& r) {
Json out={{"status",r.status},{"risk_score",optional(r.risk_score)},{"risk_level",r.risk_level},{"decision",r.decision},{"source",optional(r.source)},{"track_id",optional(r.track_id)},{"side",optional(r.side)},{"regions",Json::array()}};
if(!r.door_mask.status.empty()) {
auto const& d=r.door_mask;out["door_mask"]={{"status",d.status},{"annotation_index",d.annotation_index},{"path",optional(d.path)},{"format",d.format},{"area_px",d.area_px}};
out["warning_distance_relative"]=r.warning_distance_relative;out["release_distance_relative"]=r.release_distance_relative;
}
for(auto const& v:r.regions) {
Json j={{"status",v.status},{"risk_score",optional(v.risk_score)},{"risk_level",v.risk_level},{"decision",v.decision},{"source",v.source},{"track_id",v.track_id},{"side",v.side}};
if(v.state)j["state"]=*v.state;
if(v.region_area_px) {
j["region_area_px"]=optional(v.region_area_px);j["door_overlap_px"]=optional(v.door_overlap_px);j["region_overlap_ratio"]=optional(v.region_overlap_ratio);
j["min_distance_px"]=optional(v.min_distance_px);j["region_scale_px"]=optional(v.region_scale_px);j["distance_relative"]=optional(v.distance_relative);
}
out["regions"].push_back(std::move(j));
}
return out;
}
}
Json serializeResult(FrameResult const& r) {
Json out={{"frame_index",r.frame_index},{"track_ids",r.track_ids},{"boxes",r.boxes},{"skeletons",Json::array()},{"risk",riskJson(r.risk)}};
for(std::size_t i=0;i<r.persons.size();++i) {
auto const& p=r.persons[i];Json points=Json::array(),metadata=Json::object();for(auto const& k:p.selected)points.push_back({rounded(k.x,3),rounded(k.y,3)});
for(int s=0;s<2;++s){auto const& h=p.hands[s];metadata[s==0?"left":"right"]={{"limb_state",h.limb_consensus?"consensus":"inferred"},{"hand_source",h.source},{"hand_state",h.state},{"distance_eligible",h.distance_eligible},{"alarm_eligible",h.alarm_eligible}};}
out["skeletons"].push_back({{"track_id",p.track_id},{"person_index",i},{"layout","coco_wholebody_133"},{"keypoints",points},{"metadata",metadata}});
}
for(std::size_t i=0;i<r.persons.size();++i) {
auto const& p=r.persons[i];auto& skeleton=out["skeletons"][i];
skeleton["confidence"]=p.confidence;skeleton["keypoint_scores"]=Json::array();
skeleton["smoothed_keypoints"]=Json::array();skeleton["temporal_keypoints"]=Json::array();
for(std::size_t k=0;k<p.selected.size();++k) {
skeleton["keypoint_scores"].push_back(p.selected[k].score);
auto const& a=p.smoothed[k];auto const& b=p.temporal[k];
skeleton["smoothed_keypoints"].push_back({a.x,a.y,a.score});
skeleton["temporal_keypoints"].push_back({b.x,b.y,b.score});
}
}
out["timing"]={{"detector_ms",r.timing.detector_ms},{"pose_ms",r.timing.pose_ms},{"fusion_risk_ms",r.timing.fusion_risk_ms},{"capture_to_result_ms",r.timing.capture_to_result_ms}};
out["capture"]={{"sequence",r.capture.driver_sequence},{"capture_monotonic_ms",r.capture.capture_monotonic_ns*1e-6},{"driver_timestamp_ms",r.capture.driver_timestamp_ns*1e-6}};
out["capture_to_result_ms"]=rounded(r.timing.capture_to_result_ms,3);out["calibration_verified"]=r.door_mask_calibration_verified;return out;
}
}
+1 -77
View File
@@ -3,84 +3,8 @@
#include <opencv2/highgui.hpp>
#include <opencv2/highgui/highgui_c.h>
#include <opencv2/imgproc.hpp>
#include <algorithm>
#include <cmath>
#include <iomanip>
#include <sstream>
#include <stdexcept>
namespace cms {
namespace {
constexpr char windowName[]="CMS Camera Risk";
cv::Point point(PosePoint const& p){return {int(std::nearbyint(p.x)),int(std::nearbyint(p.y))};}
cv::Scalar color(std::string const& level) {
if(level=="danger")return {40,40,255};
if(level=="warning")return {0,160,255};
if(level=="watch")return {0,240,240};
if(level=="clear")return {80,220,90};
return {190,190,190};
}
void segment(cv::Mat& image,cv::Point a,cv::Point b,cv::Scalar c,int thickness,bool dashed) {
if(!dashed){cv::line(image,a,b,c,thickness,cv::LINE_AA);return;}
cv::Point2d d=b-a;double length=cv::norm(d);if(length<1)return;d*=1.0/length;
for(double x=0;x<length;x+=14)cv::line(image,cv::Point2d(a)+d*x,cv::Point2d(a)+d*std::min(x+8,length),c,thickness,cv::LINE_AA);
}
}
cv::Mat renderResult(FrameResult const& r) {
if(!r.image)throw std::invalid_argument("Result has no frame image");
auto const& image=*r.image;
if(image.width<=0||image.height<=0||image.stride<image.width*3||image.pixels.size()<std::size_t(image.stride)*image.height)
throw std::invalid_argument("Malformed result image");
cv::Mat view(image.height,image.width,CV_8UC3,const_cast<std::uint8_t*>(image.pixels.data()),image.stride);
auto canvas=view.clone();
if(r.door_mask_pixels) {
auto const& m=*r.door_mask_pixels;
if(m.width!=image.width||m.height!=image.height||m.stride<m.width||m.pixels.size()<std::size_t(m.stride)*m.height)
throw std::invalid_argument("Result mask does not match image");
cv::Mat mask(m.height,m.width,CV_8U,const_cast<std::uint8_t*>(m.pixels.data()),m.stride);
auto tint=canvas.clone();tint.setTo(cv::Scalar(255,180,0),mask);
cv::addWeighted(canvas,0.75,tint,0.25,0,canvas);
std::vector<std::vector<cv::Point>> contours;cv::findContours(mask.clone(),contours,cv::RETR_EXTERNAL,cv::CHAIN_APPROX_SIMPLE);
cv::drawContours(canvas,contours,-1,{255,180,0},2);
}
for(auto const& person:r.persons) {
auto const& b=person.box;
cv::rectangle(canvas,{int(b[0]),int(b[1])},{int(b[2]),int(b[3])},{0,220,255},2);
std::ostringstream text;text<<"ID "<<person.track_id<<" "<<std::fixed<<std::setprecision(2)<<person.confidence;
cv::putText(canvas,text.str(),{int(b[0])+4,std::max(180,int(b[1])+25)},cv::FONT_HERSHEY_SIMPLEX,.8,{0,220,255},2,cv::LINE_AA);
for(int s=0;s<2;++s) {
auto const& state=person.hands[s];auto const& k=person.selected;
cv::Scalar arm=s==0?cv::Scalar(40,40,255):cv::Scalar(30,230,30);
for(auto e:std::vector<std::pair<int,int>>{{5+s,7+s},{7+s,9+s}}) {
segment(canvas,point(k[e.first]),point(k[e.second]),state.limb_consensus?arm:cv::Scalar(0,140,255),3,!state.limb_consensus);
cv::circle(canvas,point(k[e.second]),4,arm,-1,cv::LINE_AA);
}
if(state.state=="rejected")continue;
int start=s==0?91:112;
auto hand=state.distance_eligible?(s==0?cv::Scalar(255,80,220):cv::Scalar(255,190,40)):cv::Scalar(0,140,255);
for(int f=0;f<5;++f)for(int j=0;j<4;++j){int a=j==0?start:start+f*4+j;int b=start+1+f*4+j;segment(canvas,point(k[a]),point(k[b]),hand,2,!state.distance_eligible);}
for(int j=0;j<21;++j)cv::circle(canvas,point(k[start+j]),3,hand,-1,cv::LINE_AA);
}
}
for(auto const& h:r.hand_regions) {
std::vector<cv::Point> points,hull;for(auto const& p:h.keypoints)points.push_back(point(p));cv::convexHull(points,hull);
auto risk=std::find_if(r.risk.regions.begin(),r.risk.regions.end(),[&](auto const& item){return item.track_id==h.track_id&&item.side==h.side;});
auto c=color(risk==r.risk.regions.end()?"unavailable":risk->risk_level);
cv::polylines(canvas,std::vector<std::vector<cv::Point>>{hull},true,c,2,cv::LINE_AA);
}
for(auto const& region:r.risk.regions)if(region.source=="YOLO_BODY_BOX_FALLBACK") {
auto it=std::find(r.track_ids.begin(),r.track_ids.end(),region.track_id);
if(it!=r.track_ids.end()){auto const& b=r.boxes.at(it-r.track_ids.begin());cv::putText(canvas,"BODY FALLBACK",{int(b[0])+4,int(b[1])+50},cv::FONT_HERSHEY_SIMPLEX,.65,{0,160,255},2,cv::LINE_AA);}
}
cv::rectangle(canvas,{0,0},{canvas.cols,148},{20,24,30},-1);
std::ostringstream headline;headline<<"RISK ";if(r.risk.risk_score)headline<<std::fixed<<std::setprecision(3)<<*r.risk.risk_score;else headline<<"N/A";headline<<" "<<r.risk.risk_level;
cv::putText(canvas,headline.str(),{24,40},cv::FONT_HERSHEY_SIMPLEX,1.0,color(r.risk.risk_level),2,cv::LINE_AA);
std::ostringstream status;status<<"camera frame "<<r.frame_index<<" | seq "<<r.capture.driver_sequence<<" | latency "<<std::fixed<<std::setprecision(1)<<r.timing.capture_to_result_ms<<" ms";
cv::putText(canvas,status.str(),{24,82},cv::FONT_HERSHEY_SIMPLEX,.72,{235,235,235},2,cv::LINE_AA);
std::string calibration=r.door_mask_calibration_verified?"door calibration verified":"door calibration NOT verified (bring-up)";
cv::putText(canvas,calibration+" | q/Esc: close preview",{24,123},cv::FONT_HERSHEY_SIMPLEX,.70,{0,190,255},2,cv::LINE_AA);
return canvas;
}
namespace { constexpr char windowName[]="CMS Camera Risk"; }
void runLocalVisualization(CameraPipeline const& pipeline) {
struct Window {
Window(){cv::namedWindow(windowName,cv::WINDOW_NORMAL);cv::resizeWindow(windowName,1000,800);}
+31
View File
@@ -0,0 +1,31 @@
# Orin 相机故障恢复与停止测试(2026-09-23)
目标:短暂取流故障在六个等待窗口内恢复时,同一进程继续推理;有效帧到达后重新计数;主动停止不等待全部窗口。
环境:192.168.195.100,Jetson Orin,L4T R35.4.1,CUDA 11.4.298,TensorRT 8.5.2;真实 /dev/video5,YOLO 与 RTMW TensorRT 引擎。三个分支在该板编译。
使用真实采集与推理进程;通过 LD_PRELOAD 在 V4L2 系统调用处注入无帧、坏帧、EIO 和 ENODEV。每次恢复测试检查同一子进程产生新的风险 JSON;错误恢复后至少新增 10 帧。Ctrl+C 通过 PTY 写入 0x03,SIGTERM 发送给测试进程。
| 分支 | Ctrl+C / SIGTERM 完整退出范围 | 持续无帧退出(含清理) | 恢复后再次断流退出(含清理) | 结果 |
|---|---:|---:|---:|---|
| lazy-inference | 801.21–942.42 ms | 13.010 s | 13.085 s | 9/9 通过 |
| inference-only | 793.22–1012.57 ms | 13.055 s | 12.945 s | 9/9 通过 |
| use-can | 786.06–907.59 ms | 13.066 s | 13.155 s | 9/9 通过 |
每个分支覆盖:断流后恢复并重新计时;坏帧恢复;EIO 恢复;ENODEV 后重新打开设备并恢复;正常推理 Ctrl+C;重试期 Ctrl+C;第六窗口 Ctrl+C;SIGTERM;持续无帧六窗口后报错。timeout/recover 场景最后的退出码 1 是预期的超限结果,不是测试失败。
默认逻辑等待约 12 秒,表中的完整进程退出另含 GPU/驱动资源清理时间。最初使用的 1 秒完整退出判据过于严格:清理 GPU 资源可使总耗时略超过 1 秒;最终以不超过 2 秒验收主动停止,同时记录实测值。lazy-inference/use-can 的流水线停止日志也已记录,详见 JSON。
实机发现并修复:use-can 关闭时遗漏 munmap,导致重新打开后 VIDIOC_S_FMT 返回 EBUSY。修复后重跑该分支全部九项。另有原生 V4L2 重开回归测试,检查连续三次重开及部分 mmap 失败后无遗留映射。
补充回归:在板端执行 tests/run_camera_timeout.sh 和 --wall-clock(默认六窗口约 12.00 秒);use-can 额外执行 tests/run_v4l2_reopen.sh。它们使用模拟采集,仅为补充,不替代上述板端真实推理测试。
测试范围限制:未进行物理拔线、USB-CAN 报文/拔插、驱动内核永久卡死、GPU 故障或长时间耐久测试。板上未接 USB-CAN,CAN 模式处于缺失回退。视频回放路径未改动,未包含在本次相机恢复测试中。
完整数据:orin_hardware_results.json。故障注入器源码:orin_capture_faults.c,仅用于测试。原始运行日志位于板端 /home/orin/vision-anti-pinch-ctrlc-test-20260923/。
## 经测试的可执行文件 SHA256
- lazy-inference: `0d77d17626644fb50b663c33edbb1a09884b34b5410a1109fc56b4d3781651b1`
- inference-only: `3a984e1e4c0fa12285967085e3359a5b7c698d744e9c2deb2480a312eb637c1c`
- use-can: `a2616e193ef9f006b49b4bb4ac43ae3ffc4cee1f1edf6d58ed6f7f9e5f933ad1`
+192
View File
@@ -0,0 +1,192 @@
// Exercise the production Camera with simulated capture and no GPU or camera.
#include "camera.hpp"
#include "uyvy_converter.hpp"
#include <linux/videodev2.h>
#include <chrono>
#include <cstring>
#include <functional>
#include <iostream>
#include <stdexcept>
#include <string>
#include <thread>
#ifdef TEST_V4L2_CAPTURE
#include "v4l2_capture.h"
#else
#include <cstdarg>
#include <cerrno>
#include <poll.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#endif
namespace {
std::string events;
std::size_t cursor{};
char current{};
unsigned sequence{};
int timeoutMs{};
bool realWait{};
std::function<void()> onStop;
unsigned char pixels[8]{};
void check(bool condition,char const* message) {
if(!condition)throw std::runtime_error(message);
}
void script(std::string value) {events=std::move(value);cursor=0;}
char next() {
check(cursor<events.size(),"Camera requested an unexpected extra capture attempt");
return current=events[cursor++];
}
void expectTimeout(cms::Camera& camera) {
try {camera.read();} catch(std::runtime_error const& e) {
check(std::string(e.what()).find("6 consecutive times")!=std::string::npos,"Wrong timeout failure");
return;
}
throw std::runtime_error("Six consecutive timeouts did not fail");
}
}
namespace cms {
struct UyvyConverter::Impl {int width,height;};
UyvyConverter::UyvyConverter(int w,int h):impl_(std::make_unique<Impl>(Impl{w,h})){}
UyvyConverter::~UyvyConverter()=default;
std::shared_ptr<const BgrImage> UyvyConverter::convert(void const*,std::size_t) {
auto image=std::make_shared<BgrImage>();image->width=impl_->width;image->height=impl_->height;
image->stride=image->width*3;image->pixels.resize(image->stride*image->height);return image;
}
}
#ifdef TEST_V4L2_CAPTURE
V4l2Capture::~V4l2Capture()=default;
bool V4l2Capture::open(std::string const&){reconnect_needed_=false;return true;}
void V4l2Capture::close(){}
bool V4l2Capture::setFormat(uint32_t w,uint32_t h,uint32_t p){width_=w;height_=h;pixelformat_=p;return true;}
bool V4l2Capture::setFps(uint32_t){return true;}
bool V4l2Capture::initMmap(uint32_t){return true;}
bool V4l2Capture::startStreaming(){return true;}
void V4l2Capture::releaseFrame(Frame const&){}
bool V4l2Capture::getFrame(Frame& f,int waitMs) {
check(waitMs<=100,"Stop polling should remain at most 100 ms");
if(next()=='s'){onStop();return false;}
if(current=='d'){reconnect_needed_=true;return false;}
if(current=='e'||current=='x'){return false;}
if(current=='t') {
// Each scripted timeout represents a full logical capture window.
// The separate stop test exercises the short-wait cancellation path.
std::this_thread::sleep_for(std::chrono::milliseconds(timeoutMs));return false;
}
check(current=='f',"Unsupported mock V4L2 event");
f.data=pixels;f.size=sizeof(pixels);f.width=2;f.height=2;f.bytes_per_line=4;f.sequence=sequence++;
return true;
}
#else
extern "C" {
int __real_poll(pollfd*,nfds_t,int);
int __real_open(char const*,int,...);
int __real_close(int);
int __real_ioctl(int,unsigned long,...);
void* __real_mmap(void*,size_t,int,int,int,off_t);
int __real_munmap(void*,size_t);
int __wrap_open(char const* path,int flags,...) {
if(std::strcmp(path,"/mock/camera")==0)return 100000;
return __real_open(path,flags);
}
int __wrap_close(int fd){return fd==100000?0:__real_close(fd);}
void* __wrap_mmap(void* a,size_t n,int p,int f,int fd,off_t o) {
return fd==100000?pixels:__real_mmap(a,n,p,f,fd,o);
}
int __wrap_munmap(void* a,size_t n){return a==pixels?0:__real_munmap(a,n);}
int __wrap_ioctl(int fd,unsigned long request,...) {
va_list args;va_start(args,request);void* p=va_arg(args,void*);va_end(args);
if(fd!=100000)return __real_ioctl(fd,request,p);
switch(request) {
case VIDIOC_QUERYCAP:static_cast<v4l2_capability*>(p)->capabilities=V4L2_CAP_VIDEO_CAPTURE|V4L2_CAP_STREAMING;break;
case VIDIOC_S_FMT:static_cast<v4l2_format*>(p)->fmt.pix.bytesperline=4;break;
case VIDIOC_G_PARM:break;
case VIDIOC_REQBUFS:static_cast<v4l2_requestbuffers*>(p)->count=2;break;
case VIDIOC_QUERYBUF:static_cast<v4l2_buffer*>(p)->length=sizeof(pixels);break;
case VIDIOC_DQBUF: {
if(current=='x'){errno=EIO;return -1;}
auto& b=*static_cast<v4l2_buffer*>(p);b.index=0;b.bytesused=sizeof(pixels);b.sequence=sequence++;
b.flags=current=='e'?V4L2_BUF_FLAG_ERROR:0;break;
}
case VIDIOC_QBUF:case VIDIOC_STREAMON:case VIDIOC_STREAMOFF:break;
default:throw std::runtime_error("Unexpected camera ioctl");
}
return 0;
}
int __wrap_poll(pollfd* fds,nfds_t count,int waitMs) {
if(count==1)return __real_poll(fds,count,waitMs);
check(count==2&&waitMs>0&&waitMs<=timeoutMs,"Camera must respect the configured timeout window");
switch(next()) {
case 't':if(realWait)std::this_thread::sleep_for(std::chrono::milliseconds(waitMs));return 0;
case 'i':errno=EINTR;return -1;
case 's':onStop();return 0;
case 'd':fds[0].revents=POLLHUP;return 1;
case 'f':case 'e':case 'x':fds[0].revents=POLLIN;return 1;
default:throw std::runtime_error("Unexpected poll event");
}
}
int __wrap___poll_chk(pollfd* fds,nfds_t count,int waitMs,size_t size) {
check(size>=count*sizeof(pollfd),"Invalid fortified poll buffer size");
return __wrap_poll(fds,count,waitMs);
}
}
#endif
int main(int argc,char** argv)try {
cms::CameraConfig config;config.device="/mock/camera";config.width=config.height=2;config.buffers=2;
check(config.timeout_ms==2000,"Default per-attempt timeout must be 2 seconds");
realWait=argc>1&&std::string(argv[1])=="--wall-clock";
if(!realWait)config.timeout_ms=100;
timeoutMs=config.timeout_ms;
if(realWait) {
cms::Camera camera(config);script("tttttt");
auto before=std::chrono::steady_clock::now();expectTimeout(camera);
auto elapsed=std::chrono::duration<double>(std::chrono::steady_clock::now()-before).count();
check(cursor==6&&elapsed>=12.0&&elapsed<15.0,"Default timeout should fail after six attempts, about 12 seconds");
std::cout<<"PASS wall-clock six-timeout duration="<<elapsed<<" s\n";
return 0;
}
{
cms::Camera camera(config);
script("tttttf");check(bool(camera.read()),"Five timeouts should recover on a valid frame");
script("tttttf");check(bool(camera.read()),"A valid frame must reset the timeout count");
script("tttttt");
expectTimeout(camera);
check(cursor==6,"Failure must occur on the sixth timeout");
}
{
cms::Camera camera(config);onStop=[&]{camera.requestStop();};
script("tts");check(!camera.read(),"Stop must cancel retries without throwing");
check(cursor==3,"Stop should not wait for all six timeouts");onStop={};
}
{
cms::Camera camera(config);onStop=[&]{
std::this_thread::sleep_for(std::chrono::milliseconds(timeoutMs));
camera.requestStop();
};
script("ttttts");check(!camera.read(),"Stop on the final timeout must take precedence over failure");
onStop={};
}
#ifndef TEST_V4L2_CAPTURE
{
cms::Camera camera(config);script("tttttif");
check(bool(camera.read()),"EINTR must not count as a timeout");
script("tttttet");expectTimeout(camera);
check(cursor==7,"A corrupt frame must not reset the timeout count");
}
{
cms::Camera camera(config);script("df");
check(bool(camera.read()),"Capture must reopen after a transient disconnection");
}
{
cms::Camera camera(config);script("xxf");
check(bool(camera.read()),"Transient EIO must recover without restarting the pipeline");
}
{
auto slow=config;slow.timeout_ms=2000;timeoutMs=slow.timeout_ms;
cms::Camera camera(slow);script(std::string(35,'e')+"f");
check(bool(camera.read()),"More than one second's frame count of corrupt frames must remain recoverable");
timeoutMs=config.timeout_ms;
}
#endif
std::cout<<"PASS camera timeout retries, recovery reset, sixth-timeout failure and cancellation\n";
return 0;
}catch(std::exception const& e){std::cerr<<"FAIL: "<<e.what()<<'\n';return 1;}
+39
View File
@@ -0,0 +1,39 @@
#define _GNU_SOURCE
#include <dlfcn.h>
#include <poll.h>
#include <sys/select.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include <stdarg.h>
#include <errno.h>
#include <linux/videodev2.h>
#include <sys/ioctl.h>
static int calls=0;
static int camera_fd(int fd){char name[64],target[256];snprintf(name,sizeof(name),"/proc/self/fd/%d",fd);ssize_t n=readlink(name,target,sizeof(target)-1);if(n<0)return 0;target[n]=0;return strcmp(target,"/dev/video5")==0;}
static long long now_ms(void){struct timespec t;clock_gettime(CLOCK_MONOTONIC,&t);return (long long)t.tv_sec*1000+t.tv_nsec/1000000;}
static void marker(const char *s){ssize_t ignored=write(2,s,strlen(s));(void)ignored;}
static int should_stall(void){
static long long since=0;static int phase=0,resumed=0;
if(!getenv("VAP_TEST_STALL"))return 0;
if(phase==0){if(++calls<60)return 0;phase=1;since=now_ms();marker("TEST_STALL_ACTIVE\n");}
if(phase==1&&getenv("VAP_TEST_RECOVER")&&now_ms()-since>=4200){phase=2;marker("TEST_CAMERA_RECOVERED\n");return 0;}
if(phase==2){if(++resumed<30)return 0;phase=3;marker("TEST_STALL_SECOND\n");}
return phase==1||phase==3;
}
int poll(struct pollfd *fds,nfds_t count,int timeout){static int(*real_poll)(struct pollfd*,nfds_t,int);if(!real_poll)real_poll=dlsym(RTLD_NEXT,"poll");for(nfds_t i=0;i<count;++i)if(!getenv("VAP_TEST_KIND")&&camera_fd(fds[i].fd)&&should_stall()){int saved=fds[i].fd;fds[i].fd=-1;int rc=real_poll(fds,count,timeout);fds[i].fd=saved;return rc;}return real_poll(fds,count,timeout);}
int __poll_chk(struct pollfd *fds,nfds_t count,int timeout,size_t size){(void)size;return poll(fds,count,timeout);}
int select(int count,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout){static int(*real_select)(int,fd_set*,fd_set*,fd_set*,struct timeval*);if(!real_select)real_select=dlsym(RTLD_NEXT,"select");if(readfds)for(int fd=0;fd<count;++fd)if(!getenv("VAP_TEST_KIND")&&FD_ISSET(fd,readfds)&&camera_fd(fd)&&should_stall()){FD_CLR(fd,readfds);break;}return real_select(count,readfds,writefds,exceptfds,timeout);}
int ioctl(int fd,unsigned long request,...){
static int(*real_ioctl)(int,unsigned long,...);if(!real_ioctl)real_ioctl=dlsym(RTLD_NEXT,"ioctl");
va_list args;va_start(args,request);void *arg=va_arg(args,void*);va_end(args);
const char *kind=getenv("VAP_TEST_KIND");
if(kind&&request==VIDIOC_DQBUF&&camera_fd(fd)&&should_stall()){
if(strcmp(kind,"bad_frames")==0){int rc=real_ioctl(fd,request,arg);if(rc==0)((struct v4l2_buffer*)arg)->flags|=V4L2_BUF_FLAG_ERROR;return rc;}
errno=strcmp(kind,"device_error")==0?ENODEV:EIO;return -1;
}
return real_ioctl(fd,request,arg);
}
+253
View File
@@ -0,0 +1,253 @@
[
{
"branch": "lazy-inference",
"mode": "recover",
"exit_code": 1,
"inference_json_frames": 88,
"gpu_models": "YOLO + RTMW TensorRT engines",
"six_timeout_exit_seconds": 13.085,
"recovered_real_capture": true
},
{
"branch": "lazy-inference",
"mode": "bad_frames_recover",
"exit_code": 0,
"inference_json_frames": 67,
"gpu_models": "YOLO + RTMW TensorRT engines",
"signal_exit_ms": 846.3,
"recovered_real_capture": true,
"frames_after_recovery": 10,
"pipeline_stop_ms": 132.56
},
{
"branch": "lazy-inference",
"mode": "io_error_recover",
"exit_code": 0,
"inference_json_frames": 67,
"gpu_models": "YOLO + RTMW TensorRT engines",
"signal_exit_ms": 836.09,
"recovered_real_capture": true,
"frames_after_recovery": 10,
"pipeline_stop_ms": 106.06
},
{
"branch": "lazy-inference",
"mode": "device_error_recover",
"exit_code": 0,
"inference_json_frames": 68,
"gpu_models": "YOLO + RTMW TensorRT engines",
"signal_exit_ms": 830.0,
"recovered_real_capture": true,
"frames_after_recovery": 10,
"pipeline_stop_ms": 133.41
},
{
"branch": "lazy-inference",
"mode": "live",
"exit_code": 0,
"inference_json_frames": 3,
"gpu_models": "YOLO + RTMW TensorRT engines",
"signal_exit_ms": 833.16,
"pipeline_stop_ms": 139.5
},
{
"branch": "lazy-inference",
"mode": "retry",
"exit_code": 0,
"inference_json_frames": 58,
"gpu_models": "YOLO + RTMW TensorRT engines",
"signal_exit_ms": 942.42,
"pipeline_stop_ms": 201.21
},
{
"branch": "lazy-inference",
"mode": "sixth_wait",
"exit_code": 0,
"inference_json_frames": 58,
"gpu_models": "YOLO + RTMW TensorRT engines",
"signal_exit_ms": 904.29,
"pipeline_stop_ms": 194.16
},
{
"branch": "lazy-inference",
"mode": "sigterm",
"exit_code": 0,
"inference_json_frames": 3,
"gpu_models": "YOLO + RTMW TensorRT engines",
"signal_exit_ms": 801.21,
"pipeline_stop_ms": 123.09
},
{
"branch": "lazy-inference",
"mode": "timeout",
"exit_code": 1,
"inference_json_frames": 58,
"gpu_models": "YOLO + RTMW TensorRT engines",
"six_timeout_exit_seconds": 13.01
},
{
"branch": "inference-only",
"mode": "recover",
"exit_code": 1,
"inference_json_frames": 88,
"gpu_models": "YOLO + RTMW TensorRT engines",
"six_timeout_exit_seconds": 12.945,
"recovered_real_capture": true
},
{
"branch": "inference-only",
"mode": "bad_frames_recover",
"exit_code": 0,
"inference_json_frames": 68,
"gpu_models": "YOLO + RTMW TensorRT engines",
"signal_exit_ms": 836.67,
"recovered_real_capture": true,
"frames_after_recovery": 10
},
{
"branch": "inference-only",
"mode": "io_error_recover",
"exit_code": 0,
"inference_json_frames": 68,
"gpu_models": "YOLO + RTMW TensorRT engines",
"signal_exit_ms": 827.43,
"recovered_real_capture": true,
"frames_after_recovery": 10
},
{
"branch": "inference-only",
"mode": "device_error_recover",
"exit_code": 0,
"inference_json_frames": 68,
"gpu_models": "YOLO + RTMW TensorRT engines",
"signal_exit_ms": 853.08,
"recovered_real_capture": true,
"frames_after_recovery": 10
},
{
"branch": "inference-only",
"mode": "live",
"exit_code": 0,
"inference_json_frames": 3,
"gpu_models": "YOLO + RTMW TensorRT engines",
"signal_exit_ms": 804.87
},
{
"branch": "inference-only",
"mode": "retry",
"exit_code": 0,
"inference_json_frames": 58,
"gpu_models": "YOLO + RTMW TensorRT engines",
"signal_exit_ms": 913.42
},
{
"branch": "inference-only",
"mode": "sixth_wait",
"exit_code": 0,
"inference_json_frames": 58,
"gpu_models": "YOLO + RTMW TensorRT engines",
"signal_exit_ms": 1012.57
},
{
"branch": "inference-only",
"mode": "sigterm",
"exit_code": 0,
"inference_json_frames": 3,
"gpu_models": "YOLO + RTMW TensorRT engines",
"signal_exit_ms": 793.22
},
{
"branch": "inference-only",
"mode": "timeout",
"exit_code": 1,
"inference_json_frames": 58,
"gpu_models": "YOLO + RTMW TensorRT engines",
"six_timeout_exit_seconds": 13.055
},
{
"branch": "use-can",
"mode": "recover",
"exit_code": 1,
"inference_json_frames": 88,
"gpu_models": "YOLO + RTMW TensorRT engines",
"six_timeout_exit_seconds": 13.155,
"recovered_real_capture": true
},
{
"branch": "use-can",
"mode": "bad_frames_recover",
"exit_code": 0,
"inference_json_frames": 67,
"gpu_models": "YOLO + RTMW TensorRT engines",
"signal_exit_ms": 815.49,
"recovered_real_capture": true,
"frames_after_recovery": 10,
"pipeline_stop_ms": 122.36
},
{
"branch": "use-can",
"mode": "io_error_recover",
"exit_code": 0,
"inference_json_frames": 68,
"gpu_models": "YOLO + RTMW TensorRT engines",
"signal_exit_ms": 836.05,
"recovered_real_capture": true,
"frames_after_recovery": 10,
"pipeline_stop_ms": 130.73
},
{
"branch": "use-can",
"mode": "device_error_recover",
"exit_code": 0,
"inference_json_frames": 68,
"gpu_models": "YOLO + RTMW TensorRT engines",
"signal_exit_ms": 814.68,
"recovered_real_capture": true,
"frames_after_recovery": 10,
"pipeline_stop_ms": 123.99
},
{
"branch": "use-can",
"mode": "live",
"exit_code": 0,
"inference_json_frames": 3,
"gpu_models": "YOLO + RTMW TensorRT engines",
"signal_exit_ms": 786.06,
"pipeline_stop_ms": 108.96
},
{
"branch": "use-can",
"mode": "retry",
"exit_code": 0,
"inference_json_frames": 58,
"gpu_models": "YOLO + RTMW TensorRT engines",
"signal_exit_ms": 862.45,
"pipeline_stop_ms": 167.55
},
{
"branch": "use-can",
"mode": "sixth_wait",
"exit_code": 0,
"inference_json_frames": 58,
"gpu_models": "YOLO + RTMW TensorRT engines",
"signal_exit_ms": 907.59,
"pipeline_stop_ms": 197.07
},
{
"branch": "use-can",
"mode": "sigterm",
"exit_code": 0,
"inference_json_frames": 3,
"gpu_models": "YOLO + RTMW TensorRT engines",
"signal_exit_ms": 827.61,
"pipeline_stop_ms": 130.51
},
{
"branch": "use-can",
"mode": "timeout",
"exit_code": 1,
"inference_json_frames": 58,
"gpu_models": "YOLO + RTMW TensorRT engines",
"six_timeout_exit_seconds": 13.066
}
]
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
test_dir="$(mktemp -d)"
trap 'rm -rf "$test_dir"' EXIT
flags=(-std=c++17 -O1 -g -Wall -Wextra -Wpedantic -pthread -fsanitize=undefined -fno-sanitize-recover=all -I"$root/include")
links=()
if [[ -f "$root/include/v4l2_capture.h" ]]; then
flags+=(-DTEST_V4L2_CAPTURE -I"$root/tests/stubs")
else
for fn in open close ioctl mmap munmap poll __poll_chk; do links+=("-Wl,--wrap=$fn"); done
fi
"${CXX:-g++}" "${flags[@]}" "${CAMERA_SOURCE:-$root/src/camera.cpp}" "$root/tests/camera_timeout_test.cpp" "${links[@]}" -o "$test_dir/camera_timeout_test"
"$test_dir/camera_timeout_test" "$@"
+19
View File
@@ -0,0 +1,19 @@
#pragma once
// Camera timeout tests exercise live capture only. Stub the unused replay API
// so the use-can Camera can be tested without OpenCV or a video decoder.
#include <stdexcept>
#include <string>
namespace cv {
constexpr int CAP_PROP_FRAME_WIDTH=3,CAP_PROP_FRAME_HEIGHT=4;
struct Mat {
int cols{},rows{};
unsigned char const* ptr(int)const {throw std::logic_error("Video replay is outside this test");}
};
class VideoCapture {
public:
bool isOpened()const{return false;}
bool open(std::string const&){throw std::logic_error("Video replay is outside this test");}
double get(int)const {throw std::logic_error("Video replay is outside this test");}
bool read(Mat&){throw std::logic_error("Video replay is outside this test");}
};
}