main.py 35 KB

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