safety_coordinator.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  1. """
  2. 安全联动控制器
  3. 整合安全检测、事件推送和语音播报功能
  4. """
  5. import time
  6. import threading
  7. import queue
  8. from typing import Optional, List, Dict, Tuple, Callable
  9. from dataclasses import dataclass
  10. from enum import Enum
  11. import numpy as np
  12. import cv2
  13. from config import (
  14. COORDINATOR_CONFIG,
  15. SAFETY_DETECTION_CONFIG,
  16. EVENT_PUSHER_CONFIG,
  17. EVENT_LISTENER_CONFIG,
  18. VOICE_ANNOUNCER_CONFIG,
  19. SYSTEM_CONFIG
  20. )
  21. from safety_detector import (
  22. SafetyDetector, SafetyDetection, PersonSafetyStatus,
  23. SafetyViolationType, draw_safety_result
  24. )
  25. from event_pusher import EventPusher, EventListener, SafetyEvent, EventType
  26. from voice_announcer import VoiceAnnouncer, VoicePriority
  27. class CoordinatorState(Enum):
  28. """控制器状态"""
  29. IDLE = 0 # 空闲
  30. DETECTING = 1 # 检测中
  31. TRACKING = 2 # 跟踪中
  32. ALERTING = 3 # 告警中
  33. @dataclass
  34. class AlertRecord:
  35. """告警记录"""
  36. track_id: int # 跟踪ID
  37. violation_type: str # 违规类型
  38. description: str # 描述
  39. frame: Optional[np.ndarray] # 图像
  40. timestamp: float # 时间戳
  41. pushed: bool = False # 是否已推送
  42. announced: bool = False # 是否已播报
  43. class SafetyCoordinator:
  44. """安全联动控制器:协调摄像头、安全检测、事件推送、语音播报、PTZ跟踪"""
  45. def __init__(self, camera, config: Dict = None, ptz_camera=None, calibrator=None):
  46. self.camera = camera
  47. self.config = config or {}
  48. self.ptz = ptz_camera # PTZ球机(可选)
  49. self.calibrator = calibrator # 校准器(可选)
  50. self.detector = None
  51. self.event_pusher = None
  52. self.voice_announcer = None
  53. self.event_listener = None
  54. self.state = CoordinatorState.IDLE
  55. self.state_lock = threading.Lock()
  56. self.running = False
  57. self.worker_thread = None
  58. self.alert_records: List[AlertRecord] = []
  59. self.alert_cooldown = {}
  60. # 告警冷却时间(按违规类型)
  61. self._violation_cooldown = {}
  62. self.stats = {
  63. 'frames_processed': 0,
  64. 'persons_detected': 0,
  65. 'violations_detected': 0,
  66. 'events_pushed': 0,
  67. 'voice_announced': 0,
  68. 'ptz_commands_sent': 0,
  69. 'start_time': None
  70. }
  71. self.stats_lock = threading.Lock()
  72. self.on_violation_detected: Optional[Callable] = None
  73. self.on_frame_processed: Optional[Callable] = None
  74. self._init_components()
  75. def _init_components(self):
  76. """初始化各组件"""
  77. # 从 SYSTEM_CONFIG 读取功能开关
  78. enable_detection = SYSTEM_CONFIG.get('enable_detection', True)
  79. enable_safety_detection = SYSTEM_CONFIG.get('enable_safety_detection', True)
  80. enable_event_push = SYSTEM_CONFIG.get('enable_event_push', True)
  81. enable_voice_announce = SYSTEM_CONFIG.get('enable_voice_announce', True)
  82. # 安全检测器
  83. if enable_detection and enable_safety_detection:
  84. try:
  85. self.detector = SafetyDetector(
  86. model_path=SAFETY_DETECTION_CONFIG.get('model_path'),
  87. use_gpu=SAFETY_DETECTION_CONFIG.get('use_gpu', True),
  88. conf_threshold=SAFETY_DETECTION_CONFIG.get('conf_threshold', 0.5),
  89. person_threshold=SAFETY_DETECTION_CONFIG.get('person_threshold', 0.8)
  90. )
  91. print("安全检测器初始化成功")
  92. except Exception as e:
  93. print(f"安全检测器初始化失败: {e}")
  94. else:
  95. print("安全检测功能已禁用")
  96. # 事件推送器
  97. if enable_event_push:
  98. try:
  99. self.event_pusher = EventPusher(EVENT_PUSHER_CONFIG)
  100. print("事件推送器初始化成功")
  101. except Exception as e:
  102. print(f"事件推送器初始化失败: {e}")
  103. else:
  104. print("事件推送功能已禁用")
  105. # 语音播报器
  106. if enable_voice_announce:
  107. try:
  108. self.voice_announcer = VoiceAnnouncer(
  109. tts_config=VOICE_ANNOUNCER_CONFIG.get('tts', {}),
  110. player_config=VOICE_ANNOUNCER_CONFIG.get('player', {})
  111. )
  112. print("语音播报器初始化成功")
  113. except Exception as e:
  114. print(f"语音播报器初始化失败: {e}")
  115. else:
  116. print("语音播报功能已禁用")
  117. # 事件监听器
  118. if EVENT_LISTENER_CONFIG.get('enabled', True):
  119. try:
  120. self.event_listener = EventListener(EVENT_LISTENER_CONFIG)
  121. # 设置语音播放回调
  122. self.event_listener.set_voice_callback(self._on_voice_command)
  123. print("事件监听器初始化成功")
  124. except Exception as e:
  125. print(f"事件监听器初始化失败: {e}")
  126. def _on_voice_command(self, cmd: Dict):
  127. """处理语音播放指令"""
  128. if not self.voice_announcer:
  129. return
  130. text = cmd.get('text', '')
  131. priority = VoicePriority(cmd.get('priority', 2))
  132. if text:
  133. self.voice_announcer.announce(text, priority=priority)
  134. def start(self) -> bool:
  135. """启动控制器"""
  136. if self.running:
  137. return True
  138. if self.event_pusher:
  139. self.event_pusher.start()
  140. if self.voice_announcer:
  141. self.voice_announcer.start()
  142. if self.event_listener:
  143. self.event_listener.start()
  144. self.running = True
  145. self.worker_thread = threading.Thread(target=self._worker, daemon=True)
  146. self.worker_thread.start()
  147. # PTZ跟踪已禁用
  148. with self.stats_lock:
  149. self.stats['start_time'] = time.time()
  150. print("安全联动控制器已启动")
  151. return True
  152. def stop(self):
  153. """停止控制器"""
  154. self.running = False
  155. if self.worker_thread:
  156. self.worker_thread.join(timeout=3)
  157. # PTZ跟踪已禁用
  158. if self.event_pusher:
  159. self.event_pusher.stop()
  160. if self.voice_announcer:
  161. self.voice_announcer.stop()
  162. if self.event_listener:
  163. self.event_listener.stop()
  164. self._print_stats()
  165. print("安全联动控制器已停止")
  166. def _worker(self):
  167. """工作线程"""
  168. # 优先使用 detection_fps,默认每秒2帧
  169. detection_fps = SAFETY_DETECTION_CONFIG.get('detection_fps', 2)
  170. detection_interval = 1.0 / detection_fps # 根据FPS计算间隔
  171. last_detection_time = 0
  172. detection_run_count = 0
  173. detection_violation_count = 0
  174. frame_count = 0
  175. last_log_time = time.time()
  176. heartbeat_interval = 30.0
  177. last_no_detect_log_time = 0
  178. import logging
  179. sc_logger = logging.getLogger(__name__)
  180. if self.detector is None:
  181. sc_logger.warning("[安全检测] ⚠️ 安全检测器未初始化! 安全检测不可用")
  182. else:
  183. sc_logger.info(f"[安全检测] ✓ 安全检测器已就绪, 检测帧率={detection_fps}fps(间隔={detection_interval:.2f}s)")
  184. while self.running:
  185. try:
  186. current_time = time.time()
  187. frame = self.camera.get_frame() if self.camera else None
  188. if frame is None:
  189. time.sleep(0.01)
  190. continue
  191. frame_count += 1
  192. self._update_stats('frames_processed')
  193. if current_time - last_log_time >= heartbeat_interval:
  194. stats = self.get_stats()
  195. state_str = self.state.name if hasattr(self.state, 'name') else str(self.state)
  196. sc_logger.info(
  197. f"[安全检测] 状态={state_str}, "
  198. f"检测轮次={detection_run_count}(有人={detection_violation_count}), "
  199. f"帧数={frame_count}"
  200. )
  201. frame_count = 0
  202. last_log_time = current_time
  203. if current_time - last_detection_time >= detection_interval:
  204. last_detection_time = current_time
  205. detection_run_count += 1
  206. result = self._process_frame_with_logging(frame, detection_run_count, detection_violation_count, last_no_detect_log_time, sc_logger)
  207. detection_violation_count = result
  208. self._cleanup_tracks()
  209. time.sleep(0.01)
  210. except Exception as e:
  211. sc_logger.error(f"[安全检测] 处理错误: {e}")
  212. time.sleep(0.1)
  213. def _process_frame_with_logging(self, frame: np.ndarray, run_count: int, violation_count: int, last_no_detect_time: float, sc_logger) -> int:
  214. """处理帧并返回更新的violation_count"""
  215. if self.detector is None:
  216. return violation_count
  217. self._set_state(CoordinatorState.DETECTING)
  218. detections = self.detector.detect(frame)
  219. status_list = self.detector.check_safety(frame, detections)
  220. self._update_stats('persons_detected', len(status_list))
  221. # 轨迹追踪已禁用
  222. has_violation = False
  223. for status in status_list:
  224. if status.is_violation:
  225. self._handle_violation(status, frame)
  226. has_violation = True
  227. if has_violation:
  228. violation_count += 1
  229. if not status_list:
  230. current_time = time.time()
  231. if current_time - last_no_detect_time >= 30.0:
  232. sc_logger.info(
  233. f"[安全检测] · YOLO检测运行正常, 本轮未检测到人员 "
  234. f"(累计检测{run_count}轮, 违规{violation_count}轮)"
  235. )
  236. if self.on_frame_processed:
  237. self.on_frame_processed(frame, detections, status_list)
  238. return violation_count
  239. def _process_frame(self, frame: np.ndarray):
  240. """处理帧"""
  241. if self.detector is None:
  242. return
  243. self._set_state(CoordinatorState.DETECTING)
  244. # 安全检测
  245. detections = self.detector.detect(frame)
  246. status_list = self.detector.check_safety(frame, detections)
  247. self._update_stats('persons_detected', len(status_list))
  248. # 检查违规(轨迹追踪已禁用)
  249. for status in status_list:
  250. if status.is_violation:
  251. self._handle_violation(status, frame)
  252. # 回调
  253. if self.on_frame_processed:
  254. self.on_frame_processed(frame, detections, status_list)
  255. # 轨迹追踪已禁用 - _update_tracks 和 _cleanup_tracks 方法已移除
  256. def _handle_violation(self, status: PersonSafetyStatus, frame: np.ndarray):
  257. """处理违规"""
  258. current_time = time.time()
  259. # 检查冷却时间(按违规类型)
  260. violation_key = status.get_violation_desc()
  261. cooldown = SAFETY_DETECTION_CONFIG.get('alert_cooldown', 3.0)
  262. if violation_key in self.alert_cooldown:
  263. if current_time - self.alert_cooldown[violation_key] < cooldown:
  264. return
  265. # 记录告警
  266. self.alert_cooldown[violation_key] = current_time
  267. description = status.get_violation_desc()
  268. violation_type = status.violation_types[0].value if status.violation_types else "未知"
  269. # 裁剪人体区域
  270. x1, y1, x2, y2 = status.person_bbox
  271. margin = 20
  272. x1 = max(0, x1 - margin)
  273. y1 = max(0, y1 - margin)
  274. x2 = min(frame.shape[1], x2 + margin)
  275. y2 = min(frame.shape[0], y2 + margin)
  276. person_image = frame[y1:y2, x1:x2].copy()
  277. record = AlertRecord(
  278. track_id=0, # 轨迹追踪已禁用
  279. violation_type=violation_type,
  280. description=description,
  281. frame=person_image,
  282. timestamp=current_time
  283. )
  284. self.alert_records.append(record)
  285. self._update_stats('violations_detected')
  286. # PTZ跟踪已禁用
  287. # 回调
  288. if self.on_violation_detected:
  289. self.on_violation_detected(status, frame)
  290. # 推送事件
  291. if self.event_pusher:
  292. self.event_pusher.push_safety_violation(
  293. description=description,
  294. image=person_image,
  295. track_id=0, # 轨迹追踪已禁用
  296. confidence=status.person_conf
  297. )
  298. self._update_stats('events_pushed')
  299. # 语音播报
  300. if self.voice_announcer:
  301. self.voice_announcer.announce_violation(description, urgent=True)
  302. self._update_stats('voice_announced')
  303. print(f"[告警] {description}")
  304. # PTZ跟踪已禁用 - _track_violator_ptz 和 _ptz_worker 方法已移除
  305. def _set_state(self, state: CoordinatorState):
  306. """设置状态"""
  307. with self.state_lock:
  308. self.state = state
  309. def get_state(self) -> CoordinatorState:
  310. """获取状态"""
  311. with self.state_lock:
  312. return self.state
  313. def _update_stats(self, key: str, value: int = 1):
  314. """更新统计"""
  315. with self.stats_lock:
  316. if key in self.stats:
  317. self.stats[key] += value
  318. def _print_stats(self):
  319. """打印统计"""
  320. with self.stats_lock:
  321. if self.stats['start_time']:
  322. elapsed = time.time() - self.stats['start_time']
  323. print("\n=== 安全检测统计 ===")
  324. print(f"运行时长: {elapsed:.1f}秒")
  325. print(f"处理帧数: {self.stats['frames_processed']}")
  326. print(f"检测人员: {self.stats['persons_detected']}次")
  327. print(f"违规检测: {self.stats['violations_detected']}次")
  328. print(f"事件推送: {self.stats['events_pushed']}次")
  329. print(f"语音播报: {self.stats['voice_announced']}次")
  330. if self.event_pusher:
  331. push_stats = self.event_pusher.get_stats()
  332. print(f"推送详情: 成功{push_stats['pushed_events']}, 失败{push_stats['failed_events']}")
  333. if self.voice_announcer:
  334. voice_stats = self.voice_announcer.get_stats()
  335. print(f"播报详情: 成功{voice_stats['played_commands']}, 失败{voice_stats['failed_commands']}")
  336. print("===================\n")
  337. def get_stats(self) -> Dict:
  338. """获取统计"""
  339. with self.stats_lock:
  340. return self.stats.copy()
  341. def get_alerts(self) -> List[AlertRecord]:
  342. """获取告警记录"""
  343. return self.alert_records.copy()
  344. def announce(self, text: str, priority: VoicePriority = VoicePriority.NORMAL):
  345. """
  346. 手动播报语音
  347. Args:
  348. text: 播报文本
  349. priority: 优先级
  350. """
  351. if self.voice_announcer:
  352. self.voice_announcer.announce(text, priority=priority)
  353. def force_detect(self, frame: np.ndarray = None) -> Tuple[List[SafetyDetection], List[PersonSafetyStatus]]:
  354. """
  355. 强制执行一次检测
  356. Args:
  357. frame: 输入帧,如果为 None 则从摄像头获取
  358. Returns:
  359. (检测结果, 安全状态列表)
  360. """
  361. if frame is None:
  362. frame = self.camera.get_frame() if self.camera else None
  363. if frame is None or self.detector is None:
  364. return [], []
  365. detections = self.detector.detect(frame)
  366. status_list = self.detector.check_safety(frame, detections)
  367. return detections, status_list
  368. class SimpleCamera:
  369. """简单摄像头封装(用于测试)"""
  370. def __init__(self, source=0):
  371. """
  372. 初始化摄像头
  373. Args:
  374. source: 视频源 (摄像头索引、RTSP地址、视频文件路径)
  375. """
  376. self.cap = None
  377. self.source = source
  378. self.connected = False
  379. def connect(self) -> bool:
  380. """连接摄像头"""
  381. try:
  382. # 使用 FFmpeg 单线程模式避免线程安全崩溃
  383. import os
  384. os.environ['OPENCV_FFMPEG_CAPTURE_OPTIONS'] = 'threads;1'
  385. self.cap = cv2.VideoCapture(self.source, cv2.CAP_FFMPEG)
  386. self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
  387. self.connected = self.cap.isOpened()
  388. return self.connected
  389. except Exception as e:
  390. print(f"连接摄像头失败: {e}")
  391. return False
  392. def disconnect(self):
  393. """断开连接"""
  394. if self.cap:
  395. self.cap.release()
  396. self.connected = False
  397. def get_frame(self) -> Optional[np.ndarray]:
  398. """获取帧"""
  399. if self.cap is None or not self.cap.isOpened():
  400. return None
  401. ret, frame = self.cap.read()
  402. return frame if ret else None
  403. def create_coordinator(camera_source=0, config: Dict = None) -> SafetyCoordinator:
  404. """
  405. 创建安全联动控制器
  406. Args:
  407. camera_source: 摄像头源
  408. config: 配置
  409. Returns:
  410. SafetyCoordinator 实例
  411. """
  412. camera = SimpleCamera(camera_source)
  413. return SafetyCoordinator(camera, config)