资讯动态

基于YOLOv5与PySide6的骨龄检测系统:从模型训练到桌面应用落地

发布时间:2026/10/20 5:41:30 来源:尧图企业网站定制
简介这套基于深度学习的骨龄检测识别系统面向医学影像分析与Python开发者融合PyTorch、Pyside6与YOLOv5模型用于从儿童手腕X光图像中识别骨骼特征并推断骨龄可服务于儿科临床辅助诊断与算法研究。压缩包共205个文件包含71个Python源码、43个编译后的pyc文件、32个YAML配置、11个模型权重文件以及6个Markdown文档和3个Jupyter Notebook教程整体大小406MB覆盖训练、推理、界面交互与部署全流程。资源内附Dockerfile、版本控制与开源许可等工程化配置便于复现和二次开发。目前已有332人学习适合希望掌握YOLOv5医学影像落地方法、并构建桌面应用的中高级Python工程师。1. 骨龄检测识别系统一个把YOLOv5检测器接进PySide6桌面的完整工程骨龄检测这个任务传统做法是放射科医生对着手腕X光片查图谱、数骨骺发育等级一张片子少说花几分钟。而这个项目把整条链路压到了秒级PySide6桌面端负责选图和结果展示YOLOv5模型先定位手骨ROI区域PyTorch骨龄网络再对裁剪出的ROI做分级界面最终输出骨龄结果与检测框可视化。我拆这套代码时印象最深的不是准确率数字而是它把训练、推理、界面三件事串成了一个能直接双击运行的桌面工具而不是一堆散落的Jupyter实验。适合两类读者一是想把深度学习模型接到桌面端做落地的Python开发者二是想在医学图像识别场景里复用YOLO检测流程的同学。2. 技术选型与整体架构PyTorch训练端、YOLOv5检测端、PySide6界面端如何各司其职2.1 为什么用YOLOv5做检测而不是直接拿分类网络一把梭骨龄评估表面上看是图像分类或者回归问题——输入X光片输出年龄。但直接拿ResNet对整张片子做分类会碰到两个让人头疼的问题。第一手腕X光片的非手骨区域占比很高。拍片时经常把部分前臂甚至铅衣边界拍进去这些区域在灰度分布上与骨骺非常接近分类网络很容易被背景带偏。第二可解释性几乎为零。医生判骨龄的依据是特定骨骺的发育形态如果模型直接说这张片子的骨龄是12岁临床上是没人敢信的得知道它看了哪块骨头、哪个区域。YOLOv5在这个项目里承担的是先定位、再判别的前置角色。模型先把手骨区域或者关键骨骺区域框出来代码再按检测框裁剪出ROI后续的骨龄网络只看ROI做分级。这样一来背景噪声被物理剪掉网络的注意力被迫集中到医学上真正相关的区域同时检测框本身也是一种可视化解释医生能看到系统到底看了哪里。选YOLOv5而不是更早的YOLOv3主要图两点一是torch.hub加载权重非常省事不用自己搭Darknet的解析逻辑二是它的导出链路成熟后续要转ONNX或者TensorRT都比较顺。YOLOv5虽然被YOLOv8抢了不少风头但如果你的任务不是追求极致速度而是需要文档多、踩坑记录全的稳定检测器它依然是性价比很高的选择。2.2 目录结构一个能跑通的工程长什么样拿到这份源码后我先把目录理了一遍。整个项目的组织非常清晰训练、推理、界面三块代码物理隔离没有混在一个大包里。核心结构大致是这样bone_age_system/ ├── main.py # 应用入口创建QApplication并启动主窗口 ├── models/ │ ├── yolov5/ # 封装好的YOLOv5检测器 │ │ ├── detector.py # HandDetector类加载权重并输出检测框 │ │ └── weights/ # 训练好的手骨检测权重 │ └── age_network.py # 骨龄评估网络基于ResNet18改造 ├── ui/ │ ├── main_window.py # 主窗口负责文件选择和结果展示 │ └── result_panel.py # 结果面板绘制检测框和骨龄信息 ├── utils/ │ ├── image_ops.py # 图像读取、ROI裁剪、CLAHE增强 │ └── bone_age_calc.py # 把网络输出的等级分布换算成骨龄 ├── weights/ # 骨龄网络的权重文件 └── data/ # 示例X光片用于快速验证我一般拿到深度学习项目会先看models和utils因为这两个目录决定了推理逻辑怎么走。这里models/yolov5/detector.py负责检测age_network.py负责分级utils/image_ops.py负责把两者衔接起来分工相当干脆。一个细节值得表扬weights目录和data目录平级意味着你不需要在代码里翻路径模型权重放哪、测试图片放哪一目了然。很多开源项目死在路径散落各处这个项目没有犯这个毛病。2.3 两套训练链路检测器与骨龄网络各训各的训练侧是两条平行的链路互不干扰。检测器用标注了手骨框的数据训练骨龄网络用裁剪好的ROI数据训练只在推理阶段通过坐标接起来。这种设计的好处是检测效果不好你只换检测器分级不准你只调骨龄网络耦合成本极低。检测器部分最可能的做法是直接用YOLOv5官方仓库的训练脚本。标注格式是YOLO的txt格式每行一个目标类别、中心点x、中心点y、宽度、高度全部归一化到0到1之间。数据集目录按官方约定组织datasets/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/训练命令走train.pypython train.py \ --data bone_age.yaml \ --weights yolov5s.pt \ --img 640 \ --epochs 100 \ --batch-size 16bone_age.yaml里指定train和val路径以及类别名列表这个场景只有一个类别所以nc1。用yolov5s这种小模型起步训练速度快、显存占用小如果你的ROI目标不算太小s版本精度够用的概率很大。骨龄网络的训练则是标准PyTorch训练循环优化器用Adam学习率设1e-4左右损失函数用交叉熵。如果类别不均衡可以给每个等级加权重。训练数据不是原始X光片而是检测器裁剪出来的ROI而且建议做离线裁剪而不是训练时现裁——训练时调检测器会让数据预处理环节变得不可控离线裁剪排错也方便。3. 核心模块拆解一张X光片从载入到输出骨龄的完整链路3.1 YOLOv5检测器封装torch.hub加载权重与ROI提取这个项目对YOLOv5的封装非常薄没有在ultralytics源码上做大量改动只是把加载和推理收拢到一个类里。这种做法我很认同——第三方库的源码尽量别碰升级的时候才不会翻车。import cv2 import torch import numpy as np class HandDetector: 封装YOLOv5的手骨检测器输入BGR图像输出检测框坐标 def __init__(self, weights_path: str, conf_thres: float 0.5): # 通过torch.hub加载自定义权重custom模式会跳过默认的COCO预训练 self.model torch.hub.load( ultralytics/yolov5, custom, pathweights_path, force_reloadFalse ) self.model.conf conf_thres # 置信度阈值低于该值的框会被丢弃 self.model.iou 0.45 # NMS的IoU阈值控制重叠框的合并力度 def detect(self, image: np.ndarray) - np.ndarray: 传入BGR图像返回形状为(N, 6)的数组xyxy坐标置信度类别 results self.model(image) boxes results.xyxy[0].cpu().numpy() return boxes def extract_roi(self, image: np.ndarray, box: np.ndarray) - np.ndarray: 按检测框裁剪出手骨区域加padding保留少量上下文 x1, y1, x2, y2 [int(v) for v in box[:4]] # 给坐标加一圈padding防止骨骼边缘被切到 pad_x int((x2 - x1) * 0.05) pad_y int((y2 - y1) * 0.05) x1 max(0, x1 - pad_x) y1 max(0, y1 - pad_y) x2 min(image.shape[1], x2 pad_x) y2 min(image.shape[0], y2 pad_y) return image[y1:y2, x1:x2]torch.hub.load传custom参数时YOLOv5会读取本地训练好的权重文件不会覆盖成COCO预训练权重这是最容易踩错的地方。conf_thres参数实际使用时要根据图像质量微调——X光片对比度通常比自然照片低默认0.5大概率漏检一般调到0.3到0.4之间。extract_roi里加5% padding是有讲究的。如果直接把检测框裁出来训练骨龄网络框边缘会恰好切在骨骼边界上丢失上下文信息稍微留一圈空白网络能多看到一点周围组织结构对分级有帮助。3.2 骨龄评估网络ResNet18改造与等级分布输出骨龄网络没有重新设计结构而是在ResNet18骨干上换分类头。这个选择在场景下是合理的——骨龄数据的样本量通常不大从头训练深层网络极易过拟合用ImageNet预训练的ResNet18做迁移收敛速度快得多。import torch import torch.nn as nn from torchvision.models import resnet18 class BoneAgeNet(nn.Module): 骨龄分级网络把ROI图像映射到骨龄等级分布 def __init__(self, num_classes: int 18, pretrained: bool True): super().__init__() self.backbone resnet18(pretrainedpretrained) # 替换最后全连接层原先输出1000类现在输出骨龄等级数 in_features self.backbone.fc.in_features self.backbone.fc nn.Sequential( nn.Dropout(p0.2), # 随机丢弃20%神经元防止过拟合 nn.Linear(in_features, num_classes) ) self.num_classes num_classes def forward(self, x: torch.Tensor) - torch.Tensor: 输入(B, 3, H, W)的归一化图像输出(B, num_classes)的softmax分布 logits self.backbone(x) return torch.softmax(logits, dim1)输出softmax概率分布而不是直接回归年龄数值这个设计很巧妙。骨龄判断本身带有主观性同一张片子不同医生可能给出相邻等级的结论分布输出比硬分类保留更多信息。后处理时对每个等级乘以对应的年龄段权重再求和就得到连续的骨龄估计值。后处理代码在utils/bone_age_calc.py里逻辑很简单def calc_bone_age(prob: torch.Tensor, age_map: torch.Tensor None) - float: 将骨龄等级分布加权为具体年龄值 参数说明 - prob: 形状为(num_classes,)的softmax概率向量 - age_map: 每个等级对应的中位年龄默认按线性区间生成 if age_map is None: # 假设18个等级均匀覆盖3岁到18岁 age_map torch.linspace(3.0, 18.0, stepsprob.shape[0]) return float(torch.dot(prob, age_map))num_classes18对应骨龄发育的18个等级分段。如果你换数据集等级数要跟着改否则加载权重时会出现形状不匹配报错这是避坑章节要重点说的问题。3.3 PySide6界面QThread后台推理与信号槽结果回传界面部分最值得学习的是它没有偷懒把推理塞在按钮点击回调里。很多初学者写PySide6程序点击按钮后直接调模型推理界面立刻卡死转圈。这个项目用标准QThread子类处理了这个问题。from PySide6.QtCore import QThread, Signal import numpy as np class InferenceThread(QThread): 后台推理线程避免阻塞Qt主线程导致界面假死 result_ready Signal(dict) # 推理完成后向主线程发送结果字典 def __init__(self, image_path, detector, age_net): super().__init__() self.image_path image_path self.detector detector self.age_net age_net def run(self): # 在子线程执行检测和分级主线程可以继续响应用户操作 image cv2.imread(self.image_path) rgb cv2.cvtColor(image, cv2.COLOR_BGR2RGB) boxes self.detector.detect(rgb) result {boxes: [], bone_age: 0.0} if len(boxes) 0: # 取置信度最高的检测框 best_box boxes[0] roi self.detector.extract_roi(rgb, best_box) roi_resized cv2.resize(roi, (224, 224)) roi_tensor torch.from_numpy(roi_resized).permute(2, 0, 1).float() / 255.0 with torch.no_grad(): prob self.age_net(roi_tensor.unsqueeze(0)).squeeze(0) bone_age calc_bone_age(prob) result[boxes] best_box[:4].tolist() result[bone_age] bone_age self.result_ready.emit(result)Signal(dict)是PySide6的信号跨线程传递Python字典非常方便。注意run方法在子线程执行而信号连接的槽函数在主线程执行UI更新是线程安全的。主窗口那边只需在点击事件里创建并启动线程然后连接信号去更新界面。class MainWindow(QMainWindow): def open_image(self): path, _ QFileDialog.getOpenFileName( self, 选择图片, , Images (*.png *.jpg) ) if path: self.thread InferenceThread(path, detector, age_net) self.thread.result_ready.connect(self.show_result) self.thread.start() def show_result(self, result: dict): # 在主线程更新UI安全地绘制检测框 ...有一点必须强调不要在子线程里直接操作任何Qt控件Qt的UI不是线程安全的所有界面更新都要通过信号回到主线程做。4. 运行与避坑环境搭建、推理验证与5个典型问题排查4.1 环境准备照着我这套装不会翻车建议用conda建一个干净的虚拟环境Python版本选3.8或3.9最稳PyTorch和PySide6在这个区间的兼容性最好。PyTorch版本主要看显卡驱动和CUDA版本CPU机器也能跑只是慢。conda create -n bone_age python3.9 -y conda activate bone_age # 深度学习骨架CPU版直接去掉cuda索引前缀 pip install torch torchvision # 桌面端和图像处理库 pip install pyside6 opencv-python numpy装完先验证环境再继续这一步能省掉后面排查的半天时间python -c import torch; print(torch.__version__, torch.cuda.is_available())如果返回True再装PySide6不然后面报错你根本分不清是代码问题还是环境问题。我当时装的是1.13左右的PyTorch版本配CUDA 11.7X光片推理一张大约几百毫秒。4.2 最小推理示例不启动界面先验证模型拿到项目第一步永远是不碰界面写一个十几行的脚本把模型跑通。这样能把模型问题和界面问题隔离定位Bug快很多。import cv2 import torch from models.yolov5.detector import HandDetector from models.age_network import BoneAgeNet # 1. 加载检测器和骨龄网络 detector HandDetector(weights/yolo_hand.pt, conf_thres0.35) age_net BoneAgeNet(num_classes18) age_net.load_state_dict( torch.load(weights/age_net.pth, map_locationcpu) ) age_net.eval() # 2. 读取X光片并检测手骨区域 image cv2.imread(data/sample_001.png) boxes detector.detect(image) print(f检测到 {len(boxes)} 个候选框) # 3. 取最高置信度框裁剪ROI并做骨龄评估 if len(boxes) 0: box boxes[0] roi detector.extract_roi(image, box) roi_resized cv2.resize(roi, (224, 224)) roi_tensor torch.from_numpy(roi_resized).permute(2, 0, 1).float() / 255.0 with torch.no_grad(): prob age_net(roi_tensor.unsqueeze(0)).squeeze(0) print(骨龄等级分布:, prob.numpy())这里有个容易被忽略的点cv2.imread读出来是BGR通道顺序而YOLOv5训练时用的是RGB。如果你直接用BGR图喂进去检测框位置可能整体偏移。遇到这种情况先检查通道顺序不要急着质疑模型权重有问题。如果你有GPU把骨龄网络切到CUDA上推理。加载权重时用map_locationcpu只是一个保险实际推理可以再.to(cuda)反正权重是按state_dict存的先在CPU加载再搬设备完全合法。跑通这个脚本后再启动main.py做端到端验证你已经排除了最可能出问题的两个环节——权重加载和图像预处理。4.3 避坑记录跑这个项目时我踩过的5个坑第一条torch.hub.load每次都重新下载权重网络慢到怀疑人生。现象是每次运行脚本都卡在下载YOLOv5的仓库文件上。原因是force_reload没有设为False机器访问外网仓库不稳定。解决方法是改成force_reloadFalse或者手动把YOLOv5仓库clone到本地后用path参数直接指过去。这个参数调起来有点玄学不同网络环境的差异表现极大。第二条Pyside6点击按钮后窗口直接变成未响应。现象是选择图片后界面立刻转圈几秒后系统提示无响应。原因是推理逻辑直接写在按钮的clicked信号槽里阻塞了Qt事件循环。解决方法是把推理全部迁移到QThread子线程只在主线程里通过信号接收结果。排查时可以按一下CtrlC看终端有没有输出如果终端也在等基本就是主线程被堵死了。第三条加载骨龄权重时报size mismatch for backbone.fc。现象是load_state_dict时提示全连接层尺寸不一致。原因是拿到的权重是在18类数据上训练的本地把num_classes改成了别的值。解决方法是检查训练时的类别数保持num_classes一致或者禁用pretrained重新初始化最后几层。这类报错通常在权重加载处看到size mismatch先确认类别数别急着重训模型。第四条OpenCV读取中文路径的图片返回None。现象是图片路径只要带中文cv2.imread就静默失败查了半天代码没毛病。原因是OpenCV的imread内部用C的fopen不支持中文路径编码。解决方法是换成np.fromfile配合cv2.imdecodedef safe_imread(path: str) - np.ndarray: data np.fromfile(path, dtypenp.uint8) return cv2.imdecode(data, cv2.IMREAD_COLOR)这条在外面跑项目几乎必踩用户给的图片路径永远有你预想不到的字符。第五条YOLOv5对X光片漏检率偏高。现象是检测器在自然图像上表现正常换到X光片上经常一个框都检测不出来。原因是YOLOv5训练数据的分布以自然图像为主X光片是灰度图纹理和对比度差异很大。解决思路不是盲目调模型结构而是先做图像增强——对灰度图做CLAHE限制对比度自适应直方图均衡化把骨骼边缘的对比度拉起来再送进检测器同时把置信度阈值调低到0.25到0.3漏检率能明显下降。import cv2 def preprocess_for_detection(image: np.ndarray) - np.ndarray: 检测前增强对比度提升灰度X光片的检测召回率 gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) enhanced clahe.apply(gray) return cv2.cvtColor(enhanced, cv2.COLOR_GRAY2BGR)5. 二次开发思路把骨龄检测改造成你自己的影像识别工具5.1 替换YOLOv5为YOLOv8的迁移路径这个项目的检测器封装得比较薄换检测器比想象中容易。核心只需要改HandDetector内部的加载和推理两处代码。YOLOv8的权重加载方式不同但它同样有conf和iou属性对外暴露的detect方法签名不需要变界面和骨龄网络完全不用动。from ultralytics import YOLO class HandDetectorV8: YOLOv8版本的检测器接口与旧版保持一致 def __init__(self, weights_path: str, conf_thres: float 0.35): self.model YOLO(weights_path) self.conf_thres conf_thres def detect(self, image): results self.model.predict(image, confself.conf_thres) if len(results) 0 or results[0].boxes is None: return np.empty((0, 6)) return results[0].boxes.xyxy.cpu().numpy()换完之后用第4章的最小推理脚本做回归验证对比同一张X光片上新旧检测器的检测框坐标。如果坐标差异在可接受范围内界面端无需任何改动。5.2 数据增强解决小样本过拟合骨龄数据集的标注成本极高需要经验丰富的医生逐张标注小样本是这个领域躲不开的难题。我一般从几个方向同时下手一是几何增强随机旋转、翻转、轻微缩放这类增强不改变骨骼结构的医学语义二是灰度增强CLAHE、随机亮度偏移、高斯噪声模拟不同设备的成像差异三是在骨龄网络的全连接层前加Dropout这个项目已经做了但如果你自己改网络要注意别把这个层删掉。5.3 打包注意点把PySide6应用打包给一线人员用PyInstaller是主流做法但有几个坑必须处理一是torch.hub加载YOLOv5时依赖的仓库文件要跟着一起打包否则没网环境下直接失败二是模型权重文件加进--add-data而不是放在项目根目录打包后的路径解析要用sys._MEIPASS做兼容三是PySide6的插件目录比较大打包前用--exclude-module剔掉用不到的Qt模块能省不少体积。我从这个项目里学到的习惯是改任何检测类任务都先写一个不依赖界面的最小验证脚本把模型跑通了再碰UI。那次我改YOLOv5的输入预处理时直接改了主窗口的代码结果界面和数据流搅在一起一个通道顺序的问题排查了整个下午。从那以后我每次动底层逻辑都强制先跑一遍无界面脚本确认输出正常再回界面效率提升不是一点半点。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑