简介:本资源为基于PyQt5与深度学习实现的骨龄识别检测Python项目源码,面向具备一定Python基础、希望将目标检测模型落地为桌面应用的开发者与学习者。项目以YOLOv5为核心检测框架,配套图形化界面,可完成骨龄相关目标的识别与检测任务,数据集来自百度飞桨平台,需按说明完成图片标注、VOC转YOLO格式及数据配置文件修改等预处理流程。压缩包共约200个文件,整体717.07MB,包含69个py源码、53个yaml与12个yml配置、18个pth模型权重,以及md说明文档、sh脚本、Dockerfile部署文件等,覆盖训练、推理与界面运行所需模块。已有726人学习下载。资源内代码均经过测试运行成功,读者可获得完整可运行的工程结构、预训练模型文件、项目使用说明与数据预处理脚本,便于快速复现骨龄检测流程并在此基础上二次开发。
1. 骨龄识别这套 PyQt5 源码,为什么值得先跑通再谈优化
骨龄评估在儿科内分泌、生长发育门诊里是个高频刚需,传统方法是医生对着左手腕 X 光片逐块骨骺比对图谱,费时且主观性强。这套基于 PyQt5 深度学习实现的骨龄识别检测 Python 源码,把推理模型和图形界面打包在一起,下载解压后就能在本地跑出一张骨龄预测结果,对想入门医学影像深度学习、又不想从零搭 GUI 的开发者来说,省掉了大量胶水代码。它适合三类人:一是做课程设计或毕设的学生,需要一套能演示、能改参数的完整工程;二是想验证骨龄回归/分类思路的算法工程师;三是需要快速搭一个医学影像推理 Demo 的产品原型开发者。资源里含 GUI 界面、项目使用说明和模型文件,数据集来自公开的骨龄影像项目,整体链路是「数据预处理 → YOLO 标注转换 → 训练/推理 → PyQt5 界面展示」。需要提醒的是,骨龄识别属于医学辅助场景,输出结果只能作为技术验证,不能替代临床诊断。下面我按自己拆包复现的顺序,把这份源码从环境到推理、从参数到踩坑讲透。
2. 拆开压缩包先看什么:目录结构、依赖与运行环境
2.1 从文件清单判断这套工程的真实形态
拿到压缩包,别急着双击运行,先看根目录的文件构成。从项目正文给出的清单能读出几个关键信号:events.out.tfevents.1686040397.HY-DKIFFGATEUML.18096.0是 TensorBoard 的事件日志,说明训练过程被记录过,模型不是空壳;CITATION.cff说明作者希望被规范引用,工程有一定完整度;setup.cfg是 Python 打包配置;三个Dockerfile(含Dockerfile-arm64、Dockerfile-cpu)说明作者考虑过不同架构和纯 CPU 部署,这对没有独立显卡的机器很友好;.dockerignore、.gitattributes、.gitignore是标准工程化文件。这套组合告诉我:它不是随手丢出来的脚本堆,而是一个带训练痕迹、可容器化、跨架构可跑的项目。实际解压后,核心目录通常包含yolov5_master/(数据预处理与检测相关脚本)、GUI 主程序、模型权重文件和一份使用说明。先确认模型文件在不在,再确认yolov5_master目录里的脚本是否齐全,这决定了你后面能不能走通预处理链路。
2.2 依赖安装:PyQt5 与深度学习框架的版本对齐
环境是这套源码翻车率最高的地方。PyQt5 和 PyTorch 对 Python 版本都敏感,我一般会先建独立虚拟环境,避免污染系统 Python。常见做法是用 conda 或 venv 隔离,Python 版本优先选 3.8 到 3.10 之间,太新容易遇到 PyQt5 轮子缺失,太旧又可能装不上新版 torch。
# 创建并激活独立环境,Python 版本建议 3.8~3.10 conda create -n boneage python=3.9 -y conda activate boneage # 先装深度学习框架,CPU 版和 GPU 版二选一 # GPU 版(需匹配本机 CUDA 版本,这里以 cu118 为例) pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 纯 CPU 版(对应 Dockerfile-cpu 的思路) # pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu # 再装 GUI 与图像处理依赖 pip install PyQt5 opencv-python numpy pillow pyyaml逻辑说明:先装 torch 再装 PyQt5,是因为 torch 的依赖解析较重,先固定它能让后续包版本更稳。参数说明:--index-url指定官方轮子源,避免从源码编译;CUDA 版本必须和本机驱动匹配,装错会报CUDA error: no kernel image is available。如果只是跑推理看界面,CPU 版足够,速度慢但不会因为显卡驱动翻车。装完用python -c "import torch, PyQt5; print(torch.__version__)"验证,两个都能导入再往下走。
2.3 模型文件与使用说明的定位
模型文件通常放在工程根目录或weights/下,扩展名多为.pt。使用说明文档会写清楚入口脚本名和默认权重路径。我一般先通读说明,确认三件事:入口是哪个.py、权重放哪、输入图片格式要求。如果说明里提到需要先做数据预处理才能推理,那说明模型可能依赖特定标注格式,不能直接喂原始 X 光片。这一步别跳,很多人界面打不开就是因为权重路径和说明写的不一致。
3. 数据预处理链路:images_tag.py 与 voc_to_yolo.py 怎么改
3.1 先跑 images_tag.py:图片路径是第一个必改项
数据集从公开的骨龄影像项目下载后,不能直接进训练。第一步是运行yolov5_master/images_tag.py,它的作用是对图片做标注整理或打标签。运行前必须先改里面的图片路径,否则脚本会去读一个不存在的目录,直接报FileNotFoundError。
# yolov5_master/images_tag.py 关键改动示意 # 原始路径大概率是作者本机路径,必须换成你自己的数据集目录 IMAGE_DIR = r"D:/boneage_dataset/images" # 改成你解压后的图片目录 LABEL_DIR = r"D:/boneage_dataset/labels" # 标注输出目录,不存在会自动建 import os os.makedirs(LABEL_DIR, exist_ok=True) # 遍历图片,按脚本原有逻辑生成标签文件 for name in os.listdir(IMAGE_DIR): if name.lower().endswith((".jpg", ".png", ".jpeg")): img_path = os.path.join(IMAGE_DIR, name) # 后续调用脚本内的标注/处理函数 # process_image(img_path, LABEL_DIR)逻辑说明:这段代码的核心是把硬编码路径替换成你自己的绝对路径,并确保输出目录存在。参数说明:IMAGE_DIR指向原始 X 光片目录,LABEL_DIR是标签输出目录,两者不要设成同一个,否则遍历时会把刚生成的标签也当图片处理。Windows 路径用反斜杠时记得加r前缀或用双反斜杠,否则\t、\n会被当转义字符,这是新手最常见的路径玄学。
3.2 voc_to_yolo.py 的类别数:9 大类必须对齐
第二步是yolov5_master/voc_to_yolo.py,它把 VOC 格式的标注转成 YOLO 格式。项目说明里明确提到:该项目有 9 个大类,作者在这个脚本里做了修改,不建议直接参考,你只需要把classes的值改成该项目的类别即可,具体类别参考数据集链接。这句话是整条链路的关键,类别数错了,训练时标签和模型输出维度对不上,loss 会直接 NaN 或报维度错误。
# yolov5_master/voc_to_yolo.py 开头参数区 # 类别列表必须和数据集真实类别一一对应,顺序不能乱 classes = [ "class_1", "class_2", "class_3", "class_4", "class_5", "class_6", "class_7", "class_8", "class_9", ] # 共 9 类,名称以数据集链接里的定义为准 # 路径同样要改成自己的 VOC_ANNOTATION_DIR = r"D:/boneage_dataset/annotations" YOLO_LABEL_DIR = r"D:/boneage_dataset/labels_yolo"逻辑说明:YOLO 格式的标签是类别索引 中心x 中心y 宽 高的归一化数值,类别索引来自classes列表的下标。参数说明:列表长度必须是 9,顺序必须和数据集标注文件里的类别顺序一致,否则索引错位会让模型学到错误映射。转换完成后,随便打开一个.txt标签,确认第一列数字在 0 到 8 之间,出现 9 及以上就说明类别数配错了。
3.3 mydata.yaml 的 path:训练配置的最后一环
数据转换完,还要改yolov5_master/data/mydata.yaml里的path。这个文件告诉 YOLO 去哪里找训练集、验证集和类别定义。
# yolov5_master/data/mydata.yaml path: D:/boneage_dataset # 数据集根目录,改成你的实际路径 train: images/train # 相对 path 的训练图片目录 val: images/val # 相对 path 的验证图片目录 nc: 9 # 类别数,必须和 classes 长度一致 names: # 类别名,顺序和 voc_to_yolo.py 保持一致 0: class_1 1: class_2 2: class_3 3: class_4 4: class_5 5: class_6 6: class_7 7: class_8 8: class_9逻辑说明:path是根,train和val是相对它的子路径,这样换机器时只改一处。参数说明:nc必须等于 9,names的键从 0 开始连续,缺号会导致加载时报KeyError。改完这三个文件,预处理链路才算真正打通,可以进入训练或直接加载已有权重推理。
提示:预处理三步的顺序不能颠倒,先 images_tag 再 voc_to_yolo 最后改 yaml,跳步会导致标签目录为空或格式错乱。
4. PyQt5 界面与推理打通:从加载权重到出结果
4.1 GUI 主程序的启动与权重加载
PyQt5 界面的价值在于把命令行推理包装成可点击的操作。启动入口通常是根目录下的主程序,运行后弹出窗口,包含「选择图片」「开始检测」「结果显示」几个区域。启动前要确认权重路径正确,很多界面打不开不是 PyQt5 的问题,而是权重文件路径写死成了作者的机器路径。
# GUI 主程序里权重加载的关键片段示意 import torch from PyQt5.QtWidgets import QApplication, QMainWindow MODEL_PATH = r"./weights/best.pt" # 改成你实际的权重路径 class MainWindow(QMainWindow): def __init__(self): super().__init__() # 加载模型,map_location 保证 CPU 环境也能加载 GPU 训练的权重 self.model = torch.load(MODEL_PATH, map_location="cpu") self.model.eval() # 推理模式,关闭 dropout 和 BN 更新逻辑说明:torch.load的map_location="cpu"是跨设备加载的关键,没有它,GPU 上训练的权重在纯 CPU 机器上会直接报错。参数说明:MODEL_PATH指向.pt权重;model.eval()必须调用,否则推理结果会因 dropout 随机性而不稳定。如果界面启动报ModuleNotFoundError,多半是 PyQt5 没装进当前环境,回到 2.2 节确认虚拟环境是否激活。
4.2 图片输入到推理输出的完整流程
界面选图后,程序内部走的是「读图 → 预处理 → 前向推理 → 后处理 → 显示」这条链。骨龄识别常见两种输出:一种是回归出具体骨龄数值,一种是分类到骨龄区间。这套源码的模型文件决定了它属于哪种,使用说明里一般会写。
import cv2 import numpy as np def predict(image_path, model, img_size=640): img = cv2.imread(image_path) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 统一色彩通道 img = cv2.resize(img, (img_size, img_size)) # 对齐训练输入尺寸 tensor = torch.from_numpy(img).float() / 255.0 # 归一化到 0~1 tensor = tensor.permute(2, 0, 1).unsqueeze(0) # HWC -> NCHW with torch.no_grad(): # 推理不建计算图,省显存 output = model(tensor) return output逻辑说明:预处理必须和训练时一致,否则推理结果会系统性偏移。参数说明:img_size要和训练配置里的输入尺寸一致,常见是 640;/255.0是归一化,如果训练时用的是 ImageNet 均值方差标准化,这里也要换成对应写法。torch.no_grad()能显著降低显存占用,推理场景必加。输出拿到后,按模型定义做后处理,回归模型直接取值,检测模型还要做 NMS 去重。
4.3 界面卡顿与线程处理
PyQt5 主线程负责界面刷新,如果把推理直接放在主线程,图片一大界面就会假死,用户以为程序崩了。常见做法是把推理放到QThread子线程,通过信号槽把结果传回界面。
from PyQt5.QtCore import QThread, pyqtSignal class InferThread(QThread): finished = pyqtSignal(object) # 推理完成信号,携带结果 def __init__(self, model, image_path): super().__init__() self.model = model self.image_path = image_path def run(self): result = predict(self.image_path, self.model) self.finished.emit(result) # 子线程算完,发信号回主线程更新 UI逻辑说明:把耗时推理移出主线程,界面保持响应。参数说明:pyqtSignal(object)用于传递任意 Python 对象,结果类型复杂时用object最省事。信号槽是跨线程安全的,不要在子线程里直接操作界面控件,否则会触发 Qt 的线程告警甚至崩溃。这一步是很多 Demo 从「能跑」到「好用」的分水岭。
5. 避坑与排查:这套源码最容易翻车的五个地方
5.1 现象:运行主程序报 No module named 'PyQt5'
原因:PyQt5 装到了系统 Python,而运行用的是虚拟环境,或者反过来。热词里「labelme 无法安装 pyqt5」「pyqt5安装」高频出现,本质都是环境错位。 解决:先which python(Windows 用where python)确认当前解释器路径,再pip list | grep PyQt5确认装没装。没装就pip install PyQt5,装错环境就激活正确环境重装。
5.2 现象:voc_to_yolo 转换后标签第一列出现 9 或更大
原因:classes列表长度和数据集真实类别数不一致,或者顺序和标注文件对不上。 解决:回到数据集链接核对类别定义,把classes改成正好 9 个且顺序一致。转换后抽查标签文件,第一列必须在 0 到 8 之间。
5.3 现象:训练或推理报 CUDA out of memory
原因:输入尺寸过大、batch 过大,或 GPU 显存本身偏小。 解决:先把img_size从 640 降到 416 或 320 试跑,再把 batch 降到 1。纯推理场景加torch.no_grad(),能省下可观的显存。实在不行切 CPU 版,慢但稳。
5.4 现象:界面能打开,选图后没反应或直接闪退
原因:推理跑在主线程导致界面假死,或图片路径含中文、空格导致cv2.imread返回 None。 解决:按 4.3 节把推理放进 QThread;图片路径尽量用纯英文无空格目录,cv2.imread对中文路径支持差,必要时用np.fromfile加cv2.imdecode绕过。
5.5 现象:mydata.yaml 改完仍报找不到数据集
原因:path用了相对路径但工作目录不对,或train/val子目录实际不存在。 解决:path先用绝对路径,确认images/train、images/val真实存在。YAML 里路径分隔符统一用正斜杠/,Windows 反斜杠在 YAML 里容易被解析成转义。
注意:以上五条里,环境错位和类别数错位占了翻车案例的大半,先把这两条排掉再查别的。
6. 进阶技巧:把推理封装成可复用接口并做结果校验
跑通界面只是第一步,真正让这套源码产生价值的是把它变成可复用、可校验的推理接口。我一般会在 GUI 之外单独抽一个inference.py,把模型加载和预测逻辑独立出来,这样既能被界面调用,也能被批量脚本调用,方便做结果对比。
# inference.py 可复用推理接口 import torch import cv2 import numpy as np class BoneAgePredictor: def __init__(self, model_path, img_size=640, device="cpu"): self.device = torch.device(device) self.img_size = img_size # 加载权重并切到对应设备 self.model = torch.load(model_path, map_location=self.device) self.model.eval() self.model.to(self.device) def preprocess(self, image_path): img = cv2.imread(image_path) if img is None: raise ValueError(f"图片读取失败,检查路径: {image_path}") img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = cv2.resize(img, (self.img_size, self.img_size)) tensor = torch.from_numpy(img).float() / 255.0 return tensor.permute(2, 0, 1).unsqueeze(0).to(self.device) def predict(self, image_path): tensor = self.preprocess(image_path) with torch.no_grad(): output = self.model(tensor) return output.cpu().numpy()逻辑说明:把加载、预处理、推理拆成三个方法,职责清晰,换模型或换设备只改初始化参数。参数说明:device支持"cpu"和"cuda",有显卡就传"cuda";img_size必须和训练一致;preprocess里对img is None做了显式判断,把「路径错」和「模型错」两类问题分开,排查时能少走弯路。
封装完接口,下一步是结果校验。骨龄识别没有绝对真值,但可以做一致性检查:同一张图连续推理三次,结果应该完全一致(因为eval模式关闭了随机性);把图片做轻微平移或缩放,结果不应剧烈跳变,如果跳变很大,说明模型对输入过于敏感,预处理或归一化可能和训练不一致。我还会拿几张训练集里的图跑一遍,看输出是否落在合理区间,如果全部输出同一个值,多半是权重没加载成功或类别映射错了。
| 校验项 | 合格表现 | 异常信号 | 排查方向 |
|---|---|---|---|
| 重复推理一致性 | 三次结果完全相同 | 每次结果不同 | 是否漏了 model.eval() |
| 轻微扰动稳定性 | 结果小幅波动 | 结果剧烈跳变 | 预处理/归一化是否对齐训练 |
| 训练集回测 | 输出落在合理区间 | 全部同值或越界 | 权重加载、类别映射 |
| 路径容错 | 正常图能读入 | 报图片读取失败 | 中文路径、空格、格式 |
这套校验习惯是我踩过坑之后养成的。早些年我直接拿界面跑一张图就交付,结果换台机器权重没加载成功,界面照样弹出结果,只是输出全是默认值,差点闹笑话。从那以后我每次接这类医学影像推理工程,都强制先跑一遍一致性校验再谈效果。希望帮到你。
本文还有配套的精品资源,点击获取