camera_group.py 15 KB

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