ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于YOLOv5与ResNet18的手骨骨龄检测全流程解析

基于YOLOv5与ResNet18的手骨骨龄检测全流程解析 简介这是一份基于PythonYOLOv5实现手骨骨龄检测的完整项目资源面向毕业设计、课程设计及实际项目开发场景适合具备一定Python基础并希望掌握目标检测与图像分类流程的开发者。资源涵盖源码、项目文档、模型训练权重、数据集和视频演示帮助使用者从数据准备到模型部署全链路快速落地。压缩包共189个文件大小约436.79MB文件类型包括Python脚本、YAML配置文件、PyTorch权重文件.pt、Markdown说明文档及IPython Notebook等覆盖数据预处理、模型训练与结果验证环节。项目中采用自适应直方图均衡化处理雾感图片并实现数据集划分与图像增强侦测模型基于YOLOv5配置分类模型采用ResNet18对DIP、MCP、PIP等关节分类测试准确度达90%以上模型权重已含在包内。当前已有198人学习下载适合课程设计、毕业设计答辩或作为目标检测与分类综合项目参考。1. 基于pythonyolov5实现的手骨骨龄检测先搞清楚这份资源到底能干什么手骨骨龄检测是我最近拆完的一套毕业设计级项目资源技术栈是pythonyolov5走的是「检测定位 分类分级」的两段式路线拿来交课程设计、毕业设计或者自己练手都很合适。它跟网上那些只给个demo的源码包不太一样——数据预处理脚本、yolov5训练配置、ResNet18分类网络、训练好的权重、视频演示是完整串起来的不是黑匣子。它解决的问题很具体从一张手骨X光片里先定位出9个关键关节区域再对每个区域做发育程度分级最终服务于骨龄评估。这中间最容易翻车的不是模型选型而是数据集的雾感处理和检测/分类两套模型的衔接。下面我从数据处理开始把每个环节的参数、脚本和坑都拆开讲。2. 先解决雾感CLAHE、幂次增强与数据集切分的完整预处理链路2.1 为什么手骨X光片必须先做直方图均衡化拿到这份资源里的数据集第一件事不是急着开训而是看一眼图片长什么样。很多手骨X光片都有明显的雾感也就是像素灰度值几乎挤在某个狭窄区间整张图对比度很低骨头的边缘和软组织边界都糊在一起。这种图直接丢给yolov5模型学到的特征会非常弱mAP很难过0.5再怎么调epochs都是白费功夫。雾感的本质是直方图分布过于集中解决办法无非就是直方图均衡化。但普通的全局均衡化cv2.equalizeHist在X光片上效果很生硬会把背景噪声一起放大。资源里用的是自适应直方图均衡化也就是CLAHEContrast Limited Adaptive Histogram Equalization它对图像分块处理每一块单独做直方图均衡再通过clipLimit限制对比度放大的幅度这样既能去掉雾感又不会把噪声一起拉起来。2.2 用bone_createCLAHE.py去掉雾感参数与调用先看手骨检测数据的预处理对应的脚本是bone_createCLAHE.py。这个脚本做的事很纯粹遍历数据集目录下的所有图片逐张做CLAHE增强然后覆盖写回或输出到新目录。核心代码逻辑大致长这样import cv2 import os import glob def clahe_enhance(img_path, clip_limit2.0, tile_grid_size(8, 8)): # 读取图像X光片一般是单通道灰度图或三通道伪彩图 img cv2.imread(img_path) # 如果是三通道先转成灰度再增强后面再转回三通道保持统一 if len(img.shape) 3: gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) else: gray img # 创建CLAHE对象clipLimit控制对比度上限tileGridSize控制分块大小 clahe cv2.createCLAHE(clipLimitclip_limit, tileGridSizetile_grid_size) enhanced clahe.apply(gray) # 转回三通道避免后面yolov5加载时通道数不一致 enhanced_bgr cv2.cvtColor(enhanced, cv2.COLOR_GRAY2BGR) cv2.imwrite(img_path, enhanced_bgr) if __name__ __main__: image_dir datasets/bone/images for img_path in glob.glob(os.path.join(image_dir, *.jpg)): clahe_enhance(img_path) print(fprocessed: {img_path})这里clip_limit2.0是相对保守的值tile_grid_size(8, 8)是CLAHE的默认分块。我实际跑下来clip_limit在1.5到3.0之间对X光片效果都不错超过3.0之后骨纹理会被过度锐化训练时反而容易过拟合到噪声上。整个过程注意的一点是yolov5在训练时会自己再做一次mosaic和hsv增强所以CLAHE只做基础去雾就够了不要在预处理阶段把对比度拉得太狠否则和yolov5内部的增强策略叠加后图片会失真。2.3 split_dataset.py按比例切分训练集和测试集预处理做完下一步是切分数据。split_dataset.py做的事情是从原始图片和标注目录中按指定比例把数据随机划分到训练集和测试集。这个脚本很关键切分不合理的话后面训练出来的指标都是虚的。import os import random import shutil from sklearn.model_selection import train_test_split image_dir datasets/bone/images label_dir datasets/bone/labels train_img_dir datasets/bone/train/images train_lbl_dir datasets/bone/train/labels test_img_dir datasets/bone/test/images test_lbl_dir datasets/bone/test/labels # 获取所有图片名不带扩展名保证图片和标签一一对应 img_names [f.split(.)[0] for f in os.listdir(image_dir)] # 按8:2切分stratify用不上因为没有类别标签纯随机即可 train_names, test_names train_test_split( img_names, test_size0.2, random_state42 ) for name in train_names: shutil.copy(os.path.join(image_dir, name .jpg), train_img_dir) shutil.copy(os.path.join(label_dir, name .txt), train_lbl_dir) for name in test_names: shutil.copy(os.path.join(image_dir, name .jpg), test_img_dir) shutil.copy(os.path.join(label_dir, name .txt), test_lbl_dir)这里random_state42是固定随机种子保证每次运行切分结果一致方便复现。test_size0.2对应8:2切分如果手骨样本量少可以改成0.15。要特别注意的是图片名和标注文件名必须严格一致否则yolov5在训练时找不到对应标注会直接跳过这张图而且不会报错只会安静地在日志里输出一条warning。我看过不少人是栽在这个细节上的数据集明明很大训练loss也正常下降但mAP就是起不来最后发现是图片和标签名字对不上一半的数据根本没参与训练。2.4 关节图怎么做增强pic_power.py的幂次变换手骨检测数据处理好之后还有一套关节数据就是九个关键区域DIP、PIP、MCP这些的局部图。这部分资源里用的是bone9_createCLAHE.py先做同样的去雾然后pic_power.py做图像增强。幂次变换的公式是s c * r^gammagamma小于1时能提升暗部细节gamma大于1时压暗亮部相当于人为制造不同的曝光条件来扩充样本量。import cv2 import numpy as np import os def power_transform(img, gamma0.6): # 幂次变换先归一化到[0,1]再做gamma映射 normalized img / 255.0 enhanced np.power(normalized, gamma) # 乘回255并转uint8 result (enhanced * 255).astype(np.uint8) return result def augment_image(img_path, output_dir, gammas[0.6, 0.8, 1.2, 1.5]): img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) base_name os.path.splitext(os.path.basename(img_path))[0] for idx, gamma in enumerate(gammas): aug_img power_transform(img, gamma) out_path os.path.join(output_dir, f{base_name}_gamma{idx}.jpg) cv2.imwrite(out_path, aug_img) # 对每张原始关节图生成4个增强版本 for img_path in os.listdir(datasets/bone9/raw): augment_image(os.path.join(datasets/bone9/raw, img_path), datasets/bone9/augmented)这份资源里把每个关节类别增强到每份1800张然后再按比例随机抽样到test目录下。增强完毕后统一做尺寸归一化也就是resize到同一大小这一步不能省因为后面分类网络是全连接输出输入尺寸必须固定。gamma值的选取可以看直方图分布来定如果原图整体偏暗优先选0.6、0.8这两个小于1的gamma值提亮如果原图偏亮就选1.2、1.5压暗。我建议不要对每一类都用同样的gamma组合先抽样看几张大图不同关节的骨骼密度不一样增强参数应该有所区分。3. 检测模型训练bone.yaml、yolov5s.yaml与train.py的关键改动3.1 骨骼检测要检测什么7类目标与标注含义预处理完成之后进入yolov5检测模型训练。这个环节里yolov5负责的是「找出手骨图上所有关键关节区域」输出的是带类别和边界框的检测结果。资源里的bone.yaml配置了7个类别这7类是从九大关节区域里合并归纳出来的标注工作在labelImg这类工具里完成格式是yolo默认的txt格式每行对应一个目标类别id、中心点x、中心点y、宽度w、高度h全部是归一化到[0,1]的值。为什么是7类而不是9类因为从9个区域里有几个在检测阶段不需要做区分先统一检测出来待分类阶段再做细分。这种「粗检测 细分类」的级联思路在这类医疗影像项目里非常常见比一次性检测9类要稳得多因为相邻关节在X光片上的尺度差别很小检测头很难区分但分类网络分割开就轻松很多。3.2 bone.yaml数据集的路径与类别描述bone.yaml是在克隆下来的yolov5项目里新建的配置文件作用是告诉yolov5训练时去哪里找数据、数据有多少类。它的内容结构是固定的# bone.yaml - 手骨检测数据集配置 train: datasets/bone/train/images val: datasets/bone/val/images nc: 7 names: [DIP, DIPFirst, MCP, MCPFirst, MIP, PIP, Radius, PIPFirst, Ulna]严格来说如果nc7那么names列表也要是7个名字上面这段是按9个区域写的示例实际使用时以你标注时的类别清单为准。train和val后面的路径是相对yolov5项目根目录的路径这个最容易出错——很多人把yaml文件放在datasets目录下路径写的是相对datasets的结果yolov5报错找不到图片。我的习惯是yaml文件放在yolov5项目根目录下路径统一写成相对项目根的相对路径这样最省心。3.3 yolov5s.yaml把nc改成7之后还要检查什么yolov5s.yaml是模型结构配置文件定义了网络的深度和宽度。这份资源里要求把分类数量改为7也就是nc: 7但要提醒的是yolov5s.yaml里的nc只在创建模型时生效如果你的训练脚本里同时传入了--data参数指定的yaml文件而那个文件中也有nc字段yolov5实际使用的是data yaml里的ncmodel yaml里的nc会被覆盖。所以正确做法是两个文件里的nc都改成7保持一致。# yolov5s.yaml 中的关键配置 nc: 7 # number of classes必须与bone.yaml中的nc一致 # 下面的depth_multiple和width_multiple控制模型大小 depth_multiple: 0.33 width_multiple: 0.50depth_multiple和width_multiple不用动yolov5s本身就是轻量版手骨检测的目标尺寸不大不需要上yolov5m或yolov5l。在实际训练中我还遇到过一个问题如果是从官方仓库克隆的yolov5yolov5s.yaml的nc默认是80COCO数据集类别数只改model yaml不改data yaml的话训练能启动但类别读取会错位最终输出的检测框对应的类别名全部错乱这个现象在推理阶段才会暴露排查起来很痛苦。3.4 train.pyepochs、batch-size、imgsz这些参数怎么定训练入口是yolov5里的train.py这份资源里已经按手骨场景调整过默认参数。几个核心参数我逐个说下python train.py \ --data bone.yaml \ --cfg yolov5s.yaml \ --weights yolov5s.pt \ --epochs 120 \ --batch-size 16 \ --imgsz 640 \ --patience 20 \ --device 0--epochs设为120是一个合理起点手骨数据集通常不算大100到150个epoch足够收敛。if用的是默认300会浪费时间而且容易过拟合。--batch-size取决于显卡显存16对应8GB显存基本能跑如果显存只有4GB降到8或者把--imgsz从640降到512。这里有个真实教训imgsz不是越大越好手骨X光片通常是长条形直接resize到640x640会导致骨骼区域被纵向压缩变形。如果图片长宽比超过2:1建议先用letterbox方式padding到正方形或者干脆用--imgsz 640 --rect参数开启矩形训练yolov5会按原始长宽比组合batch省显存还保精度。--patience 20是早停参数验证集mAP连续20个epoch不提升就提前结束训练。这个参数对于课程设计场景很有用你可以挂机跑一晚上第二天起来看结果就行不用盯着tensorboard。训练完成后yolov5会在runs/train/exp目录下生成weights/best.pt和weights/last.ptbest.pt是验证集表现最好的权重后续推理和导出都优先用这个。4. 分类模型训练ResNet18在9个关节区域的实测表现4.1 手骨发育分级需要识别什么从区域定位到分级检测模型搞定之后接下来是分类模型。检测模型告诉你的是「图上有哪些关节区域、在哪个位置」但骨龄评估还需要知道每个关节的发育等级。这份资源里分类模型针对的是9个具体区域DIP、DIPFirst、MCP、MCPFirst、MIP、PIP、PIPFirst、Radius、Ulna。简单解释一下DIP是远端指间关节PIP是近端指间关节MCP是掌指关节MIP是大拇指指间关节Radius是桡骨Ulna是尺骨First后缀表示大拇指对应的关节。不同骨龄标准里这些区域的权重不同比如手腕部的Radius和Ulna在骨龄评分中占比最高所以单独拆分出来训练。4.2 my_net.py里的网络结构选择为什么是ResNet18资源里的my_net.py定义了自己的分类网络从实际验证来看ResNet18在9个区域上的测试准确度能到90%以上。为什么选ResNet18而不是更深的ResNet50或更轻的MobileNet原因有两方面。一是关节区域图本身经过裁剪和resize后分辨率有限图像信息量不大用ResNet50反而容易过拟合训练集准确率99%而测试集只有80%出头二是这套资源的定位是课程设计和毕业设计需要跑通全流程ResNet18在一张普通显卡上训练九分类任务单epoch时间比ResNet50快一倍不止。# my_net.py中的核心网络结构 import torch.nn as nn from torchvision.models import resnet18 class BoneClassifier(nn.Module): def __init__(self, num_classes9): super(BoneClassifier, self).__init__() # 使用预训练权重迁移学习收敛更快 self.backbone resnet18(pretrainedTrue) # 替换最后一层全连接输出9个关节的类别概率 in_features self.backbone.fc.in_features self.backbone.fc nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, num_classes) ) def forward(self, x): return self.backbone(x)替换最后一层全连接是迁移学习的标准做法pretrainedTrue会加载ImageNet预训练权重。这里有个小坑ImageNet预训练是针对自然图像的手骨X光片和它分布差异很大但迁移学习依然有效因为预训练权重学到了边缘、纹理、形状等底层特征这些对X光片同样适用。Dropout(0.3)是我测试下来比较稳的值0.5对于这个小数据集来说丢得太多收敛明显变慢。4.3 训练逻辑每个区域单独训练还是多任务统一训练资源里训练分类模型时的手法是先按区域拆分数据然后在同一个ResNet18框架下做统一的多分类训练9个类别分别对应9个发育等级输出。但要注意不同区域的等级划分粒度不完全相同有的区域等级少有的等级多。实际训练时需要对每个区域分别计算损失不能混在一起算。# 训练分类模型的关键逻辑 criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-4) for epoch in range(50): for images, labels in train_loader: outputs model(images) loss criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() # 每5个epoch在验证集上评估 if epoch % 5 0: eval_acc evaluate(model, val_loader) print(fepoch {epoch}, loss: {loss.item():.4f}, acc: {eval_acc:.2f}%)lr1e-4是迁移学习场景下的常见选择如果你用的是从头训练而不是加载预训练权重学习率可以调到1e-3。epochs50在这个数据集上是够用的分类任务比检测任务简单得多90%以上的准确率一般在前30个epoch就能达到。数据加载方面要注意训练分类模型时的预处理要和检测模型不同——分类阶段不需要做mosaic这类空间增强因为关节区域的解剖结构位置很固定过度的空间变换会让模型学到错误的位置特征。我一般只保留随机旋转±15度和水平翻转。5. 避坑指南训练时最容易翻车的五个环节5.1 CLAHE参数过猛导致骨纹理失真现象预处理后的图片对比度异常高骨骼边缘出现明显的白色光晕训练mAP反而比不做预处理还低。 原因clip_limit设置过大比如超过4.0CLAHE在每个分块内过度拉伸对比度把X光片的灰度层次压扁了。手骨X光片本身是16位深度的医学图像转成的8位图信息量有限过度增强会把软组织噪声放大成伪边缘。 解决把clip_limit控制在1.5到3.0之间tile_grid_size保持(8, 8)。如果不确定参数是否合适增强后随机抽20张图出来肉眼检查重点关注骨骼边缘是否出现不自然的白色描边。5.2 训练集和测试集出现同一个人不同手骨的图片现象训练时loss降得很快验证集mAP也很高但用全新图片推理时效果一塌糊涂。 原因split_dataset.py是按图片随机切分的没有考虑同一个人的多张手骨X光片可能同时出现在训练集和测试集。模型其实记住了个体特征而不是骨龄特征这是典型的数据泄漏。 解决按患者ID分组切分确保同一个人的所有图片只出现在训练集或只出现在测试集。如果原始数据里没有患者ID字段至少把同一时间拍摄的连号图片归为一组再切分。5.3 两个yaml文件的nc不一致现象训练能正常启动也不报错但训练完成后用best.pt推理输出的类别名称全是乱的检测框数量也不对。 原因yolov5在初始化模型时读取了yolov5s.yaml的nc比如改成了7但在数据加载阶段又读取了bone.yaml的nc可能是另外的值两者不一致导致类别映射错位。yolov5对这个问题没有显式校验只在日志里打印一条不起眼的警告。 解决每次训练前执行一遍grep检查grep -n ^nc: yolov5s.yaml bone.yaml确保两个文件输出的nc值完全一致再启动训练。5.4 增强后图片尺寸不统一导致训练报错现象分类模型训练时第一个epoch还没跑完就报错形如RuntimeError: stack expects each tensor to be equal size。原因pic_power.py增强后虽然统一resize了目标尺寸但python脚本执行过程中可能因为opencv的imread失败返回None导致有些图没有被resize直接以原始尺寸进入了DataLoader。这批问题图数量少但足以让batch维度对不齐。 解决在DataLoader之前加一个尺寸过滤凡是宽或高不等于预设值的图片直接跳过from PIL import Image valid_images [] for img_path in all_images: img Image.open(img_path) if img.size (224, 224): valid_images.append(img_path) else: print(fskip invalid size: {img_path}, size{img.size})5.5 显存不足时直接调低batch-size导致BN层失效现象显卡报OOM于是把batch-size从16调到4训练loss出现剧烈震荡完全无法收敛。 原因batch-size降到4之后BatchNorm层的统计量均值和方差基于4张图计算波动太大模型训练不稳定。yolov5的默认配置里BN层的momentum是针对较大batch的小batch下需要同步调整。 解决优先调低imgsz而不是batch-size比如从640降到512显存占用会下降36%左右。实在要降batch-size就把yolov5s.yaml里所有BN层的momentum从默认值调高到0.2以上同时把学习率从0.01降到0.002。6. 视频演示与推理验证把训练好的模型真正用起来训练完成后这份资源里的视频演示环节是验证模型是否真的能用的最短路径。yolov5自带的detect.py可以直接加载best.pt权重对手骨X光片视频做实时推理不需要再写额外的推理代码。python detect.py \ --weights runs/train/exp/weights/best.pt \ --source demo.mp4 \ --conf-thres 0.25 \ --iou-thres 0.45 \ --save-txt \ --save-conf--save-txt会输出每个检测框的类别、坐标和置信度到txt文件--save-conf把置信度一并写入。对于课程设计答辩来说视频演示能直观展示检测效果但评审老师更关心的是定量指标所以建议同时跑一遍测试集统计mAP和每类的precision、recallyolov5的val.py直接就能出这些数python val.py \ --data bone.yaml \ --weights runs/train/exp/weights/best.pt \ --task test \ --conf-thres 0.25 \ --iou-thres 0.5分类模型的验证更简单写一个遍历测试集目录的脚本统计每个区域的预测准确率输出混淆矩阵。我自己的习惯是拿到一份训练好的模型不是直接信指标而是抽20张训练时没见过的图手动标注一遍再和模型输出对比哪怕准确率掉个两三个百分点也比跑一堆指标更让人心里踏实。最后说一个我踩过的坑在用这份资源时图省事直接拿默认参数训练结果检测模型的mAP只有0.6出头分类准确率倒是过的去。后来回头核查发现是数据切分那一步出了问题——PIP和PIPFirst两个相近类别的图片在切分时没有做类别均衡导致测试集里PIPFirst样本只有个位数准确率虚高。从那以后我每次做完数据切分都会强制跑一遍类别分布统计确认每个类别在训练集和测试集里的占比和总体一致才进入下一步。做医疗图像相关的项目数据分布永远是第一位的模型只是把你的数据质量映射成结果。希望这份拆解能帮你少走几步弯路把资源里的流程真正跑起来。本文还有配套的精品资源点击获取
返回列表