Textbox.Visible=False隐藏方式导致的问题

本文描述了一个因Textbox可见性设置不当导致的线上功能故障案例。具体表现为,在正式环境中第二位用户无法获取第一位用户的输入内容,经排查发现是由于Textbox的Visible属性在不同浏览器下表现不一致所致。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  今天公司的正式环境有个功能不好使,但是测试环境没有问题,经过和同事的研讨,发现应该是我在写代码的时候把Textbox的visible属性设置为false导致的。

  当时的需求是需要在发邮件的时候加上“相关说明”,而“相关说明”的内容是由第一个发邮件的人填写的,第二个人在接到邮件以后,登录系统,发送第二封邮件,

  邮件的“相关说明”是用第一个人的最后一次写的“相关说明”,自己不写“相关说明”。由于公用一个上传,发邮件页面,并且只有第一个人能够看到“相关说明”的

  文本框。因此,我把Textbox的Visible默认设置为false,第一个发邮件的时候再设置成TRUE。

  程序在测试环境运行没有问题,然而今天在正式环境运行的时候,却出现了第二个人没有获取到第一个人的“相关说明”。

  通过同事的帮忙排查,发现因为Textbox的visible被设置成了false,虽然可以赋值,可能由于不同浏览器的原因,没有解析出来Textbox,无法取值,所以导致了没有相关说明。

  同事给出了用div的方法来隐藏和显示

  

复制代码

  

复制代码

  后台用以下代码来显示

  mydiv.Style.Value = "display.block";

  以下是浏览器解析后的代码

  

复制代码

  1、信息查询

  2、系统说明

``` namespace WindowsFormsApp2 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private Student student = new Student(); private void Form1_Load(object sender, EventArgs e) { student.Courses.AddRange(new[] { new Course{ Name="数学", Credit=4, Score=92 }, new Course{ Name="语文", Credit=3, Score=80 }, new Course{ Name="英语", Credit=2, Score=98 }, new Course{ Name="物理", Credit=6, Score=70 }, new Course{ Name="化学", Credit=3, Score=89 } }); //绑定数据源 dgvCourses.DataSource = student.Courses; dgvCourses.Columns["Points"].Visible = false; //隐藏计算列 } private void dgvColumnsSetup() { dgvCourses.AutoGenerateColumns = false; dgvCourses.Columns.Clear(); DataGridViewTextBoxColumn colCredit = new DataGridViewTextBoxColumn(); colCredit.HeaderText = "学分"; colCredit.DataPropertyName = "Credit"; DataGridViewTextBoxColumn colScore = new DataGridViewTextBoxColumn(); colScore.HeaderText = "成绩"; colScore.DataPropertyName = "Score"; dgvCourses.Columns.AddRange(colCredit, colScore); } private void button1_Click(object sender, EventArgs e) { double gpa = student.CalculateGPA(); textBox1.Text = $"当前GPA:{gpa:F2}"; } } public class Course { public string Name { get; set; } //课程名称 public int Credit { get; set; } //学分 public int Score { get; set; } //成绩 public double Points { get { if (Score >= 90) return 4.0; else if (Score >= 80) return 3.0; else if (Score >= 70) return 2.0; else if (Score >= 60) return 1.0; else return 0.0; } } } public class Student { public List<Course> Courses { get; set; } = new List<Course>(); //计算GPA public double CalculateGPA() { double totalPoints = 0; int totalCredits = 0; foreach (var course in Courses) { totalPoints += course.Points * course.Credit; totalCredits += course.Credit; } return totalCredits == 0 ? 0 : totalPoints / totalCredits; } } }```为什么在datagridview控件中无数据显示
03-30
classdef ECG_Monitor_System < handle properties % 通信相关 serialPort % 串口对象 isConnected = false comPort = 'COM1' % 默认串口 baudRate = 115200 % OEM协议常用波特率 dataBits = 8 % 数据位 parity = 'none' % 校验位 stopBits = 1 % 停止位 % 数据管理 ecgData = zeros(12, 10000); filteredData = zeros(12, 10000); timeStamps = zeros(1, 10000); sampleRate = 1000; currentIndex = 1; dataLength = 0; isRecording = false; timerObj maxDataPoints = 10000 % 最大数据点数 numLeads = 12 % 导联数量 activeLead = 2 % 默认显示II导联 % 滤波参数 highPassFreq = 0.5; lowPassFreq = 100; notchFreq = 50; % 工频陷波 % 分析参数 heartRate = 0; rPeakPositions = []; qtInterval = 0; stSegment = 0; % 心电特征参数 qtIntervals = [] % QT间期数组(秒) stSegments = [] % ST段偏移数组(mV) avgQT = 0 % 平均QT间期 avgST = 0 % 平均ST偏移 % GUI组件 fig % 主控制窗口 overviewFig % 总览窗口 singleLeadFigs = gobjects(12, 1); % 单导联窗口 leadPlots = gobjects(12, 1); % 单导联绘图句柄 overviewPlots = gobjects(12, 1); % 总览窗口绘图句柄 leadNames = {'I', 'II', 'III', 'aVR', 'aVL', 'aVF', 'V1', 'V2', 'V3', 'V4', 'V5', 'V6'}; % 控制面板组件 controlPanel highPassEdit lowPassEdit notchCheckbox statusText heartRateText recordButton saveButton leadSelect dataSlider positionText % 频谱分析 spectrumAx % 报告生成 patientInfo = struct('name', '未知', 'gender', '未知', 'age', 0, 'id', ''); end methods function app = ECG_Monitor_System() app.createMainGUI(); app.createOverviewWindow(); app.initializeSystem(); end function createMainGUI(app) % 创建主控制窗口 app.fig = figure('Name', '心电数据管理系统', 'NumberTitle', 'off',... 'Position', [100, 100, 800, 600], 'CloseRequestFcn', @app.onClose,... 'MenuBar', 'none', 'ToolBar', 'none'); % 创建菜单栏 app.createMenus(); % 创建控制面板 app.createControlPanel(); end function createOverviewWindow(app) % 创建总览窗口 app.overviewFig = figure('Name', '心电图总览', 'NumberTitle', 'off',... 'Position', [950, 100, 1200, 800], 'CloseRequestFcn', @app.onOverviewClose); % 创建12导联网格布局 for i = 1:12 row = ceil(i/3); col = mod(i-1,3)+1; ax = subplot(4, 3, i, 'Parent', app.overviewFig); app.overviewPlots(i) = plot(ax, nan, nan, 'b-'); title(ax, app.leadNames{i}); xlabel(ax, '时间(s)'); ylabel(ax, '幅度(mV)'); grid(ax, 'on'); end end function createSingleLeadWindow(app, leadIndex) % 创建单导联窗口 if ishandle(app.singleLeadFigs(leadIndex)) figure(app.singleLeadFigs(leadIndex)); % 激活现有窗口 return; end figName = sprintf('导联 %s 详细视图', app.leadNames{leadIndex}); app.singleLeadFigs(leadIndex) = figure('Name', figName, 'NumberTitle', 'off',... 'Position', [100 + 50*leadIndex, 100 + 50*leadIndex, 600, 400],... 'CloseRequestFcn', @(src,evt) app.onSingleLeadClose(leadIndex)); ax = axes('Parent', app.singleLeadFigs(leadIndex), 'Position', [0.1, 0.15, 0.85, 0.75]); app.leadPlots(leadIndex) = plot(ax, nan, nan, 'b-'); title(ax, sprintf('导联 %s - 详细视图', app.leadNames{leadIndex})); xlabel(ax, '时间(s)'); ylabel(ax, '幅度(mV)'); grid(ax, 'on'); % 添加导联特定控制 uicontrol('Parent', app.singleLeadFigs(leadIndex), 'Style', 'pushbutton',... 'String', '检测R波', 'Position', [20, 10, 80, 25],... 'Callback', @(src,evt) app.detectRPeaksForLead(leadIndex)); uicontrol('Parent', app.singleLeadFigs(leadIndex), 'Style', 'pushbutton',... 'String', '频谱分析', 'Position', [120, 10, 80, 25],... 'Callback', @(src,evt) app.showSpectrumForLead(leadIndex)); end function createMenus(app) % 文件菜单 fileMenu = uimenu(app.fig, 'Text', '文件'); uimenu(fileMenu, 'Text', '加载数据', 'MenuSelectedFcn', @app.loadDataFile); uimenu(fileMenu, 'Text', '保存数据', 'MenuSelectedFcn', @app.saveData); uimenu(fileMenu, 'Text', '生成报告', 'MenuSelectedFcn', @app.generateReport); uimenu(fileMenu, 'Text', '退出', 'Separator', 'on', 'MenuSelectedFcn', @app.onClose); % 通信菜单 comMenu = uimenu(app.fig, 'Text', '通信'); uimenu(comMenu, 'Text', '连接设备', 'MenuSelectedFcn', @app.connectDevice); uimenu(comMenu, 'Text', '断开连接', 'MenuSelectedFcn', @app.disconnect); % 视图菜单 viewMenu = uimenu(app.fig, 'Text', '视图'); uimenu(viewMenu, 'Text', '显示总览窗口', 'MenuSelectedFcn', @app.showOverviewWindow); uimenu(viewMenu, 'Text', '显示所有导联窗口', 'MenuSelectedFcn', @app.showAllLeadWindows); uimenu(viewMenu, 'Text', '关闭所有导联窗口', 'MenuSelectedFcn', @app.closeAllLeadWindows); % 设置菜单 settingsMenu = uimenu(app.fig, 'Text', '设置'); uimenu(settingsMenu, 'Text', '通信设置', 'MenuSelectedFcn', @app.setCommunication); uimenu(settingsMenu, 'Text', '患者信息', 'MenuSelectedFcn', @app.setPatientInfo); uimenu(settingsMenu, 'Text', '分析参数', 'MenuSelectedFcn', @app.setAnalysisParams); end function createControlPanel(app) % 创建控制面板 app.controlPanel = uipanel('Parent', app.fig, 'Title', '控制面板',... 'Position', [0.05, 0.05, 0.9, 0.9]); % 通信控制 uicontrol('Parent', app.controlPanel, 'Style', 'pushbutton',... 'String', '连接设备', 'Position', [20, 520, 100, 30],... 'Callback', @app.connectDevice); uicontrol('Parent', app.controlPanel, 'Style', 'pushbutton',... 'String', '断开连接', 'Position', [140, 520, 100, 30],... 'Callback', @app.disconnect); app.statusText = uicontrol('Parent', app.controlPanel, 'Style', 'text',... 'String', '状态: 未连接', 'Position', [260, 520, 300, 30],... 'HorizontalAlignment', 'left', 'FontSize', 10); % 滤波设置 uicontrol('Parent', app.controlPanel, 'Style', 'text', 'String', '高通滤波(Hz):',... 'Position', [20, 470, 100, 25], 'FontSize', 10); app.highPassEdit = uicontrol('Parent', app.controlPanel, 'Style', 'edit',... 'String', '0.5', 'Position', [130, 470, 60, 25], 'FontSize', 10); uicontrol('Parent', app.controlPanel, 'Style', 'text', 'String', '低通滤波(Hz):',... 'Position', [20, 440, 100, 25], 'FontSize', 10); app.lowPassEdit = uicontrol('Parent', app.controlPanel, 'Style', 'edit',... 'String', '100', 'Position', [130, 440, 60, 25], 'FontSize', 10); uicontrol('Parent', app.controlPanel, 'Style', 'pushbutton', 'String', '应用滤波',... 'Position', [200, 450, 80, 30], 'Callback', @app.applyFilters); % 工频陷波 app.notchCheckbox = uicontrol('Parent', app.controlPanel, 'Style', 'checkbox',... 'String', '50Hz陷波', 'Position', [300, 450, 100, 25], 'Value', 1, 'FontSize', 10); % 数据记录 app.recordButton = uicontrol('Parent', app.controlPanel, 'Style', 'togglebutton',... 'String', '开始记录', 'Position', [20, 380, 100, 30],... 'Callback', @app.toggleRecording); app.saveButton = uicontrol('Parent', app.controlPanel, 'Style', 'pushbutton',... 'String', '保存数据', 'Position', [140, 380, 100, 30],... 'Callback', @app.saveData); % 导联选择 uicontrol('Parent', app.controlPanel, 'Style', 'text',... 'String', '导联选择:', 'Position', [20, 340, 80, 25], 'FontSize', 10); app.leadSelect = uicontrol('Parent', app.controlPanel, 'Style', 'popup',... 'String', app.leadNames, 'Position', [110, 340, 100, 25],... 'Callback', @app.onLeadSelected, 'FontSize', 10); % 打开导联窗口按钮 uicontrol('Parent', app.controlPanel, 'Style', 'pushbutton',... 'String', '打开导联窗口', 'Position', [230, 340, 120, 30],... 'Callback', @app.openSelectedLeadWindow); % 心率显示 uicontrol('Parent', app.controlPanel, 'Style', 'text',... 'String', '心率:', 'Position', [20, 300, 50, 25], 'FontSize', 10); app.heartRateText = uicontrol('Parent', app.controlPanel, 'Style', 'text',... 'String', '-- bpm', 'Position', [80, 300, 80, 25], 'FontSize', 10); % 数据导航 uicontrol('Parent', app.controlPanel, 'Style', 'text',... 'String', '数据位置:', 'Position', [20, 260, 80, 25], 'FontSize', 10); app.dataSlider = uicontrol('Parent', app.controlPanel, 'Style', 'slider',... 'Position', [110, 260, 300, 25], 'Callback', @app.sliderMoved,... 'Enable', 'off'); app.positionText = uicontrol('Parent', app.controlPanel, 'Style', 'text',... 'String', '0/0', 'Position', [420, 260, 80, 25], 'FontSize', 10); % 视图控制按钮 uicontrol('Parent', app.controlPanel, 'Style', 'pushbutton',... 'String', '显示总览窗口', 'Position', [20, 200, 120, 30],... 'Callback', @app.showOverviewWindow); uicontrol('Parent', app.controlPanel, 'Style', 'pushbutton',... 'String', '显示所有导联窗口', 'Position', [160, 200, 150, 30],... 'Callback', @app.showAllLeadWindows); uicontrol('Parent', app.controlPanel, 'Style', 'pushbutton',... 'String', '关闭所有导联窗口', 'Position', [330, 200, 150, 30],... 'Callback', @app.closeAllLeadWindows); % 频谱分析 spectrumPanel = uipanel('Parent', app.controlPanel, 'Title', '频谱分析',... 'Position', [0.05, 0.05, 0.9, 0.15]); app.spectrumAx = axes('Parent', spectrumPanel, 'Position', [0.1, 0.2, 0.85, 0.7]); title(app.spectrumAx, 'ECG信号频谱'); xlabel(app.spectrumAx, '频率(Hz)'); ylabel(app.spectrumAx, '幅度(dB)'); grid(app.spectrumAx, 'on'); end function initializeSystem(app) % 初始化系统参数 app.sampleRate = 1000; app.dataLength = 0; app.currentIndex = 1; app.isRecording = false; % 初始化串口参数(默认值需符合OEM协议) app.comPort = 'COM1'; app.baudRate = 115200; app.dataBits = 8; app.parity = 'none'; app.stopBits = 1; % 设置默认患者信息 app.patientInfo = struct(... 'name', '张三', ... 'gender', '男', ... 'age', 45, ... 'id', 'P2023001', ... 'date', datestr(now, 'yyyy-mm-dd')); end %% 窗口管理功能 function showOverviewWindow(app, ~, ~) % 显示总览窗口 if ishandle(app.overviewFig) figure(app.overviewFig); else app.createOverviewWindow(); end end function showAllLeadWindows(app, ~, ~) % 显示所有导联窗口 for i = 1:12 app.createSingleLeadWindow(i); end end function closeAllLeadWindows(app, ~, ~) % 关闭所有导联窗口 for i = 1:12 if ishandle(app.singleLeadFigs(i)) delete(app.singleLeadFigs(i)); end end end function openSelectedLeadWindow(app, ~, ~) % 打开选中的导联窗口 leadIdx = get(app.leadSelect, 'Value'); app.createSingleLeadWindow(leadIdx); end function onLeadSelected(app, src, ~) % 导联选择回调 leadIdx = get(src, 'Value'); app.updateSpectrumForLead(leadIdx); end function onOverviewClose(app, ~, ~) % 总览窗口关闭回调 delete(app.overviewFig); app.overviewFig = gobjects(1); end function onSingleLeadClose(app, leadIndex) % 单导联窗口关闭回调 delete(app.singleLeadFigs(leadIndex)); app.singleLeadFigs(leadIndex) = gobjects(1); end %% 通信协议实现 function setCommunication(app, ~, ~) % 串口设置对话框 prompt = {'串口号:', '波特率:', '数据位:', '校验位:', '停止位:'}; dlgtitle = '串口设置'; dims = [1 20]; definput = {app.comPort, num2str(app.baudRate), num2str(app.dataBits), ... app.parity, num2str(app.stopBits)}; answer = inputdlg(prompt, dlgtitle, dims, definput); if ~isempty(answer) app.comPort = answer{1}; app.baudRate = str2double(answer{2}); app.dataBits = str2double(answer{3}); app.parity = answer{4}; app.stopBits = str2double(answer{5}); % 验证波特率是否合法(示例:仅允许常见波特率) validBaudRates = [9600, 19200, 38400, 57600, 115200, 230400]; if ~any(app.baudRate == validBaudRates) app.baudRate = 115200; % 默认恢复为常用值 warndlg('波特率设置不合法,已恢复为115200', '设置警告'); end set(app.statusText, 'String', '串口设置已更新'); end end function connectDevice(app, ~, ~) try % 关闭现有连接 if ~isempty(app.serialPort) && isvalid(app.serialPort) fclose(app.serialPort); delete(app.serialPort); end % 创建串口对象(根据OEM协议配置参数) app.serialPort = serialport(app.comPort, app.baudRate, ... 'DataBits', app.dataBits, ... 'Parity', app.parity, ... 'StopBits', app.stopBits); % 设置数据接收回调(OEM协议通常以特定字节结尾,如CR/LF或固定帧长) configureCallback(app.serialPort, "terminator", "linefeed", ... @(src, ~) app.serialCallback(src)); % 初始化数据缓冲区 app.ecgData = zeros(12, 10000); app.filteredData = zeros(12, 10000); app.timeStamps = zeros(1, 10000); app.currentIndex = 1; app.dataLength = 0; % 启用数据导航 set(app.dataSlider, 'Enable', 'on'); set(app.statusText, 'String', ['已连接到串口: ' app.comPort ' (波特率: ' num2str(app.baudRate) ')']); app.isConnected = true; catch ME errordlg(['串口连接失败: ' ME.message], '连接错误'); app.isConnected = false; end end function disconnect(app, ~, ~) if ~isempty(app.serialPort) && isvalid(app.serialPort) fclose(app.serialPort); delete(app.serialPort); app.serialPort = []; set(app.statusText, 'String', '已断开串口连接'); app.isConnected = false; end end function serialCallback(app, src) if src.NumBytesAvailable > 0 % 根据OEM协议读取数据(示例:假设每帧13个浮点数,以逗号分隔) % 注意:实际OEM协议可能包含帧头、校验位等,需按协议解析 try % 读取一帧数据(假设每帧以换行符结尾) data = readline(src); % 解析OEM协议数据(示例:假设数据格式为时间戳+12导联值,逗号分隔) values = sscanf(data, '%f,', [13, 1]); if numel(values) == 13 % 存储数据 if app.dataLength < size(app.ecgData, 2) app.dataLength = app.dataLength + 1; end app.timeStamps(app.dataLength) = values(1) / 1000; % 转换为秒 app.ecgData(:, app.dataLength) = values(2:13); % 实时滤波 app.realTimeFiltering(app.dataLength); % 更新显示 app.updatePlots(); % 实时分析 if mod(app.dataLength, 50) == 0 app.realTimeAnalysis(); end % 更新数据导航 set(app.dataSlider, 'Value', app.dataLength / size(app.ecgData, 2)); set(app.positionText, 'String',... sprintf('%d/%d', app.dataLength, size(app.ecgData, 2))); end catch ME % 处理解析错误(如数据格式不符、校验失败等) warning(['OEM协议解析错误: ' ME.message]); end end end %% 数据处理模块 function realTimeFiltering(app, idx) % 实时滤波处理 if idx == 1 % 初始化滤波数据 app.filteredData(:, idx) = app.ecgData(:, idx); return; end % 获取滤波参数 hpFreq = str2double(get(app.highPassEdit, 'String')); lpFreq = str2double(get(app.lowPassEdit, 'String')); useNotch = get(app.notchCheckbox, 'Value'); for i = 1:12 % 应用高通滤波 if ~isnan(hpFreq) && hpFreq > 0 [b, a] = butter(2, hpFreq/(app.sampleRate/2), 'high'); if idx > 10 segment = app.ecgData(i, max(1, idx-10):idx); filteredSeg = filtfilt(b, a, segment); app.filteredData(i, idx) = filteredSeg(end); else app.filteredData(i, idx) = app.ecgData(i, idx); end else app.filteredData(i, idx) = app.ecgData(i, idx); end % 应用低通滤波 if ~isnan(lpFreq) && lpFreq > 0 [b, a] = butter(4, lpFreq/(app.sampleRate/2), 'low'); if idx > 10 segment = app.filteredData(i, max(1, idx-10):idx); filteredSeg = filtfilt(b, a, segment); app.filteredData(i, idx) = filteredSeg(end); end end % 应用50Hz陷波 if useNotch wo = 50/(app.sampleRate/2); bw = wo/10; [b, a] = iirnotch(wo, bw); if idx > 10 segment = app.filteredData(i, max(1, idx-10):idx); filteredSeg = filtfilt(b, a, segment); app.filteredData(i, idx) = filteredSeg(end); end end end end function applyFilters(app, ~, ~) % 应用全局滤波 if app.dataLength > 0 % 获取滤波参数 hpFreq = str2double(get(app.highPassEdit, 'String')); lpFreq = str2double(get(app.lowPassEdit, 'String')); useNotch = get(app.notchCheckbox, 'Value'); for i = 1:12 % 高通滤波 if ~isnan(hpFreq) && hpFreq > 0 [b, a] = butter(2, hpFreq/(app.sampleRate/2), 'high'); app.filteredData(i, 1:app.dataLength) = ... filtfilt(b, a, app.ecgData(i, 1:app.dataLength)); end % 低通滤波 if ~isnan(lpFreq) && lpFreq > 0 [b, a] = butter(4, lpFreq/(app.sampleRate/2), 'low'); app.filteredData(i, 1:app.dataLength) = ... filtfilt(b, a, app.filteredData(i, 1:app.dataLength)); end % 50Hz陷波 if useNotch wo = 50/(app.sampleRate/2); bw = wo/10; [b, a] = iirnotch(wo, bw); app.filteredData(i, 1:app.dataLength) = ... filtfilt(b, a, app.filteredData(i, 1:app.dataLength)); end end % 更新显示 app.updatePlots(); app.updateSpectrum(); set(app.statusText, 'String', '全局滤波已应用'); end end %% 实时分析模块 function realTimeAnalysis(app) % 实时分析ECG信号 if app.dataLength > 100 % 在II导联检测R波 leadIndex = 2; data = app.filteredData(leadIndex, max(1, app.dataLength-500):app.dataLength); % 使用Pan-Tompkins算法检测R波 [~, qrs_i_raw] = app.pan_tompkins(data, app.sampleRate); % 计算心率 if length(qrs_i_raw) > 1 rrIntervals = diff(qrs_i_raw) / app.sampleRate; app.heartRate = 60 / mean(rrIntervals); set(app.heartRateText, 'String', sprintf('%.1f bpm', app.heartRate)); % 更新图形标记 hold(app.ax(leadIndex), 'on'); if ~isempty(app.rPeakMarkers) && isvalid(app.rPeakMarkers) delete(app.rPeakMarkers); end % 计算实际时间位置 timeOffset = app.timeStamps(max(1, app.dataLength-500)); timePos = app.timeStamps(qrs_i_raw + max(1, app.dataLength-500) - 1); app.rPeakMarkers = plot(app.ax(leadIndex),... timePos, data(qrs_i_raw), 'ro', 'MarkerSize', 6, 'MarkerFaceColor', 'r'); hold(app.ax(leadIndex), 'off'); end end end function [qrs_amp_raw, qrs_i_raw] = pan_tompkins(~, ecg, fs) % Pan-Tompkins R波检测算法 % 1. 带通滤波 (5-15 Hz) f1 = 5/(fs/2); f2 = 15/(fs/2); [b, a] = butter(1, [f1, f2], 'bandpass'); ecg_filtered = filtfilt(b, a, ecg); % 2. 微分 diff_ecg = diff(ecg_filtered); diff_ecg = [diff_ecg(1), diff_ecg]; % 3. 平方 sqr_ecg = diff_ecg .^ 2; % 4. 移动平均窗口积分 window_size = round(0.150 * fs); integrated_ecg = movmean(sqr_ecg, window_size); % 5. 自适应阈值检测 max_h = max(integrated_ecg); threshold = 0.2 * max_h; % 6. 寻找R波位置 [qrs_amp_raw, qrs_i_raw] = findpeaks(integrated_ecg,... 'MinPeakHeight', threshold,... 'MinPeakDistance', round(0.2*fs)); end %% 数据管理 function toggleRecording(app, src, ~) % 开始/停止记录数据 if src.Value app.isRecording = true; set(src, 'String', '停止记录'); set(app.statusText, 'String', '记录中...'); % 初始化记录缓冲区 app.ecgData = zeros(12, 10000); app.filteredData = zeros(12, 10000); app.timeStamps = zeros(1, 10000); app.currentIndex = 1; app.dataLength = 0; else app.isRecording = false; set(src, 'String', '开始记录'); set(app.statusText, 'String', '记录已停止'); end end function loadDataFile(app, ~, ~) % 加载ECG数据文件 [filename, pathname] = uigetfile({'*.mat;*.csv;*.txt', '数据文件 (*.mat, *.csv, *.txt)'},... '选择ECG数据文件'); if isequal(filename, 0) return; end filepath = fullfile(pathname, filename); [~, ~, ext] = fileparts(filepath); try h = waitbar(0, '加载数据中...', 'Name', '数据处理'); switch lower(ext) case '.mat' data = load(filepath); waitbar(0.3, h); if isfield(data, 'ecgData') app.ecgData = data.ecgData; app.filteredData = data.filteredData; app.timeStamps = data.timeStamps; app.sampleRate = data.sampleRate; else error('无法识别MAT文件中的数据格式'); end case {'.csv', '.txt'} data = readmatrix(filepath); waitbar(0.3, h); if size(data, 2) >= 13 app.timeStamps = data(:, 1)'; app.ecgData = data(:, 2:13)'; app.sampleRate = 1 / mean(diff(app.timeStamps)); else error('CSV/TXT文件应包含时间戳和12导联数据'); end end app.dataLength = size(app.ecgData, 2); app.currentIndex = app.dataLength; % 应用滤波 waitbar(0.7, h, '应用滤波...'); app.applyFilters(); % 更新界面 waitbar(0.9, h, '更新界面...'); app.updatePlots(); app.updateSpectrum(); % 配置导航滑块 set(app.dataSlider, 'Min', 0, 'Max', 1, 'Value', 1,... 'SliderStep', [100/app.dataLength, 1000/app.dataLength],... 'Enable', 'on'); set(app.positionText, 'String',... sprintf('%d/%d', app.dataLength, size(app.ecgData, 2))); set(app.statusText, 'String',... sprintf('已加载: %s (%d个样本)', filename, app.dataLength)); waitbar(1, h, '完成!'); pause(0.5); delete(h); catch ME errordlg(sprintf('加载失败: %s', ME.message), '文件错误'); end end function saveData(app, ~, ~) % 保存ECG数据 if app.dataLength == 0 errordlg('没有可保存的数据', '保存错误'); return; end [filename, pathname] = uiputfile({'*.mat', 'MAT文件 (*.mat)'; '*.csv', 'CSV文件 (*.csv)'},... '保存ECG数据', 'ecg_data.mat'); if isequal(filename, 0) return; end filepath = fullfile(pathname, filename); [~, ~, ext] = fileparts(filepath); try app.ecgData = app.ecgData(:, 1:app.dataLength); app.filteredData = app.filteredData(:, 1:app.dataLength); app.timeStamps = app.timeStamps(1:app.dataLength); app.sampleRate = app.sampleRate; switch lower(ext) case '.mat' save(filepath, 'ecgData', 'filteredData', 'timeStamps', 'sampleRate'); case '.csv' dataToSave = [app.timeStamps', app.ecgData']; writematrix(dataToSave, filepath); end set(app.statusText, 'String', sprintf('数据已保存至: %s', filename)); catch ME errordlg(sprintf('保存失败: %s', ME.message), '保存错误'); end end %% 显示更新 function updatePlots(app) % 更新所有窗口的心电图显示 if app.dataLength > 0 windowSize = 500; % 显示窗口大小 startIdx = max(1, app.currentIndex - windowSize); endIdx = min(app.dataLength, app.currentIndex + windowSize); timeWindow = app.timeStamps(startIdx:endIdx); % 更新总览窗口 if ishandle(app.overviewFig) for i = 1:12 if ishandle(app.overviewPlots(i)) set(app.overviewPlots(i), 'XData', timeWindow,... 'YData', app.filteredData(i, startIdx:endIdx)); % 自动调整Y轴范围 dataRange = range(app.filteredData(i, startIdx:endIdx)); if dataRange > 0 ax = get(app.overviewPlots(i), 'Parent'); ylim(ax, [min(app.filteredData(i, startIdx:endIdx)) - 0.1*dataRange,... max(app.filteredData(i, startIdx:endIdx)) + 0.1*dataRange]); end end end drawnow; end % 更新单导联窗口 for i = 1:12 if ishandle(app.singleLeadFigs(i)) && ishandle(app.leadPlots(i)) set(app.leadPlots(i), 'XData', timeWindow,... 'YData', app.filteredData(i, startIdx:endIdx)); % 自动调整Y轴范围 dataRange = range(app.filteredData(i, startIdx:endIdx)); if dataRange > 0 ax = get(app.leadPlots(i), 'Parent'); ylim(ax, [min(app.filteredData(i, startIdx:endIdx)) - 0.1*dataRange,... max(app.filteredData(i, startIdx:endIdx)) + 0.1*dataRange]); end drawnow; end end end end function updateSpectrumForLead(app, leadIndex) % 更新指定导联的频谱分析 if app.dataLength > 0 && leadIndex >= 1 && leadIndex <= 12 signal = app.filteredData(leadIndex, max(1, app.dataLength-2000):app.dataLength); % 计算FFT N = length(signal); if N < 10, return; end f = (0:N-1)*(app.sampleRate/N); Y = fft(signal); P2 = abs(Y/N); P1 = P2(1:floor(N/2)+1); P1(2:end-1) = 2*P1(2:end-1); f = f(1:floor(N/2)+1); % 更新频谱分析图 plot(app.spectrumAx, f, 20*log10(P1), 'b'); title(app.spectrumAx, sprintf('导联 %s 频谱', app.leadNames{leadIndex})); xlabel(app.spectrumAx, '频率 (Hz)'); ylabel(app.spectrumAx, '幅度 (dB)'); xlim(app.spectrumAx, [0, min(150, app.sampleRate/2)]); grid(app.spectrumAx, 'on'); end end function showSpectrumForLead(app, leadIndex) % 在单独窗口中显示导联频谱 if app.dataLength > 0 && leadIndex >= 1 && leadIndex <= 12 figName = sprintf('导联 %s 频谱分析', app.leadNames{leadIndex}); app.fig = figure('Name', figName, 'NumberTitle', 'off',... 'Position', [200, 200, 600, 400]); ax = axes('Parent', app.fig, 'Position', [0.1, 0.15, 0.85, 0.75]); signal = app.filteredData(leadIndex, max(1, app.dataLength-2000):app.dataLength); N = length(signal); f = (0:N-1)*(app.sampleRate/N); Y = fft(signal); P2 = abs(Y/N); P1 = P2(1:floor(N/2)+1); P1(2:end-1) = 2*P1(2:end-1); f = f(1:floor(N/2)+1); plot(ax, f, 20*log10(P1), 'b'); title(ax, sprintf('导联 %s 频谱分析', app.leadNames{leadIndex})); xlabel(ax, '频率 (Hz)'); ylabel(ax, '幅度 (dB)'); xlim(ax, [0, min(150, app.sampleRate/2)]); grid(ax, 'on'); end end function detectRPeaksForLead(app, leadIndex) % 检测指定导联的R波 if app.dataLength > 0 && leadIndex >= 1 && leadIndex <= 12 data = app.filteredData(leadIndex, max(1, app.dataLength-2000):app.dataLength); % 使用Pan-Tompkins算法检测R波 [~, qrs_i_raw] = app.pan_tompkins(data, app.sampleRate); % 在图形上标记R波 if ishandle(app.singleLeadFigs(leadIndex)) app.fig = app.singleLeadFigs(leadIndex); ax = findobj(app.fig, 'Type', 'axes'); % 清除旧标记 oldMarkers = findobj(ax, 'Tag', 'RPeakMarker'); if ~isempty(oldMarkers) delete(oldMarkers); end % 添加新标记 hold(ax, 'on'); plot(ax, app.timeStamps(qrs_i_raw + max(1, app.dataLength-2000) - 1),... data(qrs_i_raw), 'ro', 'MarkerSize', 8, 'MarkerFaceColor', 'r',... 'Tag', 'RPeakMarker'); hold(ax, 'off'); % 计算并显示心率 if length(qrs_i_raw) > 1 rrIntervals = diff(qrs_i_raw) / app.sampleRate; app.heartRate = 60 / mean(rrIntervals); title(ax, sprintf('导联 %s - 心率: %.1f bpm',... app.leadNames{leadIndex}, app.heartRate)); end end end end %% 报告生成 function setPatientInfo(app, ~, ~) % 设置患者信息 prompt = {'姓名:', '性别:', '年龄:', '患者ID:'}; dlgtitle = '患者信息'; dims = [1 25]; definput = {app.patientInfo.name, app.patientInfo.gender,... num2str(app.patientInfo.age), app.patientInfo.id}; answer = inputdlg(prompt, dlgtitle, dims, definput); if ~isempty(answer) app.patientInfo.name = answer{1}; app.patientInfo.gender = answer{2}; app.patientInfo.age = str2double(answer{3}); app.patientInfo.id = answer{4}; app.patientInfo.date = datestr(now, 'yyyy-mm-dd HH:MM'); end end function generateReport(app, ~, ~) % 生成心电图报告 if app.dataLength == 0 errordlg('没有数据可生成报告', '报告错误'); return; end [filename, pathname] = uiputfile('*.pdf', '保存心电图报告', 'ECG_Report.pdf'); if filename == 0 return; end pdfFile = fullfile(pathname, filename); app.exportToPDF(pdfFile); msgbox(sprintf('心电图报告已保存至:\n%s', pdfFile), '报告生成'); end function exportToPDF(app, filename) % 导出PDF报告 app.fig = figure('Visible', 'off', 'Position', [100, 100, 1200, 900]); % 报告标题 uicontrol('Style', 'text', 'String', '心电图报告',... 'Position', [400, 850, 400, 40], 'FontSize', 20, 'FontWeight', 'bold'); % 患者信息 app.patientInfo = {sprintf('姓名: %s', app.patientInfo.name),... sprintf('性别: %s', app.patientInfo.gender),... sprintf('年龄: %d', app.patientInfo.age),... sprintf('ID: %s', app.patientInfo.id),... sprintf('检查日期: %s', app.patientInfo.date)}; uicontrol('Style', 'text', 'String', app.patientInfo,... 'Position', [50, 780, 300, 100], 'FontSize', 12, 'HorizontalAlignment', 'left'); % 分析结果 analysisInfo = {sprintf('心率: %.1f bpm', app.heartRate),... sprintf('采样率: %d Hz', app.sampleRate),... sprintf('数据长度: %.1f 秒', app.timeStamps(app.dataLength))}; uicontrol('Style', 'text', 'String', analysisInfo,... 'Position', [50, 680, 300, 60], 'FontSize', 12, 'HorizontalAlignment', 'left'); % 绘制12导联心电图 for i = 1:12 row = ceil(i/3); col = mod(i-1,3)+1; ax = subplot(4,3,i, 'Parent', app.fig); if app.dataLength > 0 % 显示完整数据 plot(ax, app.timeStamps(1:app.dataLength),... app.filteredData(i, 1:app.dataLength), 'b-'); end title(ax, app.leadNames{i}); xlabel(ax, '时间(s)'); ylabel(ax, '幅度(mV)'); grid(ax, 'on'); end % 导出为PDF exportgraphics(app.fig, filename, 'ContentType', 'vector'); close(app.fig); end %% 系统控制 function onClose(app, ~, ~) % 关闭应用程序 app.disconnect(); % 关闭所有窗口 if ishandle(app.overviewFig) delete(app.overviewFig); end for i = 1:12 if ishandle(app.singleLeadFigs(i)) delete(app.singleLeadFigs(i)); end end delete(app.fig); end end end % 辅助函数 function result = iff(condition, trueValue, falseValue) if condition result = trueValue; else result = falseValue; end end
06-15
运行:%% 混凝土裂缝检测系统 - 完整单文件实现 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 时图片不显示
最新发布
08-17
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值