简介:基于深度学习的骨龄检测识别系统是一套完整落地项目,面向医学影像算法开发者、计算机视觉学习者及儿科辅助诊断场景。系统以PyTorch为训练框架,采用Pyside6构建桌面GUI,并集成YOLOv5模型完成儿童手腕X光图像中的骨骼特征定位与骨龄推断。压缩包总计205个文件,包含71个Python源码、43个编译后pyc、32个YAML配置、11个模型权重pt、3个Jupyter教程以及类型齐全的Dockerfile、MD文档、UI设计文件等,压缩后整体约406MB。已有331人学习下载。内容覆盖模型训练、推理、界面展示、项目说明与开源规范,目录结构完整清晰,适合直接二次开发或作为毕业设计、科研课题参考。通过源码、配置与教程结合,可快速掌握从数据处理到界面部署的完整流程,并利用预训练权重自行复现骨龄检测效果。 骨龄评估这个事,说起来简单,做起来全是主观判断。儿科内分泌科的医生拿到一张左手腕X光片,要么翻开GP图谱一页页比对骨化中心的发育形态,要么按TW法对桡骨、尺骨和指骨的十多个骨化中心逐一打分,快则十几分钟,慢则半小时,同一张片子不同医生读出来的结果能差出一岁上下。这种又耗时又依赖经验的活儿,天然适合交给深度学习。这篇文章就把我做的这套"基于深度学习的骨龄检测识别系统"完整拆开讲一遍:PyTorch做算法框架,YOLOv5做手部区域检测,PySide6搭桌面客户端,从数据整理、模型训练到软件集成,把值得注意的细节都交代清楚。无论你是准备毕业设计,还是想入门一个能真正落地的计算机视觉项目,这套流程都可以直接参考。
1. 骨龄评估的痛点,以及系统为什么采用"先检测再回归"的两段式方案
1.1 GP图谱法和TW计分法的瓶颈在哪
骨龄是衡量儿童骨骼成熟度的重要指标,临床上用来辅助诊断生长迟缓、性早熟等问题。最常用的读片方法是Greulich-Pyle(GP)图谱法,医生把左腕X光片与标准发育图谱逐龄比对,通过骨化中心数量、形态和骨骺闭合程度判定骨龄;Tanner-Whitehouse(TW)计分法则把桡骨、尺骨及指骨的骨化中心拆成多个评分部位逐个打分。两种方法都严重依赖读片医生经验,骨化中心边缘算"开始出现"还是"已发育",不同医生的标准不一样,文献里观察者间误差差距很大,半年到一年的偏差并不罕见。对需要长期随访生长激素治疗效果的孩子来说,读片噪声会直接影响临床判断。
1.2 为什么先让YOLOv5定位手部,再做年龄回归
早期做骨龄深度估计,很多论文直接照搬RSNA 2017骨龄挑战赛的思路,把整张左手X光片丢给卷积回归网络。挑战赛数据是标准化裁剪过的,手在画面中央,整图回归没问题;但真实系统的X光片五花八门,有的带检查床边缘,有的有铅字标记,有的拍进一整段前臂,整图回归会让网络浪费大量感受野去理解背景,误差被放大。
所以这套系统拆成两段:
- YOLOv5在整张图上检测左手腕区域,输出一个边界框;
- 把裁剪出的手部图归一化后送进一个回归CNN,直接输出月龄。
"先定位再评估"在医学影像辅诊里很常见,本质是把一个难任务拆成两个简单任务。检测器只负责框得准,回归器只在干净区域里提取骨成熟度特征。这个过程有点像先在地图上定位城市,再放大看街道细节——第一步解决"在哪",第二步解决"细不细"。至于为什么选YOLOv5而不是YOLOv6或YOLOv8:这个项目难在数据整理和系统集成,单手部检测的精度,YOLOv5s在GPU上几十毫秒搞定,绰绰有余;加上YOLOv5社区资料最多、训练自定义数据集的坑最少,做毕设或中小型系统是最稳的选择。后续想升级检测器,回归网络可以原封不动保留。
2. 环境搭建实录:PyTorch GPU版、YOLOv5与PySide6的版本配合
2.1 先建独立环境,再解决版本匹配
这类项目依赖多,千万别在系统Python里直接pip装。我习惯用Anaconda建独立环境:
conda create -n boneage python=3.10 -y conda activate boneage选Python 3.10纯粹是因为PyTorch、PySide6、YOLOv5对它的兼容性最稳,3.11、3.12也能跑,但部分旧库在它们上面偶尔有编译兼容问题,没必要赌。Windows和Ubuntu操作一致,唯一区别是装之前先用nvidia-smi确认NVIDIA驱动支持的CUDA版本,驱动太老就得选低一档的CUDA组合。
2.2 安装GPU版PyTorch与PySide6
PyTorch安装最关键的坑是版本要和CUDA匹配。我用的是CUDA 12.1组合:
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121装完立刻验证:
python -c "import torch; print(torch.__version__, torch.cuda.is_available())"输出2.x和True才算正常。如果显示False,多半是驱动版本太低或装成了CPU版,趁早返工比后面排查省时间。接着装PySide6:
pip install pyside6当前6.6、6.7这些稳定版本在Python 3.10下都没问题。装完顺手运行pyside6-designer确认能打开UI设计器,后面画界面要用。
2.3 拉取YOLOv5仓库,依赖安装顺序有讲究
git clone https://github.com/ultralytics/yolov5 cd yolov5 pip install -r requirements.txt这里有个高频坑:一定要先装好GPU版PyTorch,再执行requirements.txt。因为YOLOv5的requirements里包含torch依赖,先装它很容易被pip装成CPU版,前面配的GPU环境直接白费。先装GPU版之后,requirements检查到torch已满足会自动跳过。
提示:如果网络访问官方源慢,普通依赖可以换国内PyPI镜像(pip install -i),但PyTorch官方GPU wheel建议还是走官方源,避免拿到不完整版本。
3. 数据准备:把公开骨龄数据集改造成YOLOv5能训练的格式
3.1 RSNA 2017骨龄挑战赛数据基础
做骨龄深度估计,最常用的公开数据是RSNA 2017骨龄挑战赛的左手X光片,训练集12595张,每张带两个标注:boneage(骨龄,单位是月)和male(性别)。数据集本身面向回归任务,图像基本居中,但没有手部检测框标注,检测器的标签得自己造。使用医学影像数据前先确认来源合规;如果是医院合作数据,必须走伦理审批并做患者信息脱敏。演示和毕设用公开集完全够,不需要碰真实临床数据。
3.2 半自动生成手腕检测框
给上万张图手动画框不现实。标准做法是先写脚本自动生成初框,再人工微调。X光片里骨骼区域明显偏亮,先做Otsu阈值拿高亮前景,再取最大连通域的边界框:
import cv2 def auto_bbox(path): img = cv2.imread(path, cv2.IMREAD_GRAYSCALE) _, th = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) contours, _ = cv2.findContours(th, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) x, y, w, h = cv2.boundingRect(max(contours, key=cv2.contourArea)) mx, my = int(w * 0.05), int(h * 0.05) # 外扩5%防止截到指尖腕骨 return max(0, x - mx), max(0, y - my), x + w + mx, y + h + my自动框偶尔会把铅字标记当目标,把这些图用LabelImg打开手工改一遍。数据量不用贪多,我最后用1200张带框图训练检测器已经很稳。标注时框可以画松一点,关键是完整包住手部,不要为了贴合把腕骨切掉。
3.3 转YOLO格式与按患者划分
YOLOv5的标签是每张图一个同名.txt,每行格式为class_id x_center y_center width height,四个坐标除以图像宽高做归一化。写转换脚本时最容易错的是坐标用成了左上角格式,训练时loss会异常震荡,建议转换后写个检查函数逐张校验。数据集划分要按患者ID切,同一个患者不能同时出现在训练集和验证集,否则评估虚高,部署到真实场景就露馅。
3.4 医学图像的增强要克制
YOLOv5自带增强直接可用,旋转、平移、缩放对骨骼检测友好,但HSV颜色增强一定要调低。X光片的灰度分布本质上就是诊断信息,颜色扰动过猛等于把骨骼纹理特征洗掉。我是把hyp里的hsv_h、hsv_s、hsv_v直接调到接近0,训练明显更稳。
4. 训练实战:手部检测器与骨龄回归器分别怎么调
4.1 手部检测器:一份配置文件加一条命令
在YOLOv5仓库下建boneage.yaml:
path: ./datasets/boneage train: images/train val: images/val nc: 1 names: ['hand']启动训练:
python train.py --img 640 --batch 16 --epochs 80 \ --data boneage.yaml --weights yolov5s.pt \ --device 0 --patience 15几个参数的解释:yolov5s.pt是COCO预训练权重,用它做迁移学习,目标域虽是医学X光片,预训练特征依然能显著加速收敛;--img 640保持默认输入;--batch 16在8GB显存卡上没问题,不够就降到8;--patience 15表示验证指标连续15个epoch不涨就早停。实测GTX 3060约45分钟收敛到mAP@0.5接近0.98。手部检测对YOLOv5几乎没有挑战,难的全在数据标注质量。
4.2 骨龄回归网络:ResNet主干加性别辅助
第二段才是核心。任务定义是输入裁剪后的手部X光图,输出0到240个月的月龄,本质是回归问题。网络最后一层是单节点输出,损失函数用SmoothL1,评估指标用MAE(月)。主干用ResNet34足够。我额外加了性别辅助输入:把male标签做embedding后在全局池化层和图像特征拼接,再进全连接。原因很直接,男女骨成熟节奏差异大,同样骨化程度对应不同月龄,性别先验能明显降低MAE,RSNA挑战赛顶尖方案也这么做。
预处理上,灰度图先做直方图均衡化再resize到224x224。不同设备拍的X光片整体亮度对比度差异很大,均衡化能抹平这部分域差异,让模型专注学骨骼形态。训练50个epoch大约1到2小时,验证集MAE做到8~10个月。跟顶尖团队4~5个月有差距,但作为工程演示系统足够说明问题。
4.3 先把推理链路跑通再做界面
训练完两个模型,强烈建议先写脚本把完整推理跑通,再动界面:
import torch device = torch.device('cuda:0') det = torch.hub.load('./yolov5', 'custom', path='best_det.pt', source='local') reg = torch.load('best_reg.pth', map_location=device) reg.eval() for *xyxy, conf, cls in det(img_path).xyxy[0].cpu().numpy(): x1, y1, x2, y2 = [int(v) for v in xyxy] crop = img[y1:y2, x1:x2] crop = preprocess(crop) # 直方图均衡化、resize、归一化 age_month = reg(crop.unsqueeze(0).to(device)).item()torch.hub.load这里用source='local',直接从本地仓库加载,避免每次联网检查版本。这条链路一旦通了,后面做界面就是纯粹的UI拼装。
5. PySide6桌面客户端:把两段模型串成可交付的软件
5.1 界面功能规划
桌面端的核心是医生能直接上手。主界面左侧放原图显示区,右侧结果面板展示检测框坐标、预测骨龄(按月龄和岁数同时显示)、性别和推理耗时,底部放打开图片、开始检测、批量检测按钮。检测框用不同颜色在原图上画出,让用户直观看清YOLOv5框在哪,不只是给一个数字。工具型软件界面越简单越好,信息层级要一眼看懂。
5.2 用QThread异步推理,避免界面卡死
新手最常见的问题是直接在按钮回调里跑模型推理。单张图推理虽然只有一两百毫秒,加上图像解码和预处理,界面依然会明显"未响应"。正确做法是把推理放独立线程,用QThread加Worker对象:
from PySide6.QtCore import QObject, Signal class InferWorker(QObject): result_ready = Signal(dict) def __init__(self, det, reg): super().__init__() self.det, self.reg = det, reg def run(self, img_path): age_month = infer(self.det, self.reg, img_path) self.result_ready.emit({'age_month': age_month, 'path': img_path})注意QThread对象要保存为成员变量,否则会被Python垃圾回收,线程跑到一半消失,这个坑很隐蔽。
5.3 模型加载与显示细节
模型只在MainWindow初始化时加载一次,全局复用,别每次点按钮都重新读权重。加载后设置eval模式,推理包在torch.no_grad()里,能省不少显存和时间。显示X光片建议用QImage的Format_Grayscale8直接渲染灰度图,不要转成RGB再显示以免颜色失真。画框可以用cv2.rectangle画好再转QImage;如果用QGraphicsView做大图缩放显示,记得处理坐标换算,否则缩放后框和图像对不上。
按钮和Worker之间用signal/slot连接:点击开始检测按钮时,把图片路径发给Worker线程的run方法,Worker跑完通过result_ready信号回到主线程更新界面。这里有个细节,如果YOLOv5在图上没有检测到任何目标,要给出明确提示而不是抛异常,我在系统里会弹出"未检测到手部区域,请确认图像为正位左手X光片"的提示。批量检测时最好加一个进度条,遍历文件夹的同时不断emit进度信号,用户体验会好很多。到这一步,一个"打开图片、点击检测、读出骨龄"的桌面软件就成型了。
6. 实测效果与高频踩坑记录
6.1 实测数据
整套系统在RTX 3060上的实测结果:
| 项目 | 数值 |
|---|---|
| 手部检测器mAP@0.5 | 0.98 |
| 骨龄回归MAE(验证集) | 8~10个月 |
| 单张纯GPU推理耗时 | 40~60ms |
| 端到端单张(含界面加载) | 约0.8~1.2s |
作为初筛工具,这个精度已经能把明显正常的孩子先筛掉,让医生集中精力看异常片。
6.2 高频踩坑记录
| 现象 | 原因 | 解决办法 |
|---|---|---|
| torch.hub.load每次联网 | 默认拉取线上最新代码 | 用source='local'指定本地仓库 |
| pyside6-designer无法识别 | Scripts目录未进PATH | 在site-packages/PySide6下找designer.exe直接运行 |
| 高DPI屏上图像模糊 | Qt缩放策略不合适 | 程序入口开启Qt.AA_EnableHighDpiScaling |
| 批量检测显存暴涨 | 每张图新建tensor未释放 | 复用同一模型实例,循环内包torch.no_grad() |
| 检测框框住铅字标号 | 阈值分割误判高亮标记 | 自动框脚本过滤最大连通域,并人工复核 |
| PyInstaller打包后模型路径失效 | 打包后资源路径变成临时目录 | 用sys._MEIPASS拼接路径,把权重外置为资源 |
这几个坑里,铅字标记那个最典型。X光片上常用铅字标记左右侧,灰度值和骨骼一样高,Otsu经常把它当目标。我后来在自动标注脚本里只保留面积最大的高亮连通域,再人工补漏,基本解决了。
6.3 还能往哪些方向扩展
想做成更完整的作品,有三个现成方向:一是把YOLOv5从检测整个手升级为检测手腕关键骨化中心区域,对每个区域做TW分期分类,输出骨龄可解释性更强,医生更容易接受;二是把模型导出ONNX,用NCNN或RKNN部署到树莓派这类边缘设备,做成低成本门诊终端;三是界面加医生复核流程,算法出初稿、医生改终稿并留痕,更贴近真实医疗软件规范。
最后聊一点做完这个项目的体会。整套系统里花时间最多的不是训练,而是数据整理和界面集成。检测器训练半天就收敛,但数据转换脚本、标注工具调优、手工校对框,加上两段模型联调,占掉整个项目一大半时间。模型虽为核心,工程才是大头,这种体验在垂直领域视觉项目里特别典型。另一个实用建议是:无论做毕设还是产品原型,先把一条最小推理链路跑通,再回头补数据和界面,整体节奏会稳很多。
本文还有配套的精品资源,点击获取