1. 新增concurrently依赖用于并行启动服务 2. 新增服务器启动脚本统一管理环境变量和虚拟环境 3. 新增PaddlePaddle推理引擎和配套工具代码 4. 新增抽烟检测Paddle模型支持,完善模型管理 5. 重构开发启动脚本,优化开发体验 6. 更新.gitignore排除不必要的外部目录和缓存 7. 完善文档说明,新增PaddlePaddle部署指南
404 lines
14 KiB
Python
404 lines
14 KiB
Python
"""
|
||
PaddleDetection 抽烟检测服务适配器
|
||
使用本地 PaddlePaddle 环境直接调用模型(无需 Docker)
|
||
"""
|
||
|
||
# 禁用 PIR API 以支持旧版模型格式(必须在任何导入之前设置)
|
||
import os
|
||
os.environ['FLAGS_enable_pir_api'] = '0'
|
||
|
||
import cv2
|
||
import numpy as np
|
||
import logging
|
||
import threading
|
||
import time
|
||
import sys
|
||
from typing import Dict, List, Optional
|
||
from pathlib import Path
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class PaddleDetectionService:
|
||
"""PaddleDetection 服务适配器(本地模式)"""
|
||
|
||
def __init__(self):
|
||
self.model_name = "smoking_detection"
|
||
self.threshold = 0.1
|
||
self._lock = threading.Lock()
|
||
|
||
# 本地环境配置
|
||
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
|
||
self.paddle_dir = os.path.join(project_root, "third-party", "paddle-inference")
|
||
self.model_dir = os.path.join(project_root, "models", "smoking_detection_paddle")
|
||
|
||
# 检测器实例(延迟加载)
|
||
self._detector = None
|
||
self._detector_initialized = False
|
||
|
||
self.available = True
|
||
logger.info(f"本地 PaddlePaddle 模式已启用")
|
||
logger.info(f"模型目录: {self.model_dir}")
|
||
logger.info(f"使用服务器虚拟环境中的 PaddlePaddle")
|
||
logger.info(f"PaddlePaddle 目录: {self.paddle_dir}")
|
||
|
||
# 禁用 PIR API 以支持旧版模型格式(必须在初始化前设置)
|
||
os.environ['FLAGS_enable_pir_api'] = '0'
|
||
|
||
# 检测系统架构
|
||
import platform
|
||
self.platform_info = platform.uname()
|
||
self.is_apple_silicon = self.platform_info.machine in ('arm64', 'aarch64') and self.platform_info.system == 'Darwin'
|
||
|
||
if self.is_apple_silicon:
|
||
logger.info("✅ 检测到 Apple Silicon (ARM64) 架构")
|
||
logger.info("✅ 使用本地 PaddlePaddle 环境获得最佳性能")
|
||
logger.info("✅ 相比 Docker 方式性能提升 5-10 倍")
|
||
|
||
try:
|
||
self._initialize_environment()
|
||
except Exception as e:
|
||
logger.error(f"初始化环境失败: {e}")
|
||
self.available = False
|
||
|
||
def _initialize_environment(self):
|
||
"""初始化本地 PaddlePaddle 环境"""
|
||
try:
|
||
# 添加 PaddleDetection 部署路径
|
||
paddle_detection_path = self.paddle_dir
|
||
if paddle_detection_path not in sys.path:
|
||
sys.path.insert(0, paddle_detection_path)
|
||
logger.info(f"✅ 添加 PaddleDetection 路径: {paddle_detection_path}")
|
||
|
||
# 检查模型目录是否存在
|
||
if not os.path.exists(self.model_dir):
|
||
raise Exception(f"模型目录不存在: {self.model_dir}")
|
||
|
||
# 检查必要文件
|
||
required_files = ['model.pdmodel', 'model.pdiparams', 'infer_cfg.yml']
|
||
for file in required_files:
|
||
file_path = os.path.join(self.model_dir, file)
|
||
if not os.path.exists(file_path):
|
||
raise Exception(f"模型文件不存在: {file}")
|
||
|
||
logger.info("✅ 环境检查通过")
|
||
|
||
# 预加载检测器(可选,用于首次检测预热)
|
||
try:
|
||
self._get_detector()
|
||
logger.info("✅ 检测器预加载成功")
|
||
except Exception as e:
|
||
logger.warning(f"检测器预加载失败,将在首次使用时初始化: {e}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"环境初始化失败: {e}")
|
||
raise
|
||
|
||
def _get_detector(self):
|
||
"""获取检测器实例(单例模式)"""
|
||
if self._detector is None or not self._detector_initialized:
|
||
try:
|
||
# 设置环境变量以支持旧版模型格式
|
||
os.environ['FLAGS_enable_pir_api'] = '0'
|
||
|
||
# 添加 PaddleDetection 路径(直接使用 self.paddle_dir)
|
||
if self.paddle_dir not in sys.path:
|
||
sys.path.insert(0, self.paddle_dir)
|
||
logger.info(f"添加 PaddleDetection 路径: {self.paddle_dir}")
|
||
|
||
# 导入 PaddleDetection 模块
|
||
from infer import Detector, PredictConfig
|
||
|
||
# 创建检测器
|
||
self._detector = Detector(
|
||
model_dir=self.model_dir,
|
||
device='CPU',
|
||
run_mode='paddle',
|
||
batch_size=1,
|
||
output_dir='output',
|
||
threshold=self.threshold
|
||
)
|
||
|
||
self._detector_initialized = True
|
||
logger.info("✅ PaddlePaddle 检测器初始化成功")
|
||
|
||
except Exception as e:
|
||
logger.error(f"检测器初始化失败: {e}")
|
||
raise
|
||
|
||
return self._detector
|
||
|
||
def detect_image(self, image: np.ndarray, threshold: float = None) -> Dict:
|
||
"""
|
||
检测图片中的抽烟行为(本地模式)
|
||
|
||
Args:
|
||
image: OpenCV 图片 (BGR格式)
|
||
threshold: 置信度阈值,如果为 None 则使用默认值
|
||
|
||
Returns:
|
||
检测结果字典
|
||
"""
|
||
if threshold is None:
|
||
threshold = self.threshold
|
||
|
||
if not self.available:
|
||
return {
|
||
'success': False,
|
||
'message': 'PaddleDetection 服务不可用',
|
||
'detections': [],
|
||
'stats': None
|
||
}
|
||
|
||
try:
|
||
with self._lock:
|
||
start_time = time.time()
|
||
|
||
# 确保检测器已初始化
|
||
detector = self._get_detector()
|
||
|
||
# 准备输入图片
|
||
if not isinstance(image, np.ndarray):
|
||
raise Exception(f"不支持的图片类型: {type(image)}")
|
||
|
||
if len(image.shape) == 2: # 灰度图转 BGR
|
||
image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
|
||
elif image.shape[2] == 4: # RGBA 转 BGR
|
||
image = cv2.cvtColor(image, cv2.COLOR_RGBA2BGR)
|
||
|
||
# 执行推理
|
||
inference_start = time.time()
|
||
|
||
# 使用 PaddleDetection API 进行推理
|
||
results = detector.predict_image(
|
||
[image],
|
||
visual=False,
|
||
save_results=False
|
||
)
|
||
|
||
inference_time = time.time() - inference_start
|
||
logger.info(f"推理耗时: {inference_time:.3f}s")
|
||
|
||
# 解析检测结果
|
||
detections = self._parse_detection_results(results, threshold)
|
||
|
||
total_time = time.time() - start_time
|
||
logger.info(f"检测总耗时: {total_time:.3f}s")
|
||
|
||
return {
|
||
'success': True,
|
||
'message': '检测完成',
|
||
'detections': detections,
|
||
'stats': {
|
||
'total_detections': len(detections),
|
||
'model_used': 'ppyoloe_crn_s_80e_smoking_visdrone',
|
||
'threshold': threshold,
|
||
'processing_time': round(total_time, 3),
|
||
'inference_time': round(inference_time, 3)
|
||
}
|
||
}
|
||
|
||
except Exception as e:
|
||
import traceback
|
||
logger.error(f"检测失败: {e}")
|
||
logger.error(f"错误堆栈: {traceback.format_exc()}")
|
||
|
||
# 重置检测器状态以允许重试
|
||
self._detector_initialized = False
|
||
|
||
return {
|
||
'success': False,
|
||
'message': f'检测失败: {e}',
|
||
'detections': [],
|
||
'stats': None
|
||
}
|
||
|
||
def _parse_detection_results(self, results: Dict, threshold: float) -> List[Dict]:
|
||
"""解析 PaddleDetection 返回的检测结果"""
|
||
detections = []
|
||
|
||
try:
|
||
if results and 'boxes' in results:
|
||
boxes = results['boxes']
|
||
|
||
if boxes is not None and len(boxes) > 0:
|
||
for box in boxes:
|
||
# 解析检测结果格式: [class_id, score, x1, y1, x2, y2]
|
||
if len(box) >= 6:
|
||
class_id = int(box[0])
|
||
confidence = float(box[1])
|
||
x1, y1, x2, y2 = float(box[2]), float(box[3]), float(box[4]), float(box[5])
|
||
|
||
# 过滤低置信度检测
|
||
if confidence >= threshold:
|
||
detections.append({
|
||
'class': 'cigarette',
|
||
'label': '香烟',
|
||
'confidence': round(confidence, 3),
|
||
'bbox': [int(x1), int(y1), int(x2), int(y2)]
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"解析检测结果失败: {e}")
|
||
import traceback
|
||
logger.error(traceback.format_exc())
|
||
|
||
return detections
|
||
|
||
def get_performance_info(self) -> Dict:
|
||
"""获取性能信息"""
|
||
return {
|
||
'mode': 'local',
|
||
'environment': 'PaddlePaddle',
|
||
'model_dir': self.model_dir,
|
||
'apple_silicon': self.is_apple_silicon,
|
||
'detector_loaded': self._detector_initialized,
|
||
'available': self.available
|
||
}
|
||
|
||
|
||
# 兼容性包装,保持与 YOLO 模型相同的接口
|
||
class SmokingDetectionModel:
|
||
"""抽烟检测模型包装器,兼容 YOLO 接口"""
|
||
|
||
def __init__(self):
|
||
self.service = PaddleDetectionService()
|
||
self.names = {0: 'cigarette'}
|
||
|
||
def __call__(self, image, conf=0.1, iou=0.45, verbose=False):
|
||
"""
|
||
模拟 YOLO 模型的调用接口
|
||
|
||
Args:
|
||
image: OpenCV 图片
|
||
conf: 置信度阈值
|
||
iou: IoU 阈值
|
||
verbose: 是否输出详细信息
|
||
|
||
Returns:
|
||
模拟 YOLO 结果的对象
|
||
"""
|
||
result = self.service.detect_image(image, threshold=conf)
|
||
|
||
return [PaddleDetectionResult(result, self.names)]
|
||
|
||
|
||
class PaddleDetectionResult:
|
||
"""模拟 YOLO 检测结果对象"""
|
||
|
||
def __init__(self, detection_result: Dict, names: Dict):
|
||
self.detection_result = detection_result
|
||
self.names = names
|
||
|
||
self.boxes = self._create_boxes()
|
||
|
||
def _create_boxes(self):
|
||
"""创建模拟的 boxes 对象"""
|
||
detections = self.detection_result.get('detections', [])
|
||
|
||
if not detections:
|
||
return MockBoxes([])
|
||
|
||
xyxy = []
|
||
conf = []
|
||
cls = []
|
||
|
||
for det in detections:
|
||
xyxy.append(det['bbox'])
|
||
conf.append(det['confidence'])
|
||
cls.append(0)
|
||
|
||
return MockBoxes(xyxy, conf, cls)
|
||
|
||
|
||
class MockBoxes:
|
||
"""模拟 YOLO boxes 对象"""
|
||
|
||
def __init__(self, xyxy_list, conf_list=None, cls_list=None):
|
||
try:
|
||
import torch
|
||
use_torch = True
|
||
except ImportError:
|
||
use_torch = False
|
||
|
||
if xyxy_list and len(xyxy_list) > 0:
|
||
if use_torch:
|
||
self.xyxy = torch.tensor(xyxy_list, dtype=torch.float32)
|
||
self.conf = torch.tensor(conf_list, dtype=torch.float32).reshape(-1, 1)
|
||
self.cls = torch.tensor(cls_list, dtype=torch.int64).reshape(-1, 1)
|
||
else:
|
||
self.xyxy = np.array(xyxy_list, dtype=np.float32)
|
||
self.conf = np.array(conf_list, dtype=np.float32).reshape(-1, 1)
|
||
self.cls = np.array(cls_list, dtype=np.int64).reshape(-1, 1)
|
||
else:
|
||
if use_torch:
|
||
self.xyxy = torch.empty((0, 4), dtype=torch.float32)
|
||
self.conf = torch.empty((0, 1), dtype=torch.float32)
|
||
self.cls = torch.empty((0, 1), dtype=torch.int64)
|
||
else:
|
||
self.xyxy = np.array([]).reshape(0, 4)
|
||
self.conf = np.array([]).reshape(0, 1)
|
||
self.cls = np.array([]).reshape(0, 1)
|
||
|
||
self._use_torch = use_torch
|
||
|
||
def __iter__(self):
|
||
for i in range(len(self.xyxy)):
|
||
yield MockBox(
|
||
self.xyxy[i],
|
||
self.conf[i][0] if len(self.conf) > i else 0.0,
|
||
self.cls[i][0] if len(self.cls) > i else 0
|
||
)
|
||
|
||
def __len__(self):
|
||
return len(self.xyxy)
|
||
|
||
def cpu(self):
|
||
return self
|
||
|
||
def numpy(self):
|
||
if self._use_torch:
|
||
if len(self.xyxy) > 0:
|
||
return (
|
||
self.xyxy.numpy(),
|
||
self.conf.numpy(),
|
||
self.cls.numpy()
|
||
)
|
||
else:
|
||
return (
|
||
np.array([]).reshape(0, 4),
|
||
np.array([]).reshape(0, 1),
|
||
np.array([], dtype=np.int64).reshape(0, 1)
|
||
)
|
||
else:
|
||
return (
|
||
self.xyxy,
|
||
self.conf,
|
||
self.cls
|
||
)
|
||
|
||
|
||
class MockBox:
|
||
"""模拟单个 YOLO box 对象"""
|
||
|
||
def __init__(self, xyxy, conf, cls):
|
||
try:
|
||
import torch
|
||
use_torch = True
|
||
except ImportError:
|
||
use_torch = False
|
||
|
||
if use_torch:
|
||
if isinstance(xyxy, torch.Tensor):
|
||
self.xyxy = xyxy
|
||
else:
|
||
self.xyxy = torch.tensor(xyxy, dtype=torch.float32)
|
||
else:
|
||
if isinstance(xyxy, np.ndarray):
|
||
self.xyxy = xyxy
|
||
else:
|
||
self.xyxy = np.array(xyxy, dtype=np.float32)
|
||
|
||
self.conf = conf
|
||
self.cls = cls
|