基于yolov5的手骨骨龄检测实战:从目标定位到模型训练
简介一套基于 Python 与 YOLOv5 的手骨骨龄检测项目专为毕业设计、课程设计与实际项目开发打造。内容覆盖从数据处理到模型评估的完整流程采用 CLAHE 自适应直方图均衡化技术去除手骨 X 光片中的雾感按比例切分训练集和测试集针对手骨与关节分别进行图像增强与统一尺寸预处理随后基于 YOLOv5 配置 bone.yaml 和 yolov5s.yaml 训练目标检测模型再使用 ResNet18 构建分类网络对 DIP、MCP、PIP 等关键部位进行骨龄等级评估测试准确度达到 90% 以上。压缩包共包含 189 个文件包括 64 个 Python 源码、51 个 YAML/14 个 YML 配置文件、10 个训练权重.pt、7 个 Markdown 文档、3 个 Jupyter Notebook以及 Dockerfile、Shell 脚本和图片示例等整体大小 436.79MB。这些文件分别对应源码实现、模型训练配置、预训练权重、项目文档和环境部署容器目录结构清晰便于直接运行与二次开发。当前已有 198 人学习适合需要完整手骨骨龄检测方案的学生与开发者可作为毕设、课设或项目起步的可靠参考。1. 用yolov5做手骨骨龄检测这活儿到底难在哪把一张左手腕X光片丢给程序让它几秒内报出骨龄约等于13岁6个月这是很多毕业设计和课程设计选题的起点。真正动手后你会发现骨龄检测并不是一个图像进去、数字出来的单一模型问题而是一套接近临床阅片流程的流水线先定位骨骼区域再评估成熟度最后换算成骨龄。yolov5在这套流水线里干的是最前面的定位活把桡骨、尺骨这些骨化中心从X光片里框出来。本文讲的就是基于python和yolov5实现手骨骨龄检测的完整落地路径从数据集整理、模型训练到参数调优和答辩演示覆盖你在毕设、课设里大概率会踩到的坑。适合准备拿这个方向做项目的学生也适合想把AI骨龄评估接进自有系统的开发者。2. 先看清任务再动手yolov5在手骨X光片上到底框什么2.1 为什么是检测分级而不是一张图直接回归骨龄很多初学者上来就把骨龄检测当成纯回归任务一张X光片输入ResNet34输出以月为单位的骨龄。在公开的RSNA骨龄挑战榜上确实有人用这种端到端方式拿到了不错的平均绝对误差但代价是模型是个彻底的黑匣子——当预测偏了你无法知道是哪个骨化中心的分期拖了后腿。答辩时被问你这个模型判断依据是什么基本答不上来。把yolov5网络结构图摊开看它的backbone是CSPDarknetneck是PANet路径聚合结构head会在640输入下输出80x80、40x40、20x20三个尺度的特征图。手骨的骨化中心在原始X光片上往往只有几十像素主要靠80x80这个浅层分支去召回这也是为什么直接拿yolov5默认配置去跑小目标漏检会很明显。所以我一般把方案拆成两段yolov5先定位骨化中心后面接一个成熟度分级或回归网络。yolov5承担定位的理由有三个单类目标标注成本低一个txt文件就能表达一个框速度和精度平衡好小数据集也能训得动X光片是灰度图复制成三通道后yolov5可以直接吃不用改网络结构。2.2 标注目标怎么定按TW3的RUS骨来还是只框腕骨临床上看左手腕X光片评骨龄主流体系是TW3和CHN中华05。TW3里的RUS骨是桡骨远端、尺骨远端、第1/3/5掌骨、第1/3/5近节指骨、第3/5中节指骨、第1/3/5远节指骨一共13个区每个区按成熟度分A到I九个级别。医学上就是这么读片的AI要模仿也得先学会看这些位置。但毕设和课设不要照搬13个类别标注成本会让你崩溃。我建议分三档来做最小可用方案只标桡骨远端和尺骨远端两个类别先把骨骺发育程度这件事跑通课程设计推荐方案在基础上加第1/3/5掌骨和第1/3/5近节指骨共8个类别足够撑起RUS评估的主体结构完整研究方案才考虑加入中节和远节指骨凑齐13个区。腕骨在青春期前有意义12岁以后用处不大可以干脆不做。标注的原则是类别宁少勿多一个框的坐标宁可多花十秒确认也别糊弄。yolov5对标注噪声的容忍度比分类网络低很多框偏了5个像素小目标的分期结果就可能偏一个等级。2.3 数据集整理与yolo格式转换一张图一个txt公开可下载的骨龄数据最常见的是RSNA 2017 Pediatric Bone Age挑战集格式一般是X光片加一个csvcsv里含性别和以月计的骨龄。注意这个原始数据没有检测框yolo训练需要自己标。我常用的路径是LabelImg导出VOC XML再统一转成yolo需要的格式。import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, out_dir, class_names): tree ET.parse(xml_path) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) lines [] for obj in root.iter(object): name obj.find(name).text if name not in class_names: continue cls_id class_names.index(name) box obj.find(bndbox) x1 float(box.find(xmin).text) y1 float(box.find(ymin).text) x2 float(box.find(xmax).text) y2 float(box.find(ymax).text) x_center (x1 x2) / 2 / img_w y_center (y1 y2) / 2 / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) if lines: out_path os.path.join(out_dir, os.path.splitext(os.path.basename(xml_path))[0] .txt) with open(out_path, w) as f: f.write(\n.join(lines)) if __name__ __main__: class_names [radius, ulna] # 必须与后面bone.yaml的names顺序一致 for xml_name in os.listdir(data/annotations): if xml_name.endswith(.xml): voc_to_yolo(os.path.join(data/annotations, xml_name), data/labels, class_names)这段脚本的逻辑是从VOC XML里读出每个目标的类别名和bbox像素坐标除以图像宽高归一化再按yolo的类别id cx cy w h格式写进txt。所有坐标都在0到1之间模型训练时不用关心原始图片尺寸。参数说明里最需要注意的是class_names的顺序它必须和后面bone.yaml里names的排列完全一致否则类别错位后模型训练出的权重毫无意义。输出txt的文件名要和图片同名yolov5是按basename去找标签文件的对不上就直接把这张图当负样本。注意labels目录不是自动生成的。你划分训练集和验证集时要同步把每张图对应的txt复制到对应子目录漏一个就少一批监督信号。转换完务必做一次抽查把txt坐标画回图上看框是否贴合骨化中心。这一步能拦掉一半后期模型为什么训不好的玄学问题。3. 从conda建环境到第一行训练命令数据集与python环境一次配齐3.1 conda创建虚拟环境python版本别追最新yolov5的环境坑主要是torch版本和python版本的匹配问题。python 3.12刚出那阵很多依赖编译不过去被坑过的人不在少数。我的建议是python选3.8到3.10之间配合PyTorch官方对CUDA的支持表来选版本稳字当头。用conda建环境是常见做法好处是删掉重来不留垃圾conda create -n bone python3.9 conda activate bone pip install -r requirements.txtconda create创建了一个名为bone的独立虚拟环境python3.9指定解释器版本不影响系统其他项目。激活后执行pip install装的是这个环境内部的依赖。requirements.txt是yolov5项目根目录自带的锁定了torch、opencv-python、numpy等基础库。如果你是Nvidia显卡建议先按PyTorch官网命令装对应的CUDA版torch再装requirements.txt。顺序反了容易装成CPU版训练时日志里看不到device: cuda速度差几十倍起步就输一半。3.2 数据集目录结构与bone.yaml配置yolov5训练自己的数据集有一套固定的目录约定images放图片labels放同basename的txt在yaml里分别指定训练集和验证集路径。我习惯的结构是这样的dataset/ images/ train/ val/ labels/ train/ val/从RSNA原始数据做出来这套结构需要先划分再拷贝。划分时固定随机种子保证每次复现的结果完全一致import os, random, shutil random.seed(42) src data/raw/images train_dir dataset/images/train val_dir dataset/images/val os.makedirs(train_dir, exist_okTrue) os.makedirs(val_dir, exist_okTrue) files [f for f in os.listdir(src) if f.endswith(.png)] random.shuffle(files) for i, f in enumerate(files): dst train_dir if i int(len(files) * 0.85) else val_dir shutil.copy(os.path.join(src, f), os.path.join(dst, f))这段脚本按85%和15%的比例把图片分到train和val两个目录并在最前面固定random.seed(42)。这个细节很重要不然你每次跑脚本划分结果都变训练日志里的mAP曲线没法跨实验对比。labels的txt要按同样的划分同步复制过去不能只拷图片。bone.yaml的配置如下train: dataset/images/train val: dataset/images/val nc: 2 names: [radius, ulna]train和val指向图片目录yolov5会自动去同级的labels目录找对应txt。nc是类别数量names是类别名列表顺序必须和标注转换脚本里的class_names一致。如果你后面加了掌骨和近节指骨把nc改成8names按统一的顺序写全。3.3 训练前先验证标注别急着开训很多人的习惯是把yolov5克隆下来直接train.py跑了半天发现loss不降回头看是标注文件全偏了。我建议训练前花十分钟做一次可视化校验把标注框画回原图import cv2, glob for img_path in glob.glob(dataset/images/val/*.png)[:20]: img cv2.imread(img_path) h, w img.shape[:2] txt img_path.replace(images, labels).replace(.png, .txt) with open(txt) as f: for line in f: cls_id, cx, cy, bw, bh map(float, line.split()) x1 int((cx - bw / 2) * w) y1 int((cy - bh / 2) * h) x2 int((cx bw / 2) * w) y2 int((cy bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite(check/ img_path.split(/)[-1], img)这段脚本读取每张验证集图片和它的同名txt把归一化坐标换算回像素坐标在图上画出绿色矩形框。参数上注意cx和cy是中心点画矩形时要用左上角和右下角的坐标推出来。随机抽20张如果超过三张框明显偏离骨化中心先回头改标注别让模型去学习错误的位置。4. 模型训练与参数调试yolov5超参数怎么设才不靠玄学4.1 模型选型yolov5s起步什么情况换n或m模型规模的选择我的默认值是yolov5s。s是速度和精度的平衡点在骨龄检测这种小目标场景里yolov5n为了轻量化砍掉了neck层的通道数小目标特征保留不足漏检率会明显上升。m的mAP比s高一些但训练时间接近翻倍对几百张标注量的毕设来说性价比不高。数据量如果超过五千张且标注质量稳定可以考虑直接上yolov5m。另一个思路是先用s把整套流水线跑通最后再换m做对比实验写进文档里这也是一个加分项。至于要不要直接上yolov8或yolov11我的看法是从零做项目选yolov5因为它的中文资料和踩坑答案最全以后想换Anchor-Free结构yolov5的工程经验能平滑迁移过去。4.2 四个必调的yolov5超参数打开data/hyps/hyp.scratch-low.yaml里面是学习率、动量、权重衰减、warmup这些超参数。我不建议全改真正影响骨龄检测效果的就四个。第一个是输入分辨率img。yolov5训练时会把你指定的图缩放到img尺寸骨化中心如果只有30x30像素640下特征太小。显存够的话直接试1280我实际对比过小目标漏检率能明显下降。显存不够就调低batch别硬扛。第二个是epochs和patience。骨龄数据集几百张起步150轮足够收敛不用一上来训300轮。设上patience让它在验证集连续不再变好时自动停止python train.py --data bone.yaml --weights yolov5s.pt --epochs 150 --batch-size 16 --img 640 --patience 20train.py是yolov5的训练入口--data指定bone.yaml--weights引用官方预训练权重来复用特征--batch-size由显存决定8G显存跑640用16没问题--patience 20表示连续20轮验证集mAP不涨就提前终止。训练日志里出现EarlyStopping说明模型已经训透不用再加轮次。第三个是loss系数。hyp文件里cls控制分类损失权重box控制定位损失权重。默认cls是0.5box是0.05。如果你发现框的位置飘、和骨化中心贴合不紧把box系数提到0.1让模型更抠定位。注意改完要把hyp文件单独存一份方便后面复现。第四个是anchors。yolov5训练时会自动对训练集标注做k-means聚类重新算anchor默认开着。如果日志出现anchors do not fit的警告先检查标注坐标是否归一化正确八成是标注脚本的bug不是anchor的问题。4.3 用--evolve跑一次自动调参yolov5内置了遗传算法搜索超参数的能力适合在骨龄这种小数据集上做一次参数空间的全局搜索python train.py --data bone.yaml --epochs 10 --evolve 20 --batch-size 8这个命令会在10轮的短训练任务上进化20代每代尝试一组超参数组合自动筛选出在当前骨龄数据上表现最好的配置最终输出到hyp.scratch.yaml。注意它要连续挂机很久适合睡前扔在GPU服务器上跑第二天直接用结果。说实话骨龄数据量下的超参数空间比较平坦evolve带来的提升通常只有两三个点mAP但它让你答辩时多一个我做过自动化调参的实锤。训练过程中要看runs/train/exp目录下的results.png里面有三条loss曲线和mAP曲线。box_loss持续下降说明定位在学obj_loss不降通常是正负样本不均衡cls_loss不降要检查类别标注是否错位。模型输出会存best.pt和last.ptbest.pt是验证集最好的权重后面推理和演示都用它。5. 手骨检测的五个翻车点和排查路径5.1 检测框全偏到画面角落现象训练正常跑loss也在降但推理可视化里框全是贴着图像边缘的小矩形跟骨化中心几乎没有重叠。原因标签txt里的坐标用的是像素值没有归一化或者归一化时除以了错误的尺寸。X光片常见分辨率是2048x2500如果脚本里把宽和高写反所有框都会被拉伸或压扁。解决回到转换脚本核对img_w和img_h取的是当前图片的真实宽高。然后写一个防御性检查遍历所有标签文件任何坐标值超过1.0的立即报警for line in open(txt): vals line.split() if any(float(x) 1.0 for x in vals[1:]): print(fbad label: {txt}, {line})这段脚本检查标签里除类别id外的四个坐标是否都在0到1之间超界的标注直接打印出来拦截在训练之前。5.2 训练150轮mAP停在0.1上下现象epochs跑满验证集mAP0.5始终在0.1到0.2之间徘徊loss曲线也不正常下降。原因最常见的是划分训练集和验证集时同一个患者的多张X光片被拆到了两边模型在验证集上见到的都是似曾相识但又不完全一样的数据。另一个高频原因是左右手方向没有统一X光片采集时有的是左手有的是右手模型学到的是边缘在左边还是右边而不是骨骼形态。解决进训练前做一次方向归一化。根据图像内容判断拇指方向统一翻转为左手位。这个预处理放进数据集整理脚本里不要在训练后返工。5.3 小目标漏检现象桡骨、尺骨能检出来第5指骨那种更小的骨化中心基本漏掉。原因640输入下小目标在特征图上的有效像素太少浅层特征经过CSPDarknet下采样后信息丢失严重。yolov5s的80x80分支虽然负责小目标但默认anchor对极端小框覆盖不足。解决先试1280输入这是成本最低的提升手段。显存不足就用切片辅助推理把大图切成带重叠的块分别检测再拼接结果。同时确认你用的是s而不是nn在小目标上确实弱。5.4 显存OOM现象bash里报CUDA out of memorybatch从16降到4还是报错。原因X光片原始分辨率高img设成1280后显存占用指数上升。mosaic增强会把四张图拼一起再缩放实际峰值显存比单张推理高很多。解决按顺序排查。先确认torch是GPU版然后batch减半img降到960最后关掉--cache缓存选项。三个都做了还OOM就不在本地死磕直接上云GPU或租卡时间也是成本。5.5 mAP挺高骨龄误差却大得没法看现象检测mAP0.5到了0.8但把检测框裁剪后喂给回归网络骨龄预测的MAE有15个月完全没法写进结题报告。原因检测框只负责把骨化中心找出来骨龄准确度由后面的成熟度分期或回归网络决定。输入图缩得太小导致裁剪图模糊、训练集年龄段分布不均都会成倍放大误差。解决把评估指标拆开看。检测部分看mAP骨龄部分看MAE并且按0到5岁、5到10岁、10到18岁三个年龄段分别统计。哪个段误差大就针对性补哪个段的数据或调哪个环节的模型不要用一个平均值骗自己。6. 验证与交付的落地技巧从mAP到骨龄误差的最后一公里6.1 用测试集算真正该给别人看的指标答辩和项目文档里检测和骨龄要分开展示。检测部分汇报mAP0.5、precision、recall骨龄部分汇报MAE。尤其注意按性别分开统计男孩女孩的骨化时间曲线不同混在一起算会把真实水平掩盖掉。写个测试脚本对测试集逐张跑检测、裁剪、回归最后按性别和年龄段分组输出MAE这张表放进项目文档比十张loss曲线图都有说服力。6.2 视频演示别只跑detectyolov5的detect.py本身能输出带框视频但只有检测框没有骨龄数字演示效果很单薄。常见做法是检测完把每个框的裁剪图喂给骨龄回归网络预测出月龄后再用cv2.putText把结果叠回画面。拍一段在屏幕上翻页播放的X光片视频作为输入输出就是框骨龄同时在动的演示效果答辩现场不用临时连摄像头也能稳定播放。我现在的习惯是每跑一组训练就把best.pt、bone.yaml、hyp配置文件、划分脚本的seed值一起归档模型和数据永远能对上号。骨龄检测这个方向yolov5只是定位环节的起点真正出彩的是后面跟临床评分体系对齐的成熟度评估逻辑。作为毕设和课设把这条流水线完整跑通的投入产出比很高要走向真正的临床辅助诊断后面还有数据合规和评分体系校准的长路要走。先把手上这套yolov5检测做扎实后续换yolov8或接更强的回归网络你的工程底子都不会亏。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →