|
|
@@ -20,7 +20,7 @@ logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
|
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)
|
|
|
@@ -28,8 +28,10 @@ class PersonInfo:
|
|
|
ptz_position: Optional[Tuple[float, float, int]] = None # (pan, tilt, zoom)
|
|
|
ptz_bbox: Optional[Tuple[int, int, int, int]] = None # 球机图中检测到的bbox (x1, y1, x2, y2)
|
|
|
ptz_image_saved: bool = False
|
|
|
- ptz_image_path: Optional[str] = None
|
|
|
- ptz_oss_url: Optional[str] = None # 球机图 OSS URL
|
|
|
+ ptz_image_path: Optional[str] = None # 标记后的球机图路径
|
|
|
+ ptz_image_original_path: Optional[str] = None # 未标记的球机图路径
|
|
|
+ ptz_oss_url: Optional[str] = None # 标记后球机图 OSS URL
|
|
|
+ ptz_oss_url_original: Optional[str] = None # 未标记球机图 OSS URL
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
@@ -38,8 +40,10 @@ class DetectionBatch:
|
|
|
batch_id: str
|
|
|
timestamp: float
|
|
|
panorama_image: Optional[object] = None # numpy array
|
|
|
- panorama_path: Optional[str] = None
|
|
|
- panorama_oss_url: Optional[str] = None # 全景图 OSS URL
|
|
|
+ panorama_path: Optional[str] = None # 标记后的全景图路径
|
|
|
+ panorama_original_path: Optional[str] = None # 未标记的全景图路径
|
|
|
+ panorama_oss_url: Optional[str] = None # 标记后全景图 OSS URL
|
|
|
+ panorama_oss_url_original: Optional[str] = None # 未标记全景图 OSS URL
|
|
|
persons: List[PersonInfo] = field(default_factory=list)
|
|
|
total_persons: int = 0
|
|
|
ptz_images_count: int = 0
|
|
|
@@ -61,26 +65,40 @@ class PairedImageSaver:
|
|
|
6. 生成 batch_info.json
|
|
|
"""
|
|
|
|
|
|
- def __init__(self, base_dir: str = '/home/admin/dsh/paired_images',
|
|
|
- time_window: float = 5.0, # 时间窗口(秒)
|
|
|
- max_batches: int = 100,
|
|
|
+ def __init__(self, base_dir: str = None,
|
|
|
+ time_window: float = None,
|
|
|
+ max_batches: int = None,
|
|
|
+ cleanup_enabled: bool = None,
|
|
|
+ retention_days: int = None,
|
|
|
enable_oss: bool = False,
|
|
|
oss_uploader = None,
|
|
|
device_config: Dict = None):
|
|
|
"""
|
|
|
初始化
|
|
|
-
|
|
|
+
|
|
|
Args:
|
|
|
- base_dir: 基础保存目录
|
|
|
+ base_dir: 基础保存目录(默认从配置读取)
|
|
|
time_window: 批次时间窗口(秒),同一窗口内的检测归为一批
|
|
|
max_batches: 最大保留批次数量
|
|
|
+ cleanup_enabled: 是否启用自动清理
|
|
|
+ retention_days: 保留天数
|
|
|
enable_oss: 是否启用 OSS 上传
|
|
|
oss_uploader: OSS 上传器实例
|
|
|
device_config: 设备配置字典
|
|
|
"""
|
|
|
- self.base_dir = Path(base_dir)
|
|
|
- self.time_window = time_window
|
|
|
- self.max_batches = max_batches
|
|
|
+ # 从配置模块读取配对图片保存配置
|
|
|
+ try:
|
|
|
+ from config import PAIRED_IMAGE_CONFIG
|
|
|
+ config = PAIRED_IMAGE_CONFIG
|
|
|
+ except ImportError:
|
|
|
+ config = {}
|
|
|
+
|
|
|
+ # 使用传入参数或配置默认值
|
|
|
+ self.base_dir = Path(base_dir or config.get('base_dir', '/home/admin/dsh/paired_images'))
|
|
|
+ self.time_window = time_window or config.get('time_window', 5.0)
|
|
|
+ self.max_batches = max_batches if max_batches is not None else config.get('max_batches', 100)
|
|
|
+ self.cleanup_enabled = cleanup_enabled if cleanup_enabled is not None else config.get('cleanup_enabled', True)
|
|
|
+ self.retention_days = retention_days if retention_days is not None else config.get('retention_days', 7)
|
|
|
|
|
|
# 从配置模块读取 OSS 和设备配置(确保即使外部不传也能正确配置)
|
|
|
try:
|
|
|
@@ -147,7 +165,8 @@ class PairedImageSaver:
|
|
|
# 确保目录存在
|
|
|
self._ensure_base_dir()
|
|
|
|
|
|
- logger.info(f"[配对保存] 初始化完成: 目录={base_dir}, 时间窗口={time_window}s, OSS={enable_oss}")
|
|
|
+ logger.info(f"[配对保存] 初始化完成: 目录={self.base_dir}, 时间窗口={self.time_window}s, "
|
|
|
+ f"最大批次={self.max_batches}, 清理={self.cleanup_enabled}, 保留天数={self.retention_days}, OSS={enable_oss}")
|
|
|
|
|
|
def _ensure_base_dir(self):
|
|
|
"""确保基础目录存在"""
|
|
|
@@ -192,15 +211,16 @@ class PairedImageSaver:
|
|
|
# 创建新批次
|
|
|
batch_id = self._generate_batch_id()
|
|
|
batch_dir = self._create_batch_dir(batch_id)
|
|
|
-
|
|
|
- # 保存全景图片
|
|
|
- panorama_path = None
|
|
|
+
|
|
|
+ # 保存全景图片(原图和标记图)
|
|
|
+ panorama_original_path = None
|
|
|
+ panorama_marked_path = None
|
|
|
if panorama_frame is not None:
|
|
|
- panorama_path = self._save_panorama_image(
|
|
|
+ panorama_original_path, panorama_marked_path = self._save_panorama_image(
|
|
|
batch_dir, batch_id, panorama_frame, persons
|
|
|
)
|
|
|
-
|
|
|
- # 创建人员信息(轨迹追踪已禁用,使用序号代替track_id)
|
|
|
+
|
|
|
+ # 创建人员信息
|
|
|
person_infos = []
|
|
|
for i, p in enumerate(persons):
|
|
|
info = PersonInfo(
|
|
|
@@ -210,17 +230,18 @@ class PairedImageSaver:
|
|
|
confidence=p.get('confidence', 0.0)
|
|
|
)
|
|
|
person_infos.append(info)
|
|
|
-
|
|
|
+
|
|
|
# 获取设备信息
|
|
|
device_id = self.device_config.get('device_id', 'UNKNOWN')
|
|
|
project_id = self.device_config.get('project_id', 'UNKNOWN')
|
|
|
-
|
|
|
+
|
|
|
# 创建批次记录
|
|
|
self._current_batch = DetectionBatch(
|
|
|
batch_id=batch_id,
|
|
|
timestamp=current_time,
|
|
|
panorama_image=panorama_frame,
|
|
|
- panorama_path=panorama_path,
|
|
|
+ panorama_path=panorama_marked_path, # 标记图作为主路径
|
|
|
+ panorama_original_path=panorama_original_path, # 原图路径
|
|
|
persons=person_infos,
|
|
|
total_persons=len(persons),
|
|
|
device_id=device_id,
|
|
|
@@ -245,51 +266,61 @@ class PairedImageSaver:
|
|
|
f"[配对保存] 新批次创建: {batch_id}, "
|
|
|
f"人员={len(persons)}, 目录={batch_dir}"
|
|
|
)
|
|
|
-
|
|
|
- # 上传全景图到 OSS
|
|
|
- logger.info(f"[配对保存] 开始新批次: enable_oss={self.enable_oss}, uploader={self.oss_uploader}, path={panorama_path}")
|
|
|
- if self.enable_oss and panorama_path and self.oss_uploader:
|
|
|
- logger.info(f"[配对保存] 准备上传全景图到 OSS: {panorama_path}")
|
|
|
- self._upload_panorama_to_oss(batch_id, panorama_path)
|
|
|
+
|
|
|
+ # 上传全景图到 OSS(原图和标记图)
|
|
|
+ logger.info(f"[配对保存] 开始新批次: enable_oss={self.enable_oss}, uploader={self.oss_uploader}")
|
|
|
+ if self.enable_oss and self.oss_uploader:
|
|
|
+ # 上传原图
|
|
|
+ if panorama_original_path:
|
|
|
+ logger.info(f"[配对保存] 准备上传全景原图到 OSS: {panorama_original_path}")
|
|
|
+ self._upload_panorama_to_oss(batch_id, panorama_original_path, image_type='panorama_original')
|
|
|
+ # 上传标记图
|
|
|
+ if panorama_marked_path:
|
|
|
+ logger.info(f"[配对保存] 准备上传全景标记图到 OSS: {panorama_marked_path}")
|
|
|
+ self._upload_panorama_to_oss(batch_id, panorama_marked_path, image_type='panorama')
|
|
|
else:
|
|
|
logger.warning(f"[配对保存] OSS未启用或上传器不可用: enable_oss={self.enable_oss}, uploader={self.oss_uploader}")
|
|
|
|
|
|
return batch_id
|
|
|
|
|
|
def _save_panorama_image(self, batch_dir: Path, batch_id: str,
|
|
|
- frame, persons: List[Dict]) -> Optional[str]:
|
|
|
+ frame, persons: List[Dict]) -> Tuple[Optional[str], Optional[str]]:
|
|
|
"""
|
|
|
- 保存全景标记图片
|
|
|
-
|
|
|
+ 保存全景原图和标记图片
|
|
|
+
|
|
|
Args:
|
|
|
batch_dir: 批次目录
|
|
|
batch_id: 批次ID
|
|
|
frame: 全景帧
|
|
|
persons: 人员列表(已由调用方过滤,此处不再过滤)
|
|
|
-
|
|
|
+
|
|
|
Returns:
|
|
|
- 保存路径或 None
|
|
|
+ (原图路径, 标记图路径) 或 (None, None)
|
|
|
"""
|
|
|
try:
|
|
|
+ # 保存原图(未标记)
|
|
|
+ original_filename = f"00_panorama_original_n{len(persons)}.png"
|
|
|
+ original_filepath = batch_dir / original_filename
|
|
|
+ cv2.imwrite(str(original_filepath), frame)
|
|
|
+
|
|
|
# 复制图像避免修改原图
|
|
|
marked_frame = frame.copy()
|
|
|
-
|
|
|
+
|
|
|
# 绘制每个人员的标记(使用连续的序号)
|
|
|
- # 注意:persons 已由调用方(coordinator)过滤,置信度均 >= 阈值
|
|
|
for i, person in enumerate(persons):
|
|
|
bbox = person.get('bbox', (0, 0, 0, 0))
|
|
|
x1, y1, x2, y2 = bbox
|
|
|
conf = person.get('confidence', 0.0)
|
|
|
-
|
|
|
+
|
|
|
# 绘制边界框(绿色)
|
|
|
cv2.rectangle(marked_frame, (x1, y1), (x2, y2), (0, 255, 0), 2)
|
|
|
-
|
|
|
+
|
|
|
# 绘制序号标签(带置信度)
|
|
|
label = f"person_{i}({conf:.2f})"
|
|
|
(label_w, label_h), baseline = cv2.getTextSize(
|
|
|
label, cv2.FONT_HERSHEY_SIMPLEX, 0.8, 2
|
|
|
)
|
|
|
-
|
|
|
+
|
|
|
# 标签背景
|
|
|
cv2.rectangle(
|
|
|
marked_frame,
|
|
|
@@ -298,7 +329,7 @@ class PairedImageSaver:
|
|
|
(0, 255, 0),
|
|
|
-1
|
|
|
)
|
|
|
-
|
|
|
+
|
|
|
# 标签文字(黑色)
|
|
|
cv2.putText(
|
|
|
marked_frame, label,
|
|
|
@@ -306,26 +337,25 @@ class PairedImageSaver:
|
|
|
cv2.FONT_HERSHEY_SIMPLEX, 0.8,
|
|
|
(0, 0, 0), 2
|
|
|
)
|
|
|
-
|
|
|
- # 保存图片(使用人员数量)
|
|
|
- filename = f"00_panorama_n{len(persons)}.png"
|
|
|
- filepath = batch_dir / filename
|
|
|
- # 保存全景图(PNG无损格式,不压缩)
|
|
|
- cv2.imwrite(str(filepath), marked_frame)
|
|
|
-
|
|
|
- logger.info(f"[配对保存] 全景图已保存: {filepath},人员数量 {len(persons)}")
|
|
|
- return str(filepath)
|
|
|
-
|
|
|
+
|
|
|
+ # 保存标记图
|
|
|
+ marked_filename = f"00_panorama_marked_n{len(persons)}.png"
|
|
|
+ marked_filepath = batch_dir / marked_filename
|
|
|
+ cv2.imwrite(str(marked_filepath), marked_frame)
|
|
|
+
|
|
|
+ logger.info(f"[配对保存] 全景图已保存: 原图={original_filepath}, 标记图={marked_filepath}, 人员数量 {len(persons)}")
|
|
|
+ return str(original_filepath), str(marked_filepath)
|
|
|
+
|
|
|
except Exception as e:
|
|
|
logger.error(f"[配对保存] 保存全景图失败: {e}")
|
|
|
- return None
|
|
|
+ return None, None
|
|
|
|
|
|
def save_ptz_image(self, batch_id: str, person_index: int,
|
|
|
ptz_frame, ptz_position: Tuple[float, float, int],
|
|
|
ptz_bbox: Tuple[int, int, int, int] = None,
|
|
|
- person_info: Dict = None) -> Optional[str]:
|
|
|
+ person_info: Dict = None) -> Tuple[Optional[str], Optional[str]]:
|
|
|
"""
|
|
|
- 保存球机聚焦图片
|
|
|
+ 保存球机聚焦图片(原图和标记图)
|
|
|
|
|
|
Args:
|
|
|
batch_id: 批次ID
|
|
|
@@ -336,120 +366,112 @@ class PairedImageSaver:
|
|
|
person_info: 额外人员信息
|
|
|
|
|
|
Returns:
|
|
|
- 保存路径或 None
|
|
|
+ (原图路径, 标记图路径) 或 (None, None)
|
|
|
"""
|
|
|
- # 【调试】记录传入的参数
|
|
|
logger.info(f"[配对保存] save_ptz_image: batch={batch_id}, person={person_index}, "
|
|
|
f"PTZ=({ptz_position[0]:.1f}°, {ptz_position[1]:.1f}°, zoom={ptz_position[2]})")
|
|
|
|
|
|
with self._batch_lock:
|
|
|
if self._current_batch is None or self._current_batch.batch_id != batch_id:
|
|
|
- logger.warning(f"[配对保存] 批次不存在或已过期: {batch_id}, current={self._current_batch.batch_id if self._current_batch else None}")
|
|
|
- return None
|
|
|
+ logger.warning(f"[配对保存] 批次不存在或已过期: {batch_id}")
|
|
|
+ return None, None
|
|
|
|
|
|
batch_dir = self.base_dir / f"batch_{batch_id}"
|
|
|
-
|
|
|
+
|
|
|
try:
|
|
|
- # 复制图像
|
|
|
+ # 保存原图(未标记)
|
|
|
+ original_filename = f"01_ptz_person{person_index}_p{int(ptz_position[0])}_t{int(ptz_position[1])}_z{int(ptz_position[2])}_original.png"
|
|
|
+ original_filepath = batch_dir / original_filename
|
|
|
+
|
|
|
+ if ptz_frame is not None:
|
|
|
+ cv2.imwrite(str(original_filepath), ptz_frame)
|
|
|
+
|
|
|
+ # 复制图像用于标记
|
|
|
marked_frame = ptz_frame.copy() if ptz_frame is not None else None
|
|
|
-
|
|
|
+
|
|
|
if marked_frame is not None:
|
|
|
- # 在球机图上添加标记(如果有检测框)
|
|
|
+ # 在球机图上添加标记
|
|
|
h, w = marked_frame.shape[:2]
|
|
|
-
|
|
|
- # 添加PTZ位置信息到图片
|
|
|
+
|
|
|
+ # 添加PTZ位置信息
|
|
|
pan, tilt, zoom = ptz_position
|
|
|
info_text = f"PTZ: P={pan:.1f} T={tilt:.1f} Z={zoom}"
|
|
|
- cv2.putText(
|
|
|
- marked_frame, info_text,
|
|
|
- (10, 30),
|
|
|
- cv2.FONT_HERSHEY_SIMPLEX, 0.7,
|
|
|
- (0, 255, 0), 2
|
|
|
- )
|
|
|
-
|
|
|
+ cv2.putText(marked_frame, info_text, (10, 30),
|
|
|
+ cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
|
|
|
+
|
|
|
# 添加人员序号
|
|
|
person_text = f"person_{person_index}"
|
|
|
- cv2.putText(
|
|
|
- marked_frame, person_text,
|
|
|
- (10, 60),
|
|
|
- cv2.FONT_HERSHEY_SIMPLEX, 0.7,
|
|
|
- (0, 255, 0), 2
|
|
|
- )
|
|
|
-
|
|
|
- # 绘制PTZ检测到的bbox(红色)
|
|
|
+ cv2.putText(marked_frame, person_text, (10, 60),
|
|
|
+ cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
|
|
|
+
|
|
|
+ # 绘制PTZ检测到的bbox
|
|
|
if ptz_bbox is not None:
|
|
|
x1, y1, x2, y2 = ptz_bbox
|
|
|
cv2.rectangle(marked_frame, (x1, y1), (x2, y2), (0, 0, 255), 2)
|
|
|
bbox_text = f"PTZ_BBox: ({x1},{y1},{x2},{y2})"
|
|
|
- cv2.putText(
|
|
|
- marked_frame, bbox_text,
|
|
|
- (10, 90),
|
|
|
- cv2.FONT_HERSHEY_SIMPLEX, 0.6,
|
|
|
- (0, 0, 255), 2
|
|
|
- )
|
|
|
-
|
|
|
- # 保存图片
|
|
|
- filename = f"01_ptz_person{person_index}_p{int(ptz_position[0])}_t{int(ptz_position[1])}_z{int(ptz_position[2])}.jpg"
|
|
|
- filepath = batch_dir / filename
|
|
|
-
|
|
|
+ cv2.putText(marked_frame, bbox_text, (10, 90),
|
|
|
+ cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
|
|
|
+
|
|
|
+ # 保存标记图
|
|
|
+ marked_filename = f"01_ptz_person{person_index}_p{int(ptz_position[0])}_t{int(ptz_position[1])}_z{int(ptz_position[2])}_marked.png"
|
|
|
+ marked_filepath = batch_dir / marked_filename
|
|
|
+
|
|
|
if marked_frame is not None:
|
|
|
- cv2.imwrite(str(filepath), marked_frame, [cv2.IMWRITE_JPEG_QUALITY, 90])
|
|
|
-
|
|
|
+ cv2.imwrite(str(marked_filepath), marked_frame)
|
|
|
+
|
|
|
+ logger.info(f"[配对保存] 球机图已保存: 原图={original_filepath}, 标记图={marked_filepath}")
|
|
|
+
|
|
|
# 更新批次信息
|
|
|
- # 【关键修复】只有当人员索引有效时才更新和计数
|
|
|
if person_index < len(self._current_batch.persons):
|
|
|
self._current_batch.persons[person_index].ptz_position = ptz_position
|
|
|
self._current_batch.persons[person_index].ptz_bbox = ptz_bbox
|
|
|
self._current_batch.persons[person_index].ptz_image_saved = True
|
|
|
- self._current_batch.persons[person_index].ptz_image_path = str(filepath)
|
|
|
-
|
|
|
+ self._current_batch.persons[person_index].ptz_image_path = str(marked_filepath)
|
|
|
+ self._current_batch.persons[person_index].ptz_image_original_path = str(original_filepath)
|
|
|
+
|
|
|
self._current_batch.ptz_images_count += 1
|
|
|
-
|
|
|
+
|
|
|
with self._stats_lock:
|
|
|
self._stats['total_ptz_images'] += 1
|
|
|
-
|
|
|
- logger.info(f"[配对保存] 球机图已保存: {filepath}, BBox={ptz_bbox}")
|
|
|
|
|
|
- # 上传球机图到 OSS
|
|
|
- logger.info(f"[配对保存] 准备上传球机图到 OSS: enable_oss={self.enable_oss}, uploader={self.oss_uploader}")
|
|
|
+ # 上传原图和标记图到 OSS
|
|
|
if self.enable_oss and self.oss_uploader:
|
|
|
- self._upload_ptz_to_oss(batch_id, person_index, str(filepath))
|
|
|
- else:
|
|
|
- # 人员索引超出范围,说明批次信息不一致,跳过保存
|
|
|
- logger.warning(f"[配对保存] 人员索引 {person_index} 超出批次范围 {len(self._current_batch.persons)},跳过计数")
|
|
|
-
|
|
|
- return str(filepath)
|
|
|
-
|
|
|
+ self._upload_ptz_to_oss(batch_id, person_index, str(original_filepath), str(marked_filepath))
|
|
|
+
|
|
|
+ return str(original_filepath), str(marked_filepath)
|
|
|
+
|
|
|
except Exception as e:
|
|
|
logger.error(f"[配对保存] 保存球机图失败: {e}")
|
|
|
- return None
|
|
|
+ return None, None
|
|
|
|
|
|
- def _upload_panorama_to_oss(self, batch_id: str, panorama_path: str):
|
|
|
+ def _upload_panorama_to_oss(self, batch_id: str, panorama_path: str, image_type: str = 'panorama'):
|
|
|
"""上传全景图到 OSS"""
|
|
|
- logger.info(f"[OSS] _upload_panorama_to_oss 被调用: batch_id={batch_id}, path={panorama_path}")
|
|
|
+ logger.info(f"[OSS] _upload_panorama_to_oss: batch_id={batch_id}, path={panorama_path}, type={image_type}")
|
|
|
+
|
|
|
+ # 确定是原图还是标记图
|
|
|
+ is_original = image_type == 'panorama_original'
|
|
|
|
|
|
def on_upload_complete(result):
|
|
|
- logger.info(f"[OSS] 全景图上传完成回调: success={result.success}, url={result.oss_url}")
|
|
|
- # 更新上传状态(即使 _current_batch 已切换也能正确记录)
|
|
|
- self._upload_status[batch_id]['panorama'] = True
|
|
|
- self._upload_status[batch_id]['panorama_url'] = result.oss_url
|
|
|
- if self._current_batch and self._current_batch.batch_id == batch_id:
|
|
|
- self._current_batch.panorama_oss_url = result.oss_url
|
|
|
+ logger.info(f"[OSS] 全景图上传完成回调: type={image_type}, url={result.oss_url}")
|
|
|
+ if is_original:
|
|
|
+ self._upload_status[batch_id]['panorama_original'] = True
|
|
|
+ self._upload_status[batch_id]['panorama_original_url'] = result.oss_url
|
|
|
+ if self._current_batch and self._current_batch.batch_id == batch_id:
|
|
|
+ self._current_batch.panorama_oss_url_original = result.oss_url
|
|
|
+ else:
|
|
|
+ self._upload_status[batch_id]['panorama'] = True
|
|
|
+ self._upload_status[batch_id]['panorama_url'] = result.oss_url
|
|
|
+ if self._current_batch and self._current_batch.batch_id == batch_id:
|
|
|
+ self._current_batch.panorama_oss_url = result.oss_url
|
|
|
with self._stats_lock:
|
|
|
self._stats['oss_upload_success'] += 1
|
|
|
- logger.info(f"[OSS] 全景图上传成功: {result.oss_url}")
|
|
|
|
|
|
def on_upload_error(result):
|
|
|
- logger.error(f"[OSS] 全景图上传失败回调: {result.error}")
|
|
|
- self._upload_status[batch_id]['panorama'] = False
|
|
|
- self._upload_status[batch_id]['panorama_url'] = None
|
|
|
+ logger.error(f"[OSS] 全景图上传失败回调: type={image_type}, error={result.error}")
|
|
|
with self._stats_lock:
|
|
|
self._stats['oss_upload_failed'] += 1
|
|
|
- logger.error(f"[OSS] 全景图上传失败: {result.error}")
|
|
|
|
|
|
- # 包装回调,同时处理成功和失败
|
|
|
def on_upload_done(result):
|
|
|
- logger.info(f"[OSS] 全景图上传结果: success={result.success}")
|
|
|
if result.success:
|
|
|
on_upload_complete(result)
|
|
|
else:
|
|
|
@@ -459,54 +481,80 @@ class PairedImageSaver:
|
|
|
oss_key = self.oss_uploader.upload_image(
|
|
|
local_path=panorama_path,
|
|
|
batch_id=batch_id,
|
|
|
- image_type='panorama',
|
|
|
+ image_type=image_type,
|
|
|
callback=on_upload_done
|
|
|
)
|
|
|
- logger.info(f"[OSS] 全景图已加入上传队列: oss_key={oss_key}")
|
|
|
+ logger.info(f"[OSS] 全景图已加入上传队列: type={image_type}, oss_key={oss_key}")
|
|
|
except Exception as e:
|
|
|
logger.error(f"[OSS] 全景图上传异常: {e}")
|
|
|
|
|
|
- def _upload_ptz_to_oss(self, batch_id: str, person_index: int, ptz_path: str):
|
|
|
- """上传球机图到 OSS"""
|
|
|
- logger.info(f"[OSS] _upload_ptz_to_oss 被调用: batch_id={batch_id}, person={person_index}, path={ptz_path}")
|
|
|
-
|
|
|
- def on_upload_complete(result):
|
|
|
- logger.info(f"[OSS] 球机图上传完成回调: person={person_index}, success={result.success}, url={result.oss_url}")
|
|
|
- # 更新上传状态(即使 _current_batch 已切换也能正确记录)
|
|
|
- self._upload_status[batch_id]['ptz'][person_index] = result.oss_url
|
|
|
- if self._current_batch and self._current_batch.batch_id == batch_id:
|
|
|
- if person_index < len(self._current_batch.persons):
|
|
|
- self._current_batch.persons[person_index].ptz_oss_url = result.oss_url
|
|
|
- with self._stats_lock:
|
|
|
- self._stats['oss_upload_success'] += 1
|
|
|
- logger.info(f"[OSS] 球机图上传成功 (person_{person_index}): {result.oss_url}")
|
|
|
-
|
|
|
- def on_upload_error(result):
|
|
|
- logger.error(f"[OSS] 球机图上传失败回调: person={person_index}, error={result.error}")
|
|
|
- self._upload_status[batch_id]['ptz'][person_index] = None
|
|
|
- with self._stats_lock:
|
|
|
- self._stats['oss_upload_failed'] += 1
|
|
|
- logger.error(f"[OSS] 球机图上传失败 (person_{person_index}): {result.error}")
|
|
|
+ def _upload_ptz_to_oss(self, batch_id: str, person_index: int, original_path: str = None, marked_path: str = None):
|
|
|
+ """上传球机图到 OSS(原图和标记图)"""
|
|
|
+ logger.info(f"[OSS] _upload_ptz_to_oss: batch={batch_id}, person={person_index}, original={original_path}, marked={marked_path}")
|
|
|
+
|
|
|
+ # 初始化上传状态
|
|
|
+ if batch_id not in self._upload_status:
|
|
|
+ self._upload_status[batch_id] = {'ptz': {}}
|
|
|
+ if 'ptz_original' not in self._upload_status[batch_id]:
|
|
|
+ self._upload_status[batch_id]['ptz_original'] = {}
|
|
|
+ if 'ptz_marked' not in self._upload_status[batch_id]:
|
|
|
+ self._upload_status[batch_id]['ptz_marked'] = {}
|
|
|
+
|
|
|
+ # 上传原图
|
|
|
+ if original_path:
|
|
|
+ def on_original_complete(result):
|
|
|
+ self._upload_status[batch_id]['ptz_original'][person_index] = result.oss_url
|
|
|
+ if self._current_batch and self._current_batch.batch_id == batch_id:
|
|
|
+ if person_index < len(self._current_batch.persons):
|
|
|
+ self._current_batch.persons[person_index].ptz_oss_url_original = result.oss_url
|
|
|
+ with self._stats_lock:
|
|
|
+ self._stats['oss_upload_success'] += 1
|
|
|
+ logger.info(f"[OSS] 球机原图上传成功: {result.oss_url}")
|
|
|
+
|
|
|
+ def on_original_done(result):
|
|
|
+ if result.success:
|
|
|
+ on_original_complete(result)
|
|
|
+ else:
|
|
|
+ logger.error(f"[OSS] 球机原图上传失败: {result.error}")
|
|
|
|
|
|
- # 包装回调,同时处理成功和失败
|
|
|
- def on_upload_done(result):
|
|
|
- logger.info(f"[OSS] 球机图上传结果: person={person_index}, success={result.success}")
|
|
|
- if result.success:
|
|
|
- on_upload_complete(result)
|
|
|
- else:
|
|
|
- on_upload_error(result)
|
|
|
+ try:
|
|
|
+ self.oss_uploader.upload_image(
|
|
|
+ local_path=original_path,
|
|
|
+ batch_id=batch_id,
|
|
|
+ image_type='ptz_original',
|
|
|
+ person_index=person_index,
|
|
|
+ callback=on_original_done
|
|
|
+ )
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"[OSS] 球机原图上传异常: {e}")
|
|
|
+
|
|
|
+ # 上传标记图
|
|
|
+ if marked_path:
|
|
|
+ def on_marked_complete(result):
|
|
|
+ self._upload_status[batch_id]['ptz_marked'][person_index] = result.oss_url
|
|
|
+ if self._current_batch and self._current_batch.batch_id == batch_id:
|
|
|
+ if person_index < len(self._current_batch.persons):
|
|
|
+ self._current_batch.persons[person_index].ptz_oss_url = result.oss_url
|
|
|
+ with self._stats_lock:
|
|
|
+ self._stats['oss_upload_success'] += 1
|
|
|
+ logger.info(f"[OSS] 球机标记图上传成功: {result.oss_url}")
|
|
|
+
|
|
|
+ def on_marked_done(result):
|
|
|
+ if result.success:
|
|
|
+ on_marked_complete(result)
|
|
|
+ else:
|
|
|
+ logger.error(f"[OSS] 球机标记图上传失败: {result.error}")
|
|
|
|
|
|
- try:
|
|
|
- oss_key = self.oss_uploader.upload_image(
|
|
|
- local_path=ptz_path,
|
|
|
- batch_id=batch_id,
|
|
|
- image_type='ptz',
|
|
|
- person_index=person_index,
|
|
|
- callback=on_upload_done
|
|
|
- )
|
|
|
- logger.info(f"[OSS] 球机图已加入上传队列: person={person_index}, oss_key={oss_key}")
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"[OSS] 球机图上传异常: {e}")
|
|
|
+ try:
|
|
|
+ self.oss_uploader.upload_image(
|
|
|
+ local_path=marked_path,
|
|
|
+ batch_id=batch_id,
|
|
|
+ image_type='ptz',
|
|
|
+ person_index=person_index,
|
|
|
+ callback=on_marked_done
|
|
|
+ )
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"[OSS] 球机标记图上传异常: {e}")
|
|
|
|
|
|
def _finalize_batch(self, batch: DetectionBatch):
|
|
|
"""完成批次处理"""
|
|
|
@@ -577,17 +625,19 @@ class PairedImageSaver:
|
|
|
Returns:
|
|
|
Dict: 批次信息字典
|
|
|
"""
|
|
|
- # 从上传状态获取最新的 OSS URL(异步回调可能已更新)
|
|
|
+ # 从上传状态获取最新的 OSS URL
|
|
|
upload_status = self._upload_status.get(batch.batch_id, {})
|
|
|
|
|
|
- # 获取全景图 OSS URL(优先使用回调更新的状态)
|
|
|
+ # 获取全景图 OSS URL
|
|
|
panorama_oss_url = upload_status.get('panorama_url', batch.panorama_oss_url)
|
|
|
+ panorama_oss_url_original = upload_status.get('panorama_original_url', batch.panorama_oss_url_original)
|
|
|
|
|
|
# 人员信息列表
|
|
|
persons_list = []
|
|
|
for person in batch.persons:
|
|
|
- # 获取球机图 OSS URL(优先使用回调更新的状态)
|
|
|
+ # 获取球机图 OSS URL
|
|
|
ptz_oss_url = upload_status.get('ptz', {}).get(person.person_index, person.ptz_oss_url)
|
|
|
+ ptz_oss_url_original = upload_status.get('ptz_original', {}).get(person.person_index, person.ptz_oss_url_original)
|
|
|
|
|
|
person_data = {
|
|
|
'person_index': person.person_index,
|
|
|
@@ -614,8 +664,10 @@ class PairedImageSaver:
|
|
|
'y2': person.ptz_bbox[3]
|
|
|
} if person.ptz_bbox else None,
|
|
|
'ptz_image_saved': person.ptz_image_saved,
|
|
|
- 'ptz_image_path': person.ptz_image_path,
|
|
|
- 'ptz_oss_url': ptz_oss_url
|
|
|
+ 'ptz_image_path': person.ptz_image_path, # 标记图
|
|
|
+ 'ptz_image_original_path': person.ptz_image_original_path, # 原图
|
|
|
+ 'ptz_oss_url': ptz_oss_url, # 标记图 OSS URL
|
|
|
+ 'ptz_oss_url_original': ptz_oss_url_original # 原图 OSS URL
|
|
|
}
|
|
|
persons_list.append(person_data)
|
|
|
|
|
|
@@ -629,12 +681,15 @@ class PairedImageSaver:
|
|
|
'total_persons': batch.total_persons,
|
|
|
'ptz_images_count': batch.ptz_images_count,
|
|
|
'panorama': {
|
|
|
- 'local_path': batch.panorama_path,
|
|
|
- 'oss_url': panorama_oss_url
|
|
|
+ 'local_path': batch.panorama_path, # 标记图
|
|
|
+ 'local_path_original': batch.panorama_original_path, # 原图
|
|
|
+ 'oss_url': panorama_oss_url, # 标记图 OSS URL
|
|
|
+ 'oss_url_original': panorama_oss_url_original # 原图 OSS URL
|
|
|
},
|
|
|
'persons': persons_list,
|
|
|
'upload_status': {
|
|
|
'panorama_uploaded': panorama_oss_url is not None,
|
|
|
+ 'panorama_original_uploaded': panorama_oss_url_original is not None,
|
|
|
'all_ptz_uploaded': all(
|
|
|
upload_status.get('ptz', {}).get(p.person_index) is not None
|
|
|
for p in batch.persons if p.ptz_image_saved
|
|
|
@@ -676,19 +731,47 @@ class PairedImageSaver:
|
|
|
|
|
|
def _cleanup_old_batches(self):
|
|
|
"""清理旧批次目录"""
|
|
|
+ if not self.cleanup_enabled:
|
|
|
+ logger.debug("[配对保存] 自动清理已禁用")
|
|
|
+ return
|
|
|
+
|
|
|
try:
|
|
|
+ from datetime import timedelta
|
|
|
+
|
|
|
batch_dirs = sorted(
|
|
|
[d for d in self.base_dir.iterdir() if d.is_dir() and d.name.startswith('batch_')],
|
|
|
key=lambda x: x.stat().st_mtime
|
|
|
)
|
|
|
-
|
|
|
- if len(batch_dirs) > self.max_batches:
|
|
|
- to_delete = batch_dirs[:len(batch_dirs) - self.max_batches]
|
|
|
- for d in to_delete:
|
|
|
- import shutil
|
|
|
- shutil.rmtree(d)
|
|
|
- logger.info(f"[配对保存] 清理旧批次: {d.name}")
|
|
|
-
|
|
|
+
|
|
|
+ if not batch_dirs:
|
|
|
+ return
|
|
|
+
|
|
|
+ now = time.time()
|
|
|
+ to_delete = []
|
|
|
+ deleted_count = 0
|
|
|
+
|
|
|
+ for d in batch_dirs:
|
|
|
+ # 1. 按数量清理(优先)
|
|
|
+ if len(batch_dirs) - deleted_count > self.max_batches:
|
|
|
+ to_delete.append(d)
|
|
|
+ continue
|
|
|
+
|
|
|
+ # 2. 按天数清理
|
|
|
+ if self.retention_days > 0:
|
|
|
+ age_days = (now - d.stat().st_mtime) / 86400
|
|
|
+ if age_days > self.retention_days:
|
|
|
+ to_delete.append(d)
|
|
|
+
|
|
|
+ # 执行删除
|
|
|
+ for d in to_delete:
|
|
|
+ import shutil
|
|
|
+ shutil.rmtree(d)
|
|
|
+ deleted_count += 1
|
|
|
+ logger.info(f"[配对保存] 清理旧批次: {d.name}")
|
|
|
+
|
|
|
+ if deleted_count > 0:
|
|
|
+ logger.info(f"[配对保存] 共清理 {deleted_count} 个旧批次")
|
|
|
+
|
|
|
except Exception as e:
|
|
|
logger.error(f"[配对保存] 清理旧批次失败: {e}")
|
|
|
|
|
|
@@ -747,30 +830,38 @@ class PairedImageSaver:
|
|
|
_paired_saver_instance: Optional[PairedImageSaver] = None
|
|
|
|
|
|
|
|
|
-def get_paired_saver(base_dir: str = None, time_window: float = 5.0,
|
|
|
+def get_paired_saver(base_dir: str = None, time_window: float = None,
|
|
|
+ max_batches: int = None, cleanup_enabled: bool = None,
|
|
|
+ retention_days: int = None,
|
|
|
enable_oss: bool = False, oss_uploader = None,
|
|
|
device_config: Dict = None) -> PairedImageSaver:
|
|
|
"""
|
|
|
获取配对保存管理器实例(单例模式)
|
|
|
-
|
|
|
+
|
|
|
如果实例已存在但缺少 OSS/设备配置,会自动从配置模块更新
|
|
|
-
|
|
|
+
|
|
|
Args:
|
|
|
- base_dir: 基础保存目录
|
|
|
- time_window: 时间窗口
|
|
|
+ base_dir: 基础保存目录(默认从配置读取)
|
|
|
+ time_window: 时间窗口(默认从配置读取)
|
|
|
+ max_batches: 最大保留批次数量(默认从配置读取)
|
|
|
+ cleanup_enabled: 是否启用自动清理(默认从配置读取)
|
|
|
+ retention_days: 保留天数(默认从配置读取)
|
|
|
enable_oss: 是否启用 OSS 上传
|
|
|
oss_uploader: OSS 上传器实例
|
|
|
device_config: 设备配置字典
|
|
|
-
|
|
|
+
|
|
|
Returns:
|
|
|
PairedImageSaver 实例
|
|
|
"""
|
|
|
global _paired_saver_instance
|
|
|
-
|
|
|
+
|
|
|
if _paired_saver_instance is None:
|
|
|
_paired_saver_instance = PairedImageSaver(
|
|
|
- base_dir=base_dir or '/home/admin/dsh/paired_images',
|
|
|
+ base_dir=base_dir,
|
|
|
time_window=time_window,
|
|
|
+ max_batches=max_batches,
|
|
|
+ cleanup_enabled=cleanup_enabled,
|
|
|
+ retention_days=retention_days,
|
|
|
enable_oss=enable_oss,
|
|
|
oss_uploader=oss_uploader,
|
|
|
device_config=device_config
|
|
|
@@ -786,7 +877,7 @@ def get_paired_saver(base_dir: str = None, time_window: float = 5.0,
|
|
|
if device_config is not None:
|
|
|
_paired_saver_instance.device_config.update(device_config)
|
|
|
logger.info(f"[配对保存] 更新设备配置: {device_config}")
|
|
|
-
|
|
|
+
|
|
|
return _paired_saver_instance
|
|
|
|
|
|
|