Add optional original, annotated and JSON frame outputs
This commit is contained in:
@@ -2,3 +2,6 @@
|
||||
/engines/
|
||||
/logs/
|
||||
*.tmp
|
||||
|
||||
# Optional per-frame output, including standalone Orin runs
|
||||
output/
|
||||
|
||||
+1
-1
@@ -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")
|
||||
|
||||
@@ -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 开销。写入失败时报告错误并停止,不静默丢失文件。
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
#include "camera.hpp"
|
||||
#include "frame_result.hpp"
|
||||
#include "output_writer.hpp"
|
||||
#include "lazy_gate.hpp"
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
@@ -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};
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
#pragma once
|
||||
#include "frame_result.hpp"
|
||||
#include "door_risk.hpp"
|
||||
namespace cms { Json serializeResult(FrameResult const& result); }
|
||||
@@ -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 开销。写入失败时报告错误并停止,不静默丢失文件。
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
Binary file not shown.
@@ -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
|
||||
)
|
||||
|
||||
@@ -52,6 +52,7 @@ struct CameraPipeline::Impl {
|
||||
BoundedQueue<Pending> 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;
|
||||
@@ -99,6 +100,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;
|
||||
@@ -192,6 +194,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
@@ -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<argc;++i) {
|
||||
std::string arg=argv[i];
|
||||
if(arg=="--config")configPath=value(i);
|
||||
else if(arg=="--save-original")output.original=true;
|
||||
else if(arg=="--save-annotated")output.annotated=true;
|
||||
else if(arg=="--save-results")output.results=true;
|
||||
else if(arg=="--visualize")visualize=true;
|
||||
else if(arg=="--duration") {duration=std::stod(value(i));if(!std::isfinite(duration)||duration<0)throw std::invalid_argument("Invalid duration");}
|
||||
else if(arg=="--help") {
|
||||
std::cout<<"camera_risk [--config FILE] [--visualize] [--duration SECONDS]\n"
|
||||
std::cout<<"camera_risk [--save-original] [--save-annotated] [--save-results] [--config FILE] [--visualize] [--duration SECONDS]\n"
|
||||
<<"C++ lazy inference; resident warm engines, continuous CAN reception.\n";return 0;
|
||||
}else throw std::invalid_argument("Unknown option: "+arg);
|
||||
}
|
||||
cv::setNumThreads(2);
|
||||
cms::CameraPipeline pipeline(cms::loadConfig(configPath));
|
||||
auto config=cms::loadConfig(configPath);config.output=output;
|
||||
cms::CameraPipeline pipeline(config);
|
||||
cms::StopSignals signals(pipeline);pipeline.start();
|
||||
auto started=std::chrono::steady_clock::now();std::atomic<bool> monitorStop{false};
|
||||
std::thread monitor([&] {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
#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}});
|
||||
}
|
||||
auto const& state=r.inference;
|
||||
out["inference"]={{"models_ready",state.models_ready},{"enabled",state.enabled},{"can_valid",state.can_valid},{"positions",state.positions},{"direction",state.direction},{"left_front_trigger",state.left_front_trigger},{"left_rear_trigger",state.left_rear_trigger},{"generation",state.generation},{"can_age_ms",state.can_age_ms},{"reason",state.reason}};
|
||||
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
@@ -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);}
|
||||
|
||||
Reference in New Issue
Block a user