Bläddra i källkod

refactor(coordinator): 重构联动控制器以优化异步检测与PTZ控制

- 将联动控制器拆分为检测线程和PTZ控制线程,分离职责提高响应速度
- 实现异步队列通信,避免检测阻塞PTZ控制
- 新增球机端人体检测初始化及自动变焦控制
- 加强目标选择策略,支持目标粘性和多策略切换
- 优化目标跨帧匹配逻辑,支持连续目标跟踪
- 添加配对图片保存功能,支持按检测批次保存图片
- 改善OCR调用频率控制及结果回调机制
- 完善状态管理与性能统计,支持多线程安全访问
- 引入事件驱动联动控制,支持基于事件触发联动跟踪
- 增加PTZ位置确认机制和
wenhongquan 2 dagar sedan
förälder
incheckning
b37fcadb2e

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


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


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


+ 11 - 0
dual_camera_system/config/coordinator.py

@@ -21,6 +21,17 @@ COORDINATOR_CONFIG = {
         'switch_on_lost': True,       # 当前目标丢失时是否切换到次优目标
         'stickiness': 0.3,            # 目标粘性(0-1),值越大越不容易切换目标
     },
+    
+    # 顺序抓拍模式配置
+    'sequential_mode': {
+        'enabled': True,              # 是否启用顺序抓拍模式
+        'ptz_stabilize_time': 1.0,    # PTZ到位后稳定等待时间(秒)
+        'capture_wait_time': 0.5,     # 抓拍间隔时间(秒)
+        'return_to_panorama': True,   # 完成后是否回到全景默认位置
+        'default_pan': 0.0,           # 默认pan角度
+        'default_tilt': 0.0,          # 默认tilt角度
+        'default_zoom': 1,            # 默认zoom(广角,1=最小变焦)
+    },
 }
 
 CALIBRATION_CONFIG = {

+ 340 - 0
dual_camera_system/coordinator.py

@@ -1534,3 +1534,343 @@ class AsyncCoordinator(Coordinator):
     def on_ptz_confirmed(self, callback: Callable):
         """注册PTZ位置确认回调"""
         self._on_ptz_confirmed = callback
+
+
+class SequentialCoordinator(AsyncCoordinator):
+    """
+    顺序联动控制器 — 全景检测→逐个PTZ抓拍→回到全景的循环模式
+    
+    工作流程:
+    1. 全景摄像头检测人员
+    2. 检测到人员后,暂停全景检测
+    3. 球机依次对每个检测到的人员进行PTZ定位+变焦抓拍
+    4. 所有人员抓拍完成后,球机回到默认位置
+    5. 恢复全景检测,进入下一轮循环
+    
+    适用于需要高质量抓拍的场景,确保每个目标都能被球机清晰拍摄
+    """
+    
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        
+        # 顺序抓拍状态
+        self._capture_state = 'idle'  # 'idle', 'detecting', 'capturing', 'returning'
+        self._capture_state_lock = threading.Lock()
+        
+        # 当前批次检测到的目标
+        self._batch_targets: List[TrackingTarget] = []
+        self._batch_targets_lock = threading.Lock()
+        self._current_capture_index = 0
+        
+        # 抓拍完成事件(用于同步)
+        self._capture_done_event = threading.Event()
+        
+        # 配置参数
+        self._capture_config = {
+            'ptz_stabilize_time': 1.0,      # PTZ到位后稳定等待时间
+            'capture_wait_time': 0.5,       # 抓拍等待时间
+            'return_to_panorama': True,     # 完成后是否回到全景默认位置
+            'default_pan': 0.0,             # 默认pan角度
+            'default_tilt': 0.0,            # 默认tilt角度
+            'default_zoom': 1,              # 默认zoom(广角)
+        }
+        
+        # 覆盖父类的PTZ冷却时间(顺序模式下可以更短)
+        self.PTZ_COMMAND_COOLDOWN = 0.1
+        
+        logger.info("[SequentialCoordinator] 顺序联动控制器初始化完成")
+    
+    def _detection_worker(self):
+        """检测线程:顺序模式下的检测逻辑"""
+        # 从 DETECTION_CONFIG 获取检测帧率,默认每秒2帧
+        detection_fps = self.config.get('detection_fps', DETECTION_CONFIG.get('detection_fps', 2))
+        detection_interval = 1.0 / detection_fps
+        last_detection_time = 0
+        
+        frame_count = 0
+        last_log_time = time.time()
+        log_interval = 5.0
+        detection_run_count = 0
+        detection_person_count = 0
+        last_no_detect_log_time = 0
+        no_detect_log_interval = 30.0
+        
+        with self.stats_lock:
+            self.stats['start_time'] = time.time()
+        
+        if self.detector is None:
+            logger.warning("[检测线程] ⚠️ 人体检测器未初始化!")
+        else:
+            logger.info(f"[检测线程] ✓ 顺序模式已就绪, 检测帧率={detection_fps}fps")
+        
+        while self.running:
+            try:
+                current_time = time.time()
+                
+                # 获取当前帧
+                frame = self.panorama.get_frame()
+                if frame is None:
+                    time.sleep(0.01)
+                    continue
+                
+                frame_count += 1
+                self._update_stats('frames_processed')
+                frame_size = (frame.shape[1], frame.shape[0])
+                
+                # 日志输出
+                if current_time - last_log_time >= log_interval:
+                    elapsed = current_time - last_log_time
+                    fps = frame_count / elapsed if elapsed > 0 else 0
+                    state_str = self._get_capture_state()
+                    logger.info(f"[检测线程] 帧率={fps:.1f}fps, 状态={state_str}, "
+                               f"检测轮次={detection_run_count}(有人={detection_person_count})")
+                    frame_count = 0
+                    last_log_time = current_time
+                
+                # 状态机处理
+                state = self._get_capture_state()
+                
+                if state == 'idle':
+                    # 空闲状态:周期性检测
+                    if current_time - last_detection_time >= detection_interval:
+                        last_detection_time = current_time
+                        detection_run_count += 1
+                        
+                        # 执行检测
+                        detections = self._detect_persons(frame)
+                        
+                        if detections:
+                            self._update_stats('persons_detected', len(detections))
+                            detection_person_count += 1
+                            
+                            # 更新跟踪目标
+                            self._update_tracking_targets(detections, frame_size)
+                            
+                            # 获取有效目标列表
+                            targets = self._get_all_valid_targets()
+                            
+                            if targets:
+                                logger.info(f"[顺序模式] 检测到 {len(targets)} 个目标,开始顺序抓拍")
+                                
+                                # 创建配对保存批次
+                                if self._enable_paired_saving and self._paired_saver is not None:
+                                    self._create_detection_batch(frame, detections, frame_size)
+                                
+                                # 切换到抓拍状态
+                                self._start_capture_sequence(targets)
+                        else:
+                            # 未检测到人员
+                            if current_time - last_no_detect_log_time >= no_detect_log_interval:
+                                logger.info(f"[检测] 本轮未检测到人员 (累计{detection_run_count}轮)")
+                                last_no_detect_log_time = current_time
+                
+                elif state == 'capturing':
+                    # 抓拍状态中:检测线程等待,不执行新检测
+                    # 等待PTZ线程完成当前批次
+                    pass
+                
+                elif state == 'returning':
+                    # 球机回到默认位置中
+                    pass
+                
+                # 清理过期目标
+                self._cleanup_expired_targets()
+                time.sleep(0.01)
+                
+            except Exception as e:
+                logger.error(f"[检测线程] 错误: {e}")
+                time.sleep(0.1)
+    
+    def _ptz_worker(self):
+        """PTZ控制线程:顺序模式下的PTZ控制逻辑"""
+        logger.info("[PTZ线程] 顺序模式PTZ控制线程启动")
+        
+        while self.running:
+            try:
+                state = self._get_capture_state()
+                
+                if state == 'capturing':
+                    # 执行顺序抓拍
+                    self._execute_sequential_capture()
+                
+                elif state == 'returning':
+                    # 回到默认位置
+                    self._return_to_default_position()
+                
+                time.sleep(0.05)
+                
+            except Exception as e:
+                logger.error(f"[PTZ线程] 错误: {e}")
+                time.sleep(0.1)
+    
+    def _get_capture_state(self) -> str:
+        """获取当前抓拍状态"""
+        with self._capture_state_lock:
+            return self._capture_state
+    
+    def _set_capture_state(self, state: str):
+        """设置抓拍状态"""
+        with self._capture_state_lock:
+            old_state = self._capture_state
+            self._capture_state = state
+            logger.info(f"[顺序模式] 状态切换: {old_state} -> {state}")
+    
+    def _start_capture_sequence(self, targets: List[TrackingTarget]):
+        """开始顺序抓拍序列"""
+        with self._batch_targets_lock:
+            self._batch_targets = targets.copy()
+            self._current_capture_index = 0
+        
+        self._set_capture_state('capturing')
+    
+    def _execute_sequential_capture(self):
+        """执行顺序抓拍(依次对每个目标进行PTZ定位和抓拍)"""
+        with self._batch_targets_lock:
+            targets = self._batch_targets.copy()
+            current_idx = self._current_capture_index
+        
+        if current_idx >= len(targets):
+            # 所有目标已抓拍完成
+            logger.info("[顺序模式] 所有目标抓拍完成")
+            self._set_capture_state('returning')
+            return
+        
+        # 获取当前目标
+        target = targets[current_idx]
+        x_ratio, y_ratio = target.position
+        
+        # 计算PTZ角度
+        if self.enable_calibration and self.calibrator and self.calibrator.is_calibrated():
+            pan, tilt = self.calibrator.transform(x_ratio, y_ratio)
+            if self.ptz.ptz_config.get('pan_flip', False):
+                pan = (pan + 180) % 360
+            zoom = self.ptz.ptz_config.get('default_zoom', 8)
+            coord_type = "校准坐标"
+        else:
+            pan, tilt, zoom = self.ptz.calculate_ptz_position(x_ratio, y_ratio)
+            coord_type = "估算坐标"
+        
+        # 获取批次信息
+        batch_id = self._current_batch_id if self._enable_paired_saving else None
+        person_index = current_idx  # 使用当前索引作为人员序号
+        
+        logger.info(f"[顺序模式] 抓拍目标 {current_idx + 1}/{len(targets)}: "
+                   f"位置=({x_ratio:.3f}, {y_ratio:.3f}) -> "
+                   f"PTZ=({pan:.1f}°, {tilt:.1f}°, zoom={zoom}) [{coord_type}]")
+        
+        # 执行PTZ移动
+        self._set_state(TrackingState.TRACKING)
+        success = self.ptz.goto_exact_position(pan, tilt, zoom)
+        
+        if success:
+            # 等待球机稳定
+            stabilize_time = self._capture_config['ptz_stabilize_time']
+            logger.info(f"[顺序模式] 等待球机稳定 {stabilize_time}s...")
+            time.sleep(stabilize_time)
+            
+            # 自动变焦(如果启用)
+            final_zoom = zoom
+            if self.enable_ptz_detection and self.auto_zoom_config.get('enabled', False):
+                auto_zoom_result = self._auto_zoom_person(pan, tilt, zoom)
+                if auto_zoom_result != zoom:
+                    final_zoom = auto_zoom_result
+                    time.sleep(0.5)  # 变焦后再次等待
+            
+            # 获取清晰的球机画面
+            ptz_frame = self._get_clear_ptz_frame()
+            
+            if ptz_frame is not None:
+                # 保存球机图片
+                if self._enable_paired_saving and batch_id is not None:
+                    ptz_frame_marked = self._mark_ptz_frame_with_detection(ptz_frame, person_index=person_index)
+                    self._save_ptz_image_for_person_batch(
+                        batch_id, person_index, ptz_frame_marked, 
+                        (pan, tilt, final_zoom)
+                    )
+                
+                # 保存到本地(无论是否启用配对保存)
+                self._save_local_snapshot(ptz_frame, current_idx, pan, tilt, final_zoom)
+                
+                logger.info(f"[顺序模式] 目标 {current_idx + 1} 抓拍完成")
+            else:
+                logger.warning(f"[顺序模式] 获取球机画面失败")
+        else:
+            logger.warning(f"[顺序模式] PTZ移动失败")
+        
+        # 移动到下一个目标
+        with self._batch_targets_lock:
+            self._current_capture_index += 1
+        
+        # 抓拍间隔
+        time.sleep(self._capture_config['capture_wait_time'])
+    
+    def _return_to_default_position(self):
+        """球机回到默认位置(广角全景)"""
+        if not self._capture_config['return_to_panorama']:
+            # 不回到默认位置,直接回到空闲状态
+            self._set_capture_state('idle')
+            return
+        
+        default_pan = self._capture_config['default_pan']
+        default_tilt = self._capture_config['default_tilt']
+        default_zoom = self._capture_config['default_zoom']
+        
+        logger.info(f"[顺序模式] 球机回到默认位置: ({default_pan}°, {default_tilt}°, zoom={default_zoom})")
+        
+        success = self.ptz.goto_exact_position(default_pan, default_tilt, default_zoom)
+        
+        if success:
+            # 等待到位
+            time.sleep(self._capture_config['ptz_stabilize_time'])
+            logger.info("[顺序模式] 球机已回到默认位置")
+        else:
+            logger.warning("[顺序模式] 球机回到默认位置失败")
+        
+        # 回到空闲状态,恢复全景检测
+        self._set_capture_state('idle')
+        
+        # 清空批次目标
+        with self._batch_targets_lock:
+            self._batch_targets = []
+            self._current_capture_index = 0
+    
+    def _save_local_snapshot(self, frame: np.ndarray, index: int, 
+                              pan: float, tilt: float, zoom: int):
+        """保存本地快照"""
+        try:
+            import os
+            from datetime import datetime
+            
+            # 创建保存目录
+            save_dir = '/home/admin/dsh/captures'
+            os.makedirs(save_dir, exist_ok=True)
+            
+            # 生成文件名
+            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3]
+            filename = f"capture_{timestamp}_person{index}_p{int(pan)}_t{int(tilt)}_z{zoom}.jpg"
+            filepath = os.path.join(save_dir, filename)
+            
+            # 保存图片
+            cv2.imwrite(filepath, frame, [cv2.IMWRITE_JPEG_QUALITY, 90])
+            logger.info(f"[顺序模式] 快照已保存: {filepath}")
+            
+        except Exception as e:
+            logger.error(f"[顺序模式] 保存快照失败: {e}")
+    
+    def set_capture_config(self, **kwargs):
+        """设置抓拍配置"""
+        self._capture_config.update(kwargs)
+        logger.info(f"[顺序模式] 配置已更新: {kwargs}")
+    
+    def get_capture_status(self) -> dict:
+        """获取当前抓拍状态"""
+        with self._batch_targets_lock:
+            total = len(self._batch_targets)
+            current = self._current_capture_index
+        
+        return {
+            'state': self._get_capture_state(),
+            'total_targets': total,
+            'current_index': current,
+            'remaining': max(0, total - current)
+        }

+ 31 - 8
dual_camera_system/main.py

@@ -34,7 +34,7 @@ from dahua_sdk import DahuaSDK
 from panorama_camera import PanoramaCamera, ObjectDetector, DetectedObject
 from ptz_camera import PTZCamera, PTZController
 from ocr_recognizer import NumberDetector, PersonInfo
-from coordinator import Coordinator, EventDrivenCoordinator, AsyncCoordinator
+from coordinator import Coordinator, EventDrivenCoordinator, AsyncCoordinator, SequentialCoordinator
 
 
 # 配置日志 - 使用LOG_CONFIG
@@ -170,13 +170,36 @@ class DualCameraSystem:
         ptz_config = self.config.get('ptz_camera', PTZ_CAMERA)
         self.ptz_camera = PTZCamera(self.sdk, ptz_config)
         
-        # 初始化联动控制器(使用异步版本,检测与PTZ分线程)
-        self.coordinator = AsyncCoordinator(
-            self.panorama_camera,
-            self.ptz_camera,
-            self.detector,
-            self.number_detector
-        )
+        # 初始化联动控制器
+        # 根据配置选择顺序模式或异步模式
+        sequential_mode = COORDINATOR_CONFIG.get('sequential_mode', {}).get('enabled', False)
+        
+        if sequential_mode:
+            logger.info("使用顺序联动控制器 (SequentialCoordinator)")
+            self.coordinator = SequentialCoordinator(
+                self.panorama_camera,
+                self.ptz_camera,
+                self.detector,
+                self.number_detector
+            )
+            # 应用顺序模式配置
+            seq_config = COORDINATOR_CONFIG.get('sequential_mode', {})
+            self.coordinator.set_capture_config(
+                ptz_stabilize_time=seq_config.get('ptz_stabilize_time', 1.0),
+                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)
+            )
+        else:
+            logger.info("使用异步联动控制器 (AsyncCoordinator)")
+            self.coordinator = AsyncCoordinator(
+                self.panorama_camera,
+                self.ptz_camera,
+                self.detector,
+                self.number_detector
+            )
         
         # 设置回调
         self._setup_callbacks()