Forráskód Böngészése

feat: 增加PTZ方向翻转功能并优化全景扫描范围

扩展PTZ配置支持方向翻转,用于球机与全景相机朝向相反的场景
将全景扫描范围从180度扩展到360度
添加全局视频捕获锁防止FFmpeg并发读取崩溃
优化校准流程,自动选择最佳重叠区间
wenhongquan 4 napja
szülő
commit
e6a5b370c5

+ 12 - 3
dual_camera_system/calibration.py

@@ -122,7 +122,7 @@ class OverlapDiscovery:
         ptz: PTZCamera,
         get_panorama_frame: Callable[[], np.ndarray],
         ptz_capture: Callable[[], Optional[np.ndarray]],
-        pan_range: Tuple[float, float] = (0, 180),
+        pan_range: Tuple[float, float] = (0, 360),
         tilt_range: Tuple[float, float] = (-30, 30),
         pan_step: float = 20,
         tilt_step: float = 15,
@@ -479,7 +479,7 @@ class CameraCalibrator:
         self.use_feature_matching = True
 
         # 重叠发现配置
-        self.overlap_pan_range = (0, 180)
+        self.overlap_pan_range = (0, 360)
         self.overlap_tilt_range = (-30, 30)
         self.overlap_pan_step = 20
         self.overlap_tilt_step = 15
@@ -533,7 +533,16 @@ class CameraCalibrator:
                 self.on_complete(self.result)
             return self.result
 
-        print(f"\n发现 {len(self.overlap_ranges)} 个重叠区间,进入阶段2校准")
+        print(f"\n发现 {len(self.overlap_ranges)} 个重叠区间")
+
+        # 选择匹配点最多的区间用于校准
+        best_range = max(self.overlap_ranges, key=lambda r: r.match_count)
+        self.overlap_ranges = [best_range]
+
+        print(f"选择最佳重叠区间: pan=[{best_range.pan_start:.0f}°, {best_range.pan_end:.0f}°], "
+              f"tilt=[{best_range.tilt_start:.0f}°, {best_range.tilt_end:.0f}°], "
+              f"匹配点={best_range.match_count}")
+        print(f"进入阶段2校准")
 
         # ===================== 阶段2: 在重叠区内精确校准 =====================
         print(f"\n{'='*60}")

+ 1 - 1
dual_camera_system/config/coordinator.py

@@ -17,7 +17,7 @@ CALIBRATION_CONFIG = {
     'min_valid_points': 4,            # 最少有效校准点数
     'rms_error_threshold': 5.0,       # RMS误差阈值(度)
     'overlap_discovery': {
-        'pan_range': (0, 180),        # 球机水平扫描范围(度) - 改为180,全景相机通常覆盖180度
+        'pan_range': (0, 360),        # 球机水平扫描范围(度) - 完整360度扫描
         'tilt_range': (-30, 30),      # 球机垂直扫描范围(度)
         'pan_step': 20,               # 水平扫描步进(度)
         'tilt_step': 15,              # 垂直扫描步进(度)

+ 4 - 0
dual_camera_system/config/ptz.py

@@ -14,4 +14,8 @@ PTZ_CONFIG = {
     'tilt_range': (-45, 45),         # 垂直视野范围 (度) - 垂直方向覆盖角度
     'pan_center': 90,                # 水平中心角度 (画面中心对应的PTZ角度)
     'tilt_center': 0,                # 垂直中心角度
+    
+    # 方向修正
+    # 如果球机与全景朝向相反(球机看后面),设为True
+    'pan_flip': False,
 }

+ 4 - 0
dual_camera_system/coordinator.py

@@ -353,6 +353,8 @@ class Coordinator:
                 if should_move:
                     if self.enable_calibration and self.calibrator and self.calibrator.is_calibrated():
                         pan, tilt = self.calibrator.transform(x_ratio, y_ratio)
+                        if self.ptz.ptz_config.get('pan_flip', False):
+                            pan = (pan + 180) % 360
                         zoom = self.ptz.ptz_config.get('default_zoom', 8)
                         self.ptz.goto_exact_position(pan, tilt, zoom)
                     else:
@@ -480,6 +482,8 @@ class Coordinator:
         if self.enable_ptz_tracking and self.enable_ptz_camera:
             if self.enable_calibration and self.calibrator and self.calibrator.is_calibrated():
                 pan, tilt = self.calibrator.transform(x_ratio, y_ratio)
+                if self.ptz.ptz_config.get('pan_flip', False):
+                    pan = (pan + 180) % 360
                 self.ptz.goto_exact_position(pan, tilt, zoom or self.ptz.ptz_config.get('default_zoom', 8))
             else:
                 self.ptz.move_to_target(x_ratio, y_ratio, zoom)

+ 1 - 1
dual_camera_system/main.py

@@ -233,7 +233,7 @@ class DualCameraSystem:
         
         # 配置重叠发现参数
         overlap_cfg = CALIBRATION_CONFIG.get('overlap_discovery', {})
-        self.calibrator.overlap_pan_range = overlap_cfg.get('pan_range', (0, 180))
+        self.calibrator.overlap_pan_range = overlap_cfg.get('pan_range', (0, 360))
         self.calibrator.overlap_tilt_range = overlap_cfg.get('tilt_range', (-30, 30))
         self.calibrator.overlap_pan_step = overlap_cfg.get('pan_step', 20)
         self.calibrator.overlap_tilt_step = overlap_cfg.get('tilt_step', 15)

+ 5 - 2
dual_camera_system/panorama_camera.py

@@ -18,6 +18,7 @@ from dataclasses import dataclass
 
 from config import PANORAMA_CAMERA, DETECTION_CONFIG
 from dahua_sdk import DahuaSDK, PTZCommand
+from video_lock import ff_lock
 
 
 @dataclass
@@ -161,7 +162,8 @@ class PanoramaCamera:
                 
                 # RTSP 模式获取帧 (推荐方式)
                 if self.rtsp_cap is not None and self.rtsp_cap.isOpened():
-                    ret, frame = self.rtsp_cap.read()
+                    with ff_lock:
+                        ret, frame = self.rtsp_cap.read()
                     if ret and frame is not None:
                         with self.frame_lock:
                             self.current_frame = frame.copy()
@@ -236,7 +238,8 @@ class PanoramaCamera:
                     time.sleep(0.1)
                     continue
                 
-                ret, frame = self.rtsp_cap.read()
+                with ff_lock:
+                    ret, frame = self.rtsp_cap.read()
                 if not ret or frame is None:
                     error_count += 1
                     if error_count > max_consecutive_errors:

+ 3 - 1
dual_camera_system/ptz_camera.py

@@ -19,6 +19,7 @@ import numpy as np
 
 from config import PTZ_CAMERA, PTZ_CONFIG
 from dahua_sdk import DahuaSDK, PTZCommand
+from video_lock import ff_lock
 
 
 @dataclass
@@ -132,7 +133,8 @@ class PTZCamera:
                     time.sleep(0.1)
                     continue
                 
-                ret, frame = self.rtsp_cap.read()
+                with ff_lock:
+                    ret, frame = self.rtsp_cap.read()
                 if not ret or frame is None:
                     error_count += 1
                     if error_count > max_consecutive_errors:

+ 37 - 0
dual_camera_system/video_lock.py

@@ -0,0 +1,37 @@
+"""
+全局视频捕获锁
+防止多个VideoCapture并发read()导致FFmpeg async_lock崩溃
+
+FFmpeg内部使用共享线程池解码,多个VideoCapture.read()并发调用
+会触发 pthread_frame.c:167 的 async_lock 断言失败导致进程崩溃。
+此模块提供全局锁,序列化所有read()调用。
+"""
+
+import threading
+
+# 全局锁:所有VideoCapture.read()必须持有此锁
+_ffmpeg_lock = threading.Lock()
+
+
+def acquire_ffmpeg_lock():
+    """获取FFmpeg全局锁"""
+    _ffmpeg_lock.acquire()
+
+
+def release_ffmpeg_lock():
+    """释放FFmpeg全局锁"""
+    _ffmpeg_lock.release()
+
+
+class FFLockContext:
+    """with语句用法: with ff_lock(): cap.read()"""
+    def __enter__(self):
+        _ffmpeg_lock.acquire()
+        return self
+
+    def __exit__(self, *args):
+        _ffmpeg_lock.release()
+
+
+# 便捷别名
+ff_lock = FFLockContext