diff --git a/.gitignore b/.gitignore index 4097644..b6b3fab 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ /engines/ /logs/ *.tmp + +# Optional per-frame output, including standalone Orin runs +output/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 151691d..5735049 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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/lazy_gate.cpp src/can_input.cpp src/can_comm.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 src/lazy_gate.cpp src/can_input.cpp src/can_comm.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 "${CMAKE_SOURCE_DIR}/orin/lib/libusbcanfd.so" "${CMAKE_SOURCE_DIR}/orin/lib/libusb-1.0.so") diff --git a/README.md b/README.md index 495f486..61a15a5 100644 --- a/README.md +++ b/README.md @@ -74,3 +74,22 @@ if (result) { - `scripts/`:编译、初始化、打包;`third_party/`:JSON 依赖。 模型和推理算法沿用纯推理基线。当前门 mask 为研发机位配置,装车需按实际机位标定。 + +## 可选保存(默认全部关闭) + +三个独立参数可以单独使用,也可以组合: + +```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 开销。写入失败时报告错误并停止,不静默丢失文件。 diff --git a/include/camera_pipeline.hpp b/include/camera_pipeline.hpp index 165a36f..d47de62 100644 --- a/include/camera_pipeline.hpp +++ b/include/camera_pipeline.hpp @@ -1,6 +1,7 @@ #pragma once #include "camera.hpp" #include "frame_result.hpp" +#include "output_writer.hpp" #include "lazy_gate.hpp" #include #include @@ -8,6 +9,7 @@ namespace cms { struct PipelineConfig { CameraConfig camera; + OutputOptions output; LazyGateConfig lazy_gate; std::filesystem::path engines,door_mask; double detector_confidence{0.30}; diff --git a/include/output_writer.hpp b/include/output_writer.hpp new file mode 100644 index 0000000..2f8f0d6 --- /dev/null +++ b/include/output_writer.hpp @@ -0,0 +1,20 @@ +#pragma once +#include "frame_result.hpp" +#include +#include +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_; +}; +} diff --git a/include/serialization.hpp b/include/serialization.hpp new file mode 100644 index 0000000..d6e7fc2 --- /dev/null +++ b/include/serialization.hpp @@ -0,0 +1,4 @@ +#pragma once +#include "frame_result.hpp" +#include "door_risk.hpp" +namespace cms { Json serializeResult(FrameResult const& result); } diff --git a/orin/README.md b/orin/README.md index c40305f..345f9cc 100644 --- a/orin/README.md +++ b/orin/README.md @@ -15,3 +15,21 @@ CAN 0x391 的 data[0..3] 为左前、右前、左后、右后百分比。左前 模型只加载预热一次并常驻;相机和 CAN 始终运行。重复相同开度保留运动方向 300 ms,之后视为静止。参数见 config/camera.json 的 lazy_inference。默认结果保存在 C++ 内存,不写逐帧文件;暂停时 latest() 为空。标准输出记录启停状态与累计模型运行次数。 文件完整性:`sha256sum -c SHA256SUMS`。 + +## 可选保存(默认全部关闭) + +三个独立参数可以单独使用,也可以组合: + +```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 开销。写入失败时报告错误并停止,不静默丢失文件。 diff --git a/orin/SHA256SUMS b/orin/SHA256SUMS index 4d682c1..33bc824 100644 --- a/orin/SHA256SUMS +++ b/orin/SHA256SUMS @@ -1,8 +1,8 @@ -cc83bfe41b66445843ef56b535e267939ed7944869474ca5aed6908f981b4687 ./README.md +5a78784dd7ea279618d3b6cf0c63be4b6b6f20f5ce6c9b13ff32bc9001e73ca6 ./README.md 6d5e874fd27fcfd75d303d7e35d9672028af71041f52dcc82a1e83676faefa65 ./bin/drivers/max9295.ko 8a78d04ef8be44e9b62a5e4ee315d3d6d679e34aedff02a234bbf4d40e222933 ./bin/drivers/max9296.ko 3da628587477cc35d5e3f8aee86e718e2ddf16808e893e00ba6e164b2307e843 ./bin/drivers/sgx-yuv-gmsl2.ko -9b467b4caf53d5cc5cdb5f71687e246146c05ef72fc7d7c593681409f63eec70 ./build/camera_risk +2675d6cfafef74d75bcf8fba460cf2cad4c3dc80779614e45b0565db34abba21 ./build/camera_risk 951c514ee76e10233a84746f25a884e0a1e8cbed84df57845a5aff66af9aecbd ./config/camera.json 9ba768d559fd3b64e833b50c62c70f2adfd6c169620aea5fba32cfb1b4c5b0e9 ./config/door_mask.json ebcd09e8c2e35008adbb0c1d222e580b17e0df1c8093df3481ba3374cd9a8174 ./engines/rtmw_l_384.engine diff --git a/orin/build/camera_risk b/orin/build/camera_risk index c57dd89..4a2fb35 100755 Binary files a/orin/build/camera_risk and b/orin/build/camera_risk differ diff --git a/scripts/package_orin.sh b/scripts/package_orin.sh index 858ac9e..ba0280c 100755 --- a/scripts/package_orin.sh +++ b/scripts/package_orin.sh @@ -7,6 +7,6 @@ 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 './outputs/*' ! -path './logs/*' ! -name SHA256SUMS -print0 | + find . -type f ! -path './output/*' ! -path './outputs/*' ! -path './logs/*' ! -name SHA256SUMS -print0 | sort -z | xargs -0 sha256sum > SHA256SUMS ) diff --git a/src/camera_pipeline.cpp b/src/camera_pipeline.cpp index 200aa6a..962c412 100644 --- a/src/camera_pipeline.cpp +++ b/src/camera_pipeline.cpp @@ -52,6 +52,7 @@ struct CameraPipeline::Impl { BoundedQueue captures; BoundedQueue detections; LatestResult results; + std::unique_ptr outputWriter; fangjia::TrtLogger logger; std::unique_ptr detector; std::unique_ptr pose; @@ -99,6 +100,7 @@ struct CameraPipeline::Impl { void start() { {std::lock_guard lock(stateMutex);if(started||stopped)throw std::logic_error("CameraPipeline can only be started once");started=true;} try { + outputWriter=std::make_unique(config.output); detector=std::make_unique(config.engines/"yolo11s.engine",logger,config.detector_confidence,config.max_people); pose=std::make_unique(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; @@ -192,6 +194,7 @@ struct CameraPipeline::Impl { FrameResultPtr immutable=result; results.publish(std::move(immutable)); {std::lock_guard lock(stateMutex);totalLatency+=result->timing.capture_to_result_ms;++published;} + outputWriter->write(*result); } active=false; } diff --git a/src/main.cpp b/src/main.cpp index 9eb5abe..d8cd6c6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -13,20 +13,25 @@ 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;double duration=0; auto value=[&](int& i){if(++i>=argc)throw std::invalid_argument("Missing option value");return std::string(argv[i]);}; for(int i=1;i monitorStop{false}; std::thread monitor([&] { diff --git a/src/output_writer.cpp b/src/output_writer.cpp new file mode 100644 index 0000000..77c96e1 --- /dev/null +++ b/src/output_writer.cpp @@ -0,0 +1,69 @@ +#include "output_writer.hpp" +#include "serialization.hpp" +#include "visualizer.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +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(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 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(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(now.time_since_epoch()).count()%1000000000; + std::ostringstream id;id<(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()); + } +} +} diff --git a/src/result_renderer.cpp b/src/result_renderer.cpp new file mode 100644 index 0000000..a315abf --- /dev/null +++ b/src/result_renderer.cpp @@ -0,0 +1,80 @@ +#include "visualizer.hpp" +#include +#include +#include +#include +#include +#include + +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(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.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> 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 "<>{{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 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>{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< Json optional(std::optional 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 #include #include -#include -#include -#include -#include -#include - 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(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.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> 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 "<>{{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 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>{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<