打架斗殴模型集成

This commit is contained in:
lubimu-647
2026-06-05 09:27:01 +08:00
parent 6819e57d79
commit 32e5dfa973
7 changed files with 632 additions and 87 deletions

View File

@@ -87,6 +87,14 @@ app.add_middleware(
) )
app.mount("/static", StaticFiles(directory="static"), name="static") app.mount("/static", StaticFiles(directory="static"), name="static")
docker_output_dir = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
"external", "video-recognition-system", "PaddlePaddle", "PaddleDetection", "output"
)
os.makedirs(docker_output_dir, exist_ok=True)
app.mount("/docker-output", StaticFiles(directory=docker_output_dir), name="docker-output")
app.include_router(detection.router, prefix="/api") app.include_router(detection.router, prefix="/api")
app.include_router(models.router, prefix="/api") app.include_router(models.router, prefix="/api")

View File

@@ -0,0 +1,265 @@
"""
PaddleVideo 行为识别服务适配器Docker API 调用方式)
通过 HTTP 请求调用运行在 Docker 中的 ppTSM 行为识别模型
"""
import os
import cv2
import numpy as np
import logging
import httpx
import base64
import json
from typing import Dict, List, Optional
logger = logging.getLogger(__name__)
# 获取外部项目路径
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
EXTERNAL_PADDLE_PATH = os.path.join(BASE_DIR, 'external', 'video-recognition-system', 'PaddlePaddle')
class ActionDetectionService:
"""行为识别服务(调用外部 Docker 服务)"""
def __init__(self):
# 从环境变量获取 Docker 服务地址
self.api_base_url = os.environ.get(
'ACTION_DETECTION_API_URL',
'http://localhost:8081' # 统一使用 8081 端口
)
self.timeout = int(os.environ.get('ACTION_DETECTION_TIMEOUT', '30'))
# 类别定义(根据你的 ppTSM 模型)
self.classes = ['fight', 'normal']
self.labels = {0: 'fight', 1: 'normal'}
# 外部项目路径
self.external_paddle_path = EXTERNAL_PADDLE_PATH
self.model_path = os.path.join(self.external_paddle_path, 'PaddleVideo', 'inference', 'ppTSM')
# 健康检查
self.available = self._check_service_health()
logger.info(f"行为识别服务初始化完成")
logger.info(f"API地址: {self.api_base_url}")
logger.info(f"外部项目路径: {self.external_paddle_path}")
logger.info(f"服务可用: {self.available}")
def _check_service_health(self) -> bool:
"""检查 Docker 服务是否可用"""
try:
with httpx.Client(timeout=5) as client:
response = client.get(f"{self.api_base_url}/health")
return response.status_code == 200
except Exception as e:
logger.warning(f"服务健康检查失败: {e}")
return False
def detect_image(self, image: np.ndarray, threshold: float = 0.5) -> Dict:
"""
调用 Docker 服务进行行为识别
Args:
image: OpenCV 图片 (BGR格式)
threshold: 置信度阈值
Returns:
检测结果字典
"""
if not self.available:
return {
'success': False,
'message': '行为识别服务不可用',
'detections': [],
'stats': None
}
try:
# 将图片转换为 base64
_, img_encoded = cv2.imencode('.jpg', image)
img_base64 = base64.b64encode(img_encoded).decode('utf-8')
# 构建请求数据
payload = {
'image': img_base64,
'threshold': threshold
}
# 调用 Docker 服务
with httpx.Client(timeout=self.timeout) as client:
response = client.post(
f"{self.api_base_url}/api/detect",
json=payload
)
if response.status_code != 200:
return {
'success': False,
'message': f"API调用失败: {response.status_code}",
'detections': [],
'stats': None
}
# 解析响应
result = response.json()
return {
'success': True,
'message': '检测完成',
'detections': result.get('detections', []),
'stats': result.get('stats', None)
}
except Exception as e:
logger.error(f"检测失败: {e}")
return {
'success': False,
'message': f'检测失败: {e}',
'detections': [],
'stats': None
}
class ActionDetectionModel:
"""行为识别模型包装器,兼容 YOLO 接口"""
def __init__(self):
self.service = ActionDetectionService()
self.names = {0: 'fight', 1: 'normal'}
def __call__(self, image, conf=0.5, iou=0.45, verbose=False):
"""
模拟 YOLO 模型的调用接口
Args:
image: OpenCV 图片
conf: 置信度阈值
iou: IoU 阈值
verbose: 是否输出详细信息
Returns:
模拟 YOLO 结果的对象
"""
result = self.service.detect_image(image, threshold=conf)
return [ActionDetectionResult(result, self.names)]
class ActionDetectionResult:
"""模拟 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:
if 'bbox' in det:
xyxy.append(det['bbox'])
conf.append(det.get('confidence', 0.0))
cls.append(det.get('class_id', 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

View File

@@ -109,6 +109,15 @@ class ModelService:
'size': '200MB', 'size': '200MB',
'description': '基于PaddlePaddle PP-YOLOE-l的违停检测模型支持车牌识别', 'description': '基于PaddlePaddle PP-YOLOE-l的违停检测模型支持车牌识别',
'name': '违停检测 (Paddle)' 'name': '违停检测 (Paddle)'
},
'action_detection': {
'path': 'docker_api',
'type': 'docker_api',
'classes': ['fight', 'normal'],
'labels': {'fight': '打架', 'normal': '正常'},
'size': 'Docker',
'description': '基于PaddleVideo ppTSM的打架检测模型通过Docker API调用',
'name': '打架检测 (Docker)'
} }
} }
@@ -117,9 +126,12 @@ class ModelService:
for model_id, config in self.model_configs.items(): for model_id, config in self.model_configs.items():
model_path = config['path'] model_path = config['path']
# 检查模型是否存在Paddle模型检查目录YOLO模型检查文件 # 检查模型是否存在
model_exists = False model_exists = False
if config['type'] == 'paddle': if config['type'] == 'docker_api':
# Docker API 类型的模型不需要检查文件,总是可用
model_exists = True
elif config['type'] == 'paddle':
model_dir = os.path.dirname(model_path) model_dir = os.path.dirname(model_path)
required_files = ['model.pdmodel', 'model.pdiparams', 'infer_cfg.yml'] required_files = ['model.pdmodel', 'model.pdiparams', 'infer_cfg.yml']
model_exists = all( model_exists = all(
@@ -153,6 +165,24 @@ class ModelService:
config = self.model_configs[model_id] config = self.model_configs[model_id]
# 处理 Docker API 模型
if config['type'] == 'docker_api':
try:
if model_id == 'action_detection':
from .action_detection_service import ActionDetectionModel
logger.info(f"正在加载 Docker API 行为识别服务: {model_id}")
model = ActionDetectionModel()
else:
logger.error(f"未知的 Docker API 模型类型: {model_id}")
return None
self.models[model_id] = model
logger.info(f"Docker API 服务加载成功: {model_id}")
return model
except Exception as e:
logger.error(f"Docker API 服务加载失败: {model_id}, 错误: {e}")
return None
# 处理 PaddleDetection 模型 # 处理 PaddleDetection 模型
if config['type'] == 'paddle': if config['type'] == 'paddle':
try: try:

View File

@@ -609,9 +609,6 @@
"arm" "arm"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -626,9 +623,6 @@
"arm" "arm"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -643,9 +637,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -660,9 +651,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -677,9 +665,6 @@
"loong64" "loong64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -694,9 +679,6 @@
"loong64" "loong64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -711,9 +693,6 @@
"ppc64" "ppc64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -728,9 +707,6 @@
"ppc64" "ppc64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -745,9 +721,6 @@
"riscv64" "riscv64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -762,9 +735,6 @@
"riscv64" "riscv64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -779,9 +749,6 @@
"s390x" "s390x"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -796,9 +763,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -813,9 +777,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -924,6 +885,7 @@
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/lodash": "*" "@types/lodash": "*"
} }
@@ -1498,13 +1460,15 @@
"version": "4.18.1", "version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/lodash-es": { "node_modules/lodash-es": {
"version": "4.18.1", "version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
"integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/lodash-unified": { "node_modules/lodash-unified": {
"version": "1.0.3", "version": "1.0.3",
@@ -1717,6 +1681,7 @@
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.21.3", "esbuild": "^0.21.3",
"postcss": "^8.4.43", "postcss": "^8.4.43",
@@ -1776,6 +1741,7 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.34.tgz", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.34.tgz",
"integrity": "sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==", "integrity": "sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@vue/compiler-dom": "3.5.34", "@vue/compiler-dom": "3.5.34",
"@vue/compiler-sfc": "3.5.34", "@vue/compiler-sfc": "3.5.34",

View File

@@ -5,6 +5,11 @@ const api = axios.create({
timeout: 30000 timeout: 30000
}) })
const dockerApi = axios.create({
baseURL: '/docker-api',
timeout: 120000
})
export const detectionApi = { export const detectionApi = {
getModels() { getModels() {
return api.get('/models') return api.get('/models')
@@ -26,5 +31,13 @@ export const detectionApi = {
}, },
params params
}) })
},
detectVideo(formData) {
return dockerApi.post('/api/fight_detect', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
} }
} }

View File

@@ -36,25 +36,51 @@
<el-upload <el-upload
:action="uploadUrl" :action="uploadUrl"
:on-success="handleUploadSuccess" :on-success="handleUploadSuccess"
:on-error="handleUploadError"
:before-upload="beforeUpload" :before-upload="beforeUpload"
:show-file-list="false" :show-file-list="false"
accept="image/*" :accept="isActionDetection ? 'video/*' : 'image/*'"
:disabled="isDetecting"
class="header-upload" class="header-upload"
> >
<el-button type="primary" size="small"> <el-button type="primary" size="small" :loading="isDetecting">
<el-icon><UploadFilled /></el-icon> <el-icon><UploadFilled /></el-icon>
<span>{{ resultImage ? '上传新图片' : '上传图片' }}</span> <span>{{ resultImage || resultVideo ? '上传新文件' : (isActionDetection ? '上传视频' : '上传图片') }}</span>
</el-button> </el-button>
</el-upload> </el-upload>
</div> </div>
</template> </template>
<div class="image-container"> <div class="image-container">
<!-- 图片显示 -->
<img <img
v-if="resultImage" v-if="resultImage && !isActionDetection"
:src="resultImage" :src="resultImage"
class="display-image" class="display-image"
alt="检测结果" alt="检测结果"
/> />
<!-- 视频显示 -->
<div v-else-if="isActionDetection" class="video-result-container">
<video
v-if="resultVideo"
:src="resultVideo"
class="display-video"
controls
alt="检测结果视频"
/>
<div v-else-if="isDetecting" class="detection-loading">
<div class="loading-spinner">
<el-icon class="loading-icon"><Loading /></el-icon>
</div>
<p class="loading-text">正在进行打架检测...</p>
<p class="loading-hint">请稍候视频检测可能需要几秒钟</p>
</div>
<div v-else class="empty-placeholder">
<el-icon class="empty-icon"><VideoCamera /></el-icon>
<p class="empty-text">请上传视频进行检测</p>
<p class="empty-hint">支持 MP4AVI 等格式</p>
</div>
</div>
<!-- 默认空状态 -->
<div v-else class="empty-placeholder"> <div v-else class="empty-placeholder">
<el-icon class="empty-icon"><Picture /></el-icon> <el-icon class="empty-icon"><Picture /></el-icon>
<p class="empty-text">请上传图片进行检测</p> <p class="empty-text">请上传图片进行检测</p>
@@ -97,8 +123,21 @@
</el-col> </el-col>
</el-row> </el-row>
<!-- Docker 检测结果 JSON 显示 -->
<el-card v-if="dockerResult && isActionDetection" class="json-result-card" shadow="hover">
<template #header>
<div class="card-header">
<el-icon class="header-icon"><Files /></el-icon>
<span>检测结果 (JSON)</span>
</div>
</template>
<div class="json-content">
<pre class="json-pre">{{ formattedJson }}</pre>
</div>
</el-card>
<!-- 检测详情 --> <!-- 检测详情 -->
<el-card v-if="detections.length > 0" class="details-card" shadow="hover"> <el-card v-if="detections.length > 0 && !isActionDetection" class="details-card" shadow="hover">
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<el-icon class="header-icon"><List /></el-icon> <el-icon class="header-icon"><List /></el-icon>
@@ -121,6 +160,40 @@
</el-table-column> </el-table-column>
</el-table> </el-table>
</el-card> </el-card>
<!-- 打架检测结果卡片 -->
<el-card v-if="actionResult && isActionDetection" class="action-result-card" shadow="hover">
<template #header>
<div class="card-header">
<el-icon class="header-icon"><Warning /></el-icon>
<span>打架检测结果</span>
</div>
</template>
<div class="action-result-content">
<div class="result-item">
<span class="result-label">视频名称</span>
<span class="result-value">{{ actionResult.video_name }}</span>
</div>
<div class="result-item">
<span class="result-label">检测结果</span>
<el-tag :type="actionResult.result === 'fight' ? 'danger' : 'success'" size="large">
{{ actionResult.result_text }}
</el-tag>
</div>
<div class="result-item">
<span class="result-label">置信度</span>
<el-tag :type="getConfidenceType(actionResult.confidence / 100)" size="large">
{{ actionResult.confidence_text }}
</el-tag>
</div>
<div class="result-item">
<span class="result-label">置信度阈值</span>
<el-tag type="info" size="large">
{{ actionResult.confidence_threshold?.toFixed(2) }}
</el-tag>
</div>
</div>
</el-card>
</div> </div>
</div> </div>
</template> </template>
@@ -131,9 +204,13 @@ import { ElMessage } from 'element-plus'
import { import {
UploadFilled, UploadFilled,
Picture, Picture,
VideoCamera,
View, View,
DataLine, DataLine,
List List,
Files,
Warning,
Loading
} from '@element-plus/icons-vue' } from '@element-plus/icons-vue'
import { detectionApi } from '@/api/detection' import { detectionApi } from '@/api/detection'
import DetectionConfig from './DetectionConfig.vue' import DetectionConfig from './DetectionConfig.vue'
@@ -192,16 +269,29 @@ const stopResize = () => {
const originalImage = ref('') const originalImage = ref('')
const resultImage = ref('') const resultImage = ref('')
const uploadedVideoUrl = ref('')
const resultVideo = ref('')
const detections = ref([]) const detections = ref([])
const stats = ref(null) const stats = ref(null)
const alerts = ref([])
const isDetecting = ref(false)
const dockerResult = ref(null)
const actionResult = ref(null)
const isActionDetection = computed(() => {
return config.value.model === 'action_detection'
})
const uploadUrl = computed(() => { const uploadUrl = computed(() => {
if (isActionDetection.value) {
return null
}
const params = new URLSearchParams({ const params = new URLSearchParams({
model_id: config.value.model, model_id: config.value.model,
confidence: config.value.confidence, confidence: config.value.confidence,
iou: config.value.iou iou: config.value.iou
}) })
// 添加算法配置
if (config.value.algorithmConfig && Object.keys(config.value.algorithmConfig).length > 0) { if (config.value.algorithmConfig && Object.keys(config.value.algorithmConfig).length > 0) {
params.append('algorithm_config', JSON.stringify(config.value.algorithmConfig)) params.append('algorithm_config', JSON.stringify(config.value.algorithmConfig))
} }
@@ -209,35 +299,61 @@ const uploadUrl = computed(() => {
return `/api/detect/image?${params.toString()}` return `/api/detect/image?${params.toString()}`
}) })
const beforeUpload = (file) => { const formattedJson = computed(() => {
if (!dockerResult.value) return ''
return JSON.stringify(dockerResult.value, null, 2)
})
const beforeUpload = async (file) => {
isDetecting.value = true
if (isActionDetection.value) {
const isVideo = file.type.startsWith('video/')
if (!isVideo) {
ElMessage.error('只能上传视频文件')
isDetecting.value = false
return false
}
const formData = new FormData()
formData.append('video', file)
formData.append('confidence', config.value.confidence)
try {
const response = await detectionApi.detectVideo(formData)
handleActionDetectionSuccess(response.data)
} catch (error) {
handleUploadError(error)
}
return false
} else {
const isImage = file.type.startsWith('image/') const isImage = file.type.startsWith('image/')
if (!isImage) { if (!isImage) {
ElMessage.error('只能上传图片文件') ElMessage.error('只能上传图片文件')
isDetecting.value = false
return false return false
} }
originalImage.value = URL.createObjectURL(file) originalImage.value = URL.createObjectURL(file)
return true return true
}
} }
const handleUploadSuccess = (response) => { const handleUploadSuccess = (response) => {
isDetecting.value = false
console.log('Upload success response:', response) console.log('Upload success response:', response)
if (response.success) { if (response.success) {
// 使用 base64 图片数据
if (response.data.image_base64) { if (response.data.image_base64) {
resultImage.value = `data:image/jpeg;base64,${response.data.image_base64}` resultImage.value = `data:image/jpeg;base64,${response.data.image_base64}`
console.log('Result image set, length:', response.data.image_base64.length)
} else { } else {
console.error('No image_base64 in response:', response.data) console.error('No image_base64 in response:', response.data)
} }
detections.value = response.data.detections || [] detections.value = response.data.detections || []
stats.value = response.data.stats stats.value = response.data.stats
// 处理告警信息
if (response.data.alerts && response.data.alerts.length > 0) { if (response.data.alerts && response.data.alerts.length > 0) {
alerts.value = response.data.alerts alerts.value = response.data.alerts
console.log('收到告警:', response.data.alerts)
// 显示告警通知
response.data.alerts.forEach(alert => { response.data.alerts.forEach(alert => {
ElMessage({ ElMessage({
message: `行为告警: ${alert.type} - ${alert.message}`, message: `行为告警: ${alert.type} - ${alert.message}`,
@@ -253,6 +369,38 @@ const handleUploadSuccess = (response) => {
} }
} }
const handleActionDetectionSuccess = (response) => {
isDetecting.value = false
console.log('Action detection response:', response)
if (response.code === 200) {
dockerResult.value = response
actionResult.value = response.data
if (response.data.output_video) {
const videoPath = response.data.output_video.replace(/^output\//, '')
resultVideo.value = `/docker-output/${videoPath}`
}
ElMessage.success(response.message)
} else {
ElMessage.error(response.message || '检测失败')
}
}
const handleUploadError = (error) => {
isDetecting.value = false
console.error('Upload error:', error)
if (error.response) {
ElMessage.error(`上传失败: ${error.response.data.message || error.response.status}`)
} else if (error.request) {
ElMessage.error('请求超时或服务器未响应请检查Docker服务是否正常运行')
} else {
ElMessage.error(`上传失败: ${error.message}`)
}
}
const getConfidenceType = (confidence) => { const getConfidenceType = (confidence) => {
if (!confidence && confidence !== 0) return 'info' if (!confidence && confidence !== 0) return 'info'
if (confidence >= 0.8) return 'success' if (confidence >= 0.8) return 'success'
@@ -285,7 +433,6 @@ const modelName = computed(() => {
overflow-y: auto; overflow-y: auto;
} }
/* 拖拽调整条 */
.resize-handle { .resize-handle {
width: 8px; width: 8px;
flex-shrink: 0; flex-shrink: 0;
@@ -326,7 +473,6 @@ const modelName = computed(() => {
overflow-x: hidden; overflow-x: hidden;
} }
/* 卡片通用样式 */
:deep(.el-card) { :deep(.el-card) {
border-radius: 16px; border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.1);
@@ -348,7 +494,6 @@ const modelName = computed(() => {
border-radius: 16px 16px 0 0; border-radius: 16px 16px 0 0;
} }
/* 卡片头部样式 */
.card-header { .card-header {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -374,7 +519,6 @@ const modelName = computed(() => {
display: block; display: block;
} }
/* 配置卡片样式 */
.config-card { .config-card {
height: auto; height: auto;
max-height: calc(100vh - 100px); max-height: calc(100vh - 100px);
@@ -401,14 +545,12 @@ const modelName = computed(() => {
width: 100%; width: 100%;
} }
/* 模型选择器 */
.model-size { .model-size {
float: right; float: right;
color: #64748B; color: #64748B;
font-size: 12px; font-size: 12px;
} }
/* 滑块样式 */
.slider-value { .slider-value {
text-align: center; text-align: center;
font-size: 14px; font-size: 14px;
@@ -424,7 +566,6 @@ const modelName = computed(() => {
font-family: 'Fira Code', monospace; font-family: 'Fira Code', monospace;
} }
/* 空状态占位区域 */
.empty-placeholder { .empty-placeholder {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -444,8 +585,53 @@ const modelName = computed(() => {
.empty-text { .empty-text {
font-size: 16px; font-size: 16px;
color: #94A3B8; color: #94A3B8;
}
.detection-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
color: #64748B;
}
.loading-spinner {
display: flex;
align-items: center;
justify-content: center;
width: 80px;
height: 80px;
margin-bottom: 16px;
}
.loading-icon {
font-size: 48px;
color: #22C55E;
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.loading-text {
font-size: 16px;
color: #F8FAFC;
font-weight: 500;
margin: 0 0 8px 0; margin: 0 0 8px 0;
font-family: 'Fira Sans', sans-serif; }
.loading-hint {
font-size: 13px;
color: #64748B;
margin: 0;
} }
.empty-hint { .empty-hint {
@@ -454,7 +640,6 @@ const modelName = computed(() => {
margin: 0; margin: 0;
} }
/* 图片展示区域 */
.image-card { .image-card {
margin-bottom: 20px; margin-bottom: 20px;
} }
@@ -488,6 +673,21 @@ const modelName = computed(() => {
background: #000; background: #000;
} }
.video-result-container {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.display-video {
width: 100%;
height: 100%;
object-fit: contain;
background: #000;
}
.placeholder { .placeholder {
text-align: center; text-align: center;
color: #64748B; color: #64748B;
@@ -505,7 +705,6 @@ const modelName = computed(() => {
margin: 0; margin: 0;
} }
/* 统计卡片 */
.stats-card { .stats-card {
margin-bottom: 20px; margin-bottom: 20px;
height: calc(100% - 20px); height: calc(100% - 20px);
@@ -553,7 +752,6 @@ const modelName = computed(() => {
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.1);
} }
/* 详情卡片 */
.details-card { .details-card {
margin-bottom: 20px; margin-bottom: 20px;
} }
@@ -627,7 +825,6 @@ const modelName = computed(() => {
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.1);
} }
/* 帮助图标样式 */
.help-icon { .help-icon {
margin-left: 6px; margin-left: 6px;
font-size: 14px; font-size: 14px;
@@ -640,7 +837,6 @@ const modelName = computed(() => {
color: #22C55E; color: #22C55E;
} }
/* 告警卡片 */
.alerts-card { .alerts-card {
margin-bottom: 20px; margin-bottom: 20px;
border: 2px solid #EF4444; border: 2px solid #EF4444;
@@ -722,7 +918,70 @@ const modelName = computed(() => {
font-family: 'Fira Code', monospace; font-family: 'Fira Code', monospace;
} }
/* 响应式布局 */ .json-result-card {
margin-bottom: 20px;
}
.json-result-card :deep(.el-card__body) {
padding: 20px;
}
.json-content {
background: #0F172A;
border-radius: 12px;
padding: 16px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.json-pre {
color: #22C55E;
font-family: 'Fira Code', monospace;
font-size: 13px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-all;
margin: 0;
max-height: 400px;
overflow-y: auto;
}
.action-result-card {
margin-bottom: 20px;
}
.action-result-card :deep(.el-card__body) {
padding: 20px;
}
.action-result-content {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.result-item {
display: flex;
flex-direction: column;
gap: 8px;
padding: 16px;
background: rgba(15, 23, 42, 0.6);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
text-align: center;
}
.result-label {
font-size: 13px;
color: #94A3B8;
font-weight: 500;
}
.result-value {
font-size: 16px;
color: #F8FAFC;
font-weight: 600;
}
@media (max-width: 768px) { @media (max-width: 768px) {
.image-detection-container { .image-detection-container {
flex-direction: column; flex-direction: column;
@@ -751,14 +1010,8 @@ const modelName = computed(() => {
min-height: 200px; min-height: 200px;
} }
.stats-descriptions :deep(.el-descriptions__body) { .action-result-content {
display: grid; grid-template-columns: 1fr;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.stats-descriptions :deep(.el-descriptions__cell) {
padding: 12px;
} }
} }

View File

@@ -10,19 +10,29 @@ export default defineConfig({
} }
}, },
server: { server: {
host: '0.0.0.0',
port: 3001, port: 3001,
proxy: { proxy: {
'/api': { '/api': {
target: 'http://localhost:8000', target: 'http://localhost:8001',
changeOrigin: true changeOrigin: true
}, },
'/static': { '/static': {
target: 'http://localhost:8000', target: 'http://localhost:8001',
changeOrigin: true changeOrigin: true
}, },
'/ws': { '/ws': {
target: 'ws://localhost:8000', target: 'ws://localhost:8000',
ws: true ws: true
},
'/docker-api': {
target: 'http://localhost:8081',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/docker-api/, '')
},
'/docker-output': {
target: 'http://localhost:8001',
changeOrigin: true
} }
} }
} }