Преглед на файлове

refactor(paired_images): 重构配对图片保存管理器,禁用轨迹追踪功能

- 将 PersonTrackingInfo 重命名为 PersonInfo,去除 track_id,改用 person_index 代替序号标识
- 调整人员信息数据结构与批次记录,移除轨迹相关字段说明
- 修改保存全景及球机图方法,使用 person_index 替代 track_id
- 更新批次信息文件内容,使用 person_index 替代原 track_id 字段
- 保留批次目录、清理旧批次、统计信息等核心逻辑不变
- 优化注释及日志,明确说明轨迹追踪功能已禁用
wenhongquan преди 2 дни
родител
ревизия
71972827cc

+ 8 - 8
dual_camera_system/paired_image_saver.py

@@ -17,9 +17,9 @@ logger = logging.getLogger(__name__)
 
 
 @dataclass
-class PersonTrackingInfo:
-    """人员跟踪信息"""
-    track_id: int
+class PersonInfo:
+    """人员信息(轨迹追踪已禁用)"""
+    person_index: int  # 人员序号(0-based)
     position: Tuple[float, float]  # (x_ratio, y_ratio)
     bbox: Tuple[int, int, int, int]  # (x1, y1, x2, y2)
     confidence: float
@@ -36,7 +36,7 @@ class DetectionBatch:
     timestamp: float
     panorama_image: Optional[object] = None  # numpy array
     panorama_path: Optional[str] = None
-    persons: List[PersonTrackingInfo] = field(default_factory=list)
+    persons: List[PersonInfo] = field(default_factory=list)
     total_persons: int = 0
     ptz_images_count: int = 0
     completed: bool = False
@@ -136,11 +136,11 @@ class PairedImageSaver:
                     batch_dir, batch_id, panorama_frame, persons
                 )
             
-            # 创建人员跟踪信息
+            # 创建人员信息(轨迹追踪已禁用,使用序号代替track_id)
             person_infos = []
             for i, p in enumerate(persons):
-                info = PersonTrackingInfo(
-                    track_id=p.get('track_id', i),
+                info = PersonInfo(
+                    person_index=i,
                     position=p.get('position', (0, 0)),
                     bbox=p.get('bbox', (0, 0, 0, 0)),
                     confidence=p.get('confidence', 0.0)
@@ -342,7 +342,7 @@ class PairedImageSaver:
                 
                 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"    Person Index: {person.person_index}\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")

+ 18 - 150
dual_camera_system/safety_coordinator.py

@@ -69,19 +69,12 @@ class SafetyCoordinator:
         self.running = False
         self.worker_thread = None
         
-        # PTZ跟踪线程(独立于检测线程)
-        self._ptz_thread = None
-        self._ptz_queue: queue.Queue = queue.Queue(maxsize=10)
-        self._ptz_cooldown = 0.15
-        self._last_ptz_time = 0.0
-        
-        # 跟踪状态
-        self.tracks = {}
-        self.next_track_id = 1
-        
         self.alert_records: List[AlertRecord] = []
         self.alert_cooldown = {}
         
+        # 告警冷却时间(按违规类型)
+        self._violation_cooldown = {}
+        
         self.stats = {
             'frames_processed': 0,
             'persons_detected': 0,
@@ -183,11 +176,7 @@ class SafetyCoordinator:
         self.worker_thread = threading.Thread(target=self._worker, daemon=True)
         self.worker_thread.start()
         
-        # 启动 PTZ 跟踪线程(如果 PTZ 可用)
-        if self.ptz and SYSTEM_CONFIG.get('enable_ptz_tracking', True):
-            self._ptz_thread = threading.Thread(target=self._ptz_worker, daemon=True)
-            self._ptz_thread.start()
-            print("[SafetyCoordinator] PTZ跟踪线程已启动")
+        # PTZ跟踪已禁用
         
         with self.stats_lock:
             self.stats['start_time'] = time.time()
@@ -202,10 +191,7 @@ class SafetyCoordinator:
         if self.worker_thread:
             self.worker_thread.join(timeout=3)
         
-        # 停止 PTZ 跟踪线程
-        if self._ptz_thread:
-            self._ptz_thread.join(timeout=2)
-            self._ptz_thread = None
+        # PTZ跟踪已禁用
         
         if self.event_pusher:
             self.event_pusher.stop()
@@ -289,7 +275,7 @@ class SafetyCoordinator:
         status_list = self.detector.check_safety(frame, detections)
         
         self._update_stats('persons_detected', len(status_list))
-        self._update_tracks(detections)
+        # 轨迹追踪已禁用
         
         has_violation = False
         for status in status_list:
@@ -326,10 +312,7 @@ class SafetyCoordinator:
         
         self._update_stats('persons_detected', len(status_list))
         
-        # 更新跟踪
-        self._update_tracks(detections)
-        
-        # 检查违规
+        # 检查违规(轨迹追踪已禁用)
         for status in status_list:
             if status.is_violation:
                 self._handle_violation(status, frame)
@@ -338,75 +321,21 @@ class SafetyCoordinator:
         if self.on_frame_processed:
             self.on_frame_processed(frame, detections, status_list)
     
-    def _update_tracks(self, detections: List[SafetyDetection]):
-        """更新跟踪状态"""
-        current_time = time.time()
-        persons = [d for d in detections if d.class_id == 3]  # 人
-        
-        # 匹配现有跟踪
-        used_ids = set()
-        
-        for person in persons:
-            best_id = None
-            min_dist = float('inf')
-            
-            for track_id, track in self.tracks.items():
-                if track_id in used_ids:
-                    continue
-                
-                dist = np.sqrt(
-                    (person.center[0] - track['center'][0])**2 +
-                    (person.center[1] - track['center'][1])**2
-                )
-                
-                if dist < min_dist and dist < 100:  # 距离阈值
-                    min_dist = dist
-                    best_id = track_id
-            
-            if best_id is not None:
-                # 更新现有跟踪
-                self.tracks[best_id]['center'] = person.center
-                self.tracks[best_id]['last_update'] = current_time
-                person.track_id = best_id
-                used_ids.add(best_id)
-            else:
-                # 新跟踪
-                track_id = self.next_track_id
-                self.next_track_id += 1
-                person.track_id = track_id
-                self.tracks[track_id] = {
-                    'center': person.center,
-                    'last_update': current_time,
-                    'alerts': []
-                }
-    
-    def _cleanup_tracks(self):
-        """清理过期跟踪"""
-        current_time = time.time()
-        timeout = COORDINATOR_CONFIG.get('tracking_timeout', 5.0)
-        
-        expired = [
-            tid for tid, t in self.tracks.items()
-            if current_time - t['last_update'] > timeout
-        ]
-        
-        for tid in expired:
-            del self.tracks[tid]
-            self.alert_cooldown.pop(tid, None)
+    # 轨迹追踪已禁用 - _update_tracks 和 _cleanup_tracks 方法已移除
     
     def _handle_violation(self, status: PersonSafetyStatus, frame: np.ndarray):
         """处理违规"""
         current_time = time.time()
-        track_id = status.track_id
         
-        # 检查冷却时间
+        # 检查冷却时间(按违规类型)
+        violation_key = status.get_violation_desc()
         cooldown = SAFETY_DETECTION_CONFIG.get('alert_cooldown', 3.0)
-        if track_id in self.alert_cooldown:
-            if current_time - self.alert_cooldown[track_id] < cooldown:
+        if violation_key in self.alert_cooldown:
+            if current_time - self.alert_cooldown[violation_key] < cooldown:
                 return
         
         # 记录告警
-        self.alert_cooldown[track_id] = current_time
+        self.alert_cooldown[violation_key] = current_time
         
         description = status.get_violation_desc()
         violation_type = status.violation_types[0].value if status.violation_types else "未知"
@@ -421,7 +350,7 @@ class SafetyCoordinator:
         person_image = frame[y1:y2, x1:x2].copy()
         
         record = AlertRecord(
-            track_id=track_id,
+            track_id=0,  # 轨迹追踪已禁用
             violation_type=violation_type,
             description=description,
             frame=person_image,
@@ -431,9 +360,7 @@ class SafetyCoordinator:
         self.alert_records.append(record)
         self._update_stats('violations_detected')
         
-        # PTZ 跟踪违规人员(如果 PTZ 可用且启用)
-        if self.ptz and SYSTEM_CONFIG.get('enable_ptz_tracking', True):
-            self._track_violator_ptz(status, frame)
+        # PTZ跟踪已禁用
         
         # 回调
         if self.on_violation_detected:
@@ -444,7 +371,7 @@ class SafetyCoordinator:
             self.event_pusher.push_safety_violation(
                 description=description,
                 image=person_image,
-                track_id=track_id,
+                track_id=0,  # 轨迹追踪已禁用
                 confidence=status.person_conf
             )
             self._update_stats('events_pushed')
@@ -454,68 +381,9 @@ class SafetyCoordinator:
             self.voice_announcer.announce_violation(description, urgent=True)
             self._update_stats('voice_announced')
         
-        print(f"[告警] {description}, 跟踪ID: {track_id}")
-    
-    def _track_violator_ptz(self, status: PersonSafetyStatus, frame: np.ndarray):
-        """违规人员PTZ跟踪:将违规人员在全景画面中的位置发送给PTZ线程"""
-        if self.ptz is None:
-            return
-        
-        frame_h, frame_w = frame.shape[:2]
-        x1, y1, x2, y2 = status.person_bbox
-        
-        # 计算违规人员在全景画面中的相对位置
-        center_x = (x1 + x2) / 2
-        center_y = (y1 + y2) / 2
-        x_ratio = center_x / frame_w
-        y_ratio = center_y / frame_h
-        
-        # 冷却检查
-        current_time = time.time()
-        if current_time - self._last_ptz_time < self._ptz_cooldown:
-            return
-        
-        # 发送PTZ命令
-        try:
-            self._ptz_queue.put_nowait({
-                'x_ratio': x_ratio,
-                'y_ratio': y_ratio,
-                'track_id': status.track_id,
-                'violation_type': status.violation_types[0].value if status.violation_types else 'unknown'
-            })
-            self._last_ptz_time = current_time
-            self._update_stats('ptz_commands_sent')
-        except queue.Full:
-            pass  # 队列满则丢弃,下一个检测周期会重发
+        print(f"[告警] {description}")
     
-    def _ptz_worker(self):
-        """PTZ控制工作线程:独立处理所有PTZ命令"""
-        while self.running:
-            try:
-                try:
-                    cmd = self._ptz_queue.get(timeout=0.1)
-                except queue.Empty:
-                    continue
-                
-                if self.ptz is None:
-                    continue
-                
-                x_ratio = cmd['x_ratio']
-                y_ratio = cmd['y_ratio']
-                
-                # 使用校准器转换坐标,或使用估算
-                if self.calibrator and self.calibrator.is_calibrated():
-                    pan, tilt = self.calibrator.transform(x_ratio, y_ratio)
-                    zoom = self.ptz.ptz_config.get('default_zoom', 8)
-                    if self.ptz.ptz_config.get('pan_flip', False):
-                        pan = (pan + 180) % 360
-                    self.ptz.goto_exact_position(pan, tilt, zoom)
-                else:
-                    self.ptz.track_target(x_ratio, y_ratio)
-                
-            except Exception as e:
-                print(f"[SafetyCoordinator] PTZ跟踪错误: {e}")
-                time.sleep(0.05)
+    # PTZ跟踪已禁用 - _track_violator_ptz 和 _ptz_worker 方法已移除
     
     def _set_state(self, state: CoordinatorState):
         """设置状态"""

+ 2 - 77
dual_camera_system/safety_detector.py

@@ -613,68 +613,7 @@ class SafetyDetector:
         
         return results
     
-    def detect_with_tracking(self, frame: np.ndarray, 
-                             prev_tracks: Dict[int, Tuple[int, int]] = None,
-                             max_disappeared: int = 30) -> Tuple[List[SafetyDetection], Dict[int, Tuple[int, int]]]:
-        """
-        带跟踪的检测
-        
-        Args:
-            frame: 输入图像
-            prev_tracks: 上一帧的跟踪状态 {track_id: center}
-            max_disappeared: 最大消失帧数
-            
-        Returns:
-            (检测结果列表, 当前跟踪状态)
-        """
-        detections = self.detect(frame)
-        
-        if prev_tracks is None:
-            prev_tracks = {}
-        
-        # 简单的质心跟踪
-        # 这里只对人体进行跟踪
-        persons = [d for d in detections if d.class_id == 3]
-        
-        # 分配跟踪ID
-        if len(persons) > 0:
-            if len(prev_tracks) == 0:
-                # 初始化
-                for i, person in enumerate(persons):
-                    person.track_id = i + 1
-            else:
-                # 匹配
-                used_ids = set()
-                for person in persons:
-                    # 找最近的已跟踪对象
-                    min_dist = float('inf')
-                    best_id = None
-                    
-                    for track_id, center in prev_tracks.items():
-                        if track_id in used_ids:
-                            continue
-                        
-                        dist = np.sqrt((person.center[0] - center[0])**2 + 
-                                      (person.center[1] - center[1])**2)
-                        if dist < min_dist:
-                            min_dist = dist
-                            best_id = track_id
-                    
-                    if best_id is not None and min_dist < 100:  # 距离阈值
-                        person.track_id = best_id
-                        used_ids.add(best_id)
-                    else:
-                        # 新ID
-                        new_id = max(prev_tracks.keys(), default=0) + 1
-                        person.track_id = new_id
-        
-        # 更新跟踪状态
-        new_tracks = {}
-        for person in persons:
-            if person.track_id is not None:
-                new_tracks[person.track_id] = person.center
-        
-        return detections, new_tracks
+    # 轨迹追踪已禁用 - detect_with_tracking 方法已移除
 
 
 def draw_safety_result(frame: np.ndarray, 
@@ -882,18 +821,4 @@ class LLMSafetyDetector:
         
         return self.number_recognizer.recognize_person_number(person_image)
     
-    def detect_with_tracking(self, frame: np.ndarray,
-                             prev_tracks: Dict[int, Tuple[int, int]] = None,
-                             max_disappeared: int = 30) -> Tuple[List[SafetyDetection], Dict[int, Tuple[int, int]]]:
-        """
-        带跟踪的检测
-        
-        Args:
-            frame: 输入图像
-            prev_tracks: 上一帧的跟踪状态
-            max_disappeared: 最大消失帧数
-            
-        Returns:
-            (检测结果列表, 当前跟踪状态)
-        """
-        return self.yolo_detector.detect_with_tracking(frame, prev_tracks, max_disappeared)
+    # 轨迹追踪已禁用 - detect_with_tracking 方法已移除

+ 8 - 53
dual_camera_system/safety_main.py

@@ -340,14 +340,10 @@ class SafetyMonitorSystem:
         detection_interval = 1.0 / detection_fps  # 根据FPS计算间隔
         last_detection_time = 0
         
-        # 告警冷却
+        # 告警冷却(按违规类型)
         alert_cooldown = {}
         cooldown_time = SAFETY_DETECTION_CONFIG.get('alert_cooldown', 3.0)
         
-        # 跟踪状态
-        tracks = {}
-        next_track_id = 1
-        
         while self.running:
             try:
                 current_time = time.time()
@@ -373,59 +369,18 @@ class SafetyMonitorSystem:
                     
                     self._update_stats('persons_detected', len(status_list))
                     
-                    # 更新跟踪
-                    persons = [d for d in detections if d.class_id == 3]
-                    used_ids = set()
-                    
-                    for person in persons:
-                        # 匹配现有跟踪
-                        best_id = None
-                        min_dist = float('inf')
-                        
-                        for tid, track in tracks.items():
-                            if tid in used_ids:
-                                continue
-                            dist = np.sqrt(
-                                (person.center[0] - track['center'][0])**2 +
-                                (person.center[1] - track['center'][1])**2
-                            )
-                            if dist < min_dist and dist < 100:
-                                min_dist = dist
-                                best_id = tid
-                        
-                        if best_id is not None:
-                            tracks[best_id]['center'] = person.center
-                            tracks[best_id]['last_update'] = current_time
-                            person.track_id = best_id
-                            used_ids.add(best_id)
-                        else:
-                            person.track_id = next_track_id
-                            tracks[next_track_id] = {
-                                'center': person.center,
-                                'last_update': current_time
-                            }
-                            next_track_id += 1
-                    
-                    # 清理过期跟踪
-                    expired = [
-                        tid for tid, t in tracks.items()
-                        if current_time - t['last_update'] > 5.0
-                    ]
-                    for tid in expired:
-                        del tracks[tid]
-                        alert_cooldown.pop(tid, None)
+                    # 轨迹追踪已禁用
                     
                     # 处理违规
                     for status in status_list:
                         if status.is_violation:
-                            tid = status.track_id
-                            
-                            # 检查冷却
-                            if tid in alert_cooldown:
-                                if current_time - alert_cooldown[tid] < cooldown_time:
+                            # 检查冷却(按违规类型)
+                            violation_key = status.get_violation_desc()
+                            if violation_key in alert_cooldown:
+                                if current_time - alert_cooldown[violation_key] < cooldown_time:
                                     continue
                             
-                            alert_cooldown[tid] = current_time
+                            alert_cooldown[violation_key] = current_time
                             
                             self._handle_violation(status, frame)
                 
@@ -484,7 +439,7 @@ class SafetyMonitorSystem:
             self.voice_announcer.announce_violation(description, urgent=True)
             self._update_stats('voice_announced')
         
-        logger.warning(f"[违规] {description}, 跟踪ID: {status.track_id}")
+        logger.warning(f"[违规] {description}")
     
     def _display_frame(self, frame: np.ndarray, 
                        detections: List[SafetyDetection],

+ 0 - 345
dual_camera_system/simple_coordinator.py

@@ -1,345 +0,0 @@
-"""
-简化版联动控制器
-核心功能:全景检测到人 → 球机逐个定位抓拍
-"""
-
-import os
-os.environ['OPENCV_FFMPEG_CAPTURE_OPTIONS'] = 'threads;1'
-
-import time
-import threading
-import logging
-from typing import Optional, List, Tuple
-from dataclasses import dataclass
-from datetime import datetime
-from pathlib import Path
-
-import cv2
-import numpy as np
-
-logger = logging.getLogger(__name__)
-
-
-@dataclass
-class PersonTarget:
-    """检测到的人员目标"""
-    index: int                    # 序号(用于标记)
-    position: Tuple[float, float] # 位置比例 (x_ratio, y_ratio)
-    bbox: Tuple[int, int, int, int]  # 边界框 (x, y, w, h)
-    area: int                     # 面积
-    confidence: float             # 置信度
-
-
-class SimpleCoordinator:
-    """
-    简化版联动控制器
-    
-    工作流程:
-    1. 全景摄像头实时检测人体
-    2. 检测到人后,按面积从大到小排序
-    3. 球机逐个定位到每个人,抓拍保存
-    4. 所有人抓拍完成后,回到初始位置
-    """
-    
-    def __init__(self, panorama_camera, ptz_camera, detector, calibrator=None):
-        """
-        初始化
-        
-        Args:
-            panorama_camera: 全景摄像头实例
-            ptz_camera: 球机实例
-            detector: 人体检测器
-            calibrator: 校准器(可选,用于坐标转换)
-        """
-        self.panorama = panorama_camera
-        self.ptz = ptz_camera
-        self.detector = detector
-        self.calibrator = calibrator
-        
-        # 配置
-        self.detection_interval = 1.0      # 检测间隔(秒)
-        self.ptz_move_wait = 0.5           # PTZ移动后等待时间(秒)
-        self.min_person_area = 2000        # 最小人体面积(像素²)
-        self.confidence_threshold = 0.7    # 置信度阈值
-        self.default_zoom = 8              # 默认变倍
-        self.save_dir = Path('/home/admin/dsh/captures')  # 抓拍保存目录
-        self.initial_position = (90, 0, 1) # 初始位置 (pan, tilt, zoom)
-        
-        # 状态
-        self.running = False
-        self.capturing = False  # 是否正在抓拍
-        self.worker_thread = None
-        self.lock = threading.Lock()
-        
-        # 统计
-        self.stats = {
-            'frames_processed': 0,
-            'persons_detected': 0,
-            'captures_taken': 0,
-        }
-        
-        # 创建保存目录
-        self.save_dir.mkdir(parents=True, exist_ok=True)
-    
-    def start(self) -> bool:
-        """启动联动系统"""
-        if self.running:
-            return True
-        
-        # 连接全景摄像头
-        if not self.panorama.connect():
-            logger.error("连接全景摄像头失败")
-            return False
-        
-        # 连接球机
-        if not self.ptz.connect():
-            logger.error("连接球机失败")
-            self.panorama.disconnect()
-            return False
-        
-        # 启动全景视频流
-        if not self.panorama.start_stream_rtsp():
-            logger.error("启动全景视频流失败")
-            self.panorama.disconnect()
-            self.ptz.disconnect()
-            return False
-        
-        # 启动球机视频流(用于抓拍)
-        if not self.ptz.start_stream_rtsp():
-            logger.warning("球机视频流启动失败,抓拍可能失败")
-        
-        # 移动球机到初始位置
-        self.ptz.goto_exact_position(*self.initial_position)
-        
-        # 启动工作线程
-        self.running = True
-        self.worker_thread = threading.Thread(target=self._worker, daemon=True)
-        self.worker_thread.start()
-        
-        logger.info("简化版联动系统已启动")
-        return True
-    
-    def stop(self):
-        """停止联动系统"""
-        self.running = False
-        
-        if self.worker_thread:
-            self.worker_thread.join(timeout=3)
-        
-        self.panorama.disconnect()
-        self.ptz.disconnect()
-        
-        logger.info(f"联动系统已停止,统计: {self.stats}")
-    
-    def _worker(self):
-        """工作线程"""
-        last_detection_time = 0
-        
-        while self.running:
-            try:
-                current_time = time.time()
-                
-                # 获取当前帧
-                frame = self.panorama.get_frame()
-                if frame is None:
-                    time.sleep(0.01)
-                    continue
-                
-                self.stats['frames_processed'] += 1
-                frame_size = (frame.shape[1], frame.shape[0])
-                
-                # 周期性检测
-                if current_time - last_detection_time >= self.detection_interval:
-                    last_detection_time = current_time
-                    
-                    # 检测人体
-                    persons = self._detect_persons(frame)
-                    
-                    if persons:
-                        logger.info(f"[检测] 检测到 {len(persons)} 人")
-                        self.stats['persons_detected'] += len(persons)
-                        
-                        # 保存全景检测图(标记人员)
-                        self._save_panorama_detection(frame, persons)
-                        
-                        # 逐个抓拍
-                        self._capture_all_persons(persons, frame_size)
-                
-                time.sleep(0.01)
-                
-            except Exception as e:
-                logger.error(f"工作线程错误: {e}")
-                time.sleep(0.1)
-    
-    def _detect_persons(self, frame: np.ndarray) -> List[PersonTarget]:
-        """检测人体"""
-        if self.detector is None:
-            return []
-        
-        # 使用检测器
-        detections = self.detector.detect_persons(frame)
-        
-        if not detections:
-            return []
-        
-        frame_h, frame_w = frame.shape[:2]
-        persons = []
-        
-        for i, det in enumerate(detections):
-            # 过滤低置信度
-            if det.confidence < self.confidence_threshold:
-                continue
-            
-            x, y, w, h = det.bbox
-            area = w * h
-            
-            # 过滤小目标
-            if area < self.min_person_area:
-                continue
-            
-            # 计算位置比例
-            center_x = x + w // 2
-            center_y = y + h // 2
-            x_ratio = center_x / frame_w
-            y_ratio = center_y / frame_h
-            
-            persons.append(PersonTarget(
-                index=i,
-                position=(x_ratio, y_ratio),
-                bbox=det.bbox,
-                area=area,
-                confidence=det.confidence
-            ))
-        
-        # 按面积从大到小排序
-        persons.sort(key=lambda p: p.area, reverse=True)
-        
-        # 重新分配序号(按排序后的顺序)
-        for i, p in enumerate(persons):
-            p.index = i
-        
-        return persons
-    
-    def _save_panorama_detection(self, frame: np.ndarray, persons: List[PersonTarget]):
-        """保存全景检测图(标记人员)"""
-        marked_frame = frame.copy()
-        
-        for p in persons:
-            x, y, w, h = p.bbox
-            
-            # 绘制边界框(绿色)
-            cv2.rectangle(marked_frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
-            
-            # 绘制序号标签
-            label = f"person_{p.index}"
-            cv2.putText(
-                marked_frame, label,
-                (x, y - 10),
-                cv2.FONT_HERSHEY_SIMPLEX, 0.8,
-                (0, 255, 0), 2
-            )
-        
-        # 保存
-        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
-        filename = f"panorama_{timestamp}_n{len(persons)}.jpg"
-        filepath = self.save_dir / filename
-        cv2.imwrite(str(filepath), marked_frame)
-        
-        logger.info(f"[全景] 保存检测图: {filepath}")
-    
-    def _capture_all_persons(self, persons: List[PersonTarget], frame_size: Tuple[int, int]):
-        """逐个抓拍所有人员"""
-        with self.lock:
-            if self.capturing:
-                logger.info("正在抓拍中,跳过本次检测")
-                return
-            self.capturing = True
-        
-        try:
-            logger.info(f"[抓拍] 开始逐个抓拍 {len(persons)} 人")
-            
-            # 创建本次抓拍的子目录
-            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
-            batch_dir = self.save_dir / f"batch_{timestamp}"
-            batch_dir.mkdir(parents=True, exist_ok=True)
-            
-            for i, person in enumerate(persons):
-                if not self.running:
-                    break
-                
-                logger.info(f"[抓拍] 正在抓拍 person_{person.index} ({i+1}/{len(persons)})")
-                
-                # 计算PTZ角度
-                pan, tilt, zoom = self._calculate_ptz_position(
-                    person.position[0], person.position[1]
-                )
-                
-                # 移动球机
-                logger.info(f"[PTZ] 移动到: pan={pan:.1f}° tilt={tilt:.1f}° zoom={zoom}")
-                self.ptz.goto_exact_position(pan, tilt, zoom)
-                
-                # 等待画面稳定
-                time.sleep(self.ptz_move_wait)
-                
-                # 抓拍球机画面
-                ptz_frame = self._get_stable_frame()
-                
-                if ptz_frame is not None:
-                    # 保存抓拍图
-                    filename = f"ptz_person_{person.index}.jpg"
-                    filepath = batch_dir / filename
-                    cv2.imwrite(str(filepath), ptz_frame)
-                    
-                    self.stats['captures_taken'] += 1
-                    logger.info(f"[抓拍] 保存球机图: {filepath}")
-                else:
-                    logger.warning(f"[抓拍] 获取球机画面失败: person_{person.index}")
-            
-            # 所有人抓拍完成,回到初始位置
-            logger.info(f"[抓拍] 完成 {len(persons)} 人抓拍,返回初始位置")
-            self.ptz.goto_exact_position(*self.initial_position)
-            
-        finally:
-            self.capturing = False
-    
-    def _calculate_ptz_position(self, x_ratio: float, y_ratio: float) -> Tuple[float, float, int]:
-        """计算PTZ位置"""
-        # 如果有校准器,使用校准结果
-        if self.calibrator and self.calibrator.is_calibrated():
-            pan, tilt = self.calibrator.transform(x_ratio, y_ratio)
-            return (pan, tilt, self.default_zoom)
-        
-        # 否则使用估算
-        # 假设全景视野对应 pan: 0-180°, tilt: -45°~45°
-        pan = x_ratio * 180
-        tilt = -45 + y_ratio * 90  # y_ratio=0.5 对应 tilt=0
-        
-        return (pan, tilt, self.default_zoom)
-    
-    def _get_stable_frame(self, max_attempts: int = 5) -> Optional[np.ndarray]:
-        """获取稳定清晰的帧"""
-        best_frame = None
-        best_score = -1
-        
-        for _ in range(max_attempts):
-            frame = self.ptz.get_frame()
-            if frame is not None:
-                # 评估清晰度
-                gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
-                score = cv2.Laplacian(gray, cv2.CV_64F).var()
-                
-                if score > best_score:
-                    best_score = score
-                    best_frame = frame.copy()
-                
-                # 清晰度足够高就直接返回
-                if score > 100:
-                    return frame
-            
-            time.sleep(0.1)
-        
-        return best_frame
-    
-    def get_stats(self) -> dict:
-        """获取统计信息"""
-        return self.stats.copy()

+ 0 - 254
dual_camera_system/simple_main.py

@@ -1,254 +0,0 @@
-"""
-简化版双摄像头联动抓拍系统
-全景检测到人 → 球机逐个定位抓拍
-
-使用方法:
-    python simple_main.py --panorama-ip 192.168.1.100 --ptz-ip 192.168.1.101
-"""
-
-import os
-os.environ['OPENCV_FFMPEG_CAPTURE_OPTIONS'] = 'threads;1'
-
-import sys
-import argparse
-import logging
-import time
-import signal
-
-# 添加项目路径
-sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
-
-from config import (
-    PANORAMA_CAMERA, PTZ_CAMERA, SDK_PATH,
-    DETECTION_CONFIG, PTZ_CONFIG, LOG_CONFIG
-)
-from dahua_sdk import DahuaSDK
-from panorama_camera import PanoramaCamera, ObjectDetector
-from ptz_camera import PTZCamera
-from simple_coordinator import SimpleCoordinator
-
-# 配置日志
-logging.basicConfig(
-    level=logging.INFO,
-    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
-)
-logger = logging.getLogger(__name__)
-
-
-class SimpleSystem:
-    """简化版双摄像头联动系统"""
-    
-    def __init__(self, config_override: dict = None):
-        self.config = config_override or {}
-        self.sdk = None
-        self.panorama_camera = None
-        self.ptz_camera = None
-        self.detector = None
-        self.coordinator = None
-        self.running = False
-    
-    def initialize(self) -> bool:
-        """初始化系统"""
-        logger.info("=" * 50)
-        logger.info("初始化简化版联动系统")
-        logger.info("=" * 50)
-        
-        # 1. 初始化检测器(必须先于SDK加载)
-        try:
-            self.detector = ObjectDetector(
-                model_path=self.config.get('model_path', DETECTION_CONFIG.get('model_path')),
-                use_gpu=self.config.get('use_gpu', True),
-                model_size=self.config.get('model_size', 'n'),
-                model_type=self.config.get('model_type', 'auto')
-            )
-            logger.info("✓ 检测器初始化成功")
-        except Exception as e:
-            logger.error(f"✗ 检测器初始化失败: {e}")
-            return False
-        
-        # 2. 初始化SDK
-        sdk_path = os.path.join(
-            self.config.get('sdk_path', SDK_PATH['lib_path']),
-            self.config.get('netsdk', SDK_PATH['netsdk'])
-        )
-        try:
-            self.sdk = DahuaSDK(sdk_path)
-            if not self.sdk.init():
-                logger.error("✗ SDK初始化失败")
-                return False
-            logger.info("✓ SDK初始化成功")
-        except Exception as e:
-            logger.error(f"✗ SDK加载失败: {e}")
-            return False
-        
-        # 3. 初始化摄像头
-        panorama_config = self.config.get('panorama_camera', PANORAMA_CAMERA)
-        self.panorama_camera = PanoramaCamera(self.sdk, panorama_config)
-        
-        ptz_config = self.config.get('ptz_camera', PTZ_CAMERA)
-        self.ptz_camera = PTZCamera(self.sdk, ptz_config)
-        
-        # 4. 初始化联动控制器
-        self.coordinator = SimpleCoordinator(
-            self.panorama_camera,
-            self.ptz_camera,
-            self.detector
-        )
-        
-        logger.info("=" * 50)
-        logger.info("系统初始化完成")
-        logger.info("=" * 50)
-        
-        return True
-    
-    def start(self) -> bool:
-        """启动系统"""
-        if self.running:
-            return True
-        
-        if not self.coordinator.start():
-            logger.error("启动联动系统失败")
-            return False
-        
-        self.running = True
-        logger.info("系统已启动")
-        return True
-    
-    def stop(self):
-        """停止系统"""
-        if not self.running:
-            return
-        
-        self.running = False
-        self.coordinator.stop()
-        
-        if self.sdk:
-            self.sdk.cleanup()
-        
-        logger.info("系统已停止")
-    
-    def get_stats(self):
-        """获取统计信息"""
-        if self.coordinator:
-            return self.coordinator.get_stats()
-        return {}
-
-
-def main():
-    """主函数"""
-    parser = argparse.ArgumentParser(
-        description='简化版双摄像头联动抓拍系统',
-        formatter_class=argparse.RawDescriptionHelpFormatter,
-        epilog="""
-示例:
-  python simple_main.py --panorama-ip 192.168.1.100 --ptz-ip 192.168.1.101
-  python simple_main.py --panorama-ip 192.168.1.100 --ptz-ip 192.168.1.101 --model-size m
-        """
-    )
-    
-    parser.add_argument('--panorama-ip', type=str, required=True,
-                        help='全景摄像头IP')
-    parser.add_argument('--ptz-ip', type=str, required=True,
-                        help='球机IP')
-    parser.add_argument('--username', type=str, default='admin',
-                        help='用户名 (默认: admin)')
-    parser.add_argument('--password', type=str, default='admin123',
-                        help='密码 (默认: admin123)')
-    parser.add_argument('--model', type=str,
-                        help='检测模型路径')
-    parser.add_argument('--model-size', type=str, default='n',
-                        choices=['n', 's', 'm', 'l', 'x'],
-                        help='YOLO11模型尺寸 (默认: n)')
-    parser.add_argument('--no-gpu', action='store_true',
-                        help='不使用GPU')
-    parser.add_argument('--detection-interval', type=float, default=1.0,
-                        help='检测间隔秒数 (默认: 1.0)')
-    parser.add_argument('--ptz-wait', type=float, default=0.5,
-                        help='PTZ移动后等待时间 (默认: 0.5)')
-    parser.add_argument('--min-area', type=int, default=2000,
-                        help='最小人体面积像素 (默认: 2000)')
-    parser.add_argument('--confidence', type=float, default=0.7,
-                        help='置信度阈值 (默认: 0.7)')
-    parser.add_argument('--zoom', type=int, default=8,
-                        help='默认变倍 (默认: 8)')
-    
-    args = parser.parse_args()
-    
-    # 构建配置
-    config = {
-        'panorama_camera': {
-            **PANORAMA_CAMERA,
-            'ip': args.panorama_ip,
-            'username': args.username,
-            'password': args.password,
-        },
-        'ptz_camera': {
-            **PTZ_CAMERA,
-            'ip': args.ptz_ip,
-            'username': args.username,
-            'password': args.password,
-        },
-        'model_path': args.model,
-        'model_size': args.model_size,
-        'use_gpu': not args.no_gpu,
-    }
-    
-    # 创建系统
-    system = SimpleSystem(config)
-    
-    # 设置信号处理
-    def signal_handler(sig, frame):
-        logger.info("\n接收到停止信号,正在退出...")
-        system.stop()
-        sys.exit(0)
-    
-    signal.signal(signal.SIGINT, signal_handler)
-    signal.signal(signal.SIGTERM, signal_handler)
-    
-    try:
-        # 初始化
-        if not system.initialize():
-            logger.error("系统初始化失败!")
-            return 1
-        
-        # 启动
-        if not system.start():
-            logger.error("系统启动失败!")
-            return 1
-        
-        # 打印运行状态
-        print("\n" + "=" * 50)
-        print("系统运行中,按 Ctrl+C 停止")
-        print("=" * 50)
-        print(f"全景摄像头: {args.panorama_ip}")
-        print(f"球机: {args.ptz_ip}")
-        print(f"检测间隔: {args.detection_interval}秒")
-        print(f"置信度阈值: {args.confidence}")
-        print(f"最小人体面积: {args.min_area}像素²")
-        print(f"默认变倍: {args.zoom}")
-        print("=" * 50 + "\n")
-        
-        # 运行循环
-        last_stats_time = time.time()
-        while system.running:
-            time.sleep(1)
-            
-            # 每30秒打印统计
-            if time.time() - last_stats_time >= 30:
-                stats = system.get_stats()
-                logger.info(f"[统计] 帧数={stats.get('frames_processed', 0)} "
-                           f"检测人数={stats.get('persons_detected', 0)} "
-                           f"抓拍次数={stats.get('captures_taken', 0)}")
-                last_stats_time = time.time()
-    
-    except KeyboardInterrupt:
-        pass
-    
-    finally:
-        system.stop()
-    
-    return 0
-
-
-if __name__ == '__main__':
-    sys.exit(main() or 0)