|
@@ -1534,3 +1534,343 @@ class AsyncCoordinator(Coordinator):
|
|
|
def on_ptz_confirmed(self, callback: Callable):
|
|
def on_ptz_confirmed(self, callback: Callable):
|
|
|
"""注册PTZ位置确认回调"""
|
|
"""注册PTZ位置确认回调"""
|
|
|
self._on_ptz_confirmed = callback
|
|
self._on_ptz_confirmed = callback
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+class SequentialCoordinator(AsyncCoordinator):
|
|
|
|
|
+ """
|
|
|
|
|
+ 顺序联动控制器 — 全景检测→逐个PTZ抓拍→回到全景的循环模式
|
|
|
|
|
+
|
|
|
|
|
+ 工作流程:
|
|
|
|
|
+ 1. 全景摄像头检测人员
|
|
|
|
|
+ 2. 检测到人员后,暂停全景检测
|
|
|
|
|
+ 3. 球机依次对每个检测到的人员进行PTZ定位+变焦抓拍
|
|
|
|
|
+ 4. 所有人员抓拍完成后,球机回到默认位置
|
|
|
|
|
+ 5. 恢复全景检测,进入下一轮循环
|
|
|
|
|
+
|
|
|
|
|
+ 适用于需要高质量抓拍的场景,确保每个目标都能被球机清晰拍摄
|
|
|
|
|
+ """
|
|
|
|
|
+
|
|
|
|
|
+ def __init__(self, *args, **kwargs):
|
|
|
|
|
+ super().__init__(*args, **kwargs)
|
|
|
|
|
+
|
|
|
|
|
+ # 顺序抓拍状态
|
|
|
|
|
+ self._capture_state = 'idle' # 'idle', 'detecting', 'capturing', 'returning'
|
|
|
|
|
+ self._capture_state_lock = threading.Lock()
|
|
|
|
|
+
|
|
|
|
|
+ # 当前批次检测到的目标
|
|
|
|
|
+ self._batch_targets: List[TrackingTarget] = []
|
|
|
|
|
+ self._batch_targets_lock = threading.Lock()
|
|
|
|
|
+ self._current_capture_index = 0
|
|
|
|
|
+
|
|
|
|
|
+ # 抓拍完成事件(用于同步)
|
|
|
|
|
+ self._capture_done_event = threading.Event()
|
|
|
|
|
+
|
|
|
|
|
+ # 配置参数
|
|
|
|
|
+ self._capture_config = {
|
|
|
|
|
+ 'ptz_stabilize_time': 1.0, # PTZ到位后稳定等待时间
|
|
|
|
|
+ 'capture_wait_time': 0.5, # 抓拍等待时间
|
|
|
|
|
+ 'return_to_panorama': True, # 完成后是否回到全景默认位置
|
|
|
|
|
+ 'default_pan': 0.0, # 默认pan角度
|
|
|
|
|
+ 'default_tilt': 0.0, # 默认tilt角度
|
|
|
|
|
+ 'default_zoom': 1, # 默认zoom(广角)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ # 覆盖父类的PTZ冷却时间(顺序模式下可以更短)
|
|
|
|
|
+ self.PTZ_COMMAND_COOLDOWN = 0.1
|
|
|
|
|
+
|
|
|
|
|
+ logger.info("[SequentialCoordinator] 顺序联动控制器初始化完成")
|
|
|
|
|
+
|
|
|
|
|
+ def _detection_worker(self):
|
|
|
|
|
+ """检测线程:顺序模式下的检测逻辑"""
|
|
|
|
|
+ # 从 DETECTION_CONFIG 获取检测帧率,默认每秒2帧
|
|
|
|
|
+ detection_fps = self.config.get('detection_fps', DETECTION_CONFIG.get('detection_fps', 2))
|
|
|
|
|
+ detection_interval = 1.0 / detection_fps
|
|
|
|
|
+ last_detection_time = 0
|
|
|
|
|
+
|
|
|
|
|
+ frame_count = 0
|
|
|
|
|
+ last_log_time = time.time()
|
|
|
|
|
+ log_interval = 5.0
|
|
|
|
|
+ detection_run_count = 0
|
|
|
|
|
+ detection_person_count = 0
|
|
|
|
|
+ last_no_detect_log_time = 0
|
|
|
|
|
+ no_detect_log_interval = 30.0
|
|
|
|
|
+
|
|
|
|
|
+ with self.stats_lock:
|
|
|
|
|
+ self.stats['start_time'] = time.time()
|
|
|
|
|
+
|
|
|
|
|
+ if self.detector is None:
|
|
|
|
|
+ logger.warning("[检测线程] ⚠️ 人体检测器未初始化!")
|
|
|
|
|
+ else:
|
|
|
|
|
+ logger.info(f"[检测线程] ✓ 顺序模式已就绪, 检测帧率={detection_fps}fps")
|
|
|
|
|
+
|
|
|
|
|
+ while self.running:
|
|
|
|
|
+ try:
|
|
|
|
|
+ current_time = time.time()
|
|
|
|
|
+
|
|
|
|
|
+ # 获取当前帧
|
|
|
|
|
+ frame = self.panorama.get_frame()
|
|
|
|
|
+ if frame is None:
|
|
|
|
|
+ time.sleep(0.01)
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ frame_count += 1
|
|
|
|
|
+ self._update_stats('frames_processed')
|
|
|
|
|
+ frame_size = (frame.shape[1], frame.shape[0])
|
|
|
|
|
+
|
|
|
|
|
+ # 日志输出
|
|
|
|
|
+ if current_time - last_log_time >= log_interval:
|
|
|
|
|
+ elapsed = current_time - last_log_time
|
|
|
|
|
+ fps = frame_count / elapsed if elapsed > 0 else 0
|
|
|
|
|
+ state_str = self._get_capture_state()
|
|
|
|
|
+ logger.info(f"[检测线程] 帧率={fps:.1f}fps, 状态={state_str}, "
|
|
|
|
|
+ f"检测轮次={detection_run_count}(有人={detection_person_count})")
|
|
|
|
|
+ frame_count = 0
|
|
|
|
|
+ last_log_time = current_time
|
|
|
|
|
+
|
|
|
|
|
+ # 状态机处理
|
|
|
|
|
+ state = self._get_capture_state()
|
|
|
|
|
+
|
|
|
|
|
+ if state == 'idle':
|
|
|
|
|
+ # 空闲状态:周期性检测
|
|
|
|
|
+ if current_time - last_detection_time >= detection_interval:
|
|
|
|
|
+ last_detection_time = current_time
|
|
|
|
|
+ detection_run_count += 1
|
|
|
|
|
+
|
|
|
|
|
+ # 执行检测
|
|
|
|
|
+ detections = self._detect_persons(frame)
|
|
|
|
|
+
|
|
|
|
|
+ if detections:
|
|
|
|
|
+ self._update_stats('persons_detected', len(detections))
|
|
|
|
|
+ detection_person_count += 1
|
|
|
|
|
+
|
|
|
|
|
+ # 更新跟踪目标
|
|
|
|
|
+ self._update_tracking_targets(detections, frame_size)
|
|
|
|
|
+
|
|
|
|
|
+ # 获取有效目标列表
|
|
|
|
|
+ targets = self._get_all_valid_targets()
|
|
|
|
|
+
|
|
|
|
|
+ if targets:
|
|
|
|
|
+ logger.info(f"[顺序模式] 检测到 {len(targets)} 个目标,开始顺序抓拍")
|
|
|
|
|
+
|
|
|
|
|
+ # 创建配对保存批次
|
|
|
|
|
+ if self._enable_paired_saving and self._paired_saver is not None:
|
|
|
|
|
+ self._create_detection_batch(frame, detections, frame_size)
|
|
|
|
|
+
|
|
|
|
|
+ # 切换到抓拍状态
|
|
|
|
|
+ self._start_capture_sequence(targets)
|
|
|
|
|
+ else:
|
|
|
|
|
+ # 未检测到人员
|
|
|
|
|
+ if current_time - last_no_detect_log_time >= no_detect_log_interval:
|
|
|
|
|
+ logger.info(f"[检测] 本轮未检测到人员 (累计{detection_run_count}轮)")
|
|
|
|
|
+ last_no_detect_log_time = current_time
|
|
|
|
|
+
|
|
|
|
|
+ elif state == 'capturing':
|
|
|
|
|
+ # 抓拍状态中:检测线程等待,不执行新检测
|
|
|
|
|
+ # 等待PTZ线程完成当前批次
|
|
|
|
|
+ pass
|
|
|
|
|
+
|
|
|
|
|
+ elif state == 'returning':
|
|
|
|
|
+ # 球机回到默认位置中
|
|
|
|
|
+ pass
|
|
|
|
|
+
|
|
|
|
|
+ # 清理过期目标
|
|
|
|
|
+ self._cleanup_expired_targets()
|
|
|
|
|
+ time.sleep(0.01)
|
|
|
|
|
+
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.error(f"[检测线程] 错误: {e}")
|
|
|
|
|
+ time.sleep(0.1)
|
|
|
|
|
+
|
|
|
|
|
+ def _ptz_worker(self):
|
|
|
|
|
+ """PTZ控制线程:顺序模式下的PTZ控制逻辑"""
|
|
|
|
|
+ logger.info("[PTZ线程] 顺序模式PTZ控制线程启动")
|
|
|
|
|
+
|
|
|
|
|
+ while self.running:
|
|
|
|
|
+ try:
|
|
|
|
|
+ state = self._get_capture_state()
|
|
|
|
|
+
|
|
|
|
|
+ if state == 'capturing':
|
|
|
|
|
+ # 执行顺序抓拍
|
|
|
|
|
+ self._execute_sequential_capture()
|
|
|
|
|
+
|
|
|
|
|
+ elif state == 'returning':
|
|
|
|
|
+ # 回到默认位置
|
|
|
|
|
+ self._return_to_default_position()
|
|
|
|
|
+
|
|
|
|
|
+ time.sleep(0.05)
|
|
|
|
|
+
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.error(f"[PTZ线程] 错误: {e}")
|
|
|
|
|
+ time.sleep(0.1)
|
|
|
|
|
+
|
|
|
|
|
+ def _get_capture_state(self) -> str:
|
|
|
|
|
+ """获取当前抓拍状态"""
|
|
|
|
|
+ with self._capture_state_lock:
|
|
|
|
|
+ return self._capture_state
|
|
|
|
|
+
|
|
|
|
|
+ def _set_capture_state(self, state: str):
|
|
|
|
|
+ """设置抓拍状态"""
|
|
|
|
|
+ with self._capture_state_lock:
|
|
|
|
|
+ old_state = self._capture_state
|
|
|
|
|
+ self._capture_state = state
|
|
|
|
|
+ logger.info(f"[顺序模式] 状态切换: {old_state} -> {state}")
|
|
|
|
|
+
|
|
|
|
|
+ def _start_capture_sequence(self, targets: List[TrackingTarget]):
|
|
|
|
|
+ """开始顺序抓拍序列"""
|
|
|
|
|
+ with self._batch_targets_lock:
|
|
|
|
|
+ self._batch_targets = targets.copy()
|
|
|
|
|
+ self._current_capture_index = 0
|
|
|
|
|
+
|
|
|
|
|
+ self._set_capture_state('capturing')
|
|
|
|
|
+
|
|
|
|
|
+ def _execute_sequential_capture(self):
|
|
|
|
|
+ """执行顺序抓拍(依次对每个目标进行PTZ定位和抓拍)"""
|
|
|
|
|
+ with self._batch_targets_lock:
|
|
|
|
|
+ targets = self._batch_targets.copy()
|
|
|
|
|
+ current_idx = self._current_capture_index
|
|
|
|
|
+
|
|
|
|
|
+ if current_idx >= len(targets):
|
|
|
|
|
+ # 所有目标已抓拍完成
|
|
|
|
|
+ logger.info("[顺序模式] 所有目标抓拍完成")
|
|
|
|
|
+ self._set_capture_state('returning')
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ # 获取当前目标
|
|
|
|
|
+ target = targets[current_idx]
|
|
|
|
|
+ x_ratio, y_ratio = target.position
|
|
|
|
|
+
|
|
|
|
|
+ # 计算PTZ角度
|
|
|
|
|
+ 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)
|
|
|
|
|
+ coord_type = "校准坐标"
|
|
|
|
|
+ else:
|
|
|
|
|
+ pan, tilt, zoom = self.ptz.calculate_ptz_position(x_ratio, y_ratio)
|
|
|
|
|
+ coord_type = "估算坐标"
|
|
|
|
|
+
|
|
|
|
|
+ # 获取批次信息
|
|
|
|
|
+ batch_id = self._current_batch_id if self._enable_paired_saving else None
|
|
|
|
|
+ person_index = current_idx # 使用当前索引作为人员序号
|
|
|
|
|
+
|
|
|
|
|
+ logger.info(f"[顺序模式] 抓拍目标 {current_idx + 1}/{len(targets)}: "
|
|
|
|
|
+ f"位置=({x_ratio:.3f}, {y_ratio:.3f}) -> "
|
|
|
|
|
+ f"PTZ=({pan:.1f}°, {tilt:.1f}°, zoom={zoom}) [{coord_type}]")
|
|
|
|
|
+
|
|
|
|
|
+ # 执行PTZ移动
|
|
|
|
|
+ self._set_state(TrackingState.TRACKING)
|
|
|
|
|
+ success = self.ptz.goto_exact_position(pan, tilt, zoom)
|
|
|
|
|
+
|
|
|
|
|
+ if success:
|
|
|
|
|
+ # 等待球机稳定
|
|
|
|
|
+ stabilize_time = self._capture_config['ptz_stabilize_time']
|
|
|
|
|
+ logger.info(f"[顺序模式] 等待球机稳定 {stabilize_time}s...")
|
|
|
|
|
+ time.sleep(stabilize_time)
|
|
|
|
|
+
|
|
|
|
|
+ # 自动变焦(如果启用)
|
|
|
|
|
+ final_zoom = zoom
|
|
|
|
|
+ if self.enable_ptz_detection and self.auto_zoom_config.get('enabled', False):
|
|
|
|
|
+ auto_zoom_result = self._auto_zoom_person(pan, tilt, zoom)
|
|
|
|
|
+ if auto_zoom_result != zoom:
|
|
|
|
|
+ final_zoom = auto_zoom_result
|
|
|
|
|
+ time.sleep(0.5) # 变焦后再次等待
|
|
|
|
|
+
|
|
|
|
|
+ # 获取清晰的球机画面
|
|
|
|
|
+ ptz_frame = self._get_clear_ptz_frame()
|
|
|
|
|
+
|
|
|
|
|
+ if ptz_frame is not None:
|
|
|
|
|
+ # 保存球机图片
|
|
|
|
|
+ if self._enable_paired_saving and batch_id is not None:
|
|
|
|
|
+ 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,
|
|
|
|
|
+ (pan, tilt, final_zoom)
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ # 保存到本地(无论是否启用配对保存)
|
|
|
|
|
+ self._save_local_snapshot(ptz_frame, current_idx, pan, tilt, final_zoom)
|
|
|
|
|
+
|
|
|
|
|
+ logger.info(f"[顺序模式] 目标 {current_idx + 1} 抓拍完成")
|
|
|
|
|
+ else:
|
|
|
|
|
+ logger.warning(f"[顺序模式] 获取球机画面失败")
|
|
|
|
|
+ else:
|
|
|
|
|
+ logger.warning(f"[顺序模式] PTZ移动失败")
|
|
|
|
|
+
|
|
|
|
|
+ # 移动到下一个目标
|
|
|
|
|
+ with self._batch_targets_lock:
|
|
|
|
|
+ self._current_capture_index += 1
|
|
|
|
|
+
|
|
|
|
|
+ # 抓拍间隔
|
|
|
|
|
+ time.sleep(self._capture_config['capture_wait_time'])
|
|
|
|
|
+
|
|
|
|
|
+ def _return_to_default_position(self):
|
|
|
|
|
+ """球机回到默认位置(广角全景)"""
|
|
|
|
|
+ if not self._capture_config['return_to_panorama']:
|
|
|
|
|
+ # 不回到默认位置,直接回到空闲状态
|
|
|
|
|
+ self._set_capture_state('idle')
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ default_pan = self._capture_config['default_pan']
|
|
|
|
|
+ default_tilt = self._capture_config['default_tilt']
|
|
|
|
|
+ default_zoom = self._capture_config['default_zoom']
|
|
|
|
|
+
|
|
|
|
|
+ logger.info(f"[顺序模式] 球机回到默认位置: ({default_pan}°, {default_tilt}°, zoom={default_zoom})")
|
|
|
|
|
+
|
|
|
|
|
+ success = self.ptz.goto_exact_position(default_pan, default_tilt, default_zoom)
|
|
|
|
|
+
|
|
|
|
|
+ if success:
|
|
|
|
|
+ # 等待到位
|
|
|
|
|
+ time.sleep(self._capture_config['ptz_stabilize_time'])
|
|
|
|
|
+ logger.info("[顺序模式] 球机已回到默认位置")
|
|
|
|
|
+ else:
|
|
|
|
|
+ logger.warning("[顺序模式] 球机回到默认位置失败")
|
|
|
|
|
+
|
|
|
|
|
+ # 回到空闲状态,恢复全景检测
|
|
|
|
|
+ self._set_capture_state('idle')
|
|
|
|
|
+
|
|
|
|
|
+ # 清空批次目标
|
|
|
|
|
+ with self._batch_targets_lock:
|
|
|
|
|
+ self._batch_targets = []
|
|
|
|
|
+ self._current_capture_index = 0
|
|
|
|
|
+
|
|
|
|
|
+ def _save_local_snapshot(self, frame: np.ndarray, index: int,
|
|
|
|
|
+ pan: float, tilt: float, zoom: int):
|
|
|
|
|
+ """保存本地快照"""
|
|
|
|
|
+ try:
|
|
|
|
|
+ import os
|
|
|
|
|
+ from datetime import datetime
|
|
|
|
|
+
|
|
|
|
|
+ # 创建保存目录
|
|
|
|
|
+ save_dir = '/home/admin/dsh/captures'
|
|
|
|
|
+ os.makedirs(save_dir, exist_ok=True)
|
|
|
|
|
+
|
|
|
|
|
+ # 生成文件名
|
|
|
|
|
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3]
|
|
|
|
|
+ filename = f"capture_{timestamp}_person{index}_p{int(pan)}_t{int(tilt)}_z{zoom}.jpg"
|
|
|
|
|
+ filepath = os.path.join(save_dir, filename)
|
|
|
|
|
+
|
|
|
|
|
+ # 保存图片
|
|
|
|
|
+ cv2.imwrite(filepath, frame, [cv2.IMWRITE_JPEG_QUALITY, 90])
|
|
|
|
|
+ logger.info(f"[顺序模式] 快照已保存: {filepath}")
|
|
|
|
|
+
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.error(f"[顺序模式] 保存快照失败: {e}")
|
|
|
|
|
+
|
|
|
|
|
+ def set_capture_config(self, **kwargs):
|
|
|
|
|
+ """设置抓拍配置"""
|
|
|
|
|
+ self._capture_config.update(kwargs)
|
|
|
|
|
+ logger.info(f"[顺序模式] 配置已更新: {kwargs}")
|
|
|
|
|
+
|
|
|
|
|
+ def get_capture_status(self) -> dict:
|
|
|
|
|
+ """获取当前抓拍状态"""
|
|
|
|
|
+ with self._batch_targets_lock:
|
|
|
|
|
+ total = len(self._batch_targets)
|
|
|
|
|
+ current = self._current_capture_index
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ 'state': self._get_capture_state(),
|
|
|
|
|
+ 'total_targets': total,
|
|
|
|
|
+ 'current_index': current,
|
|
|
|
|
+ 'remaining': max(0, total - current)
|
|
|
|
|
+ }
|