Initial commit: Video detection platform with YOLO models

Features:
- Fire detection (YOLOv10)
- Helmet detection (YOLOv8)
- Crowd detection (YOLOv8)
- Smoking detection (YOLOv8)
- Loitering detection (YOLOv8)

Tech Stack:
- Frontend: Vue 3 + Vite + Element Plus
- Backend: FastAPI + WebSocket
- Monorepo: pnpm workspace + Turbo
- Docker support included
This commit is contained in:
wwh
2026-05-18 10:54:10 +08:00
commit 8fb58c75fe
42 changed files with 6663 additions and 0 deletions

View File

View File

@@ -0,0 +1,39 @@
from pydantic import BaseModel, Field
from typing import List, Optional, Dict, Any
from enum import Enum
class ModelInfo(BaseModel):
id: str
name: str
description: str
classes: List[str]
labels: Dict[str, str]
size: str
type: str
class Detection(BaseModel):
class_name: str
label: str
confidence: float
bbox: List[int]
class DetectionStats(BaseModel):
total_detections: int
avg_confidence: float
processing_time: float
model_used: str
class ImageDetectionResult(BaseModel):
success: bool
message: str
data: Dict[str, Any]
class VideoDetectionRequest(BaseModel):
model_id: str
confidence: float = Field(default=0.5, ge=0.1, le=1.0)
iou: float = Field(default=0.45, ge=0.1, le=0.9)
class DetectionConfig(BaseModel):
model_id: str
confidence: float = Field(default=0.5, ge=0.1, le=1.0)
iou: float = Field(default=0.45, ge=0.1, le=0.9)

View File

@@ -0,0 +1,357 @@
"""
YOLO 格式的抽烟检测模型适配器
将 PaddleDetection 模型包装为 YOLO 接口
"""
import os
import cv2
import numpy as np
import subprocess
import tempfile
import logging
from typing import List, Dict, Union
from pathlib import Path
logger = logging.getLogger(__name__)
class SmokingDetectionYOLO:
"""
模拟 YOLO 接口的抽烟检测模型
底层使用 PaddleDetection Docker 容器
"""
def __init__(self, model_path=None):
"""
初始化模型
Args:
model_path: 模型路径(可选,仅用于兼容性)
"""
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
# YOLO 兼容属性
self.names = {0: 'cigarette'}
self.model = self # 自我引用,保持与 YOLO 相同的接口
# 检查 Docker
self._check_docker()
logger.info(f"抽烟检测模型初始化完成Docker可用: {self.available}")
def _check_docker(self):
"""检查 Docker 环境"""
try:
result = subprocess.run(
["docker", "info"],
capture_output=True,
text=True,
timeout=5
)
self.available = result.returncode == 0
if self.available:
# 检查镜像
result = subprocess.run(
["docker", "image", "inspect", self.docker_image],
capture_output=True,
text=True,
timeout=5
)
self.available = result.returncode == 0
except Exception as e:
logger.error(f"Docker 检查失败: {e}")
self.available = False
def __call__(self, source, conf=0.1, iou=0.45, verbose=False, stream=False):
"""
模拟 YOLO 模型的调用接口
Args:
source: 图片路径、OpenCV 图片、或图片列表
conf: 置信度阈值
iou: IoU 阈值PaddleDetection 不支持,仅用于兼容)
verbose: 是否输出详细信息
stream: 是否流式输出(仅用于兼容)
Returns:
YOLOResult 对象列表
"""
if not self.available:
logger.error("Docker 不可用,无法运行检测")
return [YOLOResult([])]
# 处理不同类型的输入
if isinstance(source, str):
# 图片路径
image = cv2.imread(source)
if image is None:
logger.error(f"无法读取图片: {source}")
return [YOLOResult([])]
return self._detect_single(image, conf, verbose)
elif isinstance(source, np.ndarray):
# OpenCV 图片
return self._detect_single(source, conf, verbose)
elif isinstance(source, list):
# 图片列表
results = []
for img in source:
if isinstance(img, str):
img = cv2.imread(img)
if img is not None:
results.extend(self._detect_single(img, conf, verbose))
return results
else:
logger.error(f"不支持的输入类型: {type(source)}")
return [YOLOResult([])]
def _detect_single(self, image: np.ndarray, conf: float, verbose: bool) -> List['YOLOResult']:
"""检测单张图片"""
try:
# 创建临时文件
with tempfile.NamedTemporaryFile(suffix='.jpg', delete=False) as f:
temp_input = f.name
# 保存输入图片
cv2.imwrite(temp_input, image)
if verbose:
logger.info(f"正在检测: {temp_input}")
# 构建 Docker 命令
cmd = [
"docker", "run", "--rm",
"-v", f"{temp_input}:/workspace/input.jpg",
self.docker_image,
"python", "deploy/python/infer.py",
f"--model_dir={self.model_dir}",
"--image_file=/workspace/input.jpg",
"--device=CPU",
"--output_dir=/workspace",
f"--threshold={conf}"
]
# 执行检测
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=60
)
if verbose:
logger.info(f"检测完成,返回码: {result.returncode}")
# 解析结果
detections = self._parse_output(result.stdout)
if verbose and detections:
logger.info(f"检测到 {len(detections)} 个目标")
# 清理临时文件
try:
os.remove(temp_input)
except:
pass
return [YOLOResult(detections)]
except subprocess.TimeoutExpired:
logger.error("检测超时")
return [YOLOResult([])]
except Exception as e:
logger.error(f"检测失败: {e}")
return [YOLOResult([])]
def _parse_output(self, output: str) -> List[Dict]:
"""解析检测输出"""
detections = []
import re
# 使用正则表达式匹配检测行
# 格式: class_id:0, confidence:0.8921, left_top:[268.66,231.64],right_bottom:[351.87,258.66]
pattern = r'class_id:\d+,\s*confidence:([\d.]+),\s*left_top:\[([\d.]+),\s*([\d.]+)\],\s*right_bottom:\[([\d.]+),\s*([\d.]+)\]'
for line in output.split('\n'):
match = re.search(pattern, line)
if match:
try:
confidence = float(match.group(1))
x1 = float(match.group(2))
y1 = float(match.group(3))
x2 = float(match.group(4))
y2 = float(match.group(5))
detections.append({
'bbox': [int(x1), int(y1), int(x2), int(y2)],
'confidence': confidence,
'class': 0,
'name': 'cigarette'
})
except Exception as e:
logger.warning(f"解析检测结果失败: {e}, line: {line}")
continue
return detections
def predict(self, source, **kwargs):
"""兼容 predict 方法"""
return self.__call__(source, **kwargs)
class YOLOResult:
"""
模拟 YOLO 检测结果对象
提供与 ultralytics YOLO 结果相同的接口
"""
def __init__(self, detections: List[Dict]):
self.detections = detections
self.names = {0: 'cigarette'}
# 创建 boxes 对象
self.boxes = Boxes(detections)
# 其他 YOLO 结果属性
self.probs = None
self.keypoints = None
self.obb = None
self.speed = {'preprocess': 0, 'inference': 0, 'postprocess': 0}
def __len__(self):
return len(self.detections)
def __getitem__(self, idx):
"""支持索引访问"""
if idx < len(self.detections):
return YOLOResult([self.detections[idx]])
return YOLOResult([])
def plot(self, **kwargs):
"""绘制检测结果(兼容方法)"""
return None
class Boxes:
"""
模拟 YOLO boxes 对象
提供 xyxy, conf, cls 等属性
"""
def __init__(self, detections: List[Dict]):
self.detections = detections
# 尝试使用 torch如果没有则使用 numpy
try:
import torch
if detections:
xyxy_list = [[d['bbox'][0], d['bbox'][1], d['bbox'][2], d['bbox'][3]] for d in detections]
conf_list = [[d['confidence']] for d in detections]
cls_list = [[d['class']] for d in detections]
self.xyxy = torch.tensor(xyxy_list, dtype=torch.float32)
self.conf = torch.tensor(conf_list, dtype=torch.float32)
self.cls = torch.tensor(cls_list, dtype=torch.int64)
self.id = None
else:
self.xyxy = torch.empty((0, 4))
self.conf = torch.empty((0, 1))
self.cls = torch.empty((0, 1), dtype=torch.int64)
self.id = None
except ImportError:
# 如果没有 torch使用 numpy
import numpy as np
if detections:
xyxy_list = [[d['bbox'][0], d['bbox'][1], d['bbox'][2], d['bbox'][3]] for d in detections]
conf_list = [[d['confidence']] for d in detections]
cls_list = [[d['class']] for d in detections]
self.xyxy = np.array(xyxy_list, dtype=np.float32)
self.conf = np.array(conf_list, dtype=np.float32)
self.cls = np.array(cls_list, dtype=np.int64)
self.id = None
else:
self.xyxy = np.empty((0, 4), dtype=np.float32)
self.conf = np.empty((0, 1), dtype=np.float32)
self.cls = np.empty((0, 1), dtype=np.int64)
self.id = None
def __len__(self):
return len(self.detections)
def __iter__(self):
"""使 Boxes 可迭代"""
for i in range(len(self.detections)):
yield Box(self, i)
def cpu(self):
"""兼容方法"""
return self
def numpy(self):
"""转换为 numpy"""
if hasattr(self.xyxy, 'numpy'):
return type('Boxes', (), {
'xyxy': self.xyxy.numpy(),
'conf': self.conf.numpy(),
'cls': self.cls.numpy(),
'id': self.id
})()
return self
class Box:
"""
模拟单个检测框对象
"""
def __init__(self, boxes: Boxes, index: int):
self._boxes = boxes
self._index = index
@property
def xyxy(self):
"""返回 xyxy 坐标 (1, 4) 形状"""
import torch
import numpy as np
coords = self._boxes.xyxy[self._index]
if isinstance(coords, torch.Tensor):
return coords.unsqueeze(0)
else:
return np.array([coords])
@property
def conf(self):
"""返回置信度 (1,) 形状 - 与 YOLO 兼容"""
import torch
import numpy as np
conf_val = self._boxes.conf[self._index]
# 返回 (1,) 形状,与 YOLO 一致
if isinstance(conf_val, torch.Tensor):
return conf_val.view(1)
else:
return np.array([conf_val])
@property
def cls(self):
"""返回类别 (1,) 形状 - 与 YOLO 兼容"""
import torch
import numpy as np
cls_val = self._boxes.cls[self._index]
# 返回 (1,) 形状,与 YOLO 一致
if isinstance(cls_val, torch.Tensor):
return cls_val.view(1)
else:
return np.array([cls_val])

View File

@@ -0,0 +1,359 @@
"""
YOLO 格式的抽烟检测模型适配器(快速版)
使用 HTTP API 与常驻 Docker 容器通信
"""
import os
import cv2
import numpy as np
import subprocess
import tempfile
import logging
import time
import json
import requests
from typing import List, Dict, Optional
from pathlib import Path
logger = logging.getLogger(__name__)
class SmokingDetectionYOLO:
"""
模拟 YOLO 接口的抽烟检测模型(快速版)
使用 HTTP API 与常驻 Docker 容器通信
"""
_container_name = "smoking-detection-daemon"
_initialized = False
_server_url = "http://localhost:8080"
def __init__(self, model_path=None):
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
# YOLO 兼容属性
self.names = {0: 'cigarette'}
self.model = self
# 检查 Docker 并启动常驻服务器
self._check_docker()
if self.available:
self._start_daemon()
logger.info(f"抽烟检测模型快速版初始化完成Docker可用: {self.available}")
def _check_docker(self):
"""检查 Docker 环境"""
try:
result = subprocess.run(
["docker", "info"],
capture_output=True,
text=True,
timeout=5
)
self.available = result.returncode == 0
if self.available:
result = subprocess.run(
["docker", "image", "inspect", self.docker_image],
capture_output=True,
text=True,
timeout=5
)
self.available = result.returncode == 0
except Exception as e:
logger.error(f"Docker 检查失败: {e}")
self.available = False
def _start_daemon(self):
"""启动常驻服务器"""
try:
# 检查容器是否已在运行
result = subprocess.run(
["docker", "ps", "-q", "-f", f"name={self._container_name}"],
capture_output=True,
text=True,
timeout=5
)
if result.stdout.strip():
logger.info(f"常驻服务器已在运行: {self._container_name}")
SmokingDetectionYOLO._initialized = True
return
# 检查容器是否存在但已停止
result = subprocess.run(
["docker", "ps", "-aq", "-f", f"name={self._container_name}"],
capture_output=True,
text=True,
timeout=5
)
if result.stdout.strip():
# 删除旧容器
logger.info("删除旧容器")
subprocess.run(
["docker", "rm", "-f", self._container_name],
capture_output=True,
timeout=10
)
# 创建新容器并启动常驻服务器
logger.info("启动常驻服务器...")
subprocess.run(
[
"docker", "run", "-d",
"--name", self._container_name,
"-p", "8080:8080",
"-v", "/tmp:/workspace/input",
"-v", "/Users/wwh/project/video-model/PaddlePaddle/PaddleDetection-release-2.9/smoking_server_daemon.py:/workspace/PaddleDetection/smoking_server_daemon.py",
"-w", "/workspace/PaddleDetection",
self.docker_image,
"python", "smoking_server_daemon.py"
],
capture_output=True,
timeout=10
)
# 等待服务器启动
logger.info("等待服务器启动...")
time.sleep(5)
SmokingDetectionYOLO._initialized = True
logger.info("常驻服务器启动成功")
except Exception as e:
logger.error(f"启动常驻服务器失败: {e}")
SmokingDetectionYOLO._initialized = False
def __call__(self, source, conf=0.1, iou=0.45, verbose=False, stream=False):
"""模拟 YOLO 模型的调用接口"""
if not self.available:
logger.error("Docker 不可用,无法运行检测")
return [YOLOResult([])]
# 处理不同类型的输入
if isinstance(source, str):
image = cv2.imread(source)
if image is None:
logger.error(f"无法读取图片: {source}")
return [YOLOResult([])]
return self._detect_single(image, conf, verbose)
elif isinstance(source, np.ndarray):
return self._detect_single(source, conf, verbose)
elif isinstance(source, list):
results = []
for img in source:
if isinstance(img, str):
img = cv2.imread(img)
if img is not None:
results.extend(self._detect_single(img, conf, verbose))
return results
else:
logger.error(f"不支持的输入类型: {type(source)}")
return [YOLOResult([])]
def _detect_single(self, image: np.ndarray, conf: float, verbose: bool) -> List['YOLOResult']:
"""检测单张图片(使用 HTTP API"""
start_time = time.time()
try:
# 创建临时文件
input_filename = f"smoking_fast_{int(time.time()*1000)}.jpg"
temp_input = f"/tmp/{input_filename}"
# 保存输入图片
cv2.imwrite(temp_input, image)
if verbose:
logger.info(f"正在检测: {temp_input}")
# 发送 HTTP 请求
request = {
'image_path': f'/workspace/input/{input_filename}',
'threshold': conf
}
response = requests.post(
f"{self._server_url}/detect",
json=request,
timeout=30
)
elapsed = time.time() - start_time
if verbose:
logger.info(f"检测完成,耗时: {elapsed:.2f}")
# 解析结果
if response.status_code == 200:
data = response.json()
if data.get('success'):
detections = data.get('detections', [])
else:
logger.error(f"检测失败: {data.get('error')}")
detections = []
else:
logger.error(f"HTTP 错误: {response.status_code}")
detections = []
# 清理临时文件
try:
os.remove(temp_input)
except:
pass
return [YOLOResult(detections)]
except Exception as e:
logger.error(f"检测失败: {e}")
return [YOLOResult([])]
def predict(self, source, **kwargs):
"""兼容 predict 方法"""
return self.__call__(source, **kwargs)
@classmethod
def stop_daemon(cls):
"""停止常驻服务器"""
try:
subprocess.run(
["docker", "stop", cls._container_name],
capture_output=True,
timeout=10
)
logger.info("常驻服务器已停止")
except Exception as e:
logger.error(f"停止常驻服务器失败: {e}")
# YOLOResult, Boxes, Box 类(与之前相同)
class YOLOResult:
"""模拟 YOLO 检测结果对象"""
def __init__(self, detections: List[Dict]):
self.detections = detections
self.names = {0: 'cigarette'}
self.boxes = Boxes(detections)
self.probs = None
self.keypoints = None
self.obb = None
self.speed = {'preprocess': 0, 'inference': 0, 'postprocess': 0}
def __len__(self):
return len(self.detections)
def __getitem__(self, idx):
if idx < len(self.detections):
return YOLOResult([self.detections[idx]])
return YOLOResult([])
def plot(self, **kwargs):
return None
class Boxes:
"""模拟 YOLO boxes 对象"""
def __init__(self, detections: List[Dict]):
self.detections = detections
try:
import torch
if detections:
xyxy_list = [[d['bbox'][0], d['bbox'][1], d['bbox'][2], d['bbox'][3]] for d in detections]
conf_list = [[d['confidence']] for d in detections]
cls_list = [[d['class']] for d in detections]
self.xyxy = torch.tensor(xyxy_list, dtype=torch.float32)
self.conf = torch.tensor(conf_list, dtype=torch.float32)
self.cls = torch.tensor(cls_list, dtype=torch.int64)
self.id = None
else:
self.xyxy = torch.empty((0, 4))
self.conf = torch.empty((0, 1))
self.cls = torch.empty((0, 1), dtype=torch.int64)
self.id = None
except ImportError:
import numpy as np
if detections:
xyxy_list = [[d['bbox'][0], d['bbox'][1], d['bbox'][2], d['bbox'][3]] for d in detections]
conf_list = [[d['confidence']] for d in detections]
cls_list = [[d['class']] for d in detections]
self.xyxy = np.array(xyxy_list, dtype=np.float32)
self.conf = np.array(conf_list, dtype=np.float32)
self.cls = np.array(cls_list, dtype=np.int64)
self.id = None
else:
self.xyxy = np.empty((0, 4), dtype=np.float32)
self.conf = np.empty((0, 1), dtype=np.float32)
self.cls = np.empty((0, 1), dtype=np.int64)
self.id = None
def __len__(self):
return len(self.detections)
def __iter__(self):
for i in range(len(self.detections)):
yield Box(self, i)
def cpu(self):
return self
def numpy(self):
if hasattr(self.xyxy, 'numpy'):
return type('Boxes', (), {
'xyxy': self.xyxy.numpy(),
'conf': self.conf.numpy(),
'cls': self.cls.numpy(),
'id': self.id
})()
return self
class Box:
"""模拟单个检测框对象"""
def __init__(self, boxes: Boxes, index: int):
self._boxes = boxes
self._index = index
@property
def xyxy(self):
import torch
import numpy as np
coords = self._boxes.xyxy[self._index]
if isinstance(coords, torch.Tensor):
return coords.unsqueeze(0)
else:
return np.array([coords])
@property
def conf(self):
import torch
import numpy as np
conf_val = self._boxes.conf[self._index]
if isinstance(conf_val, torch.Tensor):
return conf_val.view(1)
else:
return np.array([conf_val])
@property
def cls(self):
import torch
import numpy as np
cls_val = self._boxes.cls[self._index]
if isinstance(cls_val, torch.Tensor):
return cls_val.view(1)
else:
return np.array([cls_val])

View File

@@ -0,0 +1,399 @@
"""
YOLO 格式的抽烟检测模型适配器(优化版)
使用后台 Docker 容器,避免重复启动
"""
import os
import cv2
import numpy as np
import subprocess
import tempfile
import logging
import time
from typing import List, Dict, Optional
from pathlib import Path
logger = logging.getLogger(__name__)
class SmokingDetectionYOLO:
"""
模拟 YOLO 接口的抽烟检测模型(优化版)
使用后台 Docker 容器,避免每次检测都启动新容器
"""
_container_name = "smoking-detection-server"
_container_started = False
def __init__(self, model_path=None):
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
# YOLO 兼容属性
self.names = {0: 'cigarette'}
self.model = self
# 检查 Docker 并启动后台容器
self._check_docker()
if self.available:
self._start_background_container()
logger.info(f"抽烟检测模型初始化完成Docker可用: {self.available}")
def _check_docker(self):
"""检查 Docker 环境"""
try:
result = subprocess.run(
["docker", "info"],
capture_output=True,
text=True,
timeout=5
)
self.available = result.returncode == 0
if self.available:
result = subprocess.run(
["docker", "image", "inspect", self.docker_image],
capture_output=True,
text=True,
timeout=5
)
self.available = result.returncode == 0
except Exception as e:
logger.error(f"Docker 检查失败: {e}")
self.available = False
def _start_background_container(self):
"""启动后台容器"""
try:
# 检查容器是否已在运行
result = subprocess.run(
["docker", "ps", "-q", "-f", f"name={self._container_name}"],
capture_output=True,
text=True,
timeout=5
)
if result.stdout.strip():
logger.info(f"后台容器已在运行: {self._container_name}")
SmokingDetectionYOLO._container_started = True
return
# 检查容器是否存在但已停止
result = subprocess.run(
["docker", "ps", "-aq", "-f", f"name={self._container_name}"],
capture_output=True,
text=True,
timeout=5
)
if result.stdout.strip():
# 启动已存在的容器
logger.info(f"启动已存在的容器: {self._container_name}")
subprocess.run(
["docker", "start", self._container_name],
capture_output=True,
timeout=10
)
else:
# 创建新容器
logger.info(f"创建后台容器: {self._container_name}")
subprocess.run(
[
"docker", "run", "-d",
"--name", self._container_name,
"-v", "/tmp:/workspace/input",
"-v", "/tmp:/workspace/output",
self.docker_image,
"tail", "-f", "/dev/null"
],
capture_output=True,
timeout=10
)
SmokingDetectionYOLO._container_started = True
logger.info("后台容器启动成功")
except Exception as e:
logger.error(f"启动后台容器失败: {e}")
SmokingDetectionYOLO._container_started = False
def __call__(self, source, conf=0.1, iou=0.45, verbose=False, stream=False):
"""模拟 YOLO 模型的调用接口"""
if not self.available:
logger.error("Docker 不可用,无法运行检测")
return [YOLOResult([])]
# 处理不同类型的输入
if isinstance(source, str):
image = cv2.imread(source)
if image is None:
logger.error(f"无法读取图片: {source}")
return [YOLOResult([])]
return self._detect_single(image, conf, verbose)
elif isinstance(source, np.ndarray):
return self._detect_single(source, conf, verbose)
elif isinstance(source, list):
results = []
for img in source:
if isinstance(img, str):
img = cv2.imread(img)
if img is not None:
results.extend(self._detect_single(img, conf, verbose))
return results
else:
logger.error(f"不支持的输入类型: {type(source)}")
return [YOLOResult([])]
def _detect_single(self, image: np.ndarray, conf: float, verbose: bool) -> List['YOLOResult']:
"""检测单张图片(使用后台容器)"""
start_time = time.time()
try:
# 创建临时文件
input_filename = f"smoking_input_{int(time.time()*1000)}.jpg"
output_filename = f"smoking_output_{int(time.time()*1000)}.jpg"
temp_input = f"/tmp/{input_filename}"
temp_output = f"/tmp/{output_filename}"
# 保存输入图片
cv2.imwrite(temp_input, image)
if verbose:
logger.info(f"正在检测: {temp_input}")
# 使用后台容器执行检测
if SmokingDetectionYOLO._container_started:
# 使用 exec 在运行中的容器内执行
cmd = [
"docker", "exec",
self._container_name,
"python", "deploy/python/infer.py",
f"--model_dir={self.model_dir}",
f"--image_file=/workspace/input/{input_filename}",
"--device=CPU",
f"--output_dir=/workspace/output",
f"--threshold={conf}"
]
else:
# 回退到原来的方式
cmd = [
"docker", "run", "--rm",
"-v", f"{temp_input}:/workspace/input.jpg",
self.docker_image,
"python", "deploy/python/infer.py",
f"--model_dir={self.model_dir}",
"--image_file=/workspace/input.jpg",
"--device=CPU",
"--output_dir=/workspace",
f"--threshold={conf}"
]
# 执行检测
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=60
)
elapsed = time.time() - start_time
if verbose:
logger.info(f"检测完成,耗时: {elapsed:.2f}")
# 解析结果
detections = self._parse_output(result.stdout)
# 清理临时文件
try:
os.remove(temp_input)
except:
pass
return [YOLOResult(detections)]
except subprocess.TimeoutExpired:
logger.error("检测超时")
return [YOLOResult([])]
except Exception as e:
logger.error(f"检测失败: {e}")
return [YOLOResult([])]
def _parse_output(self, output: str) -> List[Dict]:
"""解析检测输出"""
detections = []
import re
pattern = r'class_id:\d+,\s*confidence:([\d.]+),\s*left_top:\[([\d.]+),\s*([\d.]+)\],\s*right_bottom:\[([\d.]+),\s*([\d.]+)\]'
for line in output.split('\n'):
match = re.search(pattern, line)
if match:
try:
confidence = float(match.group(1))
x1 = float(match.group(2))
y1 = float(match.group(3))
x2 = float(match.group(4))
y2 = float(match.group(5))
detections.append({
'bbox': [int(x1), int(y1), int(x2), int(y2)],
'confidence': confidence,
'class': 0,
'name': 'cigarette'
})
except Exception as e:
logger.warning(f"解析检测结果失败: {e}, line: {line}")
continue
return detections
def predict(self, source, **kwargs):
"""兼容 predict 方法"""
return self.__call__(source, **kwargs)
@classmethod
def stop_background_container(cls):
"""停止后台容器"""
try:
subprocess.run(
["docker", "stop", cls._container_name],
capture_output=True,
timeout=10
)
logger.info("后台容器已停止")
except Exception as e:
logger.error(f"停止后台容器失败: {e}")
# 复制 YOLOResult, Boxes, Box 类(与原版相同)
class YOLOResult:
"""模拟 YOLO 检测结果对象"""
def __init__(self, detections: List[Dict]):
self.detections = detections
self.names = {0: 'cigarette'}
self.boxes = Boxes(detections)
self.probs = None
self.keypoints = None
self.obb = None
self.speed = {'preprocess': 0, 'inference': 0, 'postprocess': 0}
def __len__(self):
return len(self.detections)
def __getitem__(self, idx):
if idx < len(self.detections):
return YOLOResult([self.detections[idx]])
return YOLOResult([])
def plot(self, **kwargs):
return None
class Boxes:
"""模拟 YOLO boxes 对象"""
def __init__(self, detections: List[Dict]):
self.detections = detections
try:
import torch
if detections:
xyxy_list = [[d['bbox'][0], d['bbox'][1], d['bbox'][2], d['bbox'][3]] for d in detections]
conf_list = [[d['confidence']] for d in detections]
cls_list = [[d['class']] for d in detections]
self.xyxy = torch.tensor(xyxy_list, dtype=torch.float32)
self.conf = torch.tensor(conf_list, dtype=torch.float32)
self.cls = torch.tensor(cls_list, dtype=torch.int64)
self.id = None
else:
self.xyxy = torch.empty((0, 4))
self.conf = torch.empty((0, 1))
self.cls = torch.empty((0, 1), dtype=torch.int64)
self.id = None
except ImportError:
import numpy as np
if detections:
xyxy_list = [[d['bbox'][0], d['bbox'][1], d['bbox'][2], d['bbox'][3]] for d in detections]
conf_list = [[d['confidence']] for d in detections]
cls_list = [[d['class']] for d in detections]
self.xyxy = np.array(xyxy_list, dtype=np.float32)
self.conf = np.array(conf_list, dtype=np.float32)
self.cls = np.array(cls_list, dtype=np.int64)
self.id = None
else:
self.xyxy = np.empty((0, 4), dtype=np.float32)
self.conf = np.empty((0, 1), dtype=np.float32)
self.cls = np.empty((0, 1), dtype=np.int64)
self.id = None
def __len__(self):
return len(self.detections)
def __iter__(self):
for i in range(len(self.detections)):
yield Box(self, i)
def cpu(self):
return self
def numpy(self):
if hasattr(self.xyxy, 'numpy'):
return type('Boxes', (), {
'xyxy': self.xyxy.numpy(),
'conf': self.conf.numpy(),
'cls': self.cls.numpy(),
'id': self.id
})()
return self
class Box:
"""模拟单个检测框对象"""
def __init__(self, boxes: Boxes, index: int):
self._boxes = boxes
self._index = index
@property
def xyxy(self):
import torch
import numpy as np
coords = self._boxes.xyxy[self._index]
if isinstance(coords, torch.Tensor):
return coords.unsqueeze(0)
else:
return np.array([coords])
@property
def conf(self):
import torch
import numpy as np
conf_val = self._boxes.conf[self._index]
if isinstance(conf_val, torch.Tensor):
return conf_val.view(1)
else:
return np.array([conf_val])
@property
def cls(self):
import torch
import numpy as np
cls_val = self._boxes.cls[self._index]
if isinstance(cls_val, torch.Tensor):
return cls_val.view(1)
else:
return np.array([cls_val])

View File

@@ -0,0 +1,379 @@
"""
YOLO 格式的抽烟检测模型适配器V2 - 常驻进程版)
使用 Docker 容器内的常驻 Python 进程,避免每次检测都启动新进程
"""
import os
import cv2
import numpy as np
import subprocess
import tempfile
import logging
import time
import json
from typing import List, Dict, Optional
from pathlib import Path
logger = logging.getLogger(__name__)
class SmokingDetectionYOLO:
"""
模拟 YOLO 接口的抽烟检测模型V2 - 常驻进程版)
使用 Docker 容器内的常驻 Python 进程
"""
_container_name = "smoking-detection-v2"
_process = None
_initialized = False
def __init__(self, model_path=None):
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
# YOLO 兼容属性
self.names = {0: 'cigarette'}
self.model = self
# 检查 Docker 并启动常驻进程
self._check_docker()
if self.available:
self._start_server()
logger.info(f"抽烟检测模型 V2 初始化完成Docker可用: {self.available}")
def _check_docker(self):
"""检查 Docker 环境"""
try:
result = subprocess.run(
["docker", "info"],
capture_output=True,
text=True,
timeout=5
)
self.available = result.returncode == 0
if self.available:
result = subprocess.run(
["docker", "image", "inspect", self.docker_image],
capture_output=True,
text=True,
timeout=5
)
self.available = result.returncode == 0
except Exception as e:
logger.error(f"Docker 检查失败: {e}")
self.available = False
def _start_server(self):
"""启动常驻服务器进程"""
try:
# 检查是否已有进程在运行
if SmokingDetectionYOLO._process is not None:
logger.info("常驻进程已在运行")
return
# 检查容器是否已存在
result = subprocess.run(
["docker", "ps", "-aq", "-f", f"name={self._container_name}"],
capture_output=True,
text=True,
timeout=5
)
if result.stdout.strip():
# 删除旧容器
logger.info("删除旧容器")
subprocess.run(
["docker", "rm", "-f", self._container_name],
capture_output=True,
timeout=10
)
# 启动新容器并运行服务器
logger.info("启动常驻服务器...")
# 获取 smoking_server.py 的绝对路径
server_script_path = "/Users/wwh/project/video-model/PaddlePaddle/PaddleDetection-release-2.9/smoking_server.py"
# 使用 Popen 保持进程运行,挂载 server 脚本
SmokingDetectionYOLO._process = subprocess.Popen(
[
"docker", "run", "-i", "--rm",
"--name", self._container_name,
"-v", "/tmp:/workspace/input",
"-v", f"{server_script_path}:/workspace/PaddleDetection/smoking_server.py",
"-w", "/workspace/PaddleDetection",
self.docker_image,
"python", "smoking_server.py"
],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1
)
# 等待服务器启动(读取模型加载完成的消息)
logger.info("等待服务器启动...")
start_wait = time.time()
while time.time() - start_wait < 30: # 最多等待30秒
if SmokingDetectionYOLO._process.poll() is not None:
# 进程已退出
stderr = SmokingDetectionYOLO._process.stderr.read()
logger.error(f"服务器启动失败: {stderr}")
SmokingDetectionYOLO._process = None
return
# 尝试读取 stderr 看是否加载完成
import select
if SmokingDetectionYOLO._process.stderr:
ready, _, _ = select.select([SmokingDetectionYOLO._process.stderr], [], [], 0.5)
if ready:
line = SmokingDetectionYOLO._process.stderr.readline()
if line:
logger.info(f"Server: {line.strip()}")
if "模型加载完成" in line:
break
time.sleep(0.1)
# 检查进程是否还在运行
if SmokingDetectionYOLO._process.poll() is None:
SmokingDetectionYOLO._initialized = True
logger.info("常驻服务器启动成功")
else:
stderr = SmokingDetectionYOLO._process.stderr.read()
logger.error(f"服务器启动失败: {stderr}")
SmokingDetectionYOLO._process = None
except Exception as e:
logger.error(f"启动常驻服务器失败: {e}")
SmokingDetectionYOLO._process = None
def __call__(self, source, conf=0.1, iou=0.45, verbose=False, stream=False):
"""模拟 YOLO 模型的调用接口"""
if not self.available:
logger.error("Docker 不可用,无法运行检测")
return [YOLOResult([])]
if not SmokingDetectionYOLO._initialized:
logger.error("常驻服务器未初始化")
return [YOLOResult([])]
# 处理不同类型的输入
if isinstance(source, str):
image = cv2.imread(source)
if image is None:
logger.error(f"无法读取图片: {source}")
return [YOLOResult([])]
return self._detect_single(image, conf, verbose)
elif isinstance(source, np.ndarray):
return self._detect_single(source, conf, verbose)
elif isinstance(source, list):
results = []
for img in source:
if isinstance(img, str):
img = cv2.imread(img)
if img is not None:
results.extend(self._detect_single(img, conf, verbose))
return results
else:
logger.error(f"不支持的输入类型: {type(source)}")
return [YOLOResult([])]
def _detect_single(self, image: np.ndarray, conf: float, verbose: bool) -> List['YOLOResult']:
"""检测单张图片(使用常驻进程)"""
start_time = time.time()
try:
# 创建临时文件
input_filename = f"smoking_v2_{int(time.time()*1000)}.jpg"
temp_input = f"/tmp/{input_filename}"
# 保存输入图片
cv2.imwrite(temp_input, image)
if verbose:
logger.info(f"正在检测: {temp_input}")
# 发送请求到常驻进程
request = {
'image_path': f'/workspace/input/{input_filename}',
'threshold': conf
}
SmokingDetectionYOLO._process.stdin.write(json.dumps(request) + '\n')
SmokingDetectionYOLO._process.stdin.flush()
# 读取响应
response_line = SmokingDetectionYOLO._process.stdout.readline()
response = json.loads(response_line)
elapsed = time.time() - start_time
if verbose:
logger.info(f"检测完成,耗时: {elapsed:.2f}")
# 解析结果
if response.get('success'):
detections = response.get('detections', [])
else:
logger.error(f"检测失败: {response.get('error')}")
detections = []
# 清理临时文件
try:
os.remove(temp_input)
except:
pass
return [YOLOResult(detections)]
except Exception as e:
logger.error(f"检测失败: {e}")
return [YOLOResult([])]
def predict(self, source, **kwargs):
"""兼容 predict 方法"""
return self.__call__(source, **kwargs)
@classmethod
def stop_server(cls):
"""停止常驻服务器"""
if cls._process is not None:
cls._process.terminate()
cls._process.wait()
cls._process = None
cls._initialized = False
logger.info("常驻服务器已停止")
# YOLOResult, Boxes, Box 类(与之前相同)
class YOLOResult:
"""模拟 YOLO 检测结果对象"""
def __init__(self, detections: List[Dict]):
self.detections = detections
self.names = {0: 'cigarette'}
self.boxes = Boxes(detections)
self.probs = None
self.keypoints = None
self.obb = None
self.speed = {'preprocess': 0, 'inference': 0, 'postprocess': 0}
def __len__(self):
return len(self.detections)
def __getitem__(self, idx):
if idx < len(self.detections):
return YOLOResult([self.detections[idx]])
return YOLOResult([])
def plot(self, **kwargs):
return None
class Boxes:
"""模拟 YOLO boxes 对象"""
def __init__(self, detections: List[Dict]):
self.detections = detections
try:
import torch
if detections:
xyxy_list = [[d['bbox'][0], d['bbox'][1], d['bbox'][2], d['bbox'][3]] for d in detections]
conf_list = [[d['confidence']] for d in detections]
cls_list = [[d['class']] for d in detections]
self.xyxy = torch.tensor(xyxy_list, dtype=torch.float32)
self.conf = torch.tensor(conf_list, dtype=torch.float32)
self.cls = torch.tensor(cls_list, dtype=torch.int64)
self.id = None
else:
self.xyxy = torch.empty((0, 4))
self.conf = torch.empty((0, 1))
self.cls = torch.empty((0, 1), dtype=torch.int64)
self.id = None
except ImportError:
import numpy as np
if detections:
xyxy_list = [[d['bbox'][0], d['bbox'][1], d['bbox'][2], d['bbox'][3]] for d in detections]
conf_list = [[d['confidence']] for d in detections]
cls_list = [[d['class']] for d in detections]
self.xyxy = np.array(xyxy_list, dtype=np.float32)
self.conf = np.array(conf_list, dtype=np.float32)
self.cls = np.array(cls_list, dtype=np.int64)
self.id = None
else:
self.xyxy = np.empty((0, 4), dtype=np.float32)
self.conf = np.empty((0, 1), dtype=np.float32)
self.cls = np.empty((0, 1), dtype=np.int64)
self.id = None
def __len__(self):
return len(self.detections)
def __iter__(self):
for i in range(len(self.detections)):
yield Box(self, i)
def cpu(self):
return self
def numpy(self):
if hasattr(self.xyxy, 'numpy'):
return type('Boxes', (), {
'xyxy': self.xyxy.numpy(),
'conf': self.conf.numpy(),
'cls': self.cls.numpy(),
'id': self.id
})()
return self
class Box:
"""模拟单个检测框对象"""
def __init__(self, boxes: Boxes, index: int):
self._boxes = boxes
self._index = index
@property
def xyxy(self):
import torch
import numpy as np
coords = self._boxes.xyxy[self._index]
if isinstance(coords, torch.Tensor):
return coords.unsqueeze(0)
else:
return np.array([coords])
@property
def conf(self):
import torch
import numpy as np
conf_val = self._boxes.conf[self._index]
if isinstance(conf_val, torch.Tensor):
return conf_val.view(1)
else:
return np.array([conf_val])
@property
def cls(self):
import torch
import numpy as np
cls_val = self._boxes.cls[self._index]
if isinstance(cls_val, torch.Tensor):
return cls_val.view(1)
else:
return np.array([cls_val])

View File

@@ -0,0 +1,377 @@
"""
YOLO 格式的抽烟检测模型适配器V2 简化版)
使用 Docker exec 在后台容器中执行检测
"""
import os
import cv2
import numpy as np
import subprocess
import tempfile
import logging
import time
import json
from typing import List, Dict, Optional
from pathlib import Path
logger = logging.getLogger(__name__)
class SmokingDetectionYOLO:
"""
模拟 YOLO 接口的抽烟检测模型V2 简化版)
使用 Docker exec 在后台容器中执行检测
"""
_container_name = "smoking-detection-server"
_initialized = False
def __init__(self, model_path=None):
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
# YOLO 兼容属性
self.names = {0: 'cigarette'}
self.model = self
# 检查 Docker 并启动后台容器
self._check_docker()
if self.available:
self._start_background_container()
logger.info(f"抽烟检测模型 V2 简化版初始化完成Docker可用: {self.available}")
def _check_docker(self):
"""检查 Docker 环境"""
try:
result = subprocess.run(
["docker", "info"],
capture_output=True,
text=True,
timeout=5
)
self.available = result.returncode == 0
if self.available:
result = subprocess.run(
["docker", "image", "inspect", self.docker_image],
capture_output=True,
text=True,
timeout=5
)
self.available = result.returncode == 0
except Exception as e:
logger.error(f"Docker 检查失败: {e}")
self.available = False
def _start_background_container(self):
"""启动后台容器"""
try:
# 检查容器是否已在运行
result = subprocess.run(
["docker", "ps", "-q", "-f", f"name={self._container_name}"],
capture_output=True,
text=True,
timeout=5
)
if result.stdout.strip():
logger.info(f"后台容器已在运行: {self._container_name}")
SmokingDetectionYOLO._initialized = True
return
# 检查容器是否存在但已停止
result = subprocess.run(
["docker", "ps", "-aq", "-f", f"name={self._container_name}"],
capture_output=True,
text=True,
timeout=5
)
if result.stdout.strip():
# 启动已存在的容器
logger.info(f"启动已存在的容器: {self._container_name}")
subprocess.run(
["docker", "start", self._container_name],
capture_output=True,
timeout=10
)
else:
# 创建新容器(保持运行)
logger.info(f"创建后台容器: {self._container_name}")
subprocess.run(
[
"docker", "run", "-d",
"--name", self._container_name,
"-v", "/tmp:/workspace/input",
"-v", "/Users/wwh/project/video-model/PaddlePaddle/PaddleDetection-release-2.9/smoking_server.py:/workspace/PaddleDetection/smoking_server.py",
"-w", "/workspace/PaddleDetection",
self.docker_image,
"tail", "-f", "/dev/null"
],
capture_output=True,
timeout=10
)
SmokingDetectionYOLO._initialized = True
logger.info("后台容器启动成功")
except Exception as e:
logger.error(f"启动后台容器失败: {e}")
SmokingDetectionYOLO._initialized = False
def __call__(self, source, conf=0.1, iou=0.45, verbose=False, stream=False):
"""模拟 YOLO 模型的调用接口"""
if not self.available:
logger.error("Docker 不可用,无法运行检测")
return [YOLOResult([])]
# 处理不同类型的输入
if isinstance(source, str):
image = cv2.imread(source)
if image is None:
logger.error(f"无法读取图片: {source}")
return [YOLOResult([])]
return self._detect_single(image, conf, verbose)
elif isinstance(source, np.ndarray):
return self._detect_single(source, conf, verbose)
elif isinstance(source, list):
results = []
for img in source:
if isinstance(img, str):
img = cv2.imread(img)
if img is not None:
results.extend(self._detect_single(img, conf, verbose))
return results
else:
logger.error(f"不支持的输入类型: {type(source)}")
return [YOLOResult([])]
def _detect_single(self, image: np.ndarray, conf: float, verbose: bool) -> List['YOLOResult']:
"""检测单张图片(使用 docker exec"""
start_time = time.time()
try:
# 创建临时文件
input_filename = f"smoking_v2_{int(time.time()*1000)}.jpg"
temp_input = f"/tmp/{input_filename}"
# 保存输入图片
cv2.imwrite(temp_input, image)
if verbose:
logger.info(f"正在检测: {temp_input}")
# 使用 docker exec 执行检测
cmd = [
"docker", "exec", "-i",
self._container_name,
"python", "smoking_server.py"
]
# 发送请求
request = {
'image_path': f'/workspace/input/{input_filename}',
'threshold': conf
}
result = subprocess.run(
cmd,
input=json.dumps(request) + '\n',
capture_output=True,
text=True,
timeout=60
)
elapsed = time.time() - start_time
if verbose:
logger.info(f"检测完成,耗时: {elapsed:.2f}")
# 解析结果
try:
# 找到最后一行 JSON 输出
lines = result.stdout.strip().split('\n')
json_line = None
for line in reversed(lines):
line = line.strip()
if line.startswith('{'):
json_line = line
break
if json_line:
response = json.loads(json_line)
if response.get('success'):
detections = response.get('detections', [])
else:
logger.error(f"检测失败: {response.get('error')}")
detections = []
else:
logger.error(f"无法解析输出: {result.stdout}")
detections = []
except Exception as e:
logger.error(f"解析结果失败: {e}, stdout: {result.stdout}")
detections = []
# 清理临时文件
try:
os.remove(temp_input)
except:
pass
return [YOLOResult(detections)]
except subprocess.TimeoutExpired:
logger.error("检测超时")
return [YOLOResult([])]
except Exception as e:
logger.error(f"检测失败: {e}")
return [YOLOResult([])]
def predict(self, source, **kwargs):
"""兼容 predict 方法"""
return self.__call__(source, **kwargs)
@classmethod
def stop_server(cls):
"""停止后台容器"""
try:
subprocess.run(
["docker", "stop", cls._container_name],
capture_output=True,
timeout=10
)
logger.info("后台容器已停止")
except Exception as e:
logger.error(f"停止后台容器失败: {e}")
# YOLOResult, Boxes, Box 类(与之前相同)
class YOLOResult:
"""模拟 YOLO 检测结果对象"""
def __init__(self, detections: List[Dict]):
self.detections = detections
self.names = {0: 'cigarette'}
self.boxes = Boxes(detections)
self.probs = None
self.keypoints = None
self.obb = None
self.speed = {'preprocess': 0, 'inference': 0, 'postprocess': 0}
def __len__(self):
return len(self.detections)
def __getitem__(self, idx):
if idx < len(self.detections):
return YOLOResult([self.detections[idx]])
return YOLOResult([])
def plot(self, **kwargs):
return None
class Boxes:
"""模拟 YOLO boxes 对象"""
def __init__(self, detections: List[Dict]):
self.detections = detections
try:
import torch
if detections:
xyxy_list = [[d['bbox'][0], d['bbox'][1], d['bbox'][2], d['bbox'][3]] for d in detections]
conf_list = [[d['confidence']] for d in detections]
cls_list = [[d['class']] for d in detections]
self.xyxy = torch.tensor(xyxy_list, dtype=torch.float32)
self.conf = torch.tensor(conf_list, dtype=torch.float32)
self.cls = torch.tensor(cls_list, dtype=torch.int64)
self.id = None
else:
self.xyxy = torch.empty((0, 4))
self.conf = torch.empty((0, 1))
self.cls = torch.empty((0, 1), dtype=torch.int64)
self.id = None
except ImportError:
import numpy as np
if detections:
xyxy_list = [[d['bbox'][0], d['bbox'][1], d['bbox'][2], d['bbox'][3]] for d in detections]
conf_list = [[d['confidence']] for d in detections]
cls_list = [[d['class']] for d in detections]
self.xyxy = np.array(xyxy_list, dtype=np.float32)
self.conf = np.array(conf_list, dtype=np.float32)
self.cls = np.array(cls_list, dtype=np.int64)
self.id = None
else:
self.xyxy = np.empty((0, 4), dtype=np.float32)
self.conf = np.empty((0, 1), dtype=np.float32)
self.cls = np.empty((0, 1), dtype=np.int64)
self.id = None
def __len__(self):
return len(self.detections)
def __iter__(self):
for i in range(len(self.detections)):
yield Box(self, i)
def cpu(self):
return self
def numpy(self):
if hasattr(self.xyxy, 'numpy'):
return type('Boxes', (), {
'xyxy': self.xyxy.numpy(),
'conf': self.conf.numpy(),
'cls': self.cls.numpy(),
'id': self.id
})()
return self
class Box:
"""模拟单个检测框对象"""
def __init__(self, boxes: Boxes, index: int):
self._boxes = boxes
self._index = index
@property
def xyxy(self):
import torch
import numpy as np
coords = self._boxes.xyxy[self._index]
if isinstance(coords, torch.Tensor):
return coords.unsqueeze(0)
else:
return np.array([coords])
@property
def conf(self):
import torch
import numpy as np
conf_val = self._boxes.conf[self._index]
if isinstance(conf_val, torch.Tensor):
return conf_val.view(1)
else:
return np.array([conf_val])
@property
def cls(self):
import torch
import numpy as np
cls_val = self._boxes.cls[self._index]
if isinstance(cls_val, torch.Tensor):
return cls_val.view(1)
else:
return np.array([cls_val])