Explorar el Código

refactor(camera_group): 简化配对图片保存器初始化逻辑

- 移除配置模块中直接导入OSS和设备相关配置,改为在PairedImageSaver内部管理
- 只传入覆盖设备配置中的group_id,减少传参复杂度
- 删除配对图片保存器初始化时OSS上传器和第三方上传器的启动控制,交由相应模块管理
- 保持第三方平台推送功能,保留启动和回调设置
- 统一日志格式,增加配对保存器初始化时OSS开关和设备ID日志输出
- 重新整理相关import,移除未使用配置导入,防止配置耦合过紧
wenhongquan hace 4 días
padre
commit
babdd8d9da

BIN
dual_camera_system/__pycache__/camera_group.cpython-310.pyc


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


BIN
dual_camera_system/__pycache__/oss_uploader.cpython-310.pyc


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


+ 7 - 18
dual_camera_system/camera_group.py

@@ -17,9 +17,8 @@ from ocr_recognizer import NumberDetector, PersonInfo
 from coordinator import SequentialCoordinator
 from coordinator import SequentialCoordinator
 from calibration import CameraCalibrator, CalibrationManager
 from calibration import CameraCalibrator, CalibrationManager
 from paired_image_saver import PairedImageSaver, get_paired_saver
 from paired_image_saver import PairedImageSaver, get_paired_saver
-from oss_uploader import get_oss_uploader
 from third_party_pusher import get_third_party_pusher
 from third_party_pusher import get_third_party_pusher
-from config import DEVICE_CONFIG, S3_COMPATIBLE_CONFIG, THIRD_PARTY_CONFIG
+from config import THIRD_PARTY_CONFIG
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
@@ -123,27 +122,15 @@ class CameraGroup:
         ptz_config['group_id'] = self.group_id
         ptz_config['group_id'] = self.group_id
         self.ptz_camera = PTZCamera(self.sdk, ptz_config)
         self.ptz_camera = PTZCamera(self.sdk, ptz_config)
         
         
-        # 3. 初始化配对图片保存器(使用单例模式,支持 OSS
+        # 3. 初始化配对图片保存器(PairedImageSaver 会自动从配置模块读取 OSS 和设备配置
         try:
         try:
-            # 获取 OSS 上传器
-            oss_uploader = None
-            enable_oss = S3_COMPATIBLE_CONFIG.get('enabled', False)
-            if enable_oss:
-                oss_uploader = get_oss_uploader()
-                if not oss_uploader.running:
-                    oss_uploader.start()
-                logger.info(f"[{self.group_id}] OSS 上传已启用")
-            
-            # 创建设备配置(包含 group_id)
-            device_config = DEVICE_CONFIG.copy()
-            device_config['group_id'] = self.group_id
+            # 创建设备配置(包含 group_id,传入覆盖默认配置)
+            device_config = {'group_id': self.group_id}
             
             
             # 使用单例获取配对保存器
             # 使用单例获取配对保存器
             self.paired_saver = get_paired_saver(
             self.paired_saver = get_paired_saver(
                 base_dir=self.paired_image_dir,
                 base_dir=self.paired_image_dir,
                 time_window=5.0,
                 time_window=5.0,
-                enable_oss=enable_oss,
-                oss_uploader=oss_uploader,
                 device_config=device_config
                 device_config=device_config
             )
             )
             
             
@@ -155,7 +142,9 @@ class CameraGroup:
                 self.paired_saver.set_upload_callback(pusher.report_batch)
                 self.paired_saver.set_upload_callback(pusher.report_batch)
                 logger.info(f"[{self.group_id}] 第三方平台推送已启用")
                 logger.info(f"[{self.group_id}] 第三方平台推送已启用")
             
             
-            logger.info(f"[{self.group_id}] 配对图片保存器初始化成功: {self.paired_image_dir}")
+            logger.info(f"[{self.group_id}] 配对图片保存器初始化成功: {self.paired_image_dir}, "
+                       f"OSS={self.paired_saver.enable_oss}, "
+                       f"device_id={self.paired_saver.device_config.get('device_id', 'N/A')}")
         except Exception as e:
         except Exception as e:
             logger.warning(f"[{self.group_id}] 配对图片保存器初始化失败: {e}")
             logger.warning(f"[{self.group_id}] 配对图片保存器初始化失败: {e}")
         
         

BIN
dual_camera_system/config/__pycache__/__init__.cpython-310.pyc


BIN
dual_camera_system/config/__pycache__/oss.cpython-310.pyc


+ 23 - 11
dual_camera_system/coordinator.py

@@ -1527,23 +1527,30 @@ class AsyncCoordinator(Coordinator):
             self.ptz.get_frame()
             self.ptz.get_frame()
             time.sleep(0.05)
             time.sleep(0.05)
         
         
-        for i in range(max_attempts):
+        # 【关键修复】只尝试少量次数,尽快拿到可用帧
+        # 避免在获取帧期间球机被移动到其他位置
+        effective_attempts = min(max_attempts, 5)
+        
+        for i in range(effective_attempts):
             frame = self.ptz.get_frame()
             frame = self.ptz.get_frame()
             if frame is not None:
             if frame is not None:
+                # 立即复制帧,防止 RTSP 流更新导致帧被覆盖
+                frame_copy = frame.copy()
+                
                 # 使用拉普拉斯算子评估图像清晰度
                 # 使用拉普拉斯算子评估图像清晰度
-                gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
+                gray = cv2.cvtColor(frame_copy, cv2.COLOR_BGR2GRAY)
                 laplacian_var = cv2.Laplacian(gray, cv2.CV_64F).var()
                 laplacian_var = cv2.Laplacian(gray, cv2.CV_64F).var()
                 
                 
-                logger.debug(f"[帧获取] 尝试 {i+1}/{max_attempts}: 清晰度={laplacian_var:.1f}")
+                logger.debug(f"[帧获取] 尝试 {i+1}/{effective_attempts}: 清晰度={laplacian_var:.1f}")
                 
                 
                 if laplacian_var > best_score:
                 if laplacian_var > best_score:
                     best_score = laplacian_var
                     best_score = laplacian_var
-                    best_frame = frame.copy()
+                    best_frame = frame_copy
                 
                 
                 # 如果清晰度足够高,直接返回
                 # 如果清晰度足够高,直接返回
-                if laplacian_var > 150:  # 【提高】清晰度阈值从100提高到150
+                if laplacian_var > 150:
                     logger.info(f"[帧获取] 获取清晰帧: 尝试 {i+1} 次, 清晰度={laplacian_var:.1f}")
                     logger.info(f"[帧获取] 获取清晰帧: 尝试 {i+1} 次, 清晰度={laplacian_var:.1f}")
-                    return frame
+                    return frame_copy
             
             
             time.sleep(wait_interval)
             time.sleep(wait_interval)
         
         
@@ -1837,13 +1844,14 @@ class SequentialCoordinator(AsyncCoordinator):
                 state = self._get_capture_state()
                 state = self._get_capture_state()
                 
                 
                 if state == 'capturing':
                 if state == 'capturing':
-                    # 执行顺序抓拍
+                    # 执行顺序抓拍(阻塞直到当前目标抓拍完成)
                     self._execute_sequential_capture()
                     self._execute_sequential_capture()
                 
                 
                 elif state == 'returning':
                 elif state == 'returning':
                     # 回到默认位置
                     # 回到默认位置
                     self._return_to_default_position()
                     self._return_to_default_position()
                 
                 
+                # idle 状态不做任何操作,让检测线程控制
                 time.sleep(0.05)
                 time.sleep(0.05)
                 
                 
             except Exception as e:
             except Exception as e:
@@ -1931,6 +1939,10 @@ class SequentialCoordinator(AsyncCoordinator):
                    f"位置=({x_ratio:.3f}, {y_ratio:.3f}) -> "
                    f"位置=({x_ratio:.3f}, {y_ratio:.3f}) -> "
                    f"PTZ=({pan:.1f}°, {tilt:.1f}°, zoom={zoom}) [{coord_type}], 批次={batch_id}")
                    f"PTZ=({pan:.1f}°, {tilt:.1f}°, zoom={zoom}) [{coord_type}], 批次={batch_id}")
         
         
+        # 【关键修复】先递增索引,防止 PTZ 线程重复进入时重复执行
+        with self._batch_targets_lock:
+            self._current_capture_index += 1
+        
         # 执行PTZ移动
         # 执行PTZ移动
         self._set_state(TrackingState.TRACKING)
         self._set_state(TrackingState.TRACKING)
         success = self.ptz.goto_exact_position(pan, tilt, zoom)
         success = self.ptz.goto_exact_position(pan, tilt, zoom)
@@ -1983,10 +1995,6 @@ class SequentialCoordinator(AsyncCoordinator):
         else:
         else:
             logger.warning(f"[顺序模式] PTZ移动失败")
             logger.warning(f"[顺序模式] PTZ移动失败")
         
         
-        # 移动到下一个目标
-        with self._batch_targets_lock:
-            self._current_capture_index += 1
-        
         # 抓拍间隔
         # 抓拍间隔
         time.sleep(self._capture_config['capture_wait_time'])
         time.sleep(self._capture_config['capture_wait_time'])
     
     
@@ -2001,6 +2009,10 @@ class SequentialCoordinator(AsyncCoordinator):
         default_tilt = self._capture_config['default_tilt']
         default_tilt = self._capture_config['default_tilt']
         default_zoom = self._capture_config['default_zoom']
         default_zoom = self._capture_config['default_zoom']
         
         
+        # 【关键修复】等待一小段时间,确保最后的帧已经被读取和保存
+        # 因为 _get_clear_ptz_frame 是异步获取帧,可能在抓拍后立刻触发返回默认位置
+        time.sleep(0.5)
+        
         logger.info(f"[顺序模式] 球机回到默认位置: ({default_pan}°, {default_tilt}°, zoom={default_zoom})")
         logger.info(f"[顺序模式] 球机回到默认位置: ({default_pan}°, {default_tilt}°, zoom={default_zoom})")
         
         
         success = self.ptz.goto_exact_position(default_pan, default_tilt, default_zoom)
         success = self.ptz.goto_exact_position(default_pan, default_tilt, default_zoom)

+ 50 - 3
dual_camera_system/paired_image_saver.py

@@ -81,9 +81,43 @@ class PairedImageSaver:
         self.base_dir = Path(base_dir)
         self.base_dir = Path(base_dir)
         self.time_window = time_window
         self.time_window = time_window
         self.max_batches = max_batches
         self.max_batches = max_batches
-        self.enable_oss = enable_oss
-        self.oss_uploader = oss_uploader
-        self.device_config = device_config or {}
+        
+        # 从配置模块读取 OSS 和设备配置(确保即使外部不传也能正确配置)
+        try:
+            from config import S3_COMPATIBLE_CONFIG, DEVICE_CONFIG
+            
+            # OSS 配置:优先使用传入参数,否则从配置模块读取
+            if enable_oss or (not enable_oss and S3_COMPATIBLE_CONFIG.get('enabled', False)):
+                self.enable_oss = True
+            else:
+                self.enable_oss = enable_oss
+            
+            # OSS 上传器:优先使用传入的实例,否则从全局获取
+            if oss_uploader is not None:
+                self.oss_uploader = oss_uploader
+            elif self.enable_oss:
+                try:
+                    from oss_uploader import get_oss_uploader
+                    self.oss_uploader = get_oss_uploader()
+                    if not self.oss_uploader.running:
+                        self.oss_uploader.start()
+                except Exception as e:
+                    logger.warning(f"[配对保存] OSS 上传器初始化失败: {e}")
+                    self.oss_uploader = None
+                    self.enable_oss = False
+            else:
+                self.oss_uploader = None
+            
+            # 设备配置:合并传入参数和配置模块
+            self.device_config = DEVICE_CONFIG.copy()
+            if device_config:
+                self.device_config.update(device_config)
+                
+        except ImportError:
+            # 配置模块不可用时使用传入参数
+            self.enable_oss = enable_oss
+            self.oss_uploader = oss_uploader
+            self.device_config = device_config or {}
         
         
         self._current_batch: Optional[DetectionBatch] = None
         self._current_batch: Optional[DetectionBatch] = None
         self._batch_lock = threading.Lock()
         self._batch_lock = threading.Lock()
@@ -648,6 +682,8 @@ def get_paired_saver(base_dir: str = None, time_window: float = 5.0,
     """
     """
     获取配对保存管理器实例(单例模式)
     获取配对保存管理器实例(单例模式)
     
     
+    如果实例已存在但缺少 OSS/设备配置,会自动从配置模块更新
+    
     Args:
     Args:
         base_dir: 基础保存目录
         base_dir: 基础保存目录
         time_window: 时间窗口
         time_window: 时间窗口
@@ -668,6 +704,17 @@ def get_paired_saver(base_dir: str = None, time_window: float = 5.0,
             oss_uploader=oss_uploader,
             oss_uploader=oss_uploader,
             device_config=device_config
             device_config=device_config
         )
         )
+    else:
+        # 单例已存在,检查是否需要更新配置
+        # PairedImageSaver.__init__ 已从配置模块自动读取,
+        # 这里只处理外部显式传入的参数覆盖
+        if oss_uploader is not None and _paired_saver_instance.oss_uploader is None:
+            _paired_saver_instance.oss_uploader = oss_uploader
+            _paired_saver_instance.enable_oss = True
+            logger.info("[配对保存] 更新 OSS 上传器配置")
+        if device_config is not None:
+            _paired_saver_instance.device_config.update(device_config)
+            logger.info(f"[配对保存] 更新设备配置: {device_config}")
     
     
     return _paired_saver_instance
     return _paired_saver_instance