camera_group.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. """
  2. 摄像头组封装类
  3. 封装一组全景+球机摄像头的所有组件
  4. """
  5. import os
  6. import time
  7. import logging
  8. import threading
  9. from typing import Optional, Dict, Any, Callable
  10. from dataclasses import dataclass
  11. import numpy as np
  12. from panorama_camera import PanoramaCamera, ObjectDetector, DetectedObject
  13. from ptz_camera import PTZCamera
  14. from ocr_recognizer import NumberDetector, PersonInfo
  15. from coordinator import SequentialCoordinator
  16. from calibration import CameraCalibrator, CalibrationManager
  17. from paired_image_saver import PairedImageSaver
  18. logger = logging.getLogger(__name__)
  19. @dataclass
  20. class GroupConfig:
  21. """摄像头组配置"""
  22. group_id: str
  23. name: str
  24. panorama_config: Dict[str, Any]
  25. ptz_config: Dict[str, Any]
  26. calibration_file: str
  27. paired_image_dir: str
  28. class CameraGroup:
  29. """
  30. 摄像头组封装类
  31. 封装一组全景+球机摄像头的所有组件,包括:
  32. - 全景摄像头实例
  33. - 球机实例
  34. - 校准器
  35. - 联动控制器
  36. - 配对图片保存器
  37. """
  38. def __init__(self,
  39. group_config: Dict[str, Any],
  40. sdk,
  41. detector: ObjectDetector,
  42. number_detector: Optional[NumberDetector] = None,
  43. shared_config: Optional[Dict[str, Any]] = None):
  44. """
  45. 初始化摄像头组
  46. Args:
  47. group_config: 组配置字典
  48. sdk: 大华SDK实例(共享)
  49. detector: 检测器实例(共享)
  50. number_detector: 编号检测器实例(可选,共享)
  51. shared_config: 共享配置(校准配置、联动配置等)
  52. """
  53. self.group_id = group_config.get('group_id', 'unknown')
  54. self.name = group_config.get('name', self.group_id)
  55. self.config = group_config
  56. self.sdk = sdk
  57. self.detector = detector
  58. self.number_detector = number_detector
  59. self.shared_config = shared_config or {}
  60. # 组件实例
  61. self.panorama_camera: Optional[PanoramaCamera] = None
  62. self.ptz_camera: Optional[PTZCamera] = None
  63. self.calibrator: Optional[CameraCalibrator] = None
  64. self.calibration_manager: Optional[CalibrationManager] = None
  65. self.coordinator: Optional[SequentialCoordinator] = None
  66. self.paired_saver: Optional[PairedImageSaver] = None
  67. # 校准数据文件路径
  68. self.calibration_file = group_config.get(
  69. 'calibration_file',
  70. f'/home/admin/dsh/calibration_{self.group_id}.json'
  71. )
  72. # 配对图片目录
  73. self.paired_image_dir = group_config.get(
  74. 'paired_image_dir',
  75. f'/home/admin/dsh/paired_images_{self.group_id}'
  76. )
  77. # 运行状态
  78. self.running = False
  79. self.initialized = False
  80. # 定时校准
  81. self.calibration_thread = None
  82. self.calibration_running = False
  83. logger.info(f"[{self.group_id}] 摄像头组实例创建: {self.name}")
  84. def initialize(self, skip_calibration: bool = False) -> bool:
  85. """
  86. 初始化组内所有组件
  87. Args:
  88. skip_calibration: 是否跳过校准
  89. Returns:
  90. 是否成功
  91. """
  92. logger.info(f"[{self.group_id}] 开始初始化摄像头组...")
  93. # 1. 初始化全景摄像头
  94. panorama_config = self.config.get('panorama', {})
  95. panorama_config['group_id'] = self.group_id
  96. self.panorama_camera = PanoramaCamera(self.sdk, panorama_config)
  97. # 2. 初始化球机
  98. ptz_config = self.config.get('ptz', {})
  99. ptz_config['group_id'] = self.group_id
  100. self.ptz_camera = PTZCamera(self.sdk, ptz_config)
  101. # 3. 初始化配对图片保存器
  102. try:
  103. self.paired_saver = PairedImageSaver(
  104. base_dir=self.paired_image_dir,
  105. time_window=5.0
  106. )
  107. logger.info(f"[{self.group_id}] 配对图片保存器初始化成功: {self.paired_image_dir}")
  108. except Exception as e:
  109. logger.warning(f"[{self.group_id}] 配对图片保存器初始化失败: {e}")
  110. # 4. 初始化联动控制器
  111. self.coordinator = SequentialCoordinator(
  112. self.panorama_camera,
  113. self.ptz_camera,
  114. self.detector,
  115. self.number_detector
  116. )
  117. # 应用顺序模式配置
  118. from config import COORDINATOR_CONFIG
  119. seq_config = COORDINATOR_CONFIG.get('sequential_mode', {})
  120. self.coordinator.set_capture_config(
  121. ptz_stabilize_time=seq_config.get('ptz_stabilize_time', 2.5),
  122. capture_wait_time=seq_config.get('capture_wait_time', 0.5),
  123. return_to_panorama=seq_config.get('return_to_panorama', True),
  124. default_pan=seq_config.get('default_pan', 0.0),
  125. default_tilt=seq_config.get('default_tilt', 0.0),
  126. default_zoom=seq_config.get('default_zoom', 1)
  127. )
  128. # 设置配对保存器
  129. if self.paired_saver:
  130. self.coordinator._paired_saver = self.paired_saver
  131. self.coordinator._enable_paired_saving = True
  132. # 5. 设置回调
  133. self._setup_callbacks()
  134. logger.info(f"[{self.group_id}] 组件初始化完成")
  135. # 6. 执行校准
  136. from config import SYSTEM_CONFIG
  137. should_calibrate = SYSTEM_CONFIG.get('enable_calibration', True)
  138. if not should_calibrate:
  139. logger.info(f"[{self.group_id}] 自动校准已禁用")
  140. elif skip_calibration:
  141. logger.info(f"[{self.group_id}] 自动校准已跳过")
  142. else:
  143. if not self._auto_calibrate():
  144. logger.error(f"[{self.group_id}] 自动校准失败!")
  145. return False
  146. self.initialized = True
  147. return True
  148. def _auto_calibrate(self, force: bool = False) -> bool:
  149. """
  150. 执行自动校准
  151. Args:
  152. force: 是否强制重新校准
  153. Returns:
  154. 是否成功
  155. """
  156. logger.info(f"[{self.group_id}] 开始自动校准...")
  157. # 连接摄像头
  158. if not self.panorama_camera.connect():
  159. logger.error(f"[{self.group_id}] 连接全景摄像头失败")
  160. return False
  161. if not self.ptz_camera.connect():
  162. logger.error(f"[{self.group_id}] 连接球机失败")
  163. self.panorama_camera.disconnect()
  164. return False
  165. # 启动视频流
  166. if not self.panorama_camera.start_stream_rtsp():
  167. logger.warning(f"[{self.group_id}] 全景RTSP启动失败,尝试SDK方式...")
  168. self.panorama_camera.start_stream()
  169. if not self.ptz_camera.start_stream_rtsp():
  170. logger.warning(f"[{self.group_id}] 球机RTSP启动失败")
  171. # 等待视频流稳定
  172. logger.info(f"[{self.group_id}] 等待视频流稳定...")
  173. time.sleep(3)
  174. # 创建校准器
  175. self.calibrator = CameraCalibrator(
  176. ptz_camera=self.ptz_camera,
  177. get_frame_func=self.panorama_camera.get_frame,
  178. detect_marker_func=None,
  179. ptz_capture_func=self._capture_ptz_frame,
  180. calibration_file=self.calibration_file
  181. )
  182. # 配置校准参数
  183. from config import CALIBRATION_CONFIG
  184. overlap_cfg = CALIBRATION_CONFIG.get('overlap_discovery', {})
  185. self.calibrator.overlap_pan_range = overlap_cfg.get('pan_range', (0, 360))
  186. self.calibrator.overlap_tilt_range = overlap_cfg.get('tilt_range', (-30, 30))
  187. self.calibrator.overlap_pan_step = overlap_cfg.get('pan_step', 20)
  188. self.calibrator.overlap_tilt_step = overlap_cfg.get('tilt_step', 15)
  189. self.calibrator.stabilize_time = overlap_cfg.get('stabilize_time', 2.0)
  190. # 创建校准管理器
  191. self.calibration_manager = CalibrationManager(self.calibrator)
  192. # 执行校准
  193. result = self.calibration_manager.auto_calibrate(
  194. force=force,
  195. fallback_on_failure=True
  196. )
  197. if not result.success:
  198. logger.error(f"[{self.group_id}] 校准失败: {result.error_message}")
  199. return False
  200. logger.info(f"[{self.group_id}] 校准成功! RMS误差: {result.rms_error:.4f}")
  201. # 设置校准器到联动控制器
  202. if self.coordinator and self.calibrator.is_calibrated():
  203. self.coordinator.set_calibrator(self.calibrator)
  204. return True
  205. def _capture_ptz_frame(self) -> Optional[np.ndarray]:
  206. """从球机抓拍一帧"""
  207. if self.ptz_camera is None:
  208. return None
  209. try:
  210. return self.ptz_camera.get_frame()
  211. except Exception as e:
  212. logger.error(f"[{self.group_id}] 球机抓拍失败: {e}")
  213. return None
  214. def _setup_callbacks(self):
  215. """设置回调函数"""
  216. def on_person_detected(person: DetectedObject, frame: np.ndarray):
  217. logger.info(f"[{self.group_id}] 检测到人体: 位置={person.center}, 置信度={person.confidence:.2f}")
  218. def on_number_recognized(person_info: PersonInfo):
  219. logger.info(f"[{self.group_id}] 识别到编号: {person_info.number_text}")
  220. if self.coordinator:
  221. self.coordinator.on_person_detected = on_person_detected
  222. self.coordinator.on_number_recognized = on_number_recognized
  223. def start(self) -> bool:
  224. """
  225. 启动组内联动
  226. Returns:
  227. 是否成功
  228. """
  229. if not self.initialized:
  230. logger.error(f"[{self.group_id}] 组件未初始化,无法启动")
  231. return False
  232. logger.info(f"[{self.group_id}] 启动摄像头组...")
  233. # 启动联动控制器
  234. if self.coordinator:
  235. if not self.coordinator.start():
  236. logger.error(f"[{self.group_id}] 联动控制器启动失败")
  237. return False
  238. self.running = True
  239. logger.info(f"[{self.group_id}] 摄像头组启动成功")
  240. return True
  241. def stop(self):
  242. """停止组内所有组件"""
  243. logger.info(f"[{self.group_id}] 停止摄像头组...")
  244. self.running = False
  245. self.calibration_running = False
  246. # 停止联动控制器
  247. if self.coordinator:
  248. self.coordinator.stop()
  249. # 停止视频流
  250. if self.panorama_camera:
  251. self.panorama_camera.stop_stream()
  252. self.panorama_camera.disconnect()
  253. if self.ptz_camera:
  254. self.ptz_camera.stop_stream()
  255. self.ptz_camera.disconnect()
  256. # 关闭配对保存器
  257. if self.paired_saver:
  258. self.paired_saver.close()
  259. logger.info(f"[{self.group_id}] 摄像头组已停止")
  260. def get_status(self) -> Dict[str, Any]:
  261. """获取组状态"""
  262. return {
  263. 'group_id': self.group_id,
  264. 'name': self.name,
  265. 'running': self.running,
  266. 'initialized': self.initialized,
  267. 'calibrated': self.calibrator.is_calibrated() if self.calibrator else False,
  268. 'panorama_connected': self.panorama_camera.is_connected() if self.panorama_camera else False,
  269. 'ptz_connected': self.ptz_camera.is_connected() if self.ptz_camera else False,
  270. }
  271. def start_scheduled_calibration(self, interval_hours: int = 24, daily_time: str = '08:00'):
  272. """
  273. 启动定时校准
  274. Args:
  275. interval_hours: 校准间隔(小时)
  276. daily_time: 每日校准时间 (HH:MM)
  277. """
  278. if self.calibration_thread and self.calibration_thread.is_alive():
  279. logger.warning(f"[{self.group_id}] 定时校准已在运行")
  280. return
  281. self.calibration_running = True
  282. self.calibration_thread = threading.Thread(
  283. target=self._calibration_scheduler,
  284. args=(interval_hours, daily_time),
  285. name=f"calibration-{self.group_id}",
  286. daemon=True
  287. )
  288. self.calibration_thread.start()
  289. logger.info(f"[{self.group_id}] 定时校准已启动,每日 {daily_time} 执行")
  290. def _calibration_scheduler(self, interval_hours: int, daily_time: str):
  291. """定时校准调度器"""
  292. from datetime import datetime, timedelta
  293. import time
  294. while self.calibration_running and self.running:
  295. try:
  296. # 计算下次校准时间
  297. now = datetime.now()
  298. target_time = datetime.strptime(daily_time, '%H:%M')
  299. next_calibration = now.replace(
  300. hour=target_time.hour,
  301. minute=target_time.minute,
  302. second=0,
  303. microsecond=0
  304. )
  305. if next_calibration <= now:
  306. next_calibration += timedelta(days=1)
  307. wait_seconds = (next_calibration - now).total_seconds()
  308. logger.info(f"[{self.group_id}] 下次校准时间: {next_calibration} (等待 {wait_seconds/3600:.1f} 小时)")
  309. # 等待
  310. time.sleep(min(wait_seconds, 60)) # 每分钟检查一次
  311. if not self.calibration_running or not self.running:
  312. break
  313. # 检查是否到达校准时间
  314. if datetime.now() >= next_calibration:
  315. logger.info(f"[{self.group_id}] 开始定时校准...")
  316. self._auto_calibrate(force=True)
  317. except Exception as e:
  318. logger.error(f"[{self.group_id}] 定时校准错误: {e}")
  319. time.sleep(60)