ARTICLE DETAIL

资讯详情

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

手骨骨龄检测实战:YOLOv5目标检测+ResNet18分类的完整流程

手骨骨龄检测实战:YOLOv5目标检测+ResNet18分类的完整流程 简介面向毕业设计、课程设计与项目开发场景这份基于Python与YOLOv5的手骨骨龄检测资源提供了从源码、项目文档、模型训练到数据集和视频演示的完整闭环。项目源码已经过严格测试可直接参考并在此之上扩展处理流程中针对图像雾感采用直方图均衡化与CLAHE自适应均衡化并按比例拆分训练集和测试集同时通过图像增强把数据扩充至每份1800张。检测模型基于YOLOv5配置相应训练参数完成训练分类模型采用自定义网络与Resnet18对手骨DIP、MCP、PIP等关节区域测试准确度达到90%以上。压缩包共计189个文件以Python脚本、yaml/yml配置、pt训练权重、markdown文档和Dockerfile等为主包体约436.79MB目录结构便于按数据处理、模型训练和分类模块分别查阅。目前已有198人学习适合希望快速上手目标检测与骨龄评估全流程的开发者参考。1. 手骨骨龄检测把医学判读拆成两个可训练的阶段骨龄检测看起来是医学影像判读的活但落到工程实现上它其实是一个典型的两阶段视觉任务先用目标检测把关键解剖区域找出来再用分类网络对每个区域做成熟度分级。这套基于 Python YOLOv5 的手骨骨龄检测项目就是把医生看图的经验拆成了「检测 分类」两条流水线检测模型负责定位分类模型负责细判最终在测试集上做到了 90% 以上的分类准确度。对做毕业设计或课程设计的同学来说它的价值不只是有个能跑的模型而是整个流程从数据清洗、图像增强、数据集切分到模型训练、测试验证都是齐的代码结构和文档路径也都理清楚了。如果你是第一次接触 YOLOv5或者想看看一个完整的视觉项目到底要经历哪些环节这份资源很值得照着走一遍。2. 预处理手骨 X 光片直方图均衡化与数据增强2.1 先解决雾感问题拿到手骨数据集后第一件要做的事不是急着训练而是先看数据质量。很多 X 光片存在明显的雾感现象——像素灰度值集中在一个狭窄区间图像对比度低骨骼边缘和背景的区分度不够。这种图直接喂给模型梯度信号会被淹没检测框收敛得很慢。这个项目里用到了自适应直方图均衡化CLAHE来处理雾感对应的脚本是bone_createCLAHE.py。它跟普通直方图均衡化的区别在于普通方法是对整张图做一个全局灰度映射容易把噪声放大CLAHE 则是把图像分成多个小块在每个块内做均衡化再用双线性插值消除块与块之间的边界效应对比度提升更温和。import cv2 import numpy as np import os def apply_clahe(image): # 转换为 LAB 色彩空间只对 L 通道做 CLAHE lab cv2.cvtColor(image, cv2.COLOR_BGR2LAB) l, a, b cv2.split(lab) clahe cv2.createCLAHE(clipLimit3.0, tileGridSize(8, 8)) l_eq clahe.apply(l) lab_eq cv2.merge((l_eq, a, b)) return cv2.cvtColor(lab_eq, cv2.COLOR_LAB2BGR)这段代码的核心在cv2.createCLAHE的clipLimit和tileGridSize两个参数。clipLimit控制对比度限制幅度设太大会让骨骼边缘出现过曝设太小又起不到去雾效果一般 2.0 到 4.0 之间比较合理tileGridSize是分块粒度X 光片这类结构相对固定的图像8×8 是个稳妥的起点。处理完后再转回 BGR 色彩空间保持下游流程的兼容性。2.2 训练集与测试集的切分逻辑数据增强做完后需要一个干净的数据集目录结构。split_dataset.py负责把图片和标注文件按比例切分为训练集和测试集。常见的做法是把整个数据集目录扫描一遍按设定的比例随机抽样文件名保持不变这样 YOLOv5 在读取标注时不会出现图片和标签错位的问题。import os import random import shutil image_dir datasets/hand/images label_dir datasets/hand/labels train_ratio 0.8 images [f for f in os.listdir(image_dir) if f.endswith(.jpg)] random.seed(42) random.shuffle(images) split_point int(len(images) * train_ratio) train_images images[:split_point] test_images images[split_point:] for img_name in train_images: shutil.copy(os.path.join(image_dir, img_name), train/images/) label_name img_name.replace(.jpg, .txt) shutil.copy(os.path.join(label_dir, label_name), train/labels/)随机种子random.seed(42)在这里很关键。同一个数据集如果每次切分结果都不一样那你和别人之间的对比就失去了意义。固定种子后每次跑脚本得到的训练集和测试集划分完全一致踩坑排查时也好复现。另外注意标注文件名的替换逻辑——图片是.jpg标注是.txt如果数据集里混了.png或.jpeg替换逻辑就得跟着改。2.3 关节区域的图像增强手骨检测和关节检测在数据增强策略上略有不同。手骨区域大、结构单一直接 CLAHE 就够但关节区域小、类别密集数据量不够时模型很容易过拟合。项目里的bone9_createCLAHE.py负责对关节图片做同样的均衡化处理而pic_power.py则更进一步通过翻转、亮度抖动、对比度调整等手段把每类关节图片增强到 1800 张再按比例随机抽样到测试目录。import cv2 import numpy as np def augment_images(img, count): augmented [] for i in range(count): flipped cv2.flip(img, 1) # 水平翻转 alpha 0.8 0.4 * np.random.random() # 对比度系数 beta -30 60 * np.random.random() # 亮度偏移 adjusted cv2.convertScaleAbs(flipped, alphaalpha, betabeta) augmented.append(adjusted) return augmentedcv2.convertScaleAbs的alpha是对比度增益beta是亮度偏移。医学图像不像自然图像那样可以随便旋转——骨骼方向是有解剖学意义的翻转和轻微亮暗调整是安全操作旋转就得谨慎。这也是为什么增强脚本里没有用随机旋转的原因。3. YOLOv5 侦测模型训练配置文件与关键参数3.1 bone.yaml 数据集配置YOLOv5 训练的第一步不是调网络结构而是把数据集的描述文件写对。bone.yaml就是干这个的它告诉训练程序去哪里读图片、有哪些类别、类别名分别叫什么。这个项目里检测模型的类别数量改成了 7跟手骨和关节检测的粒度是对应的。train: datasets/hand/train/images val: datasets/hand/test/images nc: 7 names: [hand, MCP, PIP, DIP, MIP, Radius, Ulna]nc和names必须严格一一对应顺序错了模型就废了。这里有个容易翻车的地方YOLOv5 读取的标注文件中类别索引是从 0 开始的整数如果你的标注工具导出的类别名和names里的顺序不一致那模型学到的类别语义就是错的。拿到项目后第一件事应该是随机挑几张图片把标注框和类别名可视化出来确认一遍。3.2 修改 yolov5s.yaml 分类数量模型结构文件yolov5s.yaml里的nc也要同步改成 7。很多人只改了数据集配置忘了改模型配置训练直接报维度不匹配的错误。YOLOv5 的模型结构文件定义了每一层的通道数和输出维度最后的检测头输出张量形状和类别数密切相关nc对不上loss 计算就会崩。nc: 7 # number of classes depth_multiple: 0.33 width_multiple: 0.50如果你的手骨数据集比较大也可以考虑换成yolov5m.yaml或yolov5l.yaml。depth_multiple和width_multiple分别控制网络的深度和宽度倍数s 版本是 0.33 和 0.50m 版本是 0.67 和 0.75。手骨检测目标不算太小s 版本通常够用但如果你跑出来的 AP 偏低升级到 m 版本往往比调超参数更有效。3.3 train.py 训练参数调优YOLOv5 的训练入口是train.py项目里对几个关键参数做了定制。剪贴到自己的环境中后要注意--data、--weights、--epochs、--batch-size这几个参数必须跟你的机器配置匹配。预训练权重一般用 COCO 上训练好的yolov5s.pt它会提供更好的初始特征表达训练收敛速度比从零开始快不少。python train.py \ --data bone.yaml \ --weights yolov5s.pt \ --epochs 100 \ --batch-size 16 \ --imgsz 640 \ --device 0--imgsz默认是 640手骨 X 光片的原始分辨率可能更高但受限于显存一般不会直接拿原图尺寸训练。如果显存只有 8Gbatch-size得降到 8 甚至 4反过来如果显存充足加大 batch 对收敛有帮助。--device 0表示使用第一张 GPU只有 CPU 的机器就改成--device cpu但训练时间会显著拉长做课程设计倒是无所谓。3.4 超参数配置的边界YOLOv5 自带一组默认超参数在data/hyps/hyp.scratch-low.yaml里包括学习率、动量和数据增强的强度。项目没有改这个文件但你需要知道它是干嘛用的。学习率lr0默认是 0.01如果训练 loss 震荡得很厉害可以降到 0.001 再试mosaic数据增强默认是 1.0如果你觉得检测框不太准可以试着关掉 mosaic —— 它虽然能提升模型鲁棒性但在目标较小时也会增加学习难度。这些超参数不是越多越好每次改一个变量记录效果再动下一个。4. 关节分类模型用 ResNet18 替代自定义网络4.1 my_net.py 如何定义网络检测模型把关节区域框出来剩下的事情交给分类模型。项目里定义了自己的分类网络my_net.py实际效果最好的基础架构是 ResNet18。9 个分类目标分别是 DIP、DIPFirst、MCP、MCPFirst、MIP、PIP、PIPFirst、Radius、Ulna这些是手部骨骼和关节的解剖学术语对应的分类任务就是区分每个区域属于哪种类型。import torch import torch.nn as nn import torchvision.models as models class BoneClassifier(nn.Module): def __init__(self, num_classes9): super(BoneClassifier, self).__init__() self.backbone models.resnet18(pretrainedTrue) in_features self.backbone.fc.in_features self.backbone.fc nn.Linear(in_features, num_classes) def forward(self, x): return self.backbone(x)用pretrainedTrue加载在 ImageNet 上预训练的 ResNet18 权重再把最后一层全连接替换成 9 类输出。这里有个细节ImageNet 预训练权重是针对自然图像的X 光片和自然图像差异很大那为什么还要用预训练因为底层的边缘、纹理、形状特征的通用性是跨领域的虽然输入分布不同但底层特征提取器仍然能加速收敛。如果你的数据集足够大也可以试试pretrainedFalse但训练时间可能是预训练版本的三到五倍。4.2 关节区域分类的细节设计手骨检测和关节分类的粒度不同分类模型输入的是检测模型剪裁出来的 ROI 区域。剪裁区域的尺寸不统一所以项目里在预处理阶段把增强后的图片统一 resize 到了同一大小。分类网络的输入分辨率一般用 224×224这是 ResNet 的标准输入要改也是改成 128 或 96 而不是 320 —— 分类任务中目标已经居中不需要太高的分辨率去定位小目标。from torchvision import transforms transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])Normalize用的均值和标准差是 ImageNet 的统计值不是医学图像的。这是常见的做法因为预训练模型期望的输入分布就是这个。如果你的模型是在医学图像上从头训练的那就不能用这套参数。一个比较坑的地方是很多人会把检测阶段和分类阶段的预处理搞混——检测阶段用的是 YOLOv5 内部的 letterbox 逻辑分类阶段用的是 ResNet 的标准 transform两套东西不能互相替换。4.3 训练策略与准确率验证分类模型的训练脚本和 YOLOv5 的train.py是不同的一套代码PyTorch 分类模型一般用交叉熵损失函数优化器用 Adam 或 SGD。ResNet18 在测试集上达到 90% 以上准确率团队在这份资源里明确说效果较好。在复现时建议先只训练全连接层冻结底层的 BatchNorm 和卷积参数跑 10 个 epoch然后再解冻全网络微调。这个策略对医学图像小数据集特别有效能避免从一开始就破坏预训练学到的特征表达。optimizer torch.optim.Adam([ {params: model.backbone.parameters(), lr: 1e-5}, {params: model.backbone.fc.parameters(), lr: 1e-3} ], weight_decay1e-4)分层学习率的思路很简单底层特征是通用的用很小的学习率微调分类头是随机初始化的需要更大学习率快速收敛。这样能防止微调阶段把预训练权重冲得太远尤其是手骨数据集本身不大的情况下底层特征一旦被破坏就很容易过拟合。5. 避坑指南训练骨龄检测模型最容易踩的五个坑5.1 标注文件类别索引错位现象训练 loss 正常下降但验证时 mAP 始终很低检测框和标签对不上号。原因标注工具导出的类别名顺序和bone.yaml里的names顺序不一致比如标注软件里 MCP 是第 2 类YAML 里 MCP 放在第 3 位。解决训练前写个小脚本统计每个类别出现的次数和数据集标注里的类别分布核对一遍。再随机挑三张训练集图片把标注框和类别名可视化出来看一眼别嫌麻烦这一步能省下后面一整天的排查时间。5.2 CLAHE 参数在彩色和灰度空间的效果差异现象CLAHE 处理后骨骼边缘出现明显光晕训练时 loss 反而比不处理更高。原因X 光片本质是灰度图但文件可能是三通道保存的。直接在 BGR 三通道上做 CLAHE每个通道独立均衡化通道间的对比度差会被放大。解决先转成灰度图cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)对灰度图做 CLAHE再转回三通道格式。或者按 LAB 色彩空间只处理 L 通道这两种方式都能避免通道不一致的问题。5.3 数据增强只做膨胀不做验证现象增强后的训练集涨到 1800 张但验证集上准确率不高。原因增强生成的大量图片之间高度相似模型在训练集上见过几乎所有图像变换但遇到没见过的真实 X 光片时泛化能力不足。解决增强时保留一部分原始未增强的图像到验证集验证集不要有任何增强操作。增强是在辅助模型学不变性不是让验证集也跟着膨胀。5.4 预训练权重下载失败导致训练中止现象首次执行python train.py --weights yolov5s.pt时程序卡在下载权重文件的位置然后报连接超时。原因权重托管在 GitHub Releases 上网络环境受限时很难拉下来。解决准备一个离线包把yolov5s.pt下载好放到 YOLOv5 的根目录下然后--weights yolov5s.pt会直接读本地文件不会再触发下载逻辑。ResNet18 的预训练权重同理用torchvision下载失败时可以手动下载后放到~/.cache/torch/hub/checkpoints/目录里。5.5 训练集包含测试集图片现象训练时 loss 很低测试时效果也很完美但换一张全新的手骨图推理效果明显变差。原因split_dataset.py切分后没有排除重复文件名或者增强脚本把测试集的图片也复制回了训练目录。解决切分完成后做一次去重检查对比训练集和测试集的文件 MD5。更稳妥的做法是先把图片 ID 列表做好增强只对训练集的 ID 生效这样结构上就能保证不会交叉污染。6. 换图验证与模型导出检查训练结果是否真的能用训练完成后先别急着收工拿一张没有参与过训练的手骨 X 光片走一遍完整推理流程这是验证项目成果能不能交付的最直接方式。第一步是检测模型跑出关节区域第二步是分类模型对每个区域做判断两步串联起来的脚本就是一份可复现的推理 Demo。这里我用代码把整个流程串一下注释里写清楚每个环节的输入输出。import cv2 import torch # 加载检测模型 detect_model torch.hub.load(ultralytics/yolov5, custom, pathbest.pt) # 加载分类模型 classify_model BoneClassifier(num_classes9) classify_model.load_state_dict(torch.load(bone_classifier.pth, map_locationcpu)) classify_model.eval() img cv2.imread(test_hand.jpg) results detect_model(img) for det in results.xyxy[0]: x1, y1, x2, y2, conf, cls det.tolist() if conf 0.5: continue roi img[int(y1):int(y2), int(x1):int(x2)] roi_resized cv2.resize(roi, (224, 224)) # 转换为模型输入格式 tensor torch.from_numpy(roi_resized).permute(2, 0, 1).float() tensor tensor.unsqueeze(0) with torch.no_grad(): pred classify_model(tensor) pred_idx torch.argmax(pred, dim1).item() print(fbox: ({int(x1)}, {int(y1)}, {int(x2)}, {int(y2)}), class: {pred_idx})这里碰到的第一个坑是检测结果里会出现低置信度的误检框。置信度阈值设在 0.5手骨关节如果能给出足够强的特征这个阈值偏低。实际调参时我习惯先设到 0.6 或 0.7 看一波效果如果发现漏检再降回 0.5。torch.hub加载本地权重时会自动安装依赖但如果你的 PyTorch 版本和项目要求的版本不一致会出现_pickle.UnpicklingError这是因为权重文件里的序列化对象和当前环境的类定义对不上重新安装对应版本的 PyTorch 就能解决。第二个坑是分类模型在 CPU 上跑torch.load时如果你的权重是用 GPU 保存的需要加上map_locationcpu否则会报显存设备不匹配的错误。这个是在项目文档里最容易忽略的一行。整个项目走下来最值得养成的习惯是每跑通一个阶段就存一份完整的配置快照——bone.yaml复制一份备份、训练参数记录一波、权重文件做好命名标记。从那以后我每次拿到新的 X 光片数据都会强制走一遍「CLAHE → 切分 → 训练 → 验证」的完整流程而且每次只改一个变量确保模型效果的变化可归因。这个习惯让我的翻车率降低了至少一半。希望这些对你复现手骨骨龄检测项目有帮助。本文还有配套的精品资源点击获取
返回列表