feat: add low-light brightness gate

This commit is contained in:
zmv
2026-10-09 16:02:15 +08:00
parent 4eff3825be
commit 69d565fda5
18 changed files with 319 additions and 26 deletions
+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/can_bus.cpp src/detector.cpp src/door_risk.cpp src/camera.cpp src/camera_pipeline.cpp src/inference_session.cpp src/serialization.cpp src/output_writer.cpp src/result_renderer.cpp src/config.cpp)
src/can_bus.cpp src/detector.cpp src/door_risk.cpp src/brightness_gate.cpp src/camera.cpp src/camera_pipeline.cpp src/inference_session.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_include_directories(camera_risk_core SYSTEM PRIVATE third_party/zlg)
+2
View File
@@ -5,6 +5,8 @@ C++ / TensorRT 实时摄像头风险推理,算法来自最新 run_risk.py。
相机每次取帧默认等待 **2000 ms**,连续超时 **6 次**才停止流水线(纯超时约 **12 秒**);成功取得有效帧后清零超时计数。单次等待时间由 `config/camera.json` 的 `camera.timeout_ms` 配置。运行中的坏帧、取流错误共用该重试期限;设备失效时尝试重新打开相机,恢复有效帧后继续推理。GPU 错误仍按原有异常处理退出。
原始画面在 YOLO 前经过低照度门控。启动前 5 帧不输出有效风险;明显极暗连续 15 帧后暂停人体、姿态和风险推理,亮度满足恢复阈值连续 15 帧后自动恢复。低照度结果标记 `risk_valid=false`、`risk_level=unavailable`,不会转换成 `clear`。阈值和 ROI 可在 `brightness_gate` 配置段调整,`enabled=false` 可关闭门控。阈值依据研发原始视频验证说明设置,适用边界仍包括黄昏、局部强光和镜头污染等未覆盖场景。
## Orin 直接运行
适用环境:Jetson AGX Orin、JetPack 5.1.2 / L4T R35.4.1、CUDA 11.4、TensorRT 8.5.2、OpenCV 4.5.4。
+14
View File
@@ -19,6 +19,20 @@
"release_relative": 0.45,
"calibration_verified": false
},
"brightness_gate": {
"enabled": true,
"roi": [0.02, 0.05, 0.88, 0.98],
"dark_pixel_luma": 40.0,
"enter_median_luma": 18.0,
"enter_dark_ratio": 0.85,
"enter_dark_tile_ratio": 0.75,
"exit_median_luma": 24.0,
"exit_dark_ratio": 0.80,
"exit_dark_tile_ratio": 0.70,
"startup_frames": 5,
"enter_frames": 15,
"exit_frames": 15
},
"can": {
"enabled": true,
"device_index": 0,
+33
View File
@@ -0,0 +1,33 @@
#pragma once
#include <opencv2/core.hpp>
#include <string>
namespace cms {
struct BrightnessGateConfig {
bool enabled{true};
double roi_x1{0.02}, roi_y1{0.05}, roi_x2{0.88}, roi_y2{0.98};
double dark_pixel_luma{40.0};
double enter_median_luma{18.0}, enter_dark_ratio{0.85}, enter_dark_tile_ratio{0.75};
double exit_median_luma{24.0}, exit_dark_ratio{0.80}, exit_dark_tile_ratio{0.70};
int startup_frames{5}, enter_frames{15}, exit_frames{15};
};
struct BrightnessDecision {
std::string state{"INITIALIZING"}, reason{"initializing"}, transition;
bool allow_downstream{};
double median_luma{}, p75_luma{}, mean_luma{}, dark_pixel_luma{40.0}, dark_ratio{}, dark_tile_ratio{};
int dark_streak{}, bright_streak{};
int enter_frames{}, exit_frames{};
};
class BrightnessGate {
public:
explicit BrightnessGate(BrightnessGateConfig config = {});
BrightnessDecision update(cv::Mat const& bgr);
private:
BrightnessGateConfig config_;
std::string state_{"INITIALIZING"};
int startup_count_{}, startup_dark_votes_{}, dark_streak_{}, bright_streak_{};
};
}
+2
View File
@@ -1,5 +1,6 @@
#pragma once
#include "camera.hpp"
#include "brightness_gate.hpp"
#include "frame_result.hpp"
#include "output_writer.hpp"
#include <filesystem>
@@ -16,6 +17,7 @@ struct PipelineConfig {
double detector_confidence{0.30};
int max_people{3};
double warning_relative{0.30},release_relative{0.45};
BrightnessGateConfig brightness_gate;
std::size_t queue_capacity{2};
bool door_mask_calibration_verified{false};
};
+2
View File
@@ -2,6 +2,7 @@
// Public downstream contract. Owns its data; no GPU, JSON or network handles.
#include "image.hpp"
#include "can_types.hpp"
#include "brightness_gate.hpp"
#include <array>
#include <cstdint>
#include <memory>
@@ -77,6 +78,7 @@ struct FrameResult {
std::shared_ptr<const BgrImage> image;
std::shared_ptr<const BinaryMask> door_mask_pixels;
bool door_mask_calibration_verified{};
BrightnessDecision brightness;
};
using FrameResultPtr = std::shared_ptr<const FrameResult>;
}
+3 -2
View File
@@ -5,8 +5,9 @@
namespace cms {
class CameraPipeline;
// Pure drawing from the immutable result. Never re-runs inference.
cv::Mat renderResult(FrameResult const& result,CanSnapshot const* liveCan=nullptr);
cv::Mat renderResult(FrameResult const& result,CanSnapshot const* liveCan=nullptr,bool drawText=true);
cv::Mat renderVisualization(FrameResult const& result,CanSnapshot const& liveCan);
void renderCanOverlay(cv::Mat& image,CanSnapshot const& can);
// Optional observer: closing the window returns without stopping the pipeline.
void runLocalVisualization(CameraPipeline const& pipeline);
void runLocalVisualization(CameraPipeline const& pipeline, bool keepFinalFrame = false);
}
+2 -2
View File
@@ -2,9 +2,9 @@
6d5e874fd27fcfd75d303d7e35d9672028af71041f52dcc82a1e83676faefa65 ./bin/drivers/max9295.ko
8a78d04ef8be44e9b62a5e4ee315d3d6d679e34aedff02a234bbf4d40e222933 ./bin/drivers/max9296.ko
3da628587477cc35d5e3f8aee86e718e2ddf16808e893e00ba6e164b2307e843 ./bin/drivers/sgx-yuv-gmsl2.ko
1744ec8a2c584aa5306cdf975fb7f01ec9ee6cec524a447dfc85e179b270ffc3 ./build/camera_risk
cc34c1e0281c27b380a9a90bd58cfa65fefa322612be0de42a7faa86e15ef39e ./build/camera_risk
12ebf1d21a967cc4d7620636352fc5779a697a0fb9feaa8bdb339927292a3517 ./build/door_mask_tool
d44dcf10df43c7bcd73b7f2b64e4117d207435a5218c2146bfb659566cb320ba ./config/camera.json
e1ac783278a1dc4ebeb15f48072a865a51dc44bc31dcb42b4a8aa96b8bdc067e ./config/camera.json
7808f4ae27de49a02fe51f8e6b60ae8454ff775ea3e98fa0a7c633ee85027fb4 ./config/door_mask.json
ebcd09e8c2e35008adbb0c1d222e580b17e0df1c8093df3481ba3374cd9a8174 ./engines/rtmw_l_384.engine
f6a65b926ac1144484f4aa93cd26d7997d32d62310ab93fe867f2bd4412222e9 ./engines/yolo11s.engine
Binary file not shown.
+14
View File
@@ -19,6 +19,20 @@
"release_relative": 0.45,
"calibration_verified": false
},
"brightness_gate": {
"enabled": true,
"roi": [0.02, 0.05, 0.88, 0.98],
"dark_pixel_luma": 40.0,
"enter_median_luma": 18.0,
"enter_dark_ratio": 0.85,
"enter_dark_tile_ratio": 0.75,
"exit_median_luma": 24.0,
"exit_dark_ratio": 0.80,
"exit_dark_tile_ratio": 0.70,
"startup_frames": 5,
"enter_frames": 15,
"exit_frames": 15
},
"can": {
"enabled": true,
"device_index": 0,
+79
View File
@@ -0,0 +1,79 @@
#include "brightness_gate.hpp"
#include <opencv2/imgproc.hpp>
#include <algorithm>
#include <cmath>
#include <stdexcept>
#include <vector>
namespace cms {
namespace {
double percentile(std::vector<unsigned char> const& values, double fraction) {
double position=fraction*static_cast<double>(values.size()-1);
auto lower=static_cast<std::size_t>(std::floor(position));
auto upper=static_cast<std::size_t>(std::ceil(position));
std::vector<unsigned char> ordered=values;
std::nth_element(ordered.begin(),ordered.begin()+upper,ordered.end());
double high=ordered[upper];
if(lower==upper)return high;
auto low=*std::max_element(ordered.begin(),ordered.begin()+upper);
return low+(high-low)*(position-lower);
}
}
BrightnessGate::BrightnessGate(BrightnessGateConfig config):config_(config) {
if(config_.startup_frames<1||config_.enter_frames<1||config_.exit_frames<1||
config_.roi_x1<0||config_.roi_y1<0||config_.roi_x2>1||config_.roi_y2>1||
config_.roi_x1>=config_.roi_x2||config_.roi_y1>=config_.roi_y2)
throw std::invalid_argument("Invalid brightness gate configuration");
}
BrightnessDecision BrightnessGate::update(cv::Mat const& bgr) {
if(bgr.empty()||bgr.type()!=CV_8UC3)throw std::invalid_argument("Brightness gate requires a non-empty BGR frame");
int x1=static_cast<int>(std::nearbyint(config_.roi_x1*bgr.cols));
int y1=static_cast<int>(std::nearbyint(config_.roi_y1*bgr.rows));
int x2=static_cast<int>(std::nearbyint(config_.roi_x2*bgr.cols));
int y2=static_cast<int>(std::nearbyint(config_.roi_y2*bgr.rows));
cv::Mat resized,luma;
cv::resize(bgr(cv::Rect(x1,y1,x2-x1,y2-y1)),resized,{160,120},0,0,cv::INTER_AREA);
cv::cvtColor(resized,luma,cv::COLOR_BGR2YCrCb);
std::vector<unsigned char> values;values.reserve(luma.total());
std::size_t dark=0;double sum=0;
for(int y=0;y<luma.rows;++y)for(int x=0;x<luma.cols;++x) {
auto value=luma.at<cv::Vec3b>(y,x)[0];values.push_back(value);sum+=value;
if(value<config_.dark_pixel_luma)++dark;
}
BrightnessDecision decision;
decision.enter_frames=config_.enter_frames;decision.exit_frames=config_.exit_frames;
decision.dark_pixel_luma=config_.dark_pixel_luma;
decision.median_luma=percentile(values,0.50);
decision.p75_luma=percentile(values,0.75);
decision.mean_luma=sum/values.size();decision.dark_ratio=static_cast<double>(dark)/values.size();
int dark_tiles=0;
for(int ty=0;ty<4;++ty)for(int tx=0;tx<4;++tx) {
std::vector<unsigned char> tile;tile.reserve(30*40);
for(int y=ty*30;y<(ty+1)*30;++y)for(int x=tx*40;x<(tx+1)*40;++x)tile.push_back(luma.at<cv::Vec3b>(y,x)[0]);
if(percentile(tile,0.50)<config_.dark_pixel_luma)++dark_tiles;
}
decision.dark_tile_ratio=static_cast<double>(dark_tiles)/16;
bool is_dark=decision.median_luma<=config_.enter_median_luma&&decision.dark_ratio>=config_.enter_dark_ratio&&decision.dark_tile_ratio>=config_.enter_dark_tile_ratio;
bool is_bright=decision.median_luma>=config_.exit_median_luma&&decision.dark_ratio<=config_.exit_dark_ratio&&decision.dark_tile_ratio<=config_.exit_dark_tile_ratio;
auto previous=state_;
if(!config_.enabled)state_="NORMAL";
else if(state_=="INITIALIZING") {
++startup_count_;startup_dark_votes_+=is_dark;
if(startup_count_>=config_.startup_frames)
state_=startup_dark_votes_>=((config_.startup_frames+1)/2)?"LOW_LIGHT":"NORMAL";
} else if(state_=="NORMAL") {
dark_streak_=is_dark?dark_streak_+1:0;bright_streak_=0;
if(dark_streak_>=config_.enter_frames){state_="LOW_LIGHT";dark_streak_=0;}
} else {
bright_streak_=is_bright?bright_streak_+1:0;dark_streak_=0;
if(bright_streak_>=config_.exit_frames){state_="NORMAL";bright_streak_=0;}
}
decision.state=state_;decision.allow_downstream=state_=="NORMAL";
decision.dark_streak=dark_streak_;decision.bright_streak=bright_streak_;
if(previous!=state_)decision.transition=previous+"->"+state_;
if(state_=="INITIALIZING")decision.reason="initializing";
else if(state_=="LOW_LIGHT")decision.reason=is_bright&&bright_streak_?"recovery_pending":"low_light";
else decision.reason=is_dark&&dark_streak_?"low_light_pending":"sufficient_light";
return decision;
}
}
+36 -5
View File
@@ -48,7 +48,7 @@ void attachPoses(FrameResult& result,fangjia::PoseFrameResult const& fused) {
}
}
struct CameraPipeline::Impl {
struct Detected {CapturedFrame frame;std::vector<fangjia::Box> boxes;double elapsed_ms{};};
struct Detected {CapturedFrame frame;std::vector<fangjia::Box> boxes;BrightnessDecision brightness;double elapsed_ms{};};
PipelineConfig config;
BoundedQueue<CapturedFrame> captures;
BoundedQueue<Detected> detections;
@@ -138,11 +138,13 @@ struct CameraPipeline::Impl {
captures.close();
}
void detectorLoop() {
Stream stream;
Stream stream;BrightnessGate gate(config.brightness_gate);
while(!stopped) {
auto frame=captures.pop();if(!frame)break;
auto startedAt=monotonicNs();auto boxes=detector->infer(imageView(*frame->image),stream.value);
if(!detections.push({std::move(*frame),std::move(boxes),elapsedMs(startedAt)}))break;
auto startedAt=monotonicNs();auto brightness=gate.update(imageView(*frame->image));
std::vector<fangjia::Box> boxes;
if(brightness.allow_downstream)boxes=detector->infer(imageView(*frame->image),stream.value);
if(!detections.push({std::move(*frame),std::move(boxes),std::move(brightness),elapsedMs(startedAt)}))break;
}
detections.close();
}
@@ -151,6 +153,34 @@ struct CameraPipeline::Impl {
while(!stopped) {
auto detected=detections.pop();if(!detected)break;
if(detected->frame.metadata.frame_index>static_cast<std::uint64_t>(std::numeric_limits<int>::max()))throw std::runtime_error("Frame index limit reached; restart pipeline");
if(!detected->brightness.allow_downstream) {
boxesTracker=fangjia::BoxTracker{};
fusion=fangjia::PoseFusionTracker{};
int index=static_cast<int>(detected->frame.metadata.frame_index);
auto emptyFusion=fusion.update({}, {}, index);
auto record=makeRecord(index,{}, {},emptyFusion,!config.record_session.empty(),false);
auto qualityStatus=detected->brightness.state;
Json invalidRisk={{"status",qualityStatus},{"risk_score",nullptr},{"risk_level","unavailable"},
{"decision","BRIGHTNESS_GATE"},{"source","brightness_gate"},{"track_id",nullptr},{"side",nullptr},
{"door_mask",{{"status","not_evaluated"},{"annotation_index",index},{"path",nullptr},{"format","none"},{"area_px",0}}},
{"warning_distance_relative",config.warning_relative},{"release_distance_relative",config.release_relative},{"regions",Json::array()}};
auto result=std::make_shared<FrameResult>(makeFrameResult(record,invalidRisk,detected->frame.metadata));
result->brightness=detected->brightness;
result->image=std::move(detected->frame.image);result->door_mask_pixels=doorPixels;
result->door_mask_calibration_verified=config.door_mask_calibration_verified;
if(detected->frame.replay_record) {
auto const& recorded=detected->frame.replay_record->at("result");
if(recorded.contains("can"))result->can=deserializeCan(recorded.at("can"));
else result->can.status="NOT_RECORDED";
result->can.source="replay";
} else result->can=can->snapshot();
result->timing={detected->elapsed_ms,0,0,elapsedMs(result->capture.capture_monotonic_ns)};
FrameResultPtr immutable=result;results.publish(std::move(immutable));
{std::lock_guard<std::mutex> lock(stateMutex);totalLatency+=result->timing.capture_to_result_ms;++published;}
if(!config.record_session.empty())sessionWriter->write(*result,record,detected->frame.replay_record.get());
outputWriter->write(*result);
continue;
}
auto index=static_cast<int>(detected->frame.metadata.frame_index);
auto& boxes=detected->boxes;
auto frame=imageView(*detected->frame.image);
@@ -168,13 +198,14 @@ struct CameraPipeline::Impl {
auto record=makeRecord(index,raw,isolated,fused,!config.record_session.empty(),false);
auto riskData=risk->evaluate(index,record);
auto result=std::make_shared<FrameResult>(makeFrameResult(record,riskData,detected->frame.metadata));
result->brightness=detected->brightness;
if(detected->frame.replay_record) {
auto const& recorded=detected->frame.replay_record->at("result");
if(recorded.contains("can"))result->can=deserializeCan(recorded.at("can"));
else result->can.status="NOT_RECORDED";
result->can.source="replay";
}else {
if(!sessionReader&&config.can.enabled)can->queueRisk(result->risk.risk_level);
if(!sessionReader&&config.can.enabled&&result->brightness.allow_downstream)can->queueRisk(result->risk.risk_level);
result->can=can->snapshot();
}
attachPoses(*result,fused);
+15
View File
@@ -19,6 +19,21 @@ PipelineConfig loadConfig(std::filesystem::path const& path) {
auto const& door=j.at("door");config.door_mask=resolve(door.at("mask"));
config.warning_relative=door.at("warning_relative");config.release_relative=door.at("release_relative");
config.door_mask_calibration_verified=door.at("calibration_verified");
if(j.contains("brightness_gate")) {
auto const& gate=j.at("brightness_gate");auto& c=config.brightness_gate;
c.enabled=gate.value("enabled",c.enabled);
if(gate.contains("roi")){auto const& roi=gate.at("roi");if(!roi.is_array()||roi.size()!=4)throw std::runtime_error("brightness_gate.roi must contain four normalized coordinates");c.roi_x1=roi[0];c.roi_y1=roi[1];c.roi_x2=roi[2];c.roi_y2=roi[3];}
c.dark_pixel_luma=gate.value("dark_pixel_luma",c.dark_pixel_luma);
c.enter_median_luma=gate.value("enter_median_luma",c.enter_median_luma);
c.enter_dark_ratio=gate.value("enter_dark_ratio",c.enter_dark_ratio);
c.enter_dark_tile_ratio=gate.value("enter_dark_tile_ratio",c.enter_dark_tile_ratio);
c.exit_median_luma=gate.value("exit_median_luma",c.exit_median_luma);
c.exit_dark_ratio=gate.value("exit_dark_ratio",c.exit_dark_ratio);
c.exit_dark_tile_ratio=gate.value("exit_dark_tile_ratio",c.exit_dark_tile_ratio);
c.startup_frames=gate.value("startup_frames",c.startup_frames);
c.enter_frames=gate.value("enter_frames",c.enter_frames);
c.exit_frames=gate.value("exit_frames",c.exit_frames);
}
if(j.contains("can")) {
auto const& can=j.at("can");config.can.enabled=can.value("enabled",true);
config.can.device_index=can.value("device_index",0);config.can.timeout_ms=can.value("timeout_ms",1000);
+21
View File
@@ -48,6 +48,12 @@ SessionWriter::SessionWriter(PipelineConfig const& c):directory_(c.record_sessio
Json config={{"width",c.camera.width},{"height",c.camera.height},{"fps",c.camera.fps},
{"detector_confidence",c.detector_confidence},{"max_people",c.max_people},
{"warning_relative",c.warning_relative},{"release_relative",c.release_relative},
{"brightness_gate",{{"enabled",c.brightness_gate.enabled},{"roi",{c.brightness_gate.roi_x1,c.brightness_gate.roi_y1,c.brightness_gate.roi_x2,c.brightness_gate.roi_y2}},
{"dark_pixel_luma",c.brightness_gate.dark_pixel_luma},{"enter_median_luma",c.brightness_gate.enter_median_luma},
{"enter_dark_ratio",c.brightness_gate.enter_dark_ratio},{"enter_dark_tile_ratio",c.brightness_gate.enter_dark_tile_ratio},
{"exit_median_luma",c.brightness_gate.exit_median_luma},{"exit_dark_ratio",c.brightness_gate.exit_dark_ratio},
{"exit_dark_tile_ratio",c.brightness_gate.exit_dark_tile_ratio},{"startup_frames",c.brightness_gate.startup_frames},
{"enter_frames",c.brightness_gate.enter_frames},{"exit_frames",c.brightness_gate.exit_frames}}},
{"calibration_verified",c.door_mask_calibration_verified},
{"can",{{"enabled",c.can.enabled},{"device_index",c.can.device_index},{"timeout_ms",c.can.timeout_ms},{"termination",c.can.termination}}}};
Json models=Json::object();
@@ -109,6 +115,21 @@ SessionReader::SessionReader(PipelineConfig& c):directory_(c.replay_session) {
c.camera.width=width_;c.camera.height=height_;c.camera.fps=j.at("fps");
c.detector_confidence=j.at("detector_confidence");c.max_people=j.at("max_people");
c.warning_relative=j.at("warning_relative");c.release_relative=j.at("release_relative");
if(j.contains("brightness_gate")) {
auto const& gate=j.at("brightness_gate");auto& cgate=c.brightness_gate;
cgate.enabled=gate.value("enabled",cgate.enabled);
if(gate.contains("roi")){auto const& roi=gate.at("roi");cgate.roi_x1=roi[0];cgate.roi_y1=roi[1];cgate.roi_x2=roi[2];cgate.roi_y2=roi[3];}
cgate.dark_pixel_luma=gate.value("dark_pixel_luma",cgate.dark_pixel_luma);
cgate.enter_median_luma=gate.value("enter_median_luma",cgate.enter_median_luma);
cgate.enter_dark_ratio=gate.value("enter_dark_ratio",cgate.enter_dark_ratio);
cgate.enter_dark_tile_ratio=gate.value("enter_dark_tile_ratio",cgate.enter_dark_tile_ratio);
cgate.exit_median_luma=gate.value("exit_median_luma",cgate.exit_median_luma);
cgate.exit_dark_ratio=gate.value("exit_dark_ratio",cgate.exit_dark_ratio);
cgate.exit_dark_tile_ratio=gate.value("exit_dark_tile_ratio",cgate.exit_dark_tile_ratio);
cgate.startup_frames=gate.value("startup_frames",cgate.startup_frames);
cgate.enter_frames=gate.value("enter_frames",cgate.enter_frames);
cgate.exit_frames=gate.value("exit_frames",cgate.exit_frames);
}
c.door_mask_calibration_verified=j.at("calibration_verified");
c.door_mask=directory_/"door_mask.json";
index_.open(directory_/"frames.txt");
+1 -1
View File
@@ -49,7 +49,7 @@ int main(int argc,char** argv)try {
if(visualize) {
auto display=std::getenv("DISPLAY");auto wayland=std::getenv("WAYLAND_DISPLAY");
if((!display||!*display)&&(!wayland||!*wayland))std::cerr<<"No display session; inference continues without visualization.\n";
else try {cms::runLocalVisualization(pipeline);}
else try {cms::runLocalVisualization(pipeline,!replaySession.empty());}
catch(std::exception const& e){std::cerr<<"Visualization unavailable; inference continues: "<<e.what()<<std::endl;}
}
pipeline.join();
+76 -11
View File
@@ -23,11 +23,11 @@ void segment(cv::Mat& image,cv::Point a,cv::Point b,cv::Scalar c,int thickness,b
}
}
void renderCanOverlay(cv::Mat& canvas,CanSnapshot const& can) {
cv::rectangle(canvas,{0,148},{canvas.cols,262},{20,24,30},-1);
cv::rectangle(canvas,{0,96},{canvas.cols,190},{20,24,30},-1);
std::ostringstream header;
header<<"CAN 0x391 ACTUAL POSITION | "<<can.source<<" "<<can.status<<" | RX "<<can.received
<<" TX "<<can.transmitted<<" | errors RX "<<can.receive_errors<<" TX "<<can.transmit_errors;
cv::putText(canvas,header.str(),{24,174},cv::FONT_HERSHEY_SIMPLEX,.65,{235,235,235},2,cv::LINE_AA);
cv::putText(canvas,header.str(),{24,119},cv::FONT_HERSHEY_SIMPLEX,.50,{235,235,235},1,cv::LINE_AA);
constexpr char const* labels[]={"LF","RF","LR","RR"};
auto channels=can.source=="replay"?can.channels.size():liveCanChannelCount;
for(std::size_t channel=0;channel<channels;++channel) {
@@ -38,10 +38,10 @@ void renderCanOverlay(cv::Mat& canvas,CanSnapshot const& can) {
}
if(s.received)line<<"age "<<std::fixed<<std::setprecision(0)<<s.age_ms<<" ms "<<(s.fd?"CANFD":"CAN")<<" #"<<s.received;
auto color=s.valid?cv::Scalar(80,220,90):cv::Scalar(0,190,255);
cv::putText(canvas,line.str(),{24,210+int(channel)*34},cv::FONT_HERSHEY_SIMPLEX,.72,color,2,cv::LINE_AA);
cv::putText(canvas,line.str(),{24,146+int(channel)*24},cv::FONT_HERSHEY_SIMPLEX,.52,color,1,cv::LINE_AA);
}
}
cv::Mat renderResult(FrameResult const& r,CanSnapshot const* liveCan) {
cv::Mat renderResult(FrameResult const& r,CanSnapshot const* liveCan,bool drawText) {
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)
@@ -61,8 +61,10 @@ cv::Mat renderResult(FrameResult const& r,CanSnapshot const* liveCan) {
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);
if(drawText) {
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);
@@ -83,18 +85,81 @@ cv::Mat renderResult(FrameResult const& r,CanSnapshot const* liveCan) {
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") {
if(drawText)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);
if(drawText) {
cv::rectangle(canvas,{0,0},{canvas.cols,94},{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);
cv::putText(canvas,headline.str(),{24,30},cv::FONT_HERSHEY_SIMPLEX,.72,color(r.risk.risk_level),1,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);
cv::putText(canvas,status.str(),{24,57},cv::FONT_HERSHEY_SIMPLEX,.52,{235,235,235},1,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);
cv::putText(canvas,calibration+" | q/Esc: close preview",{24,83},cv::FONT_HERSHEY_SIMPLEX,.50,{0,190,255},1,cv::LINE_AA);
renderCanOverlay(canvas,liveCan?*liveCan:r.can);
auto const& gate=r.brightness;
cv::Scalar gateColor=gate.state=="NORMAL"?cv::Scalar(80,220,90):cv::Scalar(0,190,255);
cv::rectangle(canvas,{0,191},{canvas.cols,279},{20,24,30},-1);
std::ostringstream metrics;
metrics<<"BRIGHTNESS "<<gate.state<<" | median "<<std::fixed<<std::setprecision(1)<<gate.median_luma
<<" | p75 "<<gate.p75_luma<<" | dark<"<<std::setprecision(0)<<gate.dark_pixel_luma
<<" "<<std::setprecision(1)<<gate.dark_ratio*100.0<<"%"
<<" | dark tiles "<<gate.dark_tile_ratio*100.0<<"%";
cv::putText(canvas,metrics.str(),{24,220},cv::FONT_HERSHEY_SIMPLEX,.58,gateColor,1,cv::LINE_AA);
std::ostringstream detail;
detail<<gate.reason<<" | enter "<<gate.dark_streak<<"/"<<gate.enter_frames
<<" | recovery "<<gate.bright_streak<<"/"<<gate.exit_frames;
if(!gate.transition.empty())detail<<" | "<<gate.transition;
if(!gate.allow_downstream)detail<<" | RISK INVALID";
cv::putText(canvas,detail.str(),{24,250},cv::FONT_HERSHEY_SIMPLEX,.50,{225,225,225},1,cv::LINE_AA);
}
return canvas;
}
cv::Mat renderVisualization(FrameResult const& r,CanSnapshot const& can) {
constexpr int width=1000,headerHeight=150,imageHeight=650,canvasHeight=800;
cv::Mat output(canvasHeight,width,CV_8UC3,cv::Scalar(20,24,30));
cv::rectangle(output,{0,0},{width,headerHeight},{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<<" | frame "<<r.frame_index<<" | latency "
<<std::fixed<<std::setprecision(1)<<r.timing.capture_to_result_ms<<" ms";
cv::putText(output,headline.str(),{14,24},cv::FONT_HERSHEY_SIMPLEX,.55,color(r.risk.risk_level),1,cv::LINE_AA);
std::string calibration=r.door_mask_calibration_verified?"door calibration verified":"door calibration NOT verified (bring-up)";
cv::putText(output,calibration,{14,45},cv::FONT_HERSHEY_SIMPLEX,.40,{0,190,255},1,cv::LINE_AA);
std::ostringstream canStatus;canStatus<<"CAN "<<can.status<<" | RX "<<can.received<<" TX "<<can.transmitted
<<" | errors RX "<<can.receive_errors<<" TX "<<can.transmit_errors;
cv::putText(output,canStatus.str(),{14,67},cv::FONT_HERSHEY_SIMPLEX,.40,{220,220,220},1,cv::LINE_AA);
constexpr char const* labels[]={"LF","RF","LR","RR"};
auto channels=can.source=="replay"?can.channels.size():liveCanChannelCount;
std::ostringstream positions;
for(std::size_t ch=0;ch<channels&&ch<2;++ch) {
if(ch)positions<<" | ";
positions<<"CH"<<ch<<" ";
for(std::size_t i=0;i<4;++i){positions<<labels[i]<<":";if(can.channels[ch].positions[i]<0)positions<<"--";else positions<<can.channels[ch].positions[i];if(i<3)positions<<" ";}
}
cv::putText(output,positions.str(),{14,88},cv::FONT_HERSHEY_SIMPLEX,.39,{180,210,180},1,cv::LINE_AA);
auto const& gate=r.brightness;
cv::Scalar gateColor=gate.state=="NORMAL"?cv::Scalar(80,220,90):cv::Scalar(0,190,255);
std::ostringstream metrics;
metrics<<"BRIGHTNESS "<<gate.state<<" | median "<<std::fixed<<std::setprecision(1)<<gate.median_luma
<<" | p75 "<<gate.p75_luma<<" | dark<"<<std::setprecision(0)<<gate.dark_pixel_luma
<<" "<<std::setprecision(1)<<gate.dark_ratio*100.0<<"% | dark tiles "<<gate.dark_tile_ratio*100.0<<"%";
cv::putText(output,metrics.str(),{14,112},cv::FONT_HERSHEY_SIMPLEX,.43,gateColor,1,cv::LINE_AA);
std::ostringstream detail;
detail<<gate.reason<<" | enter "<<gate.dark_streak<<"/"<<gate.enter_frames
<<" | recovery "<<gate.bright_streak<<"/"<<gate.exit_frames;
if(!gate.transition.empty())detail<<" | "<<gate.transition;
if(!gate.allow_downstream)detail<<" | RISK INVALID";
cv::putText(output,detail.str(),{14,136},cv::FONT_HERSHEY_SIMPLEX,.40,{225,225,225},1,cv::LINE_AA);
cv::Mat frame=renderResult(r,&can,false),scaled;
double scale=std::min(double(width)/frame.cols,double(imageHeight)/frame.rows);
cv::Size size(std::max(1,int(std::lround(frame.cols*scale))),std::max(1,int(std::lround(frame.rows*scale))));
cv::resize(frame,scaled,size,0,0,cv::INTER_AREA);
cv::Rect destination((width-size.width)/2,headerHeight+(imageHeight-size.height)/2,size.width,size.height);
scaled.copyTo(output(destination));
return output;
}
}
+11
View File
@@ -63,6 +63,17 @@ Json serializeResult(FrameResult const& r) {
}
}
out["can"]=serializeCan(r.can);
out["vision_enabled"]=r.brightness.allow_downstream;
out["image_quality"]=r.brightness.state;
out["risk_valid"]=r.brightness.allow_downstream;
out["brightness"]={{"state",r.brightness.state},{"reason",r.brightness.reason},
{"transition",r.brightness.transition.empty()?Json(nullptr):Json(r.brightness.transition)},
{"allow_downstream",r.brightness.allow_downstream},{"median_luma",r.brightness.median_luma},
{"p75_luma",r.brightness.p75_luma},{"mean_luma",r.brightness.mean_luma},
{"dark_pixel_luma",r.brightness.dark_pixel_luma},
{"dark_ratio",r.brightness.dark_ratio},{"dark_tile_ratio",r.brightness.dark_tile_ratio},
{"dark_streak",r.brightness.dark_streak},{"bright_streak",r.brightness.bright_streak},
{"enter_frames",r.brightness.enter_frames},{"exit_frames",r.brightness.exit_frames}};
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;
+7 -4
View File
@@ -6,23 +6,26 @@
#include <chrono>
namespace cms {
namespace { constexpr char windowName[]="CMS Camera Risk"; }
void runLocalVisualization(CameraPipeline const& pipeline) {
void runLocalVisualization(CameraPipeline const& pipeline,bool keepFinalFrame) {
struct Window {
Window(){cv::namedWindow(windowName,cv::WINDOW_NORMAL);cv::resizeWindow(windowName,1000,800);}
~Window(){try{if(cvGetWindowHandle(windowName))cv::destroyWindow(windowName);}catch(...) {}}
} window;
FrameResultPtr shown;
auto nextRefresh=std::chrono::steady_clock::now();
while(pipeline.running()) {
for(;;) {
bool running=pipeline.running();
if(!running&&!keepFinalFrame)break;
auto result=pipeline.latest();
if(!running&&!result)break;
auto now=std::chrono::steady_clock::now();
if(result!=shown||now>=nextRefresh) {
auto can=pipeline.canStatus();cv::Mat image;
if(result)image=renderResult(*result,&can);
if(result)image=renderVisualization(*result,can);
else {
image=cv::Mat(800,1000,CV_8UC3,cv::Scalar(20,24,30));
cv::putText(image,"Waiting for inference...",{24,64},cv::FONT_HERSHEY_SIMPLEX,.8,{235,235,235},2);
renderCanOverlay(image,can);
cv::putText(image,can.status,{24,120},cv::FONT_HERSHEY_SIMPLEX,.6,{235,235,235},1);
}
cv::imshow(windowName,image);shown=std::move(result);
nextRefresh=now+std::chrono::milliseconds(100);