#include "rule_engine.h" #include #include #include #include #include #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 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& 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("name", "unnamed_rule"); rule.min_score = rule_json.ValueOr("min_score", 0.0f); rule.min_box_area_ratio = rule_json.ValueOr("min_box_area_ratio", 0.0f); rule.require_track_id = rule_json.ValueOr("require_track_id", false); rule.min_duration_ms = rule_json.ValueOr("min_duration_ms", 0); rule.min_hits = rule_json.ValueOr("min_hits", 1); rule.hit_window_ms = rule_json.ValueOr("hit_window_ms", 0); rule.cooldown_ms = rule_json.ValueOr("cooldown_ms", 5000); rule.per_track_cooldown_ms = rule_json.ValueOr("per_track_cooldown_ms", 0); rule.schedule = rule_json.ValueOr("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(i)); break; } } } } // Parse ROI if (const SimpleJson* roi_json = rule_json.Find("roi")) { rule.roi.x = roi_json->ValueOr("x", 0.0f); rule.roi.y = roi_json->ValueOr("y", 0.0f); rule.roi.w = roi_json->ValueOr("w", 1.0f); rule.roi.h = roi_json->ValueOr("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) { 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 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(img_w) * static_cast(img_h))) { continue; } matched.push_back(det); } if (matched.empty()) { CheckDuration(rule.name, false); continue; } std::vector 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( 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( 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(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(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(det.bbox.w) * static_cast(det.bbox.h); const double r = a / img_area; if (r < static_cast(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