Files
Security_AI_integrated/detector.py
2026-01-12 17:38:39 +08:00

218 lines
8.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import cv2
import numpy as np
from ultralytics import YOLO
from sort import Sort
import time
import datetime
import threading
import queue
import torch
from collections import deque
class ThreadedFrameReader:
def __init__(self, src, maxsize=1):
self.cap = cv2.VideoCapture(src, cv2.CAP_FFMPEG)
self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
self.q = queue.Queue(maxsize=maxsize)
self.running = True
self.thread = threading.Thread(target=self._reader)
self.thread.daemon = True
self.thread.start()
def _reader(self):
while self.running:
ret, frame = self.cap.read()
if not ret:
time.sleep(0.1)
continue
if not self.q.empty():
try:
self.q.get_nowait()
except queue.Empty:
pass
self.q.put(frame)
def read(self):
if not self.q.empty():
return True, self.q.get()
return False, None
def release(self):
self.running = False
self.cap.release()
def is_point_in_roi(x, y, roi):
return cv2.pointPolygonTest(roi, (int(x), int(y)), False) >= 0
class OffDutyCrowdDetector:
def __init__(self, config, model, device, use_half):
self.config = config
self.model = model
self.device = device
self.use_half = use_half
# 解析 ROI
self.roi = np.array(config["roi_points"], dtype=np.int32)
self.crowd_roi = np.array(config["crowd_roi_points"], dtype=np.int32)
# 状态变量
self.tracker = Sort(
max_age=30,
min_hits=2,
iou_threshold=0.3
)
self.is_on_duty = False
self.on_duty_start_time = None
self.is_off_duty = True
self.last_no_person_time = None
self.off_duty_timer_start = None
self.last_alert_time = 0
self.last_crowd_alert_time = 0
self.crowd_history = deque(maxlen=1500) # 自动限制5分钟假设5fps
self.last_person_seen_time = None
self.frame_count = 0
# 缓存配置
self.working_start_min = config.get("working_hours", [9, 17])[0] * 60
self.working_end_min = config.get("working_hours", [9, 17])[1] * 60
self.process_every = config.get("process_every_n_frames", 3)
def in_working_hours(self):
now = datetime.datetime.now()
total_min = now.hour * 60 + now.minute
return self.working_start_min <= total_min <= self.working_end_min
def count_people_in_roi(self, boxes, roi):
count = 0
for box in boxes:
x1, y1, x2, y2 = box.xyxy[0].cpu().numpy()
cx, cy = (x1 + x2) / 2, (y1 + y2) / 2
if is_point_in_roi(cx, cy, roi):
count += 1
return count
def run(self):
"""主循环:供线程调用"""
frame_reader = ThreadedFrameReader(self.config["rtsp_url"])
try:
while True:
ret, frame = frame_reader.read()
if not ret:
time.sleep(0.01)
continue
self.frame_count += 1
if self.frame_count % self.process_every != 0:
continue
current_time = time.time()
now = datetime.datetime.now()
# YOLO 推理
results = self.model(
frame,
imgsz=self.config.get("imgsz", 480),
conf=self.config.get("conf_thresh", 0.4),
verbose=False,
device=self.device,
half=self.use_half,
classes=[0] # person class
)
boxes = results[0].boxes
# 更新 tracker可选用于ID跟踪
dets = []
for box in boxes:
x1, y1, x2, y2 = box.xyxy[0].cpu().numpy()
conf = float(box.conf)
dets.append([x1, y1, x2, y2, conf])
dets = np.array(dets) if dets else np.empty((0, 5))
self.tracker.update(dets)
# === 离岗检测 ===
if self.in_working_hours():
roi_has_person = self.count_people_in_roi(boxes, self.roi) > 0
if roi_has_person:
self.last_person_seen_time = current_time
# 入岗保护期
effective_on_duty = (
self.last_person_seen_time is not None and
(current_time - self.last_person_seen_time) < 1.0
)
if effective_on_duty:
self.last_no_person_time = None
if self.is_off_duty:
if self.on_duty_start_time is None:
self.on_duty_start_time = current_time
elif current_time - self.on_duty_start_time >= self.config.get("on_duty_confirm", 5):
self.is_on_duty = True
self.is_off_duty = False
self.on_duty_start_time = None
print(f"[{self.config['id']}] ✅ 上岗确认")
else:
self.on_duty_start_time = None
self.last_person_seen_time = None
if not self.is_off_duty:
if self.last_no_person_time is None:
self.last_no_person_time = current_time
elif current_time - self.last_no_person_time >= self.config.get("off_duty_confirm", 30):
self.is_off_duty = True
self.is_on_duty = False
self.off_duty_timer_start = current_time
print(f"[{self.config['id']}] ⏳ 开始离岗计时")
# 离岗告警
if self.is_off_duty and self.off_duty_timer_start:
elapsed = current_time - self.off_duty_timer_start
if elapsed >= self.config.get("off_duty_threshold", 300):
if current_time - self.last_alert_time >= self.config.get("alert_cooldown", 300):
print(f"[{self.config['id']}] 🚨 离岗告警!已离岗 {elapsed/60:.1f} 分钟")
self.last_alert_time = current_time
# === 聚集检测 ===
crowd_count = self.count_people_in_roi(boxes, self.crowd_roi)
self.crowd_history.append((current_time, crowd_count))
# 动态阈值
if crowd_count >= 10:
req_dur = 60
elif crowd_count >= 7:
req_dur = 120
elif crowd_count >= 5:
req_dur = 300
else:
req_dur = float('inf')
if req_dur < float('inf'):
recent = [(t, c) for t, c in self.crowd_history if current_time - t <= req_dur]
if recent:
valid = [c for t, c in recent if c >= 4]
ratio = len(valid) / len(recent)
if ratio >= 0.9 and (current_time - self.last_crowd_alert_time) >= self.config.get("crowd_cooldown", 180):
print(f"[{self.config['id']}] 🚨 聚集告警!{crowd_count}人持续{req_dur//60}分钟")
self.last_crowd_alert_time = current_time
# 可视化(可选,部署时可关闭)
if True: # 设为 True 可显示窗口
vis = results[0].plot()
overlay = vis.copy()
cv2.fillPoly(overlay, [self.roi], (0,255,0))
cv2.fillPoly(overlay, [self.crowd_roi], (0,0,255))
cv2.addWeighted(overlay, 0.2, vis, 0.8, 0, vis)
cv2.imshow(f"Monitor - {self.config['id']}", vis)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
except Exception as e:
print(f"[{self.config['id']}] Error: {e}")
finally:
frame_reader.release()
cv2.destroyAllWindows()