360 lines
13 KiB
C++
360 lines
13 KiB
C++
#include "rule_engine.h"
|
|
|
|
#include <algorithm>
|
|
#include <cstdio>
|
|
#include <ctime>
|
|
#include <mutex>
|
|
#include <sstream>
|
|
|
|
#include "behavior/behavior_event_format.h"
|
|
#include "utils/logger.h"
|
|
|
|
namespace rk3588 {
|
|
|
|
namespace {
|
|
|
|
bool SafeLocalTime(std::time_t t, std::tm& out) {
|
|
#if defined(_WIN32)
|
|
return localtime_s(&out, &t) == 0;
|
|
#elif defined(__unix__) || defined(__APPLE__)
|
|
return localtime_r(&t, &out) != nullptr;
|
|
#else
|
|
static std::mutex mu;
|
|
std::lock_guard<std::mutex> lock(mu);
|
|
std::tm* p = std::localtime(&t);
|
|
if (!p) return false;
|
|
out = *p;
|
|
return true;
|
|
#endif
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool RuleEngine::Init(const SimpleJson& rules_config, const std::vector<std::string>& labels) {
|
|
labels_ = labels;
|
|
rules_.clear();
|
|
duration_start_.clear();
|
|
last_trigger_.clear();
|
|
|
|
if (!rules_config.IsArray()) {
|
|
LogError("[RuleEngine] rules must be an array");
|
|
return false;
|
|
}
|
|
|
|
for (const auto& rule_json : rules_config.AsArray()) {
|
|
AlarmRule rule;
|
|
rule.name = rule_json.ValueOr<std::string>("name", "unnamed_rule");
|
|
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);
|
|
rule.cooldown_ms = rule_json.ValueOr<int>("cooldown_ms", 5000);
|
|
rule.per_track_cooldown_ms = rule_json.ValueOr<int>("per_track_cooldown_ms", 0);
|
|
rule.schedule = rule_json.ValueOr<std::string>("schedule", "");
|
|
|
|
// Parse class_ids
|
|
if (const SimpleJson* ids = rule_json.Find("class_ids")) {
|
|
for (const auto& id_val : ids->AsArray()) {
|
|
rule.class_ids.insert(id_val.AsInt(-1));
|
|
}
|
|
}
|
|
|
|
// Parse objects (class names) and convert to class_ids
|
|
if (const SimpleJson* objects = rule_json.Find("objects")) {
|
|
for (const auto& obj_val : objects->AsArray()) {
|
|
std::string obj_name = obj_val.AsString("");
|
|
for (size_t i = 0; i < labels_.size(); ++i) {
|
|
if (labels_[i] == obj_name) {
|
|
rule.class_ids.insert(static_cast<int>(i));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse ROI
|
|
if (const SimpleJson* roi_json = rule_json.Find("roi")) {
|
|
rule.roi.x = roi_json->ValueOr<float>("x", 0.0f);
|
|
rule.roi.y = roi_json->ValueOr<float>("y", 0.0f);
|
|
rule.roi.w = roi_json->ValueOr<float>("w", 1.0f);
|
|
rule.roi.h = roi_json->ValueOr<float>("h", 1.0f);
|
|
}
|
|
|
|
rules_.push_back(rule);
|
|
{
|
|
std::ostringstream oss;
|
|
oss << "[RuleEngine] loaded rule: " << rule.name
|
|
<< " class_ids=" << rule.class_ids.size()
|
|
<< " roi=(" << rule.roi.x << "," << rule.roi.y << "," << rule.roi.w << "," << rule.roi.h << ")"
|
|
<< " min_score=" << rule.min_score
|
|
<< " min_box_area_ratio=" << rule.min_box_area_ratio
|
|
<< " require_track_id=" << (rule.require_track_id ? "true" : "false")
|
|
<< " min_duration=" << rule.min_duration_ms << "ms"
|
|
<< " min_hits=" << rule.min_hits
|
|
<< " hit_window=" << rule.hit_window_ms << "ms"
|
|
<< " cooldown=" << rule.cooldown_ms << "ms";
|
|
LogInfo(oss.str());
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
RuleMatchResult RuleEngine::Evaluate(const std::shared_ptr<Frame>& frame) {
|
|
RuleMatchResult result;
|
|
if (!frame) return result;
|
|
|
|
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
|
|
if (!rule.schedule.empty() && !IsInSchedule(rule.schedule)) {
|
|
duration_start_.erase(rule.name);
|
|
continue;
|
|
}
|
|
|
|
if (rule.per_track_cooldown_ms <= 0) {
|
|
// Check rule-level cooldown
|
|
if (!CheckCooldown(rule.name)) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
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) {
|
|
if (!rule.class_ids.empty() &&
|
|
rule.class_ids.find(det.cls_id) == rule.class_ids.end()) {
|
|
continue;
|
|
}
|
|
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);
|
|
}
|
|
|
|
if (matched.empty()) {
|
|
CheckDuration(rule.name, false);
|
|
continue;
|
|
}
|
|
|
|
std::vector<Detection> qualified;
|
|
qualified.reserve(matched.size());
|
|
for (const auto& det : matched) {
|
|
if (rule.per_track_cooldown_ms > 0) {
|
|
if (!CheckPerTrackCooldown(rule.name, det.track_id, rule.per_track_cooldown_ms)) {
|
|
continue;
|
|
}
|
|
}
|
|
if (!CheckVote(rule.name, det.track_id, rule.min_hits, rule.hit_window_ms)) {
|
|
continue;
|
|
}
|
|
qualified.push_back(det);
|
|
}
|
|
|
|
const bool currently_matched = !qualified.empty();
|
|
if (CheckDuration(rule.name, currently_matched)) {
|
|
result.matched = true;
|
|
result.rule_name = rule.name;
|
|
result.matched_detections = qualified;
|
|
if (rule.per_track_cooldown_ms > 0) {
|
|
for (const auto& det : qualified) {
|
|
if (det.track_id >= 0) TriggerPerTrackCooldown(rule.name, det.track_id);
|
|
}
|
|
} else {
|
|
TriggerCooldown(rule.name);
|
|
}
|
|
duration_start_.erase(rule.name);
|
|
return result;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
void RuleEngine::Reset() {
|
|
duration_start_.clear();
|
|
last_trigger_.clear();
|
|
per_track_last_trigger_.clear();
|
|
vote_history_.clear();
|
|
}
|
|
|
|
bool RuleEngine::IsInRoi(const Rect& bbox, const RoiRect& roi, int img_w, int img_h) const {
|
|
// Convert bbox to normalized coordinates
|
|
float bbox_cx = (bbox.x + bbox.w / 2.0f) / img_w;
|
|
float bbox_cy = (bbox.y + bbox.h / 2.0f) / img_h;
|
|
|
|
// Check if center is in ROI
|
|
return bbox_cx >= roi.x && bbox_cx <= (roi.x + roi.w) &&
|
|
bbox_cy >= roi.y && bbox_cy <= (roi.y + roi.h);
|
|
}
|
|
|
|
bool RuleEngine::IsInSchedule(const std::string& schedule) const {
|
|
// Parse "HH:MM-HH:MM" format
|
|
if (schedule.length() != 11 || schedule[5] != '-') {
|
|
return true; // Invalid format, assume always active
|
|
}
|
|
|
|
int start_h = 0, start_m = 0, end_h = 0, end_m = 0;
|
|
(void)std::sscanf(schedule.c_str(), "%d:%d-%d:%d", &start_h, &start_m, &end_h, &end_m);
|
|
|
|
std::time_t now = std::time(nullptr);
|
|
std::tm local{};
|
|
if (!SafeLocalTime(now, local)) {
|
|
return true;
|
|
}
|
|
int curr_min = local.tm_hour * 60 + local.tm_min;
|
|
int start_min = start_h * 60 + start_m;
|
|
int end_min = end_h * 60 + end_m;
|
|
|
|
if (start_min <= end_min) {
|
|
return curr_min >= start_min && curr_min <= end_min;
|
|
} else {
|
|
// Overnight schedule (e.g., "22:00-06:00")
|
|
return curr_min >= start_min || curr_min <= end_min;
|
|
}
|
|
}
|
|
|
|
bool RuleEngine::CheckDuration(const std::string& rule_name, bool currently_matched) {
|
|
auto it = std::find_if(rules_.begin(), rules_.end(),
|
|
[&](const AlarmRule& r) { return r.name == rule_name; });
|
|
if (it == rules_.end()) return false;
|
|
|
|
int min_duration_ms = it->min_duration_ms;
|
|
auto now = std::chrono::steady_clock::now();
|
|
|
|
if (!currently_matched) {
|
|
duration_start_.erase(rule_name);
|
|
return false;
|
|
}
|
|
|
|
auto start_it = duration_start_.find(rule_name);
|
|
if (start_it == duration_start_.end()) {
|
|
duration_start_[rule_name] = now;
|
|
if (min_duration_ms <= 0) {
|
|
return true; // No duration requirement
|
|
}
|
|
return false;
|
|
}
|
|
|
|
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
now - start_it->second);
|
|
return elapsed.count() >= min_duration_ms;
|
|
}
|
|
|
|
bool RuleEngine::CheckCooldown(const std::string& rule_name) {
|
|
auto it = std::find_if(rules_.begin(), rules_.end(),
|
|
[&](const AlarmRule& r) { return r.name == rule_name; });
|
|
if (it == rules_.end()) return false;
|
|
|
|
auto last_it = last_trigger_.find(rule_name);
|
|
if (last_it == last_trigger_.end()) {
|
|
return true; // Never triggered
|
|
}
|
|
|
|
auto now = std::chrono::steady_clock::now();
|
|
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
now - last_it->second);
|
|
return elapsed.count() >= it->cooldown_ms;
|
|
}
|
|
|
|
void RuleEngine::TriggerCooldown(const std::string& rule_name) {
|
|
last_trigger_[rule_name] = std::chrono::steady_clock::now();
|
|
}
|
|
|
|
bool RuleEngine::CheckVote(const std::string& rule_name, int track_id, int min_hits, int hit_window_ms) {
|
|
if (min_hits <= 1 || hit_window_ms <= 0) return true;
|
|
const auto now = std::chrono::steady_clock::now();
|
|
const std::string key = MakeKey(rule_name, track_id);
|
|
auto& dq = vote_history_[key];
|
|
const auto window = std::chrono::milliseconds(hit_window_ms);
|
|
while (!dq.empty() && (now - dq.front()) > window) {
|
|
dq.pop_front();
|
|
}
|
|
dq.push_back(now);
|
|
return static_cast<int>(dq.size()) >= min_hits;
|
|
}
|
|
|
|
bool RuleEngine::CheckPerTrackCooldown(const std::string& rule_name, int track_id, int per_track_ms) {
|
|
if (per_track_ms <= 0) return true;
|
|
if (track_id < 0) return true;
|
|
const std::string key = MakeKey(rule_name, track_id);
|
|
auto it = per_track_last_trigger_.find(key);
|
|
if (it == per_track_last_trigger_.end()) return true;
|
|
const auto now = std::chrono::steady_clock::now();
|
|
const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - it->second).count();
|
|
return elapsed >= per_track_ms;
|
|
}
|
|
|
|
void RuleEngine::TriggerPerTrackCooldown(const std::string& rule_name, int track_id) {
|
|
if (track_id < 0) return;
|
|
per_track_last_trigger_[MakeKey(rule_name, track_id)] = std::chrono::steady_clock::now();
|
|
}
|
|
|
|
bool RuleEngine::PassQuality(const AlarmRule& rule, const Detection& det, double img_area) const {
|
|
if (rule.require_track_id && det.track_id < 0) return false;
|
|
if (rule.min_score > 0.0f && det.score < rule.min_score) return false;
|
|
if (rule.min_box_area_ratio > 0.0f && img_area > 0.0) {
|
|
const double a = static_cast<double>(det.bbox.w) * static_cast<double>(det.bbox.h);
|
|
const double r = a / img_area;
|
|
if (r < static_cast<double>(rule.min_box_area_ratio)) return false;
|
|
}
|
|
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);
|
|
}
|
|
|
|
} // namespace rk3588
|