| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279 |
- """
- 双摄像头联动抓拍系统 - 主程序
- 系统功能:
- 1. 多组全景摄像头实时监控和人体检测
- 2. 检测到人体后,球机自动变焦定位并抓拍
- 3. 配对图片保存与上传
- 注意:本版本仅保留多组摄像头联动模式,OCR/LLM/安全帽/反光衣识别已移除。
- """
- # 必须在import cv2之前设置,否则FFmpeg多线程解码会导致
- # "Assertion fctx->async_lock failed at pthread_frame.c:167" 崩溃
- import os
- os.environ['OPENCV_FFMPEG_CAPTURE_OPTIONS'] = 'rtsp_transport;tcp|threads;1'
- import sys
- import time
- import glob
- import argparse
- import logging
- import threading
- import signal
- # 添加项目路径
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
- from config import LOG_CONFIG, get_enabled_groups
- # 配置日志 - 使用LOG_CONFIG
- def _cleanup_old_logs(log_file: str, retention_days: int):
- """清理超过保留天数的日志文件"""
- if not log_file:
- return
- log_dir = os.path.dirname(log_file) or '.'
- log_basename = os.path.basename(log_file)
- # 匹配所有轮转的日志文件:app.log, app.log.1, app.log.2, ...
- patterns = [
- log_basename,
- f"{log_basename}.*",
- f"{os.path.splitext(log_basename)[0]}.*", # 处理没有扩展名的情况
- ]
- now = time.time()
- cutoff = now - (retention_days * 86400)
- for pattern in patterns:
- full_pattern = os.path.join(log_dir, pattern)
- for log_path in glob.glob(full_pattern):
- try:
- if os.path.isfile(log_path):
- mtime = os.path.getmtime(log_path)
- if mtime < cutoff:
- os.remove(log_path)
- print(f"[日志清理] 已删除过期日志: {log_path}")
- except Exception:
- pass # 忽略删除失败的日志文件
- def _log_cleanup_worker(retention_days: int, interval_hours: int = 6):
- """日志清理后台线程"""
- log_file = LOG_CONFIG.get('file')
- if not log_file:
- return
- while True:
- _cleanup_old_logs(log_file, retention_days)
- time.sleep(interval_hours * 3600)
- def setup_logging():
- """设置日志配置"""
- log_level = getattr(logging, LOG_CONFIG.get('level', 'INFO'), logging.INFO)
- log_format = LOG_CONFIG.get('format', '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
- log_file = LOG_CONFIG.get('file')
- retention_days = LOG_CONFIG.get('retention_days', 7)
- handlers = [logging.StreamHandler()]
- if log_file:
- # 确保日志目录存在
- log_dir = os.path.dirname(log_file)
- if log_dir:
- os.makedirs(log_dir, exist_ok=True)
- from logging.handlers import RotatingFileHandler
- file_handler = RotatingFileHandler(
- log_file,
- maxBytes=LOG_CONFIG.get('max_bytes', 10*1024*1024),
- backupCount=LOG_CONFIG.get('backup_count', 5)
- )
- file_handler.setFormatter(logging.Formatter(log_format))
- handlers.append(file_handler)
- # 启动日志清理后台线程
- cleanup_thread = threading.Thread(
- target=_log_cleanup_worker,
- args=(retention_days, 6),
- daemon=True
- )
- cleanup_thread.start()
- logging.basicConfig(
- level=log_level,
- format=log_format,
- handlers=handlers
- )
- setup_logging()
- logger = logging.getLogger(__name__)
- # 全局停止标志(用于信号处理)
- _shutdown_requested = False
- def _signal_handler(signum, frame):
- """信号处理函数"""
- global _shutdown_requested
- sig_name = signal.Signals(signum).name
- logger.info(f"接收到信号 {sig_name},准备优雅退出...")
- print(f"\n[信号] 接收到 {sig_name},准备停止...")
- _shutdown_requested = True
- def run_multi_group_mode(args):
- """多组摄像头模式(当前唯一支持的模式)"""
- global _shutdown_requested
- from multi_group_system import MultiGroupSystem
- # 注册信号处理
- signal.signal(signal.SIGINT, _signal_handler)
- signal.signal(signal.SIGTERM, _signal_handler)
- _shutdown_requested = False
- print("\n" + "=" * 60)
- print("多组摄像头联动抓拍系统")
- print("=" * 60)
- enabled_groups = get_enabled_groups()
- print(f"启用的摄像头组: {len(enabled_groups)} 个")
- for g in enabled_groups:
- print(f" - {g.get('name', g.get('group_id'))}")
- print()
- # 构建配置
- config = {
- 'model_size': args.model_size,
- 'use_gpu': not args.no_gpu,
- }
- if args.model:
- config['model_path'] = args.model
- # 命令行覆盖第一个启用组的摄像头 IP/凭证
- if args.panorama_ip or args.ptz_ip:
- if enabled_groups:
- first_group = enabled_groups[0]
- if args.panorama_ip and 'panorama' in first_group:
- first_group['panorama']['ip'] = args.panorama_ip
- first_group['panorama']['username'] = args.username
- first_group['panorama']['password'] = args.password
- if args.ptz_ip and 'ptz' in first_group:
- first_group['ptz']['ip'] = args.ptz_ip
- first_group['ptz']['username'] = args.username
- first_group['ptz']['password'] = args.password
- # 创建多组系统
- system = MultiGroupSystem(config)
- try:
- # 初始化
- if not system.initialize(skip_calibration=args.skip_calibration,
- force_calibration=args.force_calibration):
- print("\n系统初始化失败!")
- return 1
- # 启动
- print("\n启动多组联动系统...")
- if not system.start():
- print("启动失败")
- return 1
- print(f"\n多组摄像头系统运行中 ({len(system.groups)} 个组)")
- print("按 Ctrl+C 停止\n")
- # 等待(检查停止标志)
- while system.running and not _shutdown_requested:
- time.sleep(1)
- except KeyboardInterrupt:
- print("\n接收到停止信号")
- finally:
- print("正在停止系统...")
- system.stop()
- return 0
- def run_demo():
- """演示模式"""
- print("\n演示模式 - 双摄像头联动系统")
- print("=" * 60)
- print("""
- 系统架构:
- ┌─────────────────────────────────────────────────────────────┐
- │ 全景摄像头 (Panorama) │
- │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
- │ │ 视频流 │ -> │ 人体检测 │ -> │ 位置计算 │ │
- │ └─────────┘ └─────────┘ └─────────┘ │
- └─────────────────────────────────────────────────────────────┘
- │
- ▼ 检测到人体位置 (x_ratio, y_ratio)
- ┌─────────────────────────────────────────────────────────────┐
- │ 球机 (PTZ Camera) │
- │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
- │ │ PTZ控制 │ -> │ 精确定位 │ -> │ 变焦放大 │ │
- │ └─────────┘ └─────────┘ └─────────┘ │
- └─────────────────────────────────────────────────────────────┘
- 工作流程:
- 1. 全景摄像头实时获取视频流
- 2. 使用YOLO11检测画面中的人体
- 3. 计算人体在画面中的相对位置
- 4. 控制球机PTZ移动到对应位置
- 5. 球机变焦放大人体区域并保存配对图片
- 主要组件:
- - multi_group_system.py: 多组系统管理器
- - camera_group.py: 摄像头组封装
- - dahua_sdk.py: 大华SDK封装
- - panorama_camera.py: 全景摄像头和人体检测
- - ptz_camera.py: 球机PTZ控制
- - coordinator.py: 联动控制逻辑
- """)
- print("=" * 60)
- print("\n使用方法:")
- print(" python main.py")
- print(" python main.py --skip-calibration")
- print(" python main.py --demo")
- def main():
- """主函数"""
- parser = argparse.ArgumentParser(description='双摄像头联动抓拍系统(多组模式)')
- # 摄像头覆盖参数(应用于 config/camera.py 中第一个启用的组)
- parser.add_argument('--panorama-ip', type=str, help='全景摄像头IP')
- parser.add_argument('--ptz-ip', type=str, help='球机IP')
- parser.add_argument('--username', type=str, default='admin', help='用户名')
- parser.add_argument('--password', type=str, default='admin123', help='密码')
- # 模型与运行参数
- parser.add_argument('--model', type=str, help='检测模型路径')
- parser.add_argument('--model-size', type=str, default='n',
- choices=['n', 's', 'm', 'l', 'x'],
- help='YOLO11模型尺寸 (n/s/m/l/x)')
- parser.add_argument('--no-gpu', action='store_true', help='不使用GPU')
- parser.add_argument('--demo', action='store_true', help='演示模式(不连接实际摄像头)')
- parser.add_argument('--skip-calibration', action='store_true', help='跳过自动校准')
- parser.add_argument('--force-calibration', action='store_true', help='强制重新校准')
- args = parser.parse_args()
- # 演示模式
- if args.demo:
- print("演示模式: 使用模拟数据")
- run_demo()
- return
- return run_multi_group_mode(args)
- if __name__ == '__main__':
- sys.exit(main() or 0)
|