摘要
本研究设计并实现了一套高效、可扩展且用户友好的实时人脸表情识别系统。系统核心采用最前沿的深度学习目标检测架构——YOLO系列模型(支持YOLOv8至YOLOv12的动态切换),在包含‘愤怒’、‘厌恶’、‘恐惧’、‘快乐’、‘中性’、‘悲伤’、‘惊讶’七类表情的自有数据集上进行训练与优化。为实现便捷的交互与高效的管理,本系统创新性地构建了前后端分离的现代化Web应用:前端基于Vue.js框架提供直观的图形界面,后端业务逻辑由Spring Boot框架处理,而核心检测服务则通过Python实现,确保了算法性能与系统可维护性的平衡。系统功能全面,不仅支持图像、视频及摄像头实时流的在线检测与结果记录,还集成了DeepSeek大型语言模型的智能分析模块,对检测结果进行深度语义解读。所有检测记录与用户信息均持久化存储于MySQL数据库,并配备了完善的数据可视化看板与用户管理体系。实验结果表明,该系统在测试集上达到了优异的识别准确率,并通过多模型支持与模块化设计,为表情识别的研究与应用提供了一个功能强大且灵活的技术平台。
关键词:人脸表情识别;YOLO;深度学习;前后端分离;Web应用;智能分析;数据可视化
目录
摘要
1. 引言
2. 背景与相关工作
3. 数据集介绍
功能模块
登录注册模块
可视化模块
图像检测模块
视频检测模块
实时检测模块
图片识别记录管理
视频识别记录管理
摄像头识别记录管理
用户管理模块
数据管理模块(MySQL表设计)
模型训练结果
YOLOv8
YOLOv10
YOLOv11
YOLOv12
前端代码展示
后端代码展示
项目源码+数据集下载链接
项目安装教程
1. 引言
人脸表情作为人类最直接、最丰富的非语言情感交流方式,在人机交互、心理健康评估、智能驾驶、安防监控及远程教育等领域具有极高的应用价值。传统表情识别方法多依赖于手工设计的特征(如LBP、HOG)与浅层分类器,在复杂光照、头部姿态变化及个体差异下鲁棒性不足。
近年来,以卷积神经网络为代表的深度学习技术,凭借其强大的特征自动提取与表征学习能力,已成为表情识别领域的主流解决方案。特别是以YOLO系列为代表的单阶段目标检测模型,以其卓越的实时性能与高精度,为部署实用的实时表情识别系统奠定了基础。然而,一个完整的应用系统不仅需要强大的算法内核,还需易用的交互界面、稳定的数据管理以及可扩展的架构设计。
基于此,本研究旨在解决算法研究与实际应用之间的鸿沟,开发一个集最新算法、智能交互、数据管理于一体的综合性人脸表情识别系统。本系统的主要贡献如下:
多模型集成与兼容性:无缝集成YOLOv8至YOLOv12等多个版本的先进检测模型,用户可根据需求在精度与速度之间灵活权衡,系统具备良好的算法迭代适应性。
三层架构与智能增强:采用“Vue.js前端 + Spring Boot后端 + Python算法服务”的前后端分离架构,实现高内聚、低耦合。创新性地引入DeepSeek大模型,提供超越简单分类标签的上下文分析与情感解读。
全功能应用平台:提供从用户登录注册、多源媒体(图/视频/实时流)检测、记录管理、数据可视化到用户权限控制的完整闭环功能,所有流程均与数据库联动,形成一个真正的生产级应用。
高质量数据集构建:收集并标注了一个包含7类基本表情、总计超过5500张人脸图像的数据集,为模型的训练与评估提供了可靠保障。
本论文后续章节将详细介绍系统的背景与相关工作、数据集构建、核心方法、系统设计与实现细节,以及实验结果与分析。
2. 背景与相关工作
2.1 人脸表情识别技术演进
人脸表情识别技术发展经历了从基于几何特征、表观特征到深度学习方法的历程。早期的AAM、ASM模型以及结合LBP、SIFT等特征的算法受限于手工特征的表达能力。深度学习,尤其是CNN,通过端到端的学习方式彻底改变了这一领域。从AlexNet、VGG在静态图像上的应用,到专注于表情的FER2013数据集挑战,再到利用注意力机制、多任务学习(如同时检测人脸与表情)的最新研究,识别精度和鲁棒性不断提升。
2.2 YOLO系列目标检测模型
YOLO将目标检测重构为单一回归问题,实现了速度与精度的革命性平衡。从YOLOv1到最新的YOLOv12,其改进主要体现在:更高效的网络骨干(如CSPDarknet)、更强的特征金字塔网络(如PANet)、更先进的损失函数(如CIoU Loss)以及无锚框(Anchor-Free)设计等。本系统选择集成YOLOv8及其后续变体,正是看中了其在保持高精度的同时,为实时检测提供的卓越性能,以及其活跃的社区支持和易于部署的特性。
2.3 表情识别系统开发现状
当前不少研究止步于算法模型在标准数据集上的精度报告,缺乏将其转化为具有完整业务流程的实用系统。现有的一些演示系统往往功能单一(仅支持图片上传)、架构陈旧(前后端耦合)、或缺乏数据管理与分析能力。本系统旨在填补这一空白,通过采用现代化的软件开发范式(前后端分离、微服务思想),构建一个集算法验证、用户交互、数据运营于一体的综合性平台。
3. 数据集介绍
本系统模型训练与评估基于一个自主收集并精细标注的人脸表情数据集。
数据内容与类别:数据集涵盖人类七种基本表情,符合心理学中普遍认可的“Ekman基本情绪”分类。具体类别名称与标签为:
Angry(愤怒)、Disgusted(厌恶)、Fearful(恐惧)、Happy(快乐)、Neutral(中性)、Sad(悲伤)、Surprised(惊讶)。数据划分:为科学地进行模型训练、验证与测试,数据集被严格划分为三个互斥的子集:
训练集:包含4,483张图像,用于模型权重的学习与更新。
验证集:包含550张图像,用于在训练过程中监控模型性能,进行超参数调优及早停判断,防止过拟合。
测试集:包含566张图像,作为完全独立的“黑盒”数据,仅在最终评估时使用,以客观反映模型对未知数据的泛化能力。
功能模块
✅ 用户登录注册:支持密码检测,保存到MySQL数据库。
✅ 支持四种YOLO模型切换,YOLOv8、YOLOv10、YOLOv11、YOLOv12。
✅ 信息可视化,数据可视化。
✅ 图片检测支持AI分析功能,deepseek
✅ 支持图像检测、视频检测和摄像头实时检测,检测结果保存到MySQL数据库。
✅ 图片识别记录管理、视频识别记录管理和摄像头识别记录管理。
✅ 用户管理模块,管理员可以对用户进行增删改查。
✅ 个人中心,可以修改自己的信息,密码姓名头像等等。
登录注册模块
可视化模块
图像检测模块
YOLO模型集成(v8/v10/v11/v12)
DeepSeek多模态分析
支持格式:JPG/PNG/MP4/RTSP
视频检测模块
实时检测模块
图片识别记录管理
视频识别记录管理
摄像头识别记录管理
用户管理模块
数据管理模块(MySQL表设计)
users- 用户信息表
imgrecords- 图片检测记录表
videorecords- 视频检测记录表
camerarecords- 摄像头检测记录表
模型训练结果
#coding:utf-8 #根据实际情况更换模型 # yolon.yaml (nano):轻量化模型,适合嵌入式设备,速度快但精度略低。 # yolos.yaml (small):小模型,适合实时任务。 # yolom.yaml (medium):中等大小模型,兼顾速度和精度。 # yolob.yaml (base):基本版模型,适合大部分应用场景。 # yolol.yaml (large):大型模型,适合对精度要求高的任务。 from ultralytics import YOLO model_path = 'pt/yolo12s.pt' data_path = 'data.yaml' if __name__ == '__main__': model = YOLO(model_path) results = model.train(data=data_path, epochs=500, batch=64, device='0', workers=0, project='runs', name='exp', )YOLOv8![]()
YOLOv10
YOLOv11
YOLOv12
前端代码展示
部分代码
<template> <div class="home-container layout-pd"> <el-row :gutter="15" class="home-card-two mb15"> <el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12"> <div class="home-card-item"> <div style="height: 100%" ref="homeLineRef"></div> </div> </el-col> <el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="home-media"> <div class="home-card-item"> <div style="height: 100%" ref="homePieRef"></div> </div> </el-col> </el-row> <el-row :gutter="15" class="home-card-three"> <el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="home-media"> <div class="home-card-item"> <div style="height: 100%" ref="homeradarRef"></div> </div> </el-col> <el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12"> <div class="home-card-item"> <div class="home-card-item-title">实时表情识别记录</div> <div class="home-monitor"> <div class="flex-warp"> <el-table :data="state.paginatedData" style="width: 100%" height="360" v-loading="state.loading"> <el-table-column prop="username" label="操作用户" align="center" width="120" /> <el-table-column prop="label" label="识别结果" align="center" width="120"> <template #default="scope"> <el-tag :type="getResultType(scope.row.label)" effect="light" > {{ formatLabel(scope.row.label) }} </el-tag> </template> </el-table-column> <el-table-column prop="confidence" label="置信度" align="center" width="120"> <template #default="scope"> {{ formatConfidence(scope.row.confidence) }} </template> </el-table-column> <el-table-column prop="weight" label="模型权重" align="center" width="120" /> <el-table-column prop="conf" label="识别阈值" align="center" width="120" /> <el-table-column prop="startTime" label="识别时间" align="center" width="180" /> <el-table-column label="操作" align="center" width="100"> <template #default="scope"> <el-button link type="primary" size="small" @click="handleViewDetail(scope.row)"> 详情 </el-button> </template> </el-table-column> </el-table> <div class="pagination-container"> <el-pagination v-model:current-page="state.currentPage" v-model:page-size="state.pageSize" :page-sizes="[10, 20, 50, 100]" :small="true" :layout="layout" :total="state.total" @size-change="handleSizeChange" @current-change="handleCurrentChange" /> </div> </div> </div> </div> </el-col> </el-row> <!-- 详情弹窗 --> <el-dialog v-model="state.detailDialogVisible" :title="`表情识别记录详情 - ${state.selectedRecord?.username || ''}`" width="80%" :close-on-click-modal="false" :close-on-press-escape="false" center > <div class="detail-container" v-loading="state.detailLoading"> <el-row :gutter="20"> <!-- 人脸图片 --> <el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12"> <div class="detail-section"> <h3 class="detail-title">原始图片</h3> <div class="image-container"> <div class="img-wrapper" @click="previewImage(getImageUrl(state.selectedRecord?.inputImg), '原始图片')"> <img :src="getImageUrl(state.selectedRecord?.inputImg)" alt="原始图片" class="detection-image" v-if="state.selectedRecord?.inputImg" /> <div class="img-overlay" v-if="state.selectedRecord?.inputImg"> <el-icon><View /></el-icon> </div> <div v-else class="image-placeholder"> <el-icon><Picture /></el-icon> <span>暂无原始图片</span> </div> </div> </div> </div> </el-col> <!-- 识别信息 --> <el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12"> <div class="detail-section"> <h3 class="detail-title">识别信息</h3> <el-descriptions :column="1" border> <el-descriptions-item label="操作用户"> {{ state.selectedRecord?.username || '未知' }} </el-descriptions-item> <el-descriptions-item label="识别结果"> <el-tag :type="getResultType(state.selectedRecord?.label || '')" effect="light" > {{ formatLabel(state.selectedRecord?.label || '') }} </el-tag> </el-descriptions-item> <el-descriptions-item label="置信度"> {{ formatConfidence(state.selectedRecord?.confidence || '') }} </el-descriptions-item> <el-descriptions-item label="模型权重"> {{ state.selectedRecord?.weight || '未知' }} </el-descriptions-item> <el-descriptions-item label="识别阈值"> {{ state.selectedRecord?.conf || '未知' }} </el-descriptions-item> <el-descriptions-item label="识别时间"> {{ state.selectedRecord?.startTime || '未知' }} </el-descriptions-item> <el-descriptions-item label="表情分析详情" v-if="hasDetectionDetails"> <div class="detection-details"> <div v-for="(item, index) in getDetectionDetails()" :key="index" class="detail-item" > <span class="detail-label">{{ item.label }}:</span> <span class="detail-value">{{ item.confidence }}</span> </div> </div> </el-descriptions-item> </el-descriptions> </div> </el-col> </el-row> <!-- 原图与识别结果对比 --> <el-row :gutter="20" v-if="state.selectedRecord?.inputImg || state.selectedRecord?.outImg"> <el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12"> <div class="detail-section"> <h3 class="detail-title">原始图片</h3> <div class="image-container"> <div class="img-wrapper" @click="previewImage(getImageUrl(state.selectedRecord.inputImg), '原始图片')"> <img :src="getImageUrl(state.selectedRecord.inputImg)" alt="原始图片" class="detection-image" v-if="state.selectedRecord?.inputImg" /> <div class="img-overlay" v-if="state.selectedRecord?.inputImg"> <el-icon><View /></el-icon> </div> <div v-else class="image-placeholder"> <el-icon><Picture /></el-icon> <span>暂无原始图片</span> </div> </div> </div> </div> </el-col> <el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12"> <div class="detail-section"> <h3 class="detail-title">标注图片</h3> <div class="image-container"> <div class="img-wrapper" @click="previewImage(getImageUrl(state.selectedRecord.outImg), '标注图片')"> <img :src="getImageUrl(state.selectedRecord.outImg)" alt="标注图片" class="detection-image" v-if="state.selectedRecord?.outImg" /> <div class="img-overlay" v-if="state.selectedRecord?.outImg"> <el-icon><View /></el-icon> </div> <div v-else class="image-placeholder"> <el-icon><Picture /></el-icon> <span>暂无标注图片</span> </div> </div> </div> </div> </el-col> </el-row> </div> <template #footer> <span class="dialog-footer"> <el-button @click="state.detailDialogVisible = false">关闭</el-button> <el-button type="primary" @click="handleDownloadImage" :disabled="!state.selectedRecord?.inputImg"> <el-icon><Download /></el-icon> 下载识别图片 </el-button> </span> </template> </el-dialog> <!-- 图片预览弹窗 --> <el-dialog v-model="state.previewDialog.visible" :title="state.previewDialog.title" width="60%" align-center class="image-preview-dialog"> <div class="preview-content"> <img :src="state.previewDialog.imageUrl" :alt="state.previewDialog.title" class="preview-image" /> </div> </el-dialog> </div> </template> <script setup lang="ts" name="home"> import { reactive, onMounted, ref, watch, nextTick, onActivated, markRaw, computed } from 'vue'; import * as echarts from 'echarts'; import { storeToRefs } from 'pinia'; import { useThemeConfig } from '/@/stores/themeConfig'; import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes'; import { ElMessage, ElMessageBox } from 'element-plus'; import { Picture, Download, View } from '@element-plus/icons-vue'; import request from '/@/utils/request'; // 定义变量内容 const homeLineRef = ref(); const homePieRef = ref(); const homeradarRef = ref(); const storesTagsViewRoutes = useTagsViewRoutes(); const storesThemeConfig = useThemeConfig(); const { themeConfig } = storeToRefs(storesThemeConfig); const { isTagsViewCurrenFull } = storeToRefs(storesTagsViewRoutes); // 表情类型定义 const EMOTION_TYPES = ['愤怒', '厌恶', '恐惧', '快乐', '中性', '悲伤', '惊讶']; // 表情颜色映射 const EMOTION_COLORS: Record<string, string> = { '愤怒': '#ff4d4f', // 红色 '厌恶': '#fa8c16', // 橙色 '恐惧': '#722ed1', // 紫色 '快乐': '#52c41a', // 绿色 '中性': '#1890ff', // 蓝色 '悲伤': '#13c2c2', // 青色 '惊讶': '#fadb14', // 黄色 }; const state = reactive({ data: [] as any, paginatedData: [] as any, loading: false, currentPage: 1, pageSize: 10, total: 0, global: { homeChartOne: null, homeChartTwo: null, homeCharFour: null, dispose: [null, '', undefined], } as any, myCharts: [] as any[], charts: { theme: '', bgColor: '', color: '#303133', }, // 详情弹窗相关 detailDialogVisible: false, detailLoading: false, selectedRecord: null as any, // 图片预览弹窗 previewDialog: { visible: false, title: '', imageUrl: '', }, }); // 响应式分页数据 const layout = computed(() => { return window.innerWidth < 768 ? 'prev, pager, next' : 'total, sizes, prev, pager, next, jumper'; }); // 获取图片URL const getImageUrl = (imagePath: string) => { if (!imagePath) return ''; if (imagePath.startsWith('http')) return imagePath; return `/api${imagePath.startsWith('/') ? '' : '/'}${imagePath}`; }; // 是否有检测详情 const hasDetectionDetails = computed(() => { if (!state.selectedRecord) return false; try { const labels = JSON.parse(state.selectedRecord.label || '[]'); const confidences = JSON.parse(state.selectedRecord.confidence || '[]'); return labels.length > 0 && confidences.length > 0; } catch { return false; } }); // 获取检测详情 const getDetectionDetails = () => { if (!state.selectedRecord) return []; try { const labels = JSON.parse(state.selectedRecord.label || '[]'); const confidences = JSON.parse(state.selectedRecord.confidence || '[]'); return labels .map((label: string, index: number) => { // 如果是表情索引,转换为表情名称 const emotionIndex = parseInt(label); const emotionLabel = emotionIndex >= 0 && emotionIndex < EMOTION_TYPES.length ? EMOTION_TYPES[emotionIndex] : `${label}`; return { label: emotionLabel, confidence: confidences[index] ? `${(parseFloat(confidences[index]) * 100).toFixed(1)}%` : '0%', color: EMOTION_COLORS[emotionLabel] || '#1890ff' }; }) .filter((item: any, index: number) => labels[index] !== undefined); } catch { return []; } }; // 图片预览 const previewImage = (imageUrl: string, title: string) => { if (!imageUrl) { ElMessage.warning('没有可预览的图片'); return; } state.previewDialog.imageUrl = imageUrl; state.previewDialog.title = title; state.previewDialog.visible = true; }; // 分页处理 const handleSizeChange = (val: number) => { state.pageSize = val; state.currentPage = 1; updatePaginatedData(); }; const handleCurrentChange = (val: number) => { state.currentPage = val; updatePaginatedData(); }; const updatePaginatedData = () => { const start = (state.currentPage - 1) * state.pageSize; const end = start + state.pageSize; state.paginatedData = state.data.slice(start, end); }; // 格式化标签显示 - 表情识别逻辑 const formatLabel = (label: string) => { try { const labels = JSON.parse(label); if (labels.length > 0) { const firstLabel = labels[0]; const emotionIndex = parseInt(firstLabel); if (emotionIndex >= 0 && emotionIndex < EMOTION_TYPES.length) { return EMOTION_TYPES[emotionIndex]; } return `${firstLabel}`; } return '未识别到表情'; } catch { if (label && label.length > 0 && label !== '[]' && label !== '""') { // 尝试直接转换 const emotionIndex = parseInt(label); if (!isNaN(emotionIndex) && emotionIndex >= 0 && emotionIndex < EMOTION_TYPES.length) { return EMOTION_TYPES[emotionIndex]; } return '检测到表情'; } return '未识别到表情'; } }; // 根据识别结果设置标签类型 - 表情识别逻辑 const getResultType = (label: string) => { try { const labels = JSON.parse(label); if (labels.length > 0) { const emotionIndex = parseInt(labels[0]); if (emotionIndex >= 0 && emotionIndex < EMOTION_TYPES.length) { const emotion = EMOTION_TYPES[emotionIndex]; // 根据表情类型设置不同的tag样式 switch(emotion) { case '快乐': return 'success'; case '中性': return 'info'; case '愤怒': return 'danger'; case '悲伤': return 'warning'; case '惊讶': return 'warning'; case '厌恶': return 'danger'; case '恐惧': return 'warning'; default: return 'primary'; } } return 'primary'; } return 'info'; } catch { if (label && label.length > 0 && label !== '[]' && label !== '""') { return 'primary'; } return 'info'; } }; // 格式化置信度显示 const formatConfidence = (confidence: string) => { try { const confidences = JSON.parse(confidence); if (confidences.length === 0) return '0%'; const maxConfidence = Math.max(...confidences.map((conf: any) => { if (typeof conf === 'number') return conf * 100; if (typeof conf === 'string') { const num = parseFloat(conf.replace('%', '')); return isNaN(num) ? 0 : num; } return 0; })); return `${maxConfidence.toFixed(1)}%`; } catch { if (typeof confidence === 'number') { return `${(confidence * 100).toFixed(1)}%`; } return confidence || '0%'; } }; // 查看详情 const handleViewDetail = async (row: any) => { state.selectedRecord = row; state.detailDialogVisible = true; state.detailLoading = true; try { const res = await request.get(`/api/imgRecords/${row.id}`); if (res.code == 0) { const record = res.data; state.selectedRecord = { ...record, inputImg: record.inputImg || record.imagePath, outImg: record.outImg || record.resultImagePath }; } } catch (error) { console.error('获取详情失败:', error); state.selectedRecord = row; } finally { state.detailLoading = false; } }; // 下载图片 const handleDownloadImage = async () => { if (!state.selectedRecord?.inputImg) { ElMessage.warning('没有可下载的图片'); return; } try { const imageUrl = getImageUrl(state.selectedRecord.inputImg); const response = await fetch(imageUrl); const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; const filename = state.selectedRecord.inputImg.split('/').pop() || `emotion_detection_${state.selectedRecord.username}_${state.selectedRecord.startTime?.replace(/[: ]/g, '-') || 'unknown'}.jpg`; a.download = filename; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); ElMessage.success('图片下载成功'); } catch (error) { console.error('下载图片失败:', error); ElMessage.error('图片下载失败'); } }; // 折线图 - 近十日识别数量 const initLineChart = () => { if (!state.global.dispose.some((b: any) => b === state.global.homeChartOne)) state.global.homeChartOne?.dispose(); state.global.homeChartOne = markRaw(echarts.init(homeLineRef.value, state.charts.theme)); // 统计每天的识别数量 const counts: Record<string, number> = {}; state.data.forEach((detection: any) => { if (detection.startTime) { const date = detection.startTime.split(' ')[0]; counts[date] = (counts[date] || 0) + 1; } }); const sortedDatesDesc = Object.keys(counts).sort((a, b) => b.localeCompare(a)); const latestDatesDesc = sortedDatesDesc.slice(0, 10); const latestDates = latestDatesDesc.sort((a, b) => a.localeCompare(b)); const result = { dateData: latestDates, valueData: latestDates.map(date => counts[date]) }; const option = { backgroundColor: state.charts.bgColor, title: { text: '近十日表情识别数量趋势', x: 'left', textStyle: { fontSize: 15, color: state.charts.color }, }, grid: { top: 70, right: 20, bottom: 30, left: 30 }, tooltip: { trigger: 'axis', formatter: (params: any) => { const data = params[0]; return `${data.name}<br/>表情识别数量: ${data.value}`; } }, xAxis: { data: result.dateData, axisLabel: { color: state.charts.color, rotate: 45 }, }, yAxis: [ { type: 'value', name: '识别数量', splitLine: { show: true, lineStyle: { type: 'dashed', color: state.charts.theme === 'dark' ? '#444' : '#f5f5f5' } }, axisLabel: { color: state.charts.color, }, }, ], series: [ { name: '表情识别数量', type: 'line', symbolSize: 6, symbol: 'circle', smooth: true, data: result.valueData, lineStyle: { color: '#52c41a' }, // 使用绿色表示情感分析 itemStyle: { color: '#52c41a', borderColor: '#52c41a' }, areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: '#52c41ab3' }, { offset: 1, color: '#52c41a03' }, ]), }, }, ], }; state.global.homeChartOne.setOption(option); state.myCharts.push(state.global.homeChartOne); }; // 饼图 - 表情类型分布 const initPieChart = () => { if (!state.global.dispose.some((b: any) => b === state.global.homeChartTwo)) state.global.homeChartTwo?.dispose(); state.global.homeChartTwo = markRaw(echarts.init(homePieRef.value, state.charts.theme)); const emotionCounts: Record<string, number> = {}; state.data.forEach((detection: any) => { try { const labels = JSON.parse(detection.label || '[]'); if (labels.length > 0) { const emotionIndex = parseInt(labels[0]); if (emotionIndex >= 0 && emotionIndex < EMOTION_TYPES.length) { const emotion = EMOTION_TYPES[emotionIndex]; emotionCounts[emotion] = (emotionCounts[emotion] || 0) + 1; } else if (labels[0] in EMOTION_COLORS) { // 如果已经是表情名称 emotionCounts[labels[0]] = (emotionCounts[labels[0]] || 0) + 1; } } } catch { // 如果解析失败,尝试直接使用 if (detection.label in EMOTION_COLORS) { emotionCounts[detection.label] = (emotionCounts[detection.label] || 0) + 1; } } }); // 确保所有表情类型都有数据 EMOTION_TYPES.forEach(emotion => { if (!emotionCounts[emotion]) { emotionCounts[emotion] = 0; } }); const pieData = EMOTION_TYPES.map(emotion => ({ name: emotion, value: emotionCounts[emotion], itemStyle: { color: EMOTION_COLORS[emotion] } })).filter(item => item.value > 0); const option = { backgroundColor: state.charts.bgColor, title: { text: '表情类型分布统计', x: 'left', textStyle: { fontSize: '15', color: state.charts.color }, }, legend: { top: 'bottom', textStyle: { color: state.charts.color } }, tooltip: { trigger: 'item', formatter: '{a} <br/>{b}: {c} ({d}%)' }, series: [ { type: 'pie', radius: ['40%', '70%'], center: ['50%', '50%'], avoidLabelOverlap: true, itemStyle: { borderRadius: 10, borderColor: state.charts.bgColor, borderWidth: 2 }, label: { show: true, formatter: '{b}: {c}次', color: state.charts.color }, emphasis: { label: { show: true, fontSize: '14', fontWeight: 'bold' } }, data: pieData } ] }; state.global.homeChartTwo.setOption(option); state.myCharts.push(state.global.homeChartTwo); }; // 雷达图 - 用户置信度分析 const initradarChart = () => { if (!state.global.dispose.some((b: any) => b === state.global.homeCharFour)) state.global.homeCharFour?.dispose(); state.global.homeCharFour = markRaw(echarts.init(homeradarRef.value, state.charts.theme)); const userConfidenceStats: Record<string, { total: number, count: number }> = {}; state.data.forEach((detection: any) => { const username = detection.username || '未知用户'; let avgConfidence = 0; try { const confidences = JSON.parse(detection.confidence || '[]'); if (confidences.length > 0) { const validConfidences = confidences.filter((conf: any) => { if (typeof conf === 'number') return true; if (typeof conf === 'string') { const num = parseFloat(conf.replace('%', '')); return !isNaN(num); } return false; }); if (validConfidences.length > 0) { const numericConfidences = validConfidences.map((conf: any) => { if (typeof conf === 'number') return conf; if (typeof conf === 'string') { const num = parseFloat(conf.replace('%', '')) / 100; return isNaN(num) ? 0 : num; } return 0; }); avgConfidence = numericConfidences.reduce((sum: number, conf: number) => sum + conf, 0) / numericConfidences.length; } } } catch { if (typeof detection.confidence === 'number') { avgConfidence = detection.confidence; } else if (typeof detection.confidence === 'string') { const num = parseFloat(detection.confidence.replace('%', '')) / 100; avgConfidence = isNaN(num) ? 0 : num; } } if (!userConfidenceStats[username]) { userConfidenceStats[username] = { total: avgConfidence, count: 1 }; } else { userConfidenceStats[username].total += avgConfidence; userConfidenceStats[username].count += 1; } }); const userAvgConfidences = Object.keys(userConfidenceStats).map(username => ({ username, avgConf: userConfidenceStats[username].total / userConfidenceStats[username].count, count: userConfidenceStats[username].count })); const topUsers = userAvgConfidences .filter(user => user.count >= 3) .sort((a, b) => b.avgConf - a.avgConf) .slice(0, 7); if (topUsers.length === 0) { const option = { backgroundColor: state.charts.bgColor, title: { text: '用户识别置信度分析', x: 'left', textStyle: { fontSize: '15', color: state.charts.color }, }, graphic: { type: 'text', left: 'center', top: 'center', style: { text: '数据不足,无法生成置信度分析', fontSize: 14, fill: state.charts.color } } }; state.global.homeCharFour.setOption(option); state.myCharts.push(state.global.homeCharFour); return; } const data = topUsers.map(user => Number((user.avgConf * 100).toFixed(2))); const indicatorNames = topUsers.map(user => user.username); const indicator = indicatorNames.map((name) => ({ name, max: 100 })); const option = { backgroundColor: state.charts.bgColor, title: { text: '用户识别置信度分析', x: 'left', textStyle: { fontSize: '15', color: state.charts.color }, }, tooltip: { formatter: (params: any) => { const userIndex = indicatorNames.findIndex(name => name === params.name); const user = topUsers[userIndex]; return `${params.name}<br/>平均置信度: ${params.value}%<br/>识别次数: ${user?.count || 0}次`; } }, radar: { radius: '65%', splitNumber: 4, indicator: indicator, axisName: { color: state.charts.color, fontSize: 12 }, splitArea: { areaStyle: { color: ['rgba(82,196,26,0.1)', 'rgba(82,196,26,0.05)'], } }, splitLine: { lineStyle: { color: 'rgba(82,196,26,0.3)' } }, axisLine: { lineStyle: { color: 'rgba(82,196,26,0.5)' } } }, series: [{ type: 'radar', data: [{ value: data, name: '平均置信度', areaStyle: { color: 'rgba(82,196,26,0.3)' }, lineStyle: { color: '#52c41a' }, itemStyle: { color: '#52c41a' }, label: { show: true, formatter: (params: any) => { return params.value + '%'; } } }] }] }; state.global.homeCharFour.setOption(option); state.myCharts.push(state.global.homeCharFour); }; // 批量设置 echarts resize const initEchartsResizeFun = () => { nextTick(() => { for (let i = 0; i < state.myCharts.length; i++) { setTimeout(() => { state.myCharts[i]?.resize(); }, i * 1000); } }); }; const initEchartsResize = () => { window.addEventListener('resize', initEchartsResizeFun); }; // 加载数据 const loadData = async () => { state.loading = true; try { const res = await request.get('/api/imgRecords/all'); if (res.code == 0) { state.data = res.data.map((record: any, index: number) => { const transformedRecord = { id: record.id, num: index + 1, inputImg: record.inputImg || record.imagePath, outImg: record.outImg || record.resultImagePath, weight: record.weight, conf: record.conf, ai: record.ai, suggestion: record.suggestion, startTime: record.startTime, username: record.username, label: record.label, confidence: record.confidence, family: record.family || [] }; if (!transformedRecord.family || transformedRecord.family.length === 0) { try { const labels = JSON.parse(record.label || '[]'); const confidences = JSON.parse(record.confidence || '[]'); transformedRecord.family = labels.map((label: string, idx: number) => { const emotionIndex = parseInt(label); const emotionLabel = emotionIndex >= 0 && emotionIndex < EMOTION_TYPES.length ? EMOTION_TYPES[emotionIndex] : `${label}`; return { label: emotionLabel, confidence: confidences[idx] || 0, color: EMOTION_COLORS[emotionLabel] || '#1890ff', startTime: record.startTime }; }); } catch (error) { console.error('构建family字段失败:', error); transformedRecord.family = []; } } return transformedRecord; }).reverse(); state.total = state.data.length; updatePaginatedData(); setTimeout(() => { initLineChart(); initradarChart(); initPieChart(); }, 100); } else { ElMessage.error(res.msg || '加载数据失败'); } } catch (error) { console.error('加载数据失败:', error); ElMessage.error('加载数据失败,请检查网络连接'); } finally { state.loading = false; } }; // 页面加载时 onMounted(() => { loadData(); initEchartsResize(); }); // 由于页面缓存原因,keep-alive onActivated(() => { initEchartsResizeFun(); }); // 监听相关状态变化 watch( () => isTagsViewCurrenFull.value, () => { initEchartsResizeFun(); } ); watch( () => themeConfig.value.isIsDark, (isIsDark) => { nextTick(() => { state.charts.theme = isIsDark ? 'dark' : ''; state.charts.bgColor = isIsDark ? 'transparent' : ''; state.charts.color = isIsDark ? '#dadada' : '#303133'; setTimeout(() => { initLineChart(); initradarChart(); initPieChart(); }, 500); }); }, { deep: true, immediate: true, } ); </script> <style scoped lang="scss"> .home-container { overflow: hidden; .home-card-one, .home-card-two, .home-card-three { .home-card-item { width: 100%; height: 400px; border-radius: 4px; transition: all ease 0.3s; padding: 20px; overflow: hidden; background: var(--el-color-white); color: var(--el-text-color-primary); border: 1px solid var(--next-border-color-light); &:hover { box-shadow: 0 2px 12px var(--next-color-dark-hover); transition: all ease 0.3s; } &-title { font-size: 15px; font-weight: bold; height: 30px; margin-bottom: 15px; color: var(--el-text-color-primary); border-bottom: 1px solid var(--next-border-color-light); padding-bottom: 10px; } } } } /* 详情弹窗样式 */ .detail-container { padding: 10px 0; } .detail-section { margin-bottom: 20px; } .detail-title { font-size: 16px; font-weight: bold; margin-bottom: 15px; color: var(--el-text-color-primary); border-left: 4px solid var(--el-color-primary); padding-left: 10px; } .image-container { width: 100%; height: 300px; display: flex; justify-content: center; align-items: center; border: 1px solid var(--next-border-color-light); border-radius: 8px; overflow: hidden; background-color: var(--el-fill-color-light); margin-bottom: 10px; } .img-wrapper { position: relative; display: flex; justify-content: center; align-items: center; cursor: pointer; border-radius: 6px; overflow: hidden; height: 100%; width: 100%; &:hover { .img-overlay { opacity: 1; } .detection-image { transform: scale(1.05); } } .detection-image { width: 100%; height: 100%; object-fit: contain; border-radius: 4px; border: 1px solid var(--next-border-color-light); transition: transform 0.3s ease; } .img-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; opacity: 0; transition: opacity 0.3s ease; .el-icon { color: white; font-size: 24px; } } } .image-placeholder { display: flex; flex-direction: column; align-items: center; justify-content: center; color: var(--el-text-color-secondary); .el-icon { font-size: 48px; margin-bottom: 10px; } } .detection-details { display: flex; flex-direction: column; gap: 8px; } .detail-item { display: flex; justify-content: space-between; align-items: center; padding: 4px 0; } .detail-label { font-weight: 500; color: var(--el-text-color-primary); } .detail-value { color: var(--el-text-color-regular); } .dialog-footer { display: flex; justify-content: flex-end; gap: 10px; } // 图片预览弹窗样式 .image-preview-dialog { .preview-content { display: flex; justify-content: center; align-items: center; .preview-image { max-width: 100%; max-height: 70vh; object-fit: contain; border-radius: 8px; } } } /* 响应式调整 */ @media (max-width: 768px) { .home-media { margin-top: 15px; } .pagination-container { justify-content: center; } .image-container { height: 250px; } .detail-section { margin-bottom: 15px; } } </style>后端代码展示
项目源码+数据集下载链接
完整代码在哔哩哔哩视频下方简介内获取
项目安装教程
https://www.bilibili.com/video/BV1YLsXzJE2X/?spm_id_from=333.1387.homepage.video_card.click
YOLO+spring boot+vue项目环境部署教程(YOLOv8、YOLOv10、YOLOv11、YOLOv12)_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1YLsXzJE2X/?spm_id_from=333.1387.homepage.video_card.click&vd_source=549d0b4e2b8999929a61a037fcce3b0f