feat: consume behavior events in osd and alarm
This commit is contained in:
parent
0c98be39cd
commit
6e4bca7a05
38
include/behavior/behavior_event_format.h
Normal file
38
include/behavior/behavior_event_format.h
Normal 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
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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_;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
81
tests/test_alarm_behavior_events.cpp
Normal file
81
tests/test_alarm_behavior_events.cpp
Normal 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
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user