尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

YOLOv5+ResNet18骨龄检测实战:从数据集到部署优化

YOLOv5+ResNet18骨龄检测实战:从数据集到部署优化 简介这套骨龄检测实验资源基于YOLOv5与ResNet18联合实现面向计算机视觉、医学影像分析方向的开发者与学习者可用于骨龄评估相关课题研究及课程设计。资源涵盖完整Python源码、预训练模型权重及百度飞桨标注数据并配套PyQt5图形界面支持端到端的检测、分类与结果可视化流程。包内共200个文件以py脚本、yaml配置、pth模型权重为主辅以ipynb示例、文档及Docker环境配置压缩包整体约717MB。数据预处理与格式转换脚本已一并提供按说明运行即可处理9大类骨龄标注数据。目前已有970人学习下载适合希望快速复现骨龄检测实验、熟悉YOLOv5目标检测与ResNet18分类联合应用的进阶学习者使用。1. 基于yolov5RestNet18的骨龄检测检测与回归组合的典型范式骨龄检测不是单纯的目标检测也不只是图像分类它是典型的“先定位、再评估”的级联任务。用yolov5框出左手X光片中的腕骨或指骨区域再用RestNet18从裁剪区域中回归出骨龄比端到端一个网络更符合临床读片习惯也更容易定位误差来源。实际项目中RSNA骨龄数据集就是这种范式最常用的验证基准很多开源源代码把yolov5和RestNet18串成一条流水线数据集则以左手X光片和对应骨龄标签为主。对IT从业者来说这套组合能一次性接触到目标检测训练、图像回归、数据集划分、模型串接和部署验证值得完整拆开看一遍。下面我会从模型选型讲起给出可复现的数据准备、训练命令和排错思路。2. 任务拆解与选型YOLOv5负责定位RestNet18负责骨龄推断2.1 YOLOv5在骨龄检测中的角色ROI检测与yolov5网络结构yolov5网络结构由Backbone、Neck、Head三部分组成Backbone用CSPDarknet提取特征Neck通过FPN和PAN融合多尺度信息Head输出边界框和类别概率。在骨龄检测里输入是左手X光片检测目标通常是整个手掌区域或者是桡骨远端、尺骨远端、腕骨等特定兴趣区域。选择YOLOv5而不是Faster R-CNN的原因很实际YOLOv5在保持精度的同时更容易训练源代码组织清晰社区里直接能查到yolov5训练自己的数据集的完整流程。另一个现实因素是公开骨龄数据集的标注大多是矩形框没有分割掩码检测框已经足够让后续分类网络聚焦在关键结构上。2.1.1 为什么不用分割或关键点检测替代ROI提取分割模型要逐像素标注在X光片上标记骨骺线边界需要医生参与成本极高关键点检测虽然能定位骨骼连接点但不同年龄段的骨骼形态差异大关键点数量很难固定。检测框则不一样它只需给出区域范围哪怕框稍微大一些RestNet18也能通过卷积自动忽略不相关背景。我在实践中发现检测框的召回率比框的精确度更重要漏框会让后续骨龄预测直接缺失一个样本而偏大的框只会稍微增加背景噪声。2.2 RestNet18在骨龄推断中的角色回归头与分类头的选择RestNet18的残差结构让网络在18层的情况下依然能稳定收敛骨龄特征主要集中在骨化中心的面积、骨骺线的宽窄、骨骼边缘的锐度这些都属于中低层视觉特征不需要特别深的网络。把RestNet18的最后一层全连接改成单节点输出就变成回归模型改成多节点加softmax就变成年龄分箱分类模型。两种做法在开源项目里都存在我一般首选回归头因为骨龄标签是连续月份值回归能直接拟合分布而且评估指标用平均绝对误差MAE也更直观。2.2.1 回归还是分类两种输出头的对比输出头类型损失函数输出范围优点缺点单节点回归Smooth L1 / MAE任意连续值分辨率高直接对应月份难收敛容易过拟合异常值多节点分类CrossEntropy离散的月份bin训练稳定误差可控粒度受限bin宽度影响精度如果数据量少于5000张建议用分类头把骨龄按6个月一个bin做分类如果数据量大且标注准确回归头更合适。RSNA官方评估标准中选手们在回归头上配合MAE损失拿到最好成绩说明在足够数据下回归的表达上限更高。3. 源代码与数据集目录组织、标注转换与训练命令3.1 源代码目录设计常见做法是把yolov5官方仓库作为子目录RestNet18部分单独维护两者通过推理脚本串联。目录结构类似这样boneage_detector/ ├── yolov5/ # yolov5官方源代码 │ ├── train.py │ ├── detect.py │ └── data/ │ └── bone.yaml ├── resnet18/ # RestNet18训练与推理 │ ├── train_resnet.py │ ├── dataset.py │ └── checkpoints/ │ └── resnet18_best.pth ├── datasets/ │ ├── raw/ # 原始X光片和标签 │ ├── yolo_labels/ # 检测模型用的txt标注 │ └── cropped_roi/ # 从原图裁剪出的ROI └── inference/ ├── run_pipeline.py # 级联推理入口 └── export_onnx.py把检测和回归分开目录管理是为了让两个模型可以独立迭代。我在调试时经常只重训RestNet18而保留YOLOv5检测结果这样能快速判断误差是来自检测框还是骨龄回归。3.2 数据集准备把骨龄CSV标注转换为YOLOv5格式公开骨龄数据集中最常见的是RSNA骨龄数据集包含左手X光片和CSV标签每行有boneage以月为单位、male性别以及可选的手部边界框。YOLOv5训练需要的是每张图对应一个txt文件每行格式为class x_center y_center width height所有值都归一化到0到1之间。转换脚本一般这样写import pandas as pd from pathlib import Path def convert_csv_to_yolo(csv_path, img_dir, out_dir): df pd.read_csv(csv_path) out_dir.mkdir(exist_okTrue) for _, row in df.iterrows(): img_path Path(img_dir) / (row[id] .png) if not img_path.exists(): continue w row[width] h row[height] # 有的数据集直接给x1,y1,x2,y2也有给center点加宽高的 cx (row[x1] row[x2]) / 2.0 / w cy (row[y1] row[y2]) / 2.0 / h bw (row[x2] - row[x1]) / w bh (row[y2] - row[y1]) / h txt_path out_dir / (row[id] .txt) with open(txt_path, w) as f: f.write(f0 {cx:.4f} {cy:.4f} {bw:.4f} {bh:.4f}\n) convert_csv_to_yolo(train.csv, imgs, yolo_labels)这段脚本做了三件事读取CSV中的检测框坐标、按图像宽高归一化、写出YOLO格式txt。需要注意的是原始X光片可能是16位深度或DICOM格式YOLOv5的官方数据加载器基于OpenCV不能直接读取16位PNG需要先用代码转成8位灰度图保存。另外一个容易被忽略的点是数据集划分同一个患者多次拍摄的X光片只能进同一个集合否则会发生数据泄漏导致MAE虚低。3.3 训练YOLOv5检测模型的关键命令与参数yolov5训练自己的数据集时先要写好data配置文件bone.yamlopath: ./datasets/bone.yaml train: ./datasets/images/train val: ./datasets/images/val nc: 1 names: [hand_roi]然后执行训练命令cd yolov5 python train.py \ --data ./datasets/bone.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --hyp hyp.scratch-low.yaml这里的关键参数说明如下参数设定值说明--img640输入分辨率手骨ROI在原图中占比不大时可提高到1024但显存占用会翻倍--batch16显存不足时减小到8梯度累加可补偿--hyphyp.scratch-low.yaml低增强超参适合X光片这种背景单一的数据集--weightsyolov5s.pt用COCO预训练权重做迁移学习收敛更快在骨龄这个场景里mosaic增强的默认值需要调低。X光片中骨骼边缘和软组织对比度本身很微弱mosaic把四张图拼在一起后模型容易学会“看图左上角是否有黑色背景”这类伪特征。我通常把mosaic设置为0.3到0.5关闭mixup因为mixup的线性混合会让骨化中心的边界变得模糊。3.4 训练RestNet18骨龄评估模型的数据与超参检测模型训练完成后先对全部训练图片做一次推理裁剪出ROI并保存到cropped_roi目录再让RestNet18在这些ROI上训练。训练脚本的入口命令python resnet18/train_resnet.py \ --data_root ./datasets/cropped_roi \ --label_file ./datasets/boneage_train.csv \ --backbone resnet18 \ --input_size 224 \ --loss mae \ --batch 64 \ --epochs 60 \ --lr 1e-4训练时数据加载器里的预处理要统一先resize到224×224再减均值除以标准差。X光片是灰度图直接把单通道复制成三通道或者改成单通道输入并微调第一层卷积权重。我一般保留三通道输入因为可以直接用ImageNet预训练权重迁移效果更稳定。数据增强里只能做旋转、缩放、平移不能做水平翻转因为左右手的骨骼发育程度不完全对称翻转后会让模型学到错误的手别特征。4. 联合推理与排错级联流程、指标评估与常见坑4.1 从检测框到骨龄预测的级联推理流程级联推理是源代码里最核心的串行逻辑。先用YOLOv5对整张X光片产出检测框再对每个框执行裁剪、缩放、归一化最后逐个送入RestNet18得到年龄预测对同一张图的多框结果取平均或按置信度加权。下面是一段可运行的推理代码import torch import cv2 import numpy as np # 加载本地yolov5源代码目录中的模型 yolo torch.hub.load(./yolov5, custom, pathyolov5/runs/train/exp/weights/best.pt, sourcelocal) # 加载RestNet18, 假设保存的是完整模型字典 resnet18 torch.load(resnet18/checkpoints/resnet18_best.pth, map_locationcpu) resnet18.eval() img cv2.imread(test_hand.png) # YOLOv5输入的是BGR或RGB一致, 这里保持OpenCV默认BGR results yolo(img, size640) df results.pandas().xyxy[0] predictions [] for _, row in df.iterrows(): x1, y1, x2, y2 map(int, [row[xmin], row[ymin], row[xmax], row[ymax]]) crop img[y1:y2, x1:x2] crop cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY) crop cv2.resize(crop, (224, 224)) crop_rgb cv2.cvtColor(crop, cv2.COLOR_GRAY2RGB) crop_rgb crop_rgb.astype(np.float32) / 255.0 crop_rgb (crop_rgb - np.array([0.485, 0.456, 0.406])) / np.array([0.229, 0.224, 0.225]) inp torch.from_numpy(crop_rgb.transpose(2, 0, 1)).unsqueeze(0).float() with torch.no_grad(): pred resnet18(inp).item() predictions.append(pred * 12) # 模型输出的是年, 转成月 if predictions: print(预测骨龄(月):, np.mean(predictions)) else: print(未检测到ROI, 检查YOLOv5的置信度阈值)这段代码里有几个必须保持一致的细节YOLOv5内部会做letterbox填充检测框坐标会映射回原图尺寸所以裁剪时直接用原图坐标即可RestNet18的预处理一定要和训练时一致尤其是均值和方差否则模型输出会明显偏移最后输出的单位也需要对齐如果训练时标签是“月”就不要在代码里再乘12。4.2 评估指标MAE和按年龄分组的误差骨龄检测的核心评估指标是平均绝对误差MAERSNA官方最终的排名看整体MAE同时会单独看男童和女童亚组因为女童骨龄发育高峰期早于男童。除了MAE还应关注误差在不同年龄段上的分布常见的做法是把预测结果按真实骨龄分桶统计每个桶的MAE哪一个桶偏差最大说明该年龄段的训练样本不足。年龄段(月)样本数MAE(月)备注0-3615009.2低龄组骨化中心少容易偏小36-12032007.8样本充足误差相对稳定120-240280011.5青春期骨骼变化大误差上升如果出现某个年龄段的MAE特别高先检查该年龄段的样本量和图像质量而不是先换模型结构。4.3 常见错误排查数据泄漏、过拟合、检测框漂移数据泄漏是最隐蔽的问题很多源代码在划分训练集时直接按文件名随机划分但同一个患者的多次随访X光片会同时出现在训练集和验证集中导致MAE虚低不少。排查方法是按患者ID分组后再划分并把患者ID列表打印出来检查是否重叠。过拟合在RestNet18上很常见尤其当裁剪后的ROI图像数量只有几千张时。判断标准是训练MAE持续下降而验证MAE在第20个epoch后开始反弹。解决方法是增大dropout或加入早停也可以把训练时的随机旋转角度从10度加大到15度利用后面要讲的迁移学习来缓解。检测框漂移是指YOLOv5在验证集上AP很高但实际临床图里框的位置偏移。原因通常是训练数据来源单一X光片的拍摄角度和截断位置分布过窄。我一般会在YOLOv5训练时加入少量负样本让模型学会在没有手骨时输出空结果而不会硬给一个错框。5. 进阶实测小样本微调、蒸馏量化与ONNX验证5.1 用预训练权重在自有小数据集上微调如果你只有几百张骨龄X光片最常见的做法是从同领域大模型微调。把RestNet18换用ImageNet预训练权重然后在RSNA骨龄数据集上先训一个基座模型再冻结前4个残差块只微调最后一个block和全连接层。微调命令可以这样设置学习率降到1e-5batch size减半用早停监控验证集MAE。这里的关键是不要让冻结层被小数据集的噪声破坏同时最后一层的随机初始化需要更多epoch来稳定。5.2 模型蒸馏与INT8量化当你需要把骨龄检测系统装到边缘设备上RestNet18的精度和速度仍需平衡。蒸馏时让RestNet18学习ResNet50的输出分布损失函数用MAE加上特征对齐损失通常能比直接训练提升1到2个月的MAE。量化到INT8时先把YOLOv5和RestNet18分别导出到ONNX然后用校准集跑一遍TensorRT PTQ或ONNX Runtime的INT8接口。5.3 ONNX导出与输入尺寸一致性验证导出RestNet18时要固定输入尺寸为224×224避免动态尺寸导致后续TensorRT优化失效。导出YOLOv5时注意它的输出层包含三个尺度的检测头ONNX导出脚本里要指定--img-size 640。验证导出的模型时我习惯用同一张测试图分别跑PyTorch模型和ONNX模型比对输出差值的最大绝对值如果超过1e-3优先检查图像的预处理顺序尤其是letterbox填充的灰边填充值是否与训练时一致。ONNX Runtime下做INT8推理时Confidence阈值要从默认的0.25调高到0.4因为量化会放大低置信度框的噪声。本文还有配套的精品资源点击获取
返回列表