|
@@ -745,6 +745,8 @@ class PTZCommand:
|
|
|
y_ratio: float = 0.0
|
|
y_ratio: float = 0.0
|
|
|
use_calibration: bool = True
|
|
use_calibration: bool = True
|
|
|
track_id: Optional[int] = None # 跟踪目标ID(用于配对图片保存)
|
|
track_id: Optional[int] = None # 跟踪目标ID(用于配对图片保存)
|
|
|
|
|
+ batch_id: Optional[str] = None # 批次ID(用于配对图片保存)
|
|
|
|
|
+ person_index: int = -1 # 人员在批次中的序号(用于配对图片保存)
|
|
|
|
|
|
|
|
|
|
|
|
|
class AsyncCoordinator(Coordinator):
|
|
class AsyncCoordinator(Coordinator):
|
|
@@ -1035,11 +1037,22 @@ class AsyncCoordinator(Coordinator):
|
|
|
if self._paired_saver is None:
|
|
if self._paired_saver is None:
|
|
|
return
|
|
return
|
|
|
|
|
|
|
|
- # 构建人员信息列表
|
|
|
|
|
|
|
+ # 过滤有效人员(置信度 >= 阈值)
|
|
|
|
|
+ person_threshold = DETECTION_CONFIG.get('person_threshold', 0.8)
|
|
|
|
|
+ valid_persons = []
|
|
|
|
|
+ for det in tracked:
|
|
|
|
|
+ if det.confidence >= person_threshold:
|
|
|
|
|
+ valid_persons.append(det)
|
|
|
|
|
+
|
|
|
|
|
+ if not valid_persons:
|
|
|
|
|
+ logger.debug(f"[配对保存] 无有效人员(阈值={person_threshold}),跳过批次创建")
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ # 构建人员信息列表(只包含有效人员)
|
|
|
persons = []
|
|
persons = []
|
|
|
self._person_ptz_index = {} # 重置索引映射
|
|
self._person_ptz_index = {} # 重置索引映射
|
|
|
|
|
|
|
|
- for i, det in enumerate(tracked):
|
|
|
|
|
|
|
+ for i, det in enumerate(valid_persons):
|
|
|
x_ratio = det.center[0] / frame_size[0]
|
|
x_ratio = det.center[0] / frame_size[0]
|
|
|
y_ratio = det.center[1] / frame_size[1]
|
|
y_ratio = det.center[1] / frame_size[1]
|
|
|
|
|
|
|
@@ -1058,7 +1071,7 @@ class AsyncCoordinator(Coordinator):
|
|
|
batch_id = self._paired_saver.start_new_batch(frame, persons)
|
|
batch_id = self._paired_saver.start_new_batch(frame, persons)
|
|
|
if batch_id:
|
|
if batch_id:
|
|
|
self._current_batch_id = batch_id
|
|
self._current_batch_id = batch_id
|
|
|
- logger.info(f"[配对保存] 创建批次: {batch_id}, 人员={len(persons)}")
|
|
|
|
|
|
|
+ logger.info(f"[配对保存] 创建批次: {batch_id}, 有效人员={len(persons)}/{len(tracked)}")
|
|
|
|
|
|
|
|
def _save_ptz_image_for_person(self, track_id: int,
|
|
def _save_ptz_image_for_person(self, track_id: int,
|
|
|
ptz_frame: np.ndarray,
|
|
ptz_frame: np.ndarray,
|
|
@@ -1086,6 +1099,29 @@ class AsyncCoordinator(Coordinator):
|
|
|
ptz_bbox=getattr(self, '_last_ptz_bbox', None)
|
|
ptz_bbox=getattr(self, '_last_ptz_bbox', None)
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
+ def _save_ptz_image_for_person_batch(self, batch_id: str, person_index: int,
|
|
|
|
|
+ ptz_frame: np.ndarray,
|
|
|
|
|
+ ptz_position: Tuple[float, float, int]):
|
|
|
|
|
+ """
|
|
|
|
|
+ 保存球机聚焦图片到指定批次(直接使用 batch_id,不依赖当前批次)
|
|
|
|
|
+
|
|
|
|
|
+ Args:
|
|
|
|
|
+ batch_id: 批次ID
|
|
|
|
|
+ person_index: 人员序号
|
|
|
|
|
+ ptz_frame: 球机帧
|
|
|
|
|
+ ptz_position: PTZ位置 (pan, tilt, zoom)
|
|
|
|
|
+ """
|
|
|
|
|
+ if self._paired_saver is None:
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ self._paired_saver.save_ptz_image(
|
|
|
|
|
+ batch_id=batch_id,
|
|
|
|
|
+ person_index=person_index,
|
|
|
|
|
+ ptz_frame=ptz_frame,
|
|
|
|
|
+ ptz_position=ptz_position,
|
|
|
|
|
+ ptz_bbox=getattr(self, '_last_ptz_bbox', None)
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
def _ptz_worker(self):
|
|
def _ptz_worker(self):
|
|
|
"""PTZ控制线程:从队列接收命令并控制球机"""
|
|
"""PTZ控制线程:从队列接收命令并控制球机"""
|
|
|
while self.running:
|
|
while self.running:
|
|
@@ -1095,8 +1131,8 @@ class AsyncCoordinator(Coordinator):
|
|
|
except queue.Empty:
|
|
except queue.Empty:
|
|
|
continue
|
|
continue
|
|
|
|
|
|
|
|
- # 从命令中提取 track_id 并传递
|
|
|
|
|
- self._execute_ptz_command(cmd, track_id=cmd.track_id)
|
|
|
|
|
|
|
+ # 执行PTZ命令(batch_id 和 person_index 已在命令中)
|
|
|
|
|
+ self._execute_ptz_command(cmd)
|
|
|
|
|
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
print(f"PTZ控制线程错误: {e}")
|
|
print(f"PTZ控制线程错误: {e}")
|
|
@@ -1181,11 +1217,17 @@ class AsyncCoordinator(Coordinator):
|
|
|
logger.debug(f"[PTZ] 冷却中,跳过 (间隔={current_time - self._last_ptz_time:.2f}s < {ptz_cooldown}s)")
|
|
logger.debug(f"[PTZ] 冷却中,跳过 (间隔={current_time - self._last_ptz_time:.2f}s < {ptz_cooldown}s)")
|
|
|
return
|
|
return
|
|
|
|
|
|
|
|
|
|
+ # 获取当前批次信息和人员序号
|
|
|
|
|
+ batch_id = self._current_batch_id if self._enable_paired_saving else None
|
|
|
|
|
+ person_index = self._person_ptz_index.get(target.track_id, -1) if self._enable_paired_saving else -1
|
|
|
|
|
+
|
|
|
cmd = PTZCommand(
|
|
cmd = PTZCommand(
|
|
|
pan=0, tilt=0, zoom=0,
|
|
pan=0, tilt=0, zoom=0,
|
|
|
x_ratio=x_ratio, y_ratio=y_ratio,
|
|
x_ratio=x_ratio, y_ratio=y_ratio,
|
|
|
use_calibration=self.enable_calibration,
|
|
use_calibration=self.enable_calibration,
|
|
|
- track_id=target.track_id # 传递跟踪ID
|
|
|
|
|
|
|
+ track_id=target.track_id, # 传递跟踪ID
|
|
|
|
|
+ batch_id=batch_id, # 传递批次ID
|
|
|
|
|
+ person_index=person_index # 传递人员序号
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
try:
|
|
try:
|
|
@@ -1195,16 +1237,20 @@ class AsyncCoordinator(Coordinator):
|
|
|
except queue.Full:
|
|
except queue.Full:
|
|
|
logger.warning("[PTZ] 命令队列满,丢弃本次命令")
|
|
logger.warning("[PTZ] 命令队列满,丢弃本次命令")
|
|
|
|
|
|
|
|
- def _execute_ptz_command(self, cmd: PTZCommand, track_id: int = None):
|
|
|
|
|
|
|
+ def _execute_ptz_command(self, cmd: PTZCommand):
|
|
|
"""
|
|
"""
|
|
|
执行PTZ命令(在PTZ线程中)
|
|
执行PTZ命令(在PTZ线程中)
|
|
|
|
|
|
|
|
Args:
|
|
Args:
|
|
|
- cmd: PTZ命令
|
|
|
|
|
- track_id: 跟踪目标ID(用于配对图片保存)
|
|
|
|
|
|
|
+ cmd: PTZ命令(包含 batch_id, person_index, track_id 用于配对保存)
|
|
|
"""
|
|
"""
|
|
|
self._last_ptz_time = time.time()
|
|
self._last_ptz_time = time.time()
|
|
|
|
|
|
|
|
|
|
+ # 从命令中提取配对保存相关信息
|
|
|
|
|
+ track_id = cmd.track_id
|
|
|
|
|
+ batch_id = cmd.batch_id
|
|
|
|
|
+ person_index = cmd.person_index
|
|
|
|
|
+
|
|
|
if cmd.use_calibration and self.calibrator and self.calibrator.is_calibrated():
|
|
if cmd.use_calibration and self.calibrator and self.calibrator.is_calibrated():
|
|
|
pan, tilt = self.calibrator.transform(cmd.x_ratio, cmd.y_ratio)
|
|
pan, tilt = self.calibrator.transform(cmd.x_ratio, cmd.y_ratio)
|
|
|
if self.ptz.ptz_config.get('pan_flip', False):
|
|
if self.ptz.ptz_config.get('pan_flip', False):
|
|
@@ -1216,7 +1262,8 @@ class AsyncCoordinator(Coordinator):
|
|
|
self._set_state(TrackingState.TRACKING)
|
|
self._set_state(TrackingState.TRACKING)
|
|
|
logger.info(
|
|
logger.info(
|
|
|
f"[PTZ] 执行: pan={pan:.1f}° tilt={tilt:.1f}° zoom={zoom} "
|
|
f"[PTZ] 执行: pan={pan:.1f}° tilt={tilt:.1f}° zoom={zoom} "
|
|
|
- f"(全景位置=({cmd.x_ratio:.3f}, {cmd.y_ratio:.3f}))"
|
|
|
|
|
|
|
+ f"(全景位置=({cmd.x_ratio:.3f}, {cmd.y_ratio:.3f}), "
|
|
|
|
|
+ f"batch={batch_id}, person={person_index})"
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
success = self.ptz.goto_exact_position(pan, tilt, zoom)
|
|
success = self.ptz.goto_exact_position(pan, tilt, zoom)
|
|
@@ -1237,11 +1284,13 @@ class AsyncCoordinator(Coordinator):
|
|
|
# 获取清晰的球机画面(尝试多次获取最新帧)
|
|
# 获取清晰的球机画面(尝试多次获取最新帧)
|
|
|
ptz_frame = self._get_clear_ptz_frame()
|
|
ptz_frame = self._get_clear_ptz_frame()
|
|
|
|
|
|
|
|
- # 保存球机图片到配对批次
|
|
|
|
|
- if self._enable_paired_saving and track_id is not None and ptz_frame is not None:
|
|
|
|
|
|
|
+ # 保存球机图片到配对批次(使用命令中的 batch_id 和 person_index)
|
|
|
|
|
+ if self._enable_paired_saving and batch_id is not None and person_index >= 0 and ptz_frame is not None:
|
|
|
# 使用球机端检测器检测人体并标记
|
|
# 使用球机端检测器检测人体并标记
|
|
|
- ptz_frame_marked = self._mark_ptz_frame_with_detection(ptz_frame, person_index=self._person_ptz_index.get(track_id, 0))
|
|
|
|
|
- self._save_ptz_image_for_person(track_id, ptz_frame_marked, (final_pan, final_tilt, final_zoom))
|
|
|
|
|
|
|
+ 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, (final_pan, final_tilt, final_zoom))
|
|
|
|
|
+ elif self._enable_paired_saving:
|
|
|
|
|
+ logger.warning(f"[配对保存] 跳过球机图保存: batch_id={batch_id}, person_index={person_index}, frame={ptz_frame is not None}")
|
|
|
|
|
|
|
|
logger.info(f"[PTZ] 到位确认完成: pan={final_pan:.1f}° tilt={final_tilt:.1f}° zoom={final_zoom}")
|
|
logger.info(f"[PTZ] 到位确认完成: pan={final_pan:.1f}° tilt={final_tilt:.1f}° zoom={final_zoom}")
|
|
|
else:
|
|
else:
|