简介:本资源是一套面向计算机视觉方向本科生与研究生的骨龄检测毕业设计完整实现方案,聚焦医学影像中手腕X光片的关键骨骼区域定位与骨龄评估任务。项目创新性融合YOLOv5目标检测模型与ResNet18分类网络,先精确定位桡骨、尺骨及掌指骨等12类关键骨骼,再基于裁剪区域进行骨龄分级预测,具备临床辅助诊断潜力。压缩包共200个文件,含69个Python主程序与工具脚本(含训练/推理/可视化模块)、53个YAML/YML配置文件(涵盖数据增强、超参调优、模型结构定义)、18个预训练及微调后的.pth模型权重,以及Dockerfile、CITATION.cff、README.md等工程化支持文件,整体大小717.06MB。目前已有435人学习下载,所有代码均经导师指导与多轮调试验证,可直接运行完成端到端检测—分类全流程,附带TensorBoard日志文件(events.out.tfevents)便于结果复现与性能分析。
1. 项目背景与核心价值:为什么选择YOLOv5+ResNet18做骨龄检测?
最近在整理硬盘里的老项目,翻到了一个当年做毕设时留下的“宝藏”——一个基于YOLOv5和ResNet18实现的骨龄检测系统。这个项目在当时拿到了不错的分数,更重要的是,它完整地走通了从数据处理、模型训练到部署评估的全链路,踩过的坑和总结的经验,对于现在想入门医学影像分析或者目标检测+分类任务的朋友来说,应该还有点参考价值。骨龄检测,简单说就是通过分析左手腕部的X光片,评估骨骼的发育成熟度,在儿科、体育选材、司法鉴定等领域都有应用。传统方法依赖医生肉眼观察特定骨化中心的形态,主观性强、耗时费力。用深度学习来做,核心思路就是“先定位,再评估”。
那为什么是YOLOv5+ResNet18这个组合?这得从任务特性说起。一张手腕X光片里,我们需要关注的是多个特定的骨化中心(比如桡骨远端、掌骨、指骨等)。直接整图输入一个分类网络(比如单纯的ResNet)效果不会好,因为无关背景区域太多,噪声大。所以,第一步得先把这些关键区域“框出来”。YOLOv5作为单阶段目标检测的佼佼者,速度快、精度高、生态完善,非常适合完成这个定位任务。定位出每个关键骨块后,第二步才是对每个裁剪出来的小图(骨块)进行发育成熟度分级,这本质上是一个细粒度的图像分类任务。ResNet18模型深度适中,在ImageNet上预训练的特征提取能力很强,同时参数量不算太大,对于这种分类任务,既能保证精度,又不会让整个系统过于臃肿,非常适合作为“骨干网络”来微调。这个“检测+分类”的两阶段Pipeline,思路清晰,模块化程度高,也便于后续分别优化。
这个项目包(基于yolov5+RestNet18实现的骨龄检测源代码+模型+数据集(高分毕设项目).zip)里通常应该包含几个部分:用于训练YOLOv5的手腕X光片标注数据集(YOLO格式)、用于训练ResNet18的已裁剪骨块图像数据集(按类别分文件夹)、YOLOv5和ResNet18的训练与推理源代码、训练好的模型权重文件(.pt和.pth),以及可能的一些工具脚本(如数据预处理、结果可视化等)。下面,我就结合这个经典框架,拆解其中的关键技术点、实操步骤以及那些容易踩坑的细节。
2. 环境搭建与数据准备:避开依赖冲突与标注陷阱
动手之前,一个干净、可控的环境是成功的基石。这个项目涉及两个相对独立的模型训练,对环境的要求略有不同。
2.1 创建隔离的Python环境
强烈建议使用conda或venv创建独立的虚拟环境,避免包版本冲突。这里以conda为例:
# 创建新环境,Python版本建议3.8,兼容性最好 conda create -n bone_age python=3.8 conda activate bone_age # 安装PyTorch,请根据你的CUDA版本去官网获取对应命令 # 例如,对于CUDA 11.3 pip install torch==1.12.1+cu113 torchvision==0.13.1+cu113 torchaudio==0.12.1 --extra-index-url https://download.pytorch.org/whl/cu113 # 安装YOLOv5所需依赖 git clone https://github.com/ultralytics/yolov5 # 如果项目包里没有自带yolov5源码 cd yolov5 pip install -r requirements.txt # 这会安装opencv-python, pandas, seaborn等 # 返回项目根目录,安装其他可能需要的库 cd .. pip install scikit-learn matplotlib openpyxl tqdm注意:YOLOv5的
requirements.txt可能会安装特定版本的numpy、opencv-python等。如果后续运行ResNet18代码时出现不兼容,可以尝试先满足YOLOv5的要求,再单独升级或降级某个出问题的包。一个常见的冲突点是numpy版本,如果遇到AttributeError: module 'numpy' has no attribute 'int'这类错误,通常将numpy降级到1.23.5或类似版本可以解决:pip install numpy==1.23.5。
2.2 解析与准备数据集
数据集通常是项目里最珍贵也最棘手的部分。假设你的项目包里有两个核心数据集:
原始手腕X光片及YOLO标注文件:用于训练YOLOv5检测模型。目录结构可能如下:
/yolo_dataset /images train/ hand_001.jpg hand_002.jpg ... val/ hand_101.jpg ... /labels train/ hand_001.txt hand_002.txt ... val/ hand_101.txt ...每个
.txt标注文件对应一张图片,每行格式为:<class_id> <x_center> <y_center> <width> <height>,坐标是归一化后的(0-1之间)。class_id对应不同的骨块类型,比如0=桡骨远端,1=第一掌骨近端等。裁剪后的骨块图像数据集:用于训练ResNet18分类模型。这是用训练好的YOLOv5模型(或人工)从原始图中裁剪出各个骨块后,按发育等级(如G1, G2, ..., G8)分类存放的。结构如下:
/classification_dataset /radius_distal (桡骨远端) /G1 img_001.jpg ... /G2 ... /metacarpal_I (第一掌骨) /G1 /G2 ...
关键操作与避坑点:
- 数据检查:首先用脚本或肉眼抽查YOLO标注是否正确。一个快速检查的方法是使用YOLOv5自带的
utils.plots模块画框查看。from utils.plots import plot_images import cv2 # ... 读取图片和label,组装成batch_n plot_images(imgs, batch_n, paths=None, fname='test_batch0.jpg') - 类别平衡:检查分类数据集中各个类别(骨块类型x发育等级)的样本数量。严重不平衡会导致模型偏向多数类。对于小样本类别,可以考虑使用数据增强(旋转、亮度对比度调整等)来扩充,或者在损失函数中使用
weighted CrossEntropyLoss。 - 数据划分:确保训练集、验证集、测试集的划分是患者级别的,而不是图像级别的。即同一个患者的所有X光片应该只出现在其中一个集合中,避免数据泄露,导致模型评估结果虚高。这在医学图像分析中至关重要。
- 图像预处理:对于X光片,常见的预处理包括:归一化(如/255.0)、调整对比度(CLAHE)以增强骨骼细节。这些操作可以在数据集加载的
__getitem__函数中完成。
3. 第一阶段:YOLOv5骨块检测模型训练与调优
定位是否准确,直接决定了后续分类的输入质量。YOLOv5的训练流程已经非常标准化,但仍有细节需要注意。
3.1 配置文件准备与修改
YOLOv5使用.yaml文件来定义模型和数据。你需要准备两个文件:
数据配置文件(
bone_detection.yaml):# 数据集路径,相对于train.py的位置,或绝对路径 path: /path/to/your/yolo_dataset train: images/train val: images/val # test: images/test # 如果有测试集 # 类别数量 nc: 8 # 假设你要检测8种不同的骨块 # 类别名称列表,顺序必须与标注文件的class_id对应 names: ['radius_distal', 'ulna_distal', 'metacarpal_I', 'metacarpal_III', 'metacarpal_V', 'proximal_phalanx_I', 'proximal_phalanx_III', 'proximal_phalanx_V']模型配置文件:可以直接使用YOLOv5提供的,如
yolov5s.yaml(小模型)。只需修改其中的nc参数为你数据集的类别数(同上文的8)。
3.2 启动训练与核心参数解析
使用YOLOv5的train.py脚本进行训练。一个典型的启动命令如下:
python train.py --img 640 --batch 16 --epochs 100 --data bone_detection.yaml --cfg yolov5s.yaml --weights yolov5s.pt --name bone_det_exp1 --cache--img 640: 输入图像尺寸。X光片通常是高分辨率,但YOLOv5训练时会统一缩放到此尺寸。640是一个在速度和精度间平衡较好的值。如果你的骨块非常小,可以考虑增大到832或1024,但会显著增加显存消耗和训练时间。--batch 16: 批次大小。取决于你的GPU显存。在RTX 3080 (10GB)上,640尺寸下batch=16通常可行。如果出现CUDA out of memory,减小batch,或尝试使用--multi-scale训练(动态缩放图像尺寸)。--epochs 100: 训练轮数。对于中等规模数据集(几千张图),100-150轮通常足够。可以通过观察验证集mAP曲线来判断是否早停。--weights yolov5s.pt: 加载预训练权重。强烈建议使用预训练权重,这能极大加速收敛并提升最终精度。yolov5s.pt是在COCO数据集上预训练的。--name bone_det_exp1: 实验名称。所有输出(模型权重、日志、可视化结果)都会保存在runs/train/bone_det_exp1目录下。--cache: 将图像缓存到内存或磁盘,可以加速训练。首次训练时会慢一点,因为要创建缓存。
3.3 训练监控与性能解读
训练开始后,YOLOv5会在终端打印日志,并在runs/train/bone_det_exp1目录下生成一系列重要文件:
results.csv/results.png: 记录所有epoch的损失函数值和评估指标。重点关注的指标是mAP@0.5和mAP@0.5:0.95。前者是IoU阈值为0.5时的平均精度,后者是在多个IoU阈值(0.5到0.95,步长0.05)下的平均mAP,更严格。对于骨块检测,我们更关心mAP@0.5,因为后续分类任务对框的绝对精确度要求不是极端高,但必须能框住目标。weights/best.pt: 验证集上表现最好的模型权重。val_batchX_labels.jpg&val_batchX_pred.jpg: 验证集的标签和预测可视化,用于直观检查检测效果。
如果mAP值不理想或收敛慢,可以从以下几个方面排查:
- 数据问题:再次检查标注质量。小目标(某些骨块)是否漏标?标注框是否准确?可以使用
python detect.py --weights best.pt --source path/to/val/images --save-txt生成预测框,并与真实框对比。 - 锚框(Anchor)不匹配:YOLOv5默认使用COCO数据集上聚类得到的锚框尺寸。对于医学X光片中的骨块,目标尺寸分布可能不同。可以尝试在自己的训练集上重新聚类锚框:
如果输出的python utils/autoanchor.py --data bone_detection.yamlbest possible recall低于0.98,说明锚框匹配度不高,建议使用聚类得到的新锚框更新你的模型配置文件(yolov5s.yaml中的anchors部分),然后从头开始训练。 - 超参数调优:可以适当调整学习率
--lr0(默认0.01),对于小数据集可以调小,如0.001。也可以启用数据增强,如--mosaic 1.0(默认开启),--mixup 0.1等,但医学图像增强需谨慎,避免产生不合理的生理结构。
4. 第二阶段:基于ResNet18的骨块发育等级分类
检测模型产出的是一个个边界框。我们需要根据这些框,从原图中裁剪出各个骨块区域,然后送入分类网络判断其成熟度等级。
4.1 利用YOLOv5模型生成分类数据集
首先,你需要用训练好的最佳检测模型(best.pt)对整个数据集(包括训练、验证、测试集)进行推理,并保存裁剪后的骨块图像。这可以通过修改YOLOv5的detect.py或自己写脚本实现。核心思路是:
- 加载模型,对每张图片进行推理,得到预测框(
xyxy格式,像素坐标)。 - 根据每个预测框的坐标和类别ID,从原图中裁剪出对应区域。
- 将裁剪出的小图,根据其类别ID和人工标注(或已有)的发育等级,保存到
/classification_dataset/类别名/等级名/目录下。
这里有一个关键点:分类数据集的标签从哪里来?在完整的毕设流程中,这通常是一个独立的标注步骤。你需要有专家对每个骨块的发育等级进行评定(例如Tanner-Whitehouse分期法)。在项目包里,可能已经提供了这个标注文件(如一个CSV或JSON文件,记录了图片名、骨块类型、发育等级)。你需要根据这个标注文件,在裁剪时给图像打上正确的等级标签。
如果项目包没有提供,一种简化方法是:你可以先用检测模型在训练集上推理并裁剪,然后手动或借助一些半自动工具(比如把同一骨块的所有裁剪图展示出来人工打分)来为这些裁剪图标注等级。这虽然费时,但却是构建可靠分类模型的基础。
4.2 构建ResNet18分类模型
PyTorch提供了预训练的ResNet18,我们可以轻松地进行微调。
import torch import torch.nn as nn import torchvision.models as models from torchvision import transforms # 1. 定义数据预处理和增强 # 训练集增强可以强一些,验证/测试集只需标准化 train_transform = transforms.Compose([ transforms.RandomResizedCrop(224), # 随机裁剪并缩放到224x224 transforms.RandomHorizontalFlip(), transforms.RandomRotation(10), transforms.ColorJitter(brightness=0.2, contrast=0.2), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) # ImageNet统计量 ]) val_transform = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) # 2. 加载预训练ResNet18并修改最后一层 def get_model(num_classes): model = models.resnet18(pretrained=True) # 加载在ImageNet上预训练的权重 num_ftrs = model.fc.in_features # 获取全连接层的输入特征数 model.fc = nn.Linear(num_ftrs, num_classes) # 替换为新的全连接层,输出为类别数 return model # 假设桡骨远端有8个发育等级 num_classes = 8 model = get_model(num_classes) # 3. 定义损失函数和优化器 criterion = nn.CrossEntropyLoss() # 如果类别不平衡,可以给CrossEntropyLoss传入weight参数 # class_weights = torch.tensor([1.0, 2.0, ...]) # 少数类权重更大 # criterion = nn.CrossEntropyLoss(weight=class_weights) optimizer = torch.optim.Adam(model.parameters(), lr=0.001) # 也可以使用SGD with momentum,通常需要更精细的调参 # optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.9) # 4. 学习率调度器 scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1) # 每10个epoch学习率乘以0.14.3 训练技巧与注意事项
- 分骨块训练 vs. 统一训练:你有两种策略。一是为每一种骨块(如桡骨、掌骨)训练一个独立的ResNet18分类器。优点是模型更专注,特征更纯净,且每个骨块发育等级数可能不同。二是将所有骨块图像混在一起训练一个大的多任务分类器,但需要统一所有骨块的等级体系(可能都映射到0-7),并在输入中告诉模型这是哪种骨块(例如通过添加一个骨骼类型编码通道)。前者更简单直观,也是项目中常见做法。
- 冻结底层,微调顶层:对于数据量不大的医学图像,一种有效的策略是先冻结ResNet18的大部分底层卷积层,只训练最后的全连接层几轮,然后再解冻所有层进行微调。这可以防止预训练特征被过快破坏。
# 冻结所有层 for param in model.parameters(): param.requires_grad = False # 只解冻最后一层 for param in model.fc.parameters(): param.requires_grad = True # 训练几轮后,再解冻所有层 for param in model.parameters(): param.requires_grad = True - 评估指标:分类任务主要看准确率(Accuracy)和混淆矩阵(Confusion Matrix)。对于等级预测,相邻等级的误判(如G3判为G4)比跨级误判(G3判为G7)更可接受。可以计算平均绝对误差(MAE),即预测等级与真实等级之差的绝对值平均值,这个指标在骨龄评估中非常直观。
- 过拟合应对:医学数据集通常较小,容易过拟合。除了数据增强,还可以使用
Dropout层(在修改model.fc时可以添加)、权重衰减(optimizer = Adam(..., weight_decay=1e-4))、以及早停(Early Stopping)策略。
5. 两阶段模型集成与推理流程
训练好检测和分类模型后,需要将它们串联起来,形成一个完整的端到端骨龄评估流程(虽然训练是两阶段,但推理可以流水线化)。
5.1 构建推理脚本
下面是一个简化的推理脚本框架,展示如何将两个模型结合起来:
import cv2 import torch import numpy as np from models.experimental import attempt_load # YOLOv5模型加载 from utils.general import non_max_suppression, scale_coords from classification_model import get_classifier # 你写好的分类模型加载函数 # 1. 加载模型 device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 加载检测模型 detect_weights = 'runs/train/bone_det_exp1/weights/best.pt' detect_model = attempt_load(detect_weights, device=device) detect_model.eval() # 加载分类模型(以桡骨为例) classify_weights = 'classification_models/radius_best.pth' classify_model = get_classifier(num_classes=8).to(device) classify_model.load_state_dict(torch.load(classify_weights, map_location=device)) classify_model.eval() # 2. 定义预处理和后处理函数 def preprocess_detect(img0, img_size=640): # 仿照YOLOv5的预处理:resize, pad, BGR->RGB, HWC->CHW, 归一化 img = letterbox(img0, img_size, stride=32)[0] # letterbox函数来自YOLOv5 utils img = img[:, :, ::-1].transpose(2, 0, 1) # BGR to RGB, HWC to CHW img = np.ascontiguousarray(img) img = torch.from_numpy(img).to(device) img = img.float() / 255.0 if img.ndimension() == 3: img = img.unsqueeze(0) return img, img0 def classify_crop(crop_img, classify_transform): # crop_img是RGB格式的numpy数组 input_tensor = classify_transform(Image.fromarray(crop_img)).unsqueeze(0).to(device) with torch.no_grad(): output = classify_model(input_tensor) _, predicted = torch.max(output.data, 1) return predicted.item() # 返回预测的等级索引 # 3. 主推理循环 def predict_bone_age(image_path): # 读取图像 img0 = cv2.imread(image_path) # 检测阶段 img, orig_img = preprocess_detect(img0) with torch.no_grad(): pred = detect_model(img)[0] pred = non_max_suppression(pred, conf_thres=0.25, iou_thres=0.45)[0] # 应用NMS if pred is None: print("未检测到任何骨块") return {} # 缩放坐标回原图尺寸 pred[:, :4] = scale_coords(img.shape[2:], pred[:, :4], orig_img.shape).round() results = {} for det in pred: x1, y1, x2, y2, conf, cls = det.tolist() cls = int(cls) # 根据cls确定骨块类型 bone_type = detect_model.names[cls] # 例如 'radius_distal' # 裁剪骨块区域 (注意坐标转换和边界检查) crop = orig_img[int(y1):int(y2), int(x1):int(x2), :] crop_rgb = cv2.cvtColor(crop, cv2.COLOR_BGR2RGB) # 分类阶段 # 需要根据bone_type选择对应的分类模型和transform if bone_type == 'radius_distal': maturity_grade = classify_crop(crop_rgb, radius_transform) # 假设已定义 results[bone_type] = {'bbox': [x1, y1, x2, y2], 'grade': maturity_grade} # ... 处理其他骨块类型 # 4. 综合评估骨龄(可选) # 根据所有骨块的发育等级,查表或使用回归模型得到最终骨龄(岁) # bone_age = calculate_bone_age(results) return results # 使用 result = predict_bone_age('test_hand.jpg') print(result)5.2 性能优化与部署考量
在实际应用中,尤其是考虑部署时,还需要考虑以下几点:
- 速度优化:YOLOv5的检测速度很快。瓶颈可能在多个分类模型的串行推理上。可以考虑:
- 将不同骨块的分类模型批量处理。即将所有裁剪出的骨块图像堆叠成一个batch,一次性通过分类网络(如果所有骨块用同一个分类器且等级数相同)。
- 使用模型剪枝、量化或转换为ONNX/TensorRT等推理引擎来加速。
- 对于实时性要求不高的场景,当前流程已足够。
- 错误处理与鲁棒性:
- 检测阶段可能漏检或误检。需要设置合理的置信度阈值(
conf_thres),并对检测到的骨块数量进行合理性检查(例如,一张手腕片应该能检测到~13个骨块)。 - 分类模型对于模糊、裁剪不佳的骨块图像可能置信度很低。可以输出分类的softmax概率,并设置一个阈值,低于该阈值的结果视为“不确定”,需要人工复核。
- 检测阶段可能漏检或误检。需要设置合理的置信度阈值(
- 结果可视化:将检测框、骨块类型和预测的发育等级绘制在原图上,便于医生或用户直观理解。
6. 项目扩展与进阶思考
完成基础流程后,这个项目还有很多可以深入和优化的方向,这也是毕设项目能拿高分的关键。
6.1 模型层面的改进
- 检测模型升级:可以尝试YOLOv5的不同变体(v5m, v5l, v5x)或在YOLOv5基础上加入注意力机制(如CBAM、SE),看是否能提升小骨块的检测精度。也可以尝试更新的检测器如YOLOv8或DETR系列,但需要考虑部署复杂度和数据需求。
- 分类模型升级:ResNet18是平衡之选。可以尝试更深的ResNet50、ResNet101,或者效率更高的EfficientNet、Vision Transformer (ViT) 模型。对于细粒度分类,可以关注
BN-Inception、ResNet-IBN等网络结构。关键是要做消融实验,对比不同模型在你的数据集上的性能(准确率、MAE、参数量、推理速度)。 - 端到端模型探索:两阶段流程虽然清晰,但并非最优。可以研究一些端到端的骨龄评估方法,例如:
- 关键点检测+分类:不检测矩形框,而是检测每个骨块的关键点(如四个角点或中心点),再根据关键点区域特征分类。
- 多任务学习:一个模型同时输出检测框和每个区域的发育等级。这需要更复杂的数据标注(每个实例既有框又有等级标签)。
- 基于区域提议网络(RPN)的两阶段检测分类一体化:类似Faster R-CNN,但第二个阶段不是简单分类,而是细粒度等级分类。
6.2 数据与训练策略的深化
- 更丰富的数据增强:针对X光片的特性,可以尝试更专业的增强,如模拟不同拍摄条件的灰度变换、添加高斯噪声模拟图像噪声、弹性形变等。但要注意增强的合理性,不能改变骨骼的解剖结构。
- 利用公开数据集:RSNA Pediatric Bone Age Challenge等公开竞赛提供了大量标注数据。可以尝试在这些数据上预训练你的模型,再在自己的数据上微调(迁移学习),尤其当你的数据量较小时,效果提升会非常明显。
- 不确定性估计:在医学应用中,知道模型“有多不确定”和给出预测结果同样重要。可以引入蒙特卡洛Dropout或使用贝叶斯神经网络来估计预测的不确定性,对于低置信度的预测,系统可以标记出来交由专家判断。
6.3 工程化与系统集成
- 构建Web服务:使用Flask或FastAPI将你的模型封装成RESTful API,方便集成到医院的PACS系统或开发独立的评估软件。
from fastapi import FastAPI, File, UploadFile import uvicorn app = FastAPI() @app.post("/predict/") async def predict(file: UploadFile = File(...)): contents = await file.read() # 将contents转换为图像,调用上面的predict_bone_age函数 result = predict_bone_age_from_bytes(contents) return result - 开发简单的用户界面:使用Gradio或Streamlit快速构建一个交互式Demo,允许用户上传X光片,实时查看检测框和骨龄结果,这对于项目演示非常有帮助。
- 模型版本管理与持续集成:使用MLflow或DVC来跟踪每次训练的模型版本、超参数和性能指标,确保实验的可复现性。
回过头看,这个基于YOLOv5+ResNet18的骨龄检测项目,是一个非常好的深度学习入门和进阶的练手项目。它涵盖了计算机视觉中目标检测和图像分类两大核心任务,并且有着明确的医学应用背景。从环境配置、数据处理、模型训练调优到最后的集成推理,每一步都会遇到典型的问题。把这些问题解决了,你对深度学习项目开发的全流程就有了扎实的实践经验。最后要强调的是,任何医学辅助诊断模型,其最终目的都是辅助医生,而不是替代医生。模型的输出结果必须清晰、可解释,并且要明确指出其局限性,在实际临床应用中需要严格的验证和审批流程。
本文还有配套的精品资源,点击获取