Просмотр исходного кода

refactor(coordinator): 重构联动控制器代码以提升性能和维护性

- 优化目标选择策略,支持多种排序和优先级切换
- 实现跨帧目标匹配,增强目标粘性,减少误切换
- 分离检测线程与PTZ控制线程,采用异步命令队列通信
- 支持球机端人体检测与自动变焦控制
- 改进OCR识别流程,添加频率限制以降低性能开销
- 增强事件驱动联动控制,支持多事件类型处理
- 实现检测结果去重,避免重叠目标重复跟踪
- 增加配对图片保存功能,便于后续分析和调试
- 改善状态管理与性能统计,提供详细运行时信息
- 优化异常处理与日志输出,提高系统鲁棒性
wenhongquan 3 недель назад
Родитель
Сommit
daea91e2d8
2 измененных файлов с 106 добавлено и 44 удалено
  1. 26 5
      dual_camera_system/coordinator.py
  2. 80 39
      dual_camera_system/paired_image_saver.py

+ 26 - 5
dual_camera_system/coordinator.py

@@ -1945,12 +1945,29 @@ class SequentialCoordinator(AsyncCoordinator):
         
         # 执行PTZ移动
         self._set_state(TrackingState.TRACKING)
+
+        # 【调试】记录移动前的 PTZ 位置
+        try:
+            current_pos = self.ptz.get_current_position()
+            logger.info(f"[顺序模式] PTZ移动前: pan={current_pos.pan:.1f}° tilt={current_pos.tilt:.1f}° zoom={current_pos.zoom}")
+        except Exception as e:
+            logger.warning(f"[顺序模式] 获取当前PTZ位置失败: {e}")
+            current_pos = None
+
         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...")
+
+            # 【调试】记录移动后的 PTZ 位置
+            time.sleep(0.5)  # 短暂等待后检查
+            try:
+                after_pos = self.ptz.get_current_position()
+                logger.info(f"[顺序模式] PTZ移动后: pan={after_pos.pan:.1f}° tilt={after_pos.tilt:.1f}° zoom={after_pos.zoom}")
+            except Exception as e:
+                logger.warning(f"[顺序模式] 获取PTZ位置失败: {e}")
             time.sleep(stabilize_time)
             
             # 【关键修复】清空RTSP缓冲区,确保获取的是新位置的帧
@@ -1976,19 +1993,23 @@ class SequentialCoordinator(AsyncCoordinator):
             
             # 获取清晰的球机画面
             ptz_frame = self._get_clear_ptz_frame()
-            
+
             if ptz_frame is not None:
+                # 【调试】记录抓拍的图像信息
+                frame_h, frame_w = ptz_frame.shape[:2]
+                logger.info(f"[顺序模式] 抓拍帧: {frame_w}x{frame_h}, 目标序号={person_index}, PTZ=({pan:.1f}°, {tilt:.1f}°, zoom={final_zoom})")
+
                 # 保存球机图片
                 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, 
+                        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"[顺序模式] 获取球机画面失败")

+ 80 - 39
dual_camera_system/paired_image_saver.py

@@ -223,6 +223,7 @@ class PairedImageSaver:
             # 初始化上传状态
             self._upload_status[batch_id] = {
                 'panorama': False,
+                'panorama_url': None,
                 'ptz': {},
                 'completed': False
             }
@@ -314,7 +315,7 @@ class PairedImageSaver:
                        person_info: Dict = None) -> Optional[str]:
         """
         保存球机聚焦图片
-        
+
         Args:
             batch_id: 批次ID
             person_index: 人员序号(0-based)
@@ -322,15 +323,19 @@ class PairedImageSaver:
             ptz_position: PTZ位置 (pan, tilt, zoom)
             ptz_bbox: 球机图中检测到的bbox (x1, y1, x2, y2)
             person_info: 额外人员信息
-            
+
         Returns:
             保存路径或 None
         """
+        # 【调试】记录传入的参数
+        logger.info(f"[配对保存] save_ptz_image: batch={batch_id}, person={person_index}, "
+                   f"PTZ=({ptz_position[0]:.1f}°, {ptz_position[1]:.1f}°, zoom={ptz_position[2]})")
+
         with self._batch_lock:
             if self._current_batch is None or self._current_batch.batch_id != batch_id:
-                logger.warning(f"[配对保存] 批次不存在或已过期: {batch_id}")
+                logger.warning(f"[配对保存] 批次不存在或已过期: {batch_id}, current={self._current_batch.batch_id if self._current_batch else None}")
                 return None
-            
+
             batch_dir = self.base_dir / f"batch_{batch_id}"
             
             try:
@@ -410,68 +415,92 @@ class PairedImageSaver:
     def _upload_panorama_to_oss(self, batch_id: str, panorama_path: str):
         """上传全景图到 OSS"""
         def on_upload_complete(result):
-            if result.success:
+            # 更新上传状态(即使 _current_batch 已切换也能正确记录)
+            self._upload_status[batch_id]['panorama'] = True
+            self._upload_status[batch_id]['panorama_url'] = result.oss_url
+            if self._current_batch and self._current_batch.batch_id == batch_id:
                 self._current_batch.panorama_oss_url = result.oss_url
-                self._upload_status[batch_id]['panorama'] = True
-                with self._stats_lock:
-                    self._stats['oss_upload_success'] += 1
-                logger.info(f"[OSS] 全景图上传成功: {result.oss_url}")
+            with self._stats_lock:
+                self._stats['oss_upload_success'] += 1
+            logger.info(f"[OSS] 全景图上传成功: {result.oss_url}")
+
+        def on_upload_error(result):
+            self._upload_status[batch_id]['panorama'] = False
+            self._upload_status[batch_id]['panorama_url'] = None
+            with self._stats_lock:
+                self._stats['oss_upload_failed'] += 1
+            logger.error(f"[OSS] 全景图上传失败: {result.error}")
+
+        # 包装回调,同时处理成功和失败
+        def on_upload_done(result):
+            if result.success:
+                on_upload_complete(result)
             else:
-                with self._stats_lock:
-                    self._stats['oss_upload_failed'] += 1
-                logger.error(f"[OSS] 全景图上传失败: {result.error}")
-        
+                on_upload_error(result)
+
         self.oss_uploader.upload_image(
             local_path=panorama_path,
             batch_id=batch_id,
             image_type='panorama',
-            callback=on_upload_complete
+            callback=on_upload_done
         )
     
     def _upload_ptz_to_oss(self, batch_id: str, person_index: int, ptz_path: str):
         """上传球机图到 OSS"""
         def on_upload_complete(result):
-            if result.success:
-                # 更新人员信息中的 OSS URL
+            # 更新上传状态(即使 _current_batch 已切换也能正确记录)
+            self._upload_status[batch_id]['ptz'][person_index] = result.oss_url
+            if self._current_batch and self._current_batch.batch_id == batch_id:
                 if person_index < len(self._current_batch.persons):
                     self._current_batch.persons[person_index].ptz_oss_url = result.oss_url
-                self._upload_status[batch_id]['ptz'][person_index] = result.oss_url
-                with self._stats_lock:
-                    self._stats['oss_upload_success'] += 1
-                logger.info(f"[OSS] 球机图上传成功 (person_{person_index}): {result.oss_url}")
+            with self._stats_lock:
+                self._stats['oss_upload_success'] += 1
+            logger.info(f"[OSS] 球机图上传成功 (person_{person_index}): {result.oss_url}")
+
+        def on_upload_error(result):
+            self._upload_status[batch_id]['ptz'][person_index] = None
+            with self._stats_lock:
+                self._stats['oss_upload_failed'] += 1
+            logger.error(f"[OSS] 球机图上传失败 (person_{person_index}): {result.error}")
+
+        # 包装回调,同时处理成功和失败
+        def on_upload_done(result):
+            if result.success:
+                on_upload_complete(result)
             else:
-                with self._stats_lock:
-                    self._stats['oss_upload_failed'] += 1
-                logger.error(f"[OSS] 球机图上传失败 (person_{person_index}): {result.error}")
-        
+                on_upload_error(result)
+
         self.oss_uploader.upload_image(
             local_path=ptz_path,
             batch_id=batch_id,
             image_type='ptz',
             person_index=person_index,
-            callback=on_upload_complete
+            callback=on_upload_done
         )
     
     def _finalize_batch(self, batch: DetectionBatch):
         """完成批次处理"""
         batch.completed = True
-        
-        # 等待 OSS 上传完成(最多等待5秒)
+
+        # 等待 OSS 上传完成(最多等待10秒)
         if self.enable_oss and batch.batch_id in self._upload_status:
             wait_start = time.time()
-            while time.time() - wait_start < 5.0:
+            max_wait = 10.0  # 增加等待时间
+            while time.time() - wait_start < max_wait:
                 status = self._upload_status[batch.batch_id]
-                # 检查全景图是否上传完成
-                panorama_done = status.get('panorama', False) or not batch.panorama_path
+                # 检查全景图是否上传完成(优先检查 _upload_status 中的状态)
+                panorama_url = status.get('panorama_url')
+                panorama_done = panorama_url is not None or not batch.panorama_path
                 # 检查所有球机图是否上传完成
+                ptz_status = status.get('ptz', {})
                 ptz_done = all(
-                    idx in status.get('ptz', {})
+                    ptz_status.get(idx) is not None
                     for idx, person in enumerate(batch.persons)
                     if person.ptz_image_saved
                 )
                 if panorama_done and ptz_done:
                     break
-                time.sleep(0.1)
+                time.sleep(0.2)
         
         # 创建 batch_info.json 文件
         try:
@@ -514,13 +543,22 @@ class PairedImageSaver:
     def _build_batch_info_json(self, batch: DetectionBatch) -> Dict:
         """
         构建 batch_info.json 数据结构
-        
+
         Returns:
             Dict: 批次信息字典
         """
+        # 从上传状态获取最新的 OSS URL(异步回调可能已更新)
+        upload_status = self._upload_status.get(batch.batch_id, {})
+
+        # 获取全景图 OSS URL(优先使用回调更新的状态)
+        panorama_oss_url = upload_status.get('panorama_url', batch.panorama_oss_url)
+
         # 人员信息列表
         persons_list = []
         for person in batch.persons:
+            # 获取球机图 OSS URL(优先使用回调更新的状态)
+            ptz_oss_url = upload_status.get('ptz', {}).get(person.person_index, person.ptz_oss_url)
+
             person_data = {
                 'person_index': person.person_index,
                 'position': {
@@ -547,10 +585,10 @@ class PairedImageSaver:
                 } if person.ptz_bbox else None,
                 'ptz_image_saved': person.ptz_image_saved,
                 'ptz_image_path': person.ptz_image_path,
-                'ptz_oss_url': person.ptz_oss_url
+                'ptz_oss_url': ptz_oss_url
             }
             persons_list.append(person_data)
-        
+
         # 构建完整批次信息
         batch_info = {
             'batch_id': batch.batch_id,
@@ -562,15 +600,18 @@ class PairedImageSaver:
             'ptz_images_count': batch.ptz_images_count,
             'panorama': {
                 'local_path': batch.panorama_path,
-                'oss_url': batch.panorama_oss_url
+                'oss_url': panorama_oss_url
             },
             'persons': persons_list,
             'upload_status': {
-                'panorama_uploaded': batch.panorama_oss_url is not None,
-                'all_ptz_uploaded': all(p.ptz_oss_url is not None for p in batch.persons if p.ptz_image_saved)
+                'panorama_uploaded': panorama_oss_url is not None,
+                'all_ptz_uploaded': all(
+                    upload_status.get('ptz', {}).get(p.person_index) is not None
+                    for p in batch.persons if p.ptz_image_saved
+                )
             }
         }
-        
+
         return batch_info
     
     def _save_batch_info_txt(self, batch: DetectionBatch, txt_path: Path):