
简介面向深度学习与医学影像处理场景的骨龄识别检测项目旨在解决骨龄评估中从数据准备到模型落地的实操难题基于 PyQt5 构建了可交互图形界面覆盖从数据预处理、模型训练到检测识别的完整流程可作为高校课题、医疗人工智能入门者及中级开发者的实践参考。压缩包共含 200 个文件主要包含源代码脚本、模型权重文件、参数配置与说明文档其中 69 个源码文件涉及图像标注、格式转换、模型调用等环节18 个权重文件可直接用于推理另有部署脚本、环境配置及 Docker 辅助内容整体约 717MB目录结构清晰便于按需查阅。当前已有 726 人学习或下载具备一定参考热度。资源内置项目使用说明与关键环节指引可帮助读者快速跑通骨龄识别流程减少环境配置与代码调试中的弯路并清晰理解数据准备、参数调整、训练与测试等关键步骤适合作为论文实验、课程设计或入门项目的基础代码。1. 这个zip拆开之前先想清楚它是在解决什么问题很多人搜“基于pyqt5深度学习实现的骨龄识别检测python源码”其实是在找能直接跑起来交付的完整工程。这个zip本质上是医学图像识别的典型三件套PyQt5搭建的桌面GUI、一个预先训练好的骨龄模型、一组把X光片送进模型并显示结果的推理代码。它覆盖了从点击按钮到看到骨龄数值的完整链路适合做课程设计、毕设演示或深度学习入门项目。要注意临床骨龄评估通常结合Greulich-Pyle图谱法与Tanner-Whitehouse计分法医生会综合手腕骨化中心的形态判断深度学习模型只能作为辅助不能替代影像科医生的最终报告。接下来我就按“原理、源码、跑通、进阶”这条线把这个zip讲透顺便把中途最容易踩的坑一并清掉。2. 骨龄检测模型的理论基础从图谱法到卷积神经网络2.1 骨龄为什么能“识别”特征来源与读片逻辑骨龄与生活年龄的差值是儿科内分泌科判断生长发育是否偏离的重要依据。传统读片逻辑中医生主要观察左手腕X光片上桡骨远端、尺骨远端和各块腕骨的骨化中心出现顺序与形态。骨化中心的数量、边界清晰度、关节面覆盖比例会随年龄呈阶段性变化。深度学习模型要学习的正是这种“图像局部区域的时序性形态变化”。如果用通俗的话说这个任务等价于让模型回答一个问题这张X光片上骨头的成熟程度对应正常人几岁时的状态。注意这不是普通的目标检测不需要定位“骨化中心在哪里”并逐个打框。RSNA骨龄挑战赛的获奖方案给出了一个很高效的思路把整张左手X光片resize到固定尺寸用卷积网络提取特征最后用回归头输出一个连续值以月为单位。这个做法省去了标注关节框的繁重工作也符合深度学习的端到端风格。与此前的方法体系对比GP图谱法靠医生将整张片子与标准图谱对照TW计分法把每个骨化中心拆成若干阶段分别打分两者都有明显的主观依赖和时间成本。CNN方案则把“读片经验”压缩进网络权重里输入输出路径最短。下表能直观看出三者的差异评估方式输入类型输出形式主观依赖自动化程度GP图谱法整张X光片最接近的标准年龄高低TW计分法各骨化中心局部图各项得分之和中低CNN回归整张X光片连续月数/岁数低高说明一下这个表的实际意义如果你要复现这个工程大概率会遇到“模型预测结果和医生判读结果不一致”的情况。这不一定是你代码写错了而是GP/TW与CNN的评估口径本身就有差异。CNN学到的特征是数据驱动的当训练集标注来自某个特定医院的医生时模型会不自觉继承那位医生的读片习惯这一点在后续评估精度时值得关注。2.2 模型设计选型回归头与分类头的取舍2.2.1 回归头的主体结构import torch import torch.nn as nn import torchvision class BoneAgeRegressor(nn.Module): 骨龄回归模型 输入: (N, 3, H, W) 归一化后的图像 输出: (N, 1) 骨龄单位是月 def __init__(self): super().__init__() # 用 ResNet18 做特征提取骨架ImageNet 预训练权重加速收敛 self.backbone torchvision.models.resnet18(pretrainedTrue) # 把最后一层全连接替换成 512 - 256 - 1 self.backbone.fc nn.Sequential( nn.Linear(512, 256), nn.ReLU(inplaceTrue), nn.Dropout(p0.2), nn.Linear(256, 1) ) def forward(self, x): return self.backbone(x) model BoneAgeRegressor()这段代码的逻辑说明模型把输出层改成单通道回归层适合骨龄这类连续值预测Dropout用于防止训练数据不足时过拟合。训练时常用平滑L1Huber Loss或MSE作为损失函数平滑L1在边界上的大误差惩罚比MSE柔和不容易被个别异常标注带偏。实际训练时如果数据集覆盖0到18岁且分布均匀回归头收敛很稳定这也是多数开源项目默认采用的做法。2.2.2 分类头与混合头的价值回归头虽简单但在标注质量差的数据集上容易把误差平滑化。另一种思路是把0到18岁按年拆成19类做分类用CrossEntropy训练最后把各类概率乘年龄值求期望。分类的边界清晰但相邻年龄段之间会出现人为断裂。如果样本量足够还可以做“回归分类”的混合头用分类概率约束回归值这种方式能有效抑制完全偏离常识的预测结果。不过开源zip里出现的模型结构绝大多数是纯回归头因为它的实现最简单与PyQt5界面展示数据的衔接也更直接。如果你拿到源码后在模型定义里看到的是Linear(256, 1)这种单输出结构就可以直接按回归模型处理不需要额外改代码。如果输出层是Linear(256, 19)就要注意GUI后处理里是否做了从类别概率到数值的换算。2.3 PyQt5在骨龄检测链路中的角色界面框架与多线程PyQt5负责的是“外壳”部分包括选择图片、触发推理、显示骨龄结果、生成简单报告。它是纯Python绑定的跨平台GUI框架底层依赖Qt的C实现开发效率高与OpenCV、Pillow、NumPy这些图像库协作也顺畅。这里最容易踩的第一个大坑是直接在UI主线程里跑模型推理。骨龄模型在CPU上推理一次大约0.2到1秒这段时间窗口会假死如果演示时遇到会非常尴尬。常见的做法是使用QThread把推理搬到后台线程界面只负责展示结果from PyQt5.QtCore import QThread, pyqtSignal class InferenceWorker(QThread): # 推理完成时触发参数是预测月数 result_ready pyqtSignal(float) def __init__(self, model, image_array): super().__init__() self.model model self.image image_array def run(self): with torch.no_grad(): # image 形状: (1, 3, H, W)float32已归一化 months self.model(torch.from_numpy(self.image)).item() self.result_ready.emit(months)这段代码将推理放入独立线程run方法结束后通过信号把结果传回UI线程。QThread是PyQt5里处理耗时任务的标准组件配合信号槽机制可以安全更新界面控件不会产生线程竞争问题。后面第4章还会提到界面启动流程里的细节这里先记住一个原则PyQt5只是壳模型推理和预处理都不要阻塞UI线程。3. 解压源码后的第一个小时把工程结构和推理链路读透3.1 骨龄识别项目源码的典型文件构成拿到zip后先做一件事把文件树完整列出来确认代码分了几层。一个结构清晰的项目通常会按界面、推理、资源三条线分开组织bone_age_project/ ├── main.py # PyQt5 程序入口 ├── inference.py # 模型加载与图像预处理 ├── ui_mainwindow.py # GUI 主窗口界面代码 ├── models/ │ └── bone_age_resnet.pth # 模型权重文件 ├── images/ # 测试用 X 光片 ├── docs/ │ └── 使用说明.md └── requirements.txt这个结构中main.py负责创建QApplication并启动主界面ui_mainwindow.py负责控件布局inference.py负责把图片路径变成骨龄数值。三者各司其职。实际项目中ui_mainwindow.py可能是由Qt Designer生成的界面代码也可能直接手写布局两种做法都有。判断方法很简单看文件里是否有retranslateUi或setupUi这类方法名有就是Designer产物。3.2 模型加载与预处理三个最容易写错的参数在GUI上点击按钮后处理流程是从QFileDialog拿到图片路径用OpenCV或Pillow读取转为RGBresize到模型输入尺寸做归一化然后送入模型。工程里最常出问题的有三个参数输入尺寸、通道顺序、归一化方式。尺寸不匹配会直接报Tensor shape错误通道顺序反了会导致图像颜色失真归一化方式与训练时不一致会造成精度大幅下降。import cv2 import numpy as np def preprocess(image_path: str, target_size: int 224) - np.ndarray: img cv2.imread(image_path, cv2.IMREAD_COLOR) # BGR img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 转 RGB img cv2.resize(img, (target_size, target_size)) img img.astype(np.float32) / 255.0 # 归一化到 0-1 # 按训练时的均值方差做标准化 mean np.array([0.485, 0.456, 0.406]) std np.array([0.229, 0.224, 0.225]) img (img - mean) / std # 从 HWC 转 CHW再增加 batch 维 img np.transpose(img, (2, 0, 1))[np.newaxis, ...] return img这段代码做了三件事颜色空间转换把OpenCV默认的BGR修正为RGBresize固定模型的输入分辨率标准化按ImageNet统计值进行。需要特别注意有些骨龄模型是用灰度图训练的如果使用说明里提到“grayscale”或单通道输入就要改为cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)并去掉均值标准化同时把输入维度从3通道改成1通道。这些细节以zip内模型训练配置为准不要想当然套用通用参数。模型加载和后处理同样要按训练时的约定来写import torch device torch.device(cuda if torch.cuda.is_available() else cpu) model BoneAgeRegressor().to(device) model.load_state_dict(torch.load( models/bone_age_resnet.pth, map_locationdevice, weights_onlyTrue )) model.eval()这里map_locationdevice决定权重加载到CPU还是GPUweights_onlyTrue是PyTorch较新版本加载完整模型时应对序列化格式变化的关键参数model.eval()一定要调用否则Dropout层会在推理时保留随机行为导致每次预测结果轻微浮动。3.3 推理结果如何转换成可读的骨龄值模型输出的数值单位不一定统一。常见的有两种月数0-240和岁数0-20。开源工程里如果模型训练时用的标签是“年龄乘以12”则结果需要除以12并保留一位小数。可以构造一个独立的格式化函数def format_bone_age(raw_value: float, unit: str month) - str: if unit year: age_year raw_value else: age_year raw_value / 12.0 return f骨龄: {age_year:.1f} 岁 # 使用示例 print(format_bone_age(147.3, unitmonth)) # 骨龄: 12.3 岁这段代码的价值在于把“模型输出”和“界面展示”解耦以后换模型或换单位只需要改这个函数。注意界面里展示骨龄的同时建议把原始月数和输入图片一并保留方便人工复核。界面设计成三个独立Label而不是一个大文本框后面做报告输出时会轻松许多。4. 跑通实测从安装PyQt5到跑出第一张X光片的骨龄4.1 运行环境清单与安装序列这类项目对环境有硬性要求。Python版本推荐3.8到3.10太低装不了新版PyTorch太高会遇到Qt绑定兼容问题。建议先建虚拟环境再安装依赖避免污染系统环境也方便后面打包分发。conda create -n boneage python3.9 -y conda activate boneage pip install pyqt5 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu pip install opencv-python numpy依赖说明pyqt5是GUI的运行时核心从PyPI直接安装通常会自动带出PyQt5-Qt5和PyQt5-sip两个配套库。torch和torchvision从CPU源安装不依赖CUDA可以降低首次运行门槛。如果机器有NVIDIA显卡想用GPU推理再把--index-url换成CUDA对应版本源。opencv-python负责图像读取与预处理numpy负责数组变换。包名作用安装命令常见版本线PyQt5GUI主框架pip install pyqt55.15.xtorch模型推理运行时pip install torch torchvision1.13 – 2.xopencv-python图像读取与尺寸变换pip install opencv-python4.xnumpy数组归一化与转置pip install numpy1.21 – 1.264.2 启动项目与首跑验证依赖装完后进入项目目录运行python main.py出现带“选择图片”按钮的主窗口即表示GUI启动成功。点击按钮选一张手腕X光片再点击识别按钮观察界面状态。如果按钮在点击后变成灰色或显示“识别中…”超过1秒说明后台推理线程已正常启动如果窗口直接卡住不动基本可以确定推理代码被写在了UI主线程里需要按第2.3节的QThread方式修复。打开推理代码单独验证也是一个高效习惯。在项目根目录建一个临时脚本读一张图片直接调用preprocess和model注意不要经过GUI这样能把问题快速定位到“模型链路”还是“界面链路”。4.3 高频报错与排查清单第一类ModuleNotFoundError。优先检查conda环境是否激活或者PyCharm等IDE的解释器是否指向你创建的虚拟环境。终端里执行pip list看一下有没有PyQt5和torch没有就重新安装。第二类加载.pth时出现weights_only或UNSAFE_LEGACY相关报错。这是PyTorch 2.6以后序列化格式升级导致的加载参数时显式加上weights_onlyTrue即可。如果pth文件不是纯state_dict而是整个模型对象则改成torch.load(..., weights_onlyFalse)或者从字典里取state_dict键后再加载。第三类QWidget相关崩溃提示“Must construct a QApplication before a QWidget”。这说明在创建窗口之前没有实例化QApplication。检查main.py开头是否有这样两行import sys from PyQt5.QtWidgets import QApplication app QApplication(sys.argv)第四类界面正常但识别结果始终是同一个值。多半是模型输入没有变化比如通道顺序固定写错成BGR、resize尺寸与训练不一致或者模型未调用model.eval()。写一段独立脚本打印预处理后的数组shape和均值确认输入是否随图片变化。5. 提升可用性的三个进阶技巧模型替换、界面优化与打包分发5.1 换用自己的模型权重这套zip里的代码要想真正落地第一步通常是换成自己训练的权重。只要你的模型结构和BoneAgeRegressor一致替换模型文件即可如果换了骨干网络需要同步修改inference.py里的模型定义同时确认预处理尺寸和归一化参数与新模型训练时保持一致。具体操作是把新权重放到models目录再检查第3.2节的preprocess参数确保target_size和标准化方式匹配。这一步完成后GUI代码不需要任何改动。5.2 给界面加一个“导出报告”功能在UI里增加一个“导出报告”按钮把骨龄、图片路径和检测时间写入文本文件或PDF。简单做法是用QFileDialog拿保存路径再用Python标准库写文本想要更正式的报告可以用QTextDocument生成一个HTML格式报告直接输出成PDF。需要说明的是这份报告应当明确标注“AI辅助评估结果仅供研究参考”避免在非研究场景下误用。5.3 用PyInstaller打包成免环境可执行文件给没有Python环境的同事演示时把项目打包成exe会更方便。PyInstaller打包这类工程的难点是模型文件和Qt插件路径。pyinstaller -F -w main.py \ --add-data models;models \ --add-data images;images \ --name BoneAgeTool-w表示不显示控制台窗口--add-data把模型和图片资源打包进可执行文件或释放到临时目录。打包后程序里获取模型文件路径的方式需要使用sys._MEIPASSimport os import sys base sys._MEIPASS if hasattr(sys, _MEIPASS) else os.path.dirname(os.path.abspath(__file__)) model_path os.path.join(base, models, bone_age_resnet.pth)这里sys._MEIPASS是PyInstaller运行时解压资源用的临时目录。在Linux或macOS上打包时--add-data路径分隔符要改成冒号。另一个实用小技巧项目里所有路径处理都写成os.path.join避免硬编码Windows反斜杠这样代码在不同设备、不同系统间交接时能少掉一大半莫名其妙的环境类报错。本文还有配套的精品资源点击获取