首页 / 资讯中心 / 文章详情

基于PyTorch和YOLOv5的骨龄检测识别系统:从X光片到桌面应用

基于PyTorch和YOLOv5的骨龄检测识别系统:从X光片到桌面应用 ★ FEATURED ARTICLE
简介一套基于PyTorch与YOLOv5的骨龄检测识别系统配套Pyside6图形界面面向医学影像分析、深度学习算法工程师及儿科影像研究人群。项目以儿童手腕X光图像为输入通过YOLOv5模型定位骨骼特征并推断骨龄为生长发育评估提供自动化辅助工具适合作为毕业设计、课题研究或工程落地的参考实现。资源包共205个文件大小406.37MB。核心包括71个Python源码含模型结构、训练推理、GUI逻辑、43个编译后pyc、32个YAML与12个YML配置用于模型默认参数与部署编排、11个PyTorch权重pt文件以及Dockerfile、IPython教程、UI文件、许可证与中文说明文档等覆盖从环境构建到运行的完整链路。该资源目前已有332人学习。下载后可直接查阅源码结构结合tutorial.ipynb入门流程快速理解数据预处理、模型训练如何与Pyside6界面联动同时借助多套pt权重与配置文件可对比不同训练参数的效果减少复现踩坑时间。整套内容集成度高兼具学术参考与工程复用价值。1. 骨龄检测识别系统PyTorch 与 YOLOv5 如何把读片这件苦差事做成桌面程序如果你手上攒了几百张腕骨 X 光片要在半天内把每张的骨龄读出来并且和 TW3 骨龄标准对照出结论光靠肉眼逐张标、逐张算一上午也就能看十几张眼睛还累得不行。这个基于深度学习的骨龄检测识别系统做的就是这件事——用 python 图像识别 的思路把 YOLO 深度学习 目标检测模型搬进一个 PySide6 桌面应用里读入一张 X 光片自动框出腕骨区域再回归出骨龄结果。整套流程从选型上就考虑了落地YOLOv5 权重轻、推理快PyTorch 负责统一训练和部署Pyside6 管界面三个部件拼起来以后非深度学习背景的医生或实验人员也能直接点鼠标用。适合拿来当医学图像检测的人机交互样板或者直接作为骨龄评测项目的基线版本去扩展。2. 系统架构与运行环境从依赖安装到界面弹出的全流程2.1 技术选型为什么偏偏是 YOLOv5 而不是更新的检测模型现在做目标检测可选的东西很多YOLOv8、RT-DETR、甚至各种 Transformer 检测头都比 YOLOv5 新。但这类医学图像落地项目里YOLOv5 依然是很多人优先考虑的对象原因很实在生态太成熟了。ultralytics 官方仓库里预训练权重、标注工具、导出脚本一应俱全任何报错几乎都能在社区里搜到现成答案。对骨龄检测这个场景来说目标不是刷榜而是稳定复现YOLOv5 的工程化程度在这个场景里反而是最大优势。整个系统的技术栈分三块。PyTorch 承担模型训练和推理Pyside6 负责桌面端界面YOLOv5 负责腕骨目标检测。所谓“骨龄检测识别”落到实现上其实是两步走先用 YOLOv5 在 X 光片上检测出若干块腕骨的位置再把每一块腕骨的 ROIRegion of Interest感兴趣区域裁剪下来做骨龄评估回归。检测和评估是两条独立管线中间通过坐标数据衔接这样后续想换更好的骨龄回归模型不必动检测部分。2.2 目录结构与数据流一张腕骨 X 光片从读取到出报告这里我按这套系统的常规工程结构梳理一下。拿到源码包后你会看到一个类似下面的目录虽然不是每个骨龄项目都完全一致但大方向不会有太大出入bone-age-system/ ├── app/ │ ├── main.py # Pyside6 应用入口 │ ├── ui_main_window.py # 主窗口界面定义 │ ├── inference_thread.py # 推理线程避免 UI 卡死 │ └── report_builder.py # 骨龄报告生成 ├── core/ │ ├── detect.py # YOLOv5 检测核心封装 │ ├── preprocess.py # 图像预处理 │ └── bone_age.py # 骨龄回归计算 ├── weights/ │ └── bone.pt # YOLOv5 训练好的骨龄检测权重 ├── data/ │ ├── images/ # 测试用 X 光片 │ └── labels/ ├── requirements.txt └── README.md数据流是一条直线不复杂。用户通过 Pyside6 窗口选择一张 X 光片程序把图片路径交给 core/detect.pydetect.py 内部先做 letterbox 缩放、归一化然后喂给 YOLOv5 模型推理得到腕骨的边界框坐标这些坐标再传给 bone_age.py它会按坐标从原图里抠出腕骨小图缩放后送入骨龄回归网络输出一个数值型骨龄结果最后 app 层把检测框画回原图连同骨龄值一起渲染到 Pyside6 界面上需要的话还能导出成简单的报告文件。整条链路最需要注意的是数据格式的衔接检测模块输出的是归一化坐标还是绝对像素坐标直接决定 bone_age.py 能不能正确抠图这里在代码里必须用同一套坐标系约定。2.3 环境搭建与运行从裸机到界面弹出跑这个项目不需要多高级的机器纯 CPU 也能推理只是速度会慢一些。有 NVIDIA 显卡的话先确认驱动和 CUDA 版本再装对应版本的 PyTorch。下面是完整步骤我一般会先创建独立的虚拟环境避免把系统 Python 环境搞乱# 1. 创建虚拟环境指定 Python 3.9 或 3.10 conda create -n boneage python3.10 -y conda activate boneage # 2. 安装 PyTorch这里以 CUDA 11.8 为例 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 3. 安装项目依赖 pip install -r requirements.txt # 4. 启动 Pyside6 应用 python app/main.py关于这个 requirements.txt里面一般会固定这几类库ultralyticsYOLOv5 推理依赖、PySide6、opencv-python、numpy、pillow。有几个点容易踩torch 和 torchvision 的版本必须匹配否则 import 时会直接报错说找不到某个符号opencv-python 在 Linux 服务器上如果缺少 libGL 库会在 import cv2 的时候抛异常通常用 apt install libgl1 就能解决。如果第 4 步启动后窗口弹不出来先看终端有没有报错日志最常见的两种原因是 PySide6 缺少系统图形依赖或者显卡驱动和 PyTorch 的 CUDA 版本不对应导致模型加载失败。启动成功后会看到主窗口中间是一个图像显示区域右侧是“选择图片”和“开始检测”两个按钮点击选择 X 光片后再点检测约一两秒后图像上会出现腕骨检测框下方显示骨龄数值。到这里项目就算跑通了。3. 核心模块拆解推理链路与界面交互的关键实现3.1 模型加载与图像预处理从文件路径到模型输入张量模型加载这一步用的是 YOLOv5 的 DetectMultiBackend它能自动识别权重文件格式并加载到指定设备。常见的坑是权重文件用 .pt 格式保存着训练时的类名和超参如果换了环境但没带对应的 yaml 配置文件加载时可能报类别数不一致。推荐做法是加载后打印 model.names确认检测类别是否符合预期。import torch from models.common import DetectMultiBackend # 加载 YOLOv5 模型优先使用 GPU 设备 model DetectMultiBackend( weightsweights/bone.pt, device0 if torch.cuda.is_available() else cpu ) # 打印类别名称检查权重是否加载正确 print(model.names) # 例如 {0: wrist_bone, 1: epiphysis}预处理是图像识别链路里非常容易翻车的一环。YOLOv5 系列默认输入尺寸是 640x640训练阶段会对图像做 letterbox 缩放——保持宽高比把图缩放到短边对齐 640再用灰色填充剩余区域。这个操作看似简单但推理时如果漏掉原图直接 resize 成正方形腕骨会被拉伸变形检测精度肉眼可见地掉。这里用官方提供的 letterbox 函数import cv2 import numpy as np from utils.augmentations import letterbox def preprocess(image_path, img_size640, stride32): # 读取原图BGR 格式 img0 cv2.imread(image_path) # letterbox 保持宽高比缩放stride 对齐模型下采样倍数 img, ratio, (dw, dh) letterbox( img0, new_shapeimg_size, stridestride, autoTrue ) # BGR 转 RGBHWC 转 CHW增加 batch 维度 img img[:, :, ::-1].transpose(2, 0, 1) img np.ascontiguousarray(img) img_tensor torch.from_numpy(img).to(model.device) img_tensor img_tensor.float() / 255.0 if img_tensor.ndimension() 3: img_tensor img_tensor.unsqueeze(0) return img_tensor, ratio, (dw, dh)这段代码里有几个参数要说明。img_size 是模型输入尺寸YOLOv5 默认 640如果是自己训练的模型且改过输入尺寸这里必须跟着改stride 是模型下采样的倍数YOLOv5 是 32letterbox 会保证缩放后的宽高都是 stride 的整数倍减少推理时 Padding 的浪费返回值里的 ratio 和 (dw, dh) 是缩放比例和填充尺寸后面坐标还原到原图时必须用。通道顺序转换也很关键OpenCV 读进来是 BGR而 PyTorch 模型训练时用的是 RGB不转换的话检测效果会偏差但不会报错这种问题最容易被忽略。3.2 推理与后处理非极大值抑制与坐标还原预处理完的张量送入模型后输出的是一个形状为 (1, 25200, 85) 的预测张量其中 25200 是 640x640 输入下三个尺度特征图生成的锚框总数85 是 4 个坐标值 1 个置信度 80 个类别概率。骨龄检测模型如果类别数不是 80这个维度会在推理时自动按模型头调整。25200 个框里绝大多数是背景所以要用非极大值抑制NMS把重复的和低置信度的框滤掉。from utils.general import non_max_suppression def postprocess(pred, ratio, dw, dh, conf_thres0.25, iou_thres0.45): # 非极大值抑制去掉低置信度框和重叠框 det non_max_suppression( pred, conf_thres, iou_thres, classesNone, agnosticFalse, max_det100 )[0] boxes [] if det is None or len(det) 0: return boxes for *xyxy, conf, cls in det: # letterbox 坐标还原先减填充再除以缩放比例 xyxy np.array(xyxy, dtypenp.float32) xyxy - [dw, dh, dw, dh] xyxy np.clip(xyxy / ratio, 0, None) boxes.append({ bbox: [round(v, 2) for v in xyxy.tolist()], confidence: round(float(conf), 4), class_id: int(cls) }) return boxesconf_thres 是置信度阈值低于这个值的检测框会被丢弃。这个参数直接影响漏检和误检的平衡设得太低比如 0.1画面上会出现一堆假阳性框设得太高比如 0.7小腕骨可能就检测不到了。骨龄检测场景里腕骨目标比较大、边界清晰0.25 到 0.35 之间是个合理的区间。iou_thres 是 NMS 的 IoU 阈值用于抑制重叠框两个框重叠面积超过这个比例就保留置信度更高的那个。坐标还原那两行是整个后处理里最容易错的地方letterbox 给图像增加了填充区域这些填充里没有真实目标信息如果忘记减去 (dw, dh)所有检测框都会整体向右下偏移一段距离还原到原图上就对不上了。这也是我每次写 YOLO 系推理必检查的一步——当然也没少在这上面吃过亏。3.3 骨龄回归从腕骨边界框到骨龄数值检测到腕骨区域后接下来做骨龄评估。常见做法是把每块腕骨从原图抠出来缩放到固定尺寸送入一个回归网络输出骨龄值。YOLOv5 的另一个隐藏用法是它除了做检测还能把模型内部的骨干网络特征提取能力复用到分类或回归任务上但更常规也更可控的做法是独立训练一个小型 CNN 回归网络。import torch.nn as nn class BoneAgeRegressor(nn.Module): 轻量级骨龄回归网络输入224x224的单块腕骨图像 def __init__(self): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 32, kernel_size3, stride2, padding1), nn.ReLU(inplaceTrue), nn.AdaptiveAvgPool2d((1, 1)) ) self.regressor nn.Sequential( nn.Linear(32, 64), nn.ReLU(inplaceTrue), nn.Linear(64, 1) # 输出一个连续的骨龄值 ) def forward(self, x): x self.features(x) x x.view(x.size(0), -1) return self.regressor(x)这个网络结构极简实际项目里比这复杂得多通常会用 ResNet 或 EfficientNet 作为骨干。重点是理解骨龄回归的任务范式输入是单块骨骼的局部图输出是连续的月龄或年年龄值损失函数一般用 L1Loss 或 SmoothL1Loss因为它本质是回归任务而不是分类。训练数据里每一张 X 光片需要有对应的人工标注骨龄作为监督标签这部分通常由有经验的医生来完成标注是整个项目里成本最高的一环。推理时把 YOLOv5 抠出的每一块腕骨 ROI 逐一输入回归网络得到多个骨龄值再按 TW3 骨龄标准的权重做加权平均得到最终骨龄。加权系数一般放在配置文件里便于根据实际临床需求调整。3.4 Pyside6 界面信号槽与推理线程的结合界面这块如果不做线程隔离点击“开始检测”后主线程会阻塞在模型推理上窗口直接进入“未响应”状态体验很糟糕。Pyside6 的解决方案是用 QThread 把推理放到子线程通过 Signal 把结果传回主线程更新界面。from PySide6.QtCore import QThread, Signal class InferenceThread(QThread): # 自定义信号推理完成后携带结果回到主线程 result_ready Signal(dict) def __init__(self, image_path, model): super().__init__() self.image_path image_path self.model model def run(self): try: img_tensor, ratio, (dw, dh) preprocess(self.image_path) pred self.model(img_tensor)[0] boxes postprocess(pred, ratio, dw, dh) # 这里调用骨龄回归模块输出骨龄值 bone_age compute_bone_age(self.image_path, boxes) self.result_ready.emit({ boxes: boxes, bone_age: bone_age }) except Exception as exc: self.result_ready.emit({error: str(exc)})主窗口里连接信号的方式很直接实例化 InferenceThread 后用 .result_ready.connect(self.on_result) 绑定槽函数点击按钮启动线程。槽函数里拿到的 boxes 和 bone_age直接用来调用 QPainter 在图像上画框、更新标签文本。需要注意的一个细节是QThread 对象要保存在主窗口的成员变量里不能是局部变量否则线程启动后可能被垃圾回收导致程序崩溃。另一个细节是信号里不要传自定义 Python 对象最好只传 dict 或 list 这些基础类型跨线程传递自定义对象容易出现生命周期问题。这套“子线程推理 信号回传”的模式在 Pyside6/PyQt 的图像识别程序里几乎是通用标准写法了。4. 避坑记录移植这个项目时最常见的 5 个问题4.1 GPU 显存溢出batch_size 为 1 也会炸现象程序运行时抛出 CUDA out of memory报错指向 torch 的某个卷积操作但输入明明只有一张图。原因骨龄检测模型的输入尺寸被设置得过大或者同一进程里同时加载了 YOLOv5 检测模型和骨龄回归模型两张网络都常驻显存另外 X 光片分辨率通常很高如果预处理阶段没有对原图做缩放就直接送进模型单张图也会吃掉好几个 G 显存。解决检查 preprocess 里 img_size 参数是否真正生效确认输入张量 shape 是 (1, 3, 640, 640) 而不是 (1, 3, 2000, 2000)。还可以在加载模型后调用 torch.cuda.empty_cache() 清理缓存或者用 with torch.inference_mode(): 包裹推理代码减少梯度图的显存占用。这块属于典型的“看着是小问题排查起来很花时间”的显存玄学。4.2 权重文件与代码版本不匹配现象加载 bone.pt 时报错 KeyError或者模型能加载但检测结果全是背景框。原因YOLOv5 的权重文件关联着训练时的 YOLOv5 子版本和类名列表如果仓库用的是 v5.0 训练的权重而推理代码是从 v7.0 的 ultralytics 里拿的 YOLOv5 实现字段名和 forward 逻辑已经变了。解决先确认权重文件对应的 YOLOv5 版本最简单的方法是用官方对应版本的仓库跑一遍推理能把检测框画出来再集成到 Pyside6 应用里不要混着用也不要凭感觉乱升级 ultralytics 库。这个问题的排查成本比较高我有一个血泪经验拿到任何 .pt 权重先把文件和模型类名的 md5 记录在 README 里后面换环境能少走很多弯路。4.3 界面按钮点了没反应像卡死一样现象点击“开始检测”后窗口变成空白或者转圈点击任何地方都没有响应。原因推理代码直接写在按钮点击的槽函数里主线程被模型推理阻塞界面无法处理鼠标事件。解决把推理迁移到 QThread 里务必记住线程对象要保存在 self 上。这里还隐藏一个问题如果推理线程还没结束用户又点了第二次开始会启动第二个线程导致资源争抢。可以在按钮回调里加一个 self.is_running 标志位推理期间禁用按钮结果返回后再恢复。别小看这个细节很多界面“假死”的 bug 本质都是多线程模型下缺少并发保护。4.4 中文路径导致导入图片失败现象X 光片放在带中文名的文件夹里或者文件名本身是中文程序读不到图片。原因OpenCV 的 cv2.imread 在 Windows 上对非 ASCII 路径支持很烂会静默返回 None后续代码在 None 上调用属性直接报 AttributeError。解决不要改系统编码直接规避——在读取图片时代码统一用 np.fromfile 配合 cv2.imdecode 替代 cv2.imread或者在界面选择文件后先把文件复制到项目下的临时目录再用英文路径读取。我一般两个都做入口处拦一道复制逻辑代码内部统一走 imdecode。这个问题在骨龄检测这类要经常从医院信息系统导文件的场景里极其常见而且只在 Windows 上复现拿 Mac 和 Linux 开发的人很容易忽视。4.5 检测框在原图上位置偏移现象画检测框时框的位置和腕骨对不上整体偏向右下角而且越靠近图像边缘偏差越大。原因没有对 letterbox 的填充量和缩放比例做逆变换。X 光片原图往往是 2000x1500 甚至更高缩放成 640x640 时短边填充比例大坐标偏差被放大得特别明显。解决严格按 3.2 节里的步骤先把坐标减去填充量 (dw, dh)再除以缩放比例 ratio并且要注意 letterbox 函数返回的 ratio 是一个元组 (宽比例, 高比例)实际用的时候要做成与坐标轴对应不能直接拿一个数去除两个轴。验证是否修复很简单把带框图像保存到本地肉眼看一下框是不是紧贴腕骨边缘。5. 二次开发改造让这套系统适配你自己的数据集5.1 换数据集训练 YOLOv5 检测模型骨龄检测的权重不可能放之四海皆准——如果拍摄设备变了X 光片的对比度、亮度和腕骨位置分布都会不同原有权重的检测精度会下降。换成自己的数据集步骤是固定的用 LabelImg 或 LabelStudio 标注腕骨区域导出 YOLO 格式的 txt 标注文件新建 data.yaml 指向训练集、验证集路径和类别名然后跑 YOLOv5 官方训练命令python train.py --data data.yaml --weights yolov5s.pt --img 640 --epochs 100 --batch-size 16注意训练集至少要几百张图才有效果且标注质量比数量更重要。训练前把牙齿打了标签的图剔除避免模型学到错误特征。5.2 把骨龄回归从 TW3 换成其他标准骨龄评估标准不是只有一种还有 GP 图谱法、中华 05 法等。如果报告需要按不同标准输出改造点集中在 bone_age.py 的加权系数表上把每块腕骨的权重替换成新标准的数值即可如果新标准要求评估的不是腕骨而是肘关节、膝关节那就需要重新训练检测模块。这个扩展方向自由度很大但核心原则不变检测和回归解耦后每个模块都能单独调换。5.3 验证与回归批量对比人工读片结果改造后的第一步不是人工看图而是跑回归测试。我会准备一组带医生标注骨龄的测试图批量跑一遍检测和骨龄回归输出每张图的误差分布。通常关注两个指标检测框 IoU 是否稳定在 0.8 以上骨龄误差均值小于某个阈值。这里我一般会写一个脚本自动统计误差生成 CSV 报告再把误差偏大的样本挑出来人工检查。从那以后我每次改动模型或数据流都强制走一遍批量验证绝不只在单张图上“看着没问题”就收工。这个习惯帮我挡掉过很多次权重版本错乱和预处理改崩的翻车事故。这套骨龄检测系统的架构看起来链路很长但正是因为检测和评估解耦、线程和界面分离它比大多数一气呵成的 Demo 都更适合继续长出新功能。希望帮到你跑通并顺手改出自己想要的效果。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站