ARTICLE DETAIL

资讯详情

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

基于YOLOv5与ResNet18的骨龄检测系统:从定位到分类的医学影像分析实践

基于YOLOv5与ResNet18的骨龄检测系统:从定位到分类的医学影像分析实践 简介本资源是一套面向计算机视觉方向本科生与研究生的骨龄检测毕业设计完整实现方案聚焦医学影像中手腕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. 项目背景与核心价值为什么选择YOLOv5ResNet18做骨龄检测最近在整理硬盘里的老项目翻到了一个当年做毕设时留下的“宝藏”——一个基于YOLOv5和ResNet18实现的骨龄检测系统。这个项目在当时拿到了不错的分数更重要的是它完整地走通了从数据处理、模型训练到部署评估的全链路踩过的坑和总结的经验对于现在想入门医学影像分析或者目标检测分类任务的朋友来说应该还有点参考价值。骨龄检测简单说就是通过分析左手腕部的X光片评估骨骼的发育成熟度在儿科、体育选材、司法鉴定等领域都有应用。传统方法依赖医生肉眼观察特定骨化中心的形态主观性强、耗时费力。用深度学习来做核心思路就是“先定位再评估”。那为什么是YOLOv5ResNet18这个组合这得从任务特性说起。一张手腕X光片里我们需要关注的是多个特定的骨化中心比如桡骨远端、掌骨、指骨等。直接整图输入一个分类网络比如单纯的ResNet效果不会好因为无关背景区域太多噪声大。所以第一步得先把这些关键区域“框出来”。YOLOv5作为单阶段目标检测的佼佼者速度快、精度高、生态完善非常适合完成这个定位任务。定位出每个关键骨块后第二步才是对每个裁剪出来的小图骨块进行发育成熟度分级这本质上是一个细粒度的图像分类任务。ResNet18模型深度适中在ImageNet上预训练的特征提取能力很强同时参数量不算太大对于这种分类任务既能保证精度又不会让整个系统过于臃肿非常适合作为“骨干网络”来微调。这个“检测分类”的两阶段Pipeline思路清晰模块化程度高也便于后续分别优化。这个项目包基于yolov5RestNet18实现的骨龄检测源代码模型数据集高分毕设项目.zip里通常应该包含几个部分用于训练YOLOv5的手腕X光片标注数据集YOLO格式、用于训练ResNet18的已裁剪骨块图像数据集按类别分文件夹、YOLOv5和ResNet18的训练与推理源代码、训练好的模型权重文件.pt和.pth以及可能的一些工具脚本如数据预处理、结果可视化等。下面我就结合这个经典框架拆解其中的关键技术点、实操步骤以及那些容易踩坑的细节。2. 环境搭建与数据准备避开依赖冲突与标注陷阱动手之前一个干净、可控的环境是成功的基石。这个项目涉及两个相对独立的模型训练对环境的要求略有不同。2.1 创建隔离的Python环境强烈建议使用conda或venv创建独立的虚拟环境避免包版本冲突。这里以conda为例# 创建新环境Python版本建议3.8兼容性最好 conda create -n bone_age python3.8 conda activate bone_age # 安装PyTorch请根据你的CUDA版本去官网获取对应命令 # 例如对于CUDA 11.3 pip install torch1.12.1cu113 torchvision0.13.1cu113 torchaudio0.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 numpy1.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, pathsNone, fnametest_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尺寸下batch16通常可行。如果出现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的损失函数值和评估指标。重点关注的指标是mAP0.5和mAP0.5:0.95。前者是IoU阈值为0.5时的平均精度后者是在多个IoU阈值0.5到0.95步长0.05下的平均mAP更严格。对于骨块检测我们更关心mAP0.5因为后续分类任务对框的绝对精确度要求不是极端高但必须能框住目标。weights/best.pt: 验证集上表现最好的模型权重。val_batchX_labels.jpgval_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.yaml如果输出的best 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(brightness0.2, contrast0.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(pretrainedTrue) # 加载在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(weightclass_weights) optimizer torch.optim.Adam(model.parameters(), lr0.001) # 也可以使用SGD with momentum通常需要更精细的调参 # optimizer torch.optim.SGD(model.parameters(), lr0.001, momentum0.9) # 4. 学习率调度器 scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size10, gamma0.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_decay1e-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, devicedevice) detect_model.eval() # 加载分类模型以桡骨为例 classify_weights classification_models/radius_best.pth classify_model get_classifier(num_classes8).to(device) classify_model.load_state_dict(torch.load(classify_weights, map_locationdevice)) classify_model.eval() # 2. 定义预处理和后处理函数 def preprocess_detect(img0, img_size640): # 仿照YOLOv5的预处理resize, pad, BGR-RGB, HWC-CHW, 归一化 img letterbox(img0, img_size, stride32)[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_thres0.25, iou_thres0.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来跟踪每次训练的模型版本、超参数和性能指标确保实验的可复现性。回过头看这个基于YOLOv5ResNet18的骨龄检测项目是一个非常好的深度学习入门和进阶的练手项目。它涵盖了计算机视觉中目标检测和图像分类两大核心任务并且有着明确的医学应用背景。从环境配置、数据处理、模型训练调优到最后的集成推理每一步都会遇到典型的问题。把这些问题解决了你对深度学习项目开发的全流程就有了扎实的实践经验。最后要强调的是任何医学辅助诊断模型其最终目的都是辅助医生而不是替代医生。模型的输出结果必须清晰、可解释并且要明确指出其局限性在实际临床应用中需要严格的验证和审批流程。本文还有配套的精品资源点击获取
返回列表