资讯详情

基于YOLOv5的骨龄检测:数据集制作、训练调参与避坑指南

📅 2026/10/7 19:10:02 | 华诺云谱 👁 阅读
基于YOLOv5的骨龄检测:数据集制作、训练调参与避坑指南
简介一套基于Python与YOLOv5实现的骨龄检测完整项目面向需要完成毕业设计、课程设计或相关课题开发的计算机视觉学习者。资源覆盖从模型训练、数据预处理到推理部署的完整链路内含可运行源码、项目说明文档、数据模型、数据集与预训练权重源码已经过严格测试可在此基础上拓展定制降低二次开发门槛。压缩包共25个文件以20个Python脚本为主体涵盖模型定义、训练工具、通用模块等核心逻辑另配套Markdown说明文档、环境依赖清单及YAML/TOML配置文件整体约95KB轻量且结构紧凑。目录按模型、工具、配置等分层组织便于快速定位所需代码整体麻雀虽小、五脏俱全非常适合作为课题起步模板。目前已有107人学习参考适合作为骨龄检测方向的选题范本、功能扩展基底或答辩演示基础。1. 骨龄检测为什么选择YOLOv5这个项目到底在检测什么骨龄检测是儿科和放射科最常规的检查之一拍一张左手X光片医生数腕骨、掌骨、指骨的骨化区成熟等级再对照标准图谱换算成骨龄。这个流程依赖医生的经验和耐心而基于pythonyolov5实现的骨龄检测本质就是把“数骨化区”这一步交给目标检测模型先用YOLOv5定位并分类多个骨化关键区再按标准计分规则或轻量回归头得到骨龄值。相比端到端的CNN回归这种“检测-评分”两段式做法精度可控、过程可解释也方便在毕业设计和课程设计里把每个环节单独展示。如果你手头有源码但不知道怎么改数据、调参数或者想自己从数据集开始复现这篇就是照着做的完整路径。2. 骨龄检测数据集的准备左手X光片、关键骨化区标注与转换脚本2.1 骨龄检测要“看”什么TW3计分区与YOLO检测目标的映射骨龄检测不是把整只左手当作一个目标来分类而是要在左手X光片里找到若干骨化关键区再根据这些区域的成熟度给出骨龄。临床常用的TW3法把左手腕分成桡骨远端、尺骨远端、腕骨、掌骨近端、指骨近端等计分区每个区按成熟度分成若干等级。用YOLOv5做这个任务就是把计分区当作检测框的类别来训练。目标数量不多常见7~13个但框的大小差异很大桡骨尺骨的框细长腕骨的框小而密集。这个特点直接影响anchors的配置后面会专门说。TW3法的核心假设是手腕部各骨化区的出现顺序和形态变化有较强的时间规律桡骨和尺骨的骨化形态在青春期变化最快腕骨在低龄段更有判别力。所以不同骨化区在不同年龄段的权重并不一样这也是为什么后处理里按区域加权平均往往比简单平均更贴近临床判断。公开数据方面RSNA 2017骨龄检测挑战赛数据集是从NIH临床影像库脱敏来的包含上万张左手X光片和对应的骨龄标签是AI骨龄方向最常被引用的数据来源之一。但要提醒一点这份数据自带的是整图骨龄标签并不直接给骨化区的检测框。要做“检测-评分”两段式要么自己标关键区要么先找带ROI标注的腕骨数据集跑通流程再回头处理RSNA。课程设计想快建议先用已标注数据的子集做原型别一上来就标上万张。2.2 把COCO或VOC标注转成YOLOv5格式转换脚本与目录结构YOLOv5的标签格式是“类别ID加四个归一化坐标”一行一个目标完整写法是 class x_center y_center w h。你在网上下到的源码包里最常碰见的就是COCO格式的JSON标注。转换脚本可以从零写逻辑并不复杂# coco2yolo.py —— 把COCO标注转成YOLOv5训练用的txt标签 import json import os def coco_to_yolo(coco_json, out_dir, default_img_size1024): with open(coco_json, r, encodingutf-8) as f: coco json.load(f) os.makedirs(out_dir, exist_okTrue) # COCO的category_id是稀疏的必须重映射成0..N-1 cat_map {c[id]: idx for idx, c in enumerate(coco[categories])} img_map {img[id]: img for img in coco[images]} ann_map {} for ann in coco[annotations]: ann_map.setdefault(ann[image_id], []).append(ann) for img_id, anns in ann_map.items(): img img_map[img_id] w img.get(width, default_img_size) h img.get(height, default_img_size) txt_name os.path.splitext(img[file_name])[0] .txt lines [] for ann in anns: x, y, bw, bh ann[bbox] # COCO bbox 左上角x, 左上角y, 框宽, 框高 cx (x bw / 2) / w # 转成中心点坐标并归一化 cy (y bh / 2) / h nw bw / w nh bh / h lines.append(f{cat_map[ann[category_id]]} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}) with open(os.path.join(out_dir, txt_name), w) as out: out.write(\n.join(lines)) if __name__ __main__: coco_to_yolo(data/annotations/train.json, data/labels/train)这段转换逻辑有两个关键点第一COCO的bbox是“左上角坐标宽高”YOLOv5的格式是“中心点坐标宽高”不换算直接复制坐标是新手最容易翻车的地方第二类别ID必须重映射成从0开始的连续整数否则categories和names对不上训练的loss会异常玄学模型学完也分不清类别。default_img_size只在COCO的JSON里没写宽高时兜底实际数据最好用真实宽高否则框归一化后整体错位。转完后统一按YOLOv5约定放目录训练时直接写bone.yaml指向即可datasets/bone/ ├── images/ │ ├── train/ # 训练图片 │ └── val/ # 验证图片 ├── labels/ │ ├── train/ # 和图片同名同前缀的txt标签 │ └── val/ └── bone.yaml # 数据配置文件图片和txt文件的文件名必须完全一致包括大小写和后缀YOLOv5按文件名做匹配对不上就自动当成“没有标签的负样本”。训练验证集划分建议8:2数据少于500张时验证集可以降到5%但至少要留出验证集不然没法判断过拟合。如果你下载的源码包里自带转换工具也建议用上面脚本核对一遍因为网上不少打包的转换器写死了类别映射换数据集就错。转完之后不要急着训练先跑一个可视化GT的脚本把标注框画在原图上抽查20张。这一步能看出两个最常见问题框是不是整体偏移了半个骨头宽度类别ID是不是和names对调了。可视化不复杂用OpenCV读图、再按txt内容画矩形半小时能写完但它能避免你后面两三天都在跟一个错位标注较劲。网上很多“源码数据集”的包数据混乱的比例不低先画框检验再进训练是我每次拿到新数据集的第一件事。2.3 数据增强与切片小样本医学数据的两个补救办法医学影像数据集普遍不大左手X光片更是典型的“少量数据、高标注成本”。YOLOv5内置的mosaic增强在训练时把四张图拼成一张对小目标检测的提升明显。但它在骨龄场景里有副作用四张X光片拼接时手骨的指端和腕骨会被随机裁剪或拉伸骨化区的形态发生变化模型学到的框分布会偏移。我一般会把mosaic的概率从默认1.0降到0.6左右这个在data/hyps/hyp.scratch-low.yaml里改mosaic: 0.6就行。切片是另一个常用手段把高分辨率X光片按滑动窗口裁成若干子图用子图做检测这样能用更小的输入分辨率保留局部细节。切片要配套坐标还原逻辑否则同一骨化区会在原图坐标上被重复计数这一点在第5章有专门的坑。另外一个必须强调的是病历隐私即使项目只是课程设计也不要把带着患者姓名、检查号的DICOM原片直接丢进数据集。RSNA这类公开集已经做了脱敏自建数据务必先清理头信息再做标注。注意医学影像数据涉及患者隐私RSNA数据集已经做过脱敏但如果你自己从医院拿片子务必先去掉患者姓名、检查号等DICOM头信息再做任何标注和训练。3. 用YOLOv5训练自己的骨龄数据集模型选型、超参数设置与预训练权重选择3.1 yolov5s还是yolov5m先看显存预算和小目标密度用YOLOv5训练骨龄模型选哪个型号其实不是精度问题而是设备和目标问题。yolov5n和yolov5s适合8G显存的笔记本一个epoch几分钟适合毕设和课程设计反复试参yolov5m需要16G显存才跑得比较从容mAP比s高2~3个点但骨化区这种特征不算难的目标s通常已经能到90以上的mAP50。硬上大模型的代价是显存压力和推理延迟。而且骨龄检测对最终价值来说更重要的是骨龄误差不是检测框卡得有多准。如果你是本科毕设yolov5s是稳妥起步。如果最终要部署到rk3568这类边缘盒子s模型配合INT8量化是更现实的选择。骨龄检测并不需要追求检测框的像素级精度它需要的是“关键区找得全、类别分得清、坐标稳定”这几点s模型都够用。另外YOLOv5在训练时会根据标签框自动k-means聚类anchors骨龄数据的框形状跨度大桡骨尺骨的长宽比能到1:4腕骨接近1:1所以聚类出来的小尺度anchor通常不够密。可以先跑一个epoch看日志里打印的anchors结果如果最小尺寸的anchor明显偏大就在模型yaml里手工改小。3.2 训练命令与超参数落地imgsz、batch、epochs、hyp训练命令是这套项目里改动最频繁的入口。下面这条命令基于YOLOv5官方仓库骨龄数据按bone.yaml配置python train.py --data bone.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 150 \ --hyp data/hyps/hyp.scratch-low.yaml \ --patience 30 \ --seed 42 \ --device 0参数的选择逻辑按实际踩坑经验排个序--img 640是最安全的输入分辨率。骨化区在标准X光片里占的区域不大640已经把腕骨的细节保留得足够再往上提分辨率收益有限显存和速度却要付出明显代价。如果你的显存只有6G降到512也能跑mAP会掉1个点左右但训练速度更快。--batch以显存为准。8G卡用1624G卡可以到32或48但一般不建议超过64。骨龄数据量不大batch过大反而容易在验证集上更快过拟合。--epochs配--patience 30是最实用的组合设150的上限让早停机制在连续30个epoch没有改善时自动停保存最佳权重。这样即使你不在电脑前守着训练也不会一直无效跑下去。--hyp选hyp.scratch-low.yaml增强幅度比较温和。骨龄检测的图像构图是固定的左手永远是主体、背景统一、方向统一这和COCO那种复杂场景完全不同过强的增强反而会把解剖结构破坏掉。--seed 42是为了让两次训练可复现写论文画对比曲线时很有用。训练过程中有两处一定要盯的指标训练集loss曲线是否稳定下降以及验证集的mAP50和mAP50-95的差距。如果mAP50高但mAP50-95掉说明框定位不够准回头检查anchors和标注质量如果验证loss先降后涨说明过拟合把epochs调低或用早停。3.3 预训练权重怎么选COCO权重做迁移的边界条件yolov5s.pt用的是COCO预训练权重。很多新手觉得“X光片和COCO场景八竿子打不着”但其实backbone学到的是边缘、纹理、亮度对比这些通用表征它们能直接迁移到灰度的X光片。一个经常被忽略的细节是X光片本身是单通道灰度图而YOLOv5的输入是三通道把灰度图复制成三通道再送入网络是一种默认且有效的做法它保留了预训练权重的通道统计分布比拿一张真正的单通道图去喂网络效果好。但不要把目标检测里“冻结backbone前几层微调”的做法照搬到骨龄任务。自然图像和X光片的域差距太大冻结浅层会把模型锁死在自然图像的响应模式上导致训练后期loss卡住不动。我通常完全不解冻用0.001的初始学习率从头微调配合余弦退火效果更平稳。如果担心过拟合更有效的办法是降低数据增强强度而不是冻结网络层。注意下载yolov5s.pt时优先从官方release列表拿避免第三方打包的权重文件和代码版本不匹配。训练日志里如果出现“weight shape不匹配”的报错九成是权重文件被替换过。4. 从检测框到骨龄值回归头设计、分类输出与后处理4.1 方案一检测框特征接回归头YOLOv5检测完之后你手里有每个骨化区的类别、坐标、置信度但这些信息本身不足以推算出骨龄。要得到骨龄得把检测模型提取的特征接一个回归头。这里的核心难点是每张图检测到的目标框数量不一样特征张量的长度不固定不能直接拼起来喂全连接层。我一般用全局平均池化解决# bone_age_regressor.py import torch import torch.nn as nn class BoneAgeHead(nn.Module): 输入: 检测到的N个框的特征, shape [B, N, C] 输出: 每个样本的骨龄, shape [B, 1] def __init__(self, feat_dim256, hidden_dim64): super().__init__() # 先对每个框做特征压缩把每个框的语义信息压到64维 self.fc_boxes nn.Sequential( nn.Linear(feat_dim, hidden_dim), nn.ReLU(inplaceTrue), ) # 再把所有框的特征做全局平均池化回归一个标量 self.regressor nn.Sequential( nn.Linear(hidden_dim, hidden_dim), nn.ReLU(inplaceTrue), nn.Linear(hidden_dim, 1), ) def forward(self, box_features): # box_features: [B, N, C]N是每张图的目标框数量会变化 box_embed self.fc_boxes(box_features) # [B, N, 64] pooled box_embed.mean(dim1) # [B, 64] return self.regressor(pooled) # [B, 1]mean(dim1)把N个框的特征做平均得到的向量包含了“各个骨化区综合成熟度”的信息这和TW3评分里综合计分的直觉一致。如果你知道某些骨化区对特定年龄段更关键也可以在mean之前按类别加权而不是简单的平均。训练这个回归头时先冻结YOLOv5的backbone只训练这里的参数学习率调到1e-4量级用L1或L2损失回归骨龄。另一个常见的接入方式是把检测框在特征图上对应的区域抠出来也就是ROI Align然后逐框预测成熟度等级再汇总。这个方案更精细但实现复杂度高毕设时间紧的话全局平均池化的版本足够用了。4.2 方案二把骨龄“按月离散化”做成分类问题如果把骨龄按月离散化比如0~18岁对应216个类别骨龄检测就变成一个分类任务。RSNA挑战赛里高名次的方案大多走这条路线。分类输出的优势是抗噪相邻月份的混淆会被模型当作“接近但没完全分对”不会像回归头那样直接产生一个偏移很大的数值。最终读数是把softmax概率按月份坐标做加权平均得到的小数年龄连续性很好。# 从分类概率换算骨龄年 import numpy as np def prob_to_bone_age(probs, min_months0, num_classes216, step1.0): # probs: 长度num_classes的softmax概率 months np.arange(num_classes) * step min_months age_months np.sum(probs * months) # 概率期望值单位月 return age_months / 12.0 # 转成岁 # 示例训练时标签是“骨龄月数”编码成one-hot推理时用这个函数换回年num_classes决定了标签粒度按月是216类按两个月是108类按半岁是37类。粒度越细对数据量要求越高如果你的标注精度本身只有半岁硬上216类只会增加混淆。推算的加权求和环节不能用argmax否则输出只会是整数值和医生习惯的“岁月”表达不一致。训练时分类头用CrossEntropyLoss注意类别标签和这里的月份索引要对齐常见错误是训练用0~215的类标推理时却按1~216个月来解算偏差出整整一个月。4.3 后处理给模型输出做平滑和区间化模型直接输出的骨龄通常抖得厉害同一张图稍微改一下输入分辨率结果可能差出3个月。临床和答辩都不能接受这种不稳定。两个实用的后处理办法一是输出区间估计把结果写成“骨龄X岁±3个月”这比单一数值更诚实地反映模型不确定性也对应医生读片时对边界模糊区的处理习惯二是对连续输入做平滑比如在同一批多次推理后取中位数。如果做的是视频或连续帧的体态检测可以加一个EMA滑动平均。论文里展示后处理效果时绘制一张误差分布直方图比贴表格更有说服力横轴是“模型骨龄-医生骨龄”纵轴是样本数量图上能看到大部分样本落在±6个月内。这也是我最后写项目文档时一定会放进的一张图。5. 骨龄检测避坑指南5个真实踩坑记录与排查思路5.1 坑一检测mAP很高骨龄误差却大得离谱现象YOLOv5验证集mAP50能到0.92但把检测框信息送进回归头后骨龄预测和标注差了2岁以上。原因两段式方案最典型的翻车点。检测网络学的是“骨化区在哪”回归头却只拿到坐标和尺寸这些低层信息根本无法推断“骨化区有多熟”。等于把两个任务的前后依赖切断评价指标自然对不上。解决让回归头看到特征图而不是框坐标。两种做法一是把yolov5 backbone输出特征接入回归头二是把检测框裁出来再单独训练一个骨龄分类网络。后一种稳定但要多训一个模型前一种流程更顺但要注意特征图的感受野对应关系最好在backbone输出后接一个1x1卷积降维再接全局池化。5.2 坑二同一骨化区被切成多块后重复计数现象高分辨率X光片做滑动窗口切片提速但同一块腕骨在相邻两个切片里各出现一次汇总时被算两次骨龄整体偏高。原因滑动窗口有重叠区域后处理只按类别计数没做跨切片的去重。解决汇总时把每个检测框的类别和置信度保留并把框中心点映射回原图坐标然后做一次全局NMSIOU阈值取0.5。这个去重步骤直接决定骨龄偏不偏别省。边界上的框还要额外处理中心点落在重叠区的目标只保留置信度最高的那一个。5.3 坑三训练时显存爆掉模型却没必要那么大现象yolov5m跑batch 32在12G显卡上直接OOM有时候不报错就是训练中途卡死。原因X光片虽然resize到640但数据加载和增强时占用的中间内存比自然图像更高mosaic增强一次性拼4张图峰值显存被放大。解决换yolov5sbatch降到16开--amp混合精度显存占用能降到原来的三分之一。如果还爆检查是不是开了--cache把整个数据集预先加载进内存这是显存以外的隐藏开销。混合精度训练在骨龄这种小数据集上不会有明显的精度损失放心开。5.4 坑四标注框与骨化区边界对不上anchors聚类结果很怪现象训练完可视化预测框发现框普遍比骨化区大一圈贴在骨头边缘外侧。原因标注人员画框标准不一致有的把整个腕骨群框起来有的只框单个骨化区导致标签框整体偏大。解决训练前先对验证集逐张画GT框检查把明显过大的框按中心点裁剪到合理尺寸。再把hyp里的anchor_t从默认4改成3提高正样本筛选的阈值让模型更关注框内的真实目标区域。注意裁剪后要重新生成标签txt别改完图像尺寸忘了改标注坐标。5.5 坑五推理速度太慢单张片子要1秒以上现象yolov5m640输入在GPU上单张约200毫秒换CPU直接到2秒现场demo根本没法流畅展示。原因模型深度大、后处理NMS计算量大加上IOU阈值设置过低时候选框数量多拖慢推理。解决换yolov5s单张能压到100毫秒以内要更快就导出ONNX用半精度跑或直接上rk3568这类盒子做INT8量化单张几十毫秒足够。需要权衡的是量化后的精度损失必须用验证集重新测一次骨龄MAE而不是只看mAP。6. 把骨龄检测项目做成能讲清楚的demo验证方法、后处理展示与部署路径骨龄检测项目做到能跑只是第一步毕业设计和课程设计的区别在于能不能把“为什么这样做”讲清楚。我有一个很实用的建议用Streamlit写一个极简前端上传一张左手X光片左边显示YOLOv5画出的检测框和置信度右边显示每个关键区的类别、检测数量、以及最终骨龄和误差区间。这个demo在答辩现场的说服力比十页PPT都强因为评审能看到中间过程。验证阶段我建议在测试集上按年龄组分层统计0~6岁、7~12岁、13~18岁各挑5~10张计算每组的平均绝对误差。骨龄在不同年龄段的表现差异很大低龄组腕骨特征分化明显高龄组骨干闭合程度才是关键合成一个总MAE会把分布差异掩盖掉。汇报时给出整组MAE加分组MAE再加一张误差分布直方图基本就是一篇完整的方法学实验。部署层面如果你的场景是离线批量阅片ONNXOpenVINO在CPU上足够如果是边缘设备yolov5s导出ONNX后在rk3568上做INT8量化单张几十毫秒是可用的水平。我吃过一次亏当时为了量化省事直接对完整模型做INT8结果骨龄MAE从0.4岁涨到0.9岁后来改成只量化backbone、保留回归头浮点才把误差压下来。这也是为什么验证永远不能只看模型推理速度要回到骨龄本身的误差上。这些年的经验里我渐渐养成一个习惯拿到任何骨龄检测项目先看数据标注质量和后处理逻辑再碰模型结构。YOLOv5的训练参数是固定的套路真正决定项目成败的是“检测结果能不能转换成可信的骨龄数值”这条链路。希望这篇踩坑记录能帮你少走一段弯路把时间花在真正影响结果的地方。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑