增加劳保鞋检测节点

This commit is contained in:
haotian 2026-02-28 20:42:23 +08:00
parent 895dc2709a
commit 88b439558a
13 changed files with 933 additions and 145 deletions

View File

@ -47,49 +47,14 @@
"model_w": 768,
"model_h": 768,
"num_classes": 11,
"conf": 0.35,
"conf": 0.25,
"nms": 0.45,
"debug": {
"stats": true,
"stats_interval": 30,
"detections": true
},
"class_filter": [3, 6]
},
{
"id": "face_det_cam1",
"type": "ai_face_det",
"role": "filter",
"enable": true,
"model_path": "./models/RetinaFace_mobile320.rknn",
"conf": 0.7,
"nms": 0.4,
"max_faces": 10,
"output_landmarks": true,
"input_format": "rgb"
},
{
"id": "face_recog_cam1",
"type": "ai_face_recog",
"role": "filter",
"enable": true,
"model_path": "./models/mobilefacenet_arcface.rknn",
"align": true,
"emit_embedding": false,
"max_faces": 10,
"input_format": "rgb",
"input_dtype": "uint8",
"threshold": {
"accept": 0.45,
"margin": 0.05
},
"gallery": {
"backend": "sqlite",
"path": "./models/face_gallery.db",
"load_on_start": true,
"expected_dim": 512,
"dtype": "auto"
}
"class_filter": [3, 6, 10]
},
{
"id": "trk_cam1",
@ -99,7 +64,7 @@
"mode": "bytetrack_lite",
"per_class": true,
"state_key": "cam_ppe11_detection",
"track_classes": [3, 6],
"track_classes": [3, 6, 10],
"ignore_classes": [],
"allowed_models": ["yolov8"],
"high_th": 0.5,
@ -110,17 +75,20 @@
"max_tracks": 128
},
{
"id": "pre_face_cam1",
"type": "preprocess",
"id": "logic_cam1",
"type": "logic_gate",
"role": "filter",
"enable": true,
"dst_w": 0,
"dst_h": 0,
"dst_format": "rgb",
"dst_packed": true,
"keep_ratio": false,
"rga_gate": "cam_ppe11_detection",
"use_rga": true
"mode": "ppe_boots_check",
"anchor_class": 6,
"boots_class": 3,
"color_check": {
"enable": true,
"method": "hsv",
"dark_threshold": 80,
"roi_expand": 1.0
},
"debug": true
},
{
"id": "osd_cam1",
@ -134,7 +102,7 @@
"line_width": 2,
"font_scale": 1,
"use_rga_bbox": false,
"labels": ["helmet", "gloves", "vest", "boots", "goggles", "none", "Person", "no_helmet", "no_goggle", "no_gloves", "no_boots"]
"labels": ["helmet", "gloves", "vest", "boots", "goggles", "none", "Person", "no_helmet", "no_goggle", "no_gloves", "no_boots", "violation"]
},
{
"id": "post_cam1",
@ -153,6 +121,7 @@
"type": "publish",
"role": "filter",
"enable": true,
"queue": {"size": 2, "policy": "drop_oldest"},
"codec": "h264",
"fps": 30,
"gop": 60,
@ -177,29 +146,23 @@
"type": "alarm",
"role": "sink",
"enable": true,
"eval_fps": 10,
"labels": ["helmet", "gloves", "vest", "boots", "goggles", "none", "Person", "no_helmet", "no_goggle", "no_gloves", "no_boots"],
"eval_fps": 0,
"labels": ["helmet", "gloves", "vest", "boots", "goggles", "none", "Person", "no_helmet", "no_goggle", "no_gloves", "no_boots", "violation"],
"rules": [
{
"name": "ppe_violation",
"class_ids": [3, 6],
"roi": {
"x": 0.0,
"y": 0.0,
"w": 1.0,
"h": 1.0
},
"min_score": 0.4,
"min_box_area_ratio": 0.02,
"require_track_id": true,
"min_duration_ms": 1500,
"min_hits": 3,
"hit_window_ms": 1500,
"name": "non_compliant_boots",
"class_ids": [10],
"roi": {"x": 0.0, "y": 0.0, "w": 1.0, "h": 1.0},
"min_score": 0.1,
"min_box_area_ratio": 0.005,
"require_track_id": false,
"min_duration_ms": 500,
"min_hits": 1,
"hit_window_ms": 500,
"cooldown_ms": 5000,
"per_track_cooldown_ms": 5000
}
],
"face_rules": [],
"actions": {
"log": {
"enable": true,
@ -233,19 +196,12 @@
"secret_key": "minioadmin"
}
},
"http": {
"enable": false,
"url": "http://127.0.0.1:8080/api/alarm",
"timeout_ms": 3000,
"include_media_url": true,
"method": "POST"
},
"external_api": {
"enable": true,
"getTokenUrl": "http://10.0.0.49:8080/api/getToken",
"putMessageUrl": "http://10.0.0.49:8080/api/putMessage",
"tenantCode": "32",
"channelNo": "${vod_channelNo}",
"channelNo": "cam1",
"timeout_ms": 3000,
"include_media_url": true,
"token_header": "X-Access-Token",
@ -253,82 +209,17 @@
"token_cache_sec": 1200
}
}
},
{
"id": "alarm_face_cam1",
"type": "alarm",
"role": "sink",
"enable": true,
"eval_fps": 5,
"labels": [],
"rules": [],
"face_rules": [
{
"name": "unknown_face",
"type": "unknown",
"cooldown_ms": 7000,
"min_sim": 0.35,
"min_hits": 2,
"hit_window_ms": 1500,
"min_face_area_ratio": 0.01,
"min_face_aspect": 0.6,
"max_face_aspect": 1.6
},
{
"name": "known_person",
"type": "person",
"cooldown_ms": 7000,
"min_sim": 0.6,
"min_hits": 2,
"hit_window_ms": 1500,
"min_face_area_ratio": 0.01,
"min_face_aspect": 0.6,
"max_face_aspect": 1.6
}
],
"actions": {
"log": {
"enable": false,
"level": "info"
},
"snapshot": {
"enable": true,
"format": "jpg",
"quality": 85,
"upload": {
"type": "minio",
"endpoint": "http://10.0.0.49:9000",
"bucket": "myminio",
"region": "us-east-1",
"access_key": "minioadmin",
"secret_key": "minioadmin"
}
},
"clip": {
"enable": false
},
"http": {
"enable": true,
"url": "http://127.0.0.1:8080/api/alarm",
"timeout_ms": 3000,
"include_media_url": true,
"method": "POST"
}
}
}
],
"edges": [
["in_cam1", "pre_cam1"],
["in_cam1", "pre_face_cam1"],
["pre_cam1", "yolo_cam1"],
["yolo_cam1", "trk_cam1"],
["trk_cam1", "osd_cam1"],
["trk_cam1", "logic_cam1"],
["logic_cam1", "osd_cam1"],
["osd_cam1", "post_cam1"],
["post_cam1", "pub_cam1"],
["pub_cam1", "alarm_cam1"],
["pre_face_cam1", "face_det_cam1"],
["face_det_cam1", "face_recog_cam1"],
["face_recog_cam1", "alarm_face_cam1"]
["pub_cam1", "alarm_cam1"]
]
}
]

View File

@ -0,0 +1,120 @@
{
"queue": {
"size": 8,
"strategy": "drop_oldest"
},
"graphs": [
{
"name": "ppe_logic_gate_demo",
"nodes": [
{
"id": "in",
"type": "input_rtsp",
"role": "source",
"enable": true,
"url": "rtsp://10.0.0.49:8554/cam",
"fps": 30,
"width": 1280,
"height": 720,
"use_mpp": true,
"force_tcp": true
},
{
"id": "pre",
"type": "preprocess",
"role": "filter",
"enable": true,
"dst_w": 768,
"dst_h": 768,
"dst_format": "rgb",
"resize_mode": "stretch",
"use_rga": true
},
{
"id": "yolo",
"type": "ai_yolo",
"role": "filter",
"enable": true,
"infer_fps": 10,
"model_path": "./models/best-768.rknn",
"model_version": "v8",
"model_w": 768,
"model_h": 768,
"num_classes": 11,
"conf": 0.35,
"nms": 0.45,
"class_filter": [3, 6]
},
{
"id": "trk",
"type": "tracker",
"role": "filter",
"enable": true,
"mode": "bytetrack_lite",
"per_class": true,
"track_classes": [3, 6],
"max_age_ms": 1500,
"min_hits": 2
},
{
"id": "logic",
"type": "logic_gate",
"role": "filter",
"enable": true,
"mode": "ppe_boots_check",
"anchor_class": 6,
"boots_class": 3,
"color_check": {
"enable": true,
"method": "hsv",
"dark_threshold": 80,
"roi_expand": 1.0
},
"debug": true
},
{
"id": "osd",
"type": "osd",
"role": "filter",
"enable": true,
"draw_bbox": true,
"draw_text": true,
"labels": ["helmet", "gloves", "vest", "boots", "goggles", "none", "Person", "no_helmet", "no_goggle", "no_gloves", "no_boots"]
},
{
"id": "post",
"type": "preprocess",
"role": "filter",
"enable": true,
"dst_w": 1280,
"dst_h": 720,
"dst_format": "nv12",
"resize_mode": "stretch",
"use_rga": true
},
{
"id": "pub",
"type": "publish",
"role": "filter",
"enable": true,
"codec": "h264",
"fps": 30,
"bitrate_kbps": 2000,
"use_mpp": true,
"outputs": [
{"proto": "rtsp_server", "port": 8555, "path": "/live/cam1"}
]
}
],
"edges": [
["in", "pre"],
["pre", "yolo"],
["yolo", "trk"],
["trk", "logic"],
["logic", "osd"],
["osd", "post"],
["post", "pub"]
]
}
]
}

View File

@ -361,6 +361,55 @@
}
```
#### logic_gate逻辑门 - PPE合规检测
用于空间关联检测和颜色分析,典型场景:检测人是否穿劳保鞋及颜色是否合规。
```json
{
"id": "logic_ppe",
"type": "logic_gate",
"role": "filter",
"enable": true,
"mode": "ppe_boots_check", // 工作模式
"anchor_class": 6, // 基准类别:人(Person)
"boots_class": 3, // 目标类别:鞋(boots)
"color_check": {
"enable": true, // 启用颜色检测
"method": "hsv", // 颜色空间hsv/rgb/brightness
"dark_threshold": 80, // 深色阈值0-255
"roi_expand": 1.0 // 检测框扩大系数
},
"debug": false // 调试输出
}
```
**颜色阈值调整建议:**
| 阈值 | 效果 | 适用场景 |
|------|------|----------|
| **60** | 很严格,只有纯黑才算深色 | 环境光很暗 |
| **80** (默认) | 适中,黑色/深灰通过 | 正常室内光 |
| **100** | 较宽松,灰色也算深色 | 环境光很亮 |
| **120** | 很宽松,浅灰也算深色 | 强光/反光环境 |
**判断逻辑:**
```
is_dark = (平均亮度 < dark_threshold) && (深色像素比例 > 60%)
```
**使用示例流程:**
```
input → preprocess → ai_yolo(Person+boots) → tracker → logic_gate → osd → publish
判断:人+鞋同时存在?
是 → 检查鞋颜色
深色 → 合规
浅色 → 告警
```
---
## 4. 连接关系 (edges)

View File

@ -407,6 +407,20 @@ set_target_properties(alarm PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${RK_PLUGIN_OUTPUT_DIR}
)
# logic_gate plugin (PPE compliance logic with spatial and color analysis)
add_library(logic_gate SHARED
logic_gate/logic_gate_node.cpp
logic_gate/spatial_matcher.cpp
logic_gate/color_analyzer.cpp
)
target_include_directories(logic_gate PRIVATE ${CMAKE_SOURCE_DIR}/include ${CMAKE_SOURCE_DIR}/third_party)
target_link_libraries(logic_gate PRIVATE project_options Threads::Threads)
set_target_properties(logic_gate PROPERTIES
OUTPUT_NAME "logic_gate"
LIBRARY_OUTPUT_DIRECTORY ${RK_PLUGIN_OUTPUT_DIR}
RUNTIME_OUTPUT_DIRECTORY ${RK_PLUGIN_OUTPUT_DIR}
)
# storage plugin (continuous recording with segment management)
add_library(storage SHARED
storage/storage_node.cpp
@ -461,7 +475,7 @@ if(RK3588_ENABLE_ZLMEDIAKIT AND RK_ZLMK_API_LIB)
)
endif()
install(TARGETS input_rtsp input_file publish preprocess ai_yolo ai_face_det ai_face_recog tracker gate osd alarm storage ai_scheduler
install(TARGETS input_rtsp input_file publish preprocess ai_yolo ai_face_det ai_face_recog tracker gate osd alarm logic_gate storage ai_scheduler
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/rk3588-media-server/plugins
RUNTIME DESTINATION ${CMAKE_INSTALL_LIBDIR}/rk3588-media-server/plugins
)

View File

@ -422,6 +422,18 @@ public:
{
std::lock_guard<std::mutex> lock(mu_);
// 调试:检查 frame 和 det
if (frame->det) {
int no_boots_count = 0;
for (const auto& d : frame->det->items) {
if (d.cls_id == 10) no_boots_count++;
}
if (no_boots_count > 0 || processed_frames_ % 30 == 0) {
LogInfo("[alarm] frame received, dets=" + std::to_string(frame->det->items.size()) +
" no_boots=" + std::to_string(no_boots_count));
}
}
if (eval_interval_ms_ > 0 && frame->pts > 0) {
const int64_t pts_ms = static_cast<int64_t>(frame->pts / 1000ULL);
if (last_eval_pts_ms_ > 0 && (pts_ms - last_eval_pts_ms_) < eval_interval_ms_) {
@ -432,7 +444,10 @@ public:
}
auto result = rule_engine_.Evaluate(frame);
if (result.matched) TriggerAlarm(result, frame);
if (result.matched) {
LogInfo("[alarm] RULE MATCHED: " + result.rule_name);
TriggerAlarm(result, frame);
}
EvaluateFaceRulesLocked(frame);
++processed_frames_;

View File

@ -0,0 +1,239 @@
#include "color_analyzer.h"
#include "utils/logger.h"
#include <algorithm>
#include <cmath>
#include <cstring>
#include <fstream>
namespace rk3588 {
ColorAnalyzer::ColorAnalyzer(const ColorConfig& config) : config_(config) {}
ColorResult ColorAnalyzer::Analyze(const Frame& frame, const Rect& bbox) {
ColorResult result;
// 扩展检测框
Rect expanded = bbox;
float cx = bbox.x + bbox.w * 0.5f;
float cy = bbox.y + bbox.h * 0.5f;
expanded.w = bbox.w * config_.roi_expand;
expanded.h = bbox.h * config_.roi_expand;
expanded.x = cx - expanded.w * 0.5f;
expanded.y = cy - expanded.h * 0.5f;
// 裁剪区域
int crop_w = 0, crop_h = 0;
auto cropped = CropRegion(frame, expanded, crop_w, crop_h);
if (cropped.empty() || crop_w == 0 || crop_h == 0) {
LogWarn("[ColorAnalyzer] Failed to crop region, fmt=" +
std::to_string(static_cast<int>(frame.format)) +
" w=" + std::to_string(frame.width) +
" h=" + std::to_string(frame.height));
return result;
}
// 保存调试图像(用于检查裁剪区域)
if (config_.debug_output) {
static int save_count = 0;
if (save_count++ % 30 == 0) { // 每30帧保存一次
SaveDebugImage(cropped.data(), crop_w, crop_h, "/tmp/boot_debug_" + std::to_string(save_count) + ".ppm");
LogInfo("[ColorAnalyzer] Saved debug image: /tmp/boot_debug_" + std::to_string(save_count) + ".ppm");
}
}
// 根据方法进行分析
switch (config_.method) {
case ColorMethod::HSV:
result = AnalyzeHSV(cropped.data(), crop_w, crop_h, 3);
break;
case ColorMethod::RGB:
result = AnalyzeRGB(cropped.data(), crop_w, crop_h, 3);
break;
case ColorMethod::BRIGHTNESS:
result = AnalyzeBrightness(cropped.data(), crop_w, crop_h, 3);
break;
default:
result = AnalyzeHSV(cropped.data(), crop_w, crop_h, 3);
break;
}
// 判断是否为劳保鞋颜色
result.is_dark = IsSafetyBootsColor(result);
if (config_.debug_output) {
LogInfo("[ColorAnalyzer] Brightness=" + std::to_string(result.brightness) +
" is_dark=" + (result.is_dark ? "true" : "false") +
" dark_ratio=" + std::to_string(result.dark_ratio) +
" region=" + std::to_string(static_cast<int>(expanded.x)) + "," +
std::to_string(static_cast<int>(expanded.y)) + "," +
std::to_string(static_cast<int>(expanded.w)) + "x" +
std::to_string(static_cast<int>(expanded.h)));
}
return result;
}
void ColorAnalyzer::SaveDebugImage(const uint8_t* data, int w, int h, const std::string& filename) {
// 保存为 PPM 格式 (简单文本格式)
std::ofstream file(filename, std::ios::binary);
if (!file) return;
file << "P6\n" << w << " " << h << "\n255\n";
for (int i = 0; i < w * h * 3; i++) {
file.put(data[i]);
}
file.close();
}
bool ColorAnalyzer::IsSafetyBootsColor(const ColorResult& result) {
// 判断标准:亮度低于阈值,且深色像素比例高
bool brightness_ok = result.brightness < config_.dark_threshold;
bool ratio_ok = result.dark_ratio > 0.6f; // 60%以上像素为深色
return brightness_ok && ratio_ok;
}
std::vector<uint8_t> ColorAnalyzer::CropRegion(const Frame& frame, const Rect& bbox,
int& out_w, int& out_h) {
std::vector<uint8_t> result;
// 检查格式 - 只支持RGB24
if (frame.format != PixelFormat::RGB && frame.format != PixelFormat::BGR) {
LogWarn("[ColorAnalyzer] Unsupported pixel format: " + std::to_string(static_cast<int>(frame.format)) +
", expected RGB24/BGR24");
return result;
}
// 确保框在有效范围内
int x = static_cast<int>(std::max(0.0f, bbox.x));
int y = static_cast<int>(std::max(0.0f, bbox.y));
int w = static_cast<int>(bbox.w);
int h = static_cast<int>(bbox.h);
x = std::min(x, frame.width - 1);
y = std::min(y, frame.height - 1);
w = std::min(w, frame.width - x);
h = std::min(h, frame.height - y);
if (w <= 0 || h <= 0) {
return result;
}
out_w = w;
out_h = h;
// RGB格式
int channels = 3;
result.resize(w * h * channels);
// 裁剪(逐行拷贝)
for (int row = 0; row < h; ++row) {
const uint8_t* src_row = frame.data + ((y + row) * frame.width + x) * channels;
uint8_t* dst_row = result.data() + row * w * channels;
std::memcpy(dst_row, src_row, w * channels);
}
return result;
}
ColorResult ColorAnalyzer::AnalyzeHSV(const uint8_t* data, int w, int h, int channels) {
ColorResult result;
long long total_v = 0;
long long total_s = 0;
int dark_pixels = 0;
int total_pixels = w * h;
for (int i = 0; i < total_pixels; ++i) {
uint8_t r = data[i * channels];
uint8_t g = data[i * channels + 1];
uint8_t b = data[i * channels + 2];
float hsv_h, hsv_s, hsv_v;
RGBtoHSV(r, g, b, hsv_h, hsv_s, hsv_v);
total_v += static_cast<int>(hsv_v * 255);
total_s += static_cast<int>(hsv_s * 255);
// V < 0.3 认为是深色
if (hsv_v < 0.3f) {
dark_pixels++;
}
}
result.avg_v = total_v / static_cast<float>(total_pixels);
result.avg_s = total_s / static_cast<float>(total_pixels);
result.brightness = result.avg_v; // V通道作为亮度
result.dark_ratio = dark_pixels / static_cast<float>(total_pixels);
result.light_ratio = 1.0f - result.dark_ratio;
result.confidence = result.dark_ratio;
return result;
}
ColorResult ColorAnalyzer::AnalyzeRGB(const uint8_t* data, int w, int h, int channels) {
ColorResult result;
long long total_brightness = 0;
int dark_pixels = 0;
int total_pixels = w * h;
for (int i = 0; i < total_pixels; ++i) {
uint8_t r = data[i * channels];
uint8_t g = data[i * channels + 1];
uint8_t b = data[i * channels + 2];
// 简单亮度公式
int brightness = (r * 299 + g * 587 + b * 114) / 1000;
total_brightness += brightness;
if (brightness < config_.dark_threshold) {
dark_pixels++;
}
}
result.brightness = total_brightness / static_cast<float>(total_pixels);
result.dark_ratio = dark_pixels / static_cast<float>(total_pixels);
result.light_ratio = 1.0f - result.dark_ratio;
result.confidence = result.dark_ratio;
return result;
}
ColorResult ColorAnalyzer::AnalyzeBrightness(const uint8_t* data, int w, int h, int channels) {
// 与RGB方法类似但只关注亮度
return AnalyzeRGB(data, w, h, channels);
}
void ColorAnalyzer::RGBtoHSV(uint8_t r, uint8_t g, uint8_t b, float& h, float& s, float& v) {
float rf = r / 255.0f;
float gf = g / 255.0f;
float bf = b / 255.0f;
float max_val = std::max({rf, gf, bf});
float min_val = std::min({rf, gf, bf});
float delta = max_val - min_val;
// V
v = max_val;
// S
s = (max_val > 0.0f) ? (delta / max_val) : 0.0f;
// H
if (delta > 0.0f) {
if (max_val == rf) {
h = 60.0f * (fmod(((gf - bf) / delta), 6.0f));
} else if (max_val == gf) {
h = 60.0f * (((bf - rf) / delta) + 2.0f);
} else {
h = 60.0f * (((rf - gf) / delta) + 4.0f);
}
if (h < 0.0f) h += 360.0f;
} else {
h = 0.0f;
}
}
} // namespace rk3588

View File

@ -0,0 +1,71 @@
#pragma once
#include "frame/frame.h"
namespace rk3588 {
// 颜色分析方法
enum class ColorMethod {
HSV, // HSV颜色空间
RGB, // RGB颜色空间
BRIGHTNESS, // 亮度(灰度)
LAB // LAB颜色空间更准的亮度
};
// 颜色分析配置
struct ColorConfig {
ColorMethod method = ColorMethod::HSV;
int dark_threshold = 80; // 深色阈值亮度0-255
float roi_expand = 1.0f; // ROI扩大系数
bool debug_output = false; // 是否输出调试信息
};
// 颜色分析结果
struct ColorResult {
bool is_dark = false; // 是否为深色
float brightness = 0.0f; // 平均亮度0-255
float confidence = 0.0f; // 置信度
// HSV统计
float avg_h = 0.0f;
float avg_s = 0.0f;
float avg_v = 0.0f;
// 颜色分布
float dark_ratio = 0.0f; // 深色像素比例
float light_ratio = 0.0f; // 浅色像素比例
};
class ColorAnalyzer {
public:
ColorAnalyzer(const ColorConfig& config);
// 分析检测框内的颜色
ColorResult Analyze(const Frame& frame, const Rect& bbox);
// 判断是否为劳保鞋颜色(深色)
bool IsSafetyBootsColor(const ColorResult& result);
// 保存调试图像
void SaveDebugImage(const uint8_t* data, int w, int h, const std::string& filename);
private:
ColorConfig config_;
// 裁剪图像区域
std::vector<uint8_t> CropRegion(const Frame& frame, const Rect& bbox, int& out_w, int& out_h);
// HSV分析方法
ColorResult AnalyzeHSV(const uint8_t* data, int w, int h, int channels);
// RGB分析方法
ColorResult AnalyzeRGB(const uint8_t* data, int w, int h, int channels);
// 亮度分析方法
ColorResult AnalyzeBrightness(const uint8_t* data, int w, int h, int channels);
// RGB转HSV
void RGBtoHSV(uint8_t r, uint8_t g, uint8_t b, float& h, float& s, float& v);
};
} // namespace rk3588

View File

@ -0,0 +1,192 @@
#include <map>
#include <vector>
#include <memory>
#include <chrono>
#include <algorithm>
#include <cstring>
#include "node.h"
#include "frame/frame.h"
#include "utils/logger.h"
#include "utils/simple_json.h"
#include "spatial_matcher.h"
#include "color_analyzer.h"
namespace rk3588 {
// 违规类型
enum class ViolationType {
NONE,
MISSING_BOOTS, // 没有鞋
WRONG_COLOR_BOOTS, // 鞋颜色不对
};
// 违规信息
struct ViolationInfo {
ViolationType type = ViolationType::NONE;
std::string description;
int track_id = -1;
float confidence = 0.0f;
Rect violation_region;
};
// Logic Gate 配置
struct LogicGateConfig {
std::string mode = "ppe_boots_check";
int anchor_class = 6; // Person
int boots_class = 3; // boots
ColorConfig color;
bool enable_color_check = true;
std::string violation_key = "ppe_violation";
bool pass_through = true;
bool debug = false;
};
class LogicGateNode : public INode {
public:
LogicGateNode() = default;
~LogicGateNode() override = default;
std::string Id() const override { return id_; }
std::string Type() const override { return "logic_gate"; }
bool Init(const SimpleJson& config, const NodeContext& ctx) override {
id_ = config.ValueOr<std::string>("id", "logic_gate");
config_ = ParseConfig(config);
output_queues_ = ctx.output_queues;
// 初始化颜色分析器
if (config_.enable_color_check) {
color_analyzer_ = std::make_unique<ColorAnalyzer>(config_.color);
}
LogInfo("[LogicGateNode] Initialized, id=" + id_ + " mode=" + config_.mode +
" output_queues=" + std::to_string(output_queues_.size()));
return true;
}
bool Start() override {
LogInfo("[LogicGateNode] started id=" + id_);
return true;
}
void Stop() override {
}
NodeStatus Process(FramePtr frame) override {
if (!frame || !frame->det) {
PushToDownstream(frame);
return NodeStatus::OK;
}
if (config_.mode == "ppe_boots_check") {
ProcessPpeBootsCheck(frame);
}
PushToDownstream(frame);
return NodeStatus::OK;
}
private:
std::string id_;
LogicGateConfig config_;
std::unique_ptr<ColorAnalyzer> color_analyzer_;
std::vector<std::shared_ptr<SpscQueue<FramePtr>>> output_queues_;
LogicGateConfig ParseConfig(const SimpleJson& cfg) {
LogicGateConfig config;
config.mode = cfg.ValueOr<std::string>("mode", "ppe_boots_check");
config.anchor_class = cfg.ValueOr<int>("anchor_class", 6);
config.boots_class = cfg.ValueOr<int>("boots_class", 3);
config.violation_key = cfg.ValueOr<std::string>("violation_key", "ppe_violation");
config.pass_through = cfg.ValueOr<bool>("pass_through", true);
config.debug = cfg.ValueOr<bool>("debug", false);
config.enable_color_check = cfg.ValueOr<bool>("enable_color_check", true);
// 解析颜色配置
if (const SimpleJson* color = cfg.Find("color_check")) {
std::string method = color->ValueOr<std::string>("method", "hsv");
if (method == "hsv") config.color.method = ColorMethod::HSV;
else if (method == "rgb") config.color.method = ColorMethod::RGB;
else if (method == "brightness") config.color.method = ColorMethod::BRIGHTNESS;
config.color.dark_threshold = color->ValueOr<int>("dark_threshold", 80);
config.color.roi_expand = color->ValueOr<float>("roi_expand", 1.0f);
config.color.debug_output = config.debug;
}
return config;
}
void PushToDownstream(FramePtr frame) {
for (auto& q : output_queues_) {
if (q) q->Push(frame);
}
}
void ProcessPpeBootsCheck(FramePtr frame) {
const auto& detections = frame->det->items;
// 收集所有人和鞋
std::vector<Detection> persons;
std::vector<Detection> boots;
for (const auto& det : detections) {
if (det.cls_id == config_.anchor_class) {
persons.push_back(det);
} else if (det.cls_id == config_.boots_class) {
boots.push_back(det);
}
}
if (config_.debug) {
LogInfo("[LogicGateNode] Persons=" + std::to_string(persons.size()) +
" Boots=" + std::to_string(boots.size()));
}
// 简化逻辑:必须同时检测到人和鞋,才开始判断
if (persons.empty() || boots.empty()) {
return;
}
std::vector<ViolationInfo> violations;
// 对每只鞋进行颜色检查
for (const auto& boot : boots) {
if (config_.enable_color_check && color_analyzer_) {
auto color_result = color_analyzer_->Analyze(*frame, boot.bbox);
if (config_.debug) {
LogInfo("[LogicGateNode] Boot brightness=" +
std::to_string(color_result.brightness) +
" is_dark=" + (color_result.is_dark ? "true" : "false"));
}
if (!color_result.is_dark) {
// 颜色不对,添加 no_boots 检测框
Detection no_boots_det;
no_boots_det.cls_id = 10; // no_boots
no_boots_det.track_id = std::max(0, boot.track_id);
no_boots_det.score = std::max(0.5f, color_result.confidence);
no_boots_det.bbox = boot.bbox;
frame->det->items.push_back(no_boots_det);
if (config_.debug) {
LogInfo("[LogicGateNode] VIOLATION: Non-compliant boots color (brightness=" +
std::to_string(static_cast<int>(color_result.brightness)) +
") added no_boots(cls=10) track_id=" + std::to_string(no_boots_det.track_id));
}
}
}
}
}
};
REGISTER_NODE(LogicGateNode, "logic_gate");
} // namespace rk3588

View File

@ -0,0 +1,106 @@
#include "spatial_matcher.h"
#include <algorithm>
#include <cmath>
namespace rk3588 {
SpatialMatcher::SpatialMatcher(const SpatialConfig& config) : config_(config) {}
SpatialMatchResult SpatialMatcher::Match(const Detection& anchor, const Detection& target,
int img_w, int img_h) {
SpatialMatchResult result;
// 根据关系类型进行匹配
switch (config_.relation) {
case SpatialRelation::BELOW: {
// 计算脚下区域
result.foot_region = CalculateFootRegion(anchor.bbox, img_w, img_h);
// 计算目标与脚下区域的IOU
result.iou = CalculateIoU(result.foot_region, target.bbox);
result.matched = (result.iou >= config_.iou_threshold);
break;
}
case SpatialRelation::INSIDE: {
result.iou = CalculateIoU(anchor.bbox, target.bbox);
result.matched = (result.iou >= config_.iou_threshold);
break;
}
case SpatialRelation::OVERLAP: {
result.iou = CalculateIoU(anchor.bbox, target.bbox);
result.matched = (result.iou > 0); // 只要有重叠就算匹配
break;
}
case SpatialRelation::NEAR: {
result.distance = CalculateDistance(anchor.bbox, target.bbox, img_w, img_h);
result.matched = (result.distance <= config_.max_distance);
break;
}
default:
break;
}
return result;
}
Rect SpatialMatcher::CalculateFootRegion(const Rect& person_bbox, int img_w, int img_h) {
// 脚下区域:人体框底部向下扩展 foot_ratio 倍高度
float foot_height = person_bbox.h * config_.foot_ratio;
Rect foot;
foot.x = person_bbox.x - person_bbox.w * (config_.expand_ratio - 1.0f) * 0.5f;
foot.y = person_bbox.y + person_bbox.h; // 从人体底部开始
foot.w = person_bbox.w * config_.expand_ratio;
foot.h = foot_height * config_.expand_ratio;
// 确保在图像范围内
foot.x = std::max(0.0f, foot.x);
foot.y = std::max(0.0f, foot.y);
foot.w = std::min(static_cast<float>(img_w) - foot.x, foot.w);
foot.h = std::min(static_cast<float>(img_h) - foot.y, foot.h);
return foot;
}
float SpatialMatcher::CalculateIoU(const Rect& a, const Rect& b) {
// 计算交集
float x1 = std::max(a.x, b.x);
float y1 = std::max(a.y, b.y);
float x2 = std::min(a.x + a.w, b.x + b.w);
float y2 = std::min(a.y + a.h, b.y + b.h);
if (x2 <= x1 || y2 <= y1) {
return 0.0f;
}
float intersection = (x2 - x1) * (y2 - y1);
float area_a = a.w * a.h;
float area_b = b.w * b.h;
float union_area = area_a + area_b - intersection;
return union_area > 0 ? intersection / union_area : 0.0f;
}
float SpatialMatcher::CalculateDistance(const Rect& anchor, const Rect& target, int img_w, int img_h) {
// 计算中心点距离(归一化)
float cx1 = (anchor.x + anchor.w * 0.5f) / img_w;
float cy1 = (anchor.y + anchor.h * 0.5f) / img_h;
float cx2 = (target.x + target.w * 0.5f) / img_w;
float cy2 = (target.y + target.h * 0.5f) / img_h;
float dx = cx1 - cx2;
float dy = cy1 - cy2;
return std::sqrt(dx * dx + dy * dy);
}
bool SpatialMatcher::IsBelow(const Rect& anchor, const Rect& target, int img_w, int img_h) {
// 简单判断:目标的顶部是否在锚点底部的下方
return target.y >= (anchor.y + anchor.h * 0.8f);
}
} // namespace rk3588

View File

@ -0,0 +1,58 @@
#pragma once
#include "frame/frame.h"
#include <vector>
namespace rk3588 {
// 空间关系类型
enum class SpatialRelation {
BELOW, // 目标在锚点下方
ABOVE, // 目标在锚点上方
INSIDE, // 目标在锚点内部
OVERLAP, // 目标与锚点重叠
NEAR // 目标在锚点附近
};
// 空间匹配配置
struct SpatialConfig {
SpatialRelation relation = SpatialRelation::BELOW;
float iou_threshold = 0.3f; // IOU阈值
float foot_ratio = 0.3f; // 脚下区域比例(人体高度)
float expand_ratio = 1.0f; // 框扩大系数
float max_distance = 0.5f; // 最大归一化距离NEAR模式用
};
// 匹配结果
struct SpatialMatchResult {
bool matched = false;
float iou = 0.0f;
float distance = 0.0f; // 归一化距离
Rect foot_region; // 脚下区域(用于可视化)
};
class SpatialMatcher {
public:
SpatialMatcher(const SpatialConfig& config);
// 检查目标是否在锚点的指定空间位置
SpatialMatchResult Match(const Detection& anchor, const Detection& target,
int img_w, int img_h);
// 计算脚下区域
Rect CalculateFootRegion(const Rect& person_bbox, int img_w, int img_h);
private:
SpatialConfig config_;
// 计算两个框的IOU
float CalculateIoU(const Rect& a, const Rect& b);
// 计算归一化中心点距离
float CalculateDistance(const Rect& anchor, const Rect& target, int img_w, int img_h);
// 检查目标是否在锚点下方
bool IsBelow(const Rect& anchor, const Rect& target, int img_w, int img_h);
};
} // namespace rk3588

View File

@ -393,6 +393,7 @@ public:
return false;
}
output_queues_ = ctx.output_queues;
LogInfo("[osd] Init: id=" + id_ + " output_queues=" + std::to_string(output_queues_.size()));
return true;
}

View File

@ -327,6 +327,10 @@ public:
void Stop() override {}
NodeStatus Process(FramePtr frame) override {
static int process_count = 0;
if (id_.find("post") != std::string::npos && process_count++ % 30 == 0) {
LogInfo("[preprocess] post_cam1 Process called, frame=" + std::to_string(frame ? 1 : 0));
}
if (!frame) return NodeStatus::DROP;
if (!image_processor_) return NodeStatus::ERROR;

View File

@ -637,6 +637,7 @@ public:
id_ = config.ValueOr<std::string>("id", "publish");
input_queue_ = ctx.input_queue;
output_queues_ = ctx.output_queues;
LogInfo("[publish] Init: id=" + id_ + " output_queues=" + std::to_string(output_queues_.size()));
codec_ = config.ValueOr<std::string>("codec", "h264");
fps_ = config.ValueOr<int>("fps", 25);
gop_ = config.ValueOr<int>("gop", 50);
@ -805,7 +806,14 @@ public:
}
NodeStatus Process(FramePtr frame) override {
if (!frame) return NodeStatus::DROP;
static int process_count = 0;
if (process_count++ % 30 == 0) {
LogInfo("[publish] Process called, frame=" + std::to_string(frame ? 1 : 0));
}
if (!frame) {
LogWarn("[publish] received null frame");
return NodeStatus::DROP;
}
{
std::lock_guard<std::mutex> lock(mu_);
@ -821,13 +829,33 @@ public:
#endif
}
static int frame_count = 0;
if (frame_count++ % 30 == 0) {
int no_boots = 0;
if (frame->det) {
for (const auto& d : frame->det->items) {
if (d.cls_id == 10) no_boots++;
}
}
LogInfo("[publish] pushing frame to downstream, dets=" +
std::to_string(frame->det ? frame->det->items.size() : 0) +
" no_boots=" + std::to_string(no_boots) +
" queues=" + std::to_string(output_queues_.size()));
}
PushToDownstream(frame);
return NodeStatus::OK;
}
private:
void PushToDownstream(const FramePtr& frame) {
if (output_queues_.empty()) return;
if (output_queues_.empty()) {
static int log_count = 0;
if (log_count++ % 100 == 0) {
LogInfo("[publish] no output queues");
}
return;
}
for (auto& q : output_queues_) {
if (q) q->Push(frame);
}