main.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. """
  2. 双摄像头联动抓拍系统 - 主程序
  3. 系统功能:
  4. 1. 多组全景摄像头实时监控和人体检测
  5. 2. 检测到人体后,球机自动变焦定位并抓拍
  6. 3. 配对图片保存与上传
  7. 注意:本版本仅保留多组摄像头联动模式,OCR/LLM/安全帽/反光衣识别已移除。
  8. """
  9. # 必须在import cv2之前设置,否则FFmpeg多线程解码会导致
  10. # "Assertion fctx->async_lock failed at pthread_frame.c:167" 崩溃
  11. import os
  12. os.environ['OPENCV_FFMPEG_CAPTURE_OPTIONS'] = 'rtsp_transport;tcp|threads;1'
  13. import sys
  14. import time
  15. import glob
  16. import argparse
  17. import logging
  18. import threading
  19. import signal
  20. # 添加项目路径
  21. sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
  22. from config import LOG_CONFIG, get_enabled_groups
  23. # 配置日志 - 使用LOG_CONFIG
  24. def _cleanup_old_logs(log_file: str, retention_days: int):
  25. """清理超过保留天数的日志文件"""
  26. if not log_file:
  27. return
  28. log_dir = os.path.dirname(log_file) or '.'
  29. log_basename = os.path.basename(log_file)
  30. # 匹配所有轮转的日志文件:app.log, app.log.1, app.log.2, ...
  31. patterns = [
  32. log_basename,
  33. f"{log_basename}.*",
  34. f"{os.path.splitext(log_basename)[0]}.*", # 处理没有扩展名的情况
  35. ]
  36. now = time.time()
  37. cutoff = now - (retention_days * 86400)
  38. for pattern in patterns:
  39. full_pattern = os.path.join(log_dir, pattern)
  40. for log_path in glob.glob(full_pattern):
  41. try:
  42. if os.path.isfile(log_path):
  43. mtime = os.path.getmtime(log_path)
  44. if mtime < cutoff:
  45. os.remove(log_path)
  46. print(f"[日志清理] 已删除过期日志: {log_path}")
  47. except Exception:
  48. pass # 忽略删除失败的日志文件
  49. def _log_cleanup_worker(retention_days: int, interval_hours: int = 6):
  50. """日志清理后台线程"""
  51. log_file = LOG_CONFIG.get('file')
  52. if not log_file:
  53. return
  54. while True:
  55. _cleanup_old_logs(log_file, retention_days)
  56. time.sleep(interval_hours * 3600)
  57. def setup_logging():
  58. """设置日志配置"""
  59. log_level = getattr(logging, LOG_CONFIG.get('level', 'INFO'), logging.INFO)
  60. log_format = LOG_CONFIG.get('format', '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
  61. log_file = LOG_CONFIG.get('file')
  62. retention_days = LOG_CONFIG.get('retention_days', 7)
  63. handlers = [logging.StreamHandler()]
  64. if log_file:
  65. # 确保日志目录存在
  66. log_dir = os.path.dirname(log_file)
  67. if log_dir:
  68. os.makedirs(log_dir, exist_ok=True)
  69. from logging.handlers import RotatingFileHandler
  70. file_handler = RotatingFileHandler(
  71. log_file,
  72. maxBytes=LOG_CONFIG.get('max_bytes', 10*1024*1024),
  73. backupCount=LOG_CONFIG.get('backup_count', 5)
  74. )
  75. file_handler.setFormatter(logging.Formatter(log_format))
  76. handlers.append(file_handler)
  77. # 启动日志清理后台线程
  78. cleanup_thread = threading.Thread(
  79. target=_log_cleanup_worker,
  80. args=(retention_days, 6),
  81. daemon=True
  82. )
  83. cleanup_thread.start()
  84. logging.basicConfig(
  85. level=log_level,
  86. format=log_format,
  87. handlers=handlers
  88. )
  89. setup_logging()
  90. logger = logging.getLogger(__name__)
  91. # 全局停止标志(用于信号处理)
  92. _shutdown_requested = False
  93. def _signal_handler(signum, frame):
  94. """信号处理函数"""
  95. global _shutdown_requested
  96. sig_name = signal.Signals(signum).name
  97. logger.info(f"接收到信号 {sig_name},准备优雅退出...")
  98. print(f"\n[信号] 接收到 {sig_name},准备停止...")
  99. _shutdown_requested = True
  100. def run_multi_group_mode(args):
  101. """多组摄像头模式(当前唯一支持的模式)"""
  102. global _shutdown_requested
  103. from multi_group_system import MultiGroupSystem
  104. # 注册信号处理
  105. signal.signal(signal.SIGINT, _signal_handler)
  106. signal.signal(signal.SIGTERM, _signal_handler)
  107. _shutdown_requested = False
  108. print("\n" + "=" * 60)
  109. print("多组摄像头联动抓拍系统")
  110. print("=" * 60)
  111. enabled_groups = get_enabled_groups()
  112. print(f"启用的摄像头组: {len(enabled_groups)} 个")
  113. for g in enabled_groups:
  114. print(f" - {g.get('name', g.get('group_id'))}")
  115. print()
  116. # 构建配置
  117. config = {
  118. 'model_size': args.model_size,
  119. 'use_gpu': not args.no_gpu,
  120. }
  121. if args.model:
  122. config['model_path'] = args.model
  123. # 命令行覆盖第一个启用组的摄像头 IP/凭证
  124. if args.panorama_ip or args.ptz_ip:
  125. if enabled_groups:
  126. first_group = enabled_groups[0]
  127. if args.panorama_ip and 'panorama' in first_group:
  128. first_group['panorama']['ip'] = args.panorama_ip
  129. first_group['panorama']['username'] = args.username
  130. first_group['panorama']['password'] = args.password
  131. if args.ptz_ip and 'ptz' in first_group:
  132. first_group['ptz']['ip'] = args.ptz_ip
  133. first_group['ptz']['username'] = args.username
  134. first_group['ptz']['password'] = args.password
  135. # 创建多组系统
  136. system = MultiGroupSystem(config)
  137. try:
  138. # 初始化
  139. if not system.initialize(skip_calibration=args.skip_calibration,
  140. force_calibration=args.force_calibration):
  141. print("\n系统初始化失败!")
  142. return 1
  143. # 启动
  144. print("\n启动多组联动系统...")
  145. if not system.start():
  146. print("启动失败")
  147. return 1
  148. print(f"\n多组摄像头系统运行中 ({len(system.groups)} 个组)")
  149. print("按 Ctrl+C 停止\n")
  150. # 等待(检查停止标志)
  151. while system.running and not _shutdown_requested:
  152. time.sleep(1)
  153. except KeyboardInterrupt:
  154. print("\n接收到停止信号")
  155. finally:
  156. print("正在停止系统...")
  157. system.stop()
  158. return 0
  159. def run_demo():
  160. """演示模式"""
  161. print("\n演示模式 - 双摄像头联动系统")
  162. print("=" * 60)
  163. print("""
  164. 系统架构:
  165. ┌─────────────────────────────────────────────────────────────┐
  166. │ 全景摄像头 (Panorama) │
  167. │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
  168. │ │ 视频流 │ -> │ 人体检测 │ -> │ 位置计算 │ │
  169. │ └─────────┘ └─────────┘ └─────────┘ │
  170. └─────────────────────────────────────────────────────────────┘
  171. ▼ 检测到人体位置 (x_ratio, y_ratio)
  172. ┌─────────────────────────────────────────────────────────────┐
  173. │ 球机 (PTZ Camera) │
  174. │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
  175. │ │ PTZ控制 │ -> │ 精确定位 │ -> │ 变焦放大 │ │
  176. │ └─────────┘ └─────────┘ └─────────┘ │
  177. └─────────────────────────────────────────────────────────────┘
  178. 工作流程:
  179. 1. 全景摄像头实时获取视频流
  180. 2. 使用YOLO11检测画面中的人体
  181. 3. 计算人体在画面中的相对位置
  182. 4. 控制球机PTZ移动到对应位置
  183. 5. 球机变焦放大人体区域并保存配对图片
  184. 主要组件:
  185. - multi_group_system.py: 多组系统管理器
  186. - camera_group.py: 摄像头组封装
  187. - dahua_sdk.py: 大华SDK封装
  188. - panorama_camera.py: 全景摄像头和人体检测
  189. - ptz_camera.py: 球机PTZ控制
  190. - coordinator.py: 联动控制逻辑
  191. """)
  192. print("=" * 60)
  193. print("\n使用方法:")
  194. print(" python main.py")
  195. print(" python main.py --skip-calibration")
  196. print(" python main.py --demo")
  197. def main():
  198. """主函数"""
  199. parser = argparse.ArgumentParser(description='双摄像头联动抓拍系统(多组模式)')
  200. # 摄像头覆盖参数(应用于 config/camera.py 中第一个启用的组)
  201. parser.add_argument('--panorama-ip', type=str, help='全景摄像头IP')
  202. parser.add_argument('--ptz-ip', type=str, help='球机IP')
  203. parser.add_argument('--username', type=str, default='admin', help='用户名')
  204. parser.add_argument('--password', type=str, default='admin123', help='密码')
  205. # 模型与运行参数
  206. parser.add_argument('--model', type=str, help='检测模型路径')
  207. parser.add_argument('--model-size', type=str, default='n',
  208. choices=['n', 's', 'm', 'l', 'x'],
  209. help='YOLO11模型尺寸 (n/s/m/l/x)')
  210. parser.add_argument('--no-gpu', action='store_true', help='不使用GPU')
  211. parser.add_argument('--demo', action='store_true', help='演示模式(不连接实际摄像头)')
  212. parser.add_argument('--skip-calibration', action='store_true', help='跳过自动校准')
  213. parser.add_argument('--force-calibration', action='store_true', help='强制重新校准')
  214. args = parser.parse_args()
  215. # 演示模式
  216. if args.demo:
  217. print("演示模式: 使用模拟数据")
  218. run_demo()
  219. return
  220. return run_multi_group_mode(args)
  221. if __name__ == '__main__':
  222. sys.exit(main() or 0)