运行:%% 混凝土裂缝检测系统 - 完整单文件实现
classdef CrackDetectionSystem < handle
properties
% 模型参数
unetModel
ganGenerator
% GUI组件
appFig
imgAxes
resultAxes
resultTable
inputPathLabel
outputPathLabel
inputFolder = ''
outputFolder = ''
statusLabel
% 系统参数
pixelSize = 0.1; % 每像素代表的实际尺寸(mm)
confThreshold = 0.7;
minCrackArea = 50; % 最小裂缝面积(像素)
% 训练参数
trainDataDir = ''
end
methods
function obj = CrackDetectionSystem()
% 初始化系统
obj.createGUI();
obj.loadModels();
end
function loadModels(obj)
% 加载预训练模型
try
% 加载U-Net裂缝分割模型
if exist('crack_unet.mat', 'file')
unetData = load('crack_unet.mat');
obj.unetModel = unetData.net;
else
obj.unetModel = [];
end
% 加载GAN生成器模型
if exist('crack_gan_generator.mat', 'file')
ganData = load('crack_gan_generator.mat');
obj.ganGenerator = ganData.genNet;
else
obj.ganGenerator = [];
end
obj.updateStatus('模型加载成功');
catch ME
obj.updateStatus(sprintf('模型加载失败: %s', ME.message), 'error');
end
end
function createGUI(obj)
% 创建主界面
obj.appFig = uifigure('Name', '混凝土裂缝检测系统', 'Position', [100 100 1400 850], ...
'CloseRequestFcn', @(src,event) obj.closeSystem());
% 图像显示区域
uilabel(obj.appFig, 'Text', '原始图像', 'Position', [50 800 100 20], 'FontSize', 12, 'FontWeight', 'bold');
obj.imgAxes = uiaxes(obj.appFig, 'Position', [50 500 600 300]);
uilabel(obj.appFig, 'Text', '检测结果', 'Position', [700 800 100 20], 'FontSize', 12, 'FontWeight', 'bold');
obj.resultAxes = uiaxes(obj.appFig, 'Position', [700 500 600 300]);
% 状态栏
obj.statusLabel = uilabel(obj.appFig, 'Text', '系统已就绪', 'Position', [50 20 1000 30], ...
'FontSize', 11, 'FontColor', [0 0.5 0]);
% 文件夹选择区域
folderPanel = uipanel(obj.appFig, 'Title', '文件夹设置', 'Position', [50 400 600 80]);
uilabel(folderPanel, 'Text', '输入文件夹:', 'Position', [20 40 80 20]);
obj.inputPathLabel = uilabel(folderPanel, 'Text', '未选择', 'Position', [100 40 350 20], ...
'Interpreter', 'none');
uibutton(folderPanel, 'push', 'Text', '浏览...', 'Position', [470 40 60 20],...
'ButtonPushedFcn', @(btn,event) obj.selectInputFolder());
uilabel(folderPanel, 'Text', '输出文件夹:', 'Position', [20 10 80 20]);
obj.outputPathLabel = uilabel(folderPanel, 'Text', '未选择', 'Position', [100 10 350 20], ...
'Interpreter', 'none');
uibutton(folderPanel, 'push', 'Text', '浏览...', 'Position', [470 10 60 20],...
'ButtonPushedFcn', @(btn,event) obj.selectOutputFolder());
% 参数设置区域
paramPanel = uipanel(obj.appFig, 'Title', '检测参数', 'Position', [700 400 600 80]);
uilabel(paramPanel, 'Text', '像素尺寸(mm):', 'Position', [20 40 100 20]);
pixelEdit = uieditfield(paramPanel, 'numeric', 'Position', [130 40 80 20],...
'Value', obj.pixelSize, 'ValueChangedFcn', @(src,event) obj.setPixelSize(src.Value));
uilabel(paramPanel, 'Text', '置信度阈值:', 'Position', [230 40 100 20]);
confEdit = uieditfield(paramPanel, 'numeric', 'Position', [340 40 80 20],...
'Value', obj.confThreshold, 'Limits', [0.1 0.9], ...
'ValueChangedFcn', @(src,event) obj.setConfThreshold(src.Value));
uilabel(paramPanel, 'Text', '最小面积:', 'Position', [440 40 100 20]);
areaEdit = uieditfield(paramPanel, 'numeric', 'Position', [540 40 50 20],...
'Value', obj.minCrackArea, 'ValueChangedFcn', @(src,event) obj.setMinCrackArea(src.Value));
% 模型训练区域
trainPanel = uipanel(obj.appFig, 'Title', '模型训练', 'Position', [50 300 600 80]);
uilabel(trainPanel, 'Text', '训练数据:', 'Position', [20 40 80 20]);
trainPathLabel = uilabel(trainPanel, 'Text', '未选择', 'Position', [100 40 350 20], ...
'Interpreter', 'none');
uibutton(trainPanel, 'push', 'Text', '浏览...', 'Position', [470 40 60 20],...
'ButtonPushedFcn', @(btn,event) obj.selectTrainFolder(trainPathLabel));
uibutton(trainPanel, 'push', 'Text', '训练U-Net', 'Position', [100 10 100 20],...
'ButtonPushedFcn', @(btn,event) obj.trainUnetModel());
uibutton(trainPanel, 'push', 'Text', '训练GAN', 'Position', [220 10 100 20],...
'ButtonPushedFcn', @(btn,event) obj.trainGANModel());
% 控制按钮
detectBtn = uibutton(obj.appFig, 'push', ...
'Text', '开始批量检测', ...
'Position', [800 350 150 30], ...
'FontSize', 12, ...
'FontWeight', 'bold', ...
'BackgroundColor', [0.2 0.6 1], ...
'FontColor', [1 1 1], ...
'ButtonPushedFcn', @(btn,event) obj.batchDetection());
evalBtn = uibutton(obj.appFig, 'push', ...
'Text', '评估系统', ...
'Position', [1000 350 150 30], ...
'FontSize', 12, ...
'FontWeight', 'bold', ...
'BackgroundColor', [0.2 0.8 0.2], ...
'FontColor', [1 1 1], ...
'ButtonPushedFcn', @(btn,event) obj.evaluateSystem());
% 结果表格
obj.resultTable = uitable(obj.appFig, 'Position', [50 50 1300 230],...
'ColumnName', {'文件名', '裂缝数量', '最大长度(mm)', '平均宽度(mm)', '最大宽度(mm)', '置信度', '状态'},...
'ColumnEditable', [false false false false false false false],...
'ColumnWidth', {'auto', 80, 100, 100, 100, 80, 100},...
'CellSelectionCallback', @(src,event) obj.showSelectedImage(event));
end
function selectInputFolder(obj)
% 选择输入文件夹
folder = uigetdir('', '选择输入图像文件夹');
if folder
obj.inputFolder = folder;
obj.inputPathLabel.Text = folder;
obj.updateStatus(sprintf('输入文件夹: %s', folder));
end
end
function selectOutputFolder(obj)
% 选择输出文件夹
folder = uigetdir('', '选择结果保存文件夹');
if folder
obj.outputFolder = folder;
obj.outputPathLabel.Text = folder;
obj.updateStatus(sprintf('输出文件夹: %s', folder));
end
end
function selectTrainFolder(obj, labelHandle)
% 选择训练数据文件夹
folder = uigetdir('', '选择训练数据集文件夹');
if folder
obj.trainDataDir = folder;
labelHandle.Text = folder;
obj.updateStatus(sprintf('训练数据: %s', folder));
end
end
function setPixelSize(obj, size)
% 设置像素尺寸
obj.pixelSize = size;
obj.updateStatus(sprintf('像素尺寸设置为: %.4f mm/像素', size));
end
function setConfThreshold(obj, threshold)
% 设置置信度阈值
obj.confThreshold = threshold;
obj.updateStatus(sprintf('置信度阈值设置为: %.2f', threshold));
end
function setMinCrackArea(obj, area)
% 设置最小裂缝面积
obj.minCrackArea = area;
obj.updateStatus(sprintf('最小裂缝面积设置为: %d 像素', area));
end
function updateStatus(obj, message, type)
% 更新状态栏
if nargin < 3
type = 'info';
end
switch type
case 'info'
color = [0 0.5 0]; % 绿色
case 'warning'
color = [0.8 0.6 0]; % 黄色
case 'error'
color = [0.8 0 0]; % 红色
otherwise
color = [0 0 0]; % 黑色
end
obj.statusLabel.Text = message;
obj.statusLabel.FontColor = color;
drawnow;
end
function batchDetection(obj)
% 批量检测裂缝
if isempty(obj.inputFolder) || isempty(obj.outputFolder)
obj.updateStatus('请先选择输入和输出文件夹', 'error');
return;
end
if isempty(obj.unetModel)
obj.updateStatus('未加载U-Net模型,请先训练或加载模型', 'error');
return;
end
% 创建输出目录
resultDir = fullfile(obj.outputFolder, 'results');
maskDir = fullfile(obj.outputFolder, 'masks');
reportDir = fullfile(obj.outputFolder, 'reports');
if ~exist(resultDir, 'dir'), mkdir(resultDir); end
if ~exist(maskDir, 'dir'), mkdir(maskDir); end
if ~exist(reportDir, 'dir'), mkdir(reportDir); end
% 获取图像文件
imageFiles = dir(fullfile(obj.inputFolder, '*.jpg'));
imageFiles = [imageFiles; dir(fullfile(obj.inputFolder, '*.png'))];
imageFiles = [imageFiles; dir(fullfile(obj.inputFolder, '*.tif'))];
imageFiles = [imageFiles; dir(fullfile(obj.inputFolder, '*.bmp'))];
numImages = length(imageFiles);
if numImages == 0
obj.updateStatus('输入文件夹中没有找到图像文件', 'error');
return;
end
% 初始化结果表格
results = cell(numImages, 7);
summaryData = zeros(numImages, 4); % 裂缝数量, 最大长度, 平均宽度, 最大宽度
% 创建进度条
progressFig = uifigure('Name', '处理进度', 'Position', [500 500 400 150], ...
'CloseRequestFcn', @(src,event) setappdata(gcbf, 'cancelled', true));
setappdata(progressFig, 'cancelled', false);
progressBar = uiprogressbar(progressFig, 'Position', [50 80 300 30]);
progressLabel = uilabel(progressFig, 'Text', '准备开始...', 'Position', [50 50 300 20]);
cancelBtn = uibutton(progressFig, 'push', 'Text', '取消', 'Position', [150 20 100 20],...
'ButtonPushedFcn', @(btn,event) setappdata(progressFig, 'cancelled', true));
% 处理每张图像
for i = 1:numImages
% 检查是否取消
if getappdata(progressFig, 'cancelled')
obj.updateStatus('用户取消了处理', 'warning');
break;
end
% 更新进度
progressBar.Value = i/numImages;
progressLabel.Text = sprintf('处理 %d/%d: %s', i, numImages, imageFiles(i).name);
drawnow;
try
% 读取图像
imgPath = fullfile(imageFiles(i).folder, imageFiles(i).name);
img = imread(imgPath);
% 显示原始图像
imshow(img, 'Parent', obj.imgAxes);
title(obj.imgAxes, imageFiles(i).name, 'Interpreter', 'none');
% 使用GAN增强图像
enhancedImg = obj.enhanceWithGAN(img);
% 裂缝检测
[mask, scores] = obj.detectCracks(enhancedImg);
% 裂缝量化分析
[numCracks, maxLength, avgWidth, maxWidth] = obj.quantifyCracks(mask);
% 转换为实际尺寸
maxLengthMM = maxLength * obj.pixelSize;
avgWidthMM = avgWidth * obj.pixelSize;
maxWidthMM = maxWidth * obj.pixelSize;
% 保存结果
results(i,:) = {imageFiles(i).name, numCracks, maxLengthMM, avgWidthMM, maxWidthMM, mean(scores), '成功'};
summaryData(i,:) = [numCracks, maxLengthMM, avgWidthMM, maxWidthMM];
% 显示结果
overlayImg = labeloverlay(img, mask, 'Transparency', 0.7, ...
'Colormap', [0 0 0; 1 0 0]); % 背景黑色,裂缝红色
imshow(overlayImg, 'Parent', obj.resultAxes);
title(obj.resultAxes, '检测结果');
% 保存检测结果
imwrite(overlayImg, fullfile(resultDir, ['result_' imageFiles(i).name]));
imwrite(mask, fullfile(maskDir, ['mask_' imageFiles(i).name]));
% 更新表格
obj.resultTable.Data = results;
% 生成单个报告
obj.generateReport(imageFiles(i).name, img, mask, ...
numCracks, maxLengthMM, avgWidthMM, maxWidthMM, ...
fullfile(reportDir, ['report_' imageFiles(i).name(1:end-4) '.pdf']));
catch ME
% 错误处理
results(i,:) = {imageFiles(i).name, NaN, NaN, NaN, NaN, NaN, sprintf('错误: %s', ME.message)};
obj.resultTable.Data = results;
obj.updateStatus(sprintf('处理图像 %s 时出错: %s', imageFiles(i).name, ME.message), 'error');
end
end
% 保存结果表格
if isvalid(progressFig) && ~getappdata(progressFig, 'cancelled')
% 保存Excel报告
resultsTable = cell2table(results, 'VariableNames', ...
{'Filename', 'CrackCount', 'MaxLength_mm', 'AvgWidth_mm', 'MaxWidth_mm', 'Confidence', 'Status'});
writetable(resultsTable, fullfile(obj.outputFolder, 'detection_results.xlsx'));
% 保存汇总统计
summaryStats = array2table(summaryData, 'VariableNames', ...
{'CrackCount', 'MaxLength_mm', 'AvgWidth_mm', 'MaxWidth_mm'});
writetable(summaryStats, fullfile(obj.outputFolder, 'summary_statistics.xlsx'));
% 生成PDF总结报告
obj.generateSummaryReport(results, fullfile(obj.outputFolder, 'summary_report.pdf'));
progressLabel.Text = '处理完成!';
uibutton(progressFig, 'push', 'Text', '确定', 'Position', [150 20 100 20],...
'ButtonPushedFcn', @(btn,event) delete(progressFig));
obj.updateStatus(sprintf('成功处理 %d/%d 张图像', sum(strcmp(results(:,7), '成功')), numImages));
elseif isvalid(progressFig)
delete(progressFig);
end
end
function enhancedImg = enhanceWithGAN(obj, img)
% GAN图像增强
if ~isempty(obj.ganGenerator)
try
% 预处理图像
inputSize = obj.ganGenerator.Layers(1).InputSize(1:2);
resizedImg = imresize(img, inputSize);
% 归一化到[-1,1]范围
if isinteger(resizedImg)
resizedImg = im2single(resizedImg);
end
normalizedImg = (resizedImg - 0.5) * 2;
% 通过生成器
if isa(obj.ganGenerator, 'dlnetwork')
dlImg = dlarray(normalizedImg, 'SSCB');
dlEnhanced = predict(obj.ganGenerator, dlImg);
enhancedImg = extractdata(dlEnhanced);
else
enhancedImg = predict(obj.ganGenerator, normalizedImg);
end
% 后处理
enhancedImg = (enhancedImg / 2) + 0.5; % 转换回[0,1]范围
enhancedImg = imresize(enhancedImg, [size(img,1), size(img,2)]);
% 转换为原始数据类型
if isinteger(img)
enhancedImg = im2uint8(enhancedImg);
end
catch ME
obj.updateStatus(sprintf('GAN增强失败: %s,使用传统增强', ME.message), 'warning');
enhancedImg = obj.traditionalEnhancement(img);
end
else
enhancedImg = obj.traditionalEnhancement(img);
end
end
function img = traditionalEnhancement(obj, img)
% 传统图像增强方法
if size(img, 3) == 3
img = rgb2gray(img);
end
% 自适应直方图均衡化
img = adapthisteq(img);
% 对比度调整
img = imadjust(img);
% 锐化
img = imsharpen(img, 'Amount', 2);
end
function [mask, scores] = detectCracks(obj, img)
% 使用U-Net模型检测裂缝
inputSize = obj.unetModel.Layers(1).InputSize(1:2);
resizedImg = imresize(img, inputSize);
if isa(obj.unetModel, 'DAGNetwork') || isa(obj.unetModel, 'SeriesNetwork')
[mask, scores] = semanticseg(resizedImg, obj.unetModel);
else
% 自定义网络预测
mask = semanticseg(resizedImg, obj.unetModel);
scores = ones(size(mask)); % 默认置信度
end
% 只保留裂缝类别
mask = mask == 'crack';
% 应用置信度阈值
if exist('scores', 'var') && ~isempty(scores)
mask = mask & [s scores(:,:,2) > obj.confThreshold];
end
% 后处理
mask = bwareaopen(mask, obj.minCrackArea); % 移除小区域
mask = imclose(mask, strel('disk', 3)); % 闭合小孔
% 恢复到原始尺寸
mask = imresize(mask, [size(img,1), size(img,2)], 'nearest');
end
function [numCracks, maxLength, avgWidth, maxWidth] = quantifyCracks(obj, mask)
% 量化裂缝参数
if ~any(mask(:))
numCracks = 0;
maxLength = 0;
avgWidth = 0;
maxWidth = 0;
return;
end
% 1. 连通区域分析
cc = bwconncomp(mask);
numCracks = cc.NumObjects;
% 2. 计算裂缝长度
lengths = zeros(1, numCracks);
for i = 1:numCracks
crackImg = false(size(mask));
crackImg(cc.PixelIdxList{i}) = true;
% 骨架化计算长度
skel = bwskel(crackImg);
lengths(i) = sum(skel(:));
end
maxLength = max(lengths);
% 3. 计算裂缝宽度
dist = bwdist(~mask);
widths = 2 * dist(mask);
avgWidth = mean(widths(widths > 0));
maxWidth = max(widths);
end
function generateReport(obj, filename, img, mask, numCracks, maxLength, avgWidth, maxWidth, outputPath)
% 生成单个裂缝报告
fig = figure('Visible', 'off', 'Units', 'inches', 'Position', [0 0 8.5 11]);
% 标题
annotation(fig, 'textbox', [0.1 0.9 0.8 0.1], 'String', '混凝土裂缝检测报告', ...
'FontSize', 20, 'FontWeight', 'bold', 'HorizontalAlignment', 'center', 'LineStyle', 'none');
% 图像信息
annotation(fig, 'textbox', [0.1 0.85 0.8 0.05], 'String', ['文件名: ' filename], ...
'FontSize', 12, 'HorizontalAlignment', 'left', 'LineStyle', 'none');
% 显示原始图像和结果
ax1 = axes('Position', [0.1 0.6 0.35 0.2]);
imshow(img, 'Parent', ax1);
title('原始图像');
ax2 = axes('Position', [0.55 0.6 0.35 0.2]);
overlayImg = labeloverlay(img, mask, 'Transparency', 0.7, 'Colormap', [0 0 0; 1 0 0]);
imshow(overlayImg, 'Parent', ax2);
title('检测结果');
% 量化结果
resultsText = {...
sprintf('裂缝数量: %d', numCracks), ...
sprintf('最大长度: %.2f mm', maxLength), ...
sprintf('平均宽度: %.2f mm', avgWidth), ...
sprintf('最大宽度: %.2f mm', maxWidth)};
annotation(f, fig, 'textbox', [0.1 0.4 0.8 0.15], 'String', resultsText, ...
'FontSize', 14, 'FontWeight', 'bold', 'LineStyle', 'none');
% 裂缝属性分布
ax3 = axes('Position', [0.1 0.1 0.8 0.25]);
if numCracks > 0
% 计算每个裂缝的属性
cc = bwconncomp(mask);
lengths = zeros(1, cc.NumObjects);
widths = zeros(1, cc.NumObjects);
for i = 1:cc.NumObjects
crackImg = false(size(mask));
crackImg(cc.PixelIdxList{i}) = true;
% 长度
skel = bwskel(crackImg);
lengths(i) = sum(skel(:)) * obj.pixelSize;
% 宽度
dist = bwdist(~crackImg);
crackWidths = 2 * dist(crackImg);
widths(i) = mean(crackWidths(crackWidths > 0)) * obj.pixelSize;
end
% 绘制分布
yyaxis left;
bar(1:numCracks, lengths);
ylabel('长度 (mm)');
yyaxis right;
plot(1:numCracks, widths, 'ro-', 'MarkerSize', 8, 'LineWidth', 2);
ylabel('平均宽度 (mm)');
xlabel('裂缝编号');
title('裂缝属性分布');
legend('长度', '宽度', 'Location', 'best');
grid on;
else
text(0.5, 0.5, '未检测到裂缝', 'HorizontalAlignment', 'center', 'FontSize', 14);
axis off;
end
% 保存为PDF
exportgraphics(fig, outputPath, 'ContentType', 'vector');
close(fig);
end
function generateSummaryReport(obj, results, outputPath)
% 生成总结报告
fig = figure('Visible', 'off', 'Units', 'inches', 'Position', [0 0 8.5 11]);
% 标题
annotation(fig, 'textbox', [0.1 0.95 0.8 0.05], 'String', '混凝土裂缝检测总结报告', ...
'FontSize', 20, 'FontWeight', 'bold', 'HorizontalAlignment', 'center', 'LineStyle', 'none');
% 基本信息
infoText = {...
sprintf('检测时间: %s', datestr(now)), ...
sprintf('输入文件夹: %s', obj.inputFolder), ...
sprintf('输出文件夹: %s', obj.outputFolder), ...
sprintf('处理图像数量: %d', size(results, 1)), ...
sprintf('检测到裂缝的图像数量: %d', sum(cell2mat(results(:,2)) > 0))};
annotation(fig, 'textbox', [0.1 0.85 0.8 0.08], 'String', infoText, ...
'FontSize', 12, 'LineStyle', 'none');
% 提取数值数据
validIdx = cellfun(@(x) isscalar(x) && ~isnan(x), results(:,2));
crackCounts = cell2mat(results(validIdx,2));
maxLengths = cell2mat(results(validIdx,3));
avgWidths = cell2mat(results(validIdx,4));
maxWidths = cell2mat(results(validIdx,5));
% 统计图表
ax1 = subplot(2,2,1, 'Parent', fig);
histogram(ax1, crackCounts, 'BinMethod', 'integers');
title(ax1, '裂缝数量分布');
xlabel(ax1, '裂缝数量');
ylabel(ax1, '图像数量');
grid(ax1, 'on');
ax2 = subplot(2,2,2, 'Parent', fig);
histogram(ax2, maxLengths, 20);
title(ax2, '最大长度分布');
xlabel(ax2, '最大长度 (mm)');
ylabel(ax2, '图像数量');
grid(ax2, 'on');
ax3 = subplot(2,2,3, 'Parent', fig);
histogram(ax3, avgWidths, 20);
title(ax3, '平均宽度分布');
xlabel(ax3, '平均宽度 (mm)');
ylabel(ax3, '图像数量');
grid(ax3, 'on');
ax4 = subplot(2,2,4, 'Parent', fig);
histogram(ax4, maxWidths, 20);
title(ax4, '最大宽度分布');
xlabel(ax4, '最大宽度 (mm)');
ylabel(ax4, '图像数量');
grid(ax4, 'on');
% 保存为PDF
exportgraphics(fig, outputPath, 'ContentType', 'vector');
close(fig);
end
function showSelectedImage(obj, event)
% 显示选中的图像
if ~isempty(event.Indices)
row = event.Indices(1);
data = obj.resultTable.Data;
filename = data{row,1};
if ~isempty(obj.inputFolder)
imgPath = fullfile(obj.inputFolder, filename);
if exist(imgPath, 'file')
img = imread(imgPath);
imshow(img, 'Parent', obj.imgAxes);
title(obj.imgAxes, filename, 'Interpreter', 'none');
% 尝试显示结果
resultPath = fullfile(obj.outputFolder, 'results', ['result_' filename]);
if exist(resultPath, 'file')
resultImg = imread(resultPath);
imshow(resultImg, 'Parent', obj.resultAxes);
title(obj.resultAxes, '检测结果');
end
end
end
end
end
function evaluateSystem(obj)
% 系统评估
testDir = uigetdir('', '选择测试数据集目录');
if testDir == 0
return; % 用户取消
end
% 加载测试数据
imgDir = fullfile(testDir, 'images');
maskDir = fullfile(testDir, 'masks');
imgFiles = dir(fullfile(imgDir, '*.jpg'));
imgFiles = [imgFiles; dir(fullfile(imgDir, '*.png'))];
maskFiles = dir(fullfile(maskDir, '*.png'));
if numel(imgFiles) ~= numel(maskFiles)
errordlg('测试图像和掩码数量不匹配', '数据错误');
return;
end
% 初始化指标
totalTP = 0; totalFP = 0; totalFN = 0;
lengthErrors = [];
widthErrors = [];
detectionTimes = [];
% 进度条
progressFig = uifigure('Name', '系统评估', 'Position', [500 500 400 150]);
progressBar = uiprogressbar(progressFig, 'Position', [50 80 300 30]);
progressLabel = uilabel(progressFig, 'Text', '开始评估...', 'Position', [50 50 300 20]);
% 处理每个样本
for i = 1:numel(imgFiles)
progressBar.Value = i/numel(imgFiles);
progressLabel.Text = sprintf('评估 %d/%d: %s', i, numel(imgFiles), imgFiles(i).name);
drawnow;
try
% 读取图像和真实掩码
img = imread(fullfile(imgDir, imgFiles(i).name));
trueMask = imread(fullfile(maskDir, maskFiles(i).name));
trueMask = imbinarize(trueMask);
% 计时
tic;
% 增强和检测
enhancedImg = obj.enhanceWithGAN(img);
[predMask, ~] = obj.detectCracks(enhancedImg);
detectionTimes(end+1) = toc;
% 计算分割指标
tp = sum(predMask & trueMask, 'all');
fp = sum(predMask & ~trueMask, 'all');
fn = sum(~predMask & trueMask, 'all');
totalTP = totalTP + tp;
totalFP = totalFP + fp;
totalFN = totalFN + fn;
% 计算量化指标
if any(trueMask(:))
[~, trueMaxLength, trueAvgWidth] = obj.quantifyCracks(trueMask);
[~, predMaxLength, predAvgWidth] = obj.quantifyCracks(predMask);
lengthErrors = [lengthErrors, abs(trueMaxLength - predMaxLength) * obj.pixelSize];
widthErrors = [widthErrors, abs(trueAvgWidth - predAvgWidth) * obj.pixelSize];
end
catch ME
warning('评估样本 %s 时出错: %s', imgFiles(i).name, ME.message);
end
end
% 计算指标
precision = totalTP / (totalTP + totalFP);
recall = totalTP / (totalTP + totalFN);
f1Score = 2 * (precision * recall) / (precision + recall);
meanLengthError = mean(lengthErrors);
meanWidthError = mean(widthErrors);
meanDetectionTime = mean(detectionTimes);
% 显示结果
resultText = sprintf(['系统评估结果:\n'...
'精确率: %.4f\n'...
'召回率: %.4f\n'...
'F1分数: %.4f\n'...
'平均长度误差: %.2f mm\n'...
'平均宽度误差: %.2f mm\n'...
'平均检测时间: %.2f 秒'], ...
precision, recall, f1Score, meanLengthError, meanWidthError, meanDetectionTime);
% 保存评估报告
evalReport = fullfile(obj.outputFolder, 'evaluation_report.txt');
fid = fopen(evalReport, 'w');
fprintf(fid, '混凝土裂缝检测系统评估报告\n');
fprintf(fid, '评估时间: %s\n\n', datestr(now));
fprintf(fid, '性能指标:\n');
fprintf(fid, '精确率: %.4f\n', precision);
fprintf(fid, '召回率: %.4f\n', recall);
fprintf(fid, 'F1分数: %.4f\n', f1Score);
fprintf(fid, '平均长度误差: %.4f mm\n', meanLengthError);
fprintf(fid, '平均宽度误差: %.4f mm\n', meanWidthError);
fprintf(fid, '平均检测时间: %.4f 秒\n', meanDetectionTime);
fclose(fid);
msgbox(resultText, '评估结果');
close(progressFig);
obj.updateStatus(sprintf('评估完成! F1分数: %.2f', f1Score));
end
function trainUnetModel(obj)
% 训练U-Net模型
if isempty(obj.trainDataDir)
obj.updateStatus('请先选择训练数据集', 'error');
return;
end
obj.updateStatus('开始训练U-Net模型...');
try
% 准备数据集
imds = imageDatastore(fullfile(obj.trainDataDir, 'images'));
pxds = pixelLabelDatastore(fullfile(obj.trainDataDir, 'masks'), {'background', 'crack'}, [0 1]);
% 划分训练验证集
[imdsTrain, imdsVal, pxdsTrain, pxdsVal] = partitionDataset(imds, pxds, 0.8);
% 创建U-Net架构
inputSize = [256 256 3];
numClasses = 2;
lgraph = unetLayers(inputSize, numClasses);
% 数据增强
augmenter = imageDataAugmenter(...
'RandXReflection', true, ...
'RandYReflection', true, ...
'RandRotation', [-30 30], ...
'RandXTranslation', [-30 30], ...
'RandYTranslation', [-30 30]);
pximds = pixelLabelImageDatastore(imdsTrain, pxdsTrain, ...
'DataAugmentation', augmenter, ...
'OutputSize', inputSize(1:2));
% 训练选项
options = trainingOptions('adam', ...
'InitialLearnRate', 1e-3, ...
'MaxEpochs', 50, ...
'MiniBatchSize', 8, ...
'ValidationData', {imdsVal, pxdsVal}, ...
'ValidationFrequency', 100, ...
'Plots', 'training-progress', ...
'OutputFcn', @(info) obj.trainingUpdate(info), ...
'ExecutionEnvironment', 'gpu');
% 训练模型
[net, info] = trainNetwork(pximds, lgraph, options);
% 保存模型
save('crack_unet.mat', 'net');
obj.unetModel = net;
obj.updateStatus('U-Net模型训练完成并保存');
catch ME
obj.updateStatus(sprintf('训练失败: %s', ME.message), 'error');
end
end
function trainGANModel(obj)
% 训练GAN模型
if isempty(obj.trainDataDir)
obj.updateStatus('请先选择训练数据集', 'error');
return;
end
obj.updateStatus('开始训练GAN模型...');
try
% 加载图像数据集
imds = imageDatastore(fullfile(obj.trainDataDir, 'images'), ...
'IncludeSubfolders', true, ...
'LabelSource', 'foldernames');
% 调整图像大小
imds = augmentedImageDatastore([256 256], imds, 'ColorPreprocessing', 'gray2rgb');
% 定义生成器
generator = [
imageInputLayer([1 1 100], 'Normalization', 'none', 'Name', 'in')
transposedConv2dLayer(4, 512, 'Name', 'tconv1')
batchNormalizationLayer('Name', 'bn1')
reluLayer('Name', 'relu1')
transposedConv2dLayer(4, 256, 'Stride', 2, 'Cropping', 1, 'Name', 'tconv2')
batchNormalizationLayer('Name', 'bn2')
reluLayer('Name', 'relu2')
transposedConv2dLayer(4, 128, 'Stride', 2, 'Cropping', 1, 'Name', 'tconv3')
batchNormalizationLayer('Name', 'bn3')
reluLayer('Name', 'relu3')
transposedConv2dLayer(4, 64, 'Stride', 2, 'Cropping', 1, 'Name', 'tconv4')
batchNormalizationLayer('Name', 'bn4')
reluLayer('Name', 'relu4')
transposedConv2dLayer(4, 3, 'Stride', 2, 'Cropping', 1, 'Name', 'tconv5')
tanhLayer('Name', 'tanh')];
% 定义判别器
discriminator = [
imageInputLayer([256 256 3], 'Normalization', 'none', 'Name', 'in')
convolution2dLayer(4, 64, 'Stride', 2, 'Padding', 1, 'Name', 'conv1')
leakyReluLayer(0.2, 'Name', 'lrelu1')
convolution2dLayer(4, 128, 'Stride', 2, 'Padding', 1, 'Name', 'conv2')
batchNormalizationLayer('Name', 'bn1')
leakyReluLayer(0.2, 'Name', 'lrelu2')
convolution2dLayer(4, 256, 'Stride', 2, 'Padding', 1, 'Name', 'conv3')
batchNormalizationLayer('Name', 'bn2')
leakyReluLayer(0.2, 'Name', 'lrelu3')
convolution2dLayer(4, 512, 'Stride', 2, 'Padding', 1, 'Name', 'conv4')
batchNormalizationLayer('Name', 'bn3')
leakyReluLayer(0.2, 'Name', 'lrelu4')
convolution2dLayer(4, 1, 'Name', 'conv5')
sigmoidLayer('Name', 'sigmoid')];
% 创建GAN
gan = gan(generator, discriminator);
% 训练选项
options = trainingOptions('adam', ...
'MaxEpochs', 200, ...
'MiniBatchSize', 16, ...
'Plots', 'training-progress', ...
'OutputFcn', @(info) obj.trainingUpdate(info), ...
'ExecutionEnvironment', 'gpu');
% 训练GAN
[gan, info] = trainGAN(gan, imds, options);
% 保存生成器
genNet = gan.Generator;
save('crack_gan_generator.mat', 'genNet');
obj.ganGenerator = genNet;
obj.updateStatus('GAN模型训练完成并保存');
catch ME
obj.updateStatus(sprintf('训练失败: %s', ME.message), 'error');
end
end
function stop = trainingUpdate(~, info)
% 训练进度更新函数
stop = false;
if info.State == "iteration"
fprintf('Epoch %d, Iteration %d, Loss: %.4f\n', ...
info.Epoch, info.Iteration, info.TrainingLoss);
end
end
function closeSystem(obj)
% 关闭系统
delete(obj.appFig);
end
end
end
%% 辅助函数
function [imdsTrain, imdsVal, pxdsTrain, pxdsVal] = partitionDataset(imds, pxds, splitRatio)
% 随机划分数据集
numFiles = numel(imds.Files);
shuffledIndices = randperm(numFiles);
trainSize = round(splitRatio * numFiles);
trainIndices = shuffledIndices(1:trainSize);
valIndices = shuffledIndices(trainSize+1:end);
imdsTrain = subset(imds, trainIndices);
imdsVal = subset(imds, valIndices);
pxdsTrain = subset(pxds, trainIndices);
pxdsVal = subset(pxds, valIndices);
end
function [generator, discriminator] = createGANNetworks()
% 创建GAN网络组件
% 定义生成器
generator = [
imageInputLayer([1 1 100], 'Normalization', 'none', 'Name', 'in_gen')
transposedConv2dLayer(4, 512, 'Name', 'tconv1')
batchNormalizationLayer('Name', 'bn1_gen')
reluLayer('Name', 'relu1_gen')
transposedConv2dLayer(4, 256, 'Stride', 2, 'Cropping', 1, 'Name', 'tconv2')
batchNormalizationLayer('Name', 'bn2_gen')
reluLayer('Name', 'relu2_gen')
transposedConv2dLayer(4, 128, 'Stride', 2, 'Cropping', 1, 'Name', 'tconv3')
batchNormalizationLayer('Name', 'bn3_gen')
reluLayer('Name', 'relu3_gen')
transposedConv2dLayer(4, 64, 'Stride', 2, 'Cropping', 1, 'Name', '极conv4')
batchNormalizationLayer('Name', 'bn4_gen')
reluLayer('Name', 'relu4_gen')
transposedConv2dLayer(4, 3, 'Stride', 2, 'Cropping', 1, 'Name', 'tconv5')
tanhLayer('Name', 'tanh_gen')];
% 定义判别器
discriminator = [
imageInputLayer([256 256 3], 'Normalization', 'none', 'Name', 'in_disc')
convolution2dLayer(4, 64, 'Stride', 2, 'Padding', 1, 'Name', 'conv1_disc')
leakyReluLayer(0.2, 'Name', 'lrelu1_disc')
convolution2dLayer(4, 128, 'Stride', 2, 'Padding', 1, 'Name', 'conv2_disc')
batchNormalizationLayer('Name', 'bn1_disc')
leakyReluLayer(0.2, 'Name', 'lrelu2_disc')
convolution2dLayer(4, 256, 'Stride', 2, 'Padding', 1, 'Name', 'conv3_disc')
batchNormalizationLayer('Name', 'bn2_disc')
leakyReluLayer(0.2, 'Name', 'lrelu3_disc')
convolution2dLayer(4, 512, 'Stride', 2, 'Padding', 1, 'Name', 'conv4_disc')
batchNormalizationLayer('Name', 'bn3_disc')
leakyReluLayer(0.2, 'Name', 'lrelu4_disc')
convolution2dLayer(4, 1, 'Name', 'conv5_disc')
sigmoidLayer('Name', 'sigmoid_disc')];
end
%% 系统启动函数
function runCrackDetectionSystem()
% 创建系统实例
crackSystem = CrackDetectionSystem();
end
时图片不显示
最新发布