diff --git a/include/behavior/behavior_event_format.h b/include/behavior/behavior_event_format.h new file mode 100644 index 0000000..5c551cc --- /dev/null +++ b/include/behavior/behavior_event_format.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include +#include + +#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(item.duration_ms) / 1000.0); + + std::string label = BehaviorEventTypeName(item.type); + if (!item.track_ids.empty()) { + std::vector 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 diff --git a/plugins/alarm/actions/action_base.h b/plugins/alarm/actions/action_base.h index 0db0e1c..9c22769 100644 --- a/plugins/alarm/actions/action_base.h +++ b/plugins/alarm/actions/action_base.h @@ -15,6 +15,7 @@ struct AlarmEvent { uint64_t timestamp_ms; uint64_t frame_id; std::vector detections; + std::vector behavior_events; std::string snapshot_url; std::string clip_url; }; diff --git a/plugins/alarm/alarm_node.cpp b/plugins/alarm/alarm_node.cpp index a655181..83c1ad9 100644 --- a/plugins/alarm/alarm_node.cpp +++ b/plugins/alarm/alarm_node.cpp @@ -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); diff --git a/plugins/alarm/rule_engine.cpp b/plugins/alarm/rule_engine.cpp index 7147148..461db32 100644 --- a/plugins/alarm/rule_engine.cpp +++ b/plugins/alarm/rule_engine.cpp @@ -6,6 +6,7 @@ #include #include +#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("min_score", 0.0f); rule.min_box_area_ratio = rule_json.ValueOr("min_box_area_ratio", 0.0f); rule.require_track_id = rule_json.ValueOr("require_track_id", false); + rule.use_behavior_events = rule_json.ValueOr("use_behavior_events", false); rule.min_duration_ms = rule_json.ValueOr("min_duration_ms", 0); rule.min_hits = rule_json.ValueOr("min_hits", 1); rule.hit_window_ms = rule_json.ValueOr("hit_window_ms", 0); @@ -103,11 +105,16 @@ bool RuleEngine::Init(const SimpleJson& rules_config, const std::vector& 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* 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) { } } - // Find matching detections + if (rule.use_behavior_events) { + std::vector 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 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(img_w) * static_cast(img_h))) { continue; } - matched.push_back(det); } @@ -151,7 +176,6 @@ RuleMatchResult RuleEngine::Evaluate(const std::shared_ptr& frame) { std::vector 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(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); } diff --git a/plugins/alarm/rule_engine.h b/plugins/alarm/rule_engine.h index b9f46ea..04dabd6 100644 --- a/plugins/alarm/rule_engine.h +++ b/plugins/alarm/rule_engine.h @@ -22,10 +22,13 @@ struct RoiRect { struct AlarmRule { std::string name; std::set class_ids; + std::set event_types; + std::set 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 matched_detections; + std::vector 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 rules_; diff --git a/plugins/osd/osd_node.cpp b/plugins/osd/osd_node.cpp index 81e7be1..610a13d 100644 --- a/plugins/osd/osd_node.cpp +++ b/plugins/osd/osd_node.cpp @@ -7,6 +7,7 @@ #include #include +#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(box.x); + int y1 = static_cast(box.y); + int x2 = static_cast(box.x + box.w); + int y2 = static_cast(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(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(it.bbox.x), 0, w - 1); int y = Clamp(static_cast(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(it.bbox.x); int y1 = static_cast(it.bbox.y); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 60115ed..d510c3b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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 diff --git a/tests/test_alarm_behavior_events.cpp b/tests/test_alarm_behavior_events.cpp new file mode 100644 index 0000000..13f91da --- /dev/null +++ b/tests/test_alarm_behavior_events.cpp @@ -0,0 +1,81 @@ +#include + +#include +#include +#include + +#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->behavior_events = std::make_shared(); + 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->width = 1920; + frame->height = 1080; + frame->det = std::make_shared(); + 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 diff --git a/tests/test_behavior_event_model.cpp b/tests/test_behavior_event_model.cpp index 610f318..d6ab0b0 100644 --- a/tests/test_behavior_event_model.cpp +++ b/tests/test_behavior_event_model.cpp @@ -2,6 +2,7 @@ #include +#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