ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python+YOLOv5骨龄检测实战:数据标注、模型训练与推理全流程

Python+YOLOv5骨龄检测实战:数据标注、模型训练与推理全流程 简介基于Python与YOLOv5的骨龄检测项目面向毕业设计、课程设计与项目开发场景较完整地覆盖骨骼图像预处理、模型训练、参数调优和结果推理等环节项目中包含训练好的数据模型、数据集和权重文件源码经过严格测试可以在理解代码后直接参考并依据自身课题需求做功能延伸。资源总共有二十五个文件其中二十个为Python脚本分别承担模型定义、训练损失计算、数据增强、自动锚框、评估绘图等任务另外辅以Markdown说明文档、依赖清单和YAML/TOML配置文件帮助读者快速了解项目结构、安装环境并还原实验压缩包体积约九十五KB整体十分紧凑适合课程设计或期末项目使用。从内容预览来看工程采用YOLOv5标准的目录组织包含models、utils等核心模块以及预处理脚本结构清晰便于按模块深入阅读。目前已有一百零七人学习或下载适合需要快速完成医学图像检测类课题的高校学生参考。1. 骨龄检测到底难在哪这套PythonYOLOv5方案为什么能直接落地骨龄检测不是新鲜事临床上早就靠左手腕X光片按G-P图谱法或TW3法评估骨骼成熟度用来判断儿童生长潜力、诊断性早熟或发育迟缓。难的是这活儿极度依赖医生经验同一张片子不同医生读经常差出半年骨龄耗时长还很主观。拿YOLOv5来做核心思路是把问题拆成两段第一步检测模型把桡骨、尺骨、腕骨、指骨等关键区域框出来第二步再从框内特征回归出骨龄数值。这种方案特别适合毕业设计和课程设计因为YOLOv5生态成熟、单卡就能训练、标注只需要普通检测框而不用画像素级掩膜配好权重做迁移学习后小样本也能收敛。下面按“原理→数据→训练→避坑→交付”的顺序把整条链路讲清楚最后给一个能直接跑的推理脚本。2. YOLOv5做骨龄检测的技术路线区域检测与骨龄回归的架构怎么选2.1 端到端回归与检测回归两条路线为什么后者更适合课程设计做骨龄自动评估摆在面前的主流路线有两条端到端回归和检测回归。端到端路线最典型的代表是RSNA骨龄挑战赛里常见的那种方案整张左手腕X光片送进一个在ImageNet上预训练过的卷积网络直接回归出骨龄数值。优点是一个模型解决全部问题完全不需要检测框标注缺点也很明显模型把“看哪里”和“算多少”耦合在一个黑盒子里你很难跟答辩老师解释它到底关注了哪块骨骼换一台拍摄设备或者换一种图像分辨率性能经常说掉就掉。检测回归路线把流程拆开第一步YOLOv5检测若干关键骨骼区域输出框坐标、类别和置信度第二步把检测框裁剪出的区域送入一个独立的回归头输出骨龄值。这么做的好处是每个子任务都能单独验证检测结果可以直接画框可视化哪块骨骼漏了、错了一眼就能看到回归头只面对局部区域不会被原图远端一些与骨骼发育无关的纹理带偏后续想把性别、骨龄父母身高这些临床变量加进来做条件输入也只有拆开的方案才做得到。课程设计或毕业设计基本都会推荐后面这条路线原因很务实画框可视化效果好答辩PPT不愁没素材模块拆开后原理、训练、测试每个环节都有得讲更关键的是YOLOv5的工具链最完整从训练到部署都是现成脚本你只需要替换数据集和改少量配置。也就是说你真正要花时间去调的是数据质量和回归头而不是YOLOv5本身。2.2 数据侧怎么设计类别体系与标注框粒度路线定了之后第一件事是确定类别体系。骨龄判读最看重的是桡骨远端、尺骨远端、腕骨、近节指骨和中节指骨这些包含骨骺生长板的区域。常用做法是设五个类radial、ulnar、carpal、proximal_phalanx、middle_phalanx。这里有一个很容易犯的错为了追求“医学严谨”把五根手指每一节的指骨都单独设类结果类别数飙到十几个每个类只有两三百个框模型样本量根本撑不住mAP很难看。这类方案我见过好几个最后全都退回去合并大类。更稳的做法是腕骨区域合并为一个carpal类指骨只区分近节和中节或者干脆把全部指骨合成一个finger类。检测任务只需要把生长板所在的区域找出来不需要严格对应解剖细分——分类精度是回归头的事不是检测头的事。标注工具用LabelImg导出VOC XML或者用X-AnyLabeling导出COCO JSON后面写个脚本转成YOLO格式。框的具体标注也有讲究不要贴着骨骼边缘画留出8%到15%的边界冗余因为骨膜附近的软组织信息对骨龄判断也有价值。所有框的边界冗余比例尽量一致别有的贴边有的留太多模型学到的框尺度会很乱。2.3 模型侧怎么设计从检测框到骨龄值的链路回归头怎么加是整个方案决定成败的一步。常见做法是在YOLOv5检测模型训练完成后写一个独立的小回归网络输入是检测框裁剪区域resize到224x224输出是一个标量单位是月。骨干网络可以用ResNet18或EfficientNetB0甚至直接复用YOLOv5s的backbone权重。训练时把检测框坐标固定住不参与梯度回传两个模型独立调试哪个不行单独修哪个。如果不打算写回归网络也有一个朴素的备选把检测框的中心坐标、框宽高、类别序号和置信度组成特征向量再用线性回归或随机森林映射到骨龄值。这个做法在小数据集上能出结果我见过不少课程设计就是这么交差的但问题是特征信息量有限换成不同的数据分布精度波动很大。我一般会在课程设计里两条路都做一遍先用回归网络拿到一个较高的精度再用随机森林做对照实验。因为答辩经常会被问“你为什么不用端到端”“你为什么不用分类网络”有对照数据说话会硬气得多。3. 把原始X光片变成YOLO训练集数据检查、格式转换与增强策略3.1 数据集获取与检查数量、分辨率与泄漏问题骨龄数据不像通用目标检测数据集那么好找。公开的路径主要是RSNA骨龄竞赛数据集里面是左手腕X光片和对应骨龄标签如果拿不到公开数据就需要联系医院影像科脱敏后自制。无论哪条路拿到数据后先别急着标先把三件事查清楚。第一看数量。YOLOv5做迁移学习五类检测任务每类至少要有500到800个框才比较稳总图像量建议在800张以上越多越好。第二看分辨率。X光片原图通常是2000x2000以上训练时即使缩到640或960细节纹理仍然会损失。第三看分布。如果图像来源单一比如只来自同一家医院同一台DR设备模型很容易把设备风格带进去换成另一台设备就失效。数据泄漏也要提前防。骨龄数据一个常见的坑是同一患者不同时期的随访片子被拆到训练集和验证集里导致模型记住人而不是记住骨骼形态。做数据集划分之前一定要按患者ID去重——同一个人的多张片子只能同时出现在训练集或验证集绝不能跨集出现。这也是医学图像项目比通用视觉更容易翻车的地方很多人第模型一跑发现mAP特别高其实是被这种隐性的数据泄漏骗了。3.2 VOC和COCO标注转YOLO格式的Python脚本标注工具导出的大多是VOC XML或COCO JSONYOLOv5训练需要的是每张图对应一个同名txt文件格式为class cx cy w h全部坐标都归一化到0到1。这里给出一个VOC XML转YOLO txt的脚本类别集合按前面定的五类来。# voc2yolo.py import os import xml.etree.ElementTree as ET CLS_NAMES [radial, ulnar, carpal, proximal_phalanx, middle_phalanx] def voc_to_yolo(xml_path, out_dir, img_width, img_height): tree ET.parse(xml_path) root tree.getroot() txt_path os.path.join( out_dir, os.path.splitext(os.path.basename(xml_path))[0] .txt ) with open(txt_path, w) as f: for obj in root.iter(object): name obj.find(name).text if name not in CLS_NAMES: continue cls_id CLS_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) cx (x1 x2) / 2.0 / img_width cy (y1 y2) / 2.0 / img_height w (x2 - x1) / img_width h (y2 - y1) / img_height f.write(f{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}\n)逻辑很简单解析XML里每个object下的bndbox坐标计算出中心点和宽高再除以图像宽高做归一化。这里有个很实用的参数细节img_width和img_height建议直接用数据集config里记录的真实尺寸而不要用XML里通过像素坐标推出来的最大值因为同一个标注工具导出的每张图尺寸不一定一致。转换完以后随手抽查十几张txt把坐标乘回原图尺寸画框比对这一步能拦下八成坐标错位的低级错误。COCO JSON转YOLO格式是另一个常用脚本核心是从annotations里按image_id把标注聚合到每张图再用同上的归一化公式写txt。逻辑比VOC稍绕因为annotation里存的是[x, y, width, height]格式的绝对坐标其中x和y是框左上角需要多加一步转成中心点。写完脚本记得处理一个细节:COCO允许某些目标area为0,这类标注要过滤掉不然训练时会出现空框的异常loss。3.3 小样本下有取舍的数据增强策略YOLOv5自带的Mosaic增强在通用目标检测里效果拔群但放在骨龄X光片上要格外小心。Mosaic会把四张图拼在一起骨骼解剖位置被强行打乱模型容易学到“骨头碎片拼贴”的假特征。我的处理方式是训练初期前50个epoch关闭Mosaic——YOLOv5里对应mosaic参数设为0等模型对真实分布有了基本拟合再逐步打开并且把Mosaic概率控制在0.3以下。X光片是灰度图YOLOv5增强里的HSV色相饱和度变换几乎无效但亮度变换很关键。不同设备、不同曝光条件下X光片亮度差异很大saturation增强可以关掉hsv_h可以保留一个很小的值如0.005value即亮度增强的幅度值得加大到0.4左右让模型对曝光差异更鲁棒。左右翻转即fliplr可以开医学上左右手骨骼形态对称用它等于把样本量翻倍上下翻转必须关掉手部骨骼朝向是固定解剖方向翻转后完全违背临床语义。最后再用一个额外的预处理增强CLAHE对比度受限自适应直方图均衡化。X光片软组织对比度偏低对灰度图做CLAHE能显著增强骨骺边界。我通常把它作为离线预处理而非在线增强也就是先把所有训练图和验证图都处理好再喂给模型保证训练和验证分布一致。4. YOLOv5训练骨龄模型实战环境、命令、权重与必调参数4.1 环境配置与依赖检查YOLOv5的环境配置是整套流程里翻车率最高的一步大部分问题出在torch和CUDA版本不匹配上。稳妥的做法是先用conda隔离环境python版本选3.9或3.10不要追新很多预编译包的兼容性还没跟上。conda create -n bone-yolo python3.9 -y conda activate bone-yolo # 根据你的显卡CUDA版本选择torch这里以CUDA 11.8为例 pip install torch2.0.1 torchvision0.15.2 --index-url https://download.pytorch.org/whl/cu118装完torch先别急着下一步跑一段代码确认GPU可用。很多人栽在torch正常但CUDA不可用这个坑上如果检测不到GPU后续训练直接变成CPU慢跑几百个epoch根本等不起。YOLOv5仓库clone下来后requirements.txt必须装全漏掉哪一项训练或推理到中途就会报一个莫名其妙的ModuleNotFoundError。4.2 用预训练权重做迁移学习的训练命令环境没问题后把标注好的数据集按YOLOv5要求的目录结构放好。主目录下建images/train、images/val、labels/train、labels/val对应的图片和txt。然后在项目根目录写一个bone.yaml内容如下# bone.yaml path: ./bone_dataset train: images/train val: images/val nc: 5 names: [radial, ulnar, carpal, proximal_phalanx, middle_phalanx]训练入口用官方train.py关键参数在命令行里控制python train.py \ --data bone.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 200 \ --hyp data/hyps/hyp.scratch-low.yaml \ --cache ram --device 0这里有两个容易理解错的地方。一个是weights参数写yolov5s.pt表示加载官方COCO预训练权重做迁移学习模型会保留backbone等部分特征提取能力只用你的数据微调检测头。另一个是hyp参数YOLOv5官方默认的hyp.scratch-low.yaml针对的是自然图像骨龄场景下学习率往往需要调低具体调整在下一节讲。训练到bests权重保存在runs/train/exp*/weights/目录下best.pt是验证集mAP最高的权重last.pt是最后一个epoch结束时的权重。课程设计交底时有同学直接把last.pt当成最终模型这是不对的一定用best.pt最好同时在bests的同一目录下把对应的opt.yaml一起保存方便复现实验环境。4.3 骨龄检测的五个必调参数迁移动骨龄场景后真正影响结果的参数就这几个按优先级从高到低排。学习率初始值lr0。骨龄检测的标签比COCO稀疏得多而且框尺度变化集中默认的0.01太高我一般直接降到0.001到0.003。损失曲线如果前50个epoch就有明显波动进一步降到一个合理的水平。理解loss曲线上扬也别慌先确认lr0不是罪魁祸首再考虑别的。批次大小batch。显存允许的前提下尽量大骨龄框比较小batch太小会让归一化统计量不稳定。16或32是一个常见平衡点显存不够就先降batch不要优先降分辨率分辨率掉下来小目标检测就白做了。训练分辨率img。640能跑960效果明显更好但显存占用快翻倍如果图像里腕骨区域特别小可以试1280但迁移学习权重yolov5s.pt是为640训练出来的切到1280后需要更多epoch来重新适应。锚框anchors。YOLOv5开启自动锚框计算会在训练前重新聚类你数据集的框。这是个很隐蔽的影响维度因为X光片里骨骼框的高宽比跟COCO里的车辆行人完全不是一回事。检查训练日志里AutoAnchor那一行如果提示anchors偏差较大就把anchor参数去掉默认的强制自适应让模型按你的数据集重新聚类。超参数文件里的颜色增强相关项。前面说过HSV增强在X光片上要关掉大半。在hyp.scratch-low.yaml里把hsv_h调到0.005、hsv_s调到0这点小改动不细看没人注意到但对训练稳定性的贡献很实在。5. 避坑指南YOLOv5骨龄训练中常见的5个翻车现场与排查路径5.1 训练几十个epoch了mAP还是0现象loss曲线正常下降但验证集mAP一直是0检测结果一个框都出不来。原因最常见的是锚框与目标尺度严重失配。骨龄检测的框在整张图里占比很小比如指骨的框可能只有40x80像素在640分辨率下不到原图面积的2%。COCO预训练模型默认锚框倾向于中等偏大的目标直接迁移后检测头对小目标的响应很弱。其次是训练数据里小目标与图像尺寸比例不协调模型根本没有在对应特征层上学会检测。解决先看AutoAnchor日志确认聚类结果如果聚类后的锚框远小于默认值说明数据本身确实是多小目标这正常。不要自己动手改锚框值而是确认训练时没有关闭autoanchor。如果日志提示跳过就手动把anchors参数指到你聚类出的锚框文件上。另外检查网上最常见的坑在标注精度、类别索引错位现象这类情况mAP会段时间后从0跳起来。5.2 训练集mAP很高验证集崩了现象训练集上mAP能到0.9验证集只有0.3甚至更低明显过拟合加上数据泄漏问题的综合表现。原因优先怀疑数据泄漏。同患者多张片子被划分在不同集合里模型记住了人的骨骼结构却记了患者本身。其次是标注不一致造成的虚高某个类别只在训练集里出现验证集完全没有该类的样本。解决重新按患者ID划分数据集确保同一患者所有图片同去同留统计训练集和验证集的类别分布相差超过15%就要做分层抽样或补充样本。做分层抽样时要连同“腕骨类别起始”一起抽避免某类只在特定年龄段出现验证集却全是另一年龄段这是骨龄里一个很特殊的分布陷阱。5.3 换了一台设备拍摄的X光片模型立刻失准现象训练和验证用的是某医院A设备的片子mAP很好拿到医院B设备的片子上预测迁移后mAP掉到0.3以下。原因骨龄检测对图像风格极度敏感X光片的对比度、亮度、噪声甚至压缩质量差异都会被模型当成特征。不同DR设备、不同曝光参数出来的片子哪怕同一个患者拍一张yaml的分布都差别显著。这不是模型过拟合训练集而是典型的数据分布偏移带出的退避策略。解决图像预处理先做标准化对每张X光片做CLAHE均衡化后再输入这样能消除一部分设备差异带来的亮度对比度差异。训练时把亮度增强下重手比如把hsv_v从0.01调到0.4让模型见过各种亮度的片子。还有一招是采样归一化把灰度图按全局均值方差做z-score归一化输入分布就稳定得多这也适合部署时保持和训练一致的处理链路。5.4 显存溢出训练到一半OOM现象训练刚开始就报CUDA out of memory或者跑到一半突然炸掉。原因多半是分辨率加batch的组合超出显存上限。X光片分辨率高很多人直接把img设成1280又舍不得降batch结果一张图就吃满12G显存。解决按显存倒推参数顺序是先降batch到8或4还不够就把img降到960或640。不要同时降图片分辨率又不降低batch那样小目标检测直接完蛋。另外注意Train缓存策略开启--cache ram会把图片缓存到内存如果内存也不够会导致进程被杀这时候换成--cache disk或去掉缓存选项。训练中OOM还有一个容易被忽略的诱因验证阶段开了更长的尺度测试值设为0.5也降低并节约显存。5.5 训练过程中loss爆成NaN权重直接作废现象前面几十个epoch很正常突然某个epoch loss变成nan之后训练记录全部作废。原因学习率过大导致梯度爆炸或者混合精度训练时fp16精度不够造成溢出。骨龄检测里如果标注坐标有极小的框如1像素级别的异常框也会在归一化后造成数值不稳定。还有一些数据本身的零字节文件会引出空张量。解决先把混合精度关掉加--no_amp参数在精度不敏感的检测任务里这点加速损失完全值得。再看学习率尤其调低lr0之后仍炸就查标注找有没有宽高或多个框超过图像边界的异常标注我见过一张XML里背景区域类导致loss炸掉的案例。最后在入口代码里加一个标注合法性检查函数把坐标范围异常的样本全部筛掉再训练数据问题总是比代码问题更容易被忽视。6. 让答辩演示更有说服力写一个输出可视化报告的推理脚本检测模型训好之后大部分人会直接拿detect.py跑一批图出框然后截图放进PPT。但这个流程有两个遗憾一是没把骨龄值落到图上了答辩现场只能靠嘴说“这里框出来的是桡骨”二是演示过程不连贯说服力不如一个脚本从头到尾输出完整报告。下面给一个我在课程设计收尾时常用的单图推理脚本输入一张X光片输出带框标注的图片和一份文本报告。# infer_bone_age.py import torch import cv2 from pathlib import Path # 从本地YOLOv5仓库加载自定义权重 model torch.hub.load(./yolov5, custom, pathruns/train/exp/weights/best.pt, sourcelocal) model.conf 0.35 # 置信度阈值 model.iou 0.45 # NMS的IoU阈值 model.max_det 50 # 每张图最多输出框数 img_path demo_left_wrist.jpg results model(img_path) # 画框并保存可视化图 vis results.render()[0] # 得到带标注的BGR图像 cv2.imwrite(output_annotated.jpg, vis) # 导出文本报告 df results.pandas().xyxy[0] print(f检测到 {len(df)} 个骨骼区域) for _, row in df.iterrows(): print(f类别{row[name]} 置信度{row[confidence]:.2f} f框位置({int(row[xmin])}, {int(row[ymin])}) f尺寸({int(row[xmax] - row[xmin])}, {int(row[ymax] - row[ymin])}))这个脚本最重要的部分是torch.hub.load以sourcelocal方式加载这样推理完全依赖本地仓库不依赖联网下载任何东西答辩现场断网也能稳定跑完。conf阈值我习惯设置在0.35而不是默认的0.25骨龄检测里低置信度框往往伴随误检宁可少框不要错框。max_det设到50是为了防止某些曲张的X光片背景区域被连续错误识别成骨骼区域时输出几十个框导致报告被污染。如果你做了回归头就在conf过滤之后把每个框裁剪出来resize到224x224后送入回归网络把输出的骨龄值和对应的区域类别一起写进报告。演示时这张图和报告并排展示又从原图裁剪一个骨龄值最大的区域特写放大老师基本一眼就能看懂整套方案的逻辑。我从做第一个骨龄检测项目起就养成了一个习惯所有演示材料都用best.pt跑从不临时加载last.pt也从不直接沿用根目录里的detect.py而不检查置信度阈值——这两个细节出过太多尴尬现场。希望这份从数据到训练的完整链路能帮你的项目少走弯路顺利收尾。本文还有配套的精品资源点击获取
返回列表