Bläddra i källkod

refactor(coordinator): 重构联动控制器,拆分异步检测与PTZ控制线程

- 将联动控制器拆分为异步版本,分离检测线程与PTZ控制线程
- 检测线程负责持续读取全景帧及YOLO推理,发送PTZ命令至队列
- PTZ控制线程接收命令队列,实现独立球机控制和位置确认
- 支持基于目标选择器的目标优先选择与跟踪粘性控制
- 添加配对图片保存功能,支持按时间窗口批次管理
- 增强日志记录与性能统计,支持不同坐标转换方式
- 优化OCR执行时机与频率控制,避免资源浪费
- 保留兼容事件驱动及基础联动逻辑,支持多种配置开关
wenhongquan 3 dagar sedan
förälder
incheckning
2676c974bf

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


BIN
dual_camera_system/__pycache__/paired_image_saver.cpython-313.pyc


+ 63 - 14
dual_camera_system/coordinator.py

@@ -745,6 +745,8 @@ class PTZCommand:
     y_ratio: float = 0.0
     use_calibration: bool = True
     track_id: Optional[int] = None  # 跟踪目标ID(用于配对图片保存)
+    batch_id: Optional[str] = None  # 批次ID(用于配对图片保存)
+    person_index: int = -1  # 人员在批次中的序号(用于配对图片保存)
 
 
 class AsyncCoordinator(Coordinator):
@@ -1035,11 +1037,22 @@ class AsyncCoordinator(Coordinator):
         if self._paired_saver is None:
             return
         
-        # 构建人员信息列表
+        # 过滤有效人员(置信度 >= 阈值)
+        person_threshold = DETECTION_CONFIG.get('person_threshold', 0.8)
+        valid_persons = []
+        for det in tracked:
+            if det.confidence >= person_threshold:
+                valid_persons.append(det)
+        
+        if not valid_persons:
+            logger.debug(f"[配对保存] 无有效人员(阈值={person_threshold}),跳过批次创建")
+            return
+        
+        # 构建人员信息列表(只包含有效人员)
         persons = []
         self._person_ptz_index = {}  # 重置索引映射
         
-        for i, det in enumerate(tracked):
+        for i, det in enumerate(valid_persons):
             x_ratio = det.center[0] / frame_size[0]
             y_ratio = det.center[1] / frame_size[1]
             
@@ -1058,7 +1071,7 @@ class AsyncCoordinator(Coordinator):
         batch_id = self._paired_saver.start_new_batch(frame, persons)
         if batch_id:
             self._current_batch_id = batch_id
-            logger.info(f"[配对保存] 创建批次: {batch_id}, 人员={len(persons)}")
+            logger.info(f"[配对保存] 创建批次: {batch_id}, 有效人员={len(persons)}/{len(tracked)}")
 
     def _save_ptz_image_for_person(self, track_id: int, 
                                     ptz_frame: np.ndarray,
@@ -1086,6 +1099,29 @@ class AsyncCoordinator(Coordinator):
             ptz_bbox=getattr(self, '_last_ptz_bbox', None)
         )
 
+    def _save_ptz_image_for_person_batch(self, batch_id: str, person_index: int,
+                                          ptz_frame: np.ndarray,
+                                          ptz_position: Tuple[float, float, int]):
+        """
+        保存球机聚焦图片到指定批次(直接使用 batch_id,不依赖当前批次)
+        
+        Args:
+            batch_id: 批次ID
+            person_index: 人员序号
+            ptz_frame: 球机帧
+            ptz_position: PTZ位置 (pan, tilt, zoom)
+        """
+        if self._paired_saver is None:
+            return
+        
+        self._paired_saver.save_ptz_image(
+            batch_id=batch_id,
+            person_index=person_index,
+            ptz_frame=ptz_frame,
+            ptz_position=ptz_position,
+            ptz_bbox=getattr(self, '_last_ptz_bbox', None)
+        )
+
     def _ptz_worker(self):
         """PTZ控制线程:从队列接收命令并控制球机"""
         while self.running:
@@ -1095,8 +1131,8 @@ class AsyncCoordinator(Coordinator):
                 except queue.Empty:
                     continue
                 
-                # 从命令中提取 track_id 并传递
-                self._execute_ptz_command(cmd, track_id=cmd.track_id)
+                # 执行PTZ命令(batch_id 和 person_index 已在命令中)
+                self._execute_ptz_command(cmd)
                 
             except Exception as e:
                 print(f"PTZ控制线程错误: {e}")
@@ -1181,11 +1217,17 @@ class AsyncCoordinator(Coordinator):
             logger.debug(f"[PTZ] 冷却中,跳过 (间隔={current_time - self._last_ptz_time:.2f}s < {ptz_cooldown}s)")
             return
         
+        # 获取当前批次信息和人员序号
+        batch_id = self._current_batch_id if self._enable_paired_saving else None
+        person_index = self._person_ptz_index.get(target.track_id, -1) if self._enable_paired_saving else -1
+        
         cmd = PTZCommand(
             pan=0, tilt=0, zoom=0,
             x_ratio=x_ratio, y_ratio=y_ratio,
             use_calibration=self.enable_calibration,
-            track_id=target.track_id  # 传递跟踪ID
+            track_id=target.track_id,  # 传递跟踪ID
+            batch_id=batch_id,  # 传递批次ID
+            person_index=person_index  # 传递人员序号
         )
         
         try:
@@ -1195,16 +1237,20 @@ class AsyncCoordinator(Coordinator):
         except queue.Full:
             logger.warning("[PTZ] 命令队列满,丢弃本次命令")
     
-    def _execute_ptz_command(self, cmd: PTZCommand, track_id: int = None):
+    def _execute_ptz_command(self, cmd: PTZCommand):
         """
         执行PTZ命令(在PTZ线程中)
         
         Args:
-            cmd: PTZ命令
-            track_id: 跟踪目标ID(用于配对图片保存)
+            cmd: PTZ命令(包含 batch_id, person_index, track_id 用于配对保存)
         """
         self._last_ptz_time = time.time()
         
+        # 从命令中提取配对保存相关信息
+        track_id = cmd.track_id
+        batch_id = cmd.batch_id
+        person_index = cmd.person_index
+        
         if cmd.use_calibration and self.calibrator and self.calibrator.is_calibrated():
             pan, tilt = self.calibrator.transform(cmd.x_ratio, cmd.y_ratio)
             if self.ptz.ptz_config.get('pan_flip', False):
@@ -1216,7 +1262,8 @@ class AsyncCoordinator(Coordinator):
         self._set_state(TrackingState.TRACKING)
         logger.info(
             f"[PTZ] 执行: pan={pan:.1f}° tilt={tilt:.1f}° zoom={zoom} "
-            f"(全景位置=({cmd.x_ratio:.3f}, {cmd.y_ratio:.3f}))"
+            f"(全景位置=({cmd.x_ratio:.3f}, {cmd.y_ratio:.3f}), "
+            f"batch={batch_id}, person={person_index})"
         )
         
         success = self.ptz.goto_exact_position(pan, tilt, zoom)
@@ -1237,11 +1284,13 @@ class AsyncCoordinator(Coordinator):
             # 获取清晰的球机画面(尝试多次获取最新帧)
             ptz_frame = self._get_clear_ptz_frame()
             
-            # 保存球机图片到配对批次
-            if self._enable_paired_saving and track_id is not None and ptz_frame is not None:
+            # 保存球机图片到配对批次(使用命令中的 batch_id 和 person_index)
+            if self._enable_paired_saving and batch_id is not None and person_index >= 0 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))
+                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, (final_pan, final_tilt, final_zoom))
+            elif self._enable_paired_saving:
+                logger.warning(f"[配对保存] 跳过球机图保存: batch_id={batch_id}, person_index={person_index}, frame={ptz_frame is not None}")
             
             logger.info(f"[PTZ] 到位确认完成: pan={final_pan:.1f}° tilt={final_tilt:.1f}° zoom={final_zoom}")
         else: