feat: 新增PaddlePaddle检测支持,重构项目架构
1. 新增concurrently依赖用于并行启动服务 2. 新增服务器启动脚本统一管理环境变量和虚拟环境 3. 新增PaddlePaddle推理引擎和配套工具代码 4. 新增抽烟检测Paddle模型支持,完善模型管理 5. 重构开发启动脚本,优化开发体验 6. 更新.gitignore排除不必要的外部目录和缓存 7. 完善文档说明,新增PaddlePaddle部署指南
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
"version": "1.0.0",
|
||||
"description": "视频模型检测平台后端服务",
|
||||
"scripts": {
|
||||
"dev": "python main.py",
|
||||
"dev": "./start_server_with_env.sh",
|
||||
"start": "uvicorn main:app --host 0.0.0.0 --port 8000",
|
||||
"lint": "ruff check .",
|
||||
"test": "pytest tests/",
|
||||
|
||||
@@ -4,6 +4,7 @@ import numpy as np
|
||||
import time
|
||||
import uuid
|
||||
import logging
|
||||
import torch
|
||||
from typing import Dict, List, Optional
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
@@ -45,19 +46,60 @@ class DetectionService:
|
||||
|
||||
try:
|
||||
results = model(image, conf=confidence, iou=iou, verbose=False)
|
||||
|
||||
|
||||
detections = []
|
||||
for result in results:
|
||||
boxes = result.boxes
|
||||
|
||||
|
||||
if len(boxes) == 0:
|
||||
logger.info(f"模型 {model_id} 没有检测到目标")
|
||||
continue
|
||||
|
||||
for box in boxes:
|
||||
x1, y1, x2, y2 = box.xyxy[0].cpu().numpy()
|
||||
conf = float(box.conf[0].cpu().numpy())
|
||||
cls = int(box.cls[0].cpu().numpy())
|
||||
class_name = result.names[cls]
|
||||
try:
|
||||
|
||||
if isinstance(box.xyxy, torch.Tensor) and box.xyxy.dim() > 0:
|
||||
x1, y1, x2, y2 = float(box.xyxy[0]), float(box.xyxy[1]), float(box.xyxy[2]), float(box.xyxy[3])
|
||||
elif isinstance(box.xyxy, (list, tuple)):
|
||||
x1, y1, x2, y2 = float(box.xyxy[0]), float(box.xyxy[1]), float(box.xyxy[2]), float(box.xyxy[3])
|
||||
else:
|
||||
continue
|
||||
|
||||
|
||||
|
||||
if isinstance(box.conf, torch.Tensor):
|
||||
if box.conf.dim() == 0:
|
||||
conf = float(box.conf)
|
||||
else:
|
||||
conf = float(box.conf[0])
|
||||
elif hasattr(box.conf, '__getitem__'):
|
||||
conf = float(box.conf[0])
|
||||
else:
|
||||
conf = float(box.conf)
|
||||
|
||||
if isinstance(box.cls, torch.Tensor):
|
||||
if box.cls.dim() == 0:
|
||||
cls = int(box.cls)
|
||||
else:
|
||||
cls = int(box.cls[0])
|
||||
elif hasattr(box.cls, '__getitem__'):
|
||||
cls = int(box.cls[0])
|
||||
else:
|
||||
cls = int(box.cls)
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
logger.error(f"访问 box 属性失败: {e}, box 类型: {type(box)}")
|
||||
logger.error(f"错误堆栈: {traceback.format_exc()}")
|
||||
logger.error(f"box 属性: {vars(box) if hasattr(box, '__dict__') else '无法获取'}")
|
||||
continue
|
||||
|
||||
class_name = result.names[cls]
|
||||
|
||||
label_map = self.model_service.model_configs[model_id]['labels']
|
||||
label = label_map.get(class_name, class_name)
|
||||
|
||||
|
||||
detections.append({
|
||||
'class': class_name,
|
||||
'label': label,
|
||||
@@ -120,21 +162,58 @@ class DetectionService:
|
||||
detections = []
|
||||
for result in results:
|
||||
boxes = result.boxes
|
||||
|
||||
|
||||
for box in boxes:
|
||||
x1, y1, x2, y2 = box.xyxy[0].cpu().numpy()
|
||||
conf = float(box.conf[0].cpu().numpy())
|
||||
cls = int(box.cls[0].cpu().numpy())
|
||||
class_name = result.names[cls]
|
||||
|
||||
label_map = self.model_service.model_configs[model_id]['labels']
|
||||
label = label_map.get(class_name, class_name)
|
||||
|
||||
detections.append({
|
||||
'class': class_name,
|
||||
'label': label,
|
||||
'confidence': round(conf, 3),
|
||||
'bbox': [int(x1), int(y1), int(x2), int(y2)]
|
||||
})
|
||||
try:
|
||||
|
||||
|
||||
if isinstance(box.xyxy, torch.Tensor) and box.xyxy.dim() > 0:
|
||||
x1, y1, x2, y2 = float(box.xyxy[0]), float(box.xyxy[1]), float(box.xyxy[2]), float(box.xyxy[3])
|
||||
elif isinstance(box.xyxy, (list, tuple)):
|
||||
x1, y1, x2, y2 = float(box.xyxy[0]), float(box.xyxy[1]), float(box.xyxy[2]), float(box.xyxy[3])
|
||||
else:
|
||||
continue
|
||||
|
||||
|
||||
if isinstance(box.conf, torch.Tensor):
|
||||
if box.conf.dim() == 0:
|
||||
conf = float(box.conf)
|
||||
else:
|
||||
conf = float(box.conf[0])
|
||||
elif hasattr(box.conf, '__getitem__'):
|
||||
conf = float(box.conf[0])
|
||||
else:
|
||||
conf = float(box.conf)
|
||||
|
||||
if isinstance(box.cls, torch.Tensor):
|
||||
if box.cls.dim() == 0:
|
||||
cls = int(box.cls)
|
||||
else:
|
||||
cls = int(box.cls[0])
|
||||
elif hasattr(box.cls, '__getitem__'):
|
||||
cls = int(box.cls[0])
|
||||
else:
|
||||
cls = int(box.cls)
|
||||
|
||||
|
||||
class_name = result.names[cls]
|
||||
|
||||
label_map = self.model_service.model_configs[model_id]['labels']
|
||||
label = label_map.get(class_name, class_name)
|
||||
|
||||
detections.append({
|
||||
'class': class_name,
|
||||
'label': label,
|
||||
'confidence': round(conf, 3),
|
||||
'bbox': [int(x1), int(y1), int(x2), int(y2)]
|
||||
})
|
||||
except Exception as e:
|
||||
import traceback
|
||||
logger.error(f"VIDEO DEBUG: 访问 box 属性失败: {e}, box 类型: {type(box)}")
|
||||
logger.error(f"VIDEO DEBUG: 错误堆栈: {traceback.format_exc()}")
|
||||
logger.error(f"VIDEO DEBUG: box 属性: {vars(box) if hasattr(box, '__dict__') else '无法获取'}")
|
||||
continue
|
||||
|
||||
processing_time = time.time() - start_time
|
||||
fps = 1.0 / processing_time if processing_time > 0 else 0
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import os
|
||||
import logging
|
||||
from ultralytics import YOLO
|
||||
from typing import Dict, List, Optional
|
||||
from typing import Dict, List, Optional, Union
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ModelService:
|
||||
def __init__(self):
|
||||
self.models: Dict[str, YOLO] = {}
|
||||
self.models: Dict[str, Union[YOLO, object]] = {}
|
||||
# 基础路径:从 apps/server/services/model_service.py 到 jc-video-web 根目录
|
||||
base_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
|
||||
|
||||
@@ -46,7 +46,16 @@ class ModelService:
|
||||
'labels': {'cigarette': '香烟', 'smoke': '烟雾'},
|
||||
'size': '6MB',
|
||||
'description': '基于YOLOv8的抽烟检测模型',
|
||||
'name': '抽烟检测'
|
||||
'name': '抽烟检测 (YOLOv8)'
|
||||
},
|
||||
'smoking_detection_paddle': {
|
||||
'path': os.path.join(base_dir, 'models', 'smoking_detection_paddle', 'model.pdmodel'),
|
||||
'type': 'paddle',
|
||||
'classes': ['cigarette'],
|
||||
'labels': {'cigarette': '香烟'},
|
||||
'size': '27MB',
|
||||
'description': '基于PaddlePaddle PP-YOLOE-s的抽烟检测模型(更高准确率)',
|
||||
'name': '抽烟检测 (Paddle)'
|
||||
},
|
||||
'loitering_detection': {
|
||||
'path': os.path.join(base_dir, 'models', 'loitering_detection', 'yolov8n.pt'),
|
||||
@@ -62,7 +71,21 @@ class ModelService:
|
||||
def get_available_models(self) -> List[Dict]:
|
||||
available_models = []
|
||||
for model_id, config in self.model_configs.items():
|
||||
if os.path.exists(config['path']):
|
||||
model_path = config['path']
|
||||
|
||||
# 检查模型是否存在(Paddle模型检查目录,YOLO模型检查文件)
|
||||
model_exists = False
|
||||
if config['type'] == 'paddle':
|
||||
model_dir = os.path.dirname(model_path)
|
||||
required_files = ['model.pdmodel', 'model.pdiparams', 'infer_cfg.yml']
|
||||
model_exists = all(
|
||||
os.path.exists(os.path.join(model_dir, f))
|
||||
for f in required_files
|
||||
)
|
||||
else:
|
||||
model_exists = os.path.exists(model_path)
|
||||
|
||||
if model_exists:
|
||||
available_models.append({
|
||||
'id': model_id,
|
||||
'name': config['name'],
|
||||
@@ -73,10 +96,10 @@ class ModelService:
|
||||
'type': config['type']
|
||||
})
|
||||
else:
|
||||
logger.warning(f"模型文件不存在: {config['path']}")
|
||||
logger.warning(f"模型文件不存在: {model_path}")
|
||||
return available_models
|
||||
|
||||
async def load_model(self, model_id: str) -> Optional[YOLO]:
|
||||
async def load_model(self, model_id: str) -> Optional[Union[YOLO, object]]:
|
||||
if model_id not in self.model_configs:
|
||||
logger.error(f"未知模型ID: {model_id}")
|
||||
return None
|
||||
@@ -86,6 +109,19 @@ class ModelService:
|
||||
|
||||
config = self.model_configs[model_id]
|
||||
|
||||
# 处理 PaddleDetection 模型
|
||||
if config['type'] == 'paddle':
|
||||
try:
|
||||
from .paddle_detection_service import SmokingDetectionModel
|
||||
logger.info(f"正在加载 PaddlePaddle Docker 服务: {model_id}")
|
||||
model = SmokingDetectionModel()
|
||||
self.models[model_id] = model
|
||||
logger.info(f"PaddlePaddle Docker 服务加载成功: {model_id}")
|
||||
return model
|
||||
except Exception as e:
|
||||
logger.error(f"PaddlePaddle Docker 服务加载失败: {model_id}, 错误: {e}")
|
||||
return None
|
||||
|
||||
# 处理 YOLO 模型
|
||||
model_path = config['path']
|
||||
|
||||
@@ -94,16 +130,16 @@ class ModelService:
|
||||
return None
|
||||
|
||||
try:
|
||||
logger.info(f"正在加载模型: {model_id} from {model_path}")
|
||||
logger.info(f"正在加载 YOLO 模型: {model_id} from {model_path}")
|
||||
model = YOLO(model_path)
|
||||
self.models[model_id] = model
|
||||
logger.info(f"模型加载成功: {model_id}")
|
||||
logger.info(f"YOLO 模型加载成功: {model_id}")
|
||||
return model
|
||||
except Exception as e:
|
||||
logger.error(f"模型加载失败: {model_id}, 错误: {e}")
|
||||
logger.error(f"YOLO 模型加载失败: {model_id}, 错误: {e}")
|
||||
return None
|
||||
|
||||
def get_model(self, model_id: str) -> Optional[YOLO]:
|
||||
def get_model(self, model_id: str) -> Optional[Union[YOLO, object]]:
|
||||
return self.models.get(model_id)
|
||||
|
||||
async def unload_model(self, model_id: str) -> bool:
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
"""
|
||||
PaddleDetection 抽烟检测服务适配器
|
||||
通过 Docker 调用 Paddle 模型
|
||||
使用本地 PaddlePaddle 环境直接调用模型(无需 Docker)
|
||||
"""
|
||||
|
||||
# 禁用 PIR API 以支持旧版模型格式(必须在任何导入之前设置)
|
||||
import os
|
||||
os.environ['FLAGS_enable_pir_api'] = '0'
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import subprocess
|
||||
import tempfile
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
import sys
|
||||
from typing import Dict, List, Optional
|
||||
from pathlib import Path
|
||||
|
||||
@@ -16,59 +20,128 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PaddleDetectionService:
|
||||
"""PaddleDetection 服务适配器"""
|
||||
"""PaddleDetection 服务适配器(本地模式)"""
|
||||
|
||||
def __init__(self):
|
||||
self.model_name = "smoking_detection"
|
||||
self.docker_image = "smoking-detection:test"
|
||||
self.model_dir = "output_inference/ppyoloe_crn_s_80e_smoking_visdrone"
|
||||
self.threshold = 0.1 # 抽烟检测需要较低的阈值
|
||||
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 倍")
|
||||
|
||||
# 检查 Docker 和镜像
|
||||
self._check_docker()
|
||||
|
||||
def _check_docker(self):
|
||||
"""检查 Docker 环境"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["docker", "info"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5
|
||||
)
|
||||
if result.returncode != 0:
|
||||
logger.error("Docker 未运行")
|
||||
self.available = False
|
||||
return
|
||||
|
||||
# 检查镜像
|
||||
result = subprocess.run(
|
||||
["docker", "image", "inspect", self.docker_image],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5
|
||||
)
|
||||
self.available = result.returncode == 0
|
||||
|
||||
if self.available:
|
||||
logger.info(f"PaddleDetection 服务已就绪: {self.docker_image}")
|
||||
else:
|
||||
logger.error(f"Docker 镜像不存在: {self.docker_image}")
|
||||
|
||||
self._initialize_environment()
|
||||
except Exception as e:
|
||||
logger.error(f"Docker 检查失败: {e}")
|
||||
logger.error(f"初始化环境失败: {e}")
|
||||
self.available = False
|
||||
|
||||
def detect_image(self, image: np.ndarray) -> Dict:
|
||||
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,
|
||||
@@ -78,127 +151,110 @@ class PaddleDetectionService:
|
||||
}
|
||||
|
||||
try:
|
||||
# 创建临时文件
|
||||
with tempfile.NamedTemporaryFile(suffix='.jpg', delete=False) as f:
|
||||
temp_input = f.name
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.jpg', delete=False) as f:
|
||||
temp_output = f.name
|
||||
|
||||
# 保存输入图片
|
||||
cv2.imwrite(temp_input, image)
|
||||
|
||||
# 构建 Docker 命令
|
||||
cmd = [
|
||||
"docker", "run", "--rm",
|
||||
"-v", f"{temp_input}:/workspace/input.jpg",
|
||||
"-v", f"{os.path.dirname(temp_output)}:/workspace/output",
|
||||
self.docker_image,
|
||||
"python", "deploy/python/infer.py",
|
||||
f"--model_dir={self.model_dir}",
|
||||
"--image_file=/workspace/input.jpg",
|
||||
"--device=CPU",
|
||||
"--output_dir=/workspace/output",
|
||||
f"--threshold={self.threshold}"
|
||||
]
|
||||
|
||||
# 执行检测
|
||||
logger.info(f"执行抽烟检测: {temp_input}")
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60
|
||||
)
|
||||
|
||||
# 解析结果
|
||||
detections = self._parse_detection_output(result.stdout)
|
||||
|
||||
# 读取输出图片
|
||||
output_image = None
|
||||
output_path = temp_output.replace('.jpg', '') + '_result.jpg'
|
||||
if os.path.exists(output_path):
|
||||
output_image = cv2.imread(output_path)
|
||||
|
||||
# 清理临时文件
|
||||
self._cleanup_temp_files([temp_input, temp_output, output_path])
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'message': '检测完成',
|
||||
'detections': detections,
|
||||
'output_image': output_image,
|
||||
'stats': {
|
||||
'total_detections': len(detections),
|
||||
'model_used': 'ppyoloe_crn_s_80e_smoking_visdrone',
|
||||
'threshold': self.threshold
|
||||
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 subprocess.TimeoutExpired:
|
||||
logger.error("检测超时")
|
||||
return {
|
||||
'success': False,
|
||||
'message': '检测超时',
|
||||
'detections': [],
|
||||
'stats': None
|
||||
}
|
||||
|
||||
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'检测失败: {str(e)}',
|
||||
'message': f'检测失败: {e}',
|
||||
'detections': [],
|
||||
'stats': None
|
||||
}
|
||||
|
||||
def _parse_detection_output(self, output: str) -> List[Dict]:
|
||||
"""解析检测输出"""
|
||||
def _parse_detection_results(self, results: Dict, threshold: float) -> List[Dict]:
|
||||
"""解析 PaddleDetection 返回的检测结果"""
|
||||
detections = []
|
||||
|
||||
# 查找检测结果行
|
||||
for line in output.split('\n'):
|
||||
if 'class_id:' in line and 'confidence:' in line:
|
||||
try:
|
||||
# 解析: class_id:0, confidence:0.8921, left_top:[268.66,231.64],right_bottom:[351.87,258.66]
|
||||
parts = line.split(',')
|
||||
|
||||
# 提取置信度
|
||||
conf_part = [p for p in parts if 'confidence:' in p][0]
|
||||
confidence = float(conf_part.split(':')[1])
|
||||
|
||||
# 提取坐标
|
||||
left_top_part = [p for p in parts if 'left_top:' in p][0]
|
||||
right_bottom_part = [p for p in parts if 'right_bottom:' in p][0]
|
||||
|
||||
# 解析坐标
|
||||
left_top = eval(left_top_part.split(':')[1])
|
||||
right_bottom = eval(right_bottom_part.split(':')[1])
|
||||
|
||||
x1, y1 = left_top
|
||||
x2, y2 = right_bottom
|
||||
|
||||
detections.append({
|
||||
'class': 'cigarette',
|
||||
'label': '香烟',
|
||||
'confidence': round(confidence, 3),
|
||||
'bbox': [int(x1), int(y1), int(x2), int(y2)]
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"解析检测结果失败: {e}")
|
||||
continue
|
||||
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 _cleanup_temp_files(self, files: List[str]):
|
||||
"""清理临时文件"""
|
||||
for f in files:
|
||||
try:
|
||||
if os.path.exists(f):
|
||||
os.remove(f)
|
||||
except Exception as e:
|
||||
logger.warning(f"清理临时文件失败: {f}, {e}")
|
||||
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 模型相同的接口
|
||||
@@ -222,9 +278,8 @@ class SmokingDetectionModel:
|
||||
Returns:
|
||||
模拟 YOLO 结果的对象
|
||||
"""
|
||||
result = self.service.detect_image(image)
|
||||
result = self.service.detect_image(image, threshold=conf)
|
||||
|
||||
# 创建模拟的 YOLO 结果对象
|
||||
return [PaddleDetectionResult(result, self.names)]
|
||||
|
||||
|
||||
@@ -235,7 +290,6 @@ class PaddleDetectionResult:
|
||||
self.detection_result = detection_result
|
||||
self.names = names
|
||||
|
||||
# 创建模拟的 boxes 对象
|
||||
self.boxes = self._create_boxes()
|
||||
|
||||
def _create_boxes(self):
|
||||
@@ -245,7 +299,6 @@ class PaddleDetectionResult:
|
||||
if not detections:
|
||||
return MockBoxes([])
|
||||
|
||||
# 转换为 YOLO 格式
|
||||
xyxy = []
|
||||
conf = []
|
||||
cls = []
|
||||
@@ -253,7 +306,7 @@ class PaddleDetectionResult:
|
||||
for det in detections:
|
||||
xyxy.append(det['bbox'])
|
||||
conf.append(det['confidence'])
|
||||
cls.append(0) # cigarette 类别
|
||||
cls.append(0)
|
||||
|
||||
return MockBoxes(xyxy, conf, cls)
|
||||
|
||||
@@ -262,13 +315,89 @@ class MockBoxes:
|
||||
"""模拟 YOLO boxes 对象"""
|
||||
|
||||
def __init__(self, xyxy_list, conf_list=None, cls_list=None):
|
||||
import torch
|
||||
try:
|
||||
import torch
|
||||
use_torch = True
|
||||
except ImportError:
|
||||
use_torch = False
|
||||
|
||||
if xyxy_list:
|
||||
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)
|
||||
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:
|
||||
self.xyxy = torch.empty((0, 4))
|
||||
self.conf = torch.empty((0, 1))
|
||||
self.cls = torch.empty((0, 1), dtype=torch.int64)
|
||||
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
|
||||
|
||||
38
apps/server/start_server_with_env.sh
Executable file
38
apps/server/start_server_with_env.sh
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 服务器启动包装脚本
|
||||
# 确保 PaddlePaddle 环境变量正确设置
|
||||
|
||||
set -e
|
||||
|
||||
# 进入脚本所在目录(apps/server)
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# 设置 PaddlePaddle 环境变量(必须在 Python 启动前设置)
|
||||
export FLAGS_enable_pir_api=0
|
||||
|
||||
# 显示环境信息
|
||||
echo "🔧 服务器启动环境"
|
||||
echo "======================================"
|
||||
echo "🏷️ FLAGS_enable_pir_api: $FLAGS_enable_pir_api"
|
||||
echo "📂 工作目录: $(pwd)"
|
||||
echo "======================================"
|
||||
|
||||
# 激活服务器虚拟环境(包含所有必需的 PaddlePaddle 依赖)
|
||||
if [ -f "venv/bin/activate" ]; then
|
||||
echo "✅ 激活服务器虚拟环境"
|
||||
source venv/bin/activate
|
||||
echo "🐍 Python 解释器: $(which python)"
|
||||
else
|
||||
echo "⚠️ 服务器虚拟环境不存在,使用系统环境"
|
||||
fi
|
||||
|
||||
# 显示 Python 版本
|
||||
echo "📦 Python 版本: $(python --version)"
|
||||
|
||||
# 启动服务器
|
||||
echo "🚀 启动服务器..."
|
||||
echo "======================================"
|
||||
|
||||
# 使用服务器虚拟环境的 Python 运行服务器
|
||||
exec python main.py
|
||||
Reference in New Issue
Block a user