feat: add region event plugin
This commit is contained in:
parent
6f30513a73
commit
d14083c1f8
@ -458,6 +458,18 @@ set_target_properties(logic_gate PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY ${RK_PLUGIN_OUTPUT_DIR}
|
||||
)
|
||||
|
||||
# region_event plugin (rule-driven region behavior events)
|
||||
add_library(region_event SHARED
|
||||
region_event/region_event_node.cpp
|
||||
)
|
||||
target_include_directories(region_event PRIVATE ${CMAKE_SOURCE_DIR}/include ${CMAKE_SOURCE_DIR}/third_party)
|
||||
target_link_libraries(region_event PRIVATE project_options Threads::Threads)
|
||||
set_target_properties(region_event PROPERTIES
|
||||
OUTPUT_NAME "region_event"
|
||||
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
|
||||
@ -529,7 +541,7 @@ set_target_properties(ai_shoe_det PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY ${RK_PLUGIN_OUTPUT_DIR}
|
||||
)
|
||||
|
||||
install(TARGETS input_rtsp input_file publish preprocess ai_yolo ai_face_det ai_scrfd ai_scrfd_sliding ai_face_recog tracker gate osd alarm logic_gate storage ai_scheduler ai_shoe_det
|
||||
install(TARGETS input_rtsp input_file publish preprocess ai_yolo ai_face_det ai_scrfd ai_scrfd_sliding ai_face_recog tracker gate osd alarm logic_gate region_event storage ai_scheduler ai_shoe_det
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/rk3588-media-server/plugins
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_LIBDIR}/rk3588-media-server/plugins
|
||||
)
|
||||
|
||||
232
plugins/region_event/region_event_node.cpp
Normal file
232
plugins/region_event/region_event_node.cpp
Normal file
@ -0,0 +1,232 @@
|
||||
#include "region_event_node.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "behavior/behavior_event.h"
|
||||
#include "utils/logger.h"
|
||||
|
||||
namespace rk3588 {
|
||||
namespace {
|
||||
|
||||
enum class RegionEventKind {
|
||||
Intrusion,
|
||||
Climb
|
||||
};
|
||||
|
||||
struct NormalizedLine {
|
||||
float x1 = 0.0f;
|
||||
float y1 = 0.0f;
|
||||
float x2 = 0.0f;
|
||||
float y2 = 0.0f;
|
||||
};
|
||||
|
||||
struct RegionEventRule {
|
||||
RegionEventKind kind = RegionEventKind::Intrusion;
|
||||
std::string region_id;
|
||||
Rect roi{};
|
||||
NormalizedLine line{};
|
||||
uint64_t min_duration_ms = 0;
|
||||
float min_vertical_motion = 0.0f;
|
||||
};
|
||||
|
||||
struct TrackState {
|
||||
Rect bbox{};
|
||||
uint64_t pts = 0;
|
||||
};
|
||||
|
||||
static bool ParseRules(const SimpleJson& config, std::vector<RegionEventRule>& out, std::string& err) {
|
||||
const SimpleJson* events = config.Find("events");
|
||||
if (!events || !events->IsArray()) {
|
||||
err = "events must be array";
|
||||
return false;
|
||||
}
|
||||
|
||||
out.clear();
|
||||
for (const auto& ev : events->AsArray()) {
|
||||
if (!ev.IsObject()) {
|
||||
err = "event entry must be object";
|
||||
return false;
|
||||
}
|
||||
|
||||
RegionEventRule rule;
|
||||
const std::string type = ev.ValueOr<std::string>("type", "");
|
||||
rule.region_id = ev.ValueOr<std::string>("region_id", "");
|
||||
rule.min_duration_ms = static_cast<uint64_t>(std::max(0, ev.ValueOr<int>("min_duration_ms", 0)));
|
||||
|
||||
if (type == "intrusion") {
|
||||
const SimpleJson* roi = ev.Find("roi");
|
||||
if (!roi || !roi->IsObject()) {
|
||||
err = "intrusion event requires roi";
|
||||
return false;
|
||||
}
|
||||
rule.kind = RegionEventKind::Intrusion;
|
||||
rule.roi.x = roi->ValueOr<float>("x", 0.0f);
|
||||
rule.roi.y = roi->ValueOr<float>("y", 0.0f);
|
||||
rule.roi.w = roi->ValueOr<float>("w", 0.0f);
|
||||
rule.roi.h = roi->ValueOr<float>("h", 0.0f);
|
||||
} else if (type == "climb") {
|
||||
const SimpleJson* line = ev.Find("line");
|
||||
if (!line || !line->IsObject()) {
|
||||
err = "climb event requires line";
|
||||
return false;
|
||||
}
|
||||
rule.kind = RegionEventKind::Climb;
|
||||
rule.line.x1 = line->ValueOr<float>("x1", 0.0f);
|
||||
rule.line.y1 = line->ValueOr<float>("y1", 0.0f);
|
||||
rule.line.x2 = line->ValueOr<float>("x2", 0.0f);
|
||||
rule.line.y2 = line->ValueOr<float>("y2", 0.0f);
|
||||
rule.min_vertical_motion = ev.ValueOr<float>("min_vertical_motion", 0.0f);
|
||||
} else {
|
||||
err = "unsupported event type: " + type;
|
||||
return false;
|
||||
}
|
||||
|
||||
out.push_back(std::move(rule));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static float CenterX(const Rect& rect) {
|
||||
return rect.x + (rect.w * 0.5f);
|
||||
}
|
||||
|
||||
static float CenterY(const Rect& rect) {
|
||||
return rect.y + (rect.h * 0.5f);
|
||||
}
|
||||
|
||||
static bool IsInsideNormalizedRoi(const Rect& bbox, const Rect& roi, int img_w, int img_h) {
|
||||
if (img_w <= 0 || img_h <= 0) return false;
|
||||
const float cx = CenterX(bbox) / static_cast<float>(img_w);
|
||||
const float cy = CenterY(bbox) / static_cast<float>(img_h);
|
||||
return cx >= roi.x && cx <= (roi.x + roi.w) && cy >= roi.y && cy <= (roi.y + roi.h);
|
||||
}
|
||||
|
||||
static bool CrossedHorizontalLine(const Rect& previous, const Rect& current, const RegionEventRule& rule, int img_h) {
|
||||
if (img_h <= 0) return false;
|
||||
const float line_y = rule.line.y1 * static_cast<float>(img_h);
|
||||
const float previous_top = previous.y;
|
||||
const float current_top = current.y;
|
||||
return previous_top > line_y &&
|
||||
current_top <= line_y &&
|
||||
(previous_top - current_top) >= rule.min_vertical_motion;
|
||||
}
|
||||
|
||||
static BehaviorEventType ToBehaviorEventType(RegionEventKind kind) {
|
||||
return kind == RegionEventKind::Intrusion ? BehaviorEventType::Intrusion : BehaviorEventType::Climb;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct RegionEventNode::Impl {
|
||||
std::string init_err;
|
||||
std::vector<RegionEventRule> rules;
|
||||
std::map<int, TrackState> track_history;
|
||||
};
|
||||
|
||||
RegionEventNode::RegionEventNode() : impl_(std::make_unique<Impl>()) {}
|
||||
|
||||
RegionEventNode::~RegionEventNode() = default;
|
||||
|
||||
std::string RegionEventNode::Id() const {
|
||||
return id_;
|
||||
}
|
||||
|
||||
std::string RegionEventNode::Type() const {
|
||||
return "region_event";
|
||||
}
|
||||
|
||||
bool RegionEventNode::Init(const SimpleJson& config, const NodeContext& ctx) {
|
||||
id_ = config.ValueOr<std::string>("id", "region_event");
|
||||
if (!ParseRules(config, impl_->rules, impl_->init_err)) {
|
||||
LogError("[region_event] invalid config: " + impl_->init_err);
|
||||
return false;
|
||||
}
|
||||
output_queues_ = ctx.output_queues;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RegionEventNode::Start() {
|
||||
return true;
|
||||
}
|
||||
|
||||
void RegionEventNode::Stop() {}
|
||||
|
||||
NodeStatus RegionEventNode::Process(FramePtr frame) {
|
||||
if (!frame) return NodeStatus::DROP;
|
||||
|
||||
EnsureBehaviorEvents(*frame);
|
||||
if (!frame->det) {
|
||||
PushToDownstream(frame);
|
||||
return NodeStatus::OK;
|
||||
}
|
||||
|
||||
const int img_w = frame->det->img_w > 0 ? frame->det->img_w : frame->width;
|
||||
const int img_h = frame->det->img_h > 0 ? frame->det->img_h : frame->height;
|
||||
|
||||
for (const auto& det : frame->det->items) {
|
||||
if (det.track_id < 0) continue;
|
||||
|
||||
for (const auto& rule : impl_->rules) {
|
||||
if (rule.kind == RegionEventKind::Intrusion) {
|
||||
if (!IsInsideNormalizedRoi(det.bbox, rule.roi, img_w, img_h)) continue;
|
||||
|
||||
BehaviorEventItem event;
|
||||
event.type = ToBehaviorEventType(rule.kind);
|
||||
event.status = BehaviorEventStatus::Active;
|
||||
event.score = det.score;
|
||||
event.bbox = det.bbox;
|
||||
event.track_ids.push_back(det.track_id);
|
||||
event.start_pts = frame->pts;
|
||||
event.last_pts = frame->pts;
|
||||
event.duration_ms = rule.min_duration_ms;
|
||||
event.source = "region_event";
|
||||
event.region_id = rule.region_id;
|
||||
frame->behavior_events->items.push_back(std::move(event));
|
||||
} else {
|
||||
auto it = impl_->track_history.find(det.track_id);
|
||||
if (it == impl_->track_history.end()) continue;
|
||||
if (!CrossedHorizontalLine(it->second.bbox, det.bbox, rule, img_h)) continue;
|
||||
|
||||
BehaviorEventItem event;
|
||||
event.type = ToBehaviorEventType(rule.kind);
|
||||
event.status = BehaviorEventStatus::Active;
|
||||
event.score = det.score;
|
||||
event.bbox = det.bbox;
|
||||
event.track_ids.push_back(det.track_id);
|
||||
event.start_pts = it->second.pts;
|
||||
event.last_pts = frame->pts;
|
||||
event.duration_ms = frame->pts >= it->second.pts ? (frame->pts - it->second.pts) : 0;
|
||||
event.source = "region_event";
|
||||
event.region_id = rule.region_id;
|
||||
frame->behavior_events->items.push_back(std::move(event));
|
||||
}
|
||||
}
|
||||
|
||||
impl_->track_history[det.track_id] = TrackState{det.bbox, frame->pts};
|
||||
}
|
||||
|
||||
PushToDownstream(frame);
|
||||
return NodeStatus::OK;
|
||||
}
|
||||
|
||||
void RegionEventNode::EnsureBehaviorEvents(Frame& frame) {
|
||||
if (!frame.behavior_events) {
|
||||
frame.behavior_events = std::make_shared<BehaviorEventResult>();
|
||||
}
|
||||
}
|
||||
|
||||
void RegionEventNode::PushToDownstream(const FramePtr& frame) {
|
||||
for (auto& q : output_queues_) {
|
||||
if (q) q->Push(frame);
|
||||
}
|
||||
}
|
||||
|
||||
REGISTER_NODE(RegionEventNode, "region_event");
|
||||
|
||||
} // namespace rk3588
|
||||
33
plugins/region_event/region_event_node.h
Normal file
33
plugins/region_event/region_event_node.h
Normal file
@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "node.h"
|
||||
|
||||
namespace rk3588 {
|
||||
|
||||
class RegionEventNode final : public INode {
|
||||
public:
|
||||
RegionEventNode();
|
||||
~RegionEventNode() override;
|
||||
|
||||
std::string Id() const override;
|
||||
std::string Type() const override;
|
||||
bool Init(const SimpleJson& config, const NodeContext& ctx) override;
|
||||
bool Start() override;
|
||||
void Stop() override;
|
||||
NodeStatus Process(FramePtr frame) override;
|
||||
|
||||
private:
|
||||
void EnsureBehaviorEvents(Frame& frame);
|
||||
void PushToDownstream(const FramePtr& frame);
|
||||
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
std::string id_;
|
||||
std::vector<std::shared_ptr<SpscQueue<FramePtr>>> output_queues_;
|
||||
};
|
||||
|
||||
} // namespace rk3588
|
||||
@ -38,6 +38,7 @@ add_executable(rk3588_gtests
|
||||
test_hw_factory.cpp
|
||||
test_frame_buffer.cpp
|
||||
test_behavior_event_model.cpp
|
||||
test_region_event.cpp
|
||||
test_infer_backend.cpp
|
||||
test_image_processor.cpp
|
||||
test_codec_backend.cpp
|
||||
@ -47,6 +48,7 @@ add_executable(rk3588_gtests
|
||||
${CMAKE_SOURCE_DIR}/src/ai_scheduler.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/utils/config_expand.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/utils/dma_alloc.cpp
|
||||
${CMAKE_SOURCE_DIR}/plugins/region_event/region_event_node.cpp
|
||||
)
|
||||
|
||||
target_include_directories(rk3588_gtests PRIVATE
|
||||
|
||||
113
tests/test_region_event.cpp
Normal file
113
tests/test_region_event.cpp
Normal file
@ -0,0 +1,113 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "frame/frame.h"
|
||||
#include "node.h"
|
||||
#include "utils/simple_json.h"
|
||||
#include "../plugins/region_event/region_event_node.h"
|
||||
|
||||
namespace rk3588 {
|
||||
namespace {
|
||||
|
||||
SimpleJson ParseConfigText(const std::string& text) {
|
||||
SimpleJson config;
|
||||
std::string err;
|
||||
EXPECT_TRUE(ParseSimpleJson(text, config, err)) << err;
|
||||
return config;
|
||||
}
|
||||
|
||||
TEST(RegionEventTest, EmitsIntrusionWhenTrackedPersonStaysInsideRegion) {
|
||||
RegionEventNode node;
|
||||
|
||||
const std::string config_text = R"({
|
||||
"id": "region_evt",
|
||||
"events": [
|
||||
{
|
||||
"type": "intrusion",
|
||||
"region_id": "zone_a",
|
||||
"roi": {"x": 0.10, "y": 0.10, "w": 0.50, "h": 0.50},
|
||||
"min_duration_ms": 0
|
||||
}
|
||||
]
|
||||
})";
|
||||
|
||||
SimpleJson config = ParseConfigText(config_text);
|
||||
NodeContext ctx;
|
||||
auto out = std::make_shared<SpscQueue<FramePtr>>(4, QueueDropStrategy::DropOldest);
|
||||
ctx.output_queues.push_back(out);
|
||||
|
||||
ASSERT_TRUE(node.Init(config, ctx));
|
||||
ASSERT_TRUE(node.Start());
|
||||
|
||||
auto frame = std::make_shared<Frame>();
|
||||
frame->width = 1920;
|
||||
frame->height = 1080;
|
||||
frame->pts = 1000;
|
||||
frame->det = std::make_shared<DetectionResult>();
|
||||
frame->det->img_w = 1920;
|
||||
frame->det->img_h = 1080;
|
||||
frame->det->items.push_back(Detection{0, 0.95f, Rect{300.0f, 220.0f, 180.0f, 420.0f}, 42});
|
||||
|
||||
EXPECT_EQ(static_cast<int>(node.Process(frame)), static_cast<int>(NodeStatus::OK));
|
||||
ASSERT_NE(frame->behavior_events, nullptr);
|
||||
ASSERT_EQ(frame->behavior_events->items.size(), 1u);
|
||||
EXPECT_EQ(frame->behavior_events->items[0].type, BehaviorEventType::Intrusion);
|
||||
ASSERT_EQ(frame->behavior_events->items[0].track_ids.size(), 1u);
|
||||
EXPECT_EQ(frame->behavior_events->items[0].track_ids[0], 42);
|
||||
}
|
||||
|
||||
TEST(RegionEventTest, EmitsClimbWhenTrackCrossesBoundaryOverTime) {
|
||||
RegionEventNode node;
|
||||
|
||||
const std::string config_text = R"({
|
||||
"id": "region_evt",
|
||||
"events": [
|
||||
{
|
||||
"type": "climb",
|
||||
"region_id": "fence_1",
|
||||
"line": {"x1": 0.20, "y1": 0.40, "x2": 0.80, "y2": 0.40},
|
||||
"min_duration_ms": 0,
|
||||
"min_vertical_motion": 40
|
||||
}
|
||||
]
|
||||
})";
|
||||
|
||||
SimpleJson config = ParseConfigText(config_text);
|
||||
NodeContext ctx;
|
||||
auto out = std::make_shared<SpscQueue<FramePtr>>(4, QueueDropStrategy::DropOldest);
|
||||
ctx.output_queues.push_back(out);
|
||||
|
||||
ASSERT_TRUE(node.Init(config, ctx));
|
||||
ASSERT_TRUE(node.Start());
|
||||
|
||||
auto frame1 = std::make_shared<Frame>();
|
||||
frame1->width = 1920;
|
||||
frame1->height = 1080;
|
||||
frame1->pts = 1000;
|
||||
frame1->det = std::make_shared<DetectionResult>();
|
||||
frame1->det->img_w = 1920;
|
||||
frame1->det->img_h = 1080;
|
||||
frame1->det->items.push_back(Detection{0, 0.90f, Rect{700.0f, 520.0f, 120.0f, 260.0f}, 7});
|
||||
|
||||
auto frame2 = std::make_shared<Frame>();
|
||||
frame2->width = 1920;
|
||||
frame2->height = 1080;
|
||||
frame2->pts = 1100;
|
||||
frame2->det = std::make_shared<DetectionResult>();
|
||||
frame2->det->img_w = 1920;
|
||||
frame2->det->img_h = 1080;
|
||||
frame2->det->items.push_back(Detection{0, 0.92f, Rect{700.0f, 320.0f, 120.0f, 260.0f}, 7});
|
||||
|
||||
EXPECT_EQ(static_cast<int>(node.Process(frame1)), static_cast<int>(NodeStatus::OK));
|
||||
EXPECT_EQ(static_cast<int>(node.Process(frame2)), static_cast<int>(NodeStatus::OK));
|
||||
ASSERT_NE(frame2->behavior_events, nullptr);
|
||||
ASSERT_EQ(frame2->behavior_events->items.size(), 1u);
|
||||
EXPECT_EQ(frame2->behavior_events->items[0].type, BehaviorEventType::Climb);
|
||||
ASSERT_EQ(frame2->behavior_events->items[0].track_ids.size(), 1u);
|
||||
EXPECT_EQ(frame2->behavior_events->items[0].track_ids[0], 7);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace rk3588
|
||||
Loading…
Reference in New Issue
Block a user