ARTICLE DETAIL

资讯详情

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

基于YOLOv5的骨龄检测实战:目标检测与回归两阶段管线解析

基于YOLOv5的骨龄检测实战:目标检测与回归两阶段管线解析 简介一套基于Python与YOLOv5的骨龄检测项目面向高校毕业设计、课程设计与项目开发场景适合计算机视觉及医学影像分析方向的学习者参考。资源包含完整源码、项目文档、数据模型、数据集与训练权重覆盖数据处理、模型训练、验证与导出等环节可直接用于骨龄估算实验的复现与二次开发。包内共25个文件以Python源码为主20个py文件涵盖训练、推理、预处理等模块同时配有YAML/Toml配置依赖与Markdown项目文档目录结构清晰方便按需定位与修改。压缩包整体仅95KB打包精简易于部署与迁移。目前已有107人学习下载。代码经过严格测试文档和配置能帮助理解YOLOv5在骨龄预测任务中的实现思路在此基础上可进一步扩展检测目标、对比不同模型效果性价比高适合作为课设、毕设的起点。1. 骨龄检测选 YOLOv5一套能交差也能发论文的落地组合在骨龄评估这件事上“读片”和“读码”的盲区恰好互补。放射科医生对着左手腕 X 光片按图谱对比骨骺发育等级单张读片三五分钟是常态遇到骨骺发育不典型的还要翻图谱、找参考而计算机视觉这边的现实是骨龄检测任务近几年在公开数据集上的主流方案已经从“纯分类”转向“检测 回归”YOLOv5 在其中承担的不是花活而是把 ROI 定位、预处理和特征提取串成一条够稳的流水线。这套方案的典型技术形态是先用 YOLOv5 检测手腕/手骨区域裁剪出骨骺区再挂一个回归头输出骨龄值或者直接把 TW3 分级当作目标检测来做。这篇笔记面向三类人拿骨龄检测做毕业设计的学生想把目标检测落地到医疗影像方向的研究生以及课程设计里需要“能跑、能讲、能答辩”的开发者。你会发现这套方案真正难的不是训练而是数据怎么标、结果怎么解释、失败的时候看哪个指标。我会把训练命令、标注转换脚本、避坑记录和验证方法逐一拆开讲确保你照着能复现出一版可演示的骨龄检测流程。2. 为什么是 YOLOv5 而不是直接端到端回归管线设计的取舍逻辑2.1 骨龄检测的任务本质检测是前提回归才是核心骨龄检测不是“看一张图输出一个数字”这么简单。临床评估常用的方法是 Greulich-PyleG-P图谱法和 Tanner-WhitehouseTW3评分法前者靠整张左手的 X 光片与标准图谱比对后者则把腕骨、桡骨、尺骨分成若干 ROI逐块打分加权。TW3 分级天然适合目标检测你要先找到每块骨骺的位置才能对每一块单独评级。直接端到端训练一个 ResNet 回归骨龄理论可行但模型会学偏——它可能只盯着图像里最亮的桡骨端忽略了尺骨和腕骨的发育信息。YOLOv5 在这套方案里不是用来直接输出骨龄的而是负责第一阶段检测出左手腕骨区域甚至检测出多个骨骺 ROI。它的必要性体现在三个层面裁剪后的 ROI 消除了背景干扰回归模型不需要在无意义的软组织区域浪费参数检测框天然提供了可解释性你能画出来“模型是根据哪些区域判断骨龄”答辩和调试都有据可查数据增强可以分别作用于检测和回归两个阶段比如对 ROI 做旋转、缩放不会破坏骨龄标签。所以常见的工程方案是YOLOv5 检测手部或分块骨骺然后用一个轻量回归网络ResNet18、EfficientNet 都行对每个检测框提取特征输出骨龄偏差值或 TW3 分级。严格说这不是模型创新但它是把目标检测能力迁移到医学影像任务里最稳的起点。2.2 YOLOv5 相比 YOLOv8 和 Faster R-CNN 的选型理由选型时最常被问的是“为什么不用 YOLOv8”。YOLOv8 确实更好用训练脚本更简洁精度略高但骨龄检测这类任务有一个特殊约束你需要的是可解释的中间产物检测框而不是单纯追求 mAP 高一点。YOLOv5 的生态更成熟网上有大量骨龄、手骨、X 光检测的预训练权重和调参笔记遇到问题搜得到答案YOLOv8 的 Anchor-Free 设计在某些小目标骨骺检测上表现不稳定而 YOLOv5 的 Anchor 机制经过充分调优后对腕骨这种尺度变化大的目标更友好。Faster R-CNN 精度不差但推理速度慢一个量级。骨龄检测最终要面向体检中心之类的高并发场景单张 X 光片推理速度如果超过 200ms上线压力会非常大。YOLOv5s 在 GPU 上单张推理能压到 10ms 以内配合 TensorRT 还能更快。对课程设计和毕业设计来说性价比特别高。还有一个实际考虑你拿到的预训练权重大多是 COCO 80 类头两层的低层特征对骨龄检测依然有效但网络最后几层需要改输出维度。YOLOv5 的模型配置文件是 YAML 结构改 nc类别数比 YOLOv8 更直观这对初学者非常友好。后面第 4 章会讲具体改法。2.3 方案总览检测 回归两阶段管线我的建议是不要一上来就把检测和回归揉成一个模型先用两阶段管线把流程跑通再考虑端到端优化。完整方案长这样阶段一YOLOv5输入原始左手 X 光片输出手部检测框或骨龄区域检测框。预处理按检测结果裁剪 ROI缩放至固定尺寸如 224x224做归一化和数据增强。阶段二回归网络对每个 ROI 提取特征输出骨龄值以月为单位或 TW3 分级。后处理多 ROI 结果融合加权平均或投票加上性别修正——这是骨龄检测特有的坑男孩和女孩的骨骺发育速度不同后面会专门讲。这套管线的优点是每一层的输入输出都清晰可控检测错了就去修标注回归不准就去调网络结构不会出现“模型不收敛但不知道是哪部分出了问题”的玄学局面。下面从数据准备开始逐步实现这条管线。3. 把公开骨龄数据集改造成 YOLOv5 能吃的格式转换脚本和边界坑3.1 数据集来源与标注形式做骨龄检测绕不开公开数据集。RSNA 在 2017 年举办过儿科骨龄挑战赛提供了上万张左手 X 光片和对应的骨龄标签单位月是训练回归模型的主要数据源。国内还有中华 05 标准的相关数据集但获取门槛更高。除了直接给骨龄值的数据集Tanner-Whitehouse 分级数据集会把每块骨头标注成 A、B、C、D、E、F、G、H、I 等级这种标注形式更适合 YOLOv5 做目标检测和分类的联合任务。互相关联的边界情况要注意RSNA 原始数据只有 CSV 格式的年龄标签和性别没有标记骨龄区域的检测框。常见做法是用预训练的 YOLOv5 模型在 COCO 上训练的先对 X 光片做一次推理看能不能框出骨骼区域然后手工标注几百张作为训练集。更省事的方案是直接以整张图为检测目标把“右手/左手”整体框出来但这样做回归网络还是会学到背景噪声。我推荐一个折中方案手标 500800 张检测目标设为“桡骨远端”“尺骨远端”“腕骨区”三个类别。这三个区域是 TW3 评分的关键 ROI训练 YOLOv5 做三分类检测后面再把每个检测框对应的区域接回归网络。对于骨龄变化明显的时期315 岁这三个区域的形态差异足够模型学到可区分特征。3.2 从 CSV 标注到 YOLO txt 格式的转换脚本YOLOv5 需要的标注格式是每张图片对应一个同名 .txt 文件每行内容为类别id cx cy w h其中 cx、cy 是中心点坐标w、h 是宽高全部相对图片尺寸归一化到 [0, 1]。如果数据集给的是 Pascal VOC 格式XML或 COCO 格式JSON需要先转换。如果数据集中没有现成检测框你得先用标注工具手工标。开源工具可以用 LabelImg比较简单也可以用 X-AnyLabeling支持自动标注插件。标注完成后从输出的 XML 或 JSON 转成 YOLO txt核心逻辑如下import os import xml.etree.ElementTree as ET def convert_xml_to_yolo(xml_path, out_dir, class_names): 将 Pascal VOC 格式标注XML转换为 YOLOv5 训练所需的 txt 格式。 class_names: [radius, ulna, carpal] 对应你的类别列表 tree ET.parse(xml_path) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) base_name os.path.splitext(os.path.basename(xml_path))[0] out_txt os.path.join(out_dir, base_name .txt) lines [] for obj in root.findall(object): cls_name obj.find(name).text if cls_name not in class_names: continue cls_id class_names.index(cls_name) bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) # 防止边界框越界X 光片经常有黑边标注时容易把框拖到图外 xmin max(0, min(xmin, img_w - 1)) xmax max(0, min(xmax, img_w - 1)) ymin max(0, min(ymin, img_h - 1)) ymax max(0, min(ymax, img_h - 1)) # 中心点坐标 归一化 cx (xmin xmax) / 2.0 / img_w cy (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h # YOLO 格式要求 w、h 必须 0过滤掉标成一条线的无效框 if w 0 or h 0: continue lines.append(f{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) with open(out_txt, w) as f: f.write(\n.join(lines)) return len(lines)这段代码的关键在于边界检查X 光片四周通常有黑色背景标注时很容易把框拖到黑边范围外归一化后坐标会失真另外过滤掉宽高为 0 的无效框能避免 YOLOv5 在训练时报 “all labels are empty” 错误。转换完成后要把图片路径和标注路径组织成 YOLOv5 要求的目录结构# YOLOv5 标准数据集目录结构 bone_age_dataset/ ├── images/ │ ├── train/ # 训练集图片 │ └── val/ # 验证集图片 ├── labels/ │ ├── train/ # 训练集 txt 标注 │ └── val/ # 验证集 txt 标注 ├── data.yaml # 数据集配置文件 └── test/ # 可选测试集图片3.3 data.yaml 的正确写法与类别平衡问题YOLOv5 训练前需要一个 data.yaml内容就是告诉框架数据集在哪、有几类、类别名叫什么。一个能直接跑的示例# data.yaml train: /absolute/path/to/bone_age_dataset/images/train val: /absolute/path/to/bone_age_dataset/images/val nc: 3 names: [radius, ulna, carpal]路径建议用绝对路径。YOLOv5 对相对路径的解析容易踩坑特别是你在项目目录和数据集目录之间切换时相对路径会静默指向错误的位置训练半天才发现没读对数据。类别平衡是骨龄检测里最容易被忽略的点。桡骨远端、尺骨远端和腕骨区在 X 光片上大小差异明显桡骨和尺骨是比较大的长条结构腕骨区则是一堆小骨头的集合锚框尺寸差异大。标准 YOLOv5 的默认锚框针对 COCO 数据集设计的对“大条状 小碎块”的混合场景不一定合适。可以先用 k-means 聚类重新计算锚框在后续训练命令里用--anchor参数传入。4. 训练骨龄检测模型修改模型配置、选参数、跑通最小命令4.1 修改 YOLOv5 模型配置从 COCO 80 类改成 3 类YOLOv5 的模型结构定义在models/目录下比如yolov5s.yaml。它开头的部分要改nc: 80改成nc: 3。这个改法很简单但深度上值得展开说一下。改类别数只影响 Detect 层的输出通道数特征提取部分骨干网络完全保留 COCO 预训练权重这也是迁移学习能成立的原因。如果你用的是 ultralytics 官方仓库建议复制一份 yaml 再改比如保存成yolov5s_bone.yaml# yolov5s_bone.yaml只展示头部和尾部改动 nc: 3 # number of classes原配置是 80骨龄检测只检测 radius/ulna/carpal # 中间部分不需要动backbone 沿用原网络结构 # Detect 层会自动根据 nc 调整输出维度无需手工改 anchors 数量YOLOv5 从 6.0 版本开始引入了 Anchor-Free 的 Decoupled Head 实验分支但默认模型仍是 Anchor-Based。对这个任务来说建议先用默认 Anchor等训练完看P_/R_指标如果小目标腕骨区召回率特别低再考虑用 K-means 聚类重新生成 Anchor。4.2 训练最小命令从预训练权重开始训练第一步先把预训练权重下载到本地。官方仓库的weights/目录下通常会留一个下载链接把yolov5s.pt放到weights/目录下然后执行训练命令python train.py \ --data /path/to/bone_age_dataset/data.yaml \ --weights weights/yolov5s.pt \ --epochs 300 \ --batch-size 16 \ --img 640 \ --device 0 \ --workers 8 \ --name bone_age_run参数的含义和精神--data指向刚才写好的 data.yaml--weights用 COCO 预训练权重做迁移学习初始化不要用--weights 从零训练X 光片和自然图像的低级特征边缘、纹理是共享的从零训练只会浪费时间--img 640是输入分辨率。X 光片原图通常超过 2000x2000直接缩放成 640 会丢失骨骺细节。我的做法是先用 640 跑通流程后续用--img 1024或--img 1280精调一轮mAP 一般有 24 个点的收益但这取决于显存够不够--batch-size 16在 11GB 显存如 RTX 2080Ti下跑 640 分辨率没问题如果显存不够优先降 batch 而不是降分辨率--workers 8是数据加载进程数Windows 上超过 4 容易出问题Linux 上 8 没问题训练过程中要盯两个指标loss 下降曲线和验证集的 mAP。如果 loss 前 50 轮还在大幅震荡可能是学习率默认值0.01偏大可以在命令里加--lr0 0.005降低初始学习率。骨龄检测数据集通常比 COCO 小得多几百张图训练 300 轮大概率过拟合所以建议开早停YOLOv5 的 train.py 内置了--patience参数比如设 100 表示 100 轮指标没提升就自动停止。4.3 回归网络让检测结果真正输出“骨龄月数”训练完 YOLOv5 拿到检测框不是终点你还需要一个回归模型把 ROI 映射到骨龄。最常见的做法很简单用 ResNet18 做回归骨架去掉最后的分类层替换成单节点输出。输入是检测框裁剪后的图像如 224x224x3输出是一个浮点数表示骨龄月数。关键是把任务当成回归而不是分类损失函数用 Smooth L1 Loss而不是交叉熵。下面是这个小模型的完整定义和训练循环骨架import torch import torch.nn as nn import torchvision.models as models class BoneAgeRegressor(nn.Module): 从检测框裁剪的 ROI 图像回归骨龄单位月。 注意输入张量需要先做 ImageNet 标准化mean/std 与预训练权重一致。 def __init__(self, pretrainedTrue): super().__init__() # 用 ResNet18 作为特征提取器参数量小X 光片上不易过拟合 self.backbone models.resnet18(weightsmodels.ResNet18_Weights.DEFAULT if pretrained else None) in_features self.backbone.fc.in_features self.backbone.fc nn.Identity() # 去掉分类层 self.reg_head nn.Sequential( nn.Linear(in_features, 128), nn.ReLU(inplaceTrue), nn.Dropout(0.3), # 骨龄数据噪声大Dropout 能防止过拟合 nn.Linear(128, 1) ) def forward(self, x): feat self.backbone(x) return self.reg_head(feat).squeeze(-1) # 输出 [B]每个样本一个骨龄值 # 损失函数SmoothL1 对离群值比 MSE 更稳 criterion nn.SmoothL1Loss() optimizer torch.optim.Adam(model.parameters(), lr1e-4) # 训练循环核心示意 for images, age_months in train_loader: optimizer.zero_grad() pred model(images) # images 是检测框裁剪图 batch loss criterion(pred, age_months.float()) loss.backward() optimizer.step()这里有一点必须重视回归网络输入图像的标准化不能随意选。如果直接载入 ImageNet 预训练权重就必须使用 ImageNet 的 mean/std[0.485, 0.456, 0.406] 和 [0.229, 0.224, 0.225]否则预训练的统计特性会被打破模型收敛会慢很多。这是很多做迁移学习的开发者在骨龄项目里翻车的第一个坑。性别信息也是必要的输入。同样的手腕 X 光片男孩和女孩的骨龄差距可以达到 12 个月以上。常见做法是把性别先编码成一个 one-hot 向量在回归头部拼接到特征后面# 性别编码0 男1 女 sex_tensor torch.nn.functional.one_hot(sex, num_classes2).float() # 在 backbone 特征后拼接 feat model.backbone(images) # [B, 512] feat torch.cat([feat, sex_tensor], dim1) # [B, 514] reg_head nn.Linear(512 2, 1) # 输入维度同步改4.4 超参数配置背靠背对比的必调项YOLOv5 的训练超参数在data/hyps/hyp.scratch-low.yaml里骨龄检测可以参考但需要调整两项hsv_h/hsv_s/hsv_vX 光片是灰度图色相和饱和度增强没有意义建议全部设为 0只保留亮度增强hsv_v微调防止模型学到伪彩色相关性degrees旋转角度骨龄检测中手腕姿态可能有旋转但旋转超过 30 度会剪掉骨骼区域建议degrees: 15而不是默认的 0fliplr水平翻转这个可以保留左右手 X 光片在翻转语义上等价mosaic马赛克增强YOLOv5 默认开启但在小数据集 大目标场景下马赛克把多个图拼在一起会产生大量小目标反而干扰骨骼检测。建议将mosaic: 0.0关掉或只在最后 50 轮用--close-mosaic 50关闭。训练完以后在项目根目录runs/train/bone_age_run/下能找到weights/best.pt这就是后续推理和导出使用的最终权重。5. 骨龄检测常见问题排查五个必踩的坑和解决办法5.1 检测框全落在黑边上回归模块学到全黑图现象训练完 YOLOv5 后在验证集上画检测框发现大量检测框出现在图片四周的黑色背景区域而不是骨骼结构上。原因标注数据没有统一裁剪黑边。X 光片为了成像美观扫描时通常在四周留了很宽的黑色区域YOLOv5 很容易把这部分黑边当成背景却难以区分骨骼边缘和软组织边界模型会倾向于预测一个覆盖全图的大框来降低 loss。解决预处理时统一做自适应阈值裁剪把黑色背景切掉。代码思路是先用 Otsu 阈值找到手部区域的二值掩膜再根据掩膜的边界框裁剪图片然后重新生成标注坐标。不过要注意如果数据集中的图片尺寸差异大裁剪后会改变图像的高宽比YOLOv5 训练时会把所有图 resize 到固定尺寸比例失真会导致检测框偏移。因此裁剪后最好手动检查 100 张图片的标注框和骨骼位置是否仍然对齐。5.2 模型不收敛loss 高居不下现象训练 200 轮后box_loss 和 cls_loss 还是有明显波动验证集 mAP 趋近于 0。原因大概率不是模型结构问题而是标签文件出错了。YOLOv5 对标签格式非常严格比如类别 ID 超过了 data.yaml 里的 nc或者标注坐标没有归一化到 [0, 1]。解决写一个快速校验脚本遍历所有 txt 文件检查两个条件每行的第一个数字要小于 nc后面的 cx、cy、w、h 都在 [0, 1] 区间内。import os def check_labels(labels_dir, nc3): bad_files [] for root, _, files in os.walk(labels_dir): for f in files: if not f.endswith(.txt): continue path os.path.join(root, f) with open(path) as fp: for line in fp: parts line.strip().split() if len(parts) ! 5: bad_files.append((path, 长度不是5, line)) break cls_id int(parts[0]) vals list(map(float, parts[1:])) if cls_id nc: bad_files.append((path, f类别ID {cls_id} 超出范围, line)) break if any(v 0 or v 1 for v in vals): bad_files.append((path, 坐标值不在[0,1], line)) break return bad_files bad check_labels(bone_age_dataset/labels/train, nc3) print(f有问题的文件数: {len(bad)}) for item in bad[:10]: print(item)还有一个隐蔽的坑标签文件中的浮点数精度。如果人工把坐标写成了0.5而不是0.500000YOLOv5 能读但如果坐标值过大如150.3而没有除以图片宽高训练时 loss 会瞬间爆炸。这也是为什么尽量用脚本自动转换而不是手工编辑标签。5.3 回归结果性别偏差男女孩骨龄普遍预测偏大/偏小现象在测试集上按性别分组计算 MAE发现男生组的平均误差是 15 个月而女生组只有 8 个月误差分布明显不均衡。原因训练数据里男女生样本比例失衡。RSNA 竞赛数据集的男女比例不是严格的 1:1如果随机切分训练/验证集按 9:1 切分后男生可能只剩几百张回归模型没能充分学到男生的骨骺形态特征。解决按性别分层抽样切分数据集。用 sklearn 的StratifiedShuffleSplit以性别作为分层标签确保训练集和验证集里男女比例一致而不是按文件名随机切分。另外在回归网络里显式加入性别特征前文代码已给出能在结构上强迫模型利用性别信息而不是让所有误差都归到视觉特征上。5.4 有遮挡、发育不全的样本检测框置信度低现象模型对正常样本的推理效果很好但对骨骺发育不全、骨折、或拍摄角度奇怪的样本检测框置信度很低甚至漏检。原因数据分布外OOD问题。X 光片的拍摄角度、曝光量、儿童年龄跨度都可能导致图像分布偏移。YOLOv5 对 OOD 样本的泛化能力取决于训练集的覆盖度小数据集尤其容易翻车。解决TTATest-Time Augmentation。YOLOv5 自带了--augment推理参数推理时会做多尺度、翻转等增强然后综合多个结果得出更稳的检测框# 用增强推理提升鲁棒性代价是单张推理时间增加约 3 倍 python detect.py \ --weights runs/train/bone_age_run/weights/best.pt \ --source /path/to/test/images \ --img 1024 \ --augment \ --conf-thres 0.25如果 TTA 也救不回来那就要看标注了——这类困难样本通常在训练集中数量极少可以考虑单独做个目标检测微调或者把这些样本挑出来重新标注检查是否漏标了部分骨骺区域。5.5 新数据集上复现结果打不到原报告的 mAP现象照着论文或别人的 README 设置训练参数但复现出的 mAP 低了 510 个点。原因这几乎是迁移学习在医学影像方向的通病叫“指标漂移”。原报告可能用的是 1024 分辨率训练加多尺度测试你用的是 640原报告可能做了 5 折交叉验证取平均你只跑了单折原报告的验证集划分可能和你的完全不一样。解决把验证方法统一。固定测试集比如 500 张在文档里写明训练分辨率、测试分辨率、是否开启 TTA、置信度阈值然后和别人对比时才有一致的基础。骨龄检测里最关键的对比指标是 MAE平均绝对误差单位月建议在论文或项目文档里同时报告整体 MAE、按年龄分段 MAE如 37 岁、811 岁、1215 岁这样能更清楚地看出模型在哪个年龄段收敛不好也属于答辩时非常加分的分析维度。6. 推理部署与最终验证从 best.pt 到骨龄报告6.1 单张 X 光片的完整推理流程训练完的权重最终要用到两种地方一种是在项目里演示输了图出结果另一种是导出成 ONNX 或 TensorRT 模型部署到服务端。先看直接推理的写法用 YOLOv5 的 detect.py 跑一遍# 先跑检测得到裁剪区域 python detect.py \ --weights runs/train/bone_age_run/weights/best.pt \ --source /path/to/test/001.png \ --img 1024 \ --conf-thres 0.3 \ --project ./inference_output \ --name demo \ --save-txt--save-txt会把检测框坐标保存到inference_output/demo/labels/001.txt。接下来用脚本读取这个 txt对检测框裁剪再喂给回归模型import cv2 import torch import numpy as np def load_yolo_preds(txt_path, img_shape): 读取 detect.py 保存的文本检测结果 h, w img_shape[:2] boxes [] with open(txt_path) as f: for line in f: parts line.strip().split() cls_id int(parts[0]) cx, cy, bw, bh map(float, parts[1:]) x1 int((cx - bw / 2) * w) y1 int((cy - bh / 2) * h) x2 int((cx bw / 2) * w) y2 int((cy bh / 2) * h) boxes.append((cls_id, x1, y1, x2, y2)) return boxes def predict_bone_age(image_path, reg_model, devicecuda): 完整流程检测 - 裁剪 - 回归 - 输出骨龄月数 性别修正 img cv2.imread(image_path) rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) h, w rgb.shape[:2] # 1. 读取 YOLOv5 检测结果这里假设 detect.py 已提前运行 txt_path image_path.replace(.png, .txt).replace(images, labels) boxes load_yolo_preds(txt_path, (h, w)) # 2. 对每个检测框裁剪resize 到 224x224然后标准化 patches [] coords [] for cls_id, x1, y1, x2, y2 in boxes: # 适当外扩 10% 边界避免骨骺紧贴框边 pad_x int((x2 - x1) * 0.1) pad_y int((y2 - y1) * 0.1) x1, y1 max(0, x1 - pad_x), max(0, y1 - pad_y) x2, y2 min(w, x2 pad_x), min(h, y2 pad_y) patch cv2.resize(rgb[y1:y2, x1:x2], (224, 224)) patch patch / 255.0 mean np.array([0.485, 0.456, 0.406]) std np.array([0.229, 0.224, 0.225]) patch (patch - mean) / std patches.append(patch) coords.append((x1, y1, x2, y2)) if not patches: return None, None # 3. 批量送入回归模型 batch torch.tensor(np.stack(patches), dtypetorch.float32).permute(0, 3, 1, 2).to(device) with torch.no_grad(): ages reg_model(batch).cpu().numpy() # 4. 多 ROI 结果融合不同骨龄检测区域可能给出不同预测取中位数抗异常值 final_age float(np.median(ages)) return final_age, coords说明一下最后一行的中位数融合桡骨、尺骨、腕骨三个区域的骨龄预测应当在合理区间内一致但偶尔某个 ROI 检测框定位偏差会带来异常预测取中位数可以容忍一个区域出错比均值更稳。6.2 怎么验证你的骨龄检测模型是真的可用验证环节分两层第一层是目标检测质量用 mAP 和 IoU第二层才是骨龄预测的核心指标 MAE。对医疗相关项目mAP 高不代表骨龄准关键看 MAE 是否在可接受范围。对比基准是放射科医生手工读片的误差大约在 612 个月之间你的模型如果 MAE 在 12 个月以内就说明已经接近人工水平。在测试集上做按性别和年龄段分组的误差分析一个 python 脚本就能完成import pandas as pd # results.csv 至少要有三列true_age_months, pred_age_months, gender, age_group df pd.read_csv(results.csv) # 整体 MAE overall_mae (df[true_age_months] - df[pred_age_months]).abs().mean() # 按性别分组的 MAE用于检查性别偏差 by_gender df.groupby(gender).apply( lambda x: (x[true_age_months] - x[pred_age_months]).abs().mean() ) # 按年龄段分组的 MAE青春期10~14岁的误差通常高于低龄段 df[age_group] pd.cut(df[true_age_months], bins[36, 84, 144, 204]) by_age df.groupby(age_group).apply( lambda x: (x[true_age_months] - x[pred_age_months]).abs().mean() ) print(Overall MAE:, overall_mae) print(By gender:\n, by_gender) print(By age group:\n, by_age)这类分析在项目答辩时非常加分因为它把“模型的误差从哪来”讲清楚了。如果按年龄分组后发现 1215 岁年龄段误差明显偏大就要考虑是不是该年龄段训练样本不足而不是盲目调网络结构。6.3 从 PyTorch 权重到 ONNX 导出搭一个简单的命令行工具课程设计和毕业设计如果允许建议把整个流程包装成一个命令行工具这样演示时更可信。YOLOv5 自带 ONNX 导出功能回归模型也可以手动导出# YOLOv5 检测模型导出 ONNX python export.py --weights runs/train/bone_age_run/weights/best.pt --include onnx --img 1024 # 回归模型导出用 torch.onnx.export python -c import torch from models import BoneAgeRegressor model BoneAgeRegressor(pretrainedFalse) model.load_state_dict(torch.load(bone_age_regressor.pt)) model.eval() dummy torch.randn(1, 3, 224, 224) torch.onnx.export(model, dummy, bone_age_regressor.onnx, input_names[roi_image], output_names[age_months], dynamic_axes{roi_image: {0: batch}}) 然后写一个简单的 CLI输入图片路径输出骨龄预测和检测图大概就是 100 行 Python 的事。我用这类工具做课程设计和毕业设计演示的时候一般会先录一段 30 秒的屏幕录像确保现场演示时不会翻车如果现场网络环境差预先下载好权重文件不要在演示时临时torch.hub.load。最后说一个习惯问题在骨龄检测项目里我会把一个runs/目录从第一版训练保存到最后一版每个版本的训练命令和 data.yaml 都同步归档。这个习惯救了我很多次——调试模型时改坏的参数改回去只要翻历史命令记录不用靠记忆重试。训练深度学习模型本来就有不少玄学成分但这不是项目失控的借口保留可复现的环境才能让每一轮迭代都有价值。希望这套 yolo 骨龄检测方案能帮你少走几个月的弯路祝顺利跑通。本文还有配套的精品资源点击获取
返回列表