X光骨骼关节分割实战:从Roboflow txt标签到PyTorch语义分割
2026/9/15 6:11:36 网站建设 项目流程

简介:面向医学影像分析与深度学习语义分割任务,该X射线骨骼关节图像数据集提供了训练、验证与测试划分,其中训练集约1100张、验证集约100张,适合训练像素级分割模型或进行迁移学习。资源共2000个文件,以1185个PNG和813个JPG图像为主体,另附1个类别相关txt与1个Python工具脚本,整体压缩包仅12MB,轻量便捷。已有151人学习,属于轻量级医学影像数据集,可直接用于分割实验。对于从事骨骼影像分析的研究者或初学者,可利用该数据快速搭建分割流程,验证模型在X光图像上的表现,并通过类别文件和脚本了解数据组织方式,便于后续扩展自己的训练管道。整套内容完整,适合作为课程设计、论文实验或算法对比的基准数据。

1. 骨骼X光关节分割:一个把灰度图当语义分割做的数据集

把这个X射线骨骼关节分割数据集下载下来,你会看到一堆472_png.rf.44371cac8276ef31c203b35751e81c71.jpg这种Roboflow导出命名的文件,真正的掩码不在PNG里,而在同名txt里,每行记录一组归一化多边形坐标。这是一套典型的小样本医疗语义分割数据:3个骨骼关节相关类别,训练图像约1100张、验证集约100张、测试集按要求单独划分,具体类别名以json类别文件为准。X光图比自然图像难分割得多——骨骼边缘是骨密度在投影方向上的连续衰减,软组织与骨骼灰度高度重叠,肉眼都难界定的边界,模型更容易在关节间隙处输出碎块。适合它的读者,是正在做骨科AI诊断前置模块的工程师,也可以是从零训练第一个语义分割模型的深度学习入门者,下面所有代码都能直接复现。

2. Roboflow导出的txt多边形标签,先转成PyTorch能用的语义掩码

2.1 从.rf.命名反推数据集结构与标签格式

文件名里的.rf.是Roboflow导入导出时留下的版本标记,472_png是原始样本编号,后半段哈希是本次导出会话标识,同名jpg与txt一一对应。常规Roboflow导出结构会把数据按images/trainlabels/trainimages/test等目录组织,训练、验证、测试的划分已在摘要中明确:训练约1100张,验证约100张,测试集另计。解压后的第一件事是把目录完整列出来做一次文件名比对,缺标签的样本直接剔除,否则训练时DataLoader会读到空列表,报错信息又恰好指向不明,排查起来非常耗时间。

路径内容说明
images/train/*.jpgX射线原图灰度图,关节区域占画面主体
labels/train/*.txt同名标签每行一个实例的多边形
data.yaml / json类别定义类别数3,ID从0开始递增

txt标签是YOLO segmentation格式,一行结构是类别ID x1 y1 x2 y2 ...,坐标为相对图像宽高的归一化浮点数。多个顶点依次相连构成封闭多边形,覆盖一个关节实例。因为坐标做了归一化,解析时必须要用原图尺寸还原像素坐标,这是新手最容易出错的地方。如果数据里有部分样本是用矩形框标注的,多边形只有4个顶点,解析逻辑同样适用,只是掩码边缘会比人工多边形略微生硬。这正好呼应了"语义分割数据集如何制作"这条路上最常见的坑:标注格式和训练框架不对齐。

2.2 解析归一化多边形并绘制真正的掩码

import cv2 import numpy as np from pathlib import Path def parse_yolo_seg_txt(txt_path: Path, img_w: int, img_h: int, min_poly_area: float = 25.0): masks, class_ids = [], [] lines = txt_path.read_text().strip().splitlines() for line in lines: parts = line.strip().split() if len(parts) < 7: # 类别ID + 至少3个坐标点 continue cls_id = int(parts[0]) xy = np.asarray(parts[1:], dtype=np.float32).reshape(-1, 2) xy[:, 0] *= img_w # 归一化坐标还原为像素x xy[:, 1] *= img_h # 归一化坐标还原为像素y if cv2.contourArea(xy.astype(np.int32)) < min_poly_area: continue # 面积过小视为标注噪点 mask = np.zeros((img_h, img_w), dtype=np.uint8) cv2.fillPoly(mask, [xy.astype(np.int32)], 1) masks.append(mask) class_ids.append(cls_id) return masks, class_ids

split()按连续空白字符切分,能兼容空格或逗号分隔的txt;reshape(-1, 2)要求坐标点总数是偶数,解析前可以先打印一行原始数据确认有没有尾随空格或空行。cv2.contourArea计算的是多边形面积,过滤掉极小噪点,这个阈值在X光图上一般设在25~100像素之间,具体看原图分辨率。fillPoly[H, W]的零矩阵上原地绘制,掩码值设为1,多个实例的合并放到Dataset层处理,解析函数保持单实例职责更清晰。

2.3 组装BoneJointSegDataset并把同类别实例合并

一个关节类别在同一图像里可能出现多个实例,比如双侧同名关节,而语义分割要求每个像素只有一个类别ID,所以同一类别的所有mask要在这里合并:

import torch from torch.utils.data import Dataset class BoneJointSegDataset(Dataset): def __init__(self, image_dir, label_dir, num_classes=3): self.image_paths = sorted( p for p in Path(image_dir).glob("*.jpg") if (Path(label_dir) / (p.stem + ".txt")).exists() ) self.label_dir = Path(label_dir) self.num_classes = num_classes def __len__(self): return len(self.image_paths) def __getitem__(self, idx): img_path = self.image_paths[idx] image = cv2.imread(str(img_path), cv2.IMREAD_GRAYSCALE) h, w = image.shape[:2] masks, class_ids = parse_yolo_seg_txt( self.label_dir / (img_path.stem + ".txt"), w, h ) seg_label = np.zeros((h, w), dtype=np.int64) for cls_id, mask in zip(class_ids, masks): seg_label[mask > 0] = int(cls_id) # 覆盖式合并同类别实例 image = torch.from_numpy(image).unsqueeze(0).float() / 255.0 seg_label = torch.from_numpy(seg_label) return image, seg_label

seg_label[mask > 0]直接覆盖赋值,如果同一像素被两个不同类别命中,后一个会覆盖前一个,这类标注重叠需要打开原图人工确认是绘制错误还是关节自然嵌合。灰度图unsqueeze(0)变成[1, H, W],除以255归一化到[0,1]区间,标签保持int64类型供CrossEntropyLoss使用。

注意:千万不要把txt路径直接传给PyTorch语义分割模型,YOLO系模型原生支持txt,但大多数语义分割框架只认掩码图,必须先完成上面这一步转换。

3. 训练路线选择:YOLOv8-seg与DeepLabV3+的取舍

3.1 先回答三个问题再定方案

什么样的任务适合用yolov8训练自己的数据集,什么样的场景该上deeplabv3语义分割,关键看三点。第一,如果同一张图里出现两个同类别实例,后续流程是否需要区分“左侧关节”和“右侧关节”,需要区分就选YOLOv8-seg,它输出天然带实例ID。第二,如果下游只是拿关节掩码区域做特征统计,DeepLabV3+直接输出像素级类别概率,后处理更少。第三,训练成本与预期精度:YOLOv8预训练权重来自COCO实例分割,与X光场景差异大,但胜在框架收敛快、评估链路完整;DeepLabV3+的ASPP空洞空间金字塔池化对低对比度边缘建模更强,关节间隙处的连续性往往更好。这个数据集本身是多实例多边形标注,两条路线都能跑通,我建议先跑DeepLabV3+做基线,再用YOLOv8-seg对比实例级效果差距。

3.2 路线A:YOLOv8-seg直接吃txt标签

安装ultralytics后,segment子命令原生支持上面解析过的YOLO分割txt格式,只需要准备一个data.yaml,不需要写Dataset类。

# data.yaml path: /data/bone-joint-xray train: images/train val: images/valid test: images/test nc: 3 names: 0: joint_a 1: joint_b 2: joint_c

names的值必须与json类别文件一一对应,顺序错位时表现为预测类别和掩码错位,但训练loss依然下降,最容易漏掉。训练命令如下:

yolo segment train \ data=data.yaml \ model=yolov8s-seg.pt \ epochs=120 \ imgsz=640 \ batch=8 \ device=0

yolov8s-seg.pt是s尺寸权重,关节这类边缘精细目标不建议用n模型,它容易在高噪声X光上把软组织误判成骨骼;显存有余量就升级yolov8m-seg.ptimgsz=640适配关节占画面主体的场景,原图分辨率特别高时先看一眼缩略图确认关节边缘是否仍然清晰。训练完用yolo segment val data=data.yaml model=runs/segment/train/weights/best.pt跑验证,会自动输出每个类别的mAP50与分割掩码IoU。

3.3 路线B:DeepLabV3+用smp库快速复现

pip install segmentation-models-pytorch后,按照语义分割模型的标配流程组装即可。DeepLabV3+的亮点在解码器端的ASPP模块,以不同膨胀率的空洞卷积并行抓取多尺度上下文,很适合骨密度渐变的X光灰度图像:

import segmentation_models_pytorch as smp import torch model = smp.DeepLabV3Plus( encoder_name="resnet50", encoder_weights="imagenet", classes=3, activation="softmax2d", ) criterion = torch.nn.CrossEntropyLoss() optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4)

smp默认输入三通道RGB,而X光数据集是单通道,常见做法是把灰度通道复制成三份喂给ImageNet预训练编码器。预训练权重通常能让模型前30轮训练稳定掉loss,但X光与自然图像特征差异大,验证指标一直不涨时就删掉encoder_weights从头训练。训练循环里补上通道复制:

for images, masks in train_loader: images = images.repeat(1, 3, 1, 1).to(device) # [B,1,H,W] -> [B,3,H,W] logits = model(images) # [B,3,H,W] loss = criterion(logits, masks.to(device)) optimizer.zero_grad() loss.backward() optimizer.step()

repeat(1, 3, 1, 1)在通道维上复制三次,batch维、高宽维保持不变。activation="softmax2d"已经内置Softmax,推理时直接用logits.argmax(dim=1)取类别ID。两条路线的差异总结如下:

对比项YOLOv8-segDeepLabV3+ (smp)
输出粒度实例掩码+类别像素级类别概率
标签格式原生支持txt需转成掩码图
预训练来源COCO实例分割ImageNet分类
代码量低,yaml即训需自写训练循环
边缘平滑性依赖后处理ASPP天然平滑

测试集只有理论上独立的一份数据,最终评估要以它为准,不要反复拿验证集调epoch,100张验证图的随机涨跌噪声足以掩盖真实精度差距。

4. 灰度投影图的增强策略与关节掩码不均衡处理

4.1 增强不是越多越好,先把灰度语义想清楚

自然图像分割常用的颜色抖动、HSV扰动在X光数据集上要慎用。X光灰度反映组织密度投影,骨骼亮、软组织暗,是物理量纲而非图像风格。把对比度拉得过强,模型会把软组织灰阶误判成骨骼边缘——大量入坑者在关节间隙处得到大片伪掩码,根因就是这个。几何类增强安全得多:水平翻转对骨骼关节特别有效,人体左右解剖结构基本对称;旋转控制在±15°内,超过这个范围关节相对位置关系失真;平移与缩放配合可模拟拍摄距离和摆位差异。垂直翻转在解剖学上没有对应操作,建议关闭。

4.2 用albumentations组装医疗影像增强管线

albumentations会同步变换图像与掩码,Compose里的几何增强自动作用于两者,这是它比手写torchvision变换省事的地方:

import albumentations as A import cv2 train_transform = A.Compose([ A.HorizontalFlip(p=0.5), A.Rotate(limit=15, border_mode=cv2.BORDER_CONSTANT, value=0, p=0.6), A.ShiftScaleRotate(shift_limit=0.05, scale_limit=0.12, border_mode=cv2.BORDER_CONSTANT, value=0, p=0.5), A.RandomBrightnessContrast(brightness_limit=0.15, contrast_limit=0.15, p=0.5), A.GaussNoise(var_limit=(10.0, 30.0), p=0.3), A.Resize(512, 512), ], is_check_shapes=True)

BORDER_CONSTANT配合value=0让旋转和平移产生的空白区域呈黑色,与X光黑底背景一致,避免白边被模型学成高密度骨骼特征。GaussNoise的方差按0~255灰度级设置,10到30对应中等噪声,模拟低剂量X光采集时的量子噪声。Resize放最后保证输出尺寸统一,缩放前不要先做其他会改变尺寸的变换,否则标签和图像会错位。

4.3 掩码占比差异大,混合损失函数兜底

不同类别在图像上的面积占比差异明显,关节掩码整体可能只占全图的3%~20%。纯CrossEntropyLoss被大面积背景主导,训练出的模型会把关节边缘往里收缩,宁可少预测也不多预测;纯DiceLoss在小目标上梯度震荡大,验证loss忽高忽低。常见做法是两者按1:0.5加权组合:

import torch import torch.nn as nn import torch.nn.functional as F class DiceLoss(nn.Module): def __init__(self, smooth: float = 1e-5): super().__init__() self.smooth = smooth def forward(self, logits, targets): probs = torch.softmax(logits, dim=1) _, c = probs.shape[:2] target_onehot = F.one_hot(targets, num_classes=c).permute(0, 3, 1, 2).float() intersection = (probs * target_onehot).sum(dim=(2, 3)) union = probs.sum(dim=(2, 3)) + target_onehot.sum(dim=(2, 3)) dice = (2.0 * intersection + self.smooth) / (union + self.smooth) return 1.0 - dice.mean() criterion = DiceLoss() + 0.5 * nn.CrossEntropyLoss()

F.one_hot[B,H,W]的标签转成[B,H,W,C]permute(0,3,1,2)调整成与logits一致的[B,C,H,W]smooth参数防除零,全背景样本同样适用。训练时留意两类loss的量级:DiceLoss长期在0.8以上不降,说明类别不均衡严重,应提高CE权重;CE很低而Dice波动大,则是边缘预测与标注存在系统性偏移,这类偏移纯CE下看不出来,正是混合损失的监控价值。

5. 用IoU和Dice校准模型:评估、过拟合识别与落地技巧

5.1 验证集逐类统计IoU与Dice

def compute_metrics(pred: torch.Tensor, gt: torch.Tensor, num_classes: int = 3): ious, dices = [], [] for c in range(num_classes): p = (pred == c) g = (gt == c) inter = (p & g).sum().item() union = (p | g).sum().item() ious.append(inter / max(union, 1)) dices.append(2.0 * inter / max(p.sum().item() + g.sum().item(), 1)) return ious, dices

验证循环里逐batch累计,全部算完再求平均,得到每个类别的IoU与Dice,mIoU取三类均值。关节类别在IoU上的数值通常会低于Dice,因为IoU对漏检更敏感,报告模型效果时建议两个指标都写。

5.2 过拟合的三种信号

训练loss下降而验证mIoU连续5轮不升,优先怀疑过拟合,回退epoch或加大几何增强强度;训练mIoU与验证mIoU差距超过0.15为明显过拟合,直接取倒数第二或第三轮权重往往更实用;预测图出现细碎孔洞而训练集掩码边缘光滑,多为标注噪声被模型放大,用连通域过滤即可缓解。

5.3 三个低成本提分技巧

第一,水平翻转TTA。推理时把输入翻转一次,同一个模型把softmax概率翻回来与原输出平均,关节对称性让这个操作直接提升边缘稳定性:

flip_logits = model(image.flip(-1)).flip(-1) final_logits = (logits + flip_logits) / 2

第二,连通域过滤。cv2.connectedComponents去除面积小于100像素的前景块,细小伪掩码大多落在关节间隙噪声区:

num, labels = cv2.connectedComponents(pred_mask.astype(np.uint8)) for lbl in range(1, num): if np.sum(labels == lbl) < 100: pred_mask[labels == lbl] = 0

第三,半精度推理。model.half()配合torch.no_grad()在CUDA上提速约1.8倍,mIoU下降通常小于0.005,批量后处理场景下值得先对比一次精度再决定是否启用。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询