☰
PyQt5+ONNX实现语义分割GUI系统(毕设可运行版)
2026/10/14 15:31:47 网站建设 项目流程

简介:这是一套面向计算机相关专业本科生与初阶AI学习者的语义分割毕设级实战项目,适用于课程设计、期末大作业及毕业设计场景,覆盖计科、人工智能、数据科学、物联网等方向。项目基于Python与PyQt5构建完整GUI交互界面,集成预训练分割模型(含ckpt_epoch_50.pth)、图像处理工具链(utils/、LDN_transforms.py)、主流解码器模块(如DeepLabV3Plus、UPerNet、HAM Head)及数据集生成脚本(mini_dataset.py),开箱即用且全部功能经实测验证稳定。压缩包共25个文件,含20个Python源码(含主程序main.py、UI逻辑Qt_seg.py、模型定义DFormer.py等)、1个Qt界面文件(Qt_seg.ui)、1个说明文档(介绍.md)、1张效果示意图(result.jpg)、1个依赖清单(requirements.txt)和1个模型权重文件,整体44.7MB。目前已有284人学习下载,提供清晰目录结构、可二次开发的模块化设计、中文注释及运行注意事项提示,是入门语义分割工程落地与GUI集成的高价值参考范例。

1. 毕设能跑通语义分割的GUI系统,为什么90%的学生卡在“模型加载就崩”这一步?

这不是一个教你怎么调参、怎么刷SOTA指标的高阶项目,而是一个专为本科毕设场景打磨的可交付、可演示、可答辩的语义分割落地闭环:从拖拽图片进窗口,到实时显示带颜色掩码的分割结果,再到导出标注图和类别统计——全程点鼠标,不敲命令行。很多同学用PyTorch写完训练脚本,一到部署就懵:模型转ONNX失败?PyQt5调用GPU报错?QLabel显示分割图是全黑?其实问题根本不在算法,而在Python生态里模型推理与GUI线程的天然冲突——PyQt5主线程不能被长耗时的模型前向阻塞,而初学者常把model(input)直接塞进按钮回调里,结果界面假死、程序崩溃、答辩前夜重装环境。本方案用轻量级UNet(非ResNet101+DeepLabv3+那种重型结构),搭配ONNX Runtime CPU推理+双线程信号机制,实测在i5-8250U+8GB内存笔记本上,单图推理+渲染<1.2秒。适合图像处理、智能医疗、农业识别等方向的毕设选题,尤其推荐给没接触过模型部署、但需要现场演示效果的同学。


2. 用PyQt5搭出能加载模型、拖入图片、显示分割结果的最小GUI框架

2.1 创建主窗口并布局三大核心区域:控件区、原图区、分割结果区

我们不追求炫酷动效,只保留最必要的交互元素:顶部放“加载模型”和“选择图片”按钮,中间左侧显示原始图像(QLabel),右侧显示带颜色掩码的分割结果(QLabel),底部加一个状态栏显示当前类别数和推理耗时。关键在于所有QLabel必须预设固定尺寸(如640×480)并启用setScaledContents(True),否则图片拉伸变形、掩码错位。以下代码生成基础窗口结构:

# main_window.py import sys from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QStatusBar, QFileDialog, QMessageBox) from PyQt5.QtCore import Qt, QSize from PyQt5.QtGui import QPixmap, QImage class SegmentationWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("语义分割演示系统(毕设版)") self.resize(1280, 720) # 中央部件与主布局 central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout(central_widget) # 顶部按钮区 btn_layout = QHBoxLayout() self.load_model_btn = QPushButton("加载模型(.onnx)") self.load_img_btn = QPushButton("选择图片(.jpg/.png)") btn_layout.addWidget(self.load_model_btn) btn_layout.addWidget(self.load_img_btn) main_layout.addLayout(btn_layout) # 图像显示区(左右并排) img_layout = QHBoxLayout() self.original_label = QLabel("原始图像") self.original_label.setAlignment(Qt.AlignCenter) self.original_label.setMinimumSize(QSize(640, 480)) self.original_label.setStyleSheet("background-color: #f0f0f0; border: 1px solid #ccc;") self.original_label.setScaledContents(True) self.result_label = QLabel("分割结果") self.result_label.setAlignment(Qt.AlignCenter) self.result_label.setMinimumSize(QSize(640, 480)) self.result_label.setStyleSheet("background-color: #f0f0f0; border: 1px solid #ccc;") self.result_label.setScaledContents(True) img_layout.addWidget(self.original_label) img_layout.addWidget(self.result_label) main_layout.addLayout(img_layout) # 状态栏 self.statusBar().showMessage("就绪:请先加载模型") # 初始化状态变量 self.model_path = None self.image_path = None self.session = None # ONNX Runtime会话对象

提示:setMinimumSize()比resize()更可靠——它强制控件不随窗口缩放而压缩,避免QLabel内部图像比例错乱;setScaledContents(True)让QImage自动适配控件尺寸,省去手动resize图像的步骤,这对后续快速显示分割结果至关重要。

2.2 实现模型加载逻辑:验证ONNX格式、检查输入输出节点、绑定推理会话

毕设场景下,模型必须脱离PyTorch环境独立运行,因此我们强制使用ONNX格式(.onnx后缀)。加载时需做三重校验:文件存在性、ONNX格式合法性、输入输出张量匹配性。常见翻车点是训练时用torch.nn.Upsample,导出ONNX后某些版本不支持动态scale_factor,导致RuntimeError。本方案采用torch.nn.functional.interpolate并固定size参数,确保导出稳定。加载代码如下:

# 在SegmentationWindow类中添加方法 def load_model(self): options = QFileDialog.Options() file_name, _ = QFileDialog.getOpenFileName( self, "选择ONNX模型文件", "", "ONNX Files (*.onnx);;All Files (*)", options=options ) if not file_name: return try: import onnxruntime as ort # 验证ONNX文件结构 onnx_model = ort.InferenceSession(file_name, providers=['CPUExecutionProvider']) # 检查输入输出是否符合预期(UNet典型结构) inputs = onnx_model.get_inputs() outputs = onnx_model.get_outputs() if len(inputs) != 1 or len(outputs) != 1: raise ValueError(f"模型输入/输出节点数量异常:输入{len(inputs)}个,输出{len(outputs)}个") input_shape = inputs[0].shape output_shape = outputs[0].shape if not (len(input_shape) == 4 and input_shape[1] == 3): # NCHW, 3通道 raise ValueError(f"输入应为4D张量且第2维=3(RGB),实际形状{input_shape}") if not (len(output_shape) == 4 and output_shape[1] >= 2): # 至少2类(背景+目标) raise ValueError(f"输出应为4D张量且第2维≥2(类别数),实际形状{output_shape}") self.session = onnx_model self.model_path = file_name self.statusBar().showMessage(f"✅ 模型加载成功:{file_name.split('/')[-1]} | 输入{input_shape} → 输出{output_shape}") QMessageBox.information(self, "成功", f"模型已加载,支持{output_shape[1]}类分割") except Exception as e: self.session = None self.statusBar().showMessage(f"❌ 模型加载失败:{str(e)}") QMessageBox.critical(self, "错误", f"加载失败:{str(e)}\n请确认模型为标准ONNX格式,且输入为NCHW RGB图像")

参数说明:providers=['CPUExecutionProvider']显式指定CPU推理,避免在无GPU环境触发CUDA初始化失败;input_shape[1] == 3校验RGB通道,防止灰度图误加载;output_shape[1] >= 2确保至少有背景和一个目标类,这是语义分割的基本前提。若你的模型输出是HWC格式(如某些TensorRT导出变体),此处需额外做transpose校验。

2.3 图片加载与预处理:统一尺寸、归一化、转为NCHW张量

GUI中图片加载必须兼顾显示质量和推理兼容性。我们约定:所有输入图像强制缩放到模型输入尺寸(如512×512),而非保持原始分辨率——因为UNet等编码器-解码器结构对输入尺寸敏感,任意尺寸会导致特征图错位。预处理流程为:读取→BGR转RGB→缩放→归一化(除以255.0)→HWC转NCHW→增加batch维度。注意:cv2.imread默认BGR,必须cv2.cvtColor(..., cv2.COLOR_BGR2RGB),否则颜色通道颠倒,分割结果全错。

# 在SegmentationWindow类中添加方法 def load_image(self): if not self.model_path: QMessageBox.warning(self, "警告", "请先加载模型!") return options = QFileDialog.Options() file_name, _ = QFileDialog.getOpenFileName( self, "选择测试图片", "", "Images (*.jpg *.jpeg *.png);;All Files (*)", options=options ) if not file_name: return try: import cv2 import numpy as np # 读取并预处理 img_bgr = cv2.imread(file_name) if img_bgr is None: raise ValueError("无法读取图片,请检查路径和格式") img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) # 获取模型期望输入尺寸(假设为[1,3,512,512]) input_shape = self.session.get_inputs()[0].shape target_h, target_w = input_shape[2], input_shape[3] # 缩放并保持宽高比(填充黑边,非拉伸) h, w = img_rgb.shape[:2] scale = min(target_h / h, target_w / w) new_h, new_w = int(h * scale), int(w * scale) resized = cv2.resize(img_rgb, (new_w, new_h), interpolation=cv2.INTER_AREA) # 填充至目标尺寸(居中填充,黑边) pad_h = (target_h - new_h) // 2 pad_w = (target_w - new_w) // 2 padded = np.pad(resized, ((pad_h, target_h - new_h - pad_h), (pad_w, target_w - new_w - pad_w), (0, 0)), mode='constant', constant_values=0) # 归一化 + 转NCHW + 增加batch维度 tensor = padded.astype(np.float32) / 255.0 tensor = np.transpose(tensor, (2, 0, 1)) # HWC → CHW tensor = np.expand_dims(tensor, axis=0) # CHW → NCHW # 保存原始图像用于显示(缩放但不填充,保持视觉真实) display_img = cv2.resize(img_bgr, (640, 480), interpolation=cv2.INTER_AREA) qimg = QImage(display_img.data, display_img.shape[1], display_img.shape[0], display_img.strides[0], QImage.Format_BGR888) self.original_label.setPixmap(QPixmap.fromImage(qimg)) self.image_path = file_name self.preprocessed_tensor = tensor self.statusBar().showMessage(f"✅ 图片加载成功:{file_name.split('/')[-1]} | 预处理为{tensor.shape}") except Exception as e: self.statusBar().showMessage(f"❌ 图片加载失败:{str(e)}") QMessageBox.critical(self, "错误", f"图片处理失败:{str(e)}")

关键细节:“填充黑边”比“拉伸”更科学——拉伸会扭曲物体比例,导致分割边界偏移;np.pad居中填充保证模型看到的是完整物体,只是周围有黑边(模型对黑边鲁棒);cv2.INTER_AREA用于缩小,cv2.INTER_CUBIC用于放大,此处统一用AREA避免锯齿;QImage.Format_BGR888是因为cv2.imread返回BGR,直接喂给QImage无需再转RGB,提升效率。


3. 把PyTorch训练好的UNet模型导出为ONNX,并验证输入输出一致性

3.1 修改UNet模型定义:移除训练专用层,确保ONNX导出无动态控制流

PyTorch模型导出ONNX失败的头号原因是动态if/for、未注册的自定义op、或依赖torch.jit.trace无法捕获的分支。UNet常见雷区:nn.Dropout在eval模式下会被优化掉,但若训练时用了DropBlock等第三方模块,ONNX不识别;F.interpolate若用scale_factor参数,在旧版ONNX opset中不支持。解决方案:全部改用size=(h,w)参数,并在导出前将模型设为eval模式,禁用所有dropout和bn更新。以下是精简后的UNet核心代码(仅保留导出友好部分):

# unet_model.py import torch import torch.nn as nn import torch.nn.functional as F class DoubleConv(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.conv = nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding=1, bias=False), nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True), nn.Conv2d(out_ch, out_ch, 3, padding=1, bias=False), nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True) ) def forward(self, x): return self.conv(x) class UNet(nn.Module): def __init__(self, n_channels=3, n_classes=2, bilinear=True): super(UNet, self).__init__() self.n_channels = n_channels self.n_classes = n_classes self.bilinear = bilinear self.inc = DoubleConv(n_channels, 64) self.down1 = Down(64, 128) self.down2 = Down(128, 256) self.down3 = Down(256, 512) self.down4 = Down(512, 512) self.up1 = Up(1024, 256, bilinear) self.up2 = Up(512, 128, bilinear) self.up3 = Up(256, 64, bilinear) self.up4 = Up(128, 64, bilinear) self.outc = OutConv(64, n_classes) def forward(self, x): x1 = self.inc(x) x2 = self.down1(x1) x3 = self.down2(x2) x4 = self.down3(x3) x5 = self.down4(x4) x = self.up1(x5, x4) x = self.up2(x, x3) x = self.up3(x, x2) x = self.up4(x, x1) logits = self.outc(x) return logits class Down(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.mpconv = nn.Sequential( nn.MaxPool2d(2), DoubleConv(in_ch, out_ch) ) def forward(self, x): return self.mpconv(x) class Up(nn.Module): def __init__(self, in_ch, out_ch, bilinear=True): super().__init__() if bilinear: self.up = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True) else: self.up = nn.ConvTranspose2d(in_ch // 2, in_ch // 2, 2, stride=2) self.conv = DoubleConv(in_ch, out_ch) def forward(self, x1, x2): x1 = self.up(x1) # 因为upsample可能引入小数尺寸,需裁剪x2以匹配 diffY = x2.size()[2] - x1.size()[2] diffX = x2.size()[3] - x1.size()[3] x2 = x2[:, :, diffY//2:(x2.size()[2]-diffY//2), diffX//2:(x2.size()[3]-diffX//2)] x = torch.cat([x2, x1], dim=1) return self.conv(x) class OutConv(nn.Module): def __init__(self, in_ch, out_ch): super(OutConv, self).__init__() self.conv = nn.Conv2d(in_ch, out_ch, 1) def forward(self, x): return self.conv(x)

注意:Up模块中x2的裁剪逻辑必须显式写出(diffY//2等),不能依赖F.interpolate的align_corners=False自动对齐——后者在ONNX中行为不稳定;nn.Upsample的mode='bilinear'比'nearest'更易导出,且视觉质量更好。

3.2 导出脚本:指定固定输入尺寸、设置opset_version、验证ONNX模型

导出不是torch.onnx.export(model, dummy_input, ...)一行了事。必须:① 构造与训练时完全一致的dummy_input(含batch=1, channel=3, height=512, width=512);② 设置opset_version=11(兼容性最好,支持Resize等新op);③ 用onnx.checker.check_model()验证结构;④ 用onnxruntime加载并跑一次前向,对比PyTorch与ONNX输出是否一致(容忍1e-4误差)。以下是完整导出脚本:

# export_onnx.py import torch import onnx import onnxruntime as ort import numpy as np def export_unet_to_onnx(model_path, onnx_path, input_height=512, input_width=512, opset=11): # 加载训练好的PyTorch模型 model = torch.load(model_path, map_location='cpu') model.eval() # 必须设为eval模式 # 构造dummy input(NCHW, float32) dummy_input = torch.randn(1, 3, input_height, input_width, dtype=torch.float32) # 导出 torch.onnx.export( model, dummy_input, onnx_path, export_params=True, # 存储权重 opset_version=opset, # 推荐11 do_constant_folding=True, # 优化常量 input_names=['input'], # 输入名 output_names=['output'], # 输出名 dynamic_axes={ 'input': {0: 'batch_size', 2: 'height', 3: 'width'}, 'output': {0: 'batch_size', 2: 'height', 3: 'width'} } ) print(f"✅ ONNX模型已导出至:{onnx_path}") # 验证ONNX模型结构 onnx_model = onnx.load(onnx_path) onnx.checker.check_model(onnx_model) print("✅ ONNX模型结构验证通过") # 对比PyTorch与ONNX输出 ort_session = ort.InferenceSession(onnx_path, providers=['CPUExecutionProvider']) torch_out = model(dummy_input) ort_inputs = {ort_session.get_inputs()[0].name: dummy_input.numpy()} ort_out = ort_session.run(None, ort_inputs) # 计算最大绝对误差 max_diff = np.max(np.abs(torch_out.detach().numpy() - ort_out[0])) if max_diff < 1e-4: print(f"✅ 输出一致性验证通过,最大误差:{max_diff:.2e}") else: print(f"❌ 输出差异过大:{max_diff:.2e},请检查模型结构或导出参数") if __name__ == "__main__": # 示例调用 export_unet_to_onnx( model_path="weights/unet_best.pth", onnx_path="weights/unet_best.onnx", input_height=512, input_width=512 )

血泪经验:dynamic_axes必须显式声明,否则ONNX Runtime在推理时会报“输入尺寸不匹配”;do_constant_folding=True能合并BN层到Conv,减小模型体积;若验证时误差超阈值,大概率是Up模块中x2裁剪逻辑在ONNX中未正确映射,此时应改用F.interpolate(x1, size=x2.shape[2:], mode='bilinear')替代nn.Upsample。

3.3 ONNX模型轻量化:用onnx-simplifier移除冗余节点,降低部署门槛

导出的ONNX模型常含大量调试节点(如Identity、Print)、未使用的分支、重复的reshape操作。这些不影响精度,但增大文件体积、拖慢推理速度。onnx-simplifier是业界标准工具,能自动折叠常量、删除无用节点、合并连续op。安装与使用极简:

pip install onnx-simplifier python -m onnxsim weights/unet_best.onnx weights/unet_best_sim.onnx

简化后模型体积通常减少30%~50%,且ONNX Runtime加载速度提升20%以上。更重要的是,它能修复某些PyTorch导出的“隐性bug”——例如BatchNorm层在eval模式下被导出为Constant,但ONNX Runtime未正确处理,简化后自动转为等效的Mul+Add组合,彻底规避兼容性问题。建议毕设交付包中始终提供simplified后缀的ONNX文件,并在README中注明。


4. 在PyQt5中安全调用ONNX Runtime推理:线程隔离、信号通信、结果可视化

4.1 创建独立推理线程类:继承QThread,封装session.run()并发射结果信号

PyQt5的GUI主线程(即QApplication.exec_()所在线程)必须保持响应,任何耗时操作(如模型推理)都必须放入子线程。但不能直接在QThread.run()中调用self.session.run()——因为ONNX Runtime会话对象不是线程安全的,多线程并发调用会崩溃。正确做法:每个线程创建自己的InferenceSession实例,或复用主线程创建的session(需加锁)。本方案选择前者,确保绝对安全。以下是线程类实现:

# inference_thread.py from PyQt5.QtCore import QThread, pyqtSignal import numpy as np class InferenceThread(QThread): # 定义信号:推理完成时发射结果和耗时 finished = pyqtSignal(np.ndarray, float) # mask_array, inference_time error = pyqtSignal(str) def __init__(self, session, input_tensor, parent=None): super().__init__(parent) self.session = session # ONNX Runtime会话(每个线程独享) self.input_tensor = input_tensor def run(self): try: import time start_time = time.time() # 创建新会话(线程安全) from onnxruntime import InferenceSession local_session = InferenceSession( self.session._model_path, providers=['CPUExecutionProvider'] ) # 执行推理 ort_inputs = {local_session.get_inputs()[0].name: self.input_tensor} ort_out = local_session.run(None, ort_inputs) end_time = time.time() inference_time = (end_time - start_time) * 1000 # ms # 输出为(N, C, H, W),取第一个batch,argmax得mask pred = ort_out[0][0] # (C, H, W) mask = np.argmax(pred, axis=0).astype(np.uint8) # (H, W) self.finished.emit(mask, inference_time) except Exception as e: self.error.emit(str(e))

关键设计:local_session在run()中创建,确保每个线程拥有独立会话;self.session._model_path是主线程session的模型路径(私有属性,但安全可用);mask = np.argmax(pred, axis=0)将logits转为整型类别掩码,这是后续彩色可视化的基础。

4.2 主窗口中连接线程信号:启动推理、禁用按钮、更新状态栏、接收结果

线程启动后,GUI需立即反馈用户“正在处理”,避免重复点击。结果返回后,要将mask数组转为彩色QImage并显示在result_label上。这里有两个技术难点:①QImage构造要求数据为uint8且连续内存(np.ascontiguousarray);② 彩色映射需预定义颜色表(colormap),本方案用Pascal VOC标准19类颜色(含背景),但毕设常用2~4类,故提供精简版:

# 在SegmentationWindow类中添加方法 def start_inference(self): if not self.session or not hasattr(self, 'preprocessed_tensor'): QMessageBox.warning(self, "警告", "请先加载模型和图片!") return # 禁用按钮防重复提交 self.load_model_btn.setEnabled(False) self.load_img_btn.setEnabled(False) self.statusBar().showMessage("⏳ 正在推理中...") # 创建并启动线程 self.infer_thread = InferenceThread(self.session, self.preprocessed_tensor) self.infer_thread.finished.connect(self.on_inference_finished) self.infer_thread.error.connect(self.on_inference_error) self.infer_thread.start() def on_inference_finished(self, mask, inference_time): # 启用按钮 self.load_model_btn.setEnabled(True) self.load_img_btn.setEnabled(True) try: import cv2 import numpy as np # 定义颜色映射(示例:2类:背景=黑,目标=红) # 实际使用时按你的类别数扩展,如n_classes=4则定义4种颜色 colormap = np.array([ [0, 0, 0], # 背景(索引0) [255, 0, 0], # 类别1(索引1) # [0, 255, 0], # 类别2(索引2),依此类推 ], dtype=np.uint8) # 将mask转为彩色图(H,W,3) h, w = mask.shape color_mask = np.zeros((h, w, 3), dtype=np.uint8) for idx in range(len(colormap)): color_mask[mask == idx] = colormap[idx] # 转QImage(注意:cv2.cvtColor BGR→RGB) color_mask_bgr = cv2.cvtColor(color_mask, cv2.COLOR_RGB2BGR) qimg = QImage( color_mask_bgr.data, w, h, color_mask_bgr.strides[0], QImage.Format_BGR888 ) self.result_label.setPixmap(QPixmap.fromImage(qimg)) # 更新状态栏 self.statusBar().showMessage( f"✅ 推理完成 | 耗时:{inference_time:.1f}ms | 类别数:{len(colormap)}" ) except Exception as e: self.statusBar().showMessage(f"❌ 可视化失败:{str(e)}") QMessageBox.critical(self, "错误", f"结果渲染失败:{str(e)}") def on_inference_error(self, error_msg): self.load_model_btn.setEnabled(True) self.load_img_btn.setEnabled(True) self.statusBar().showMessage(f"❌ 推理失败:{error_msg}") QMessageBox.critical(self, "错误", f"推理异常:{error_msg}")

提示:colormap必须是np.uint8类型,否则QImage构造失败;cv2.cvtColor必须用COLOR_RGB2BGR,因为QImage.Format_BGR888要求BGR顺序;若mask中出现超出colormap索引的值(如预测出类别5但colormap只有4行),color_mask[mask == idx]会静默忽略,导致部分区域黑色——这是调试时的重要线索。

4.3 实现分割结果叠加显示:原图+半透明掩码,增强演示效果

纯色掩码虽清晰,但缺乏空间上下文。答辩时更推荐“原图+半透明掩码叠加”效果。只需将color_mask与原始图像(已缩放至同尺寸)按权重融合。注意:原始图是BGR,color_mask是RGB,需统一为BGR再融合:

# 在on_inference_finished中,替换color_mask生成逻辑 # ... 上面已得到color_mask (H,W,3) RGB格式 ... # 获取原始图(已缩放至640x480,但mask是512x512,需再次缩放) orig_h, orig_w = 480, 640 mask_resized = cv2.resize(color_mask, (orig_w, orig_h), interpolation=cv2.INTER_NEAREST) orig_bgr = cv2.resize(cv2.imread(self.image_path), (orig_w, orig_h), interpolation=cv2.INTER_AREA) # 转BGR并融合 mask_bgr = cv2.cvtColor(mask_resized, cv2.COLOR_RGB2BGR) alpha = 0.5 # 掩码透明度 overlay = cv2.addWeighted(orig_bgr, 1-alpha, mask_bgr, alpha, 0) # 转QImage显示 qimg = QImage( overlay.data, orig_w, orig_h, overlay.strides[0], QImage.Format_BGR888 ) self.result_label.setPixmap(QPixmap.fromImage(qimg))

这样生成的图既有原图纹理,又有醒目的分割边界,答辩时老师一眼看懂效果。cv2.INTER_NEAREST用于掩码缩放,避免插值模糊类别边界;addWeighted比cv2.bitwise_or更自然,支持任意alpha值调节。


5. 常见问题排查:从“界面假死”到“全黑输出”的5个真实踩坑记录

5.1 现象:点击“选择图片”后界面卡死10秒,然后报错“QPixmap: Cannot create a QPixmap from a null QImage”

原因:cv2.imread读取路径含中文字符(如C:\用户\毕设\test.jpg),Windows下返回None,后续QImage构造失败。PyQt5不报具体错误,只静默卡死。

解决:改用cv2.imdecode绕过文件系统编码问题:

# 替换原load_image中的cv2.imread with open(file_name, 'rb') as f: file_bytes = np.asarray(bytearray(f.read()), dtype=np.uint8) img_bgr = cv2.imdecode(file_bytes, cv2.IMREAD_COLOR)

5.2 现象:模型加载成功,但推理后result_label显示全黑,且mask数组全是0

原因:ONNX模型输出是logits(未softmax),而np.argmax在logits上仍可工作,但若模型最后一层是nn.Sigmoid(二分类),输出范围是[0,1],argmax恒为0(背景类)。本质是训练时损失函数与推理逻辑不匹配。

解决:检查模型最后一层。若为Sigmoid,推理时需mask = (pred[0,1] > 0.5).astype(np.uint8);若为Softmax,用argmax。导出前在PyTorch模型中显式添加nn.Softmax(dim=1)层,确保ONNX输出为概率分布。

5.3 现象:InferenceThread启动后立即触发error信号,报错“Could not find an implementation for the node Resize”

原因:ONNX模型使用了新版Resizeop(opset>=13),但安装的onnxruntime版本太低(如1.7.0只支持opset<=12)。

解决:升级ONNX Runtime:

pip uninstall onnxruntime pip install onnxruntime==1.16.3 # 当前兼容性最好的稳定版

或导出时指定更低opset:opset_version=12。

5.4 现象:同一张图多次推理,第一次耗时800ms,后续降到120ms,但关闭程序重开又变慢

原因:ONNX Runtime首次加载模型时会进行图优化(graph optimization)和内存分配,耗时较长。这是正常现象,但毕设演示需“秒开秒响应”。

解决:在load_model成功后,立即用dummy input执行一次“热身推理”:

# 在load_model方法末尾添加 dummy = np.random.randn(1,3,512,512).astype(np.float32) _ = self.session.run(None, {self.session.get_inputs()[0].name: dummy}) self.statusBar().showMessage("✅ 模型已热身,后续推理将加速")

5.5 现象:导出ONNX时torch.onnx.export报错“Unsupported value type: <class 'torch.nn.modules.container.ModuleList'>”

原因:模型中使用了nn.ModuleList存储多个Down模块,但torch.jit.trace无法追踪其动态索引(如self.downs[i](x))。

解决:改用普通Python list,并在forward中显式展开:

# 错误写法 self.downs = nn.ModuleList([Down(64,128), Down(128,256)]) # 正确写法(导出友好) self.down1 = Down(64,128) self.down2 = Down(128,256) # forward中写 self.x2 = self.down1(x1); self.x3 = self.down2(self.x2)

6. 毕设答辩加分技巧:一键导出标注图、类别统计报表、模型信息面板

6.1 添加“导出分割图”按钮:保存为PNG,支持透明通道和类别ID图

答辩时老师常问:“这个结果能导出吗?”、“能知道每类占多少像素吗?”。我们增加两个导出功能:① 彩色分割图(PNG,带alpha通道,方便PPT叠加);② 纯类别ID图(uint16 TIFF,供后续GIS分析)。关键点:QImage保存PNG时默认无alpha,需手动创建ARGB格式:

# 在SegmentationWindow中添加导出方法 def export_segmentation(self): if not hasattr(self, 'mask_array') or self.mask_array is None: QMessageBox.warning(self, "警告", "无可导出 <p> <a href="https://download.csdn.net/download/Runnymmede/89465470" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询