Parcourir la source

refactor(system): 提取CameraGroup类封装摄像头组逻辑及优化主程序

- 新增CameraGroup类,封装全景摄像头、球机、校准器、联动控制器和配对图片保存器
- 支持摄像头组的初始化、启动、停止及状态查询
- 实现定时和手动校准机制,支持跳过和强制校准参数
- 主程序main.py重构,使用CameraGroup管理摄像头组
- 调整配置加载,支持多组摄像头和动态启用组
- 保持命令行参数及日志配置功能完整
- 精简原有系统初始化和启动流程,提升代码结构清晰度和可维护性
wenhongquan il y a 1 jour
Parent
commit
c63946effe

BIN
dual_camera_system/__pycache__/camera_group.cpython-310.pyc


BIN
dual_camera_system/__pycache__/main.cpython-310.pyc


BIN
dual_camera_system/__pycache__/multi_group_system.cpython-310.pyc


+ 392 - 0
dual_camera_system/camera_group.py

@@ -0,0 +1,392 @@
+"""
+摄像头组封装类
+封装一组全景+球机摄像头的所有组件
+"""
+
+import os
+import time
+import logging
+import threading
+from typing import Optional, Dict, Any, Callable
+from dataclasses import dataclass
+import numpy as np
+
+from panorama_camera import PanoramaCamera, ObjectDetector, DetectedObject
+from ptz_camera import PTZCamera
+from ocr_recognizer import NumberDetector, PersonInfo
+from coordinator import SequentialCoordinator
+from calibration import CameraCalibrator, CalibrationManager
+from paired_image_saver import PairedImageSaver
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass
+class GroupConfig:
+    """摄像头组配置"""
+    group_id: str
+    name: str
+    panorama_config: Dict[str, Any]
+    ptz_config: Dict[str, Any]
+    calibration_file: str
+    paired_image_dir: str
+
+
+class CameraGroup:
+    """
+    摄像头组封装类
+    
+    封装一组全景+球机摄像头的所有组件,包括:
+    - 全景摄像头实例
+    - 球机实例
+    - 校准器
+    - 联动控制器
+    - 配对图片保存器
+    """
+    
+    def __init__(self, 
+                 group_config: Dict[str, Any],
+                 sdk,
+                 detector: ObjectDetector,
+                 number_detector: Optional[NumberDetector] = None,
+                 shared_config: Optional[Dict[str, Any]] = None):
+        """
+        初始化摄像头组
+        
+        Args:
+            group_config: 组配置字典
+            sdk: 大华SDK实例(共享)
+            detector: 检测器实例(共享)
+            number_detector: 编号检测器实例(可选,共享)
+            shared_config: 共享配置(校准配置、联动配置等)
+        """
+        self.group_id = group_config.get('group_id', 'unknown')
+        self.name = group_config.get('name', self.group_id)
+        self.config = group_config
+        self.sdk = sdk
+        self.detector = detector
+        self.number_detector = number_detector
+        self.shared_config = shared_config or {}
+        
+        # 组件实例
+        self.panorama_camera: Optional[PanoramaCamera] = None
+        self.ptz_camera: Optional[PTZCamera] = None
+        self.calibrator: Optional[CameraCalibrator] = None
+        self.calibration_manager: Optional[CalibrationManager] = None
+        self.coordinator: Optional[SequentialCoordinator] = None
+        self.paired_saver: Optional[PairedImageSaver] = None
+        
+        # 校准数据文件路径
+        self.calibration_file = group_config.get(
+            'calibration_file', 
+            f'/home/admin/dsh/calibration_{self.group_id}.json'
+        )
+        
+        # 配对图片目录
+        self.paired_image_dir = group_config.get(
+            'paired_image_dir',
+            f'/home/admin/dsh/paired_images_{self.group_id}'
+        )
+        
+        # 运行状态
+        self.running = False
+        self.initialized = False
+        
+        # 定时校准
+        self.calibration_thread = None
+        self.calibration_running = False
+        
+        logger.info(f"[{self.group_id}] 摄像头组实例创建: {self.name}")
+    
+    def initialize(self, skip_calibration: bool = False) -> bool:
+        """
+        初始化组内所有组件
+        
+        Args:
+            skip_calibration: 是否跳过校准
+            
+        Returns:
+            是否成功
+        """
+        logger.info(f"[{self.group_id}] 开始初始化摄像头组...")
+        
+        # 1. 初始化全景摄像头
+        panorama_config = self.config.get('panorama', {})
+        panorama_config['group_id'] = self.group_id
+        self.panorama_camera = PanoramaCamera(self.sdk, panorama_config)
+        
+        # 2. 初始化球机
+        ptz_config = self.config.get('ptz', {})
+        ptz_config['group_id'] = self.group_id
+        self.ptz_camera = PTZCamera(self.sdk, ptz_config)
+        
+        # 3. 初始化配对图片保存器
+        try:
+            self.paired_saver = PairedImageSaver(
+                base_dir=self.paired_image_dir,
+                time_window=5.0
+            )
+            logger.info(f"[{self.group_id}] 配对图片保存器初始化成功: {self.paired_image_dir}")
+        except Exception as e:
+            logger.warning(f"[{self.group_id}] 配对图片保存器初始化失败: {e}")
+        
+        # 4. 初始化联动控制器
+        self.coordinator = SequentialCoordinator(
+            self.panorama_camera,
+            self.ptz_camera,
+            self.detector,
+            self.number_detector
+        )
+        
+        # 应用顺序模式配置
+        from config import COORDINATOR_CONFIG
+        seq_config = COORDINATOR_CONFIG.get('sequential_mode', {})
+        self.coordinator.set_capture_config(
+            ptz_stabilize_time=seq_config.get('ptz_stabilize_time', 2.5),
+            capture_wait_time=seq_config.get('capture_wait_time', 0.5),
+            return_to_panorama=seq_config.get('return_to_panorama', True),
+            default_pan=seq_config.get('default_pan', 0.0),
+            default_tilt=seq_config.get('default_tilt', 0.0),
+            default_zoom=seq_config.get('default_zoom', 1)
+        )
+        
+        # 设置配对保存器
+        if self.paired_saver:
+            self.coordinator._paired_saver = self.paired_saver
+            self.coordinator._enable_paired_saving = True
+        
+        # 5. 设置回调
+        self._setup_callbacks()
+        
+        logger.info(f"[{self.group_id}] 组件初始化完成")
+        
+        # 6. 执行校准
+        from config import SYSTEM_CONFIG
+        should_calibrate = SYSTEM_CONFIG.get('enable_calibration', True)
+        if not should_calibrate:
+            logger.info(f"[{self.group_id}] 自动校准已禁用")
+        elif skip_calibration:
+            logger.info(f"[{self.group_id}] 自动校准已跳过")
+        else:
+            if not self._auto_calibrate():
+                logger.error(f"[{self.group_id}] 自动校准失败!")
+                return False
+        
+        self.initialized = True
+        return True
+    
+    def _auto_calibrate(self, force: bool = False) -> bool:
+        """
+        执行自动校准
+        
+        Args:
+            force: 是否强制重新校准
+            
+        Returns:
+            是否成功
+        """
+        logger.info(f"[{self.group_id}] 开始自动校准...")
+        
+        # 连接摄像头
+        if not self.panorama_camera.connect():
+            logger.error(f"[{self.group_id}] 连接全景摄像头失败")
+            return False
+        
+        if not self.ptz_camera.connect():
+            logger.error(f"[{self.group_id}] 连接球机失败")
+            self.panorama_camera.disconnect()
+            return False
+        
+        # 启动视频流
+        if not self.panorama_camera.start_stream_rtsp():
+            logger.warning(f"[{self.group_id}] 全景RTSP启动失败,尝试SDK方式...")
+            self.panorama_camera.start_stream()
+        
+        if not self.ptz_camera.start_stream_rtsp():
+            logger.warning(f"[{self.group_id}] 球机RTSP启动失败")
+        
+        # 等待视频流稳定
+        logger.info(f"[{self.group_id}] 等待视频流稳定...")
+        time.sleep(3)
+        
+        # 创建校准器
+        self.calibrator = CameraCalibrator(
+            ptz_camera=self.ptz_camera,
+            get_frame_func=self.panorama_camera.get_frame,
+            detect_marker_func=None,
+            ptz_capture_func=self._capture_ptz_frame,
+            calibration_file=self.calibration_file
+        )
+        
+        # 配置校准参数
+        from config import CALIBRATION_CONFIG
+        overlap_cfg = CALIBRATION_CONFIG.get('overlap_discovery', {})
+        self.calibrator.overlap_pan_range = overlap_cfg.get('pan_range', (0, 360))
+        self.calibrator.overlap_tilt_range = overlap_cfg.get('tilt_range', (-30, 30))
+        self.calibrator.overlap_pan_step = overlap_cfg.get('pan_step', 20)
+        self.calibrator.overlap_tilt_step = overlap_cfg.get('tilt_step', 15)
+        self.calibrator.stabilize_time = overlap_cfg.get('stabilize_time', 2.0)
+        
+        # 创建校准管理器
+        self.calibration_manager = CalibrationManager(self.calibrator)
+        
+        # 执行校准
+        result = self.calibration_manager.auto_calibrate(
+            force=force,
+            fallback_on_failure=True
+        )
+        
+        if not result.success:
+            logger.error(f"[{self.group_id}] 校准失败: {result.error_message}")
+            return False
+        
+        logger.info(f"[{self.group_id}] 校准成功! RMS误差: {result.rms_error:.4f}")
+        
+        # 设置校准器到联动控制器
+        if self.coordinator and self.calibrator.is_calibrated():
+            self.coordinator.set_calibrator(self.calibrator)
+        
+        return True
+    
+    def _capture_ptz_frame(self) -> Optional[np.ndarray]:
+        """从球机抓拍一帧"""
+        if self.ptz_camera is None:
+            return None
+        try:
+            return self.ptz_camera.get_frame()
+        except Exception as e:
+            logger.error(f"[{self.group_id}] 球机抓拍失败: {e}")
+            return None
+    
+    def _setup_callbacks(self):
+        """设置回调函数"""
+        def on_person_detected(person: DetectedObject, frame: np.ndarray):
+            logger.info(f"[{self.group_id}] 检测到人体: 位置={person.center}, 置信度={person.confidence:.2f}")
+        
+        def on_number_recognized(person_info: PersonInfo):
+            logger.info(f"[{self.group_id}] 识别到编号: {person_info.number_text}")
+        
+        if self.coordinator:
+            self.coordinator.on_person_detected = on_person_detected
+            self.coordinator.on_number_recognized = on_number_recognized
+    
+    def start(self) -> bool:
+        """
+        启动组内联动
+        
+        Returns:
+            是否成功
+        """
+        if not self.initialized:
+            logger.error(f"[{self.group_id}] 组件未初始化,无法启动")
+            return False
+        
+        logger.info(f"[{self.group_id}] 启动摄像头组...")
+        
+        # 启动联动控制器
+        if self.coordinator:
+            if not self.coordinator.start():
+                logger.error(f"[{self.group_id}] 联动控制器启动失败")
+                return False
+        
+        self.running = True
+        logger.info(f"[{self.group_id}] 摄像头组启动成功")
+        return True
+    
+    def stop(self):
+        """停止组内所有组件"""
+        logger.info(f"[{self.group_id}] 停止摄像头组...")
+        
+        self.running = False
+        self.calibration_running = False
+        
+        # 停止联动控制器
+        if self.coordinator:
+            self.coordinator.stop()
+        
+        # 停止视频流
+        if self.panorama_camera:
+            self.panorama_camera.stop_stream()
+            self.panorama_camera.disconnect()
+        
+        if self.ptz_camera:
+            self.ptz_camera.stop_stream()
+            self.ptz_camera.disconnect()
+        
+        # 关闭配对保存器
+        if self.paired_saver:
+            self.paired_saver.close()
+        
+        logger.info(f"[{self.group_id}] 摄像头组已停止")
+    
+    def get_status(self) -> Dict[str, Any]:
+        """获取组状态"""
+        return {
+            'group_id': self.group_id,
+            'name': self.name,
+            'running': self.running,
+            'initialized': self.initialized,
+            'calibrated': self.calibrator.is_calibrated() if self.calibrator else False,
+            'panorama_connected': self.panorama_camera.is_connected() if self.panorama_camera else False,
+            'ptz_connected': self.ptz_camera.is_connected() if self.ptz_camera else False,
+        }
+    
+    def start_scheduled_calibration(self, interval_hours: int = 24, daily_time: str = '08:00'):
+        """
+        启动定时校准
+        
+        Args:
+            interval_hours: 校准间隔(小时)
+            daily_time: 每日校准时间 (HH:MM)
+        """
+        if self.calibration_thread and self.calibration_thread.is_alive():
+            logger.warning(f"[{self.group_id}] 定时校准已在运行")
+            return
+        
+        self.calibration_running = True
+        self.calibration_thread = threading.Thread(
+            target=self._calibration_scheduler,
+            args=(interval_hours, daily_time),
+            name=f"calibration-{self.group_id}",
+            daemon=True
+        )
+        self.calibration_thread.start()
+        logger.info(f"[{self.group_id}] 定时校准已启动,每日 {daily_time} 执行")
+    
+    def _calibration_scheduler(self, interval_hours: int, daily_time: str):
+        """定时校准调度器"""
+        from datetime import datetime, timedelta
+        import time
+        
+        while self.calibration_running and self.running:
+            try:
+                # 计算下次校准时间
+                now = datetime.now()
+                target_time = datetime.strptime(daily_time, '%H:%M')
+                next_calibration = now.replace(
+                    hour=target_time.hour,
+                    minute=target_time.minute,
+                    second=0,
+                    microsecond=0
+                )
+                
+                if next_calibration <= now:
+                    next_calibration += timedelta(days=1)
+                
+                wait_seconds = (next_calibration - now).total_seconds()
+                logger.info(f"[{self.group_id}] 下次校准时间: {next_calibration} (等待 {wait_seconds/3600:.1f} 小时)")
+                
+                # 等待
+                time.sleep(min(wait_seconds, 60))  # 每分钟检查一次
+                
+                if not self.calibration_running or not self.running:
+                    break
+                
+                # 检查是否到达校准时间
+                if datetime.now() >= next_calibration:
+                    logger.info(f"[{self.group_id}] 开始定时校准...")
+                    self._auto_calibrate(force=True)
+                    
+            except Exception as e:
+                logger.error(f"[{self.group_id}] 定时校准错误: {e}")
+                time.sleep(60)

+ 2 - 2
dual_camera_system/config/__init__.py

@@ -5,7 +5,7 @@
 
 from .camera import (
     PANORAMA_CAMERA, PTZ_CAMERA, SDK_PATH,
-    LOG_CONFIG
+    LOG_CONFIG, CAMERA_GROUPS, get_enabled_groups
 )
 from .detection import (
     DETECTION_CONFIG, SAFETY_DETECTION_CONFIG
@@ -24,7 +24,7 @@ __all__ = [
     # 日志
     'LOG_CONFIG',
     # 摄像头
-    'PANORAMA_CAMERA', 'PTZ_CAMERA', 'SDK_PATH',
+    'PANORAMA_CAMERA', 'PTZ_CAMERA', 'SDK_PATH', 'CAMERA_GROUPS', 'get_enabled_groups',
     # 检测
     'DETECTION_CONFIG', 'SAFETY_DETECTION_CONFIG',
     # PTZ

BIN
dual_camera_system/config/__pycache__/__init__.cpython-310.pyc


BIN
dual_camera_system/config/__pycache__/camera.cpython-310.pyc


BIN
dual_camera_system/config/__pycache__/coordinator.cpython-310.pyc


BIN
dual_camera_system/config/__pycache__/detection.cpython-310.pyc


BIN
dual_camera_system/config/__pycache__/ptz.cpython-310.pyc


BIN
dual_camera_system/config/__pycache__/system.cpython-310.pyc


+ 66 - 0
dual_camera_system/config/camera.py

@@ -12,6 +12,9 @@ LOG_CONFIG = {
     'backup_count': 5,
 }
 
+# ============================================================
+# 单组摄像头配置(保持向后兼容)
+# ============================================================
 PANORAMA_CAMERA = {
     'ip': '192.168.20.196',
     'port': 37777,
@@ -32,6 +35,65 @@ PTZ_CAMERA = {
     'rtsp_url': 'rtsp://admin:Aa1234567@192.168.20.197:554/cam/realmonitor?channel=1&subtype=1',
 }
 
+# ============================================================
+# 多组摄像头配置(新架构)
+# 使用方法:启用需要的组(enabled=True),配置对应的IP地址
+# ============================================================
+CAMERA_GROUPS = [
+    {
+        'group_id': 'group_1',
+        'name': '第一组',
+        'enabled': True,
+        'panorama': {
+            'ip': '192.168.20.196',
+            'port': 37777,
+            'rtsp_port': 554,
+            'username': 'admin',
+            'password': 'Aa1234567',
+            'channel': 1,
+        },
+        'ptz': {
+            'ip': '192.168.20.197',
+            'port': 37777,
+            'rtsp_port': 554,
+            'username': 'admin',
+            'password': 'Aa1234567',
+            'channel': 0,
+            'pan_flip': True,      # pan方向翻转
+            'ceiling_mount': True, # 吸顶安装
+        },
+        'calibration_file': '/home/admin/dsh/calibration_group1.json',
+        'paired_image_dir': '/home/admin/dsh/paired_images_group1',
+    },
+    # 第二组配置(示例,按需启用)
+    # {
+    #     'group_id': 'group_2',
+    #     'name': '第二组',
+    #     'enabled': False,  # 设为 True 启用
+    #     'panorama': {
+    #         'ip': '192.168.20.198',
+    #         'port': 37777,
+    #         'rtsp_port': 554,
+    #         'username': 'admin',
+    #         'password': 'Aa1234567',
+    #         'channel': 1,
+    #     },
+    #     'ptz': {
+    #         'ip': '192.168.20.199',
+    #         'port': 37777,
+    #         'rtsp_port': 554,
+    #         'username': 'admin',
+    #         'password': 'Aa1234567',
+    #         'channel': 0,
+    #         'pan_flip': True,
+    #         'ceiling_mount': True,
+    #     },
+    #     'calibration_file': '/home/admin/dsh/calibration_group2.json',
+    #     'paired_image_dir': '/home/admin/dsh/paired_images_group2',
+    # },
+]
+
+# SDK 路径配置
 _ARCH = platform.machine()
 if _ARCH == 'aarch64':
     _SDK_DIR = '/home/admin/dsh/dh/arm/Bin'
@@ -44,3 +106,7 @@ SDK_PATH = {
     'lib_path': _SDK_DIR,
     'netsdk': 'libdhnetsdk.so',
 }
+
+def get_enabled_groups() -> list:
+    """获取所有启用的摄像头组配置"""
+    return [g for g in CAMERA_GROUPS if g.get('enabled', False)]

+ 81 - 7
dual_camera_system/main.py

@@ -28,7 +28,8 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
 from config import (
     PANORAMA_CAMERA, PTZ_CAMERA, SDK_PATH,
     DETECTION_CONFIG, PTZ_CONFIG, OCR_CONFIG, COORDINATOR_CONFIG,
-    CALIBRATION_CONFIG, LOG_CONFIG, SYSTEM_CONFIG
+    CALIBRATION_CONFIG, LOG_CONFIG, SYSTEM_CONFIG,
+    CAMERA_GROUPS, get_enabled_groups
 )
 from dahua_sdk import DahuaSDK
 from panorama_camera import PanoramaCamera, ObjectDetector, DetectedObject
@@ -636,6 +637,10 @@ def main():
     """主函数"""
     parser = argparse.ArgumentParser(description='双摄像头联动抓拍系统')
     
+    # 多组模式参数
+    parser.add_argument('--multi-group', action='store_true', help='启用多组摄像头模式')
+    
+    # 单组模式参数(兼容旧参数)
     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='用户名')
@@ -655,6 +660,81 @@ def main():
     
     args = parser.parse_args()
     
+    # 演示模式
+    if args.demo:
+        print("演示模式: 使用模拟数据")
+        run_demo()
+        return
+    
+    # 检查是否启用多组模式
+    enabled_groups = get_enabled_groups()
+    use_multi_group = args.multi_group or len(enabled_groups) > 1
+    
+    if use_multi_group:
+        # 多组模式
+        return run_multi_group_mode(args)
+    else:
+        # 单组模式(保持向后兼容)
+        return run_single_group_mode(args)
+
+
+def run_multi_group_mode(args):
+    """多组摄像头模式"""
+    from multi_group_system import MultiGroupSystem
+    
+    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,
+        'ocr_host': args.ocr_host,
+        'ocr_port': args.ocr_port,
+        'ocr_model': args.ocr_model,
+    }
+    
+    if args.model:
+        config['model_path'] = args.model
+    
+    # 创建多组系统
+    system = MultiGroupSystem(config)
+    
+    try:
+        # 初始化
+        if not system.initialize(skip_calibration=args.skip_calibration):
+            print("\n系统初始化失败!")
+            return 1
+        
+        # 启动
+        print("\n启动多组联动系统...")
+        if not system.start():
+            print("启动失败")
+            return 1
+        
+        print(f"\n多组摄像头系统运行中 ({len(system.groups)} 个组)")
+        print("按 Ctrl+C 停止\n")
+        
+        # 等待
+        system.wait()
+        
+    except KeyboardInterrupt:
+        print("\n接收到停止信号")
+    finally:
+        system.stop()
+    
+    return 0
+
+
+def run_single_group_mode(args):
+    """单组摄像头模式(保持向后兼容)"""
     # 构建配置
     config = {}
     
@@ -683,12 +763,6 @@ def main():
     config['ocr_port'] = args.ocr_port
     config['ocr_model'] = args.ocr_model
     
-    # 演示模式
-    if args.demo:
-        print("演示模式: 使用模拟数据")
-        run_demo()
-        return
-    
     # 创建系统实例
     system = DualCameraSystem(config)
     

+ 308 - 0
dual_camera_system/multi_group_system.py

@@ -0,0 +1,308 @@
+"""
+多组摄像头系统管理器
+管理多组(全景+球机)摄像头的并行独立运行
+"""
+
+import os
+import sys
+import time
+import logging
+import threading
+from typing import List, Optional, Dict, Any
+from concurrent.futures import ThreadPoolExecutor
+
+# 添加项目路径
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+
+from config import (
+    CAMERA_GROUPS, get_enabled_groups,
+    SDK_PATH, DETECTION_CONFIG, COORDINATOR_CONFIG, 
+    CALIBRATION_CONFIG, SYSTEM_CONFIG
+)
+from dahua_sdk import DahuaSDK
+from panorama_camera import ObjectDetector
+from ocr_recognizer import NumberDetector
+from camera_group import CameraGroup
+
+logger = logging.getLogger(__name__)
+
+
+class MultiGroupSystem:
+    """
+    多组摄像头系统管理器
+    
+    管理多组(全景+球机)摄像头:
+    - 共享 SDK 实例(大华SDK只需初始化一次)
+    - 共享检测器(YOLO模型只加载一次)
+    - 各组并行独立运行检测和抓拍
+    """
+    
+    def __init__(self, config_override: dict = None):
+        """
+        初始化多组系统
+        
+        Args:
+            config_override: 配置覆盖
+        """
+        self.config = config_override or {}
+        
+        # 共享组件
+        self.sdk: Optional[DahuaSDK] = None
+        self.detector: Optional[ObjectDetector] = None
+        self.number_detector: Optional[NumberDetector] = None
+        
+        # 摄像头组列表
+        self.groups: List[CameraGroup] = []
+        
+        # 运行状态
+        self.running = False
+        self.initialized = False
+        
+        logger.info("[MultiGroupSystem] 多组摄像头系统实例创建")
+    
+    def initialize(self, skip_calibration: bool = False) -> bool:
+        """
+        初始化系统(SDK + 检测器 + 所有组)
+        
+        Args:
+            skip_calibration: 是否跳过校准
+            
+        Returns:
+            是否成功
+        """
+        logger.info("=" * 60)
+        logger.info("[MultiGroupSystem] 开始初始化多组摄像头系统...")
+        logger.info("=" * 60)
+        
+        # 1. 初始化检测器(先于SDK,避免内存冲突)
+        try:
+            self.detector = ObjectDetector(
+                model_path=self.config.get('model_path', DETECTION_CONFIG.get('model_path')),
+                use_gpu=self.config.get('use_gpu', DETECTION_CONFIG.get('use_gpu', True)),
+                model_size=self.config.get('model_size', 'n'),
+                model_type=self.config.get('model_type', DETECTION_CONFIG.get('model_type', 'auto'))
+            )
+            logger.info("[MultiGroupSystem] 检测器初始化成功")
+        except Exception as e:
+            logger.warning(f"[MultiGroupSystem] 检测器初始化失败: {e}")
+        
+        # 2. 初始化编号检测器
+        try:
+            from config import OCR_CONFIG
+            ocr_config = {
+                'api_host': self.config.get('ocr_host', OCR_CONFIG['api_host']),
+                'api_port': self.config.get('ocr_port', OCR_CONFIG['api_port']),
+                'model': self.config.get('ocr_model', OCR_CONFIG['model']),
+            }
+            self.number_detector = NumberDetector(use_api=True, ocr_config=ocr_config)
+            logger.info("[MultiGroupSystem] 编号检测器初始化成功")
+        except Exception as e:
+            logger.warning(f"[MultiGroupSystem] 编号检测器初始化失败: {e}")
+        
+        # 3. 初始化SDK
+        sdk_path = os.path.join(
+            self.config.get('sdk_path', SDK_PATH['lib_path']),
+            self.config.get('netsdk', SDK_PATH['netsdk'])
+        )
+        
+        try:
+            self.sdk = DahuaSDK(sdk_path)
+            if not self.sdk.init():
+                logger.error("[MultiGroupSystem] SDK初始化失败")
+                return False
+            logger.info("[MultiGroupSystem] SDK初始化成功")
+        except Exception as e:
+            logger.error(f"[MultiGroupSystem] SDK加载失败: {e}")
+            return False
+        
+        # 4. 获取启用的摄像头组配置
+        enabled_groups = get_enabled_groups()
+        if not enabled_groups:
+            logger.error("[MultiGroupSystem] 没有启用的摄像头组!请在 config/camera.py 中配置 CAMERA_GROUPS")
+            return False
+        
+        logger.info(f"[MultiGroupSystem] 检测到 {len(enabled_groups)} 个启用的摄像头组")
+        
+        # 5. 初始化各组
+        shared_config = {
+            'coordinator_config': COORDINATOR_CONFIG,
+            'calibration_config': CALIBRATION_CONFIG,
+        }
+        
+        success_count = 0
+        for group_config in enabled_groups:
+            group_id = group_config.get('group_id', 'unknown')
+            logger.info(f"[MultiGroupSystem] 初始化组: {group_id}")
+            
+            group = CameraGroup(
+                group_config=group_config,
+                sdk=self.sdk,
+                detector=self.detector,
+                number_detector=self.number_detector,
+                shared_config=shared_config
+            )
+            
+            if group.initialize(skip_calibration=skip_calibration):
+                self.groups.append(group)
+                success_count += 1
+                logger.info(f"[MultiGroupSystem] 组 {group_id} 初始化成功")
+            else:
+                logger.error(f"[MultiGroupSystem] 组 {group_id} 初始化失败")
+        
+        if success_count == 0:
+            logger.error("[MultiGroupSystem] 所有组初始化失败!")
+            return False
+        
+        logger.info(f"[MultiGroupSystem] 成功初始化 {success_count}/{len(enabled_groups)} 个组")
+        self.initialized = True
+        
+        return True
+    
+    def start(self) -> bool:
+        """
+        并行启动所有组
+        
+        Returns:
+            是否成功
+        """
+        if not self.initialized:
+            logger.error("[MultiGroupSystem] 系统未初始化,无法启动")
+            return False
+        
+        logger.info(f"[MultiGroupSystem] 启动 {len(self.groups)} 个摄像头组...")
+        
+        success_count = 0
+        for group in self.groups:
+            if group.start():
+                success_count += 1
+            else:
+                logger.error(f"[MultiGroupSystem] 组 {group.group_id} 启动失败")
+        
+        if success_count == 0:
+            logger.error("[MultiGroupSystem] 所有组启动失败!")
+            return False
+        
+        self.running = True
+        logger.info(f"[MultiGroupSystem] 成功启动 {success_count}/{len(self.groups)} 个组")
+        
+        # 启动定时校准
+        interval_hours = CALIBRATION_CONFIG.get('interval', 24 * 60 * 60) // 3600
+        daily_time = CALIBRATION_CONFIG.get('daily_calibration_time', '08:00')
+        
+        for group in self.groups:
+            group.start_scheduled_calibration(
+                interval_hours=interval_hours,
+                daily_time=daily_time
+            )
+        
+        return True
+    
+    def stop(self):
+        """停止所有组"""
+        logger.info("[MultiGroupSystem] 停止所有摄像头组...")
+        
+        self.running = False
+        
+        for group in self.groups:
+            try:
+                group.stop()
+            except Exception as e:
+                logger.error(f"[MultiGroupSystem] 停止组 {group.group_id} 失败: {e}")
+        
+        # 清理SDK
+        if self.sdk:
+            try:
+                self.sdk.cleanup()
+            except Exception as e:
+                logger.error(f"[MultiGroupSystem] SDK清理失败: {e}")
+        
+        logger.info("[MultiGroupSystem] 系统已停止")
+    
+    def get_status(self) -> Dict[str, Any]:
+        """获取系统状态"""
+        return {
+            'running': self.running,
+            'initialized': self.initialized,
+            'total_groups': len(self.groups),
+            'groups': [group.get_status() for group in self.groups],
+        }
+    
+    def calibrate_all(self, force: bool = False) -> bool:
+        """
+        校准所有组
+        
+        Args:
+            force: 是否强制重新校准
+            
+        Returns:
+            是否全部成功
+        """
+        logger.info(f"[MultiGroupSystem] 开始校准所有组...")
+        
+        success_count = 0
+        for group in self.groups:
+            logger.info(f"[MultiGroupSystem] 校准组: {group.group_id}")
+            if group._auto_calibrate(force=force):
+                success_count += 1
+        
+        logger.info(f"[MultiGroupSystem] 校准完成: {success_count}/{len(self.groups)} 组成功")
+        return success_count == len(self.groups)
+    
+    def wait(self):
+        """等待系统运行(阻塞)"""
+        try:
+            while self.running:
+                time.sleep(1)
+        except KeyboardInterrupt:
+            logger.info("[MultiGroupSystem] 收到中断信号")
+            self.stop()
+
+
+def main():
+    """多组系统主入口"""
+    import argparse
+    
+    parser = argparse.ArgumentParser(description='多组摄像头联动抓拍系统')
+    parser.add_argument('--skip-calibration', action='store_true', help='跳过自动校准')
+    parser.add_argument('--calibrate-only', action='store_true', help='仅执行校准后退出')
+    args = parser.parse_args()
+    
+    # 设置日志
+    logging.basicConfig(
+        level=logging.INFO,
+        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
+    )
+    
+    # 创建系统
+    system = MultiGroupSystem()
+    
+    # 初始化
+    if not system.initialize(skip_calibration=args.skip_calibration):
+        print("系统初始化失败!")
+        return 1
+    
+    # 仅校准模式
+    if args.calibrate_only:
+        print("校准完成,退出。")
+        system.stop()
+        return 0
+    
+    # 启动
+    if not system.start():
+        print("系统启动失败!")
+        return 1
+    
+    print(f"\n多组摄像头系统已启动 ({len(system.groups)} 个组)")
+    print("按 Ctrl+C 停止\n")
+    
+    # 等待
+    try:
+        system.wait()
+    except KeyboardInterrupt:
+        pass
+    
+    return 0
+
+
+if __name__ == '__main__':
+    sys.exit(main())