235 lines
7.3 KiB
C++
235 lines
7.3 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_duration_ms = rule_json.ValueOr<int>("min_duration_ms", 0);
|
|
rule.cooldown_ms = rule_json.ValueOr<int>("cooldown_ms", 5000);
|
|
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_duration=" << rule.min_duration_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;
|
|
}
|
|
|
|
// Check 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;
|
|
}
|
|
|
|
matched.push_back(det);
|
|
}
|
|
|
|
bool currently_matched = !matched.empty();
|
|
|
|
// Check duration
|
|
if (CheckDuration(rule.name, currently_matched)) {
|
|
result.matched = true;
|
|
result.rule_name = rule.name;
|
|
result.matched_detections = matched;
|
|
TriggerCooldown(rule.name);
|
|
duration_start_.erase(rule.name);
|
|
return result;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
void RuleEngine::Reset() {
|
|
duration_start_.clear();
|
|
last_trigger_.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();
|
|
}
|
|
|
|
} // namespace rk3588
|