Explorar el Código

refactor(coordinator): 重构并优化联动控制器代码结构

- 重写联动控制器模块,优化类和函数结构,提升代码可维护性
- 重新组织目标选择策略,简化目标筛选与得分计算逻辑
- 调整多线程处理流程,实现异步检测与PTZ控制分离
- 更新状态管理及统计机制,增强性能监控和日志记录
- 优化PTZ命令发送与位置确认,减少误动作并支持自动对焦
- 精简导入与依赖,改进模块间耦合设计
- 兼容并改进原有功能,包括事件驱动及异步联动支持
wenhongquan hace 3 días
padre
commit
5ce17b6eb2

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


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


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


+ 5 - 0
dual_camera_system/config/detection.py

@@ -13,6 +13,11 @@ DETECTION_CONFIG = {
     'save_detection_image': True,   # 是否保存检测到人的图片
     'detection_image_dir': '/home/admin/dsh/detection_images',  # 图片保存目录
     
+    # 配对图片保存配置(全景+球机图片归入同一目录)
+    'enable_paired_saving': True,   # 是否启用配对图片保存
+    'paired_image_dir': '/home/admin/dsh/paired_images',  # 配对图片保存目录
+    'paired_time_window': 5.0,      # 批次时间窗口(秒),同一窗口内的检测归为一批
+    
     # RK3588 平台使用 RKNN 安全检测模型 (包含人体检测)
     # 类别映射: 0=安全帽, 3=人, 4=反光衣
     'model_path': '/home/admin/dsh/testrk3588/yolo11m_safety.rknn',

+ 116 - 9
dual_camera_system/coordinator.py

@@ -18,6 +18,7 @@ from panorama_camera import PanoramaCamera, ObjectDetector, PersonTracker, Detec
 from ptz_camera import PTZCamera, PTZController
 from ocr_recognizer import NumberDetector, PersonInfo
 from ptz_person_tracker import PTZPersonDetector, PTZAutoZoomController
+from paired_image_saver import PairedImageSaver, get_paired_saver
 
 logger = logging.getLogger(__name__)
 
@@ -742,6 +743,7 @@ class PTZCommand:
     x_ratio: float = 0.0
     y_ratio: float = 0.0
     use_calibration: bool = True
+    track_id: Optional[int] = None  # 跟踪目标ID(用于配对图片保存)
 
 
 class AsyncCoordinator(Coordinator):
@@ -774,6 +776,18 @@ class AsyncCoordinator(Coordinator):
         
         # 上次PTZ命令时间
         self._last_ptz_time = 0.0
+        
+        # 配对图片保存器
+        self._enable_paired_saving = DETECTION_CONFIG.get('enable_paired_saving', False)
+        self._paired_saver: Optional[PairedImageSaver] = None
+        self._current_batch_id: Optional[str] = None
+        self._person_ptz_index: Dict[int, int] = {}  # track_id -> person_index
+        
+        if self._enable_paired_saving:
+            save_dir = DETECTION_CONFIG.get('paired_image_dir', '/home/admin/dsh/paired_images')
+            time_window = DETECTION_CONFIG.get('paired_time_window', 5.0)
+            self._paired_saver = get_paired_saver(base_dir=save_dir, time_window=time_window)
+            logger.info(f"[AsyncCoordinator] 配对图片保存已启用: 目录={save_dir}, 时间窗口={time_window}s")
     
     def start(self) -> bool:
         """启动联动(覆盖父类,启动双线程)"""
@@ -843,6 +857,11 @@ class AsyncCoordinator(Coordinator):
         if self.coordinator_thread:
             self.coordinator_thread.join(timeout=1)
         
+        # 关闭配对保存器
+        if self._paired_saver is not None:
+            self._paired_saver.close()
+            self._paired_saver = None
+        
         self.panorama.disconnect()
         if self.enable_ptz_camera:
             self.ptz.disconnect()
@@ -926,6 +945,10 @@ class AsyncCoordinator(Coordinator):
                     tracked = self.tracker.update(detections)
                     self._update_tracking_targets(tracked, frame_size)
                     
+                    # 配对图片保存:创建新批次
+                    if tracked and self._enable_paired_saving and self._paired_saver is not None:
+                        self._create_detection_batch(frame, tracked, frame_size)
+                    
                     # 打印检测日志
                     if tracked:
                         for t in tracked:
@@ -997,6 +1020,70 @@ class AsyncCoordinator(Coordinator):
             print(f"[AsyncCoordinator] 球机端检测器初始化失败: {e}")
             self.enable_ptz_detection = False
 
+    def _create_detection_batch(self, frame: np.ndarray, 
+                                 tracked: List[DetectedObject],
+                                 frame_size: Tuple[int, int]):
+        """
+        创建检测批次,用于配对图片保存
+        
+        Args:
+            frame: 全景帧
+            tracked: 跟踪到的人员列表
+            frame_size: 帧尺寸
+        """
+        if self._paired_saver is None:
+            return
+        
+        # 构建人员信息列表
+        persons = []
+        self._person_ptz_index = {}  # 重置索引映射
+        
+        for i, det in enumerate(tracked):
+            x_ratio = det.center[0] / frame_size[0]
+            y_ratio = det.center[1] / frame_size[1]
+            
+            person_info = {
+                'track_id': det.track_id,
+                'position': (x_ratio, y_ratio),
+                'bbox': (det.bbox[0], det.bbox[1], 
+                        det.bbox[0] + det.bbox[2], 
+                        det.bbox[1] + det.bbox[3]),
+                'confidence': det.confidence
+            }
+            persons.append(person_info)
+            self._person_ptz_index[det.track_id] = i
+        
+        # 创建新批次
+        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)}")
+
+    def _save_ptz_image_for_person(self, track_id: int, 
+                                    ptz_frame: np.ndarray,
+                                    ptz_position: Tuple[float, float, int]):
+        """
+        保存球机聚焦图片到对应批次
+        
+        Args:
+            track_id: 人员跟踪ID
+            ptz_frame: 球机帧
+            ptz_position: PTZ位置 (pan, tilt, zoom)
+        """
+        if (self._paired_saver is None or 
+            self._current_batch_id is None or
+            track_id not in self._person_ptz_index):
+            return
+        
+        person_index = self._person_ptz_index[track_id]
+        
+        self._paired_saver.save_ptz_image(
+            batch_id=self._current_batch_id,
+            person_index=person_index,
+            ptz_frame=ptz_frame,
+            ptz_position=ptz_position
+        )
+
     def _ptz_worker(self):
         """PTZ控制线程:从队列接收命令并控制球机"""
         while self.running:
@@ -1006,7 +1093,8 @@ class AsyncCoordinator(Coordinator):
                 except queue.Empty:
                     continue
                 
-                self._execute_ptz_command(cmd)
+                # 从命令中提取 track_id 并传递
+                self._execute_ptz_command(cmd, track_id=cmd.track_id)
                 
             except Exception as e:
                 print(f"PTZ控制线程错误: {e}")
@@ -1094,7 +1182,8 @@ class AsyncCoordinator(Coordinator):
         cmd = PTZCommand(
             pan=0, tilt=0, zoom=0,
             x_ratio=x_ratio, y_ratio=y_ratio,
-            use_calibration=self.enable_calibration
+            use_calibration=self.enable_calibration,
+            track_id=target.track_id  # 传递跟踪ID
         )
         
         try:
@@ -1104,8 +1193,14 @@ class AsyncCoordinator(Coordinator):
         except queue.Full:
             logger.warning("[PTZ] 命令队列满,丢弃本次命令")
     
-    def _execute_ptz_command(self, cmd: PTZCommand):
-        """执行PTZ命令(在PTZ线程中)"""
+    def _execute_ptz_command(self, cmd: PTZCommand, track_id: int = None):
+        """
+        执行PTZ命令(在PTZ线程中)
+        
+        Args:
+            cmd: PTZ命令
+            track_id: 跟踪目标ID(用于配对图片保存)
+        """
         self._last_ptz_time = time.time()
         
         if cmd.use_calibration and self.calibrator and self.calibrator.is_calibrated():
@@ -1129,21 +1224,30 @@ class AsyncCoordinator(Coordinator):
             
             # 球机端人体检测与自动对焦
             if self.enable_ptz_detection and self.auto_zoom_config.get('enabled', False):
-                self._auto_zoom_person(pan, tilt, zoom)
-            else:
-                self._confirm_ptz_position(cmd.x_ratio, cmd.y_ratio)
+                final_zoom = self._auto_zoom_person(pan, tilt, zoom)
+                if final_zoom != zoom:
+                    zoom = final_zoom
+            
+            # 保存球机图片到配对批次
+            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))
             
             logger.info(f"[PTZ] 到位确认完成: pan={pan:.1f}° tilt={tilt:.1f}°")
         else:
             logger.warning(f"[PTZ] 命令执行失败: pan={pan:.1f}° tilt={tilt:.1f}° zoom={zoom}")
     
-    def _auto_zoom_person(self, initial_pan: float, initial_tilt: float, initial_zoom: int):
+    def _auto_zoom_person(self, initial_pan: float, initial_tilt: float, initial_zoom: int) -> int:
         """
         自动对焦人体
         在球机画面中检测人体,自动调整zoom使人体居中且大小合适
+        
+        Returns:
+            最终的 zoom 值
         """
         if self.auto_zoom_controller is None:
-            return
+            return initial_zoom
         
         logger.info("[AutoZoom] 开始自动对焦...")
         
@@ -1155,10 +1259,13 @@ class AsyncCoordinator(Coordinator):
             
             if success:
                 logger.info(f"[AutoZoom] 自动对焦成功: zoom={final_zoom}")
+                return final_zoom
             else:
                 logger.warning("[AutoZoom] 自动对焦未能定位人体")
+                return initial_zoom
         except Exception as e:
             logger.error(f"[AutoZoom] 自动对焦异常: {e}")
+            return initial_zoom
     
     def _confirm_ptz_position(self, x_ratio: float, y_ratio: float):
         """PTZ位置确认:读取球机帧验证目标是否可见"""

+ 416 - 0
dual_camera_system/paired_image_saver.py

@@ -0,0 +1,416 @@
+"""
+配对图片保存管理器
+将全景检测图片和对应的球机聚焦图片保存到同一目录
+"""
+
+import os
+import cv2
+import time
+import logging
+import threading
+from pathlib import Path
+from datetime import datetime
+from typing import Optional, List, Dict, Tuple
+from dataclasses import dataclass, field
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass
+class PersonTrackingInfo:
+    """人员跟踪信息"""
+    track_id: int
+    position: Tuple[float, float]  # (x_ratio, y_ratio)
+    bbox: Tuple[int, int, int, int]  # (x1, y1, x2, y2)
+    confidence: float
+    ptz_position: Optional[Tuple[float, float, int]] = None  # (pan, tilt, zoom)
+    ptz_image_saved: bool = False
+    ptz_image_path: Optional[str] = None
+
+
+@dataclass
+class DetectionBatch:
+    """一批检测记录"""
+    batch_id: str
+    timestamp: float
+    panorama_image: Optional[object] = None  # numpy array
+    panorama_path: Optional[str] = None
+    persons: List[PersonTrackingInfo] = field(default_factory=list)
+    total_persons: int = 0
+    ptz_images_count: int = 0
+    completed: bool = False
+
+
+class PairedImageSaver:
+    """
+    配对图片保存管理器
+    
+    功能:
+    1. 为每次全景检测创建批次目录
+    2. 保存全景标记图到批次目录
+    3. 为每个人员保存对应的球机聚焦图到同一目录
+    4. 支持时间窗口内的批量保存
+    """
+    
+    def __init__(self, base_dir: str = '/home/admin/dsh/paired_images',
+                 time_window: float = 5.0,  # 时间窗口(秒)
+                 max_batches: int = 100):
+        """
+        初始化
+        
+        Args:
+            base_dir: 基础保存目录
+            time_window: 批次时间窗口(秒),同一窗口内的检测归为一批
+            max_batches: 最大保留批次数量
+        """
+        self.base_dir = Path(base_dir)
+        self.time_window = time_window
+        self.max_batches = max_batches
+        
+        self._current_batch: Optional[DetectionBatch] = None
+        self._batch_lock = threading.Lock()
+        self._last_batch_time = 0.0
+        
+        # 统计信息
+        self._stats = {
+            'total_batches': 0,
+            'total_persons': 0,
+            'total_ptz_images': 0
+        }
+        self._stats_lock = threading.Lock()
+        
+        # 确保目录存在
+        self._ensure_base_dir()
+        
+        logger.info(f"[配对保存] 初始化完成: 目录={base_dir}, 时间窗口={time_window}s")
+    
+    def _ensure_base_dir(self):
+        """确保基础目录存在"""
+        try:
+            self.base_dir.mkdir(parents=True, exist_ok=True)
+        except Exception as e:
+            logger.error(f"[配对保存] 创建目录失败: {e}")
+    
+    def _generate_batch_id(self) -> str:
+        """生成批次ID"""
+        return datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3]
+    
+    def _create_batch_dir(self, batch_id: str) -> Path:
+        """创建批次目录"""
+        batch_dir = self.base_dir / f"batch_{batch_id}"
+        try:
+            batch_dir.mkdir(parents=True, exist_ok=True)
+            return batch_dir
+        except Exception as e:
+            logger.error(f"[配对保存] 创建批次目录失败: {e}")
+            return self.base_dir
+    
+    def start_new_batch(self, panorama_frame, persons: List[Dict]) -> Optional[str]:
+        """
+        开始新批次
+        
+        Args:
+            panorama_frame: 全景帧图像
+            persons: 人员列表,每项包含 track_id, position, bbox, confidence
+            
+        Returns:
+            batch_id: 批次ID,失败返回 None
+        """
+        with self._batch_lock:
+            current_time = time.time()
+            
+            # 检查是否需要创建新批次(时间窗口已过)
+            if (self._current_batch is not None and 
+                current_time - self._last_batch_time < self.time_window):
+                # 仍在当前时间窗口内,复用当前批次
+                return self._current_batch.batch_id
+            
+            # 完成上一批次
+            if self._current_batch is not None:
+                self._finalize_batch(self._current_batch)
+            
+            # 创建新批次
+            batch_id = self._generate_batch_id()
+            batch_dir = self._create_batch_dir(batch_id)
+            
+            # 保存全景图片
+            panorama_path = None
+            if panorama_frame is not None:
+                panorama_path = self._save_panorama_image(
+                    batch_dir, batch_id, panorama_frame, persons
+                )
+            
+            # 创建人员跟踪信息
+            person_infos = []
+            for i, p in enumerate(persons):
+                info = PersonTrackingInfo(
+                    track_id=p.get('track_id', i),
+                    position=p.get('position', (0, 0)),
+                    bbox=p.get('bbox', (0, 0, 0, 0)),
+                    confidence=p.get('confidence', 0.0)
+                )
+                person_infos.append(info)
+            
+            # 创建批次记录
+            self._current_batch = DetectionBatch(
+                batch_id=batch_id,
+                timestamp=current_time,
+                panorama_image=panorama_frame,
+                panorama_path=panorama_path,
+                persons=person_infos,
+                total_persons=len(persons)
+            )
+            
+            self._last_batch_time = current_time
+            
+            with self._stats_lock:
+                self._stats['total_batches'] += 1
+                self._stats['total_persons'] += len(persons)
+            
+            logger.info(
+                f"[配对保存] 新批次创建: {batch_id}, "
+                f"人员={len(persons)}, 目录={batch_dir}"
+            )
+            
+            return batch_id
+    
+    def _save_panorama_image(self, batch_dir: Path, batch_id: str,
+                              frame, persons: List[Dict]) -> Optional[str]:
+        """
+        保存全景标记图片
+        
+        Args:
+            batch_dir: 批次目录
+            batch_id: 批次ID
+            frame: 全景帧
+            persons: 人员列表
+            
+        Returns:
+            保存路径或 None
+        """
+        try:
+            # 复制图像避免修改原图
+            marked_frame = frame.copy()
+            
+            # 绘制每个人员的标记
+            for i, person in enumerate(persons):
+                bbox = person.get('bbox', (0, 0, 0, 0))
+                x1, y1, x2, y2 = bbox
+                
+                # 绘制边界框(绿色)
+                cv2.rectangle(marked_frame, (x1, y1), (x2, y2), (0, 255, 0), 2)
+                
+                # 绘制序号标签
+                label = f"person_{i}"
+                (label_w, label_h), baseline = cv2.getTextSize(
+                    label, cv2.FONT_HERSHEY_SIMPLEX, 0.8, 2
+                )
+                
+                # 标签背景
+                cv2.rectangle(
+                    marked_frame,
+                    (x1, y1 - label_h - 8),
+                    (x1 + label_w, y1),
+                    (0, 255, 0),
+                    -1
+                )
+                
+                # 标签文字(黑色)
+                cv2.putText(
+                    marked_frame, label,
+                    (x1, y1 - 4),
+                    cv2.FONT_HERSHEY_SIMPLEX, 0.8,
+                    (0, 0, 0), 2
+                )
+            
+            # 保存图片
+            filename = f"00_panorama_n{len(persons)}.jpg"
+            filepath = batch_dir / filename
+            cv2.imwrite(str(filepath), marked_frame, [cv2.IMWRITE_JPEG_QUALITY, 90])
+            
+            logger.info(f"[配对保存] 全景图已保存: {filepath}")
+            return str(filepath)
+            
+        except Exception as e:
+            logger.error(f"[配对保存] 保存全景图失败: {e}")
+            return None
+    
+    def save_ptz_image(self, batch_id: str, person_index: int,
+                       ptz_frame, ptz_position: Tuple[float, float, int],
+                       person_info: Dict = None) -> Optional[str]:
+        """
+        保存球机聚焦图片
+        
+        Args:
+            batch_id: 批次ID
+            person_index: 人员序号(0-based)
+            ptz_frame: 球机帧
+            ptz_position: PTZ位置 (pan, tilt, zoom)
+            person_info: 额外人员信息
+            
+        Returns:
+            保存路径或 None
+        """
+        with self._batch_lock:
+            if self._current_batch is None or self._current_batch.batch_id != batch_id:
+                logger.warning(f"[配对保存] 批次不存在或已过期: {batch_id}")
+                return None
+            
+            batch_dir = self.base_dir / f"batch_{batch_id}"
+            
+            try:
+                # 复制图像
+                marked_frame = ptz_frame.copy() if ptz_frame is not None else None
+                
+                if marked_frame is not None:
+                    # 在球机图上添加标记(如果有检测框)
+                    h, w = marked_frame.shape[:2]
+                    
+                    # 添加PTZ位置信息到图片
+                    pan, tilt, zoom = ptz_position
+                    info_text = f"PTZ: P={pan:.1f} T={tilt:.1f} Z={zoom}"
+                    cv2.putText(
+                        marked_frame, info_text,
+                        (10, 30),
+                        cv2.FONT_HERSHEY_SIMPLEX, 0.7,
+                        (0, 255, 0), 2
+                    )
+                    
+                    # 添加人员序号
+                    person_text = f"person_{person_index}"
+                    cv2.putText(
+                        marked_frame, person_text,
+                        (10, 60),
+                        cv2.FONT_HERSHEY_SIMPLEX, 0.7,
+                        (0, 255, 0), 2
+                    )
+                
+                # 保存图片
+                filename = f"01_ptz_person{person_index}_p{int(ptz_position[0])}_t{int(ptz_position[1])}_z{int(ptz_position[2])}.jpg"
+                filepath = batch_dir / filename
+                
+                if marked_frame is not None:
+                    cv2.imwrite(str(filepath), marked_frame, [cv2.IMWRITE_JPEG_QUALITY, 90])
+                
+                # 更新批次信息
+                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_image_saved = True
+                    self._current_batch.persons[person_index].ptz_image_path = str(filepath)
+                
+                self._current_batch.ptz_images_count += 1
+                
+                with self._stats_lock:
+                    self._stats['total_ptz_images'] += 1
+                
+                logger.info(f"[配对保存] 球机图已保存: {filepath}")
+                return str(filepath)
+                
+            except Exception as e:
+                logger.error(f"[配对保存] 保存球机图失败: {e}")
+                return None
+    
+    def _finalize_batch(self, batch: DetectionBatch):
+        """完成批次处理"""
+        batch.completed = True
+        
+        # 创建批次信息文件
+        try:
+            batch_dir = self.base_dir / f"batch_{batch.batch_id}"
+            info_path = batch_dir / "batch_info.txt"
+            
+            with open(info_path, 'w', encoding='utf-8') as f:
+                f.write(f"批次ID: {batch.batch_id}\n")
+                f.write(f"时间戳: {datetime.fromtimestamp(batch.timestamp)}\n")
+                f.write(f"总人数: {batch.total_persons}\n")
+                f.write(f"球机图数量: {batch.ptz_images_count}\n")
+                f.write(f"全景图: {batch.panorama_path}\n")
+                f.write("\n人员详情:\n")
+                
+                for i, person in enumerate(batch.persons):
+                    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"    Confidence: {person.confidence:.2f}\n")
+                    f.write(f"    PTZ Position: {person.ptz_position}\n")
+                    f.write(f"    PTZ Image: {person.ptz_image_path}\n")
+            
+            logger.info(f"[配对保存] 批次完成: {batch.batch_id}, "
+                       f"人员={batch.total_persons}, 球机图={batch.ptz_images_count}")
+            
+        except Exception as e:
+            logger.error(f"[配对保存] 保存批次信息失败: {e}")
+        
+        # 清理旧批次
+        self._cleanup_old_batches()
+    
+    def _cleanup_old_batches(self):
+        """清理旧批次目录"""
+        try:
+            batch_dirs = sorted(
+                [d for d in self.base_dir.iterdir() if d.is_dir() and d.name.startswith('batch_')],
+                key=lambda x: x.stat().st_mtime
+            )
+            
+            if len(batch_dirs) > self.max_batches:
+                to_delete = batch_dirs[:len(batch_dirs) - self.max_batches]
+                for d in to_delete:
+                    import shutil
+                    shutil.rmtree(d)
+                    logger.info(f"[配对保存] 清理旧批次: {d.name}")
+                    
+        except Exception as e:
+            logger.error(f"[配对保存] 清理旧批次失败: {e}")
+    
+    def get_current_batch_id(self) -> Optional[str]:
+        """获取当前批次ID"""
+        with self._batch_lock:
+            return self._current_batch.batch_id if self._current_batch else None
+    
+    def get_stats(self) -> Dict:
+        """获取统计信息"""
+        with self._stats_lock:
+            return self._stats.copy()
+    
+    def close(self):
+        """关闭管理器,完成当前批次"""
+        with self._batch_lock:
+            if self._current_batch is not None:
+                self._finalize_batch(self._current_batch)
+                self._current_batch = None
+        
+        logger.info("[配对保存] 管理器已关闭")
+
+
+# 全局单例实例
+_paired_saver_instance: Optional[PairedImageSaver] = None
+
+
+def get_paired_saver(base_dir: str = None, time_window: float = 5.0) -> PairedImageSaver:
+    """
+    获取配对保存管理器实例(单例模式)
+    
+    Args:
+        base_dir: 基础保存目录
+        time_window: 时间窗口
+        
+    Returns:
+        PairedImageSaver 实例
+    """
+    global _paired_saver_instance
+    
+    if _paired_saver_instance is None:
+        _paired_saver_instance = PairedImageSaver(
+            base_dir=base_dir or '/home/admin/dsh/paired_images',
+            time_window=time_window
+        )
+    
+    return _paired_saver_instance
+
+
+def reset_paired_saver():
+    """重置单例实例(用于测试)"""
+    global _paired_saver_instance
+    if _paired_saver_instance is not None:
+        _paired_saver_instance.close()
+    _paired_saver_instance = None

+ 21 - 18
dual_camera_system/panorama_camera.py

@@ -540,7 +540,7 @@ class ObjectDetector:
     
     def _save_detection_image(self, frame: np.ndarray, detections: List[DetectedObject]):
         """
-        保存带有检测标记的图片(只对超过置信度阈值的人编号
+        保存带有检测标记的图片(只标记达到置信度阈值的人
         Args:
             frame: 原始图像
             detections: 检测结果列表
@@ -559,30 +559,28 @@ class ObjectDetector:
             
             # 置信度阈值(人员检测用更高阈值)
             person_threshold = self.config.get('person_threshold', 0.8)
-            conf_threshold = self.config.get('confidence_threshold', 0.5)
             
-            # 只对超过置信度阈值的人编号
+            # 只标记达到阈值的人
             person_count = 0
             
             for det in detections:
-                x, y, w, h = det.bbox
+                # 只处理人且达到阈值
+                is_person = det.class_name in ['person']
+                if not is_person:
+                    continue
                 
-                is_person = det.class_name in ['person', '人']
-                box_color = (0, 255, 0) if is_person else (255, 165, 0)  # 绿色/橙色
+                # 未达阈值的不标记
+                if det.confidence < person_threshold:
+                    continue
                 
-                # 绘制边界框
-                cv2.rectangle(marked_frame, (x, y), (x + w, y + h), box_color, 2)
+                x, y, w, h = det.bbox
                 
-                # 标签:人且超过阈值才编号,其他显示类别名
-                if is_person:
-                    if det.confidence >= person_threshold:
-                        label = f"person_{person_count}"
-                        person_count += 1
-                    else:
-                        label = f"person({det.confidence:.2f})"  # 未达阈值,显示置信度
-                else:
-                    label = det.class_name
+                # 绘制边界框(绿色)
+                cv2.rectangle(marked_frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
                 
+                # 绘制序号标签
+                label = f"person_{person_count}"
+                person_count += 1
                 
                 (label_w, label_h), baseline = cv2.getTextSize(
                     label, cv2.FONT_HERSHEY_SIMPLEX, 0.8, 2
@@ -591,7 +589,7 @@ class ObjectDetector:
                     marked_frame, 
                     (x, y - label_h - 8),
                     (x + label_w, y),
-                    box_color, 
+                    (0, 255, 0), 
                     -1
                 )
                 
@@ -604,6 +602,11 @@ class ObjectDetector:
                 )
             
             
+            # 无有效目标则不保存
+            if person_count == 0:
+                return
+            
+            
             # 生成文件名(时间戳+有效人数)
             timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
             filename = f"panorama_{timestamp}_n{person_count}.jpg"

+ 15 - 9
dual_camera_system/ptz_person_tracker.py

@@ -283,7 +283,7 @@ class PTZPersonDetector:
     
     def _save_detection_image(self, frame: np.ndarray, persons: List[DetectedPerson]):
         """
-        保存带有检测标记的图片(只对超过置信度阈值的人编号
+        保存带有检测标记的图片(只标记达到置信度阈值的人
         Args:
             frame: 原始图像
             persons: 检测到的人体列表
@@ -300,22 +300,23 @@ class PTZPersonDetector:
             # 复制图像避免修改原图
             marked_frame = frame.copy()
             
-            # 只对超过置信度阈值的人编号
+            # 只标记达到阈值的人
             person_count = 0
             
             for person in persons:
+                # 未达阈值的不标记
+                if person.confidence < self.confidence_threshold:
+                    continue
+                
+                
                 x1, y1, x2, y2 = person.bbox
                 
                 # 绘制边界框(绿色)
                 cv2.rectangle(marked_frame, (x1, y1), (x2, y2), (0, 255, 0), 2)
                 
-                # 标签:超过阈值才编号
-                if person.confidence >= self.confidence_threshold:
-                    label = f"person_{person_count}"
-                    person_count += 1
-                else:
-                    label = f"person({person.confidence:.2f})"  # 未达阈值,显示置信度
-                
+                # 绘制序号标签
+                label = f"person_{person_count}"
+                person_count += 1
                 
                 (label_w, label_h), baseline = cv2.getTextSize(
                     label, cv2.FONT_HERSHEY_SIMPLEX, 0.8, 2
@@ -337,6 +338,11 @@ class PTZPersonDetector:
                 )
             
             
+            # 无有效目标则不保存
+            if person_count == 0:
+                return
+            
+            
             # 生成文件名(时间戳+有效人数)
             timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
             filename = f"ptz_{timestamp}_n{person_count}.jpg"