简介:本资源是一套基于PyQt5与深度学习实现的骨龄识别检测Python项目源码,面向具备一定Python基础、希望学习医学影像检测与GUI开发的学生、开发者及算法爱好者。项目整合了YOLOv5目标检测流程与图形化界面,可用于骨龄评估场景的实践与二次开发。压缩包共200个文件,包含69个py源码、53个yaml配置、18个pth模型权重、12个yml及8个md说明文档等,涵盖训练配置、模型文件与使用说明,整体约717MB。资源内代码均经过测试运行成功,并附有项目使用说明,读者可据此完成数据预处理、类别配置与路径修改等关键环节,理解从数据集准备到模型推理的完整链路。目前已有726人学习下载,适合作为深度学习检测类项目的参考范例。
1. 骨龄识别项目为什么值得用 PyQt5 做桌面端落地
很多做医学图像方向的朋友,第一次接触骨龄识别都是在论文里看到一堆 CNN 结构对比,但真到自己要交付一个能用的东西时,卡点往往不在模型,而在"怎么让医生或体育老师点两下就能出结果"。基于 PyQt5 深度学习实现的骨龄识别检测 Python 源码,本质上是把一套训练好的骨龄回归模型,包进一个本地桌面 GUI 里,输入一张手部 X 光片,输出预测骨龄值和对应的发育等级。它解决的是"模型跑在 notebook 里、业务方用不了"这个断层问题,适合三类人:想拿深度学习实战项目练手的 Python 学习者、需要给科室做小工具的技术人员、以及要把骨龄评估流程做成离线可复现方案的开发者。选 PyQt5 而不是 Web 端,核心原因是 X 光片涉及隐私,本地推理不联网更省心,而且打包成 exe 后部署成本极低。
2. 骨龄识别模型选型与 PyQt5 界面骨架怎么搭
2.1 为什么骨干网络优先选 CNN 而不是传统机器学习
骨龄评估的输入是手部 X 光片,关键判读区域集中在腕骨、掌骨骨骺和桡尺骨远端,这些结构的成熟度差异是渐变的、空间相关的。传统机器学习模型要靠人工先提取 ROI、再算形状和纹理特征,特征工程一旦没覆盖到某个发育阶段,泛化就崩。深度学习 CNN 的优势在于卷积核能自动学到骨骺线的闭合程度这类局部模式,再通过堆叠层把局部特征组合成整体成熟度表征。常见做法是拿 ResNet50 或 EfficientNet 系列做骨干,把最后的分类头换成单神经元回归输出,损失用 MSE 或 Huber。选 Huber 的原因是骨龄标签本身有标注者间误差,纯 MSE 对离群样本太敏感,容易让模型去拟合噪声。
需要提醒的是,骨龄识别不是纯分类任务。有人图省事把它做成 0-18 岁每岁一个类别的分类,结果相邻年龄的预测在边界上跳变,比如真实 11.5 岁一会儿判 11 一会儿判 12。回归输出配合四舍五入取整,再映射到发育等级,才是更稳的路子。
2.2 PyQt5 界面设计:三个区域的最小可用布局
PyQt5 界面设计不需要一上来就堆控件,先把信息流理清:用户要选图、要看图、要触发推理、要看结果。对应到布局就是左侧图像预览区、右侧参数与结果区、底部操作按钮区。下面是一个可直接跑的最小骨架,用 QLabel 显示图像,用 QPushButton 触发推理,用 QTextEdit 输出结果。
import sys from PyQt5.QtWidgets import (QApplication, QWidget, QLabel, QPushButton, QVBoxLayout, QHBoxLayout, QTextEdit, QFileDialog) from PyQt5.QtGui import QPixmap from PyQt5.QtCore import Qt class BoneAgeWindow(QWidget): def __init__(self): super().__init__() self.setWindowTitle("骨龄识别检测") self.resize(900, 520) self.img_label = QLabel("尚未选择影像") self.img_label.setAlignment(Qt.AlignCenter) self.img_label.setFixedSize(480, 480) self.img_label.setStyleSheet("border:1px solid #999;") self.result_box = QTextEdit() self.result_box.setReadOnly(True) self.btn_open = QPushButton("选择X光片") self.btn_run = QPushButton("开始识别") self.btn_open.clicked.connect(self.load_image) self.btn_run.clicked.connect(self.run_infer) left = QVBoxLayout() left.addWidget(self.img_label) right = QVBoxLayout() right.addWidget(self.result_box) right.addWidget(self.btn_open) right.addWidget(self.btn_run) root = QHBoxLayout() root.addLayout(left) root.addLayout(right) self.setLayout(root) self.img_path = None def load_image(self): path, _ = QFileDialog.getOpenFileName(self, "选择影像", "", "Images (*.png *.jpg *.jpeg *.bmp)") if path: self.img_path = path pix = QPixmap(path).scaled(480, 480, Qt.KeepAspectRatio) self.img_label.setPixmap(pix) def run_infer(self): if not self.img_path: self.result_box.setText("请先选择一张X光片") return # 这里接模型推理,见 2.3 self.result_box.setText("推理占位:模型加载后替换此处") if __name__ == "__main__": app = QApplication(sys.argv) win = BoneAgeWindow() win.show() sys.exit(app.exec_())逻辑说明:img_label固定尺寸是为了让不同分辨率的 X 光片缩放后不撑破窗口,Qt.KeepAspectRatio保证不变形,这点在医学影像上很重要,拉伸会改变骨骺比例。result_box设为只读,避免用户误改输出。参数上,resize(900, 520)是经验值,480 的预览区加右侧控件刚好不挤。load_image里过滤了常见图片格式,实际项目里如果读 DICOM,需要换成 pydicom 解析后再转 QImage。
2.3 把模型推理接进按钮槽函数
界面搭好后,推理逻辑要单独抽成一个模块,别塞进窗口类里,否则后期换模型要动 UI 代码。常见做法是写一个predictor.py,暴露load_model()和predict(img_path)两个函数。
import numpy as np import torch from torchvision import transforms from PIL import Image MEAN = [0.485, 0.456, 0.406] STD = [0.229, 0.224, 0.225] def load_model(weight_path, device="cpu"): model = torch.load(weight_path, map_location=device) model.eval() return model def predict(model, img_path, device="cpu"): tf = transforms.Compose([ transforms.Grayscale(num_output_channels=3), # X光单通道转3通道 transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(MEAN, STD) ]) img = Image.open(img_path).convert("L") x = tf(img).unsqueeze(0).to(device) with torch.no_grad(): out = model(x) return float(out.squeeze().cpu().numpy())逻辑说明:X 光片原始是单通道,但 ImageNet 预训练骨干期望 3 通道,所以用Grayscale(num_output_channels=3)复制通道,而不是直接 convert("RGB"),后者在某些灰度图上会产生色彩偏移。归一化参数用的是 ImageNet 统计值,如果你训练时用的是自定义均值方差,这里必须同步改,否则推理分布和训练分布对不上,预测会系统性偏移。torch.no_grad()关掉梯度,桌面端推理能省一半内存。参数224x224要和训练时的输入尺寸一致,改小会丢骨骺细节,改大会拖慢 CPU 推理。
3. 从零跑通骨龄识别项目的完整步骤
3.1 环境配置:PyQt5 安装与 Python 版本选择
环境这一步翻车的人最多。PyQt5 对 Python 版本有要求,3.8 到 3.11 都比较稳,3.12 早期版本装 PyQt5 有过 wheel 缺失的情况。python 安装教程网上很多,但装完记得勾选 Add to PATH,否则后面 pip 都找不到。推荐用虚拟环境隔离,避免和系统里的包打架。
python -m venv boneage_env # Windows boneage_env\Scripts\activate # Linux / macOS source boneage_env/bin/activate pip install pyqt5==5.15.9 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu pip install pillow numpy pydicom逻辑说明:pyqt5==5.15.9是社区里兼容性较好的版本,锁版本能避免 Qt 插件加载报错。torch 这里装的是 CPU 版,桌面端骨龄推理单张图 CPU 也就几百毫秒,没必要强上 GPU,还能减小打包体积。如果你确实要用 GPU,把 index-url 换成对应 CUDA 版本即可。pydicom 是为读 DICOM 准备的,如果项目只处理 png/jpg 可以暂时不装。
提示:装完 PyQt5 后如果运行报 "could not find or load the Qt platform plugin windows",八成是 conda 和 pip 混装导致的 Qt 库冲突,卸载重装到干净虚拟环境即可。
3.2 模型文件加载与权重路径处理
源码包里通常带一个模型文件,格式可能是.pth、.pt或.onnx。加载前先确认它是 state_dict 还是整个模型对象。如果是 state_dict,需要先实例化网络结构再load_state_dict;如果是torch.save(model)保存的整个对象,直接torch.load就行。路径处理上,别写死绝对路径,用相对脚本的路径,打包后才不会找不到文件。
import os import torch BASE_DIR = os.path.dirname(os.path.abspath(__file__)) WEIGHT_PATH = os.path.join(BASE_DIR, "weights", "boneage_resnet50.pth") def load_model_safe(weight_path=WEIGHT_PATH, device="cpu"): if not os.path.exists(weight_path): raise FileNotFoundError(f"模型文件缺失: {weight_path}") ckpt = torch.load(weight_path, map_location=device) if isinstance(ckpt, dict) and "state_dict" in ckpt: from model_zoo import build_resnet50 # 按你项目里的网络定义导入 model = build_resnet50(num_classes=1) model.load_state_dict(ckpt["state_dict"]) else: model = ckpt model.eval() return model逻辑说明:BASE_DIR用__file__推导,保证无论从哪个目录启动脚本,权重路径都对。判断state_dict是为了兼容两种保存习惯,很多开源项目会把 epoch、optimizer 一起存进 dict,直接 load 会报 unexpected key。num_classes=1对应回归输出,如果你拿到的是分类版权重,这里要改成实际类别数。加载失败时先看报错是 key 不匹配还是文件损坏,key 不匹配基本是网络结构定义和训练时不一致。
3.3 图像预处理与推理结果映射到发育等级
模型输出的是一个浮点年龄值,业务上通常还要给一个发育等级,比如"骨龄落后""正常""提前"。这一步的阈值不能拍脑袋,要参考实际数据的分布。常见做法是按实际年龄和预测骨龄的差值分档。
| 差值区间(预测骨龄 - 实际年龄) | 发育判定 | 建议 |
|---|---|---|
| < -1.0 岁 | 明显落后 | 建议复查 |
| -1.0 ~ -0.5 岁 | 偏落后 | 观察 |
| -0.5 ~ 0.5 岁 | 正常范围 | 常规随访 |
| 0.5 ~ 1.0 岁 | 偏提前 | 观察 |
| > 1.0 岁 | 明显提前 | 建议复查 |
def age_to_level(pred_age, real_age): diff = pred_age - real_age if diff < -1.0: return "明显落后" elif diff < -0.5: return "偏落后" elif diff <= 0.5: return "正常范围" elif diff <= 1.0: return "偏提前" else: return "明显提前"逻辑说明:差值法比绝对骨龄更符合临床判读习惯,因为同样 10 岁骨龄,对 8 岁和 12 岁的孩子意义完全不同。阈值 0.5 和 1.0 是常见参考值,实际项目里应该用你手头数据的标准差来定,如果标注误差本身就有 0.6 岁,那 0.5 的档位就没意义了。real_age需要用户在界面上输入,所以 GUI 里要加一个 QLineEdit 或 QDoubleSpinBox 收集实际年龄。
4. 骨龄识别项目避坑与常见问题排查
4.1 预测值恒定为同一个数
现象:不管输入哪张 X 光片,输出骨龄都是 11.2 岁左右。原因通常是预处理和训练不一致,最常见的是归一化参数写错,或者图像被 resize 到错误尺寸导致模型只看到一片灰。解决:打印推理前的 tensor 均值和方差,和训练时的统计值对比,偏差大就改 transforms;再确认 Resize 尺寸和训练配置一致。
4.2 PyQt5 界面卡死无响应
现象:点"开始识别"后窗口白屏,几秒后才恢复。原因是推理跑在主线程里,阻塞了 Qt 事件循环。解决:把推理放到 QThread 里,通过信号槽把结果传回主线程更新 UI。简单点也可以用QApplication.processEvents()临时缓解,但正规做法还是开线程。
4.3 打包成 exe 后模型加载失败
现象:源码里跑得好好的,PyInstaller 打包后提示权重文件找不到。原因是打包时没把 weights 目录加进去,或者用了__file__在打包环境下路径变了。解决:用sys._MEIPASS判断是否在打包环境,把资源路径统一走一个resource_path()函数,并在 spec 文件里用 datas 声明权重目录。
4.4 不同来源的 X 光片预测偏差大
现象:A 医院的片子预测准,B 医院的片子系统性偏高。原因是设备参数、曝光条件不同导致图像灰度分布差异,模型没做过跨域适配。解决:推理前加一步直方图均衡化或 CLAHE,把灰度拉回统一范围;更彻底的做法是训练时做域随机化增强。
4.5 显存或内存溢出
现象:连续识别多张图后程序崩溃。原因是每次推理都新建 tensor 没释放,或者模型重复加载。解决:模型只在启动时加载一次,推理用with torch.no_grad(),大图先缩放再送模型,别把原始 3000x3000 的 DICOM 直接塞进去。
5. 让骨龄识别结果更可信的两个进阶技巧
第一个技巧是加不确定性输出。骨龄回归模型只给一个点估计,用户没法判断这个值靠不靠谱。可以在模型里加一个 dropout 层,推理时保持 dropout 开启,跑 20 次取均值和标准差,标准差大就说明这张图模型没把握,界面上标黄提醒复查。这个做法叫 MC Dropout,改动小但效果明显。
def predict_with_uncertainty(model, img_path, n=20): model.train() # 保持dropout开启 preds = [] for _ in range(n): preds.append(predict(model, img_path)) model.eval() preds = np.array(preds) return preds.mean(), preds.std()逻辑说明:model.train()只影响 dropout 和 batchnorm 行为,这里要的是 dropout 随机性,所以不能 eval。n 取 20 是精度和耗时的折中,CPU 上大概多花几秒。标准差超过 0.8 岁就建议人工复核,这个阈值按你数据调。
第二个技巧是结果可追溯。每次推理把图像路径、模型版本、预测值、时间戳写进一个本地 CSV 或 SQLite,方便后续复盘。我自己的习惯是连预处理参数一起记,因为排查偏差时经常发现是某次改了归一化没同步。骨龄识别这种涉及健康的工具,宁可多留日志,也别等出问题再找后悔药。希望帮到你。
本文还有配套的精品资源,点击获取