feat: consume behavior events in osd and alarm

This commit is contained in:
sladro 2026-04-01 10:35:45 +08:00
parent 0c98be39cd
commit 6e4bca7a05
9 changed files with 251 additions and 17 deletions

View File

@ -0,0 +1,38 @@
#pragma once
#include <algorithm>
#include <cstdio>
#include <string>
#include <vector>
#include "behavior/behavior_event.h"
namespace rk3588 {
inline const char* BehaviorEventTypeName(BehaviorEventType type) {
switch (type) {
case BehaviorEventType::Intrusion: return "intrusion";
case BehaviorEventType::Climb: return "climb";
case BehaviorEventType::Fall: return "fall";
case BehaviorEventType::Fight: return "fight";
}
return "unknown";
}
inline std::string FormatBehaviorEventLabel(const BehaviorEventItem& item) {
char duration_text[32];
std::snprintf(duration_text, sizeof(duration_text), "%.1fs",
static_cast<double>(item.duration_ms) / 1000.0);
std::string label = BehaviorEventTypeName(item.type);
if (!item.track_ids.empty()) {
std::vector<int> track_ids = item.track_ids;
std::sort(track_ids.begin(), track_ids.end());
label += " #" + std::to_string(track_ids.front());
}
label += " ";
label += duration_text;
return label;
}
} // namespace rk3588

View File

@ -15,6 +15,7 @@ struct AlarmEvent {
uint64_t timestamp_ms;
uint64_t frame_id;
std::vector<Detection> detections;
std::vector<BehaviorEventItem> behavior_events;
std::string snapshot_url;
std::string clip_url;
};

View File

@ -664,6 +664,7 @@ private:
std::chrono::system_clock::now().time_since_epoch()).count();
event.frame_id = frame->frame_id;
event.detections = result.matched_detections;
event.behavior_events = result.matched_behavior_events;
AlarmJob job;
job.event = std::move(event);

View File

@ -6,6 +6,7 @@
#include <mutex>
#include <sstream>
#include "behavior/behavior_event_format.h"
#include "utils/logger.h"
namespace rk3588 {
@ -46,6 +47,7 @@ bool RuleEngine::Init(const SimpleJson& rules_config, const std::vector<std::str
rule.min_score = rule_json.ValueOr<float>("min_score", 0.0f);
rule.min_box_area_ratio = rule_json.ValueOr<float>("min_box_area_ratio", 0.0f);
rule.require_track_id = rule_json.ValueOr<bool>("require_track_id", false);
rule.use_behavior_events = rule_json.ValueOr<bool>("use_behavior_events", false);
rule.min_duration_ms = rule_json.ValueOr<int>("min_duration_ms", 0);
rule.min_hits = rule_json.ValueOr<int>("min_hits", 1);
rule.hit_window_ms = rule_json.ValueOr<int>("hit_window_ms", 0);
@ -103,11 +105,16 @@ bool RuleEngine::Init(const SimpleJson& rules_config, const std::vector<std::str
RuleMatchResult RuleEngine::Evaluate(const std::shared_ptr<Frame>& frame) {
RuleMatchResult result;
if (!frame || !frame->det) return result;
if (!frame) return result;
const auto& detections = frame->det->items;
int img_w = frame->det->img_w > 0 ? frame->det->img_w : frame->width;
int img_h = frame->det->img_h > 0 ? frame->det->img_h : frame->height;
const std::vector<Detection>* detections = nullptr;
int img_w = frame->width;
int img_h = frame->height;
if (frame->det) {
detections = &frame->det->items;
img_w = frame->det->img_w > 0 ? frame->det->img_w : frame->width;
img_h = frame->det->img_h > 0 ? frame->det->img_h : frame->height;
}
for (auto& rule : rules_) {
// Check schedule
@ -123,24 +130,42 @@ RuleMatchResult RuleEngine::Evaluate(const std::shared_ptr<Frame>& frame) {
}
}
// Find matching detections
if (rule.use_behavior_events) {
std::vector<BehaviorEventItem> matched;
if (frame->behavior_events) {
for (const auto& event : frame->behavior_events->items) {
if (!MatchBehaviorEventType(rule, event)) continue;
if (!PassBehaviorQuality(rule, event)) continue;
matched.push_back(event);
}
}
if (!matched.empty()) {
result.matched = true;
result.rule_name = rule.name;
result.matched_behavior_events = std::move(matched);
TriggerCooldown(rule.name);
duration_start_.erase(rule.name);
return result;
}
duration_start_.erase(rule.name);
continue;
}
if (!detections) continue;
std::vector<Detection> matched;
for (const auto& det : detections) {
// Check class_id (if empty, match all)
for (const auto& det : *detections) {
if (!rule.class_ids.empty() &&
rule.class_ids.find(det.cls_id) == rule.class_ids.end()) {
continue;
}
// Check ROI
if (!IsInRoi(det.bbox, rule.roi, img_w, img_h)) {
continue;
}
if (!PassQuality(rule, det, static_cast<double>(img_w) * static_cast<double>(img_h))) {
continue;
}
matched.push_back(det);
}
@ -151,7 +176,6 @@ RuleMatchResult RuleEngine::Evaluate(const std::shared_ptr<Frame>& frame) {
std::vector<Detection> qualified;
qualified.reserve(matched.size());
const auto now = std::chrono::steady_clock::now();
for (const auto& det : matched) {
if (rule.per_track_cooldown_ms > 0) {
if (!CheckPerTrackCooldown(rule.name, det.track_id, rule.per_track_cooldown_ms)) {
@ -314,6 +338,20 @@ bool RuleEngine::PassQuality(const AlarmRule& rule, const Detection& det, double
return true;
}
bool RuleEngine::PassBehaviorQuality(const AlarmRule& rule, const BehaviorEventItem& event) const {
if (event.status != BehaviorEventStatus::Active && event.status != BehaviorEventStatus::Ended) return false;
if (rule.min_score > 0.0f && event.score < rule.min_score) return false;
if (rule.min_duration_ms > 0 && static_cast<int>(event.duration_ms) < rule.min_duration_ms) return false;
if (rule.require_track_id && event.track_ids.empty()) return false;
if (!rule.region_ids.empty() && rule.region_ids.find(event.region_id) == rule.region_ids.end()) return false;
return true;
}
bool RuleEngine::MatchBehaviorEventType(const AlarmRule& rule, const BehaviorEventItem& event) const {
if (rule.event_types.empty()) return true;
return rule.event_types.find(BehaviorEventTypeName(event.type)) != rule.event_types.end();
}
std::string RuleEngine::MakeKey(const std::string& rule_name, int track_id) {
return rule_name + "#" + std::to_string(track_id);
}

View File

@ -22,10 +22,13 @@ struct RoiRect {
struct AlarmRule {
std::string name;
std::set<int> class_ids;
std::set<std::string> event_types;
std::set<std::string> region_ids;
RoiRect roi;
float min_score = 0.0f;
float min_box_area_ratio = 0.0f;
bool require_track_id = false;
bool use_behavior_events = false;
int min_duration_ms = 0;
int min_hits = 1;
int hit_window_ms = 0;
@ -38,6 +41,7 @@ struct RuleMatchResult {
bool matched = false;
std::string rule_name;
std::vector<Detection> matched_detections;
std::vector<BehaviorEventItem> matched_behavior_events;
};
class RuleEngine {
@ -56,6 +60,8 @@ private:
bool CheckPerTrackCooldown(const std::string& rule_name, int track_id, int per_track_ms);
void TriggerPerTrackCooldown(const std::string& rule_name, int track_id);
bool PassQuality(const AlarmRule& rule, const Detection& det, double img_area) const;
bool PassBehaviorQuality(const AlarmRule& rule, const BehaviorEventItem& event) const;
bool MatchBehaviorEventType(const AlarmRule& rule, const BehaviorEventItem& event) const;
static std::string MakeKey(const std::string& rule_name, int track_id);
std::vector<AlarmRule> rules_;

View File

@ -7,6 +7,7 @@
#include <thread>
#include <vector>
#include "behavior/behavior_event_format.h"
#include "face/face_result.h"
#include "node.h"
#include "utils/dma_alloc.h"
@ -42,8 +43,8 @@ struct Color {
};
const Color kClassColors[] = {
{255, 0, 0}, {0, 255, 0}, {0, 0, 255}, {255, 255, 0}, {255, 0, 255}, // 0-4: 红,绿,蓝,黄,紫
{0, 255, 255}, {0, 255, 0}, {0, 128, 0}, {0, 0, 128}, {128, 128, 0}, // 5-9: 青,绿(人),深绿,深蓝,橄榄
{255, 0, 0}, {0, 255, 0}, {0, 0, 255}, {255, 255, 0}, {255, 0, 255}, // 0-4: red, green, blue, yellow, magenta
{0, 255, 255}, {0, 255, 0}, {0, 128, 0}, {0, 0, 128}, {128, 128, 0}, // 5-9: cyan, person green, dark green, dark blue, olive
{128, 0, 128}, {0, 128, 128}, {255, 128, 0}, {255, 0, 128}, {128, 255, 0}, // 10-14
{0, 255, 128}, {128, 0, 255}, {0, 128, 255}, {255, 128, 128}, {128, 255, 128} // 15-19
};
@ -59,6 +60,16 @@ inline const char* GetClassName(int cls_id) {
return "unknown";
}
inline Color GetBehaviorEventColor(BehaviorEventType type) {
switch (type) {
case BehaviorEventType::Intrusion: return {255, 128, 0};
case BehaviorEventType::Climb: return {255, 0, 255};
case BehaviorEventType::Fall: return {255, 0, 0};
case BehaviorEventType::Fight: return {255, 255, 0};
}
return {255, 255, 255};
}
inline int Clamp(int val, int min_val, int max_val) {
return val < min_val ? min_val : (val > max_val ? max_val : val);
}
@ -428,11 +439,12 @@ public:
if (!frame) return NodeStatus::DROP;
FramePtr out = frame;
if (out->data && (out->det || out->face_det || out->face_recog)) {
if (out->data && (out->det || out->face_det || out->face_recog || out->behavior_events)) {
if (out.use_count() > 1) {
out = CloneFrameForWrite(out);
}
DrawDetections(out);
DrawBehaviorEvents(out);
if (draw_face_det_) DrawFaceDet(out);
if (draw_face_recog_) DrawFaceRecog(out);
}
@ -799,6 +811,51 @@ private:
}
}
void DrawBehaviorEvents(FramePtr frame) {
if (!frame->behavior_events || frame->behavior_events->items.empty()) return;
int w = frame->width;
int h = frame->height;
uint8_t* data = frame->planes[0].data ? frame->planes[0].data : frame->data;
PixelFormat fmt = frame->format;
int stride;
if (fmt == PixelFormat::RGB || fmt == PixelFormat::BGR) {
stride = frame->planes[0].stride > 0 ? frame->planes[0].stride : (frame->stride > 0 ? frame->stride : w * 3);
} else if (fmt == PixelFormat::NV12 || fmt == PixelFormat::YUV420) {
stride = frame->planes[0].stride > 0 ? frame->planes[0].stride : (frame->stride > 0 ? frame->stride : w);
} else {
return;
}
if (frame->DmaFd() >= 0) {
frame->SyncStart();
}
for (const auto& event : frame->behavior_events->items) {
if (event.status == BehaviorEventStatus::Pending) continue;
const Rect& box = event.bbox;
int x1 = static_cast<int>(box.x);
int y1 = static_cast<int>(box.y);
int x2 = static_cast<int>(box.x + box.w);
int y2 = static_cast<int>(box.y + box.h);
const Color color = GetBehaviorEventColor(event.type);
DrawRect(data, w, h, stride, fmt, x1, y1, x2, y2, line_width_, color);
if (draw_text_) {
const std::string label = FormatBehaviorEventLabel(event);
int text_y = y1 - static_cast<int>(8 * font_scale_);
if (text_y < 0) text_y = y1 + 2;
DrawText(data, w, h, stride, fmt, x1, text_y, label.c_str(), font_scale_, color);
}
}
if (frame->DmaFd() >= 0) {
frame->SyncEnd();
}
}
void DrawFaceDet(FramePtr frame) {
if (!frame->face_det || frame->face_det->faces.empty()) return;
@ -829,7 +886,7 @@ private:
LogRgaBboxErrorThrottled("imbeginJob failed");
} else {
bool ok = true;
const Color color{0, 180, 180}; // 深青色,不那么刺眼
const Color color{0, 180, 180}; // Soft cyan for face overlays
for (const auto& it : frame->face_det->faces) {
int x = Clamp(static_cast<int>(it.bbox.x), 0, w - 1);
int y = Clamp(static_cast<int>(it.bbox.y), 0, h - 1);
@ -877,7 +934,7 @@ private:
frame->SyncStart();
}
const Color color{0, 180, 180}; // 深青色,不那么刺眼
const Color color{0, 180, 180}; // Soft cyan for face overlays
for (const auto& it : frame->face_det->faces) {
int x1 = static_cast<int>(it.bbox.x);
int y1 = static_cast<int>(it.bbox.y);

View File

@ -41,6 +41,7 @@ add_executable(rk3588_gtests
test_region_event.cpp
test_action_recog.cpp
test_event_fusion.cpp
test_alarm_behavior_events.cpp
test_infer_backend.cpp
test_image_processor.cpp
test_codec_backend.cpp
@ -53,6 +54,7 @@ add_executable(rk3588_gtests
${CMAKE_SOURCE_DIR}/plugins/region_event/region_event_node.cpp
${CMAKE_SOURCE_DIR}/plugins/action_recog/action_recog_node.cpp
${CMAKE_SOURCE_DIR}/plugins/event_fusion/event_fusion_node.cpp
${CMAKE_SOURCE_DIR}/plugins/alarm/rule_engine.cpp
)
target_include_directories(rk3588_gtests PRIVATE

View File

@ -0,0 +1,81 @@
#include <gtest/gtest.h>
#include <memory>
#include <string>
#include <vector>
#include "behavior/behavior_event.h"
#include "frame/frame.h"
#include "utils/simple_json.h"
#include "../plugins/alarm/rule_engine.h"
namespace rk3588 {
namespace {
SimpleJson ParseAlarmRules(const std::string& text) {
SimpleJson config;
std::string err;
const bool ok = ParseSimpleJson(text, config, err);
EXPECT_TRUE(ok) << err;
return config;
}
TEST(AlarmBehaviorEventsTest, MatchesBehaviorEventByTypeAndDuration) {
RuleEngine engine;
const auto rules = ParseAlarmRules(R"([
{
"name": "fall_event",
"use_behavior_events": true,
"event_types": ["fall"],
"min_duration_ms": 1000,
"cooldown_ms": 0
}
])");
ASSERT_TRUE(engine.Init(rules, {}));
auto frame = std::make_shared<Frame>();
frame->behavior_events = std::make_shared<BehaviorEventResult>();
BehaviorEventItem item;
item.type = BehaviorEventType::Fall;
item.status = BehaviorEventStatus::Active;
item.duration_ms = 1500;
item.track_ids = {8};
frame->behavior_events->items.push_back(item);
const auto result = engine.Evaluate(frame);
EXPECT_TRUE(result.matched);
EXPECT_EQ(result.rule_name, "fall_event");
ASSERT_EQ(result.matched_behavior_events.size(), 1u);
EXPECT_EQ(result.matched_behavior_events[0].type, BehaviorEventType::Fall);
}
TEST(AlarmBehaviorEventsTest, PreservesLegacyDetectionRules) {
RuleEngine engine;
const auto rules = ParseAlarmRules(R"([
{
"name": "person_detected",
"class_ids": [0],
"cooldown_ms": 0
}
])");
ASSERT_TRUE(engine.Init(rules, {"person"}));
auto frame = std::make_shared<Frame>();
frame->width = 1920;
frame->height = 1080;
frame->det = std::make_shared<DetectionResult>();
frame->det->img_w = 1920;
frame->det->img_h = 1080;
frame->det->items.push_back(Detection{0, 0.9f, Rect{100.0f, 120.0f, 80.0f, 200.0f}, 3});
const auto result = engine.Evaluate(frame);
EXPECT_TRUE(result.matched);
EXPECT_EQ(result.rule_name, "person_detected");
ASSERT_EQ(result.matched_detections.size(), 1u);
EXPECT_EQ(result.matched_detections[0].cls_id, 0);
}
} // namespace
} // namespace rk3588

View File

@ -2,6 +2,7 @@
#include <memory>
#include "behavior/behavior_event_format.h"
#include "frame/frame.h"
namespace rk3588 {
@ -29,5 +30,14 @@ TEST(BehaviorEventModelTest, FrameStoresBehaviorEventsSeparatelyFromDetections)
EXPECT_EQ(frame->behavior_events->items[0].duration_ms, 1600u);
}
TEST(BehaviorEventModelTest, FormatsBehaviorEventLabelForOsd) {
BehaviorEventItem item;
item.type = BehaviorEventType::Intrusion;
item.track_ids = {9};
item.duration_ms = 2100;
EXPECT_EQ(FormatBehaviorEventLabel(item), "intrusion #9 2.1s");
}
} // namespace
} // namespace rk3588