2026-01-30 15:15:09 +08:00
|
|
|
|
import logging
|
2026-01-29 18:33:12 +08:00
|
|
|
|
import os
|
|
|
|
|
|
import sys
|
2026-01-30 15:15:09 +08:00
|
|
|
|
import threading
|
2026-01-29 18:33:12 +08:00
|
|
|
|
import time
|
|
|
|
|
|
from collections import deque
|
|
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
|
|
|
|
|
|
|
|
import cv2
|
|
|
|
|
|
import numpy as np
|
2026-01-30 15:15:09 +08:00
|
|
|
|
import redis
|
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
2026-01-29 18:33:12 +08:00
|
|
|
|
|
|
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LeavePostAlgorithm:
|
2026-02-12 15:41:05 +08:00
|
|
|
|
"""
|
|
|
|
|
|
离岗检测算法(优化版 v2.0)
|
|
|
|
|
|
|
|
|
|
|
|
状态机:
|
|
|
|
|
|
INIT → CONFIRMING_ON_DUTY → ON_DUTY → CONFIRMING_OFF_DUTY
|
|
|
|
|
|
→ OFF_DUTY_COUNTDOWN → ALARMED → (回岗) → CONFIRMING_ON_DUTY
|
|
|
|
|
|
|
|
|
|
|
|
业务流程:
|
|
|
|
|
|
1. 启动后检测到人 → 上岗确认期(confirm_on_duty_sec,默认10秒)
|
|
|
|
|
|
2. 确认上岗后 → 在岗状态(ON_DUTY)
|
|
|
|
|
|
3. 人离开ROI → 离岗确认期(confirm_off_duty_sec,默认30秒)
|
|
|
|
|
|
4. 确认离岗后 → 离岗倒计时(leave_countdown_sec,默认300秒)
|
|
|
|
|
|
5. 倒计时结束 → 触发告警(ALARMED状态)
|
|
|
|
|
|
6. 人员回岗 → 回岗确认期(confirm_return_sec,默认10秒)
|
|
|
|
|
|
7. 确认回岗 → 发送resolve事件 → 重新上岗确认
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
# 状态定义
|
|
|
|
|
|
STATE_INIT = "INIT" # 初始化
|
|
|
|
|
|
STATE_CONFIRMING_ON_DUTY = "CONFIRMING_ON_DUTY" # 上岗确认中
|
|
|
|
|
|
STATE_ON_DUTY = "ON_DUTY" # 在岗
|
|
|
|
|
|
STATE_CONFIRMING_OFF_DUTY = "CONFIRMING_OFF_DUTY" # 离岗确认中
|
|
|
|
|
|
STATE_OFF_DUTY_COUNTDOWN = "OFF_DUTY_COUNTDOWN" # 离岗倒计时中
|
|
|
|
|
|
STATE_ALARMED = "ALARMED" # 已告警(等待回岗)
|
|
|
|
|
|
STATE_NON_WORK_TIME = "NON_WORK_TIME" # 非工作时间
|
2026-01-29 18:33:12 +08:00
|
|
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
|
|
self,
|
2026-02-12 15:41:05 +08:00
|
|
|
|
confirm_on_duty_sec: int = 10, # 上岗确认窗口(持续检测到人的时长)
|
|
|
|
|
|
confirm_off_duty_sec: int = 30, # 离岗确认窗口(持续未检测到人的时长)
|
|
|
|
|
|
confirm_return_sec: int = 10, # 回岗确认窗口(告警后回来需确认时长)
|
|
|
|
|
|
leave_countdown_sec: int = 300, # 离岗倒计时(确认离岗后等待告警的时长)
|
|
|
|
|
|
cooldown_sec: int = 600, # 告警冷却期(两次告警的最小间隔)
|
2026-01-29 18:33:12 +08:00
|
|
|
|
working_hours: Optional[List[Dict]] = None,
|
2026-01-30 13:23:22 +08:00
|
|
|
|
target_class: Optional[str] = "person",
|
2026-02-12 15:41:05 +08:00
|
|
|
|
# 兼容旧参数名(向后兼容)
|
|
|
|
|
|
confirm_leave_sec: Optional[int] = None,
|
2026-01-29 18:33:12 +08:00
|
|
|
|
):
|
2026-02-12 15:41:05 +08:00
|
|
|
|
# 时间参数(处理向后兼容)
|
2026-01-30 13:23:22 +08:00
|
|
|
|
self.confirm_on_duty_sec = confirm_on_duty_sec
|
2026-02-12 15:41:05 +08:00
|
|
|
|
self.confirm_off_duty_sec = confirm_leave_sec if confirm_leave_sec is not None else confirm_off_duty_sec
|
|
|
|
|
|
self.confirm_return_sec = confirm_return_sec
|
|
|
|
|
|
self.leave_countdown_sec = leave_countdown_sec
|
2026-01-30 13:23:22 +08:00
|
|
|
|
self.cooldown_sec = cooldown_sec
|
2026-02-12 15:41:05 +08:00
|
|
|
|
|
|
|
|
|
|
# 工作时间和目标类别
|
2026-01-29 18:33:12 +08:00
|
|
|
|
self.working_hours = working_hours or []
|
2026-01-30 13:23:22 +08:00
|
|
|
|
self.target_class = target_class
|
2026-01-29 18:33:12 +08:00
|
|
|
|
|
2026-02-12 15:41:05 +08:00
|
|
|
|
# 状态变量
|
|
|
|
|
|
self.state: str = self.STATE_INIT
|
2026-01-29 18:33:12 +08:00
|
|
|
|
self.state_start_time: Optional[datetime] = None
|
2026-01-30 13:23:22 +08:00
|
|
|
|
|
2026-02-12 15:41:05 +08:00
|
|
|
|
# 滑动窗口(用于平滑检测结果)
|
|
|
|
|
|
self.detection_window: deque = deque() # [(timestamp, has_person), ...]
|
|
|
|
|
|
self.window_size_sec = 10 # 滑动窗口大小:10秒
|
2026-01-29 18:33:12 +08:00
|
|
|
|
|
2026-02-12 15:41:05 +08:00
|
|
|
|
# 告警追踪
|
|
|
|
|
|
self._last_alarm_id: Optional[str] = None
|
|
|
|
|
|
self._leave_start_time: Optional[datetime] = None # 人员离开时间(用于计算持续时长)
|
|
|
|
|
|
self._alarm_triggered_time: Optional[datetime] = None # 告警触发时间
|
|
|
|
|
|
self.alert_cooldowns: Dict[str, datetime] = {}
|
2026-02-11 17:55:35 +08:00
|
|
|
|
|
2026-01-30 13:23:22 +08:00
|
|
|
|
def _is_in_working_hours(self, dt: Optional[datetime] = None) -> bool:
|
2026-02-12 15:41:05 +08:00
|
|
|
|
"""检查是否在工作时间"""
|
2026-01-29 18:33:12 +08:00
|
|
|
|
if not self.working_hours:
|
|
|
|
|
|
return True
|
2026-02-12 15:41:05 +08:00
|
|
|
|
|
2026-02-02 14:00:21 +08:00
|
|
|
|
import json
|
2026-02-12 15:41:05 +08:00
|
|
|
|
|
2026-02-02 14:00:21 +08:00
|
|
|
|
working_hours = self.working_hours
|
|
|
|
|
|
if isinstance(working_hours, str):
|
|
|
|
|
|
try:
|
|
|
|
|
|
working_hours = json.loads(working_hours)
|
|
|
|
|
|
except:
|
|
|
|
|
|
return True
|
2026-02-12 15:41:05 +08:00
|
|
|
|
|
2026-02-02 14:00:21 +08:00
|
|
|
|
if not working_hours:
|
|
|
|
|
|
return True
|
2026-02-12 15:41:05 +08:00
|
|
|
|
|
2026-01-29 18:33:12 +08:00
|
|
|
|
dt = dt or datetime.now()
|
|
|
|
|
|
current_minutes = dt.hour * 60 + dt.minute
|
2026-02-02 14:00:21 +08:00
|
|
|
|
for period in working_hours:
|
|
|
|
|
|
start_str = period["start"] if isinstance(period, dict) else period
|
|
|
|
|
|
end_str = period["end"] if isinstance(period, dict) else period
|
|
|
|
|
|
start_minutes = self._parse_time_to_minutes(start_str)
|
|
|
|
|
|
end_minutes = self._parse_time_to_minutes(end_str)
|
2026-01-29 18:33:12 +08:00
|
|
|
|
if start_minutes <= current_minutes < end_minutes:
|
|
|
|
|
|
return True
|
|
|
|
|
|
return False
|
2026-02-12 15:41:05 +08:00
|
|
|
|
|
2026-02-02 14:00:21 +08:00
|
|
|
|
def _parse_time_to_minutes(self, time_str: str) -> int:
|
|
|
|
|
|
"""将时间字符串转换为分钟数"""
|
|
|
|
|
|
if isinstance(time_str, int):
|
|
|
|
|
|
return time_str
|
|
|
|
|
|
try:
|
|
|
|
|
|
parts = time_str.split(":")
|
|
|
|
|
|
return int(parts[0]) * 60 + int(parts[1])
|
|
|
|
|
|
except:
|
|
|
|
|
|
return 0
|
2026-01-29 18:33:12 +08:00
|
|
|
|
|
2026-01-30 13:23:22 +08:00
|
|
|
|
def _check_detection_in_roi(self, detection: Dict, roi_id: str) -> bool:
|
2026-02-12 15:41:05 +08:00
|
|
|
|
"""检查检测结果是否在ROI内"""
|
2026-01-29 18:33:12 +08:00
|
|
|
|
matched_rois = detection.get("matched_rois", [])
|
2026-02-12 15:41:05 +08:00
|
|
|
|
return any(roi.get("roi_id") == roi_id for roi in matched_rois)
|
2026-01-29 18:33:12 +08:00
|
|
|
|
|
2026-02-12 15:41:05 +08:00
|
|
|
|
def _check_target_class(self, detection: Dict, target_class: str) -> bool:
|
|
|
|
|
|
"""检查是否为目标类别"""
|
2026-01-30 13:23:22 +08:00
|
|
|
|
if not target_class:
|
|
|
|
|
|
return True
|
|
|
|
|
|
return detection.get("class") == target_class
|
|
|
|
|
|
|
|
|
|
|
|
def _get_latest_bbox(self, tracks: List[Dict], roi_id: str) -> List[float]:
|
2026-02-12 15:41:05 +08:00
|
|
|
|
"""获取ROI内最新的检测框"""
|
2026-01-30 13:23:22 +08:00
|
|
|
|
for det in tracks:
|
2026-02-12 15:41:05 +08:00
|
|
|
|
if self._check_detection_in_roi(det, roi_id) and self._check_target_class(det, self.target_class):
|
2026-01-30 13:23:22 +08:00
|
|
|
|
return det.get("bbox", [])
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
2026-02-12 15:41:05 +08:00
|
|
|
|
def _update_detection_window(self, current_time: datetime, has_person: bool):
|
|
|
|
|
|
"""更新滑动窗口"""
|
|
|
|
|
|
self.detection_window.append((current_time, has_person))
|
|
|
|
|
|
|
|
|
|
|
|
# 移除窗口外的旧数据
|
|
|
|
|
|
while self.detection_window:
|
|
|
|
|
|
oldest_time, _ = self.detection_window[0]
|
|
|
|
|
|
if (current_time - oldest_time).total_seconds() > self.window_size_sec:
|
|
|
|
|
|
self.detection_window.popleft()
|
|
|
|
|
|
else:
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
def _get_detection_ratio(self) -> float:
|
|
|
|
|
|
"""计算滑动窗口内的检测命中率"""
|
|
|
|
|
|
if not self.detection_window:
|
|
|
|
|
|
return 0.0
|
|
|
|
|
|
|
|
|
|
|
|
person_count = sum(1 for _, has_person in self.detection_window if has_person)
|
|
|
|
|
|
return person_count / len(self.detection_window)
|
|
|
|
|
|
|
2026-01-29 18:33:12 +08:00
|
|
|
|
def process(
|
|
|
|
|
|
self,
|
|
|
|
|
|
roi_id: str,
|
|
|
|
|
|
camera_id: str,
|
|
|
|
|
|
tracks: List[Dict],
|
|
|
|
|
|
current_time: Optional[datetime] = None,
|
|
|
|
|
|
) -> List[Dict]:
|
2026-02-12 15:41:05 +08:00
|
|
|
|
"""
|
|
|
|
|
|
处理单帧检测结果
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
roi_id: ROI区域ID
|
|
|
|
|
|
camera_id: 摄像头ID
|
|
|
|
|
|
tracks: 检测结果列表
|
|
|
|
|
|
current_time: 当前时间(用于测试,生产环境传None)
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
告警列表 [{"alert_type": "leave_post", ...}, {"alert_type": "alarm_resolve", ...}]
|
|
|
|
|
|
"""
|
2026-01-29 18:33:12 +08:00
|
|
|
|
current_time = current_time or datetime.now()
|
|
|
|
|
|
alerts = []
|
|
|
|
|
|
|
2026-02-12 15:41:05 +08:00
|
|
|
|
# 检查ROI内是否有目标
|
|
|
|
|
|
roi_has_person = any(
|
|
|
|
|
|
self._check_detection_in_roi(det, roi_id) and self._check_target_class(det, self.target_class)
|
|
|
|
|
|
for det in tracks
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 更新滑动窗口
|
|
|
|
|
|
self._update_detection_window(current_time, roi_has_person)
|
|
|
|
|
|
detection_ratio = self._get_detection_ratio()
|
|
|
|
|
|
|
|
|
|
|
|
# 检查工作时间
|
|
|
|
|
|
in_working_hours = self._is_in_working_hours(current_time)
|
|
|
|
|
|
|
|
|
|
|
|
# === 非工作时间处理 ===
|
|
|
|
|
|
if not in_working_hours:
|
|
|
|
|
|
if self.state != self.STATE_NON_WORK_TIME:
|
|
|
|
|
|
# 进入非工作时间,清理状态
|
|
|
|
|
|
if self._last_alarm_id and self._leave_start_time:
|
|
|
|
|
|
# 如果有未结束的告警,发送resolve事件(非工作时间自动关闭)
|
|
|
|
|
|
duration_ms = int((current_time - self._leave_start_time).total_seconds() * 1000)
|
|
|
|
|
|
alerts.append({
|
|
|
|
|
|
"alert_type": "alarm_resolve",
|
|
|
|
|
|
"resolve_alarm_id": self._last_alarm_id,
|
|
|
|
|
|
"duration_ms": duration_ms,
|
|
|
|
|
|
"last_frame_time": current_time.strftime('%Y-%m-%d %H:%M:%S'),
|
|
|
|
|
|
"resolve_type": "non_work_time",
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
self.state = self.STATE_NON_WORK_TIME
|
|
|
|
|
|
self.state_start_time = None
|
|
|
|
|
|
self.detection_window.clear()
|
2026-02-11 17:55:35 +08:00
|
|
|
|
self._last_alarm_id = None
|
|
|
|
|
|
self._leave_start_time = None
|
2026-02-12 15:41:05 +08:00
|
|
|
|
self._alarm_triggered_time = None
|
2026-02-11 17:55:35 +08:00
|
|
|
|
return alerts
|
2026-01-29 18:33:12 +08:00
|
|
|
|
|
2026-02-12 15:41:05 +08:00
|
|
|
|
# === 工作时间处理 ===
|
|
|
|
|
|
|
|
|
|
|
|
# 从非工作时间恢复
|
2026-01-30 13:23:22 +08:00
|
|
|
|
if self.state == self.STATE_NON_WORK_TIME:
|
2026-02-12 15:41:05 +08:00
|
|
|
|
self.state = self.STATE_INIT
|
|
|
|
|
|
self.state_start_time = current_time
|
|
|
|
|
|
self.detection_window.clear()
|
2026-01-29 18:33:12 +08:00
|
|
|
|
|
2026-02-12 15:41:05 +08:00
|
|
|
|
# === 状态机处理 ===
|
2026-01-30 13:23:22 +08:00
|
|
|
|
|
2026-02-12 15:41:05 +08:00
|
|
|
|
if self.state == self.STATE_INIT:
|
|
|
|
|
|
# 初始化状态:等待检测到人
|
2026-01-30 13:23:22 +08:00
|
|
|
|
if roi_has_person:
|
2026-02-12 15:41:05 +08:00
|
|
|
|
self.state = self.STATE_CONFIRMING_ON_DUTY
|
2026-01-30 13:23:22 +08:00
|
|
|
|
self.state_start_time = current_time
|
2026-02-12 15:41:05 +08:00
|
|
|
|
logger.debug(f"ROI {roi_id}: INIT → CONFIRMING_ON_DUTY")
|
2026-01-30 13:23:22 +08:00
|
|
|
|
|
2026-02-12 15:41:05 +08:00
|
|
|
|
elif self.state == self.STATE_CONFIRMING_ON_DUTY:
|
|
|
|
|
|
# 上岗确认中:需要持续检测到人
|
|
|
|
|
|
elapsed = (current_time - self.state_start_time).total_seconds()
|
|
|
|
|
|
|
|
|
|
|
|
if detection_ratio == 0:
|
|
|
|
|
|
# 人消失了,回到INIT
|
|
|
|
|
|
self.state = self.STATE_INIT
|
|
|
|
|
|
self.state_start_time = current_time
|
|
|
|
|
|
self.detection_window.clear()
|
|
|
|
|
|
logger.debug(f"ROI {roi_id}: CONFIRMING_ON_DUTY → INIT (人消失)")
|
|
|
|
|
|
elif elapsed >= self.confirm_on_duty_sec and detection_ratio >= 0.7:
|
|
|
|
|
|
# 上岗确认成功(命中率>=70%)
|
|
|
|
|
|
self.state = self.STATE_ON_DUTY
|
|
|
|
|
|
self.state_start_time = current_time
|
|
|
|
|
|
self.alert_cooldowns.clear() # 确认在岗后清除冷却记录
|
|
|
|
|
|
logger.info(f"ROI {roi_id}: CONFIRMING_ON_DUTY → ON_DUTY (上岗确认成功)")
|
2026-02-11 10:01:20 +08:00
|
|
|
|
|
2026-01-30 13:23:22 +08:00
|
|
|
|
elif self.state == self.STATE_ON_DUTY:
|
2026-02-12 15:41:05 +08:00
|
|
|
|
# 在岗状态:监控是否离岗
|
|
|
|
|
|
if detection_ratio == 0:
|
|
|
|
|
|
# 滑动窗口内完全没有人,进入离岗确认
|
|
|
|
|
|
self.state = self.STATE_CONFIRMING_OFF_DUTY
|
2026-01-30 13:23:22 +08:00
|
|
|
|
self.state_start_time = current_time
|
2026-02-12 15:41:05 +08:00
|
|
|
|
logger.debug(f"ROI {roi_id}: ON_DUTY → CONFIRMING_OFF_DUTY")
|
2026-01-30 13:23:22 +08:00
|
|
|
|
|
2026-02-12 15:41:05 +08:00
|
|
|
|
elif self.state == self.STATE_CONFIRMING_OFF_DUTY:
|
|
|
|
|
|
# 离岗确认中:需要持续未检测到人
|
2026-01-30 13:23:22 +08:00
|
|
|
|
elapsed = (current_time - self.state_start_time).total_seconds()
|
|
|
|
|
|
|
|
|
|
|
|
if roi_has_person:
|
2026-02-12 15:06:46 +08:00
|
|
|
|
# 人回来了,回到ON_DUTY
|
2026-01-30 13:23:22 +08:00
|
|
|
|
self.state = self.STATE_ON_DUTY
|
|
|
|
|
|
self.state_start_time = current_time
|
2026-02-12 15:41:05 +08:00
|
|
|
|
logger.debug(f"ROI {roi_id}: CONFIRMING_OFF_DUTY → ON_DUTY (人回来了)")
|
|
|
|
|
|
elif elapsed >= self.confirm_off_duty_sec and detection_ratio == 0:
|
|
|
|
|
|
# 离岗确认成功,进入倒计时
|
|
|
|
|
|
self.state = self.STATE_OFF_DUTY_COUNTDOWN
|
2026-01-30 13:23:22 +08:00
|
|
|
|
self.state_start_time = current_time
|
2026-02-12 15:41:05 +08:00
|
|
|
|
self._leave_start_time = self.state_start_time # 记录离开时间
|
|
|
|
|
|
logger.info(f"ROI {roi_id}: CONFIRMING_OFF_DUTY → OFF_DUTY_COUNTDOWN (离岗确认成功)")
|
2026-02-11 17:55:35 +08:00
|
|
|
|
|
2026-02-12 15:41:05 +08:00
|
|
|
|
elif self.state == self.STATE_OFF_DUTY_COUNTDOWN:
|
|
|
|
|
|
# 离岗倒计时中:等待告警触发
|
|
|
|
|
|
elapsed = (current_time - self.state_start_time).total_seconds()
|
2026-02-12 15:06:46 +08:00
|
|
|
|
|
2026-01-30 13:23:22 +08:00
|
|
|
|
if roi_has_person:
|
2026-02-12 15:41:05 +08:00
|
|
|
|
# 倒计时期间人回来了,回到ON_DUTY(未触发告警)
|
|
|
|
|
|
self.state = self.STATE_ON_DUTY
|
2026-02-11 17:55:35 +08:00
|
|
|
|
self.state_start_time = current_time
|
2026-02-12 15:41:05 +08:00
|
|
|
|
self._leave_start_time = None
|
|
|
|
|
|
logger.info(f"ROI {roi_id}: OFF_DUTY_COUNTDOWN → ON_DUTY (倒计时期间回来)")
|
|
|
|
|
|
elif elapsed >= self.leave_countdown_sec:
|
|
|
|
|
|
# 倒计时结束,触发告警
|
2026-02-12 15:06:46 +08:00
|
|
|
|
cooldown_key = f"{camera_id}_{roi_id}"
|
2026-02-12 15:41:05 +08:00
|
|
|
|
if cooldown_key not in self.alert_cooldowns or \
|
|
|
|
|
|
(current_time - self.alert_cooldowns[cooldown_key]).total_seconds() > self.cooldown_sec:
|
|
|
|
|
|
|
2026-02-12 15:06:46 +08:00
|
|
|
|
bbox = self._get_latest_bbox(tracks, roi_id)
|
2026-02-12 15:41:05 +08:00
|
|
|
|
total_off_duty_sec = (current_time - self._leave_start_time).total_seconds()
|
|
|
|
|
|
elapsed_minutes = int(total_off_duty_sec / 60)
|
|
|
|
|
|
|
2026-02-12 15:06:46 +08:00
|
|
|
|
alerts.append({
|
|
|
|
|
|
"track_id": roi_id,
|
|
|
|
|
|
"camera_id": camera_id,
|
|
|
|
|
|
"bbox": bbox,
|
|
|
|
|
|
"duration_minutes": elapsed_minutes,
|
|
|
|
|
|
"alert_type": "leave_post",
|
|
|
|
|
|
"message": f"离岗 {elapsed_minutes} 分钟",
|
|
|
|
|
|
})
|
2026-02-12 15:41:05 +08:00
|
|
|
|
|
|
|
|
|
|
self.alert_cooldowns[cooldown_key] = current_time
|
|
|
|
|
|
self._alarm_triggered_time = current_time
|
|
|
|
|
|
self.state = self.STATE_ALARMED
|
|
|
|
|
|
# _last_alarm_id 由 main.py 通过 set_last_alarm_id() 回填
|
|
|
|
|
|
logger.warning(f"ROI {roi_id}: OFF_DUTY_COUNTDOWN → ALARMED (告警触发)")
|
|
|
|
|
|
|
|
|
|
|
|
elif self.state == self.STATE_ALARMED:
|
|
|
|
|
|
# 已告警状态:等待人员回岗
|
|
|
|
|
|
if roi_has_person:
|
|
|
|
|
|
# 检测到人,进入回岗确认
|
|
|
|
|
|
self.state = self.STATE_CONFIRMING_ON_DUTY # 复用上岗确认状态
|
|
|
|
|
|
self.state_start_time = current_time
|
|
|
|
|
|
logger.info(f"ROI {roi_id}: ALARMED → CONFIRMING_ON_DUTY (检测到人回岗)")
|
|
|
|
|
|
|
|
|
|
|
|
# 特殊处理:从CONFIRMING_ON_DUTY再次确认上岗时,如果有未结束的告警,发送resolve
|
|
|
|
|
|
if self.state == self.STATE_ON_DUTY and self._last_alarm_id:
|
|
|
|
|
|
# 回岗确认成功,发送resolve事件
|
|
|
|
|
|
duration_ms = int((current_time - self._leave_start_time).total_seconds() * 1000)
|
|
|
|
|
|
alerts.append({
|
|
|
|
|
|
"alert_type": "alarm_resolve",
|
|
|
|
|
|
"resolve_alarm_id": self._last_alarm_id,
|
|
|
|
|
|
"duration_ms": duration_ms,
|
|
|
|
|
|
"last_frame_time": current_time.strftime('%Y-%m-%d %H:%M:%S'),
|
|
|
|
|
|
"resolve_type": "person_returned",
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
# 清理告警追踪信息
|
|
|
|
|
|
self._last_alarm_id = None
|
|
|
|
|
|
self._leave_start_time = None
|
|
|
|
|
|
self._alarm_triggered_time = None
|
|
|
|
|
|
logger.info(f"ROI {roi_id}: 告警已解决(人员回岗)")
|
2026-01-30 13:23:22 +08:00
|
|
|
|
|
|
|
|
|
|
return alerts
|
2026-01-29 18:33:12 +08:00
|
|
|
|
|
2026-02-11 17:55:35 +08:00
|
|
|
|
def set_last_alarm_id(self, alarm_id: str):
|
|
|
|
|
|
"""由 main.py 在告警生成后回填 alarm_id"""
|
|
|
|
|
|
self._last_alarm_id = alarm_id
|
|
|
|
|
|
|
2026-01-29 18:33:12 +08:00
|
|
|
|
def reset(self):
|
2026-02-12 15:41:05 +08:00
|
|
|
|
"""重置算法状态"""
|
|
|
|
|
|
self.state = self.STATE_INIT
|
2026-01-29 18:33:12 +08:00
|
|
|
|
self.state_start_time = None
|
2026-02-12 15:41:05 +08:00
|
|
|
|
self.detection_window.clear()
|
2026-02-11 17:55:35 +08:00
|
|
|
|
self._last_alarm_id = None
|
|
|
|
|
|
self._leave_start_time = None
|
2026-02-12 15:41:05 +08:00
|
|
|
|
self._alarm_triggered_time = None
|
|
|
|
|
|
self.alert_cooldowns.clear()
|
2026-01-29 18:33:12 +08:00
|
|
|
|
|
2026-01-30 13:23:22 +08:00
|
|
|
|
def get_state(self, roi_id: str) -> Dict[str, Any]:
|
2026-02-12 15:41:05 +08:00
|
|
|
|
"""获取当前状态(用于调试和监控)"""
|
|
|
|
|
|
state_info = {
|
2026-01-29 18:33:12 +08:00
|
|
|
|
"state": self.state,
|
2026-02-12 15:41:05 +08:00
|
|
|
|
"state_start_time": self.state_start_time.isoformat() if self.state_start_time else None,
|
|
|
|
|
|
"detection_ratio": self._get_detection_ratio(),
|
|
|
|
|
|
"window_size": len(self.detection_window),
|
2026-01-29 18:33:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-12 15:41:05 +08:00
|
|
|
|
# 添加状态特定信息
|
|
|
|
|
|
if self.state == self.STATE_OFF_DUTY_COUNTDOWN and self.state_start_time:
|
|
|
|
|
|
elapsed = (datetime.now() - self.state_start_time).total_seconds()
|
|
|
|
|
|
state_info["countdown_remaining_sec"] = max(0, self.leave_countdown_sec - elapsed)
|
|
|
|
|
|
|
|
|
|
|
|
if self.state == self.STATE_ALARMED and self._leave_start_time:
|
|
|
|
|
|
total_off_duty_sec = (datetime.now() - self._leave_start_time).total_seconds()
|
|
|
|
|
|
state_info["total_off_duty_sec"] = total_off_duty_sec
|
|
|
|
|
|
state_info["alarm_id"] = self._last_alarm_id
|
|
|
|
|
|
|
|
|
|
|
|
return state_info
|
|
|
|
|
|
|
2026-01-29 18:33:12 +08:00
|
|
|
|
|
|
|
|
|
|
class IntrusionAlgorithm:
|
2026-01-30 13:23:22 +08:00
|
|
|
|
def __init__(
|
|
|
|
|
|
self,
|
2026-02-11 09:57:02 +08:00
|
|
|
|
cooldown_seconds: int = 300,
|
2026-01-30 13:27:28 +08:00
|
|
|
|
confirm_seconds: int = 5,
|
2026-01-30 13:23:22 +08:00
|
|
|
|
target_class: Optional[str] = None,
|
|
|
|
|
|
):
|
2026-01-29 18:33:12 +08:00
|
|
|
|
self.cooldown_seconds = cooldown_seconds
|
2026-01-30 13:23:22 +08:00
|
|
|
|
self.confirm_seconds = confirm_seconds
|
|
|
|
|
|
self.target_class = target_class
|
|
|
|
|
|
|
|
|
|
|
|
self.last_alert_time: Dict[str, datetime] = {}
|
2026-01-29 18:33:12 +08:00
|
|
|
|
self.alert_triggered: Dict[str, bool] = {}
|
2026-01-30 13:23:22 +08:00
|
|
|
|
self.detection_start: Dict[str, Optional[datetime]] = {}
|
2026-01-29 18:33:12 +08:00
|
|
|
|
|
2026-01-30 13:23:22 +08:00
|
|
|
|
def _check_detection_in_roi(self, detection: Dict, roi_id: str) -> bool:
|
|
|
|
|
|
matched_rois = detection.get("matched_rois", [])
|
|
|
|
|
|
for roi in matched_rois:
|
|
|
|
|
|
if roi.get("roi_id") == roi_id:
|
|
|
|
|
|
return True
|
2026-01-29 18:33:12 +08:00
|
|
|
|
return False
|
|
|
|
|
|
|
2026-01-30 13:23:22 +08:00
|
|
|
|
def _check_target_class(self, detection: Dict, target_class: Optional[str]) -> bool:
|
|
|
|
|
|
if not target_class:
|
|
|
|
|
|
return True
|
|
|
|
|
|
return detection.get("class") == target_class
|
|
|
|
|
|
|
|
|
|
|
|
def _get_latest_bbox(self, tracks: List[Dict], roi_id: str) -> List[float]:
|
2026-01-29 18:33:12 +08:00
|
|
|
|
for det in tracks:
|
2026-01-30 13:23:22 +08:00
|
|
|
|
if self._check_detection_in_roi(det, roi_id):
|
|
|
|
|
|
return det.get("bbox", [])
|
2026-01-29 18:33:12 +08:00
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
def process(
|
|
|
|
|
|
self,
|
|
|
|
|
|
roi_id: str,
|
|
|
|
|
|
camera_id: str,
|
|
|
|
|
|
tracks: List[Dict],
|
|
|
|
|
|
current_time: Optional[datetime] = None,
|
|
|
|
|
|
) -> List[Dict]:
|
2026-01-30 13:23:22 +08:00
|
|
|
|
current_time = current_time or datetime.now()
|
|
|
|
|
|
key = f"{camera_id}_{roi_id}"
|
|
|
|
|
|
|
|
|
|
|
|
roi_has_person = False
|
|
|
|
|
|
for det in tracks:
|
|
|
|
|
|
if self._check_detection_in_roi(det, roi_id) and self._check_target_class(det, self.target_class):
|
|
|
|
|
|
roi_has_person = True
|
|
|
|
|
|
break
|
2026-01-29 18:33:12 +08:00
|
|
|
|
|
|
|
|
|
|
if not roi_has_person:
|
2026-01-30 13:23:22 +08:00
|
|
|
|
self.detection_start.pop(key, None)
|
|
|
|
|
|
self.alert_triggered[key] = False
|
2026-01-29 18:33:12 +08:00
|
|
|
|
return []
|
|
|
|
|
|
|
2026-01-30 13:23:22 +08:00
|
|
|
|
if self.alert_triggered.get(key, False):
|
|
|
|
|
|
elapsed_since_alert = (current_time - self.last_alert_time.get(key, datetime.min)).total_seconds()
|
|
|
|
|
|
if elapsed_since_alert < self.cooldown_seconds:
|
|
|
|
|
|
return []
|
2026-01-29 18:33:12 +08:00
|
|
|
|
self.alert_triggered[key] = False
|
|
|
|
|
|
|
2026-01-30 13:23:22 +08:00
|
|
|
|
if self.detection_start.get(key) is None:
|
|
|
|
|
|
self.detection_start[key] = current_time
|
2026-01-29 18:33:12 +08:00
|
|
|
|
|
2026-01-30 13:23:22 +08:00
|
|
|
|
elapsed = (current_time - self.detection_start[key]).total_seconds()
|
|
|
|
|
|
if elapsed < self.confirm_seconds:
|
2026-01-29 18:33:12 +08:00
|
|
|
|
return []
|
|
|
|
|
|
|
2026-01-30 13:23:22 +08:00
|
|
|
|
bbox = self._get_latest_bbox(tracks, roi_id)
|
|
|
|
|
|
self.last_alert_time[key] = current_time
|
2026-01-29 18:33:12 +08:00
|
|
|
|
self.alert_triggered[key] = True
|
2026-01-30 13:23:22 +08:00
|
|
|
|
self.detection_start[key] = None
|
2026-01-29 18:33:12 +08:00
|
|
|
|
|
|
|
|
|
|
return [{
|
|
|
|
|
|
"roi_id": roi_id,
|
2026-01-30 13:23:22 +08:00
|
|
|
|
"camera_id": camera_id,
|
2026-01-29 18:33:12 +08:00
|
|
|
|
"bbox": bbox,
|
|
|
|
|
|
"alert_type": "intrusion",
|
2026-02-11 09:57:02 +08:00
|
|
|
|
"alarm_level": 3,
|
2026-01-29 18:33:12 +08:00
|
|
|
|
"message": "检测到周界入侵",
|
|
|
|
|
|
}]
|
|
|
|
|
|
|
|
|
|
|
|
def reset(self):
|
|
|
|
|
|
self.last_alert_time.clear()
|
|
|
|
|
|
self.alert_triggered.clear()
|
2026-01-30 13:23:22 +08:00
|
|
|
|
self.detection_start.clear()
|
2026-01-29 18:33:12 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-02-05 15:16:47 +08:00
|
|
|
|
# class CrowdDetectionAlgorithm:
|
|
|
|
|
|
# """人群聚集检测算法 - 暂时注释,后续需要时再启用"""
|
|
|
|
|
|
#
|
|
|
|
|
|
# def __init__(
|
|
|
|
|
|
# self,
|
|
|
|
|
|
# max_count: int = 10,
|
|
|
|
|
|
# cooldown_seconds: int = 300,
|
|
|
|
|
|
# target_class: Optional[str] = "person",
|
|
|
|
|
|
# ):
|
|
|
|
|
|
# self.max_count = max_count
|
|
|
|
|
|
# self.cooldown_seconds = cooldown_seconds
|
|
|
|
|
|
# self.target_class = target_class
|
|
|
|
|
|
#
|
|
|
|
|
|
# self.last_alert_time: Dict[str, datetime] = {}
|
|
|
|
|
|
# self.alert_triggered: Dict[str, bool] = {}
|
|
|
|
|
|
#
|
|
|
|
|
|
# def _check_detection_in_roi(self, detection: Dict, roi_id: str) -> bool:
|
|
|
|
|
|
# matched_rois = detection.get("matched_rois", [])
|
|
|
|
|
|
# for roi in matched_rois:
|
|
|
|
|
|
# if roi.get("roi_id") == roi_id:
|
|
|
|
|
|
# return True
|
|
|
|
|
|
# return False
|
|
|
|
|
|
#
|
|
|
|
|
|
# def _check_target_class(self, detection: Dict, target_class: Optional[str]) -> bool:
|
|
|
|
|
|
# if not target_class:
|
|
|
|
|
|
# return True
|
|
|
|
|
|
# return detection.get("class") == target_class
|
|
|
|
|
|
#
|
|
|
|
|
|
# def _get_bboxes(self, tracks: List[Dict], roi_id: str) -> List[List[float]]:
|
|
|
|
|
|
# bboxes = []
|
|
|
|
|
|
# for det in tracks:
|
|
|
|
|
|
# if self._check_detection_in_roi(det, roi_id) and self._check_target_class(det, self.target_class):
|
|
|
|
|
|
# bboxes.append(det.get("bbox", []))
|
|
|
|
|
|
# return bboxes
|
|
|
|
|
|
#
|
|
|
|
|
|
# def process(
|
|
|
|
|
|
# self,
|
|
|
|
|
|
# roi_id: str,
|
|
|
|
|
|
# camera_id: str,
|
|
|
|
|
|
# tracks: List[Dict],
|
|
|
|
|
|
# current_time: Optional[datetime] = None,
|
|
|
|
|
|
# ) -> List[Dict]:
|
|
|
|
|
|
# current_time = current_time or datetime.now()
|
|
|
|
|
|
# key = f"{camera_id}_{roi_id}"
|
|
|
|
|
|
#
|
|
|
|
|
|
# person_count = 0
|
|
|
|
|
|
# for det in tracks:
|
|
|
|
|
|
# if self._check_detection_in_roi(det, roi_id) and self._check_target_class(det, self.target_class):
|
|
|
|
|
|
# person_count += 1
|
|
|
|
|
|
#
|
|
|
|
|
|
# if person_count <= self.max_count:
|
|
|
|
|
|
# self.alert_triggered[key] = False
|
|
|
|
|
|
# return []
|
|
|
|
|
|
#
|
|
|
|
|
|
# if self.alert_triggered.get(key, False):
|
|
|
|
|
|
# elapsed_since_alert = (current_time - self.last_alert_time.get(key, datetime.min)).total_seconds()
|
|
|
|
|
|
# if elapsed_since_alert < self.cooldown_seconds:
|
|
|
|
|
|
# return []
|
|
|
|
|
|
# self.alert_triggered[key] = False
|
|
|
|
|
|
#
|
|
|
|
|
|
# bboxes = self._get_bboxes(tracks, roi_id)
|
|
|
|
|
|
# self.last_alert_time[key] = current_time
|
|
|
|
|
|
# self.alert_triggered[key] = True
|
|
|
|
|
|
#
|
|
|
|
|
|
# return [{
|
|
|
|
|
|
# "roi_id": roi_id,
|
|
|
|
|
|
# "camera_id": camera_id,
|
|
|
|
|
|
# "bbox": bboxes[0] if bboxes else [],
|
|
|
|
|
|
# "alert_type": "crowd_detection",
|
|
|
|
|
|
# "message": f"检测到人群聚集,当前人数: {person_count}",
|
|
|
|
|
|
# "count": person_count,
|
|
|
|
|
|
# }]
|
|
|
|
|
|
#
|
|
|
|
|
|
# def reset(self):
|
|
|
|
|
|
# self.last_alert_time.clear()
|
|
|
|
|
|
# self.alert_triggered.clear()
|
2026-02-03 14:26:52 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-01-29 18:33:12 +08:00
|
|
|
|
class AlgorithmManager:
|
|
|
|
|
|
def __init__(self, working_hours: Optional[List[Dict]] = None):
|
|
|
|
|
|
self.algorithms: Dict[str, Dict[str, Any]] = {}
|
|
|
|
|
|
self.working_hours = working_hours or []
|
2026-01-30 13:51:58 +08:00
|
|
|
|
self._update_lock = threading.Lock()
|
2026-02-04 16:47:26 +08:00
|
|
|
|
self._registered_keys: set = set() # 已注册的 (roi_id, bind_id, algo_type) 缓存
|
2026-01-29 18:33:12 +08:00
|
|
|
|
|
|
|
|
|
|
self.default_params = {
|
|
|
|
|
|
"leave_post": {
|
2026-01-30 13:23:22 +08:00
|
|
|
|
"confirm_on_duty_sec": 10,
|
2026-02-11 10:01:20 +08:00
|
|
|
|
"confirm_leave_sec": 30,
|
2026-02-11 09:57:02 +08:00
|
|
|
|
"cooldown_sec": 600,
|
2026-01-30 13:23:22 +08:00
|
|
|
|
"target_class": "person",
|
2026-01-29 18:33:12 +08:00
|
|
|
|
},
|
|
|
|
|
|
"intrusion": {
|
2026-02-11 09:57:02 +08:00
|
|
|
|
"cooldown_seconds": 300,
|
2026-01-30 13:27:28 +08:00
|
|
|
|
"confirm_seconds": 5,
|
2026-01-30 13:23:22 +08:00
|
|
|
|
"target_class": None,
|
2026-01-29 18:33:12 +08:00
|
|
|
|
},
|
2026-02-05 15:16:47 +08:00
|
|
|
|
# "crowd_detection": {
|
|
|
|
|
|
# "max_count": 10,
|
|
|
|
|
|
# "cooldown_seconds": 300,
|
|
|
|
|
|
# "target_class": "person",
|
|
|
|
|
|
# },
|
2026-01-29 18:33:12 +08:00
|
|
|
|
}
|
2026-01-30 13:51:58 +08:00
|
|
|
|
|
|
|
|
|
|
self._pubsub = None
|
|
|
|
|
|
self._pubsub_thread = None
|
|
|
|
|
|
self._running = False
|
|
|
|
|
|
|
|
|
|
|
|
def start_config_subscription(self):
|
|
|
|
|
|
"""启动配置变更订阅"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
from config.settings import get_settings
|
|
|
|
|
|
settings = get_settings()
|
2026-02-11 09:57:02 +08:00
|
|
|
|
if settings.config_sync_mode != "REDIS":
|
|
|
|
|
|
logger.info("CONFIG_SYNC_MODE=LOCAL: 跳过 Redis 配置订阅")
|
|
|
|
|
|
return
|
2026-01-30 13:51:58 +08:00
|
|
|
|
redis_client = redis.Redis(
|
|
|
|
|
|
host=settings.redis.host,
|
|
|
|
|
|
port=settings.redis.port,
|
|
|
|
|
|
db=settings.redis.db,
|
|
|
|
|
|
password=settings.redis.password,
|
|
|
|
|
|
decode_responses=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
self._pubsub = redis_client.pubsub()
|
|
|
|
|
|
self._pubsub.subscribe("config_update")
|
|
|
|
|
|
|
|
|
|
|
|
self._running = True
|
|
|
|
|
|
self._pubsub_thread = threading.Thread(
|
|
|
|
|
|
target=self._config_update_worker,
|
|
|
|
|
|
name="ConfigUpdateSub",
|
|
|
|
|
|
daemon=True
|
|
|
|
|
|
)
|
|
|
|
|
|
self._pubsub_thread.start()
|
|
|
|
|
|
logger.info("已启动配置变更订阅")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"启动配置订阅失败: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
def _config_update_worker(self):
|
|
|
|
|
|
"""配置更新订阅工作线程"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
for message in self._pubsub.listen():
|
|
|
|
|
|
if not self._running:
|
|
|
|
|
|
break
|
|
|
|
|
|
if message["type"] == "message":
|
|
|
|
|
|
try:
|
|
|
|
|
|
import json
|
|
|
|
|
|
data = json.loads(message["data"])
|
2026-02-05 09:59:48 +08:00
|
|
|
|
update_type = data.get("type", "full")
|
|
|
|
|
|
if update_type == "roi":
|
2026-01-30 13:51:58 +08:00
|
|
|
|
roi_ids = data.get("ids", [])
|
|
|
|
|
|
if roi_ids:
|
|
|
|
|
|
for roi_id in roi_ids:
|
|
|
|
|
|
self.reload_algorithm(roi_id)
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.reload_all_algorithms()
|
2026-02-05 09:59:48 +08:00
|
|
|
|
elif update_type == "bind":
|
2026-02-03 14:26:52 +08:00
|
|
|
|
bind_ids = data.get("ids", [])
|
|
|
|
|
|
if bind_ids:
|
|
|
|
|
|
for bind_id in bind_ids:
|
|
|
|
|
|
self.reload_bind_algorithm(bind_id)
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.reload_all_algorithms()
|
2026-02-05 09:59:48 +08:00
|
|
|
|
else:
|
|
|
|
|
|
# type="full" / "camera" / unknown → 全量重载
|
|
|
|
|
|
self.reload_all_algorithms()
|
2026-01-30 13:51:58 +08:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"处理配置更新消息失败: {e}")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"配置订阅线程异常: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
def stop_config_subscription(self):
|
|
|
|
|
|
"""停止配置变更订阅"""
|
|
|
|
|
|
self._running = False
|
|
|
|
|
|
if self._pubsub:
|
|
|
|
|
|
self._pubsub.close()
|
|
|
|
|
|
if self._pubsub_thread and self._pubsub_thread.is_alive():
|
|
|
|
|
|
self._pubsub_thread.join(timeout=5)
|
|
|
|
|
|
logger.info("配置订阅已停止")
|
|
|
|
|
|
|
2026-02-03 14:26:52 +08:00
|
|
|
|
def load_bind_from_redis(self, bind_id: str) -> bool:
|
|
|
|
|
|
"""从Redis加载单个绑定配置的算法"""
|
2026-01-30 13:51:58 +08:00
|
|
|
|
try:
|
|
|
|
|
|
from core.config_sync import get_config_sync_manager
|
|
|
|
|
|
config_manager = get_config_sync_manager()
|
2026-02-03 14:26:52 +08:00
|
|
|
|
bind_config = config_manager.get_algo_bind_from_redis(bind_id)
|
2026-01-30 13:51:58 +08:00
|
|
|
|
|
2026-02-03 14:26:52 +08:00
|
|
|
|
if not bind_config:
|
2026-01-30 13:51:58 +08:00
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
with self._update_lock:
|
2026-02-03 14:26:52 +08:00
|
|
|
|
roi_id = bind_config.get("roi_id")
|
|
|
|
|
|
algo_code = bind_config.get("algo_code", "leave_post")
|
2026-02-11 09:57:02 +08:00
|
|
|
|
raw_params = bind_config.get("params")
|
|
|
|
|
|
if isinstance(raw_params, str):
|
|
|
|
|
|
try:
|
|
|
|
|
|
import json
|
|
|
|
|
|
params = json.loads(raw_params) or {}
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
params = {}
|
|
|
|
|
|
elif isinstance(raw_params, dict):
|
|
|
|
|
|
params = raw_params
|
|
|
|
|
|
else:
|
|
|
|
|
|
params = {}
|
2026-02-03 14:26:52 +08:00
|
|
|
|
|
|
|
|
|
|
if roi_id not in self.algorithms:
|
|
|
|
|
|
self.algorithms[roi_id] = {}
|
2026-01-30 13:51:58 +08:00
|
|
|
|
|
2026-02-03 14:26:52 +08:00
|
|
|
|
key = f"{roi_id}_{bind_id}"
|
|
|
|
|
|
|
|
|
|
|
|
if algo_code == "leave_post":
|
|
|
|
|
|
algo_params = {
|
|
|
|
|
|
"confirm_on_duty_sec": params.get("confirm_on_duty_sec", 10),
|
2026-02-11 10:01:20 +08:00
|
|
|
|
"confirm_leave_sec": params.get("confirm_leave_sec", 30),
|
2026-02-11 09:57:02 +08:00
|
|
|
|
"cooldown_sec": params.get("cooldown_sec", 600),
|
2026-02-03 14:26:52 +08:00
|
|
|
|
"working_hours": params.get("working_hours", []),
|
|
|
|
|
|
"target_class": params.get("target_class", bind_config.get("target_class", "person")),
|
2026-01-30 13:51:58 +08:00
|
|
|
|
}
|
2026-02-03 14:26:52 +08:00
|
|
|
|
if key in self.algorithms.get(roi_id, {}) and "leave_post" in self.algorithms[roi_id].get(key, {}):
|
|
|
|
|
|
algo = self.algorithms[roi_id][key]["leave_post"]
|
|
|
|
|
|
algo.confirm_on_duty_sec = algo_params["confirm_on_duty_sec"]
|
|
|
|
|
|
algo.confirm_leave_sec = algo_params["confirm_leave_sec"]
|
|
|
|
|
|
algo.cooldown_sec = algo_params["cooldown_sec"]
|
|
|
|
|
|
algo.target_class = algo_params["target_class"]
|
|
|
|
|
|
if algo_params["working_hours"]:
|
|
|
|
|
|
algo.working_hours = algo_params["working_hours"]
|
|
|
|
|
|
logger.info(f"已热更新算法参数: {key}")
|
2026-01-30 13:51:58 +08:00
|
|
|
|
else:
|
2026-02-03 14:26:52 +08:00
|
|
|
|
self.algorithms[roi_id][key] = {}
|
|
|
|
|
|
self.algorithms[roi_id][key]["leave_post"] = LeavePostAlgorithm(
|
|
|
|
|
|
confirm_on_duty_sec=algo_params["confirm_on_duty_sec"],
|
|
|
|
|
|
confirm_leave_sec=algo_params["confirm_leave_sec"],
|
2026-02-12 15:06:46 +08:00
|
|
|
|
leave_countdown_sec=algo_params.get("leave_countdown_sec", 300), # 离岗倒计时,默认5分钟
|
2026-02-03 14:26:52 +08:00
|
|
|
|
cooldown_sec=algo_params["cooldown_sec"],
|
|
|
|
|
|
working_hours=algo_params["working_hours"],
|
|
|
|
|
|
target_class=algo_params["target_class"],
|
|
|
|
|
|
)
|
|
|
|
|
|
logger.info(f"已从Redis加载算法: {key}")
|
|
|
|
|
|
elif algo_code == "intrusion":
|
|
|
|
|
|
algo_params = {
|
2026-02-11 09:57:02 +08:00
|
|
|
|
"cooldown_seconds": params.get("cooldown_seconds", 300),
|
2026-02-03 14:26:52 +08:00
|
|
|
|
"confirm_seconds": params.get("confirm_seconds", 5),
|
|
|
|
|
|
"target_class": params.get("target_class", bind_config.get("target_class")),
|
|
|
|
|
|
}
|
|
|
|
|
|
self.algorithms[roi_id][key] = {}
|
|
|
|
|
|
self.algorithms[roi_id][key]["intrusion"] = IntrusionAlgorithm(
|
|
|
|
|
|
cooldown_seconds=algo_params["cooldown_seconds"],
|
|
|
|
|
|
confirm_seconds=algo_params["confirm_seconds"],
|
|
|
|
|
|
target_class=algo_params["target_class"],
|
|
|
|
|
|
)
|
|
|
|
|
|
logger.info(f"已从Redis加载算法: {key}")
|
2026-01-30 13:51:58 +08:00
|
|
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"从Redis加载算法配置失败: {e}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
2026-02-03 14:26:52 +08:00
|
|
|
|
def reload_bind_algorithm(self, bind_id: str) -> bool:
|
|
|
|
|
|
"""重新加载单个绑定的算法配置"""
|
|
|
|
|
|
return self.load_bind_from_redis(bind_id)
|
|
|
|
|
|
|
2026-01-30 13:51:58 +08:00
|
|
|
|
def reload_algorithm(self, roi_id: str) -> bool:
|
2026-02-03 14:26:52 +08:00
|
|
|
|
"""重新加载单个ROI的所有算法绑定配置"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
from core.config_sync import get_config_sync_manager
|
|
|
|
|
|
config_manager = get_config_sync_manager()
|
|
|
|
|
|
bindings = config_manager.get_bindings_from_redis(roi_id)
|
|
|
|
|
|
|
|
|
|
|
|
if not bindings:
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
for bind in bindings:
|
|
|
|
|
|
bind_id = bind.get("bind_id")
|
|
|
|
|
|
self.reset_algorithm(roi_id, bind_id)
|
|
|
|
|
|
self.load_bind_from_redis(bind_id)
|
|
|
|
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"重新加载ROI算法配置失败: {e}")
|
|
|
|
|
|
return False
|
2026-01-30 13:51:58 +08:00
|
|
|
|
|
|
|
|
|
|
def reload_all_algorithms(self) -> int:
|
|
|
|
|
|
"""重新加载所有算法配置"""
|
|
|
|
|
|
count = 0
|
|
|
|
|
|
try:
|
|
|
|
|
|
from core.config_sync import get_config_sync_manager
|
|
|
|
|
|
config_manager = get_config_sync_manager()
|
2026-02-03 14:26:52 +08:00
|
|
|
|
bindings = config_manager.get_bindings_from_redis("")
|
2026-01-30 13:51:58 +08:00
|
|
|
|
|
2026-02-03 14:26:52 +08:00
|
|
|
|
for bind in bindings:
|
|
|
|
|
|
bind_id = bind.get("bind_id")
|
|
|
|
|
|
roi_id = bind.get("roi_id")
|
|
|
|
|
|
self.reset_algorithm(roi_id, bind_id)
|
|
|
|
|
|
if self.load_bind_from_redis(bind_id):
|
2026-01-30 13:51:58 +08:00
|
|
|
|
count += 1
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"已重新加载 {count} 个算法配置")
|
|
|
|
|
|
return count
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"重新加载所有算法配置失败: {e}")
|
|
|
|
|
|
return count
|
|
|
|
|
|
|
2026-01-29 18:33:12 +08:00
|
|
|
|
def register_algorithm(
|
|
|
|
|
|
self,
|
|
|
|
|
|
roi_id: str,
|
2026-02-03 14:26:52 +08:00
|
|
|
|
bind_id: str,
|
2026-01-29 18:33:12 +08:00
|
|
|
|
algorithm_type: str,
|
|
|
|
|
|
params: Optional[Dict[str, Any]] = None,
|
|
|
|
|
|
):
|
2026-02-04 16:47:26 +08:00
|
|
|
|
"""注册算法(支持绑定ID),使用缓存避免每帧重复查询"""
|
|
|
|
|
|
cache_key = (roi_id, bind_id, algorithm_type)
|
|
|
|
|
|
|
|
|
|
|
|
# 快速路径:已注册直接返回
|
|
|
|
|
|
if cache_key in self._registered_keys:
|
2026-01-30 13:23:22 +08:00
|
|
|
|
return
|
2026-02-04 16:47:26 +08:00
|
|
|
|
|
|
|
|
|
|
key = f"{roi_id}_{bind_id}"
|
2026-02-03 14:26:52 +08:00
|
|
|
|
|
2026-01-29 18:33:12 +08:00
|
|
|
|
if roi_id not in self.algorithms:
|
|
|
|
|
|
self.algorithms[roi_id] = {}
|
2026-02-03 14:26:52 +08:00
|
|
|
|
|
|
|
|
|
|
if key not in self.algorithms[roi_id]:
|
|
|
|
|
|
self.algorithms[roi_id][key] = {}
|
|
|
|
|
|
|
2026-01-30 13:23:22 +08:00
|
|
|
|
algo_params = self.default_params.get(algorithm_type, {}).copy()
|
2026-01-29 18:33:12 +08:00
|
|
|
|
if params:
|
|
|
|
|
|
algo_params.update(params)
|
|
|
|
|
|
|
|
|
|
|
|
if algorithm_type == "leave_post":
|
|
|
|
|
|
roi_working_hours = algo_params.get("working_hours") or self.working_hours
|
2026-02-03 14:26:52 +08:00
|
|
|
|
self.algorithms[roi_id][key]["leave_post"] = LeavePostAlgorithm(
|
2026-01-30 13:23:22 +08:00
|
|
|
|
confirm_on_duty_sec=algo_params.get("confirm_on_duty_sec", 10),
|
2026-02-11 10:01:20 +08:00
|
|
|
|
confirm_leave_sec=algo_params.get("confirm_leave_sec", 30),
|
2026-02-12 15:06:46 +08:00
|
|
|
|
leave_countdown_sec=algo_params.get("leave_countdown_sec", 300), # 离岗倒计时,默认5分钟
|
2026-02-11 09:57:02 +08:00
|
|
|
|
cooldown_sec=algo_params.get("cooldown_sec", 600),
|
2026-01-29 18:33:12 +08:00
|
|
|
|
working_hours=roi_working_hours,
|
2026-01-30 13:23:22 +08:00
|
|
|
|
target_class=algo_params.get("target_class", "person"),
|
2026-01-29 18:33:12 +08:00
|
|
|
|
)
|
|
|
|
|
|
elif algorithm_type == "intrusion":
|
2026-02-03 14:26:52 +08:00
|
|
|
|
self.algorithms[roi_id][key]["intrusion"] = IntrusionAlgorithm(
|
2026-02-11 09:57:02 +08:00
|
|
|
|
cooldown_seconds=algo_params.get("cooldown_seconds", 300),
|
2026-01-30 13:27:28 +08:00
|
|
|
|
confirm_seconds=algo_params.get("confirm_seconds", 5),
|
2026-01-30 13:23:22 +08:00
|
|
|
|
target_class=algo_params.get("target_class"),
|
2026-01-29 18:33:12 +08:00
|
|
|
|
)
|
2026-02-05 15:16:47 +08:00
|
|
|
|
# elif algorithm_type == "crowd_detection":
|
|
|
|
|
|
# from algorithms import CrowdDetectionAlgorithm
|
|
|
|
|
|
# self.algorithms[roi_id][key]["crowd_detection"] = CrowdDetectionAlgorithm(
|
|
|
|
|
|
# max_count=algo_params.get("max_count", 10),
|
|
|
|
|
|
# cooldown_seconds=algo_params.get("cooldown_seconds", 300),
|
|
|
|
|
|
# target_class=algo_params.get("target_class", "person"),
|
|
|
|
|
|
# )
|
2026-02-04 16:47:26 +08:00
|
|
|
|
|
|
|
|
|
|
self._registered_keys.add(cache_key)
|
2026-02-03 14:26:52 +08:00
|
|
|
|
|
2026-01-29 18:33:12 +08:00
|
|
|
|
def process(
|
|
|
|
|
|
self,
|
|
|
|
|
|
roi_id: str,
|
2026-02-03 14:26:52 +08:00
|
|
|
|
bind_id: str,
|
2026-01-29 18:33:12 +08:00
|
|
|
|
camera_id: str,
|
|
|
|
|
|
algorithm_type: str,
|
|
|
|
|
|
tracks: List[Dict],
|
|
|
|
|
|
current_time: Optional[datetime] = None,
|
|
|
|
|
|
) -> List[Dict]:
|
2026-02-03 14:26:52 +08:00
|
|
|
|
"""处理检测结果(支持绑定ID)"""
|
|
|
|
|
|
key = f"{roi_id}_{bind_id}"
|
|
|
|
|
|
algo = self.algorithms.get(roi_id, {}).get(key, {}).get(algorithm_type)
|
2026-01-29 18:33:12 +08:00
|
|
|
|
if algo is None:
|
|
|
|
|
|
return []
|
|
|
|
|
|
return algo.process(roi_id, camera_id, tracks, current_time)
|
|
|
|
|
|
|
|
|
|
|
|
def update_roi_params(
|
|
|
|
|
|
self,
|
|
|
|
|
|
roi_id: str,
|
2026-02-03 14:26:52 +08:00
|
|
|
|
bind_id: str,
|
2026-01-29 18:33:12 +08:00
|
|
|
|
algorithm_type: str,
|
|
|
|
|
|
params: Dict[str, Any],
|
|
|
|
|
|
):
|
2026-02-03 14:26:52 +08:00
|
|
|
|
"""更新算法参数(支持绑定ID)"""
|
|
|
|
|
|
key = f"{roi_id}_{bind_id}"
|
|
|
|
|
|
if roi_id in self.algorithms and key in self.algorithms[roi_id] and algorithm_type in self.algorithms[roi_id][key]:
|
|
|
|
|
|
algo = self.algorithms[roi_id][key][algorithm_type]
|
2026-02-04 16:47:26 +08:00
|
|
|
|
for param_key, value in params.items():
|
|
|
|
|
|
if hasattr(algo, param_key):
|
|
|
|
|
|
setattr(algo, param_key, value)
|
2026-02-03 14:26:52 +08:00
|
|
|
|
|
|
|
|
|
|
def reset_algorithm(self, roi_id: str, bind_id: Optional[str] = None):
|
|
|
|
|
|
"""重置算法状态(支持绑定ID)"""
|
2026-01-29 18:33:12 +08:00
|
|
|
|
if roi_id not in self.algorithms:
|
|
|
|
|
|
return
|
2026-02-04 16:47:26 +08:00
|
|
|
|
|
2026-02-03 14:26:52 +08:00
|
|
|
|
if bind_id:
|
|
|
|
|
|
key = f"{roi_id}_{bind_id}"
|
|
|
|
|
|
if key in self.algorithms[roi_id]:
|
2026-02-04 16:47:26 +08:00
|
|
|
|
for algo in self.algorithms[roi_id][key].values():
|
|
|
|
|
|
algo.reset()
|
|
|
|
|
|
# 清除注册缓存
|
|
|
|
|
|
self._registered_keys = {
|
|
|
|
|
|
k for k in self._registered_keys
|
|
|
|
|
|
if not (k[0] == roi_id and k[1] == bind_id)
|
|
|
|
|
|
}
|
2026-01-29 18:33:12 +08:00
|
|
|
|
else:
|
2026-02-03 14:26:52 +08:00
|
|
|
|
for key in self.algorithms[roi_id]:
|
|
|
|
|
|
for algo in self.algorithms[roi_id][key].values():
|
|
|
|
|
|
algo.reset()
|
2026-02-04 16:47:26 +08:00
|
|
|
|
# 清除该 roi 的所有注册缓存
|
|
|
|
|
|
self._registered_keys = {
|
|
|
|
|
|
k for k in self._registered_keys if k[0] != roi_id
|
|
|
|
|
|
}
|
2026-02-03 14:26:52 +08:00
|
|
|
|
|
2026-01-29 18:33:12 +08:00
|
|
|
|
def reset_all(self):
|
2026-02-03 14:26:52 +08:00
|
|
|
|
"""重置所有算法"""
|
2026-01-29 18:33:12 +08:00
|
|
|
|
for roi_algorithms in self.algorithms.values():
|
2026-02-03 14:26:52 +08:00
|
|
|
|
for bind_algorithms in roi_algorithms.values():
|
|
|
|
|
|
for algo in bind_algorithms.values():
|
|
|
|
|
|
algo.reset()
|
|
|
|
|
|
|
2026-01-29 18:33:12 +08:00
|
|
|
|
def remove_roi(self, roi_id: str):
|
2026-02-03 14:26:52 +08:00
|
|
|
|
"""移除ROI的所有算法"""
|
2026-01-29 18:33:12 +08:00
|
|
|
|
if roi_id in self.algorithms:
|
2026-02-03 14:26:52 +08:00
|
|
|
|
for key in list(self.algorithms[roi_id].keys()):
|
|
|
|
|
|
self.reset_algorithm(roi_id, key.split("_")[-1] if "_" in key else None)
|
2026-01-29 18:33:12 +08:00
|
|
|
|
del self.algorithms[roi_id]
|
2026-02-03 14:26:52 +08:00
|
|
|
|
|
|
|
|
|
|
def remove_bind(self, roi_id: str, bind_id: str):
|
|
|
|
|
|
"""移除绑定的算法"""
|
|
|
|
|
|
key = f"{roi_id}_{bind_id}"
|
|
|
|
|
|
if roi_id in self.algorithms and key in self.algorithms[roi_id]:
|
|
|
|
|
|
for algo in self.algorithms[roi_id][key].values():
|
|
|
|
|
|
algo.reset()
|
|
|
|
|
|
del self.algorithms[roi_id][key]
|
|
|
|
|
|
|
2026-01-29 18:33:12 +08:00
|
|
|
|
def get_status(self, roi_id: str) -> Dict[str, Any]:
|
2026-02-03 14:26:52 +08:00
|
|
|
|
"""获取算法状态"""
|
2026-01-29 18:33:12 +08:00
|
|
|
|
status = {}
|
|
|
|
|
|
if roi_id in self.algorithms:
|
2026-02-03 14:26:52 +08:00
|
|
|
|
for key, bind_algorithms in self.algorithms[roi_id].items():
|
|
|
|
|
|
bind_id = key.split("_", 1)[-1] if "_" in key else ""
|
|
|
|
|
|
for algo_type, algo in bind_algorithms.items():
|
|
|
|
|
|
if algo_type == "leave_post":
|
|
|
|
|
|
status[f"{algo_type}_{bind_id}"] = {
|
|
|
|
|
|
"state": getattr(algo, "state", "WAITING"),
|
|
|
|
|
|
"alarm_sent": getattr(algo, "alarm_sent", False),
|
|
|
|
|
|
}
|
|
|
|
|
|
else:
|
|
|
|
|
|
status[f"{algo_type}_{bind_id}"] = {
|
|
|
|
|
|
"detection_count": len(getattr(algo, "detection_start", {})),
|
|
|
|
|
|
}
|
2026-01-29 18:33:12 +08:00
|
|
|
|
return status
|