main.py 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889
  1. """
  2. 双摄像头联动抓拍系统 - 主程序
  3. 系统功能:
  4. 1. 全景摄像头实时监控和物体检测
  5. 2. 检测到人体后,球机自动变焦定位
  6. 3. 对人体进行分割并OCR识别衣服上的编号
  7. """
  8. # 必须在import cv2之前设置,否则FFmpeg多线程解码会导致
  9. # "Assertion fctx->async_lock failed at pthread_frame.c:167" 崩溃
  10. import os
  11. os.environ['OPENCV_FFMPEG_CAPTURE_OPTIONS'] = 'threads;1'
  12. import sys
  13. import time
  14. import argparse
  15. import logging
  16. import threading
  17. import signal
  18. from typing import Optional
  19. import cv2
  20. import numpy as np
  21. # 添加项目路径
  22. sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
  23. from config import (
  24. PANORAMA_CAMERA, PTZ_CAMERA, SDK_PATH,
  25. DETECTION_CONFIG, PTZ_CONFIG, OCR_CONFIG, COORDINATOR_CONFIG,
  26. CALIBRATION_CONFIG, LOG_CONFIG, SYSTEM_CONFIG,
  27. CAMERA_GROUPS, get_enabled_groups
  28. )
  29. from dahua_sdk import DahuaSDK
  30. from panorama_camera import PanoramaCamera, ObjectDetector, DetectedObject
  31. from ptz_camera import PTZCamera, PTZController
  32. from ocr_recognizer import NumberDetector, PersonInfo
  33. from coordinator import Coordinator, EventDrivenCoordinator, AsyncCoordinator, SequentialCoordinator
  34. # 配置日志 - 使用LOG_CONFIG
  35. def setup_logging():
  36. """设置日志配置"""
  37. log_level = getattr(logging, LOG_CONFIG.get('level', 'INFO'), logging.INFO)
  38. log_format = LOG_CONFIG.get('format', '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
  39. log_file = LOG_CONFIG.get('file')
  40. handlers = [logging.StreamHandler()]
  41. if log_file:
  42. from logging.handlers import RotatingFileHandler
  43. file_handler = RotatingFileHandler(
  44. log_file,
  45. maxBytes=LOG_CONFIG.get('max_bytes', 10*1024*1024),
  46. backupCount=LOG_CONFIG.get('backup_count', 5)
  47. )
  48. file_handler.setFormatter(logging.Formatter(log_format))
  49. handlers.append(file_handler)
  50. logging.basicConfig(
  51. level=log_level,
  52. format=log_format,
  53. handlers=handlers
  54. )
  55. setup_logging()
  56. logger = logging.getLogger(__name__)
  57. # 全局停止标志(用于信号处理)
  58. _shutdown_requested = False
  59. def _signal_handler(signum, frame):
  60. """信号处理函数"""
  61. global _shutdown_requested
  62. sig_name = signal.Signals(signum).name
  63. logger.info(f"接收到信号 {sig_name},准备优雅退出...")
  64. print(f"\n[信号] 接收到 {sig_name},准备停止...")
  65. _shutdown_requested = True
  66. class DualCameraSystem:
  67. """
  68. 双摄像头联动抓拍系统
  69. """
  70. def __init__(self, config_override: dict = None):
  71. """
  72. 初始化系统
  73. Args:
  74. config_override: 配置覆盖
  75. """
  76. self.config = config_override or {}
  77. # SDK
  78. self.sdk = None
  79. # 摄像头
  80. self.panorama_camera = None
  81. self.ptz_camera = None
  82. # 检测器和识别器
  83. self.detector = None
  84. self.number_detector = None
  85. # 联动控制器
  86. self.coordinator = None
  87. # 校准器
  88. self.calibrator = None
  89. self.calibration_manager = None
  90. # 定时校准
  91. self.calibration_interval = CALIBRATION_CONFIG.get('interval', 24 * 60 * 60) # 默认24小时
  92. self.daily_calibration_time = CALIBRATION_CONFIG.get('daily_calibration_time', '08:00') # 每日校准时间
  93. self.force_daily_recalibration = CALIBRATION_CONFIG.get('force_daily_recalibration', True) # 强制重新校准
  94. self.calibration_thread = None
  95. self.calibration_running = False
  96. self.last_calibration_time = 0
  97. # 运行标志
  98. self.running = False
  99. def initialize(self, skip_calibration: bool = False) -> bool:
  100. """
  101. 初始化系统组件
  102. Args:
  103. skip_calibration: 是否跳过校准
  104. Returns:
  105. 是否成功
  106. """
  107. logger.info("初始化双摄像头联动系统...")
  108. # 先初始化检测器(YOLO/PyTorch),再加载大华SDK
  109. # 大华SDK与PyTorch共享进程空间时可能导致内存冲突,
  110. # 先加载PyTorch可避免SDK的内存映射覆盖PyTorch运行时
  111. # 初始化检测器 (YOLO11/RKNN/ONNX)
  112. try:
  113. from config import DETECTION_CONFIG
  114. self.detector = ObjectDetector(
  115. model_path=self.config.get('model_path', DETECTION_CONFIG.get('model_path')),
  116. use_gpu=self.config.get('use_gpu', DETECTION_CONFIG.get('use_gpu', True)),
  117. model_size=self.config.get('model_size', 'n'),
  118. model_type=self.config.get('model_type', DETECTION_CONFIG.get('model_type', 'auto'))
  119. )
  120. logger.info("检测器初始化成功")
  121. except Exception as e:
  122. logger.warning(f"检测器初始化失败: {e}")
  123. # 初始化编号检测器 (使用llama-server API)
  124. try:
  125. ocr_config = {
  126. 'api_host': self.config.get('ocr_host', OCR_CONFIG['api_host']),
  127. 'api_port': self.config.get('ocr_port', OCR_CONFIG['api_port']),
  128. 'model': self.config.get('ocr_model', OCR_CONFIG['model']),
  129. }
  130. self.number_detector = NumberDetector(use_api=True, ocr_config=ocr_config)
  131. logger.info("编号检测器初始化成功 (使用llama-server API)")
  132. except Exception as e:
  133. logger.warning(f"编号检测器初始化失败: {e}")
  134. # 初始化SDK(在检测器之后,避免SDK内存映射与PyTorch冲突)
  135. sdk_path = os.path.join(
  136. self.config.get('sdk_path', SDK_PATH['lib_path']),
  137. self.config.get('netsdk', SDK_PATH['netsdk'])
  138. )
  139. try:
  140. self.sdk = DahuaSDK(sdk_path)
  141. if not self.sdk.init():
  142. logger.error("SDK初始化失败")
  143. return False
  144. logger.info("SDK初始化成功")
  145. except Exception as e:
  146. logger.error(f"SDK加载失败: {e}")
  147. return False
  148. # 初始化摄像头
  149. panorama_config = self.config.get('panorama_camera', PANORAMA_CAMERA)
  150. self.panorama_camera = PanoramaCamera(self.sdk, panorama_config)
  151. ptz_config = self.config.get('ptz_camera', PTZ_CAMERA)
  152. self.ptz_camera = PTZCamera(self.sdk, ptz_config)
  153. # 初始化联动控制器
  154. # 根据配置选择顺序模式或异步模式
  155. sequential_mode = COORDINATOR_CONFIG.get('sequential_mode', {}).get('enabled', False)
  156. if sequential_mode:
  157. logger.info("使用顺序联动控制器 (SequentialCoordinator)")
  158. self.coordinator = SequentialCoordinator(
  159. self.panorama_camera,
  160. self.ptz_camera,
  161. self.detector,
  162. self.number_detector
  163. )
  164. # 应用顺序模式配置
  165. seq_config = COORDINATOR_CONFIG.get('sequential_mode', {})
  166. self.coordinator.set_capture_config(
  167. ptz_stabilize_time=seq_config.get('ptz_stabilize_time', 1.0),
  168. capture_wait_time=seq_config.get('capture_wait_time', 0.5),
  169. return_to_panorama=seq_config.get('return_to_panorama', True),
  170. default_pan=seq_config.get('default_pan', 0.0),
  171. default_tilt=seq_config.get('default_tilt', 0.0),
  172. default_zoom=seq_config.get('default_zoom', 1)
  173. )
  174. else:
  175. logger.info("使用异步联动控制器 (AsyncCoordinator)")
  176. self.coordinator = AsyncCoordinator(
  177. self.panorama_camera,
  178. self.ptz_camera,
  179. self.detector,
  180. self.number_detector
  181. )
  182. # 设置回调
  183. self._setup_callbacks()
  184. logger.info("系统初始化完成")
  185. # 执行自动校准(受配置开关和参数双重控制)
  186. should_calibrate = SYSTEM_CONFIG.get('enable_calibration', True)
  187. if not should_calibrate:
  188. logger.info("自动校准已禁用 (enable_calibration=False)")
  189. elif skip_calibration:
  190. logger.info("自动校准已跳过 (--skip-calibration)")
  191. else:
  192. if not self._auto_calibrate():
  193. logger.error("自动校准失败!")
  194. return False
  195. return True
  196. def _auto_calibrate(self, force: bool = False) -> bool:
  197. """
  198. 执行自动校准
  199. Args:
  200. force: 是否强制重新校准(不使用已有数据)
  201. Returns:
  202. 是否成功
  203. """
  204. from calibration import CameraCalibrator, CalibrationManager
  205. logger.info("=" * 50)
  206. logger.info("开始自动校准...")
  207. logger.info("=" * 50)
  208. # 连接摄像头
  209. if not self.panorama_camera.connect():
  210. logger.error("连接全景摄像头失败,无法进行校准")
  211. return False
  212. if not self.ptz_camera.connect():
  213. logger.error("连接球机失败,无法进行校准")
  214. self.panorama_camera.disconnect()
  215. return False
  216. # 启动视频流获取帧数据
  217. if not self.panorama_camera.start_stream_rtsp():
  218. logger.warning("RTSP视频流启动失败,尝试SDK方式...")
  219. if not self.panorama_camera.start_stream():
  220. logger.error("无法启动视频流,校准可能无法获取画面")
  221. # 启动球机视频流(校准需要球机画面做特征匹配)
  222. if not self.ptz_camera.start_stream_rtsp():
  223. logger.warning("球机RTSP视频流启动失败,校准将无法进行特征匹配")
  224. # 等待视频流稳定,确保帧数据可用
  225. logger.info("等待视频流稳定...")
  226. max_wait = 15
  227. for i in range(max_wait):
  228. pan_frame = self.panorama_camera.get_frame()
  229. ptz_frame = self.ptz_camera.get_frame() if self.ptz_camera else None
  230. if pan_frame is not None:
  231. logger.info(f"全景帧就绪 ({pan_frame.shape}), 等待中...")
  232. break
  233. time.sleep(1)
  234. if (i + 1) % 3 == 0:
  235. logger.info(f"等待全景帧... ({i + 1}/{max_wait}秒)")
  236. # 再等几秒让流完全稳定
  237. ptz_ready = False
  238. for i in range(10):
  239. ptz_frame = self.ptz_camera.get_frame() if self.ptz_camera else None
  240. if ptz_frame is not None:
  241. ptz_ready = True
  242. logger.info(f"球机帧就绪 ({ptz_frame.shape})")
  243. break
  244. time.sleep(1)
  245. if not ptz_ready:
  246. logger.warning("球机帧未就绪,校准可能仅依赖运动检测")
  247. final_frame = self.panorama_camera.get_frame()
  248. if final_frame is None:
  249. logger.error("5秒后仍无法获取全景帧,校准可能失败")
  250. # 创建校准器 - 支持视野重叠发现
  251. self.calibrator = CameraCalibrator(
  252. ptz_camera=self.ptz_camera,
  253. get_frame_func=self.panorama_camera.get_frame,
  254. detect_marker_func=None,
  255. ptz_capture_func=self._capture_ptz_frame
  256. )
  257. # 配置重叠发现参数
  258. overlap_cfg = CALIBRATION_CONFIG.get('overlap_discovery', {})
  259. self.calibrator.overlap_pan_range = overlap_cfg.get('pan_range', (0, 360))
  260. self.calibrator.overlap_tilt_range = overlap_cfg.get('tilt_range', (-30, 30))
  261. self.calibrator.overlap_pan_step = overlap_cfg.get('pan_step', 20)
  262. self.calibrator.overlap_tilt_step = overlap_cfg.get('tilt_step', 15)
  263. self.calibrator.stabilize_time = overlap_cfg.get('stabilize_time', 2.0)
  264. self.calibrator.max_overlap_ranges = overlap_cfg.get('max_overlap_ranges', 3)
  265. self.calibrator.min_positions_per_range = overlap_cfg.get('min_positions_per_range', 3)
  266. # 校准进度回调
  267. def on_progress(current: int, total: int, message: str):
  268. logger.info(f"校准进度: {current}/{total} - {message}")
  269. # 校准完成回调
  270. def on_complete(result):
  271. if result.success:
  272. logger.info(f"校准完成! RMS误差: {result.rms_error:.4f}")
  273. else:
  274. logger.error(f"校准失败: {result.error_message}")
  275. self.calibrator.on_progress = on_progress
  276. self.calibrator.on_complete = on_complete
  277. # 创建校准管理器
  278. self.calibration_manager = CalibrationManager(self.calibrator)
  279. # 执行视觉校准(根据参数决定是否强制重新校准)
  280. result = self.calibration_manager.auto_calibrate(
  281. force=force,
  282. fallback_on_failure=True # 校准失败时回退使用已有数据
  283. )
  284. if not result.success:
  285. logger.error("=" * 50)
  286. logger.error("校准失败!")
  287. logger.error(f"原因: {result.error_message}")
  288. logger.error("=" * 50)
  289. logger.error("")
  290. logger.error("请检查以下问题:")
  291. logger.error("1. 全景摄像头和球机是否正确连接")
  292. logger.error("2. 球机PTZ控制是否正常")
  293. logger.error("3. 两台摄像头的视野是否有重叠区域")
  294. logger.error("4. 场景是否有足够的纹理/特征用于匹配")
  295. logger.error("5. 球机RTSP视频流是否正常(特征匹配需要球机画面)")
  296. logger.error("")
  297. logger.error("您可以尝试:")
  298. logger.error("- 检查摄像头连接和网络")
  299. logger.error("- 手动移动球机确认PTZ控制正常")
  300. logger.error("- 确保场景有足够的纹理/特征")
  301. logger.error("- 使用 --skip-calibration 跳过校准")
  302. logger.error("=" * 50)
  303. # 释放资源
  304. self.panorama_camera.disconnect()
  305. self.ptz_camera.stop_stream()
  306. self.ptz_camera.disconnect()
  307. return False
  308. logger.info("=" * 50)
  309. logger.info("校准成功!")
  310. logger.info(f"有效校准点: {len(result.points)}")
  311. logger.info(f"RMS误差: {result.rms_error:.4f} 度")
  312. logger.info("=" * 50)
  313. return True
  314. def _capture_ptz_frame(self) -> Optional[np.ndarray]:
  315. """
  316. 从球机抓拍一帧图像
  317. 用于校准时特征匹配
  318. """
  319. if self.ptz_camera is None:
  320. return None
  321. try:
  322. return self.ptz_camera.get_frame()
  323. except Exception as e:
  324. logger.error(f"球机抓拍失败: {e}")
  325. return None
  326. def _setup_callbacks(self):
  327. """设置回调函数"""
  328. def on_person_detected(person: DetectedObject, frame: np.ndarray):
  329. """人体检测回调"""
  330. logger.info(f"检测到人体: 位置={person.center}, 置信度={person.confidence:.2f}")
  331. def on_number_recognized(person_info: PersonInfo):
  332. """编号识别回调"""
  333. logger.info(
  334. f"识别到编号: ID={person_info.person_id}, "
  335. f"编号={person_info.number_text}, "
  336. f"置信度={person_info.number_confidence:.2f}, "
  337. f"位置={person_info.number_location}"
  338. )
  339. self.coordinator.on_person_detected = on_person_detected
  340. self.coordinator.on_number_recognized = on_number_recognized
  341. def start(self) -> bool:
  342. """
  343. 启动系统
  344. Returns:
  345. 是否成功
  346. """
  347. if self.running:
  348. logger.warning("系统已在运行")
  349. return True
  350. logger.info("启动联动系统...")
  351. if not self.coordinator.start():
  352. logger.error("联动系统启动失败")
  353. return False
  354. self.running = True
  355. logger.info("联动系统启动成功")
  356. # 启动定时校准
  357. self._start_periodic_calibration()
  358. return True
  359. def stop(self):
  360. """停止系统"""
  361. if not self.running:
  362. return
  363. logger.info("停止联动系统...")
  364. # 停止定时校准
  365. self._stop_periodic_calibration()
  366. self.coordinator.stop()
  367. self.running = False
  368. logger.info("联动系统已停止")
  369. def get_results(self):
  370. """获取识别结果"""
  371. if self.coordinator:
  372. return self.coordinator.get_results()
  373. return []
  374. def _start_periodic_calibration(self):
  375. """启动定时校准"""
  376. if not SYSTEM_CONFIG.get('enable_calibration', True):
  377. logger.info("定时校准已禁用 (enable_calibration=False)")
  378. return
  379. if self.calibration_running:
  380. return
  381. self.calibration_running = True
  382. self.calibration_thread = threading.Thread(
  383. target=self._periodic_calibration_worker,
  384. daemon=True
  385. )
  386. self.calibration_thread.start()
  387. logger.info(f"定时校准已启动 (每日 {self.daily_calibration_time} 自动校准)")
  388. def _stop_periodic_calibration(self):
  389. """停止定时校准"""
  390. self.calibration_running = False
  391. if self.calibration_thread:
  392. self.calibration_thread.join(timeout=2)
  393. self.calibration_thread = None
  394. logger.info("定时校准已停止")
  395. def _get_seconds_until_target_time(self, target_time_str: str) -> int:
  396. """
  397. 计算到目标时间的秒数
  398. Args:
  399. target_time_str: 目标时间字符串 (HH:MM格式)
  400. Returns:
  401. 到目标时间的秒数,如果已过今天的目标时间则返回到明天目标时间的秒数
  402. """
  403. from datetime import datetime, timedelta
  404. now = datetime.now()
  405. target_hour, target_minute = map(int, target_time_str.split(':'))
  406. target_time = now.replace(hour=target_hour, minute=target_minute, second=0, microsecond=0)
  407. # 如果已过今天的目标时间,则计算到明天的目标时间
  408. if now >= target_time:
  409. target_time += timedelta(days=1)
  410. return int((target_time - now).total_seconds())
  411. def _periodic_calibration_worker(self):
  412. """定时校准工作线程 - 每日指定时间执行校准"""
  413. from datetime import datetime
  414. while self.calibration_running:
  415. try:
  416. # 计算到下一个校准时间的等待秒数
  417. wait_seconds = self._get_seconds_until_target_time(self.daily_calibration_time)
  418. next_time = datetime.now().replace(
  419. hour=int(self.daily_calibration_time.split(':')[0]),
  420. minute=int(self.daily_calibration_time.split(':')[1])
  421. )
  422. if datetime.now() >= next_time:
  423. from datetime import timedelta
  424. next_time += timedelta(days=1)
  425. logger.info(f"下次校准时间: {next_time.strftime('%Y-%m-%d %H:%M:%S')} (等待 {wait_seconds // 3600}小时{(wait_seconds % 3600) // 60}分钟)")
  426. # 等待到校准时间,每分钟检查一次是否需要停止
  427. for i in range(wait_seconds):
  428. if not self.calibration_running:
  429. return
  430. time.sleep(1)
  431. if not self.calibration_running:
  432. return
  433. # 执行校准
  434. logger.info("=" * 50)
  435. logger.info(f"执行每日定时校准 (时间: {self.daily_calibration_time})...")
  436. logger.info("=" * 50)
  437. # 每日校准强制重新校准(不使用已有数据),失败时可回退
  438. result = self._auto_calibrate(force=self.force_daily_recalibration)
  439. if result:
  440. logger.info("每日定时校准成功!")
  441. else:
  442. logger.warning("每日定时校准失败!")
  443. except Exception as e:
  444. logger.error(f"定时校准错误: {e}")
  445. time.sleep(60) # 出错后等待1分钟再重试
  446. def manual_calibrate(self) -> bool:
  447. """
  448. 手动触发校准
  449. Returns:
  450. 是否成功
  451. """
  452. logger.info("手动触发校准...")
  453. return self._auto_calibrate()
  454. def cleanup(self):
  455. """清理资源"""
  456. self.stop()
  457. # 确保定时校准停止
  458. self._stop_periodic_calibration()
  459. if self.sdk:
  460. self.sdk.cleanup()
  461. logger.info("系统资源已清理")
  462. def run_interactive(system: DualCameraSystem):
  463. """
  464. 交互模式运行
  465. Args:
  466. system: 系统实例
  467. """
  468. print("\n双摄像头联动系统 - 交互模式")
  469. print("=" * 50)
  470. print("命令:")
  471. print(" s - 开始/停止联动")
  472. print(" r - 获取识别结果")
  473. print(" t - 手动跟踪 (输入 x y)")
  474. print(" c - 抓拍快照")
  475. print(" b - 手动校准")
  476. print(" q - 退出")
  477. print("=" * 50)
  478. running = False
  479. while True:
  480. try:
  481. cmd = input("\n> ").strip().lower()
  482. if cmd == 'q':
  483. break
  484. elif cmd == 's':
  485. if running:
  486. system.stop()
  487. running = False
  488. print("联动已停止")
  489. else:
  490. if system.start():
  491. running = True
  492. print("联动已启动")
  493. elif cmd == 'r':
  494. results = system.get_results()
  495. if results:
  496. print(f"获取到 {len(results)} 个识别结果:")
  497. for r in results:
  498. print(f" ID={r.person_id}, 编号={r.number_text}, 置信度={r.number_confidence:.2f}")
  499. else:
  500. print("暂无识别结果")
  501. elif cmd == 't':
  502. try:
  503. coords = input("输入坐标 (x y, 范围0-1): ").strip().split()
  504. x, y = float(coords[0]), float(coords[1])
  505. system.coordinator.force_track_position(x, y)
  506. print(f"已移动到位置 ({x:.2f}, {y:.2f})")
  507. except Exception as e:
  508. print(f"输入错误: {e}")
  509. elif cmd == 'c':
  510. frame = system.coordinator.capture_snapshot()
  511. if frame is not None:
  512. filename = f"snapshot_{int(time.time())}.jpg"
  513. cv2.imwrite(filename, frame)
  514. print(f"快照已保存: {filename}")
  515. else:
  516. print("抓拍失败")
  517. elif cmd == 'b':
  518. print("开始手动校准...")
  519. if system.manual_calibrate():
  520. print("校准成功!")
  521. else:
  522. print("校准失败!")
  523. else:
  524. print("未知命令")
  525. except KeyboardInterrupt:
  526. break
  527. except Exception as e:
  528. print(f"错误: {e}")
  529. print("退出交互模式")
  530. def main():
  531. """主函数"""
  532. parser = argparse.ArgumentParser(description='双摄像头联动抓拍系统')
  533. # 多组模式参数
  534. parser.add_argument('--multi-group', action='store_true', help='启用多组摄像头模式')
  535. # 单组模式参数(兼容旧参数)
  536. parser.add_argument('--panorama-ip', type=str, help='全景摄像头IP')
  537. parser.add_argument('--ptz-ip', type=str, help='球机IP')
  538. parser.add_argument('--username', type=str, default='admin', help='用户名')
  539. parser.add_argument('--password', type=str, default='admin123', help='密码')
  540. parser.add_argument('--model', type=str, help='检测模型路径 (默认使用YOLO11n)')
  541. parser.add_argument('--model-size', type=str, default='n',
  542. choices=['n', 's', 'm', 'l', 'x'],
  543. help='YOLO11模型尺寸 (n/s/m/l/x)')
  544. parser.add_argument('--no-gpu', action='store_true', help='不使用GPU')
  545. parser.add_argument('--ocr-host', type=str, default='localhost', help='OCR API服务器地址')
  546. parser.add_argument('--ocr-port', type=int, default=8111, help='OCR API端口')
  547. parser.add_argument('--ocr-model', type=str, default='PaddleOCR-VL-1.5-GGUF.gguf', help='OCR模型名称')
  548. parser.add_argument('--interactive', action='store_true', help='交互模式')
  549. parser.add_argument('--demo', action='store_true', help='演示模式(不连接实际摄像头)')
  550. parser.add_argument('--skip-calibration', action='store_true', help='跳过自动校准')
  551. parser.add_argument('--force-calibration', action='store_true', help='强制重新校准')
  552. args = parser.parse_args()
  553. # 演示模式
  554. if args.demo:
  555. print("演示模式: 使用模拟数据")
  556. run_demo()
  557. return
  558. # 检查是否启用多组模式
  559. enabled_groups = get_enabled_groups()
  560. use_multi_group = args.multi_group or len(enabled_groups) > 1
  561. if use_multi_group:
  562. # 多组模式
  563. return run_multi_group_mode(args)
  564. else:
  565. # 单组模式(保持向后兼容)
  566. return run_single_group_mode(args)
  567. def run_multi_group_mode(args):
  568. """多组摄像头模式"""
  569. global _shutdown_requested
  570. from multi_group_system import MultiGroupSystem
  571. # 注册信号处理
  572. signal.signal(signal.SIGINT, _signal_handler)
  573. signal.signal(signal.SIGTERM, _signal_handler)
  574. _shutdown_requested = False
  575. print("\n" + "=" * 60)
  576. print("多组摄像头联动抓拍系统")
  577. print("=" * 60)
  578. enabled_groups = get_enabled_groups()
  579. print(f"启用的摄像头组: {len(enabled_groups)} 个")
  580. for g in enabled_groups:
  581. print(f" - {g.get('name', g.get('group_id'))}")
  582. print()
  583. # 构建配置
  584. config = {
  585. 'model_size': args.model_size,
  586. 'use_gpu': not args.no_gpu,
  587. 'ocr_host': args.ocr_host,
  588. 'ocr_port': args.ocr_port,
  589. 'ocr_model': args.ocr_model,
  590. }
  591. if args.model:
  592. config['model_path'] = args.model
  593. # 创建多组系统
  594. system = MultiGroupSystem(config)
  595. try:
  596. # 初始化
  597. if not system.initialize(skip_calibration=args.skip_calibration):
  598. print("\n系统初始化失败!")
  599. return 1
  600. # 启动
  601. print("\n启动多组联动系统...")
  602. if not system.start():
  603. print("启动失败")
  604. return 1
  605. print(f"\n多组摄像头系统运行中 ({len(system.groups)} 个组)")
  606. print("按 Ctrl+C 停止\n")
  607. # 等待(检查停止标志)
  608. while system.running and not _shutdown_requested:
  609. time.sleep(1)
  610. except KeyboardInterrupt:
  611. print("\n接收到停止信号")
  612. finally:
  613. print("正在停止系统...")
  614. system.stop()
  615. return 0
  616. def run_single_group_mode(args):
  617. """单组摄像头模式(保持向后兼容)"""
  618. global _shutdown_requested
  619. # 注册信号处理
  620. signal.signal(signal.SIGINT, _signal_handler)
  621. signal.signal(signal.SIGTERM, _signal_handler)
  622. _shutdown_requested = False
  623. # 构建配置
  624. config = {}
  625. if args.panorama_ip:
  626. config['panorama_camera'] = {
  627. **PANORAMA_CAMERA,
  628. 'ip': args.panorama_ip,
  629. 'username': args.username,
  630. 'password': args.password,
  631. }
  632. if args.ptz_ip:
  633. config['ptz_camera'] = {
  634. **PTZ_CAMERA,
  635. 'ip': args.ptz_ip,
  636. 'username': args.username,
  637. 'password': args.password,
  638. }
  639. if args.model:
  640. config['model_path'] = args.model
  641. config['model_size'] = args.model_size
  642. config['use_gpu'] = not args.no_gpu
  643. config['ocr_host'] = args.ocr_host
  644. config['ocr_port'] = args.ocr_port
  645. config['ocr_model'] = args.ocr_model
  646. # 创建系统实例
  647. system = DualCameraSystem(config)
  648. try:
  649. # 初始化 (包含自动校准)
  650. if not system.initialize(skip_calibration=args.skip_calibration):
  651. print("\n系统初始化失败!")
  652. if not args.skip_calibration:
  653. print("提示: 可使用 --skip-calibration 跳过校准")
  654. return 1
  655. # 运行
  656. if args.interactive:
  657. run_interactive(system)
  658. else:
  659. # 自动模式
  660. print("启动联动系统...")
  661. if not system.start():
  662. print("启动失败")
  663. return 1
  664. print("系统运行中,按Ctrl+C停止")
  665. while not _shutdown_requested:
  666. time.sleep(1)
  667. results = system.get_results()
  668. if results:
  669. for r in results:
  670. if r.number_text:
  671. print(f"[识别] ID={r.person_id}, 编号={r.number_text}")
  672. except KeyboardInterrupt:
  673. print("\n接收到停止信号")
  674. finally:
  675. print("正在停止系统...")
  676. system.cleanup()
  677. return 0
  678. def run_demo():
  679. """演示模式"""
  680. print("\n演示模式 - 双摄像头联动系统")
  681. print("=" * 60)
  682. print("""
  683. 系统架构:
  684. ┌─────────────────────────────────────────────────────────────┐
  685. │ 全景摄像头 (Panorama) │
  686. │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
  687. │ │ 视频流 │ -> │ 人体检测 │ -> │ 位置计算 │ │
  688. │ └─────────┘ └─────────┘ └─────────┘ │
  689. └─────────────────────────────────────────────────────────────┘
  690. ▼ 检测到人体位置 (x_ratio, y_ratio)
  691. ┌─────────────────────────────────────────────────────────────┐
  692. │ 球机 (PTZ Camera) │
  693. │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
  694. │ │ PTZ控制 │ -> │ 精确定位 │ -> │ 变焦放大 │ │
  695. │ └─────────┘ └─────────┘ └─────────┘ │
  696. └─────────────────────────────────────────────────────────────┘
  697. ▼ 变焦后的人体图像
  698. ┌─────────────────────────────────────────────────────────────┐
  699. │ 识别模块 (OCR) │
  700. │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
  701. │ │人体分割 │ -> │ 区域检测 │ -> │ OCR识别 │ -> 编号结果 │
  702. │ └─────────┘ └─────────┘ └─────────┘ │
  703. └─────────────────────────────────────────────────────────────┘
  704. 工作流程:
  705. 1. 全景摄像头实时获取视频流
  706. 2. 使用YOLO11检测画面中的人体
  707. 3. 计算人体在画面中的相对位置
  708. 4. 控制球机PTZ移动到对应位置
  709. 5. 球机变焦放大人体区域
  710. 6. 对人体进行分割,提取服装区域
  711. 7. 使用OCR识别服装上的编号
  712. 8. 输出识别结果
  713. 主要组件:
  714. - dahua_sdk.py: 大华SDK封装
  715. - panorama_camera.py: 全景摄像头和人体检测
  716. - ptz_camera.py: 球机PTZ控制
  717. - ocr_recognizer.py: 人体分割和OCR识别
  718. - coordinator.py: 联动控制逻辑
  719. """)
  720. print("=" * 60)
  721. print("\n使用方法:")
  722. print(" python main.py --panorama-ip 192.168.1.100 --ptz-ip 192.168.1.101")
  723. print(" python main.py --interactive # 交互模式")
  724. print(" python main.py --demo # 演示说明")
  725. if __name__ == '__main__':
  726. sys.exit(main() or 0)