打架斗殴模型集成

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

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

View File

@@ -36,25 +36,51 @@
<el-upload
:action="uploadUrl"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
:before-upload="beforeUpload"
:show-file-list="false"
accept="image/*"
:accept="isActionDetection ? 'video/*' : 'image/*'"
:disabled="isDetecting"
class="header-upload"
>
<el-button type="primary" size="small">
<el-button type="primary" size="small" :loading="isDetecting">
<el-icon><UploadFilled /></el-icon>
<span>{{ resultImage ? '上传新图片' : '上传图片' }}</span>
<span>{{ resultImage || resultVideo ? '上传新文件' : (isActionDetection ? '上传视频' : '上传图片') }}</span>
</el-button>
</el-upload>
</div>
</template>
<div class="image-container">
<!-- 图片显示 -->
<img
v-if="resultImage"
v-if="resultImage && !isActionDetection"
:src="resultImage"
class="display-image"
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">
<el-icon class="empty-icon"><Picture /></el-icon>
<p class="empty-text">请上传图片进行检测</p>
@@ -97,8 +123,21 @@
</el-col>
</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>
<div class="card-header">
<el-icon class="header-icon"><List /></el-icon>
@@ -121,6 +160,40 @@
</el-table-column>
</el-table>
</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>
</template>
@@ -131,9 +204,13 @@ import { ElMessage } from 'element-plus'
import {
UploadFilled,
Picture,
VideoCamera,
View,
DataLine,
List
List,
Files,
Warning,
Loading
} from '@element-plus/icons-vue'
import { detectionApi } from '@/api/detection'
import DetectionConfig from './DetectionConfig.vue'
@@ -192,16 +269,29 @@ const stopResize = () => {
const originalImage = ref('')
const resultImage = ref('')
const uploadedVideoUrl = ref('')
const resultVideo = ref('')
const detections = ref([])
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(() => {
if (isActionDetection.value) {
return null
}
const params = new URLSearchParams({
model_id: config.value.model,
confidence: config.value.confidence,
iou: config.value.iou
})
// 添加算法配置
if (config.value.algorithmConfig && Object.keys(config.value.algorithmConfig).length > 0) {
params.append('algorithm_config', JSON.stringify(config.value.algorithmConfig))
}
@@ -209,35 +299,61 @@ const uploadUrl = computed(() => {
return `/api/detect/image?${params.toString()}`
})
const beforeUpload = (file) => {
const isImage = file.type.startsWith('image/')
if (!isImage) {
ElMessage.error('只能上传图片文件')
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/')
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) => {
isDetecting.value = false
console.log('Upload success response:', response)
if (response.success) {
// 使用 base64 图片数据
if (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 {
console.error('No image_base64 in response:', response.data)
}
detections.value = response.data.detections || []
stats.value = response.data.stats
// 处理告警信息
if (response.data.alerts && response.data.alerts.length > 0) {
alerts.value = response.data.alerts
console.log('收到告警:', response.data.alerts)
// 显示告警通知
response.data.alerts.forEach(alert => {
ElMessage({
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) => {
if (!confidence && confidence !== 0) return 'info'
if (confidence >= 0.8) return 'success'
@@ -285,7 +433,6 @@ const modelName = computed(() => {
overflow-y: auto;
}
/* 拖拽调整条 */
.resize-handle {
width: 8px;
flex-shrink: 0;
@@ -326,7 +473,6 @@ const modelName = computed(() => {
overflow-x: hidden;
}
/* 卡片通用样式 */
:deep(.el-card) {
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.1);
@@ -348,7 +494,6 @@ const modelName = computed(() => {
border-radius: 16px 16px 0 0;
}
/* 卡片头部样式 */
.card-header {
display: flex;
align-items: center;
@@ -374,7 +519,6 @@ const modelName = computed(() => {
display: block;
}
/* 配置卡片样式 */
.config-card {
height: auto;
max-height: calc(100vh - 100px);
@@ -401,14 +545,12 @@ const modelName = computed(() => {
width: 100%;
}
/* 模型选择器 */
.model-size {
float: right;
color: #64748B;
font-size: 12px;
}
/* 滑块样式 */
.slider-value {
text-align: center;
font-size: 14px;
@@ -424,7 +566,6 @@ const modelName = computed(() => {
font-family: 'Fira Code', monospace;
}
/* 空状态占位区域 */
.empty-placeholder {
display: flex;
flex-direction: column;
@@ -444,8 +585,53 @@ const modelName = computed(() => {
.empty-text {
font-size: 16px;
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;
font-family: 'Fira Sans', sans-serif;
}
.loading-hint {
font-size: 13px;
color: #64748B;
margin: 0;
}
.empty-hint {
@@ -454,7 +640,6 @@ const modelName = computed(() => {
margin: 0;
}
/* 图片展示区域 */
.image-card {
margin-bottom: 20px;
}
@@ -488,6 +673,21 @@ const modelName = computed(() => {
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 {
text-align: center;
color: #64748B;
@@ -505,7 +705,6 @@ const modelName = computed(() => {
margin: 0;
}
/* 统计卡片 */
.stats-card {
margin-bottom: 20px;
height: calc(100% - 20px);
@@ -553,7 +752,6 @@ const modelName = computed(() => {
border: 1px solid rgba(255, 255, 255, 0.1);
}
/* 详情卡片 */
.details-card {
margin-bottom: 20px;
}
@@ -627,7 +825,6 @@ const modelName = computed(() => {
border: 1px solid rgba(255, 255, 255, 0.1);
}
/* 帮助图标样式 */
.help-icon {
margin-left: 6px;
font-size: 14px;
@@ -640,7 +837,6 @@ const modelName = computed(() => {
color: #22C55E;
}
/* 告警卡片 */
.alerts-card {
margin-bottom: 20px;
border: 2px solid #EF4444;
@@ -722,7 +918,70 @@ const modelName = computed(() => {
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) {
.image-detection-container {
flex-direction: column;
@@ -751,14 +1010,8 @@ const modelName = computed(() => {
min-height: 200px;
}
.stats-descriptions :deep(.el-descriptions__body) {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.stats-descriptions :deep(.el-descriptions__cell) {
padding: 12px;
.action-result-content {
grid-template-columns: 1fr;
}
}
@@ -768,4 +1021,4 @@ const modelName = computed(() => {
min-height: 180px;
}
}
</style>
</style>