项目摘要

本项目是一个基于先进深度学习技术构建的、功能全面且用户友好的裂缝智能检测系统。系统采用现代化的前后端分离架构,前端使用Vue.js构建响应式用户界面,后端采用SpringBoot框架提供稳健的API服务。核心检测算法集成了最新的YOLO系列模型(v8/v10/v11/v12),为用户提供了灵活且高性能的模型选择。此外,系统创新性地引入了DeepSeek大语言模型,为检测结果提供智能化的分析与解释,极大地提升了系统的实用价值。系统不仅支持图像、视频和摄像头实时流的全面检测模式,还配备了完善的用户管理、数据可视化与记录管理功能,所有数据均持久化存储于MySQL数据库中。本项目旨在为建筑等基础设施的维护提供一套高效、准确、可追溯的自动化裂缝检测解决方案。


项目详细介绍

本项目是一个综合性的Web应用程序,旨在将尖端的计算机视觉技术应用于实际的工程安全监测领域。以下是对系统各个维度的详细阐述:

1. 系统架构与技术栈

  • 前端 (Front-end): 采用 Vue.js 框架开发,构建了一个单页面应用。这种设计确保了用户界面的流畅交互和快速的响应速度。使用Element UI或Ant Design Vue等组件库,保证了界面风格统一、美观且易于操作。

  • 后端 (Back-end): 使用 SpringBoot 框架构建RESTful API。该框架简化了配置和开发流程,提供了强大的依赖管理和模块化支持,确保了后端服务的稳定性、可扩展性和易于维护性。它负责处理业务逻辑、模型调用、数据库交互和用户认证。

  • 前后端分离: 这种架构模式使得前端与后端解耦,二者通过HTTP API进行数据通信。优势在于允许前后端独立开发、测试和部署,提升了开发效率,并便于未来技术的迭代升级。

2. 核心AI检测功能

  • 多模型支持: 系统的一大亮点是支持 YOLOv8, v10, v11, v12 四种模型的动态切换。用户可以根据不同场景对检测速度和精度的要求,灵活选择最合适的模型。这体现了系统设计的前瞻性和灵活性,能够持续集成YOLO社区的最新成果。

  • 多模态检测:

    • 图像检测: 用户上传裂缝图片,系统实时返回带检测框的图片,并记录检测结果。

    • 视频检测: 对上传的视频文件进行逐帧分析,生成带有检测结果的视频,并保存关键信息。

    • 摄像头实时检测: 开启电脑摄像头,进行实时流媒体分析,可实现持续的监控与预警。

  • DeepSeek智能分析: 这是项目的创新功能。在完成裂缝检测后,系统将检测到的裂缝位置、数量等信息作为上下文,调用DeepSeek大模型进行深度分析。分析内容可能包括:裂缝的潜在风险等级评估、可能的成因推测、维护建议等,并以自然语言的形式呈现给用户,极大地提升了系统的智能化水平和决策支持能力。

3. 系统功能模块

  • 用户体系:

    • 登录/注册: 提供安全的用户认证,保障账户安全。

    • 个人中心: 用户可自主修改个人信息,如姓名、头像、密码等。

    • 用户管理 (管理员): 管理员拥有高级权限,可以对系统所有用户进行增、删、改、查操作。

  • 数据管理与可视化:

    • 记录管理: 系统详细记录每一次检测任务(图片、视频、摄像头)的信息,包括检测时间、使用的模型、检测结果等,并形成历史记录库,方便用户查询和追溯。

    • 信息可视化: 通过图表(如折线图、柱状图)等形式,对历史检测数据进行可视化展示。例如,展示不同时间段内的裂缝检测数量趋势、各模型的使用频率对比等,帮助用户从宏观层面理解数据。

  • 数据持久化: 所有关键数据,包括用户信息、模型配置、每一次的检测记录与结果,都通过 MySQL 数据库进行可靠的存储,确保数据的完整性和可查询性。


数据集介绍

本项目所使用的数据集是一个专门为裂缝检测任务精心构建的图像数据集。该数据集的质量和规模是保证本系统检测模型高准确率的基石。

  • 任务类型: 单目标检测。数据集中所有图像只包含一个需要被识别的目标类别——‘crack’(裂缝)。这使得模型能够集中全部注意力学习裂缝在各种复杂背景下的视觉特征,如纹理、走向、颜色和形状等。

  • 数据规模与划分: 数据集遵循机器学习项目的标准实践,被明确地划分为三个子集,以确保模型的泛化能力并防止过拟合:

    • 训练集: 3,717张图像。这是数量最大的部分,用于“训练”或“教导”YOLO模型。模型通过学习这些图片中的裂缝特征,来构建其内部的识别模式。

    • 验证集: 200张图像。在训练过程中,使用验证集来评估模型在未见过的数据上的表现,用于调整超参数(如学习率)和监控训练过程,决定何时停止训练以获得最佳模型。

    • 测试集: 112张图像。在模型训练完成之后,使用测试集对模型的最终性能进行一次性的、无偏的评估。测试集的结果最能反映模型在真实世界应用中的实际表现。

项目源码+数据集下载链接

完整代码在哔哩哔哩视频下方简介内获取

基于深度学习的裂缝检测系统(web界面+YOLOv8/v10/v11/v12+DeepSeek智能分析 +前后端分离)_哔哩哔哩_bilibili

基于深度学习的裂缝检测系统(web界面+YOLOv8/v10/v11/v12+DeepSeek智能分析 +前后端分离)_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1fLUaBGEbJ/?vd_source=549d0b4e2b8999929a61a037fcce3b0f#reply115614228219652

https://www.bilibili.com/video/BV1fLUaBGEbJ

目录

 项目摘要

项目详细介绍

数据集介绍

项目源码+数据集下载链接

功能模块

登录注册模块

可视化模块

图像检测模块

视频检测模块

实时检测模块

图片识别记录管理

视频识别记录管理

摄像头识别记录管理

用户管理模块

数据管理模块(MySQL表设计)

模型训练结果

YOLOv8

YOLOv10

YOLOv11

YOLOv12

前端代码展示

项目源码+数据集下载链接

 项目安装教程


功能模块


✅ 用户登录注册:支持密码检测,保存到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-one mb15">
			<el-col :xs="12" :sm="8" :md="8" :lg="8" :xl="8">
				<div class="home-card-item card-stat">
					<div class="card-stat-content">
						<div class="stat-icon detection-icon">
							<el-icon><Camera /></el-icon>
						</div>
						<div class="stat-info">
							<div class="stat-value">{{ state.statistics.totalDetections }}</div>
							<div class="stat-label">总检测数量</div>
						</div>
					</div>
				</div>
			</el-col>
			<el-col :xs="12" :sm="8" :md="8" :lg="8" :xl="8">
				<div class="home-card-item card-stat">
					<div class="card-stat-content">
						<div class="stat-icon crack-icon">
							<el-icon><Warning /></el-icon>
						</div>
						<div class="stat-info">
							<div class="stat-value">{{ state.statistics.crackDetections }}</div>
							<div class="stat-label">裂缝识别数</div>
						</div>
					</div>
				</div>
			</el-col>
			<el-col :xs="12" :sm="8" :md="8" :lg="8" :xl="8">
				<div class="home-card-item card-stat">
					<div class="card-stat-content">
						<div class="stat-icon normal-icon">
							<el-icon><Check /></el-icon>
						</div>
						<div class="stat-info">
							<div class="stat-value">{{ state.statistics.normalDetections }}</div>
							<div class="stat-label">正常结构数</div>
						</div>
					</div>
				</div>
			</el-col>
		</el-row>

		<!-- 中间图表区域 -->
		<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 chart-card">
					<div class="chart-header">
						<h3>检测趋势分析</h3>
						<el-tag type="primary">近10日</el-tag>
					</div>
					<div style="height: 300px" 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 chart-card">
					<div class="chart-header">
						<h3>检测结果分布</h3>
						<el-tag type="success">实时</el-tag>
					</div>
					<div style="height: 300px" ref="homePieRef"></div>
				</div>
			</el-col>
		</el-row>

		<!-- 底部区域 -->
		<el-row :gutter="15" class="home-card-three">
			<el-col :xs="24" :sm="8" :md="8" :lg="8" :xl="8" class="home-media">
				<div class="home-card-item chart-card">
					<div class="chart-header">
						<h3>置信度分析</h3>
						<el-tag type="warning">TOP 7</el-tag>
					</div>
					<div style="height: 300px" ref="homeradarRef"></div>
				</div>
			</el-col>
			<el-col :xs="24" :sm="16" :md="16" :lg="16" :xl="16">
				<div class="home-card-item record-card">
					<div class="card-header">
						<div class="header-title">
							<el-icon><List /></el-icon>
							<span>实时检测记录</span>
						</div>
						<el-button type="primary" link @click="loadData">
							<el-icon><Refresh /></el-icon>
							刷新
						</el-button>
					</div>
					<div class="record-table">
						<el-table 
							:data="state.paginatedData" 
							style="width: 100%" 
							height="360" 
							v-loading="state.loading"
							:row-class-name="tableRowClassName"
						>
							<el-table-column prop="num" label="序号" align="center" width="80" />
							<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"
										:class="getResultClass(scope.row.label)"
									>
										<el-icon v-if="hasCrack(scope.row.label)"><Warning /></el-icon>
										<el-icon v-else><Check /></el-icon>
										{{ formatLabel(scope.row.label) }}
									</el-tag>
								</template>
							</el-table-column>
							<el-table-column prop="confidence" label="置信度" align="center" width="120">
								<template #default="scope">
									<div class="confidence-cell">
										<el-progress 
											:percentage="parseFloat(formatConfidence(scope.row.confidence))" 
											:show-text="false"
											:color="getConfidenceColor(parseFloat(formatConfidence(scope.row.confidence)))"
										/>
										<span class="confidence-text">{{ formatConfidence(scope.row.confidence) }}</span>
									</div>
								</template>
							</el-table-column>
							<el-table-column prop="startTime" label="检测时间" align="center" width="180" />
							<el-table-column label="操作" align="center" width="100" fixed="right">
								<template #default="scope">
									<el-button link type="primary" size="small" @click="handleViewDetail(scope.row)">
										<el-icon><View /></el-icon>
										详情
									</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>
			</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
			class="detail-dialog"
		>
			<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">
								<el-icon><Picture /></el-icon>
								原始结构图像
							</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><ZoomIn /></el-icon>
										<span>点击预览</span>
									</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">
								<el-icon><InfoFilled /></el-icon>
								检测信息
							</h3>
							<el-descriptions :column="1" border class="detail-descriptions">
								<el-descriptions-item label="检测对象">
									<el-tag>{{ state.selectedRecord?.username || '未知' }}</el-tag>
								</el-descriptions-item>
								
								<el-descriptions-item label="检测结果">
									<el-tag 
										:type="getResultType(state.selectedRecord?.label || '')"
										effect="light"
										:class="getResultClass(state.selectedRecord?.label || '')"
									>
										<el-icon v-if="hasCrack(state.selectedRecord?.label || '')"><Warning /></el-icon>
										<el-icon v-else><Check /></el-icon>
										{{ formatLabel(state.selectedRecord?.label || '') }}
									</el-tag>
								</el-descriptions-item>
								
								<el-descriptions-item label="置信度">
									<div class="confidence-display">
										<el-progress 
											:percentage="parseFloat(formatConfidence(state.selectedRecord?.confidence || ''))" 
											:color="getConfidenceColor(parseFloat(formatConfidence(state.selectedRecord?.confidence || '')))"
											:stroke-width="12"
										/>
										<span class="confidence-value">{{ formatConfidence(state.selectedRecord?.confidence || '') }}</span>
									</div>
								</el-descriptions-item>
								
								<el-descriptions-item label="检测时间">
									<el-tag type="info">
										<el-icon><Clock /></el-icon>
										{{ state.selectedRecord?.startTime || '未知' }}
									</el-tag>
								</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">
								<el-icon><Picture /></el-icon>
								原始结构图像
							</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><ZoomIn /></el-icon>
										<span>点击预览</span>
									</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">
								<el-icon><Search /></el-icon>
								裂缝识别结果
							</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><ZoomIn /></el-icon>
										<span>点击预览</span>
									</div>
									<div v-else class="image-placeholder">
										<el-icon><Search /></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, ZoomIn, Camera, Warning, Check, TrendCharts, List, Refresh, InfoFilled, Clock, Search } 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 CRACK_TYPES = ['裂缝', '正常'];

const state = reactive({
	data: [] as any,
	paginatedData: [] as any,
	loading: false,
	currentPage: 1,
	pageSize: 10,
	total: 0,
	statistics: {
		totalDetections: 0,
		crackDetections: 0,
		normalDetections: 0
	},
	global: {
		homeChartOne: null,
		homeChartTwo: null,
		homeCharFour: null,
		dispose: [null, '', undefined],
	} as any,
	myCharts: [] as EmptyArrayType,
	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) => ({
			label,
			confidence: confidences[index] ? `${(parseFloat(confidences[index]) * 100).toFixed(1)}%` : '0%'
		}));
	} 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);
		return labels.length > 0 ? labels.join(', ') : '正常';
	} catch {
		return label || '正常';
	}
};

// 判断是否包含裂缝
const hasCrack = (label: string) => {
	try {
		const labels = JSON.parse(label);
		return labels.includes('裂缝');
	} catch {
		return label?.includes('裂缝');
	}
};

// 根据检测结果设置标签类型
const getResultType = (label: string) => {
	if (hasCrack(label)) {
		return 'danger';
	}
	return 'success';
};

// 获取结果样式类
const getResultClass = (label: string) => {
	return hasCrack(label) ? 'crack-result' : 'normal-result';
};

// 获取置信度颜色
const getConfidenceColor = (percentage: number) => {
	if (percentage >= 80) return '#67C23A';
	if (percentage >= 60) return '#E6A23C';
	return '#F56C6C';
};

// 格式化置信度显示
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 tableRowClassName = ({ row }: { row: any }) => {
	return hasCrack(row.label) ? 'warning-row' : '';
};

// 查看详情
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() || 
			`crack_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 updateStatistics = () => {
	let crackCount = 0;
	let normalCount = 0;
	
	state.data.forEach((item: any) => {
		if (hasCrack(item.label)) {
			crackCount++;
		} else {
			normalCount++;
		}
	});
	
	state.statistics.totalDetections = state.data.length;
	state.statistics.crackDetections = crackCount;
	state.statistics.normalDetections = normalCount;
};

// 折线图 - 近十日检测数量
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((prediction: any) => {
		if (prediction.startTime) {
			const date = prediction.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: '检测趋势分析',
			textStyle: { fontSize: 16, color: state.charts.color, fontWeight: 'bold' },
			left: 'center'
		},
		grid: { top: 50, right: 20, bottom: 30, left: 40 },
		tooltip: { 
			trigger: 'axis',
			backgroundColor: 'rgba(0,0,0,0.8)',
			formatter: (params: any) => {
				const data = params[0];
				return `<div style="font-size: 12px;">
					<div style="margin-bottom: 5px;">${data.name}</div>
					<div>检测数量: <span style="color: #36A2EB; font-weight: bold;">${data.value}</span></div>
				</div>`;
			}
		},
		xAxis: {
			data: result.dateData,
			axisLabel: {
				color: state.charts.color,
				rotate: 45
			},
			axisLine: {
				lineStyle: {
					color: state.charts.theme === 'dark' ? '#555' : '#ddd'
				}
			}
		},
		yAxis: [
			{
				type: 'value',
				name: '检测数量',
				splitLine: { 
					show: true, 
					lineStyle: { 
						type: 'dashed', 
						color: state.charts.theme === 'dark' ? '#444' : '#f0f0f0' 
					} 
				},
				axisLabel: {
					color: state.charts.color,
				},
			},
		],
		series: [
			{
				name: '检测数量',
				type: 'line',
				symbolSize: 8,
				symbol: 'circle',
				smooth: true,
				data: result.valueData,
				lineStyle: { 
					color: '#36A2EB',
					width: 3
				},
				itemStyle: { 
					color: '#36A2EB', 
					borderColor: '#fff',
					borderWidth: 2
				},
				areaStyle: {
					color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
						{ offset: 0, color: '#36A2EBb3' },
						{ offset: 1, color: '#36A2EB03' },
					]),
				},
			},
		],
	};

	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));
	
	let crackCount = 0;
	let normalCount = 0;
	
	state.data.forEach((item: any) => {
		if (hasCrack(item.label)) {
			crackCount++;
		} else {
			normalCount++;
		}
	});

	const pieData = [
		{ name: '裂缝', value: crackCount, itemStyle: { color: '#F56C6C' } },
		{ name: '正常', value: normalCount, itemStyle: { color: '#67C23A' } }
	];

	const option = {
		backgroundColor: state.charts.bgColor,
		title: {
			text: '检测结果分布',
			textStyle: { fontSize: 16, color: state.charts.color, fontWeight: 'bold' },
			left: 'center'
		},
		legend: {
			top: 'bottom',
			textStyle: {
				color: state.charts.color
			}
		},
		tooltip: {
			trigger: 'item',
			formatter: '{a} <br/>{b}: {c} ({d}%)',
			backgroundColor: 'rgba(0,0,0,0.8)'
		},
		series: [
			{
				type: 'pie',
				radius: ['50%', '70%'],
				center: ['50%', '50%'],
				avoidLabelOverlap: true,
				itemStyle: {
					borderRadius: 8,
					borderColor: state.charts.bgColor,
					borderWidth: 3
				},
				label: {
					show: true,
					formatter: '{b}\n{d}%',
					color: state.charts.color,
					fontSize: 12,
					fontWeight: 'bold'
				},
				emphasis: {
					label: {
						show: true,
						fontSize: '14',
						fontWeight: 'bold'
					},
					itemStyle: {
						shadowBlur: 10,
						shadowOffsetX: 0,
						shadowColor: 'rgba(0, 0, 0, 0.5)'
					}
				},
				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 confStatsByUser: Record<string, { total: number, count: number }> = {};
	
	state.data.forEach((prediction: any) => {
		const username = prediction.username || '未知对象';
		let confidenceValue = 0;
		
		try {
			const confidences = JSON.parse(prediction.confidence || '[]');
			if (confidences.length > 0) {
				confidenceValue = Math.max(...confidences.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;
				}));
			}
		} catch {
			if (typeof prediction.confidence === 'number') {
				confidenceValue = prediction.confidence;
			}
		}
		
		if (!confStatsByUser[username]) {
			confStatsByUser[username] = { total: confidenceValue, count: 1 };
		} else {
			confStatsByUser[username].total += confidenceValue;
			confStatsByUser[username].count += 1;
		}
	});

	const avgConfData = Object.keys(confStatsByUser).map(username => ({
		username,
		avgConf: confStatsByUser[username].total / confStatsByUser[username].count,
	}));

	const topAvgConfData = avgConfData.slice(0, 7);
	const data = topAvgConfData.map(item => Number((item.avgConf * 100).toFixed(2)));
	const indicatorNames = topAvgConfData.map(item => item.username);

	const indicator = indicatorNames.map((name) => ({ 
		name, 
		max: 100 
	}));

	const option = {
		backgroundColor: state.charts.bgColor,
		title: {
			text: '检测置信度分析',
			textStyle: { fontSize: 16, color: state.charts.color, fontWeight: 'bold' },
			left: 'center'
		},
		tooltip: {
			formatter: (params: any) => {
				return `<div style="font-size: 12px;">
					<div style="margin-bottom: 5px;">${params.name}</div>
					<div>置信度: <span style="color: #36A2EB; font-weight: bold;">${params.value}%</span></div>
				</div>`;
			},
			backgroundColor: 'rgba(0,0,0,0.8)'
		},
		radar: {
			radius: '65%',
			splitNumber: 4,
			indicator: indicator,
			axisName: {
				color: state.charts.color,
				fontSize: 12,
				fontWeight: 'bold'
			},
			splitArea: {
				areaStyle: {
					color: ['rgba(54,162,235,0.1)', 'rgba(54,162,235,0.05)'],
				}
			},
			splitLine: {
				lineStyle: {
					color: 'rgba(54,162,235,0.3)'
				}
			},
			axisLine: {
				lineStyle: {
					color: 'rgba(54,162,235,0.5)'
				}
			}
		},
		series: [{
			type: 'radar',
			data: [{
				value: data,
				name: '置信度',
				areaStyle: {
					color: 'rgba(54,162,235,0.3)'
				},
				lineStyle: {
					color: '#36A2EB',
					width: 2
				},
				itemStyle: {
					color: '#36A2EB'
				},
				label: {
					show: true,
					formatter: (params: any) => {
						return params.value + '%';
					},
					color: state.charts.color,
					fontWeight: 'bold'
				}
			}]
		}]
	};

	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) => ({
							label: label,
							confidence: confidences[idx] || 0,
							startTime: record.startTime
						}));
					} catch (error) {
						console.error('构建family字段失败:', error);
						transformedRecord.family = [];
					}
				}
				
				return transformedRecord;
			}).reverse();
			
			state.total = state.data.length;
			updatePaginatedData();
			updateStatistics();
			
			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>

项目源码+数据集下载链接

完整代码在哔哩哔哩视频下方简介内获取

基于深度学习的裂缝检测系统(web界面+YOLOv8/v10/v11/v12+DeepSeek智能分析 +前后端分离)_哔哩哔哩_bilibili

基于深度学习的裂缝检测系统(web界面+YOLOv8/v10/v11/v12+DeepSeek智能分析 +前后端分离)_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1fLUaBGEbJ/?vd_source=549d0b4e2b8999929a61a037fcce3b0f#reply115614228219652

https://www.bilibili.com/video/BV1fLUaBGEbJ

 项目安装教程

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

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐