main.py 32 KB

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