Procházet zdrojové kódy

refactor(coordinator): 重构联动控制器及异步处理逻辑

- 重构 Coordinator 类,优化全景与球机联动控制架构
- 引入异步联动控制器 AsyncCoordinator,分离检测与PTZ控制线程
- 实现目标选择器 TargetSelector,支持多策略目标排序和粘性机制
- 增强 OCR 识别逻辑,添加频率控制与结果回调
- 支持事件驱动联动,增加事件队列处理和事件触发跟踪
- 引入配对图片保存机制,支持跨时间窗口批次管理
- 改进性能统计与日志记录,增加帧率及检测状态输出
- 优化 PTZ 控制,添加命令冷却和位置变化阈值判断
- 统一异常处理与线程管理,提升系统稳定性和可维护性
wenhongquan před 3 dny
rodič
revize
ea798a2ab2

binární
dual_camera_system/__pycache__/coordinator.cpython-313.pyc


binární
dual_camera_system/__pycache__/paired_image_saver.cpython-313.pyc


+ 2 - 2
dual_camera_system/config/detection.py

@@ -10,7 +10,7 @@ DETECTION_CONFIG = {
     'detection_interval': 0.5,       # 兼容保留:检测间隔(秒),当detection_fps=2时间隔为0.5秒
     
     # 检测图片保存配置
-    'save_detection_image': True,   # 是否保存检测到人的图片
+    'save_detection_image': False,   # 是否保存检测到人的图片
     'detection_image_dir': '/home/admin/dsh/detection_images',  # 图片保存目录
     
     # 配对图片保存配置(全景+球机图片归入同一目录)
@@ -65,5 +65,5 @@ SAFETY_DETECTION_CONFIG = {
     # 检测图片保存配置
     'save_detection_image': True,     # 是否保存检测到人的图片
     'detection_image_dir': '/home/admin/dsh/detection_images',  # 图片保存目录
-    'detection_image_max_count': 1000,  # 最大保存图片数量,超过后自动清理旧图片
+    'detection_image_max_count': 100,  # 最大保存图片数量,超过后自动清理旧图片
 }

+ 122 - 9
dual_camera_system/coordinator.py

@@ -12,6 +12,7 @@ from dataclasses import dataclass, field
 from enum import Enum
 
 import numpy as np
+import cv2
 
 from config import COORDINATOR_CONFIG, SYSTEM_CONFIG, PTZ_CONFIG, DETECTION_CONFIG
 from panorama_camera import PanoramaCamera, ObjectDetector, PersonTracker, DetectedObject
@@ -1081,7 +1082,8 @@ class AsyncCoordinator(Coordinator):
             batch_id=self._current_batch_id,
             person_index=person_index,
             ptz_frame=ptz_frame,
-            ptz_position=ptz_position
+            ptz_position=ptz_position,
+            ptz_bbox=getattr(self, '_last_ptz_bbox', None)
         )
 
     def _ptz_worker(self):
@@ -1220,21 +1222,28 @@ class AsyncCoordinator(Coordinator):
         success = self.ptz.goto_exact_position(pan, tilt, zoom)
         
         if success:
+            # 等待球机物理移动到位(增加额外等待确保画面清晰)
             time.sleep(self.PTZ_CONFIRM_WAIT)
             
             # 球机端人体检测与自动对焦
+            final_pan, final_tilt, final_zoom = pan, tilt, zoom
             if self.enable_ptz_detection and self.auto_zoom_config.get('enabled', False):
-                final_zoom = self._auto_zoom_person(pan, tilt, zoom)
-                if final_zoom != zoom:
-                    zoom = final_zoom
+                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 self._enable_paired_saving and track_id is not None:
-                ptz_frame = self.ptz.get_frame()
-                if ptz_frame is not None:
-                    self._save_ptz_image_for_person(track_id, ptz_frame, (pan, tilt, zoom))
+            if self._enable_paired_saving and track_id is not None and ptz_frame is not None:
+                # 使用球机端检测器检测人体并标记
+                ptz_frame_marked = self._mark_ptz_frame_with_detection(ptz_frame, person_index=self._person_ptz_index.get(track_id, 0))
+                self._save_ptz_image_for_person(track_id, ptz_frame_marked, (final_pan, final_tilt, final_zoom))
             
-            logger.info(f"[PTZ] 到位确认完成: pan={pan:.1f}° tilt={tilt:.1f}°")
+            logger.info(f"[PTZ] 到位确认完成: pan={final_pan:.1f}° tilt={final_tilt:.1f}° zoom={final_zoom}")
         else:
             logger.warning(f"[PTZ] 命令执行失败: pan={pan:.1f}° tilt={tilt:.1f}° zoom={zoom}")
     
@@ -1267,6 +1276,110 @@ class AsyncCoordinator(Coordinator):
             logger.error(f"[AutoZoom] 自动对焦异常: {e}")
             return initial_zoom
     
+    def _get_clear_ptz_frame(self, max_attempts: int = 5, wait_interval: float = 0.1) -> Optional[np.ndarray]:
+        """
+        获取清晰的球机画面
+        尝试多次获取,丢弃模糊/过渡帧
+        
+        Args:
+            max_attempts: 最大尝试次数
+            wait_interval: 每次等待间隔
+            
+        Returns:
+            清晰的球机帧或 None
+        """
+        best_frame = None
+        best_score = -1
+        
+        for i in range(max_attempts):
+            frame = self.ptz.get_frame()
+            if frame is not None:
+                # 使用拉普拉斯算子评估图像清晰度
+                gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
+                laplacian_var = cv2.Laplacian(gray, cv2.CV_64F).var()
+                
+                if laplacian_var > best_score:
+                    best_score = laplacian_var
+                    best_frame = frame.copy()
+                
+                # 如果清晰度足够高,直接返回
+                if laplacian_var > 100:  # 清晰度阈值
+                    return frame
+            
+            time.sleep(wait_interval)
+        
+        return best_frame
+    
+    def _mark_ptz_frame_with_detection(self, frame: np.ndarray, person_index: int) -> np.ndarray:
+        """
+        在球机帧上标记检测到的人体
+        
+        Args:
+            frame: 球机帧
+            person_index: 人员序号
+            
+        Returns:
+            标记后的帧
+        """
+        marked_frame = frame.copy()
+        h, w = marked_frame.shape[:2]
+        
+        # 重置保存的bbox
+        self._last_ptz_bbox = None
+        
+        # 使用球机端检测器检测人体
+        if self.ptz_detector is not None:
+            try:
+                persons = self.ptz_detector.detect(frame)
+                if persons:
+                    # 找到最大的人体(假设是目标)
+                    largest_person = max(persons, key=lambda p: p.area)
+                    x1, y1, x2, y2 = largest_person.bbox
+                    
+                    # 保存bbox供后续使用
+                    self._last_ptz_bbox = (x1, y1, x2, y2)
+                    
+                    # 绘制边界框(红色,区别于全景的绿色)
+                    cv2.rectangle(marked_frame, (x1, y1), (x2, y2), (0, 0, 255), 2)
+                    
+                    # 绘制标签
+                    label = f"person_{person_index} ({largest_person.confidence:.2f})"
+                    (label_w, label_h), _ = cv2.getTextSize(
+                        label, cv2.FONT_HERSHEY_SIMPLEX, 0.7, 2
+                    )
+                    
+                    # 标签背景(红色)
+                    cv2.rectangle(
+                        marked_frame,
+                        (x1, y1 - label_h - 8),
+                        (x1 + label_w, y1),
+                        (0, 0, 255),
+                        -1
+                    )
+                    
+                    # 标签文字(白色)
+                    cv2.putText(
+                        marked_frame, label,
+                        (x1, y1 - 4),
+                        cv2.FONT_HERSHEY_SIMPLEX, 0.7,
+                        (255, 255, 255), 2
+                    )
+                    
+                    logger.info(f"[配对保存] 球机图标记: person_{person_index}, "
+                               f"位置=({x1},{y1},{x2},{y2}), 置信度={largest_person.confidence:.2f}")
+                else:
+                    # 未检测到人体,在画面中心添加提示
+                    cv2.putText(
+                        marked_frame, f"person_{person_index} (no detection)",
+                        (w // 2 - 100, h // 2),
+                        cv2.FONT_HERSHEY_SIMPLEX, 0.8,
+                        (0, 0, 255), 2
+                    )
+            except Exception as e:
+                logger.error(f"[配对保存] 球机图检测标记失败: {e}")
+        
+        return marked_frame
+    
     def _confirm_ptz_position(self, x_ratio: float, y_ratio: float):
         """PTZ位置确认:读取球机帧验证目标是否可见"""
         if not hasattr(self.ptz, 'get_frame') or self.ptz.get_frame() is None:

+ 38 - 8
dual_camera_system/paired_image_saver.py

@@ -24,6 +24,7 @@ class PersonTrackingInfo:
     bbox: Tuple[int, int, int, int]  # (x1, y1, x2, y2)
     confidence: float
     ptz_position: Optional[Tuple[float, float, int]] = None  # (pan, tilt, zoom)
+    ptz_bbox: Optional[Tuple[int, int, int, int]] = None  # 球机图中检测到的bbox (x1, y1, x2, y2)
     ptz_image_saved: bool = False
     ptz_image_path: Optional[str] = None
 
@@ -192,16 +193,25 @@ class PairedImageSaver:
             # 复制图像避免修改原图
             marked_frame = frame.copy()
             
-            # 绘制每个人员的标记
-            for i, person in enumerate(persons):
+            # 过滤有效人员(置信度 >= 阈值)
+            person_threshold = 0.8  # 人员检测置信度阈值
+            valid_persons = []
+            for person in persons:
+                conf = person.get('confidence', 0.0)
+                if conf >= person_threshold:
+                    valid_persons.append(person)
+            
+            # 绘制每个有效人员的标记(使用连续的序号)
+            for i, person in enumerate(valid_persons):
                 bbox = person.get('bbox', (0, 0, 0, 0))
                 x1, y1, x2, y2 = bbox
+                conf = person.get('confidence', 0.0)
                 
                 # 绘制边界框(绿色)
                 cv2.rectangle(marked_frame, (x1, y1), (x2, y2), (0, 255, 0), 2)
                 
-                # 绘制序号标签
-                label = f"person_{i}"
+                # 绘制序号标签(带置信度)
+                label = f"person_{i}({conf:.2f})"
                 (label_w, label_h), baseline = cv2.getTextSize(
                     label, cv2.FONT_HERSHEY_SIMPLEX, 0.8, 2
                 )
@@ -223,12 +233,12 @@ class PairedImageSaver:
                     (0, 0, 0), 2
                 )
             
-            # 保存图片
-            filename = f"00_panorama_n{len(persons)}.jpg"
+            # 保存图片(使用有效人员数量)
+            filename = f"00_panorama_n{len(valid_persons)}.jpg"
             filepath = batch_dir / filename
             cv2.imwrite(str(filepath), marked_frame, [cv2.IMWRITE_JPEG_QUALITY, 90])
             
-            logger.info(f"[配对保存] 全景图已保存: {filepath}")
+            logger.info(f"[配对保存] 全景图已保存: {filepath},有效人员 {len(valid_persons)}/{len(persons)}")
             return str(filepath)
             
         except Exception as e:
@@ -237,6 +247,7 @@ class PairedImageSaver:
     
     def save_ptz_image(self, batch_id: str, person_index: int,
                        ptz_frame, ptz_position: Tuple[float, float, int],
+                       ptz_bbox: Tuple[int, int, int, int] = None,
                        person_info: Dict = None) -> Optional[str]:
         """
         保存球机聚焦图片
@@ -246,6 +257,7 @@ class PairedImageSaver:
             person_index: 人员序号(0-based)
             ptz_frame: 球机帧
             ptz_position: PTZ位置 (pan, tilt, zoom)
+            ptz_bbox: 球机图中检测到的bbox (x1, y1, x2, y2)
             person_info: 额外人员信息
             
         Returns:
@@ -284,6 +296,18 @@ class PairedImageSaver:
                         cv2.FONT_HERSHEY_SIMPLEX, 0.7,
                         (0, 255, 0), 2
                     )
+                    
+                    # 绘制PTZ检测到的bbox(红色)
+                    if ptz_bbox is not None:
+                        x1, y1, x2, y2 = ptz_bbox
+                        cv2.rectangle(marked_frame, (x1, y1), (x2, y2), (0, 0, 255), 2)
+                        bbox_text = f"PTZ_BBox: ({x1},{y1},{x2},{y2})"
+                        cv2.putText(
+                            marked_frame, bbox_text,
+                            (10, 90),
+                            cv2.FONT_HERSHEY_SIMPLEX, 0.6,
+                            (0, 0, 255), 2
+                        )
                 
                 # 保存图片
                 filename = f"01_ptz_person{person_index}_p{int(ptz_position[0])}_t{int(ptz_position[1])}_z{int(ptz_position[2])}.jpg"
@@ -295,6 +319,7 @@ class PairedImageSaver:
                 # 更新批次信息
                 if person_index < len(self._current_batch.persons):
                     self._current_batch.persons[person_index].ptz_position = ptz_position
+                    self._current_batch.persons[person_index].ptz_bbox = ptz_bbox
                     self._current_batch.persons[person_index].ptz_image_saved = True
                     self._current_batch.persons[person_index].ptz_image_path = str(filepath)
                 
@@ -303,7 +328,7 @@ class PairedImageSaver:
                 with self._stats_lock:
                     self._stats['total_ptz_images'] += 1
                 
-                logger.info(f"[配对保存] 球机图已保存: {filepath}")
+                logger.info(f"[配对保存] 球机图已保存: {filepath}, BBox={ptz_bbox}")
                 return str(filepath)
                 
             except Exception as e:
@@ -331,8 +356,13 @@ class PairedImageSaver:
                     f.write(f"\n  Person {i}:\n")
                     f.write(f"    Track ID: {person.track_id}\n")
                     f.write(f"    Position: ({person.position[0]:.3f}, {person.position[1]:.3f})\n")
+                    f.write(f"    BBox: ({person.bbox[0]}, {person.bbox[1]}, {person.bbox[2]}, {person.bbox[3]})\n")
                     f.write(f"    Confidence: {person.confidence:.2f}\n")
                     f.write(f"    PTZ Position: {person.ptz_position}\n")
+                    if person.ptz_bbox:
+                        f.write(f"    PTZ BBox: ({person.ptz_bbox[0]}, {person.ptz_bbox[1]}, {person.ptz_bbox[2]}, {person.ptz_bbox[3]})\n")
+                    else:
+                        f.write(f"    PTZ BBox: None\n")
                     f.write(f"    PTZ Image: {person.ptz_image_path}\n")
             
             logger.info(f"[配对保存] 批次完成: {batch.batch_id}, "