camera_group.py 16 KB

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