拓冰建站拓冰建站
首页 / 资讯中心 / 正文

基于YOLOv5的骨龄检测实战:从数据预处理到模型推理全攻略

简介基于Python与YOLOv5实现的骨龄检测项目面向毕业设计、课程设计与项目开发场景。项目围绕手骨影像中的骨龄评估任务整合了源码、项目文档、数据模型、数据集和权重文件可支撑从环境配置、模型训练到推理评估的完整链路源码经过严格测试适合直接在此基础上延展使用。资源包共25个文件以20个Python脚本为主体涵盖模型构建、损失计算、数据增强、工具函数等模块另附Markdown说明文档、requirements依赖文件与YAML/TOML配置方便快速还原运行环境和梳理项目结构。压缩包整体仅95KB轻量、整洁便于本地部署与二次开发。目前已有107人学习下载。通过项目文档可以了解数据预处理、训练流程和骨龄预测的关键步骤权重文件也能帮助快速启动识别效果演示无论用于课程汇报、毕设答辩还是YOLOv5应用入门都是一份贴合实际任务的实用参考。1. 骨龄检测为什么选 Python YOLOv5一次把毕设、课设和真实项目都打通的路线骨龄检测Bone Age Assessment是儿科内分泌和法医学里最常见的影像读片任务拍一张左手腕 X 光正位片对照标准图谱估算骨骼成熟度用来判断生长潜力、辅助诊断性早熟或生长迟缓。传统做法靠医生肉眼比对 GP 图谱主观性强、耗时长所以这几年几乎所有人都把目光投向深度学习而 YOLOv5 恰好是这个方向里最稳的起步选择。它不负责直接预测骨龄而是负责从整张片子里快速定位手部或关键骨骼区域把在哪儿看和读出多少岁拆成两级任务既降低了模型学习难度也让整套系统在答辩时能演示、能讲清楚、能出量化指标。适合的人很明确需要完成医学影像类课程设计或毕业设计的在校生以及刚接触医学 AI 想最快跑通全流程的开发者。2. 先搞清楚 YOLOv5 在骨龄任务里的真实位置检测 ROI不是回归年龄2.1 骨龄评估到底在评什么GP 图谱法与 TW3 法的核心逻辑骨龄不是看手有多大而是看骨骼发育的成熟度。医生读片时盯的是桡骨远端、尺骨远端、8 块腕骨、以及各节指骨的化骨核出现顺序、骨骺与骨干之间的愈合程度、关节面形态变化。不同年龄段关注的部位完全不同0 到 7 岁主要看腕骨化骨核出现了几块7 到 14 岁看骨骺宽度和骨化范围14 岁以后看骺线是否闭合。这就是为什么直接拿整张图丢给一个 CNN 做回归虽然能跑出数字但很难解释模型到底看了哪里。GP 图谱法的本质是把整只手的成熟度浓缩成一张标准图谱医生凭经验对照TW3 法则更细把 13 个 ROI 分别评分再加权求和。无论哪种方法前提都是先定位到具体的骨骼区域。这给了目标检测一个非常自然的切入点与其让模型端到端黑匣子输出不如先用 YOLOv5 把关键区域框出来再让后续的骨龄网络专注学骨骼形态。这个思路不是我的发明当年 RSNA 骨龄挑战赛的头部方案也走了先分割手部区域再用 CNN 回归的路线本质同构。2.2 两阶段方案YOLOv5 负责检测框骨龄网络负责读数常见做法是把任务拆成两段。第一阶段用 YOLOv5 检测 ROI最省事的做法是只检一类整只左手。训练简单、标签好做、稳定性高下游把框内区域裁剪出来送给骨龄分类网络即可。进阶做法是检测多个 ROI比如 0 表示桡骨远端、1 表示尺骨远端、2 表示腕骨区、3 表示各节指骨这样做的好处是能与 TW3 评分法一一对应缺点是需要人工标注几百张片子工作量明显上升。我一般建议以单类手部检测为基线先把整条链路跑通如果时间充裕再升级多类。因为 YOLOv5 在这一级任务里只是个定位器它的输出质量只看框准不准、稳不稳不需要像检测小目标那样追求极致。框稍微偏几个像素对下游骨龄网络的影响很小但框如果漏检或把背景大片包进来裁剪出的图像就会引入大量噪声骨龄准确率立刻垮掉。所以两阶段方案里YOLOv5 的验收标准不是 mAP 好看而是它能不能稳定输出一个紧贴手部轮廓的矩形框。有人会问为什么不直接用 YOLOv8 或者干脆用关键点检测YOLOv8 同样可以做但 YOLOv5 的生态更成熟从环境配置、教程数量到踩坑记录都更全对课程设计和毕业设计来说遇到问题时能搜到的参考最多这是实打实的优势。关键点检测方案比如定位 21 个手部关键点也很有价值但标注成本高得多而且骨龄评估依赖的是骨骼内部的成熟度特征不是手型轮廓关键点提供的信息在这条任务里不够用。2.3 标签设计为什么按月分类比按岁回归更稳骨龄数据集里最常用的标签来自 RSNA Pediatric Bone Age Challenge样本是左手腕 DICOM 影像标签只有两个关键字段性别和骨龄以月为单位范围 0 到 228 个月。注意这套公开数据集本身没有提供任何检测框YOLOv5 所需的标注需要自己生成这一点后面专门讲。骨龄网络的设计上把月龄当成 229 类分类问题比直接回归一个连续值更稳定。原因很简单相邻月龄的 X 光片差异非常小回归模型很容易被个别异常样本带偏而分类模型天然具备概率输出可以进一步做平滑。推理时不要用 argmax 取整数值而是对 softmax 概率做加权求和得到连续的期望月龄效果会顺滑很多。性别信息必须参与建模因为同月龄的男孩女孩骨骼发育差异明显尤其是在青春期前后。常见做法是训练两个性别模型或者把性别向量拼进骨龄网络的特征层。PyTorch 里实现期望月龄的计算非常直接模型输出 229 维 logitssoftmax 后与 0 到 228 的月份向量做点积import torch import torch.nn.functional as F def expected_bone_age(logits): prob F.softmax(logits, dim1) # [B, 229] months torch.arange(0, 229, dtypetorch.float32, devicelogits.device) # 连续月龄向量 return (prob * months).sum(dim1, keepdimTrue)这里把分类输出转成连续预测比直接取 argmax 更平滑也能让 MAE 指标更好看。训练骨龄网络时损失函数可以用 CrossEntropyLoss也可以对标签做高斯平滑把真实月龄前后 6 个月的类别都赋予一定权重因为相邻月龄在视觉上本来就很接近硬编码成 one-hot 反而会让模型过度自信。3. 把骨龄 X 光片变成 YOLOv5 能训练的数据集DICOM 转换、伪标签与人工抽检3.1 RSNA 骨龄数据集的结构拿到手先做什么整个项目的数据基础来自 RSNA Pediatric Bone Age Challenge在 Kaggle 上可以下载包含训练集约 1.2 万张左手腕 X 光片格式是 DICOM 或已转换的 PNG配套一个 CSV 标签文件每一行对应一个病例 ID、性别和骨龄月龄。有一个关键事实要先说清楚这个数据集只给了骨龄和性别没有给任何检测框。网上有些教程会让你直接训练 YOLOv5但如果你眼尖一点就会发现官方并没有提供 YOLO 格式的 labels 目录所有框都要自己造。拿到数据后先做三件事解压并确认目录结构、把 CSV 读进来检查年龄和性别分布、抽样看几张影像的质量。年龄分布特别重要RSNA 数据在低年龄段样本偏少如果后续按性别分模型会发现某些年龄段的样本量非常紧张需要做数据增强或干脆合并年龄段。先写一段脚本把标签文件读进来看看样本分布import pandas as pd df pd.read_csv(boneage_train.csv) df[bone_age_months] df[bone_age].astype(int) df[is_male] (df[male] True).astype(int) print(df.head()) print(df.groupby(is_male)[bone_age_months].describe())这段代码的作用是快速确认数据字段和男女样本量。RSNA 的 CSV 里性别列是布尔字符串需要转成 0/1 整数后续做数据划分和训练时直接用这个字段。3.2 DICOM 转 PNG位深、窗口与对比度是第一个坑DICOM 格式本身不是普通图片它的像素可能是 16 位有符号整数直接转成 8 位 PNG 会丢失大量信息甚至出现一片死白或死黑。正确做法是按分位数做截断把像素值 1% 到 99% 分位之间的范围线性拉伸到 0 到 255。这个操作本质上就是模仿放射科医生调节窗宽窗位的过程。import pydicom import numpy as np import cv2 def dicom_to_png(dcm_path, out_path): ds pydicom.dcmread(dcm_path) img ds.pixel_array.astype(np.float32) # 用 1%/99% 分位截断去除背景和金属异物造成的极值 lo, hi np.percentile(img, (1, 99)) if hi lo: hi lo 1.0 img np.clip((img - lo) / (hi - lo), 0.0, 1.0) img (img * 255.0).astype(np.uint8) cv2.imwrite(out_path, img)这里有两个参数值得留意。分位数选 1% 和 99%是因为 X 光片背景占比大且像素值接近 0顶点处的金属异物可能产生极高亮噪点直接 min-max 归一化会让骨骼区域压暗。如果发现手部骨骼对比度仍然不够可以在截断后加一次 CLAHE 增强但要注意训练和推理时保持同一套预处理否则模型在真实场景会翻车。另外处理 DICOM 前最好检查一下 PhotometricInterpretation 字段某些设备输出的图像是反色的需要取反再保存不然手部是黑的、背景是白的所有基于灰度分布的伪标签逻辑都会失效。3.3 自动生成 YOLOv5 伪标签阈值分割与最大连通域RSNA 没有检测框但我有一个低成本方案左手腕 X 光片的背景在正确窗口化后几乎是纯黑手部是显著高亮区域所以可以用阈值分割加最大连通域来自动生成整只手的包围盒。这个伪标签训练 YOLOv5 是完全够用的因为单类检测只需要手部的大致位置不需要精确到骨骼边缘。import cv2 import numpy as np def auto_hand_bbox(img): 自动生成手部外接矩形返回 [x1, y1, x2, y2] # 背景接近 0手部像素明显偏亮 _, th cv2.threshold(img, 30, 255, cv2.THRESH_BINARY) # 闭运算把手指之间的小缝隙连起来避免轮廓断裂 ksize max(15, img.shape[1] // 60) kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (ksize, ksize)) th cv2.morphologyEx(th, cv2.MORPH_CLOSE, kernel) contours, _ cv2.findContours(th, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return None # 取面积最大的轮廓通常就是手部 c max(contours, keycv2.contourArea) x, y, w, h cv2.boundingRect(c) return [x, y, x w, y h]这个函数有两个参数值得反复调。阈值 30 不是拍脑袋定的RSNA 片子在 1%/99% 分位归一化后背景通常集中在 0 到 10 附近手部骨骼起始于 40 以上如果发现框把手臂或大片黑影包进来优先调高阈值而不是改核大小。闭运算的核大小我按图像宽度除以 60 计算这样在不同分辨率下行为一致。生成完所有伪标签后要抽检一般是每 100 张里人工看 10 张把明显框偏的样本单独挑出来手工修正。3.4 转换 YOLO 标签格式与数据集划分避免同人不同图泄漏YOLOv5 的标签格式是纯文本每行一个目标内容为 class, x_center, y_center, width, height全部归一化到 0 到 1。这里最容易出错的点有两个一是坐标必须归一化二是越界保护必须有。RSNA 的图存在少量手部顶到边缘的情况外接矩形可能超出图像边界必须裁回 0 到 1 范围内。def make_yolo_label(bbox, img_w, img_h): 像素坐标 bbox: [x1, y1, x2, y2] - YOLO 归一化标签 x1, y1, x2, y2 bbox # 越界保护防止矩形超出图像范围 x1 max(0, min(x1, img_w - 1)) y1 max(0, min(y1, img_h - 1)) x2 max(0, min(x2, img_w - 1)) y2 max(0, min(y2, img_h - 1)) cx ((x1 x2) / 2) / img_w cy ((y1 y2) / 2) / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h return f0 {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}\n在生成标签的同时要把训练集和验证集分开。这里有一个容易被忽略的泄漏问题RSNA 数据集里同一个患者可能存在多张片子如果直接随机划分文件同一患者的片子可能同时出现在训练和验证集里导致验证指标虚高。正确做法是先按 patient_id 分组再按组划分确保同一患者的所有影像要么全在训练集要么全在验证集。划分比例 8:2 就够然后在数据集根目录下建立 images/train、images/val、labels/train、labels/val 四个目录并写一个 boneage.yaml 指向它们。4. 用 YOLOv5 训练骨龄检测模型官方仓库、自定义数据集与关键参数4.1 从环境到最小训练命令照着跑就能出结果YOLOv5 官方仓库是最省事的起点。用 conda 创建独立环境Python 版本选 3.8 或 3.9 都行装好依赖后克隆官方仓库然后直接开训。网上大量教程会让人在 VSCode 里反复折腾 Python 环境配置实际上只要 conda 环境干净官方 requirements 一次装齐训练命令敲进去就能跑。conda create -n boneage python3.9 -y conda activate boneage git clone https://github.com/ultralytics/yolov5.git cd yolov5 pip install -r requirements.txt python train.py \ --data ../datasets/boneage.yaml \ --weights yolov5s.pt \ --img 1024 \ --batch 16 \ --epochs 100 \ --name boneage_yolov5s这条命令里 --img 1024 和 --batch 16 是按 12GB 显存估的如果你的显卡只有 6GB把 batch 降到 8 或 4再把 --workers 调小否则会直接显存溢出。--weights 指定 yolov5s.pt 预训练权重这一步相当于让模型从 COCO 学到的特征出发做迁移学习比从零初始化收敛快得多也稳得多。4.2 四个必调参数imgsz、batch、epochs 与 anchors第一个参数是 imgsz也是影响结果最大的一个。YOLOv5 默认 640但骨龄 X 光片的分辨率普遍很高手部骨骼细节密集尤其是腕骨区域8 块小骨挤在一起640 分辨率下特征已经糊成一片。提高到 1024 通常能带来明显的 mAP 提升有条件上 1280 效果更好代价是训练时间翻倍。我建议直接用 1024 起步显存不够就降低 batch。第二个参数是 batch。它的意义不只是训练速度还直接影响 BatchNorm 统计量的稳定性。小数据集上 batch 开太大容易过拟合开太小又会让 BatchNorm 抖动。12GB 显存、imgsz1024 时 batch16 是个甜点值如果是 6GB 卡就老实降到 8或者把 imgsz 降到 960。第三个参数是 epochs。骨龄检测任务里训练集只有一万多张YOLOv5 一般在 60 到 80 个 epoch 就收敛了设 100 配合早停机制比较稳妥。YOLOv5 默认会保存 last.pt 和 best.pt如果连续多轮验证集指标不再提升可以手动停掉不必等满 100 轮。第四个参数容易被忽略anchors。YOLOv5 默认的 anchor 尺寸是 COCO 数据集上聚类出来的适合普通物体。骨龄 X 光片上的检测目标是手部或骨骼区域宽高比非常特殊手部框通常是长条形或接近方形直接用默认 anchor 不是不能用但收敛会慢。训练时可以加 --noautoanchor 关闭自动聚类也可以先跑一轮完整的训练然后读取模型权重里的 anchor 分布用 k-means 重新聚类再手动写进模型配置里。这道工序对单类手部检测影响不大但对多类骨骼检测帮助明显。4.3 训练完看什么best.pt 之外的指标才是真正的判断依据训练结束后YOLOv5 会在 runs/train/boneage_yolov5s/ 下生成 weights/best.pt、confusion_matrix.png、val_batch0_pred.jpg 等文件。很多人只盯着 best.pt 拿去做推理忽略了对验证集可视化结果的检查这是一个大坑。首先看 val_batch*_pred.jpg这是验证集预测框直接画在原图上的结果。重点检查三点框是否紧贴手部边界、有没有把背景大块包进来、有没有漏检。对单类手部检测来说IOU 稍微偏低可以接受但框的稳定性必须好如果同一个人在不同图上框的位置忽大忽小下游裁剪模块会很不稳定。其次看 P/R/mAP 的组合。mAP50 对骨龄任务参考价值有限因为两步方案的后续网络只需要一个语义准确的框mAP50-95 能到 0.7 以上基本够用。更实用的指标是置信度分布跑验证集时统计不同置信度区间下的检测数量如果大量框集中在 0.9 以上说明模型对这类图非常自信推理时可以放心把 conf-thres 设到 0.5如果分布比较散就保持 0.25。最后做一次真实图推理别只依赖验证集。用训练时没见过的 X 光片跑一遍 detect.py确认模型输出的框在真实条件下不会漂移python detect.py \ --weights runs/train/boneage_yolov5s/weights/best.pt \ --source ../datasets/boneage/samples \ --img 1024 \ --conf-thres 0.25 \ --save-txt检测结果会存在 runs/detect/exp 目录下包括带框的原图和每个框的归一化坐标。对这些坐标做一次统计看看宽高比和面积的分布是否稳定如果出现极端宽高比多半是标注或预处理环节有问题。5. 骨龄检测避坑指南4 个高频翻车点的现象、原因与解决方案5.1 翻车点一伪标签把整只手连同大片背景一起框住现象训练出的模型预测框特别大把黑色背景大量包含进来裁剪后的图像里手部只占中间一小块。原因分析下来有两个一是阈值分割时阈值设得太低把手臂旁的低灰度阴影也并进了前景二是闭运算核尺寸偏大手指缝隙被全部填死导致轮廓外扩严重。解决办法也直接把阈值从 30 提高到 Otsu 自动计算值同时把核大小减少到原来的三分之一。判断标准很简单框的宽度除以手部区域宽度应该在 1.2 倍以内超过这个范围就要重新生成标签。5.2 翻车点二DICOM 转换后骨骼区一团死白训练 loss 完全不降现象训练了二十多个 epoch验证集 mAP 还在 0.1 附近打转把训练图打印出来一看骨骼区域全是白斑皮质骨和松质骨根本分不出来。原因是 16 位 DICOM 像素直接按 min-max 拉伸而影像里存在少量极高亮噪点把整幅图的灰度范围全部压扁了。解决方法是改用分位截断同时检查 DICOM 的 PhotometricInterpretation如果输出是 MONOCHROME1 需要先取反。最关键的一点是DICOM 转换脚本和推理时的预处理必须完全一致否则训练时模型学到的对比度特征在推理时全部失效这块是很多项目翻车最严重的地方。5.3 翻车点三男女混合训练青春期样本误差飙到 2 岁以上现象整体测试集 MAE 在 12 个月左右看起来还能接受但按年龄段一分组就露馅了12 到 16 岁区间的 MAE 超过 25 个月。原因是男孩女孩在青春期的骨骼发育节奏差异极大同一个月龄的男孩和女孩骨化程度可能相差一到两个年龄段单一模型为了兼顾两者反而在青春期这个关键区间谁也没学好。解决方法是把性别信息纳入模型最简单粗暴的是按性别训练两个独立模型男骨龄模型只在男性数据上训练女性模型反之进阶做法是在骨龄分类网络里把性别编码成一个向量拼进特征层让同一模型内部学会区分。评估时也要按性别分组报告 MAE而不是只报一个总平均。5.4 翻车点四腕骨区密集小目标严重漏检现象单类手部检测一切正常但升级到多类骨骼检测时桡骨和尺骨都能框出来唯独腕骨区经常只出一两个框其他腕骨全部漏检。原因很单纯腕骨区域 8 块小骨彼此紧挨形态相似在 1024 分辨率下已经逼近 YOLOv5 检测能力的下限。解决策略有三个优先级从高到低排列第一升级检测策略对手腕区域做一次粗检测后裁剪放大再在放大的局部图上跑细检测相当于两级检测这个方案效果最明显第二把 imgsz 从 1024 再提到 1280但对显存要求较高第三对腕骨类单独标注并单独训练一个检测器避免与其他骨骼类别互相干扰。注意不要指望单纯调低 conf-thres 能解决漏检漏检根因是特征分辨率不足不是置信度阈值问题。6. 两级联动的完整推理链路从 YOLOv5 检测框到最终骨龄值把两段模型串成一条完整的推理管线是让整个项目从训练完变成能用的关键一步。管线逻辑很清晰先读入一张左手腕 X 光片做与训练时完全相同的 DICOM 预处理然后把图送进 YOLOv5 检测手部框按置信度过滤接着把框内区域裁剪出来缩放到骨龄网络要求的尺寸最后骨龄网络输出 229 个月龄的概率分布用期望值公式得出预测骨龄。import cv2 import torch import torch.nn.functional as F def infer_bone_age(image, detect_model, age_model, devicecpu): results detect_model(image) # YOLOv5 检测手部 boxes results.xyxy[0].cpu().numpy() # [x1, y1, x2, y2, conf, cls] if len(boxes) 0: return None # 没检到手部直接放弃 # 取置信度最高的框向外扩 30 像素避免切掉骨骼边缘 x1, y1, x2, y2, conf, cls boxes[0] m 30 h, w image.shape[:2] x1 max(0, int(x1) - m); y1 max(0, int(y1) - m) x2 min(w - 1, int(x2) m); y2 min(h - 1, int(y2) m) roi image[y1:y2, x1:x2] roi cv2.resize(roi, (224, 224), interpolationcv2.INTER_AREA) roi torch.from_numpy(roi).float().unsqueeze(0) / 255.0 logits age_model(roi) prob F.softmax(logits, dim1) months torch.arange(0, 229, dtypetorch.float32) age_months (prob * months).sum().item() return age_months推理效果验证上我习惯把结果可视化把检测框画在原图左上角预测骨龄和真实骨龄标在图上生成一组对比网格图这个素材对答辩非常有用。指标层面测试集整体 MAE 是一个数分性别、分年龄段的 MAE 折线图更有说服力能突出模型在哪个年龄段表现好、哪个年龄段还有短板。如果想把项目再往工程方向推可以把 YOLOv5 的 best.pt 导出 ONNX在 ONNX Runtime 里做推理之后上树莓派这类边缘设备也就没有门槛了。回看整个方案YOLOv5 在骨龄检测里的角色被很多人误读成直接预测骨龄的模型实际上它是整个系统中的定位器真正出骨龄结果的是后面的分类网络。把它放在正确的架构位置上整个项目的可解释性、可演示性和答辩说服力都会提升一个台阶。我自己做这个方向时最后悔的是没有在一开始就把 DICOM 转换和伪标签生成封装成独立模块导致后期调参时反复改预处理逻辑。如果你也走这条路把这几个环节先固化后面会省很多事希望帮到你。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门