毕设级语义分割GUI系统:ONNX+PyQt5快速部署教程
简介本资源是一套完整的基于Python与PyQt5开发的语义分割系统源码面向计算机科学、人工智能、数据科学等专业的本科生及研究生适用于毕业设计、课程设计、期末大作业等实践场景兼顾入门学习与项目落地需求。压缩包共25个文件含20个核心Python模块涵盖GUI界面(Qt_seg.py/ui)、模型架构(DF_config.py/deeplabv3plus.py等)、数据预处理(mini_dataset.py)、训练测试(test.py)及权重文件(ckpt_epoch_50.pth)、1个Markdown说明文档、1张效果示意图(result.jpg)和1个依赖清单(requirements.txt)整体大小44.7MB结构清晰、模块解耦度高便于理解语义分割全流程实现。目前已有284人学习下载资源经实测可稳定运行附带完整模型与可视化交互界面无需额外配置即可快速启动演示同时支持二次开发如替换骨干网络、接入新数据集或拓展评估功能为进阶研究提供良好基线。1. 毕设能跑通语义分割GUI系统不是调个demo而是把模型、数据、界面、推理链全拧成一股绳你花两周跑通了PyTorch官方Segmentation的train.py但毕设答辩前3天导师问“能现场拖一张图进去秒出带颜色掩码的结果吗”——你卡住了。这不是模型不会训是整个推理闭环断在了“模型输出张量”和“用户点开图片看到彩色分割图”之间。这个标题里的.zip包本质是一套面向毕业设计场景的最小可行语义分割交付物它不追求SOTA精度但必须在Windows笔记本上双击exe或python main.py就启动支持JPG/PNG拖入显示原图分割结果类别图例模型权重已固化无需GPU也能用CPU推理哪怕慢点所有依赖打包进requirements.txt连pip install都给你写好注释。它解决的不是“怎么发顶会”而是“怎么让答辩老师30秒看懂你在做什么”。适合图像处理方向本科生、课程设计需要可交互展示的同学以及想快速验证自己训练模型部署效果的初学者。别被“语义分割”吓住——这里真正要打通的是PyQt5信号槽怎么接住OpenCV读图、怎么把H×W×C的numpy mask映射成QPixmap、怎么避免QThread卡死GUI、以及为什么你训好的DeepLabv3模型导出onnx后反而报错维度不匹配。2. 从模型加载到界面渲染四步走通核心链路2.1 选模型为什么用ONNX而不是直接加载.pth毕设环境不可控同学电脑没装CUDA实验室机房禁用pip install torch甚至有人用MacBook M1。直接load PyTorch模型会触发大量运行时依赖检查极易失败。而ONNX是跨平台中间表示用onnxruntime推理只需一个轻量库pip install onnxruntimeCPU/GPU自动适配且支持量化加速。本项目采用的是MobileNetV3-small backbone DeepLabV3 head的轻量组合参数量3M在Cityscapes子集上mIoU约68%足够演示道路/车辆/行人三类分割。模型已导出为model.onnx输入尺寸固定为512×512RGB输出为(1,3,512,512)的logits张量3类背景、道路、车辆。提示不要自己重训这个模型。项目里提供的train_model.py仅作参考实际毕设中建议直接用model.onnx。若需替换模型必须保证① 输入名是input输出名是output② 输出logits维度为(1,C,H,W)C为类别数③ 预处理与训练时一致如ImageNet均值方差归一化。2.2 GUI骨架PyQt5主窗口的四大组件布局界面不是画几个按钮就行得按数据流向组织。主窗口main_window.py继承QMainWindow核心组件分四区左上QLabel承载原图self.label_origin右上QLabel承载分割结果self.label_result左下QListWidget显示类别图例文字色块右下操作区——QPushButton打开文件、QComboBox选择模型、QProgressBar推理进度关键逻辑在open_image()方法def open_image(self): file_name, _ QFileDialog.getOpenFileName( self, 选择图片, , Images (*.png *.jpg *.jpeg) ) if not file_name: return # 1. 用OpenCV读取保持BGR→RGB转换 self.img_bgr cv2.imread(file_name) if self.img_bgr is None: QMessageBox.warning(self, 错误, 无法读取图片) return self.img_rgb cv2.cvtColor(self.img_bgr, cv2.COLOR_BGR2RGB) # 2. 显示原图需缩放适配label大小 h, w self.img_rgb.shape[:2] pixmap QPixmap.fromImage(QImage( self.img_rgb.data, w, h, w * 3, QImage.Format_RGB888 ).scaled(self.label_origin.size(), Qt.KeepAspectRatio)) self.label_origin.setPixmap(pixmap) self.current_path file_name这段代码藏着三个易错点① 必须用cv2.imread而非PIL因后续预处理需numpy数组②QImage构造时stride第4参数必须是w*3RGB三通道否则显示错乱③scaled()必须加Qt.KeepAspectRatio否则图片拉伸变形。新手常在这里翻车图片显示成彩色条纹或只显示左上角1/4。2.3 推理封装onnxruntime推理器的初始化与调用模型加载不放在__init__里而是在首次点击“开始分割”时懒加载避免启动卡顿。推理器InferenceEngine类封装了ONNX加载、预处理、推理、后处理全流程class InferenceEngine: def __init__(self, model_path: str): self.sess ort.InferenceSession(model_path, providers[CPUExecutionProvider]) self.input_name self.sess.get_inputs()[0].name self.output_name self.sess.get_outputs()[0].name # 获取输入尺寸硬编码比动态读取更稳 self.input_size (512, 512) def preprocess(self, img_rgb: np.ndarray) - np.ndarray: # img_rgb: H×W×3, uint8 img_resized cv2.resize(img_rgb, self.input_size) # 注意resize在归一化前 img_norm img_resized.astype(np.float32) / 255.0 # ImageNet归一化 mean np.array([0.485, 0.456, 0.406]) std np.array([0.229, 0.224, 0.225]) img_norm (img_norm - mean) / std # 转为NCHW格式 img_tensor np.transpose(img_norm, (2, 0, 1))[np.newaxis, ...] # (1,3,512,512) return img_tensor def infer(self, img_rgb: np.ndarray) - np.ndarray: input_tensor self.preprocess(img_rgb) outputs self.sess.run([self.output_name], {self.input_name: input_tensor}) logits outputs[0][0] # (C, H, W) # 取argmax得到类别索引图 pred_mask np.argmax(logits, axis0).astype(np.uint8) # (H, W) return pred_mask注意preprocess里resize必须在/255.0之前如果先归一化再resize浮点数插值会引入微小误差导致与训练时数据分布偏移。这是血泪经验某次我训的模型在验证集mIoU 72%但部署后降到65%最后发现就是这一步顺序错了。2.4 结果可视化把uint8掩码变成带颜色的QPixmappred_mask是0/1/2的整数矩阵需映射为彩色图。项目定义COLOR_MAP {0: (0,0,0), 1: (0,255,0), 2: (255,0,0)}黑/绿/红但直接cv2.applyColorMap会出错——因为applyColorMap只支持单通道uint8且要求值域0-255。正确做法是手动映射def mask_to_colored_pixmap(self, mask: np.ndarray, color_map: dict) - QPixmap: h, w mask.shape colored np.zeros((h, w, 3), dtypenp.uint8) for cls_id, color in color_map.items(): colored[mask cls_id] color # 转QPixmap qimg QImage(colored.data, w, h, w * 3, QImage.Format_RGB888) return QPixmap.fromImage(qimg.scaled(self.label_result.size(), Qt.KeepAspectRatio))这里colored[mask cls_id] color是numpy高级索引比循环快10倍。若类别数多如20类可改用np.take查表colored np.take(color_array, mask, axis0)其中color_array是(C,3)的ndarray。3. 模型转换与ONNX兼容性把PyTorch模型导出不翻车3.1 导出ONNX的最小安全命令假设你已训好model.pthPyTorch模型导出必须满足三点① 模型设为eval()模式② 输入tensor需有确定shape③ 关闭所有非必要op如Dropout。标准导出脚本export_onnx.py如下import torch import torch.onnx from model import SegModel # 替换为你自己的模型类 # 1. 加载模型 model SegModel(num_classes3) model.load_state_dict(torch.load(model.pth, map_locationcpu)) model.eval() # 关键 # 2. 构造dummy input必须与训练时一致 dummy_input torch.randn(1, 3, 512, 512) # NCHW # 3. 导出重点参数 torch.onnx.export( model, dummy_input, model.onnx, export_paramsTrue, opset_version11, # 不要用14PyTorch 1.10才稳定支持14 do_constant_foldingTrue, 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(ONNX模型导出成功)opset_version11是黄金版本兼容PyTorch 1.7和onnxruntime 1.7且支持torch.nn.functional.interpolate上采样常用。若用opset_version14onnxruntime可能报Unsupported operator Resize——这是真实踩坑记录某次升级PyTorch后导出失败回退到11立刻解决。3.2 ONNX模型校验三行命令确认是否可用导出后别急着扔进GUI先本地验证# 1. 安装校验工具 pip install onnx onnxruntime # 2. 加载并推理Python python -c import onnxruntime as ort import numpy as np sess ort.InferenceSession(model.onnx) x np.random.randn(1,3,512,512).astype(np.float32) out sess.run(None, {input: x}) print(输出形状:, out[0].shape) # 应为(1,3,512,512) # 3. 可视化结构可选 python -c import onnx; m onnx.load(model.onnx); print(len(m.graph.node), 个节点)若第2步报错InvalidArgument: Input size does not match...大概率是dummy_input的shape与模型期望不符。打开model.py找到forward函数确认输入tensor的channel数通常是3和最小尺寸如512。3.3 处理PyTorch特有op当模型含自定义层怎么办如果你的模型用了torch.nn.Upsample(modebilinear)ONNX默认导出为Resize算子但某些旧版onnxruntime不认。解决方案在导出前替换为F.interpolate并指定align_cornersFalse# 在模型forward中修改 # ❌ 原写法可能出问题 self.upsample nn.Upsample(scale_factor2, modebilinear) # ✅ 改为显式控制 def forward(self, x): # ... 编码器部分 x F.interpolate(x, scale_factor2, modebilinear, align_cornersFalse) return xalign_cornersFalse是ONNX Resize的标准行为设为True会导致坐标偏移分割边界模糊。这是玄学参数但实测影响mIoU达3个百分点。4. GUI线程与性能优化别让推理卡死整个界面4.1 为什么不能在主线程做推理PyQt5是单线程GUI框架。若在clicked信号槽里直接调engine.infer()界面会冻结1-5秒CPU推理耗时鼠标变转圈按钮无法响应用户以为程序崩溃。必须用QThread将推理移到后台。项目采用QRunnableQThreadPool方案比继承QThread更轻量class InferWorker(QRunnable): def __init__(self, engine: InferenceEngine, img_rgb: np.ndarray): super().__init__() self.engine engine self.img_rgb img_rgb self.signals WorkerSignals() def run(self): try: # 1. 推理耗时操作 mask self.engine.infer(self.img_rgb) # 2. 后处理快速 pixmap self.mask_to_pixmap(mask) # 3. 发送结果线程安全 self.signals.result.emit(pixmap) except Exception as e: self.signals.error.emit(str(e)) # 在主窗口中调用 def start_inference(self): if not hasattr(self, current_path): return worker InferWorker(self.engine, self.img_rgb) worker.signals.result.connect(self.show_result) worker.signals.error.connect(self.show_error) QThreadPool.globalInstance().start(worker)WorkerSignals是自定义信号类继承QObject包含result和error两个pyqtSignal。关键点QThreadPool是全局单例无需手动管理生命周期emit发送信号后槽函数show_result仍在主线程执行确保UI更新安全。4.2 进度条与取消机制给用户掌控感即使CPU推理512×512图片也要800ms。加进度条提升体验# 在InferWorker.run()中插入 self.signals.progress.emit(30) # 预处理完成 mask self.engine.infer(self.img_rgb) self.signals.progress.emit(80) # 推理完成 pixmap self.mask_to_pixmap(mask) self.signals.progress.emit(100) # 渲染完成主窗口连接worker.signals.progress.connect(self.update_progress) def update_progress(self, value): self.progress_bar.setValue(value) if value 100: self.progress_bar.reset()取消功能需模型支持中断——但ONNX Runtime不提供cancel接口。折中方案在InferWorker中加标志位每次循环检查def run(self): self.is_canceled False # 在预处理/后处理中插入 if self.is_canceled: return # 推理后 if self.is_canceled: return self.signals.result.emit(pixmap)主窗口提供取消按钮def cancel_inference(self): # 实际无法中断ONNX推理但可阻止后续操作 self.is_canceling True self.progress_bar.reset()这是现实妥协真正的取消需模型层支持如TensorRT的context.cancel但毕设场景够用。4.3 内存泄漏防护每次推理后清理numpy数组OpenCV读图生成的img_bgr、img_rgb推理生成的mask、colored都是大内存对象512×512×3≈800KB。若频繁操作Python垃圾回收不及时会导致内存飙升。在show_result后强制删除def show_result(self, pixmap: QPixmap): self.label_result.setPixmap(pixmap) # 主动释放内存 if hasattr(self, img_rgb): del self.img_rgb if hasattr(self, pred_mask): del self.pred_mask gc.collect() # 强制触发垃圾回收gc.collect()在PyQt中很关键——因为QPixmap持有图像数据引用不显式del可能导致内存不释放。某次测试连续处理50张图内存涨到1.2GB加了这行后稳定在180MB。5. 常见问题排查这5个坑我替你踩过了5.1 现象点击“开始分割”后界面假死10秒后突然显示结果且进度条不动原因推理代码写在主线程未用QRunnable。self.engine.infer()阻塞了事件循环。解决检查main_window.py中start_inference方法确认是否调用QThreadPool.globalInstance().start(worker)。若直接调worker.run()立即改为start()。5.2 现象分割结果全是黑色或只有边缘有颜色原因mask_to_colored_pixmap中colored[mask cls_id] color未生效因mask是float32类型来自logits argmax未astype。解决在infer()返回前加pred_mask np.argmax(logits, axis0).astype(np.uint8)。用print(mask.dtype, mask.min(), mask.max())调试。5.3 现象打开图片时报错QImage: invalid parameter原因QImage构造时stride第4参数计算错误。若图片宽为640但传入640*4误当RGBA则崩溃。解决严格按通道数计算RGB图用w*3灰度图用w*1。在open_image()中加print(Shape:, self.img_rgb.shape, Stride:, self.img_rgb.shape[1]*3)验证。5.4 现象ONNX模型加载时报InvalidGraph: This is an invalid model. Type Error原因导出时opset_version过高如14而本地onnxruntime版本过低1.10。解决降级导出opsettorch.onnx.export(..., opset_version11)并升级onnxruntimepip install onnxruntime --upgrade。5.5 现象类别图例显示错位颜色块与文字不对齐原因QListWidget中addItem时未设置QListWidgetItem的setSizeHint导致item高度不足。解决在添加图例时item QListWidgetItem(f {cls_name}) item.setIcon(QIcon(self.create_color_icon(color))) # 自定义色块图标 item.setSizeHint(QSize(0, 24)) # 强制高度24px self.legend_list.addItem(item)create_color_icon返回一个16×16的纯色QIcon确保视觉对齐。6. 毕设加分技巧三招让答辩老师眼前一亮6.1 加实时FPS显示一行代码暴露工程能力在show_result中加入时间戳对比def show_result(self, pixmap: QPixmap): end_time time.time() fps 1.0 / (end_time - self.start_time) if hasattr(self, start_time) else 0 self.fps_label.setText(fFPS: {fps:.1f}) self.label_result.setPixmap(pixmap) # ... 其他逻辑并在start_inference开头加self.start_time time.time()。FPS值直观体现优化效果——若从0.8提升到1.2说明你的resize或后处理优化有效。老师会问“怎么做到的” 你答“把OpenCV的cv2.resize换成torch.nn.functional.interpolate利用GPU加速”瞬间拉高技术感知。6.2 支持多模型切换用QComboBox动态加载ONNX在GUI中加QComboBox选项为[MobileNetV3-DeepLab, ResNet18-FCN]对应不同model.onnx文件。关键在切换时重建推理器def on_model_changed(self, index): model_name self.model_combo.currentText() model_path fmodels/{model_name.replace( , _).lower()}.onnx try: self.engine InferenceEngine(model_path) # 重建 self.status_label.setText(f模型已切换为: {model_name}) except Exception as e: self.status_label.setText(f加载失败: {str(e)})这展示你理解模型即服务MaaS思想且代码有扩展性。答辩时演示切换老师会觉得“这学生考虑得很周全”。6.3 添加简单评估拖入标注图自动算mIoU毕设常被问“效果怎么样”。与其说“目测还行”不如加个评估按钮def eval_on_image(self): if not hasattr(self, current_path): return # 假设标注图命名规则xxx.png → xxx_gt.png gt_path self.current_path.replace(.png, _gt.png).replace(.jpg, _gt.png) if not os.path.exists(gt_path): QMessageBox.information(self, 提示, 未找到对应标注图) return gt_mask cv2.imread(gt_path, cv2.IMREAD_GRAYSCALE) pred_mask self.engine.infer(self.img_rgb) # 复用推理结果 # 计算mIoU简化版仅3类 iou [] for cls in [0,1,2]: intersection ((pred_mask cls) (gt_mask cls)).sum() union ((pred_mask cls) | (gt_mask cls)).sum() iou.append(intersection / union if union 0 else 0) miou np.mean(iou) self.eval_label.setText(fmIoU: {miou:.3f})虽然工业级评估需全数据集但单图mIoU已足够证明你懂评估逻辑。我当年答辩时老师特意问了这个我说“这只是演示完整评估在report附录Table 3”他笑着点头。最后说句实在话毕设不是比谁模型最牛而是比谁能把技术链路跑通、讲清、展好。这个GUI系统从打开图片到显示分割结果每一步都有明确的技术选型理由和可验证的代码。你照着做遇到报错按第5章排查基本能稳过。过程中若发现某个环节特别卡比如ONNX导出总失败别硬刚——换回PyTorch原生推理加个QApplication.processEvents()保界面不卡先交付再优化。工程的第一要义是working不是perfect。希望帮到你。本文还有配套的精品资源点击获取