import cv2 import numpy as np import yaml import torch from ultralytics import YOLO import time import datetime import threading import queue import sys import argparse from collections import defaultdict class ThreadedFrameReader: def __init__(self, cam_id, rtsp_url): self.cam_id = cam_id self.rtsp_url = rtsp_url self.cap = cv2.VideoCapture(rtsp_url, cv2.CAP_FFMPEG) self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) self.q = queue.Queue(maxsize=2) self.running = True self.thread = threading.Thread(target=self._reader, 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 self.q.full(): 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() class MultiCameraMonitor: def __init__(self, config_path): with open(config_path, 'r', encoding='utf-8') as f: self.cfg = yaml.safe_load(f) # === 全局模型(只加载一次)=== model_cfg = self.cfg['model'] self.device = model_cfg.get('device', 'auto') if self.device == 'auto' or not self.device: self.device = 'cuda' if torch.cuda.is_available() else 'cpu' print(f"🚀 全局加载模型到 {self.device}...") self.model = YOLO(model_cfg['path']) self.model.to(self.device) self.use_half = (self.device == 'cuda') if self.use_half: print("✅ 启用 FP16 推理") self.imgsz = model_cfg['imgsz'] self.conf_thresh = model_cfg['conf_threshold'] # === 初始化所有摄像头 === self.common = self.cfg['common'] self.cameras = {} self.frame_readers = {} self.queues = {} # cam_id -> queue for detection results for cam_cfg in self.cfg['cameras']: cam_id = cam_cfg['id'] self.cameras[cam_id] = CameraLogic(cam_id, cam_cfg, self.common) self.frame_readers[cam_id] = ThreadedFrameReader(cam_id, cam_cfg['rtsp_url']) self.queues[cam_id] = queue.Queue(maxsize=1) # 存放检测结果 # === 控制信号 === self.running = True self.inference_thread = threading.Thread(target=self._inference_loop, daemon=True) self.inference_thread.start() def _inference_loop(self): """统一推理线程:轮询各摄像头最新帧,逐个推理""" while self.running: processed = False for cam_id, reader in self.frame_readers.items(): ret, frame = reader.read() if not ret: continue # 获取该摄像头是否需要处理此帧 cam_logic = self.cameras[cam_id] if cam_logic.should_skip_frame(): continue # 执行推理 results = self.model( frame, imgsz=self.imgsz, conf=self.conf_thresh, verbose=False, device=self.device, half=self.use_half, classes=[0] # person ) # 将结果送回对应摄像头逻辑 if not self.queues[cam_id].full(): self.queues[cam_id].put((frame.copy(), results[0])) processed = True if not processed: time.sleep(0.01) def run(self): """启动所有摄像头的显示和告警逻辑(主线程)""" try: while self.running: for cam_id, cam_logic in self.cameras.items(): if not self.queues[cam_id].empty(): frame, results = self.queues[cam_id].get() cam_logic.process(frame, results) # 让 OpenCV 刷新所有窗口 key = cv2.waitKey(1) & 0xFF if key == ord('q'): # 可选:按 q 退出 break time.sleep(0.01) # 避免 CPU 占用过高 except KeyboardInterrupt: pass finally: self.stop() def stop(self): self.running = False for reader in self.frame_readers.values(): reader.release() cv2.destroyAllWindows() class CameraLogic: def __init__(self, cam_id, cam_cfg, common_cfg): self.cam_id = cam_id self.roi_off_duty = np.array(cam_cfg['roi_points'], dtype=np.int32) self.roi_crowd = np.array(cam_cfg['crowd_roi_points'], dtype=np.int32) self.off_duty_threshold_sec = cam_cfg['off_duty_threshold_sec'] self.on_duty_confirm_sec = cam_cfg['on_duty_confirm_sec'] self.off_duty_confirm_sec = cam_cfg['off_duty_confirm_sec'] self.crowd_threshold_min = cam_cfg['crowd_threshold'] self.working_hours = common_cfg['working_hours'] self.process_every_n = cam_cfg.get('process_every_n_frames', common_cfg['process_every_n_frames']) self.alert_cooldown_sec = cam_cfg.get('alert_cooldown_sec', common_cfg['alert_cooldown_sec']) self.crowd_cooldown_sec = cam_cfg.get('crowd_cooldown_sec', common_cfg['crowd_cooldown_sec']) self.frame_count = 0 self.is_on_duty = False self.is_off_duty = True self.on_duty_start_time = None 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 = [] self.last_person_seen_time = None def should_skip_frame(self): self.frame_count += 1 return self.frame_count % self.process_every_n != 0 def is_point_in_roi(self, x, y, roi): return cv2.pointPolygonTest(roi, (int(x), int(y)), False) >= 0 def in_working_hours(self): now = datetime.datetime.now() h = now.hour start_h, end_h = self.working_hours return start_h <= h < end_h or (start_h == end_h == 0) def detect_crowd_count(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 self.is_point_in_roi(cx, cy, roi): count += 1 return count def process(self, frame, results): current_time = time.time() now = datetime.datetime.now() in_work = self.in_working_hours() boxes = results.boxes # === 离岗逻辑 === roi_has_person_raw = any( self.is_point_in_roi((b.xyxy[0][0] + b.xyxy[0][2]) / 2, (b.xyxy[0][1] + b.xyxy[0][3]) / 2, self.roi_off_duty) for b in boxes ) if in_work: if roi_has_person_raw: self.last_person_seen_time = current_time effective = ( self.last_person_seen_time is not None and (current_time - self.last_person_seen_time) < 1.0 # grace period ) if effective: 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.on_duty_confirm_sec: self.is_on_duty, self.is_off_duty = True, False self.on_duty_start_time = None print(f"[{self.cam_id}] ✅ 上岗确认成功 ({now.strftime('%H:%M:%S')})") 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.off_duty_confirm_sec: self.is_off_duty, self.is_on_duty = True, False self.last_no_person_time = None self.off_duty_timer_start = current_time print(f"[{self.cam_id}] 🚪 进入离岗计时") # === 聚集检测 === crowd_count = self.detect_crowd_count(boxes, self.roi_crowd) self.crowd_history.append((current_time, crowd_count)) self.crowd_history = [(t, c) for t, c in self.crowd_history if current_time - t <= 300] if in_work and crowd_count >= self.crowd_threshold_min: recent = [c for t, c in self.crowd_history[-10:]] if len(recent) >= 5 and sum(c >= self.crowd_threshold_min for c in recent[-5:]) >= 4: if current_time - self.last_crowd_alert_time >= self.crowd_cooldown_sec: print(f"[{self.cam_id}] 🚨 聚集告警:{crowd_count}人") self.last_crowd_alert_time = current_time # === 离岗告警 === if in_work and self.is_off_duty and self.off_duty_timer_start: if (current_time - self.off_duty_timer_start) >= self.off_duty_threshold_sec: if (current_time - self.last_alert_time) >= self.alert_cooldown_sec: print(f"[{self.cam_id}] 🚨 离岗告警!") self.last_alert_time = current_time # === 可视化 === vis = results.plot() overlay = vis.copy() cv2.fillPoly(overlay, [self.roi_off_duty], (0, 255, 0)) cv2.fillPoly(overlay, [self.roi_crowd], (0, 0, 255)) cv2.addWeighted(overlay, 0.2, vis, 0.8, 0, vis) status = "OUT OF HOURS" color = (128, 128, 128) if in_work: if self.is_on_duty: status, color = "ON DUTY", (0, 255, 0) elif self.is_off_duty: if self.off_duty_timer_start: elapsed = int(current_time - self.off_duty_timer_start) if elapsed >= self.off_duty_threshold_sec: status, color = "OFF DUTY!", (0, 0, 255) else: status = f"IDLE - {elapsed}s" color = (0, 255, 255) else: status, color = "OFF DUTY", (255, 0, 0) cv2.putText(vis, f"[{self.cam_id}] {status}", (20, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, color, 2) cv2.imshow(f"Monitor - {self.cam_id}", vis) def main(): parser = argparse.ArgumentParser() parser.add_argument("--config", default="config.yaml", help="配置文件路径") args = parser.parse_args() monitor = MultiCameraMonitor(args.config) monitor.run() if __name__ == "__main__": main()