
简介面向医学影像与计算机视觉学习者这套X光掌骨分割数据包提供完整的二分割数据集、类别说明与可视化脚本适合用于骨分割模型的训练、验证与效果检查。数据集中训练集含1486张jpg图像与对应png掩膜测试集含92对图像原图分辨率统一为256×512采用0/1阈值标注前景区域清晰同时对部分样本做了缩放与翻转增广适应更多训练场景。包体共2000个文件包括1486个png掩膜、512个jpg原图、1个txt类别文件及1个python可视化脚本压缩后约15.47MB结构简洁便于使用。配套可视化脚本可直接运行随机抽取一张图并展示原始图像、GT掩膜及原图叠加蒙板的效果帮助快速核对数据质量。目前已有193人学习下载适合希望获得现成掌骨分割数据与调试工具的初学者及研究人员。1. 掌骨X射线分割数据集带标签、带脚本、能直接跑的二分割样本图像分割项目里最耗时间的一环往往是数据整理而不是模型结构。真正上手做医学影像项目的人才懂训练脚本半小时写完清洗标签反而要熬两三个晚上。我拿到这套掌骨X射线分割数据集时只扫了一眼目录就判断它能用——jpg原图、png掩码、classes文件、可视化脚本全齐了训练集1486张测试集92张分辨率统一为256×512前景用0/1阈值把掌骨区域和背景分开边界干净还带了数据增强样本。适合医学影像方向的学生、想快速验证分割算法的研究者以及需要小规模二分割基准做迁移实验的工程师。它省掉的是从原始X光片到标注掩码之间最烦琐的那段路。2. 数据集结构与classes语义1486张训练图像的类别组织与二值约定2.1 目录划分与文件名配对images和masks怎样才算一一对应拿到压缩包之后先不要急着跑训练脚本第一步是把目录结构看清楚。数据集按train和test分成两个独立大目录每个大目录下都有一对images和masks子目录。训练集里images放1486张jpg原图masks放1486张png掩码两者通过文件名字前缀一一配对测试集同理92对。图像和掩码之间的命名关系是同名不同扩展名比如图像文件M2-110--16-4-12_jpg.rf.d8659a31b287295de44cfcdda8dd55af.jpg在masks目录里对应的掩码就是M2-110--16-4-12_jpg.rf.d8659a31b287295de44cfcdda8dd55af.png把后缀从jpg替换成png就能定位到掩码。这套规则在分割数据集里很常见也是自动化加载逻辑里最简单可靠的对齐方式。文件名里那一串_jpg.rf.开头的哈希是数据从标注平台导出时留下的身份标记对训练本身没有任何影响。我见过有人试图把这串哈希当成图像特征参与建模没必要分割任务里文件名只是索引真正决定数据质量的是图像与掩码的几何对齐关系。建议先写一段配对校验逻辑遍历images目录里的jpg文件检查masks里是否有同名的png缺失数量不为零就要回头处理数据不能带着缺漏直接开始训练否则训练过程中会在某个epoch突然抛FileNotFoundError那时候再排查目录结构代价就高了。注意不要用os.listdir的返回值顺序作为配对依据那不可靠。要以jpg文件列表为准用os.path.splitext把扩展名替换成png再构造掩码路径去检查。2.2 classes文件与0/1前景语义单通道掩码为什么更稳这套数据的核心约定是前景区域用0和1做阈值分割落到实际文件里就是掩码以单通道PNG存储像素值为1的区域代表掌骨为0的区域代表背景X光片里骨组织与软组织的灰度对比明显所以分割边界不需要人工二次修正。classes文件在这时候起两个作用记录类别名与索引的对应关系同时告诉后续使用方背景和前景的排序约定。大多数二分割任务的惯例是类别索引0对应背景、1对应前景如果你的训练代码从1开始编类索引Loss计算时类别就全体错位验证集Dice直接飘到负数。单通道0/1掩码比三通道RGB掩码省事得多。RGB掩码保存时经常带调色板工程中常见的是用红色区域代表前景加载时还得做颜色到类别的像素映射而单通道0/1掩码本身就是标签值读进来就能用。读取方式一般是用PIL的Image.open(掩码路径).convert(L)转成灰度转成numpy数组后要注意取值范围有些工具链会把掩码写成0和255而不是0和1。如果是0/255必须先除以255或者做一次阈值化把它拉回0/1再送进二值交叉熵或Dice损失。这个细节我在YOLOv8训练自己的分割任务时踩过一次PNG被读成0/255后整个掩码参与计算时全部被判成前景损失曲线看起来正常但Dice指标一直是0属于最隐蔽的翻车点。2.3 数据增强痕迹缩放、翻转过的掩码不能独立变换摘要里说“对部分数据进行了缩放、翻转图像增广”意思是这批掌骨数据里已经有一部分样本做过离线增强而且增强后的掩码是同步变换过的几何关系自洽。这也带来一个隐藏信息你无法从文件名直接看出哪些样本是增强出来的所以后续如果要继续做在线增强必须对图像和掩码做同步变换不能把两路数据当成独立样本分别随机处理。我一般推荐用albumentations库处理这类分割增强它内部保证image和mask使用同一套变换参数如果用torchvision的RandomHorizontalFlip就要让image和mask共享同一个随机状态保证翻转向量一致。还有一个容易忽视的边界离线增强里的缩放操作如果填充成黑色会在图像边缘引入一圈纯黑像素这些像素会被当成背景参与训练。掌骨区域在256×512画面里本身占比不高边缘黑边会进一步稀释前景的学习信号。我的做法是在加载图像时加一个尺寸断言如果发现图像宽高不是512和256先记录文件名统一做一次resize而不是依赖训练管线隐式处理。3. 可视化脚本一次运行同时确认原图、GT掩码与叠加效果3.1 脚本逻辑拆解随机采样、三图并列与保存数据集附带的可视化脚本解决的核心问题是“标签能不能直接用”。它会从训练集里随机抽一张图把原始X光图、GT掩码图、以及GT叠加在原图上的效果并排画出来最后保存成一张图片。这个过程比直接看文件列表直观得多你能快速确认掩码是否贴合骨骼边缘、有没有空洞前景0/1的边界是不是干净顺便还能发现某些样本是否存在翻转导致的错位。典型实现可以拆成四步扫描images和masks目录构造同名配对列表用random.choice从列表里抽一张用PIL分别读入图像和掩码掩码按单通道灰度读入并转成布尔类型最后用matplotlib画三个子图并保存。下面这段代码可以当作参考模板拿到数据集后对照它检查自己的目录名是否一致import os import random import numpy as np import matplotlib matplotlib.use(Agg) import matplotlib.pyplot as plt from PIL import Image image_dir train/images mask_dir train/masks # 构造同名配对不依赖listdir返回顺序 pairs [] for f in sorted(os.listdir(image_dir)): if not f.endswith(.jpg): continue mask_name os.path.splitext(f)[0] .png if os.path.exists(os.path.join(mask_dir, mask_name)): pairs.append((f, mask_name)) assert len(pairs) 0, 没有找到任何图像-掩码配对请检查目录名 # 随机抽一张样本 img_name, mask_name random.choice(pairs) img np.array(Image.open(os.path.join(image_dir, img_name)).convert(RGB)) mask np.array(Image.open(os.path.join(mask_dir, mask_name)).convert(L)) mask_bool mask 0 # 不管原掩码是0/1还是0/255统一变成布尔掩码 # 在原始图像上叠加半透明绿色蒙版 overlay img.copy() overlay[mask_bool] ( overlay[mask_bool] * 0.4 np.array([0, 255, 0]) * 0.6 ).astype(np.uint8) fig, axes plt.subplots(1, 3, figsize(15, 5)) axes[0].imshow(img) axes[0].set_title(Original X-ray) axes[1].imshow(mask, cmapgray) axes[1].set_title(GT mask) axes[2].imshow(overlay) axes[2].set_title(GT overlay) for ax in axes: ax.axis(off) plt.tight_layout() plt.savefig(vis_random_sample.png, dpi150) print(f样本已保存{img_name})代码里的配对逻辑不依赖listdir的返回顺序而是先对jpg文件名排序再替换扩展名去构造掩码路径从机制上保证图像和掩码一定对齐。读取掩码时用convert(L)转成单通道灰度再做mask 0的布尔化这样存储值是0/1还是0/255都不影响叠加结果。叠加部分用的是绿色通道加权融合实际就是把掌骨区域在原图上做了标亮这张图可以作为快速验收样本也可以直接拿去给临床同事做粗糙预览。3.2 运行方式与依赖不改代码直接运行的边界条件如果你下载解压后的目录结构跟上面一致也就是train/images和train/masks都存在那么直接执行python visualize.py就能得到一张vis_random_sample.png保存在当前工作目录下。运行前只要确认Python环境里有pillow、matplotlib、numpy三个库缺少哪个用pip补哪个版本没有特殊要求Python 3.8以上都能跑。脚本里特意加了assert检查避免目录配错时用空列表悄悄跑完、最后存一张空白面板的情况——那种错误最难排查。我习惯把这第一张可视化图当作数据集验收的第一道手续。如果GT覆盖区域跟X光片上可辨认的掌骨轮廓基本重合再进入下一步如果看到掩码覆盖到了软组织甚至图像边框就要怀疑掩码在导出时发生了平移。数据里已经有离线增强样本所以偶尔抽到一张翻转过的图是正常的只要掩码和骨骼位置仍然对得上就可以放心用。4. 把该数据集接入分割训练从Dataset类到损失函数的选择4.1 自定义PyTorch Dataset配对读取、尺寸校验与二值转换可视化脚本解决的是“数据能不能用”的问题但让数据真正参与训练还需要一个稳定的数据加载器。下面这个Dataset类专门针对这套掌骨数据设计兼容PIL和PyTorch读入后完成resize、归一化和二值化三个动作。关键点在于掩码的类别语义永远是前景1、背景0所以从PNG里读出来的像素值不能直接当标签用必须先做阈值判断import os import numpy as np import torch from torch.utils.data import Dataset from PIL import Image class BoneSegDataset(Dataset): def __init__(self, image_dir, mask_dir, size(256, 512)): self.image_dir image_dir self.mask_dir mask_dir self.size size self.names [] for f in sorted(os.listdir(image_dir)): if f.endswith(.jpg) and os.path.exists( os.path.join(mask_dir, os.path.splitext(f)[0] .png) ): self.names.append(f) def __len__(self): return len(self.names) def __getitem__(self, idx): name self.names[idx] mask_name os.path.splitext(name)[0] .png img Image.open(os.path.join(self.image_dir, name)).convert(RGB) mask Image.open(os.path.join(self.mask_dir, mask_name)).convert(L) img img.resize(self.size, Image.BILINEAR) mask mask.resize(self.size, Image.NEAREST) # 掩码不能用平滑插值 x np.array(img).astype(np.float32) / 255.0 x torch.from_numpy(x).permute(2, 0, 1) y np.array(mask).astype(np.float32) y (y 0).astype(np.float32) # 统一转成0/1标签 y torch.from_numpy(y).unsqueeze(0) return x, y一个常见错误是掩码resize时也用BILINEAR甚至BICUBIC这会在类别边界产生过渡灰阶而二分割掩码的类别边界应该是硬边所以这里用Image.NEAREST是必须的。图像除以255归一化到0到1掩码通过(y 0)统一成0/1这样原始掩码是0/1还是0/255都不影响标签值域。返回时掩码加上了单通道维度形状是(1, 256, 512)跟模型输出的(B, 1, H, W)对齐后面直接接BCEWithLogitsLoss时不需要再压缩维度。这里要额外提醒一点不要对torch.from_numpy的结果直接做mask 0判断因为布尔张量会打断梯度链先转成float再比较是为了让掩码只参与Loss计算、不进入反向传播路径。这个细节在自定义数据集里经常被忽略等训练到第二个epoch突然报RuntimeError时想回头找问题就得翻一遍整个数据加载链路。4.2 训练配置BCE、Dice损失与掌骨小目标的评估口径掌骨分割是典型的类不平衡二分割任务掌骨区域在256×512的图像里通常只占10%到20%背景占绝对多数。直接用普通交叉熵训练模型很容易收敛到“全预测为背景”的退化解因为这种解已经能拿到80%以上的准确率。实际训练时要把BCE和Dice损失联合使用Dice损失直接优化区域重叠程度对前景召回更敏感import torch.nn.functional as F def bce_dice_loss(pred, target): pred_prob pred.sigmoid() bce F.binary_cross_entropy(pred_prob, target) inter (pred_prob * target).sum(dim(2, 3)) union pred_prob.sum(dim(2, 3)) target.sum(dim(2, 3)) dice 1.0 - (2.0 * inter 1.0) / (union 1.0) return bce dice.mean()这个函数先对logits做sigmoid转成概率再分别算BCE和软Dice。inter和union按单样本在宽高维度聚合所以返回的Loss是batch维度上的均值。分子分母里加1.0的平滑常量是为了避免某个batch里target恰好全为零时出现除零这在掌骨分割的边缘样本里确实会遇到。一般来说BCE和Dice两部分量级接近不需要额外加权如果训练初期Loss下降很慢可以给Dice部分乘1.5其余超参不变。输入分辨率256×512对大多数分割模型都很友好。U-Net这类编码器-解码器结构在这个尺度下感受野足够覆盖整个掌骨区域即使换成MobileNetV3这类轻量骨干这个尺度也能保证骨骼边缘细节不丢失太多。训练时batch size建议8到16更大的batch在小样本数据集上会加剧过拟合毕竟训练集只有1486张优化器用Adam初始学习率1e-4起步配合ReduceLROnPlateau在Dice连续5个epoch不涨时降学习率这是我在类似医学分割项目里验证过比较稳的参数组合。5. 避坑与常见问题掩码全黑、尺寸不一致与增强错位的四项排查5.1 现象掩码读出来全黑或全白跑完可视化脚本GT掩码面板要么纯黑要么纯白完全看不到骨骼结构。原因通常是掩码的存储值域和读取方式不匹配。如果掩码以0/255存储代码里直接拿原始值做灰度显示纯黑和纯白会同时出现因为0和255对应灰度条的两端如果掩码以0/1存储但显示时用了默认灰度映射视觉上也会偏黑。解决方式统一收敛到一条读入后先执行mask 0转成布尔掩码再做显示或者参与Loss计算。我在这个数据集上的实践是不信任任何原始值所有掩码进入模型前都强制过这一道能省掉后面一整类疑难杂症。5.2 现象增强后掩码与骨骼区域错位训练时加了水平翻转和随机旋转跑几个epoch后发现验证集Dice异常低抽看预测结果骨骼位置跟原图对不上。原因是在线增强时图像和掩码没用同一套随机变换参数或者旋转时掩码的填充值设置不对。解决方式是改用albumentations它内部保证image和mask同步变换如果坚持用torchvision先把随机种子固定成同一个值再分别对图像和掩码调用同一个随机翻转函数。最容易出问题的是旋转旋转之后的边缘填充值默认补0这部分会被当成背景参与训练累积下来会让分割区域向内收缩。我的做法是旋转角度不超过15度并且显式指定填充值。5.3 现象训练损失下降但Dice接近0这是最隐蔽的一种翻车。Loss曲线一直在降训练集准确率也很高但Dice算出来就是0原因大概率是标签值范围不一致网络输出经过sigmoid后取值在0到1之间掩码如果还是0/255两个张量几乎不会产生交集Dice自然趋近于0。另一个常见原因是在评估阶段把阈值设成了0导致所有像素都被判定为前景。解决方式是在评估函数里统一执行(torch.sigmoid(pred) 0.5)掩码统一执行(mask 0)两边都落在布尔域里再算交集和并集这样算出来的Dice才有对比意义。这条坑我踩过不止一次现在每次写评估脚本都先确认阈值和标签域。5.4 现象脚本报FileNotFoundError目录里明明有文件错误信息提示找不到文件但资源管理器里能看到对应的jpg和png。原因大多是工作目录和数据集目录不在同一层。可视化脚本普遍用相对路径train/images做寻址如果把脚本放在项目根目录、数据集解压到别处运行时就会找不到。解决方式是把目录结构理顺让train文件夹跟脚本同级或者在脚本开头用一个可配置的路径变量统一管理数据根目录。我一般先执行一次ls train/masks | head -5看一下目录是否存在再跑训练脚本这一条小习惯能让一半的文件读取问题直接消失。6. 进阶用Dice与mIoU把二分割结果量化为可信数值可视化只能证明“分割图看起来像”要交付结果还得有数值指标。下面这段代码对模型输出的logits和GT掩码计算Dice与mIoU两个指标都按类别分别计算后取平均背景类也纳入统计。注意两个细节预测概率阈值取0.5背景类的预测由1减去前景概率推导出来这样不用单独算背景分支import numpy as np def seg_metrics(pred, mask_bool, eps1e-7): pred_bool pred 0.5 dice_list [] iou_list [] for c in range(2): pred_c pred_bool if c 1 else ~pred_bool mask_c mask_bool if c 1 else ~mask_bool inter (pred_c mask_c).sum() union (pred_c | mask_c).sum() dice_list.append(2.0 * inter / (pred_c.sum() mask_c.sum() eps)) iou_list.append(inter / (union eps)) return np.mean(dice_list), np.mean(iou_list)背景和前景分别算IoU再取平均能更均衡地反映分割质量掌骨这类小面积目标背景IoU往往在0.98以上前景IoU可能只有0.75只看均值会掩盖前景的真实水平所以我通常前景IoU单独看一遍均值只作为总体参考。从那以后我每次拿到新的分割数据集第一件事都是强制自己跑一遍可视化脚本确认样本配对完整、掩码值域正常再往下写数据加载和训练代码。这套掌骨数据让我把“先验收标签、再碰模型”这个习惯彻底固化下来了希望帮到你。本文还有配套的精品资源点击获取