打架斗殴模型集成
This commit is contained in:
@@ -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")
|
||||||
|
|
||||||
|
|||||||
265
apps/server/services/action_detection_service.py
Normal file
265
apps/server/services/action_detection_service.py
Normal 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
|
||||||
@@ -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:
|
||||||
|
|||||||
48
apps/web/package-lock.json
generated
48
apps/web/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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'
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">支持 MP4、AVI 等格式</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(() => {
|
||||||
const isImage = file.type.startsWith('image/')
|
if (!dockerResult.value) return ''
|
||||||
if (!isImage) {
|
return JSON.stringify(dockerResult.value, null, 2)
|
||||||
ElMessage.error('只能上传图片文件')
|
})
|
||||||
|
|
||||||
|
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
|
return false
|
||||||
|
} else {
|
||||||
|
const isImage = file.type.startsWith('image/')
|
||||||
|
if (!isImage) {
|
||||||
|
ElMessage.error('只能上传图片文件')
|
||||||
|
isDetecting.value = false
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
originalImage.value = URL.createObjectURL(file)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
originalImage.value = URL.createObjectURL(file)
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user