diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index ac9c549..b677cb8 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -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 ) diff --git a/plugins/region_event/region_event_node.cpp b/plugins/region_event/region_event_node.cpp new file mode 100644 index 0000000..dc3fb39 --- /dev/null +++ b/plugins/region_event/region_event_node.cpp @@ -0,0 +1,232 @@ +#include "region_event_node.h" + +#include +#include +#include +#include +#include +#include +#include + +#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& 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("type", ""); + rule.region_id = ev.ValueOr("region_id", ""); + rule.min_duration_ms = static_cast(std::max(0, ev.ValueOr("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("x", 0.0f); + rule.roi.y = roi->ValueOr("y", 0.0f); + rule.roi.w = roi->ValueOr("w", 0.0f); + rule.roi.h = roi->ValueOr("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("x1", 0.0f); + rule.line.y1 = line->ValueOr("y1", 0.0f); + rule.line.x2 = line->ValueOr("x2", 0.0f); + rule.line.y2 = line->ValueOr("y2", 0.0f); + rule.min_vertical_motion = ev.ValueOr("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(img_w); + const float cy = CenterY(bbox) / static_cast(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(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 rules; + std::map track_history; +}; + +RegionEventNode::RegionEventNode() : impl_(std::make_unique()) {} + +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("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(); + } +} + +void RegionEventNode::PushToDownstream(const FramePtr& frame) { + for (auto& q : output_queues_) { + if (q) q->Push(frame); + } +} + +REGISTER_NODE(RegionEventNode, "region_event"); + +} // namespace rk3588 diff --git a/plugins/region_event/region_event_node.h b/plugins/region_event/region_event_node.h new file mode 100644 index 0000000..8106523 --- /dev/null +++ b/plugins/region_event/region_event_node.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include + +#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_; + std::string id_; + std::vector>> output_queues_; +}; + +} // namespace rk3588 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 306127d..772f1e6 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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 diff --git a/tests/test_region_event.cpp b/tests/test_region_event.cpp new file mode 100644 index 0000000..b2fce59 --- /dev/null +++ b/tests/test_region_event.cpp @@ -0,0 +1,113 @@ +#include + +#include +#include + +#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>(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->width = 1920; + frame->height = 1080; + frame->pts = 1000; + frame->det = std::make_shared(); + 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(node.Process(frame)), static_cast(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>(4, QueueDropStrategy::DropOldest); + ctx.output_queues.push_back(out); + + ASSERT_TRUE(node.Init(config, ctx)); + ASSERT_TRUE(node.Start()); + + auto frame1 = std::make_shared(); + frame1->width = 1920; + frame1->height = 1080; + frame1->pts = 1000; + frame1->det = std::make_shared(); + 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(); + frame2->width = 1920; + frame2->height = 1080; + frame2->pts = 1100; + frame2->det = std::make_shared(); + 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(node.Process(frame1)), static_cast(NodeStatus::OK)); + EXPECT_EQ(static_cast(node.Process(frame2)), static_cast(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