OrangePi3588Media/plugins/alarm/rule_engine.cpp
2026-01-20 16:59:30 +08:00

322 lines
11 KiB
C++

#include "rule_engine.h"
#include <algorithm>
#include <cstdio>
#include <ctime>
#include <mutex>
#include <sstream>
#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.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 || !frame->det) 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;
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;
}
}
// Find matching detections
std::vector<Detection> matched;
for (const auto& det : detections) {
// Check class_id (if empty, match all)
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);
}
if (matched.empty()) {
CheckDuration(rule.name, false);
continue;
}
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)) {
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;
}
std::string RuleEngine::MakeKey(const std::string& rule_name, int track_id) {
return rule_name + "#" + std::to_string(track_id);
}
} // namespace rk3588