恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
PyQt5+深度学习骨龄识别系统:从模型训练到GUI部署实践
首页
资讯中心
/
PyQt5+深度学习骨龄识别系统:从模型训练到GUI部署实践
PyQt5+深度学习骨龄识别系统:从模型训练到GUI部署实践
发布时间:2026/10/5 9:35:51
简介一套基于PyQt5框架与深度学习技术打造的骨龄识别检测系统面向本科毕业设计、课程设计及期末大作业场景也适合想掌握完整桌面端AI项目的新手。项目自带图形界面、推理模块、预训练模型和说明文档源码含详细注释下载后稍作配置即可演示上手门槛较低。整个资源压缩包共两百个文件大小约七百余兆其中py文件为项目核心源码pth文件为已训练好的模型权重yaml与yml为训练推理配置ipynb与md用于代码演示和项目说明sh及Dockerfile等文件则辅助一键部署与容器化运行。该资源已被306人学习侧面说明其实战价值项目方注明为高分毕业设计并获导师认可。资源内附训练日志、Dockerfile等部署配置文件能从环境配置、模型加载到界面展示串起完整链路适合作为毕设答辩、功能演示和二次开发的直接参考。1. 骨龄识别检测项目pyqt5 深度学习为什么是毕设稳妥选型答辩现场最容易出现的尴尬不是模型精度不够而是导师随手拖了一张X光片进你的系统界面直接卡死转了十几秒后还弹出一个红色报错。你站在台上解释“数据预处理有点慢”但心里清楚是线程模型写崩了。这个“基于pyqt5深度学习的骨龄识别检测PYTHON实现代码GUI模型”的题目表面上是训练一个CNN预测骨龄实际上考验的是把深度学习模型封装成可用桌面软件的综合能力。它同时覆盖了模型训练、PyQt5界面、线程调度、文件IO、模型部署几个模块适合毕设拿高分也适合作为了解医学影像AI落地的入门项目。适合谁做有一定Python基础会用PyTorch或TensorFlow训练图像模型但对GUI开发完全陌生的学生。核心难点不在算法创新而在把散落的代码整合成一个能演示、能交互、能出报告的完整系统。2. 骨龄识别算法选型从CNN回归到多任务分类先搞清任务边界2.1 骨龄识别是回归不是分类关键点与评估指标如果你把骨龄检测做成“骨龄年龄分类模型”比如输出正常、提前、延迟三档答辩时很容易被追问“连续月龄怎么处理分档边界谁定义的”骨龄识别的标准做法是回归问题输入左手X光片输出一个浮点数单位通常是月。常用的评估指标是平均绝对误差MAE比如预测骨龄120个月实际骨龄118个月误差2个月。RSNA骨龄数据集就是按月标注的很多开源代码直接以“月龄”作为学习目标。为什么不能直接用分类一是因为骨龄本身是连续变量二是因为临床报告需要具体月龄而不是一个“接近正常”的模糊结论。回归头的设计也简单在CNN特征提取器后面接一个全连接层输出一个数值。也可以用多任务学习同时预测骨龄和性别性别对骨龄评估影响很大男孩和女孩的骨骺闭合速度不同。多任务输出时骨龄和性别共享特征提取层再分两个头分别回归和分类。性别分类头其实能在训练时帮骨干网络学到更有区分度的特征这也是提分的小技巧。我一般先跑一个最简单的单回归头基线再看有没有必要加性别分支。如果数据集本来就带性别字段多任务比把性别拼到全连接层更稳因为性别特征会在训练中与图像特征相互强化。2.2 用PyQt5做GUI前的模型推理接口设计PyQt5只是壳模型才是核心。如果你把数据预处理、模型推理、结果后处理全部塞进界面文件里后期换模型结构、调整输入尺寸会非常痛苦。更常见的做法是把模型封装成一个独立类叫BoneAgeInferencer它只负责加载权重、预处理图像、推断、后处理四个动作GUI那边只调用一个predict(image_path)方法拿到骨龄和置信度。# inference.py - 模型推理封装类 import torch from torchvision import transforms from PIL import Image class BoneAgeInferencer: def __init__(self, model_path, devicecuda): # 这里只实现加载逻辑具体网络定义后文给出 self.device device if torch.cuda.is_available() else cpu self.model self._build_model() self.model.load_state_dict(torch.load(model_path, map_locationself.device)) self.model.to(self.device) self.model.eval() # 训练时用的预处理 transform 必须保证和推理时一致 self.transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) def _build_model(self): # 后期替换成 ResNet、EfficientNet 都在这里改 raise NotImplementedError def predict(self, image_path): img Image.open(image_path).convert(RGB) tensor self.transform(img).unsqueeze(0).to(self.device) with torch.no_grad(): bone_age_month self.model(tensor).item() return {bone_age_month: round(bone_age_month, 2), bone_age_year: round(bone_age_month / 12.0, 2), device: self.device}这段代码的逻辑很简单构建模型加载权重将PIL图像转为张量归一化后前向传播。normalize参数用的是ImageNet的统计值因为预训练模型是在ImageNet上训练过的。如果你的训练数据不是ImageNet分布的X光片最好在训练阶段重新统计均值和方差否则预测结果会整体偏移。predict返回月龄和岁数两个字段GUI显示时直接用不需要界面再算一遍。2.3 最少必要代码加载模型对单张X光片推理为了让上面的封装类不报错还需要一个具体的网络实现。简单又稳的方案是用ResNet18做特征提取器把最后一层全连接替换成输出1个值的全连接层。手写残差网络没必要直接用torchvision预训练模型迁移学习能在小数据上拿到更好效果。下面是一个最小实现import torch.nn as nn from torchvision import models class BoneAgeRegressor(nn.Module): def __init__(self, pretrainedTrue): super().__init__() # 预训练 ResNet18去掉原始全连接层 resnet models.resnet18(pretrainedpretrained) self.features nn.Sequential(*list(resnet.children())[:-1]) # 将 512 维特征映射到骨龄月龄 self.regressor nn.Sequential( nn.Flatten(), nn.Linear(512, 128), nn.ReLU(), nn.Dropout(p0.2), nn.Linear(128, 1) ) def forward(self, x): features self.features(x) # 输出形状 [B, 512, 1, 1] return self.regressor(features).squeeze(-1) # [B]这里用nn.Sequential(*list(resnet.children())[:-1])把ResNet18最后一个全连接层丢掉只保留前面的卷积层。features输出的空间维度是1x1所以后面直接Flatten成512维向量。回归头用了两层全连接加DropoutDropout能减少过拟合尤其在只有几千张X光片的小数据集上很值得加。推理时调用model BoneAgeRegressor(pretrainedFalse) model.load_state_dict(torch.load(bone_age_model.pth, map_locationcpu)) model.eval()这里必须设置pretrainedFalse因为你是在加载自己的训练权重不是重新初始化预训练模型。很多初学者会在预测时忘记写model.eval()导致BatchNorm和Dropout行为变成训练模式预测结果抖得厉害。另一个坑是load_state_dict的路径如果你训练时用了DataParallel权重键名会多出module.前缀加载后要做一个键名清理。3. 数据准备与模型训练构建可复现的骨龄训练pipeline3.1 数据来源与预处理从RSNA骨龄数据集到本地目录骨龄识别领域最常用的公开数据集是RSNA 2017 Pediatric Bone Age Challenge包含大概一万多张左手X光片和对应的年龄月、性别标注。图像是DICOM或PNG格式文件有的大有几MB需要统一转成RGB图片并缩放到模型输入尺寸。做毕设时你不需要复现整个比赛用一部分数据训练也能跑通流程。常见做法是把csv标注文件读进来按比例划分训练集和验证集然后把图像路径和标签写入一个文本文件或直接生成内存数据集。下面的脚本演示如何划分# prepare_data.py import pandas as pd from sklearn.model_selection import train_test_split df pd.read_csv(boneage_dataset.csv) # 假设字段: image_path, bone_age_month, male train_df, val_df train_test_split( df, test_size0.2, random_state42, stratifypd.cut(df[bone_age_month], 10) ) # 写成训练与验证清单 train_df.to_csv(train.csv, indexFalse) val_df.to_csv(val.csv, indexFalse) print(f训练集 {len(train_df)} 张验证集 {len(val_df)} 张)stratify参数非常关键。骨龄标签分布通常是不均匀的青春期附近样本多新生儿少。如果不分层抽样验证集的骨龄分布可能和训练集差很多MAE看起来很高其实只是抽样偏差。test_size0.2是常见比例如果数据量很少改成0.15也行。3.2 训练脚本PyTorch实现骨龄回归网络训练脚本是整套代码的中枢。你需要一个自定义Dataset一个训练循环一个验证循环以及模型保存逻辑。这里给出最核心的Dataset和训练循环避免写成几百行没人看的脚本。# train.py import torch from torch.utils.data import Dataset, DataLoader from PIL import Image from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.3), transforms.RandomAffine(degrees5, translate(0.02, 0.02)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) class BoneAgeDataset(Dataset): def __init__(self, csv_path, transformNone): import pandas as pd self.df pd.read_csv(csv_path) self.transform transform def __len__(self): return len(self.df) def __getitem__(self, idx): row self.df.iloc[idx] img Image.open(row[image_path]).convert(RGB) label torch.tensor(float(row[bone_age_month]), dtypetorch.float32) if self.transform: img self.transform(img) return img, label数据增强里RandomAffine的旋转角度只给5度这是故意的。X光片本身有标准摆位旋转太大会破坏骨骼结构信息也容易引入错误边界。水平翻转可以加因为左右手在数据集里已经统一为左手翻转相当于模拟镜像不影响骨龄特征。随机亮度对比度增强其实可以加但要用小幅度避免骨骼软组织对比失真。训练循环用Smooth L1损失也就是Huber Loss。相比MSE它对离群点的梯度更温和骨龄预测中偶尔会有标注噪声Smooth L1不容易被个别脏数据带偏。# 训练循环核心 model BoneAgeRegressor(pretrainedTrue) optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-5) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30) criterion torch.nn.SmoothL1Loss() for epoch in range(30): model.train() epoch_loss 0.0 for images, labels in train_loader: images images.to(device) labels labels.to(device) preds model(images).squeeze(-1) loss criterion(preds, labels) optimizer.zero_grad() loss.backward() optimizer.step() epoch_loss loss.item() * images.size(0) scheduler.step() # 验证时用MAE model.eval() val_mae 0.0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) preds model(images).squeeze(-1) val_mae torch.abs(preds - labels).sum().item() val_mae / len(val_df) print(fEpoch {epoch1} | loss {epoch_loss/len(train_df):.3f} | val MAE {val_mae:.2f} 月)AdamW比Adam多了正确的权重衰减实现配合CosineAnnealingLR在训练后期平滑降低学习率。T_max设置为总epoch数让学习率在最后一个epoch降到最低。优化器学习率用1e-4迁移学习时对新加的回归头来说可以稍高但对微调预训练骨干层来说1e-4是一个安全起点。如果你的显卡显存小batch size可以降到8同时学习率也降到5e-5因为batch变小后梯度噪声变大。3.3 参数设置与训练过程监控训练参数直接决定你能不能顺利毕业。下面这张表我每次做图像回归项目都会先按它初始化再根据显卡和数据集微调。不要小看这些数值很多翻车都源于参数拍脑袋。参数推荐值说明输入尺寸224x224与ImageNet预训练对齐batch size166GB显存可用优化器AdamW比SGD收敛快比Adam训练更稳初始学习率1e-4如果loss不降先看看是不是lr太大损失函数SmoothL1Loss对标注噪声宽容数据增强水平翻转、5度旋转、轻微平移X光片不能重缩放训练轮数30-50小数据提前做早停早停patience5验证集MAE连续5轮不降则停止训练时我习惯把每轮的loss、MAE、学习率写入csv日志。没有日志你根本看不出第15轮开始过拟合。另外模型保存要同时存权重和优化器状态最好是整个checkpoint字典。只存权重的话中断训练后没法恢复优化器状态只能重新训练。4. PyQt5 GUI搭建从界面布局到模型推理线程4.1 主界面设计图片选择、结果显示、报告导出PyQt5的界面设计有两种路线纯代码写界面或用Qt Designer画好.ui文件再转成.py。毕设答辩时一个拖拽文件就出结果、带按钮和状态栏的界面比命令行输出有说服力得多。我推荐用Qt Designer画然后通过pyuic5工具把.ui转成Python类这样界面调整布局不会影响业务逻辑。主界面至少要有五个区域左侧是图片预览区用QLabel显示X光片右侧上方显示骨龄结果包括月龄、岁龄、性别右侧下方是“开始识别”“选择图片”“保存报告”三个按钮底部是一个QStatusBar用于提示状态顶部菜单栏可以放“关于”和“打开文件夹”。整体布局用QHBoxLayout和QVBoxLayout嵌套窗口默认大小900x600。用Qt Designer生成代码后你需要在主窗口初始化时把推理类挂上来而不是直接在主窗口类里写模型加载逻辑。4.2 关键代码QThread后台推理避免界面卡死这是整个GUI项目最容易翻车的地方。如果你在按钮点击槽函数里直接调用模型推理PyQt5的界面线程会被阻塞按钮无法点击窗口标题栏会出现“未响应”。推理一张224x224的X光片在CPU上可能耗时0.2秒但在GPU不存在的电脑上加上加载大图、预处理有可能卡好几秒。更严重的是如果你加载一个上百MB的模型界面会直接黑掉几秒钟。正确方案是把推理放进QThread工作线程通过信号把结果传回主线程。下面是一个线程封装# worker.py from PyQt5.QtCore import QThread, pyqtSignal class InferenceWorker(QThread): result_ready pyqtSignal(dict) error_occurred pyqtSignal(str) def __init__(self, inferencer, image_path): super().__init__() self.inferencer inferencer self.image_path image_path def run(self): try: result self.inferencer.predict(self.image_path) self.result_ready.emit(result) except Exception as e: self.error_occurred.emit(str(e))在主窗口里每次点击“开始识别”都新建一个worker线程连接信号后启动。注意不能把worker存成局部变量Python的垃圾回收可能把线程对象回收掉导致程序崩掉。常见做法是保存到self.worker确保线程生命周期和窗口同步。# 主窗口槽函数 def on_start_clicked(self): if not self.image_path: self.statusBar().showMessage(请先选择图片) return self.statusBar().showMessage(正在识别...) self.worker InferenceWorker(self.inferencer, self.image_path) self.worker.result_ready.connect(self.show_result) self.worker.error_occurred.connect(self.handle_error) self.worker.start()show_result里更新QLabel文本和图片预览handle_error里弹QMessageBox并恢复状态栏。这里有个细节在worker运行期间应该禁用“开始识别”按钮等结果回来再启用避免用户连续点击创建多个线程。否则多个线程同时推理会有两个结果竞争写同一个标签虽然不会崩溃但显示结果可能被覆盖。4.3 模型与GUI解耦C/S设计思路很多毕设代码把模型类、数据类、界面类全写在一个文件里第一眼看上去很猛代码一千行但答辩老师问到“如果换个模型你要改哪些地方”就会支支吾吾。更优雅的做法是把推理封装成独立的“服务”GUI只是客户端。你可以再进一步把推理服务写成REST APIGUI用requests调用本地端口但这会引入启动服务额外成本对U盘答辩环境不友好。折中方案是模块分离至少保证模型相关代码不引用任何PyQt5的东西。按照这个思路项目目录结构可以这样boneage_project/ ├── main.py # 程序入口 ├── gui/ │ ├── __init__.py │ ├── main_window.py # PyQt5主窗口 │ └── worker.py # QThread工作线程 ├── inference/ │ ├── __init__.py │ ├── regressor.py # 模型定义 │ └── inferencer.py # 推理封装 ├── train/ │ ├── train.py │ └── prepare_data.py └── models/ └── best_model.pthGUI目录只负责界面和线程inference目录负责模型加载和推理。这样即使你日后把PyTorch换ONNX Runtime只需要重写inferencer.pyGUI一行不用动。这个解耦设计在答辩时可以直接说“我参考了软件工程的分层思想”很加分。5. 骨龄识别检测里的七个避坑点从环境依赖到精度玄学5.1 PyQt5装不上或运行报错版本与Python版本匹配现象在Python 3.9环境里pip install pyqt5成功但运行程序报ModuleNotFoundError: No module named PyQt5.sip或者QT相关dll找不到。原因新版本PyQt5与sip版本冲突或者你用的是Python 3.12PyQt5的某些二进制包还没完全适配。尤其是Windows下Python 3.12对PyQt5的兼容性没有Python 3.10稳定。解决创建虚拟环境时固定Python 3.10然后执行pip install pyqt55.15.10这是大众验证最多的版本。遇到sip缺失再补一条pip install PyQt5-sip。如果你的系统是macOS还需要安装XQuartz否则窗口显示异常。5.2 模型输入尺寸不匹配导致预测结果异常现象训练时用224x224输入GUI里把原图直接送入模型报错Expected 4D input to conv2d或者预测结果全是同一个值。原因原图尺寸接近数千像素没有resize到224。Conv层对任意维度都能算NCHW的H、W变化后全连接层输入维度就错了。如果全连接层适应了结果当然全乱。解决在预处理transform里第一项强制Resize((224,224))检查inferencer.predict是否对路径和PIL图片都走了同样的transform。还有一个隐藏坑有些网络对输入尺寸敏感Resize比例不均会导致骨骼形变所以要么改成等比例缩放加padding要么统一固定宽高比。对骨龄识别来说直接resize到正方形效果尚可因为关键骨骼位置不会因为扭曲而完全消失。5.3 GUI卡死把推理放到工作线程现象点击识别后窗口变模糊拖动窗口没反应几秒后恢复然后一次性弹出结果。原因推理逻辑直接写在槽函数里阻塞了Qt事件循环。这几乎是新手最常见问题。解决参考4.2的InferenceWorker把推理挪到子线程。注意不要在worker里访问任何界面控件所有数据通过信号传递。另外模型实例在worker里每次推理都用同一个不要每次新建否则显存和内存会一点点涨上去。5.4 骨龄预测值偏移回归头与损失函数的选择现象训练时MAE只有25个月但对一个人的片子预测结果比临床估计低了一岁多且所有预测都偏向某个范围。原因很可能是回归头直接用MSE损失而目标月龄的方差很大模型学会了预测均值。比如训练集月龄中位数是120个月模型对所有输入都输出120附近MAE反而中等偏低。这是回归任务的典型陷阱。解决改用SmoothL1Loss同时检查预测分布是否比真实分布窄很多。另一种有效做法是把目标标准化先计算训练集月龄的均值和标准差把label转为z-score训练网络输出z-score预测后再反标准化。这样梯度尺度更平稳抵消输出层初始化偏差。5.5 训练和推理预处理不一致归一化参数要固化现象训练时在Dataset里用了transforms.Normalize(mean, std)推理时只做了ToTensor()结果预测的骨龄忽高忽低甚至出现负数。原因训练时归一化到标准正态推理时却用原始0-1范围模型输入分布完全不同。这种错误不会直接报错但结果完全不可信。解决把transform定义为全局常量放在一个单独preprocess.py里训练和推理都从这个模块导入。不要在两处各写一份。归一化mean和std建议用训练集X光片的实际统计值替换ImageNet默认值因为骨龄X光片以灰白为主RGB通道均值与自然图像差异很大。你可以用torchvision.transforms.ToTensor()后对所有训练图片的每个通道算均值和标准差这个小脚本值得写效果提升是实打实的。5.6 模型加载慢且内存爆掉设备选择与模型轻量化现象程序启动后加载一个2GB的模型文件内存占用暴涨旧电脑直接卡死。原因毕设一般没有GPU服务器也用不起大模型。有些同学直接拿EfficientNetV2-L、ResNet152训练推理时CPU承受不住。解决先确认推理设备。最省心的组合是ResNet18或EfficientNetV2-S单张图片CPU推理不到1秒。模型文件用torch.save(model.state_dict(), best_model.pth)保存不要保存整个model对象文件体积会包含模型结构值和优化状态。加载时用map_locationcpu。如果还嫌慢可以转成ONNX并使用ONNX Runtime推理能提速不少。答辩时能拿出“我把模型从2.1GB压缩到180MB”这种数据本身就很有亮点。5.7 ONNX导出失败动态轴与opset版本现象模型在PyTorch里跑得好好的用torch.onnx.export导出后在ONNX Runtime里报错Resize operation failed。原因导出时没有指定动态轴模型被固定成单一batch size或者opset版本和推理框架不匹配。解决导出时指定opset_version13并设置dynamic_axes{input: {0: batch_size}, output: {0: batch_size}}。同时检查onnxruntime版本旧版可能不支持新opset。如果导出成功但预测结果偏差很大多半是torch和onnx的算子实现精度差异可以改用torch.onnx.export的dynamic_axes并导出后先用onnxruntime跑同一张图对比输出。如果你不打算用ONNXPyTorch原生加载也能用但ONNX这个技能写在简历上是加分项。6. 把项目升级成“高分毕设”实验对比、可视化与交付清单到了这一步你的系统已经能跑了但如果想从“能用”变成“高分”还需要做三件事实验对比、结果可视化和完整交付文档。实验对比不是简单贴一个表说MAE是多少。你要做的是消融实验基线ResNet18不加数据增强 vs 加了增强 vs 多任务性别预测 vs 换用EfficientNet。每组实验用相同的数据划分和随机种子记录训练loss下降曲线和验证MAE。画成折线图放进论文里比任何文字都直观。答辩时可以放一张图片显示同一个X光片的预测骨龄与真实骨龄的差距再用热力图或Grad-CAM高亮模型关注区域。骨龄识别模型通常关注骨骺区域热力图出现在手腕远端骨骼附近说明模型学到了合理特征这会给评委留下深刻印象。我还会在GUI里加一个“保存报告”功能生成一份包含患者ID、图片路径、预测骨龄月龄、岁龄、预测时间、模型版本的报告格式用HTML或CSV。HTML可以用浏览器直接打开后续也能改写成PDF。这一项被很多同学忽略但它让系统看起来像一个完整产品。最后说一个我自己的教训把所有代码和数据集文件打成一个压缩包时别忘记把requirements.txt和README写清楚。README至少包括环境版本、数据集目录结构、训练命令、推理命令、模型路径。很多同学debug了两周最后答辩前夜发现自己当时写了一个只在绝对路径下能跑的脚本评委换台机器就崩了。相对路径和os.path.join处理要提前检查。这个方向做完你不仅能完成一个高分毕设还能真正理解深度学习从训练到落地的全过程。如果你时间紧张优先保证训练脚本、推理封装和GUI线程这三块是对的其他功能都属于锦上添花。希望帮到你。本文还有配套的精品资源点击获取