
简介这份资源面向医学图像处理初学者与骨骼分割方向的算法实践者提供X光手掌影像下的掌骨二分类分割数据集前景采用0/1阈值标注边界清晰适合练手语义分割模型或验证医学影像预处理流程。包内共2000个文件以1486个png掩膜、512个jpg图像为主另含1个txt类别说明与1个py可视化脚本压缩包约15.47MB图像统一为256×512分辨率。数据已划分训练集与测试集训练集含1486张原图及对应mask测试集含92张原图及对应mask部分样本做过缩放与翻转增广具体规则可参考classes文件。附带的Python脚本无需修改即可直接运行随机抽取一张图片同时展示原始图像、GT图像以及GT叠加在原图上的蒙板效果并自动保存到当前目录便于快速核对标注质量。目前已有193人学习适合希望低成本上手骨分割任务、搭建训练与评估基线的读者。1. 掌骨 X 光分割数据集从拿到手到跑通第一个可视化医学影像里有一类分割任务数据量不大、类别不多但标注成本极高X 光下的掌骨分割就是典型。它要做的只有两件事把每根掌骨从背景里抠出来再把不同掌骨区分开——也就是标题里说的「2 分割」通常指前景骨骼与背景的二分类或者左右手/不同骨段的二分类。这类数据集的价值不在于规模而在于它把「放射影像 像素级标注 类别映射文件」打包好了省掉最耗时的标注环节。拿到一个掌骨分割数据集你真正要关心的不是它有多少张图而是三件事标注是掩膜还是多边形、classes 文件里的类别顺序对不对、X 光灰度分布会不会让模型把软组织误判成骨。这篇笔记就按「数据集结构 → classes 文件解析 → 可视化验证 → 训练前处理 → 踩坑排查 → 进阶技巧」的顺序把一套能直接复现的流程讲清楚。适合刚接触医学图像分割的工程师也适合想快速验证一个分割 pipeline 的熟手。2. 拆开数据集目录结构、标注格式与 classes 文件2.1 先看目录别急着写 DataLoader掌骨 X 光数据集常见的组织方式有两种一种是images/和masks/平行存放文件名一一对应另一种是train/val/test下各自带images和masks。不管哪种第一步都是把目录树打印出来确认图像和掩膜的数量、扩展名、命名规则是否一致。# 打印数据集目录结构只看两层避免输出爆炸 find ./hand_bone_dataset -maxdepth 2 -type d | sort # 统计图像和掩膜数量确认是否配对 echo images: $(find ./hand_bone_dataset -path *images* -name *.png | wc -l) echo masks: $(find ./hand_bone_dataset -path *masks* -name *.png | wc -l)逻辑说明find -maxdepth 2只展开两层防止深层目录刷屏两条wc -l分别统计图像和掩膜数量数量不一致说明有缺失或命名不匹配。参数上如果你的数据是.jpg或.bmp把-name改掉即可。这一步看起来简单但我见过太多人直接开训跑到一半才发现掩膜少了几十张loss 曲线诡异波动回头查半天。2.2 classes 文件到底存了什么classes.txt或classes.json是这类数据集的灵魂。它通常记录类别名和类别索引的映射比如background bone或者带索引0 background 1 bone这里有个关键点索引从 0 还是从 1 开始直接决定你的掩膜像素值怎么映射。如果 classes 文件写的是0 background / 1 bone那掩膜里像素值 0 是背景、1 是骨如果写的是1 background / 2 bone而你的代码按 0/1 处理就会把背景当骨、骨当背景训练出来的模型完全反向。常见做法是写一个解析函数把 classes 文件读成字典再和掩膜的唯一像素值做交叉验证。import numpy as np from PIL import Image def parse_classes(classes_path): 解析 classes 文件返回 {索引: 类别名} 和 {类别名: 索引} idx2name, name2idx {}, {} with open(classes_path, r, encodingutf-8) as f: for line in f: line line.strip() if not line: continue parts line.split() # 兼容 0 background 和 background 两种写法 if parts[0].isdigit(): idx, name int(parts[0]), parts[1] else: idx, name len(idx2name), parts[0] idx2name[idx] name name2idx[name] idx return idx2name, name2idx def check_mask_values(mask_path, idx2name): 检查掩膜里出现的像素值是否都在 classes 索引范围内 mask np.array(Image.open(mask_path)) unique_vals np.unique(mask) print(f掩膜唯一像素值: {unique_vals}) for v in unique_vals: if v not in idx2name: print(f警告: 像素值 {v} 不在 classes 索引中) return unique_vals idx2name, name2idx parse_classes(./hand_bone_dataset/classes.txt) print(类别映射:, idx2name) check_mask_values(./hand_bone_dataset/masks/0001.png, idx2name)逻辑说明parse_classes同时兼容带索引和不带索引的写法不带索引时按出现顺序自动编号。check_mask_values把掩膜的唯一像素值和 classes 索引比对任何不在映射里的值都要警惕——可能是标注工具留下的边缘值也可能是类别定义和实际标注不一致。参数上如果你的掩膜是彩色图需要先转灰度或按通道解析不能直接np.unique。2.3 图像与掩膜的配对校验配对校验不只是数数量还要确认文件名能对上、尺寸一致、掩膜不是全黑。写一个批量校验脚本跑一遍比肉眼翻图靠谱得多。import os from PIL import Image def validate_pairs(img_dir, mask_dir): img_files sorted(os.listdir(img_dir)) mask_files sorted(os.listdir(mask_dir)) assert len(img_files) len(mask_files), 图像与掩膜数量不一致 issues [] for img_name, mask_name in zip(img_files, mask_files): img_path os.path.join(img_dir, img_name) mask_path os.path.join(mask_dir, mask_name) img Image.open(img_path) mask Image.open(mask_path) if img.size ! mask.size: issues.append(f尺寸不匹配: {img_name} {img.size} vs {mask_name} {mask.size}) mask_np np.array(mask) if mask_np.max() 0: issues.append(f掩膜全黑: {mask_name}) return issues issues validate_pairs(./hand_bone_dataset/images, ./hand_bone_dataset/masks) for i in issues[:10]: print(i) print(f共发现 {len(issues)} 个问题)逻辑说明zip按排序后一一配对前提是图像和掩膜文件名完全一致包括扩展名。尺寸不匹配在 X 光数据里很常见因为有些掩膜是原图裁剪后再标注的。掩膜全黑说明这张图没有有效标注训练时要么剔除要么当负样本处理。参数上如果文件名前缀一致但扩展名不同需要先把名字归一化再配对。3. 可视化验证把灰度图和掩膜叠在一起看3.1 为什么必须先可视化再训练X 光图像的灰度分布和自然图像完全不同骨骼、软组织、背景的对比度低直接看掩膜可能觉得「标得挺好」但叠到原图上就会发现边缘偏移、细小骨段漏标。可视化是成本最低的质检手段也是排查「模型学不动」的第一入口。常见做法是把原图转成三通道掩膜用半透明色覆盖再并排显示。import matplotlib.pyplot as plt import numpy as np from PIL import Image def visualize_overlay(img_path, mask_path, alpha0.4): img np.array(Image.open(img_path).convert(L)) mask np.array(Image.open(mask_path)) # 原图转 RGB掩膜区域涂红 img_rgb np.stack([img]*3, axis-1) overlay img_rgb.copy() overlay[mask 0] [255, 0, 0] blended (img_rgb * (1 - alpha) overlay * alpha).astype(np.uint8) fig, axes plt.subplots(1, 3, figsize(15, 5)) axes[0].imshow(img, cmapgray); axes[0].set_title(原图) axes[1].imshow(mask, cmapgray); axes[1].set_title(掩膜) axes[2].imshow(blended); axes[2].set_title(叠加) for ax in axes: ax.axis(off) plt.tight_layout() plt.savefig(overlay_check.png, dpi150) plt.show() visualize_overlay(./hand_bone_dataset/images/0001.png, ./hand_bone_dataset/masks/0001.png)逻辑说明convert(L)把 X 光图转灰度避免彩色通道干扰mask 0把所有非背景像素涂红alpha控制透明度太低看不清边界太高盖住原图细节0.3~0.5 比较合适。保存成图片而不是只show方便批量抽查。参数上如果掩膜是多类需要按类别给不同颜色不能统一涂红。3.2 批量生成缩略图墙快速定位脏数据单张看效率低批量拼成网格能一眼扫出异常样本。下面这个脚本把前 16 张的叠加图拼成 4x4 网格。def grid_overlay(img_dir, mask_dir, n16, cols4): img_files sorted(os.listdir(img_dir))[:n] rows (n cols - 1) // cols fig, axes plt.subplots(rows, cols, figsize(cols*3, rows*3)) for ax, name in zip(axes.flatten(), img_files): img np.array(Image.open(os.path.join(img_dir, name)).convert(L)) mask_path os.path.join(mask_dir, name) if not os.path.exists(mask_path): ax.set_title(f缺失掩膜: {name}); ax.axis(off); continue mask np.array(Image.open(mask_path)) img_rgb np.stack([img]*3, axis-1) img_rgb[mask 0] [255, 0, 0] ax.imshow(img_rgb); ax.set_title(name, fontsize8); ax.axis(off) plt.tight_layout() plt.savefig(grid_check.png, dpi120) grid_overlay(./hand_bone_dataset/images, ./hand_bone_dataset/masks)逻辑说明axes.flatten()把二维子图数组拉平方便和文件列表zip。遇到缺失掩膜时直接标红提示不中断整个流程。参数上n和cols按你的屏幕和数据集大小调16 张足够发现系统性问题。这一步的血泪经验是别跳过可视化直接开训否则你会在 loss 不降的时候怀疑模型、怀疑学习率、怀疑人生最后发现是掩膜和图像错位。3.3 灰度分布检查X 光的对比度陷阱X 光片常有曝光不均的问题同一批数据里有的偏亮、有的偏暗。如果直接归一化到 [0,1]暗图的骨骼细节会被压掉。建议先统计每张图的均值和方差看看分布是否集中。def gray_stats(img_dir, sample50): files sorted(os.listdir(img_dir))[:sample] means, stds [], [] for name in files: img np.array(Image.open(os.path.join(img_dir, name)).convert(L), dtypenp.float32) means.append(img.mean()); stds.append(img.std()) print(f均值范围: {min(means):.1f} ~ {max(means):.1f}) print(f标准差范围: {min(stds):.1f} ~ {max(stds):.1f}) return means, stds gray_stats(./hand_bone_dataset/images)逻辑说明均值和标准差范围过大说明曝光差异明显需要考虑直方图均衡化或自适应归一化。参数上sample50是抽样数量数据量大时可以只抽一部分。如果均值集中在很窄的区间说明数据一致性较好常规归一化就够。4. 训练前处理从掩膜到标签、增强与划分4.1 掩膜转标签别让类别索引错位分割模型通常要求标签是 0 到 N-1 的连续整数。如果 classes 文件里背景是 0、骨是 1那掩膜可以直接用如果掩膜里骨是 255就需要映射。写一个转换函数把原始掩膜映射成训练用标签。def mask_to_label(mask, mapping): mapping: {原始像素值: 训练标签} label np.zeros_like(mask, dtypenp.uint8) for raw_val, new_val in mapping.items(): label[mask raw_val] new_val return label # 假设原始掩膜里 0 是背景255 是骨 mapping {0: 0, 255: 1} raw_mask np.array(Image.open(./hand_bone_dataset/masks/0001.png)) label mask_to_label(raw_mask, mapping) print(转换后唯一值:, np.unique(label))逻辑说明mapping显式定义原始值到训练标签的对应关系避免隐式假设。np.zeros_like保证输出尺寸和类型一致。参数上如果有多类mapping 要覆盖所有原始像素值漏掉的会被当成背景。这一步的坑在于有些标注工具用 1 表示背景、2 表示前景而你按 0/1 处理结果就是模型把背景学成前景。4.2 数据增强X 光图能做什么、不能做什么医学图像增强和自然图像不一样。水平翻转要谨慎——左右手掌骨镜像后解剖结构变了如果任务不区分左右手翻转可以如果区分翻转会制造错误标签。旋转、缩放、弹性变形相对安全亮度对比度扰动也能提升鲁棒性。import albumentations as A train_transform A.Compose([ A.RandomRotate90(p0.5), A.ShiftScaleRotate(shift_limit0.05, scale_limit0.1, rotate_limit15, p0.7), A.RandomBrightnessContrast(brightness_limit0.2, contrast_limit0.2, p0.5), A.ElasticTransform(alpha1, sigma50, p0.3), ]) # 注意图像和掩膜要同步变换 augmented train_transform(imageimg, masklabel)逻辑说明ShiftScaleRotate的rotate_limit15控制旋转角度太大可能把骨骼转出画面ElasticTransform模拟软组织形变alpha和sigma控制形变强度。参数上p是触发概率训练集可以高一些验证集不要用增强。关键点图像和掩膜必须用同一个 transformalbumentations 的Compose会自动同步但如果你手动写增强很容易只转图像不转掩膜。4.3 数据集划分别按文件名顺序切按文件名顺序切分是常见翻车点。如果数据是按患者或时间采集的前 80% 和后 20% 可能来自不同分布验证集指标会虚高或虚低。常见做法是先打乱索引再切或者按患者 ID 分组切分。import random def split_dataset(img_dir, mask_dir, val_ratio0.2, seed42): files sorted(os.listdir(img_dir)) random.seed(seed) random.shuffle(files) n_val int(len(files) * val_ratio) val_files files[:n_val] train_files files[n_val:] return train_files, val_files train_files, val_files split_dataset(./hand_bone_dataset/images, ./hand_bone_dataset/masks) print(f训练集: {len(train_files)}, 验证集: {len(val_files)})逻辑说明random.seed(seed)保证可复现shuffle打乱后再切避免顺序偏差。参数上val_ratio0.2是常见比例数据量小时可以降到 0.1。如果数据有患者 ID应该按 ID 分组确保同一患者的图不会同时出现在训练和验证集。5. 避坑与排查掌骨分割里最容易翻车的 5 个点5.1 掩膜像素值和 classes 索引对不上现象训练 loss 一直不降或者预测结果全是背景/全是前景。原因掩膜里骨骼像素是 255但代码按 1 处理模型学到的标签全是 0。解决用 2.2 节的check_mask_values先打印唯一像素值再用mask_to_label显式映射不要靠猜。5.2 图像和掩膜尺寸不一致导致错位现象叠加图里掩膜整体偏移或者边缘出现黑边。原因部分掩膜是原图裁剪后标注的尺寸和原图不同resize时又没同步。解决在validate_pairs里检查尺寸不一致的要么重新对齐要么剔除。如果必须 resize图像和掩膜用同一组参数掩膜用最近邻插值别用双线性。5.3 灰度归一化把骨骼细节压没了现象模型对细小骨段分割效果差边缘模糊。原因直接除以 255 做全局归一化暗图的骨骼对比度被压缩。解决先做直方图均衡化或 CLAHE再归一化或者按每张图的均值和标准差做标准化而不是固定除以 255。5.4 数据增强把解剖结构转坏了现象训练集指标很高验证集一塌糊涂。原因用了垂直翻转或大角度旋转掌骨解剖结构被破坏模型学到了错误模式。解决医学图像增强要保守旋转控制在 ±15 度以内翻转只在任务不区分左右手时用。验证集不做增强。5.5 验证集和训练集分布不一致现象验证 loss 比训练 loss 高很多且不收敛。原因按文件名顺序切分训练集和验证集来自不同曝光条件或不同采集设备。解决打乱后再切或者按患者/设备分组切分。切完后用 3.3 节的灰度统计分别看训练集和验证集的均值分布差异大就重新切。6. 进阶技巧用可视化反推标注质量与模型瓶颈6.1 把预测结果和标注叠在一起看比看指标有用训练到一定阶段后mIoU、Dice 这些指标只能告诉你「好不好」不能告诉你「哪里不好」。把模型预测、人工标注、原图三者叠在一起能直接看出模型是漏标了细小骨段还是把软组织误判成了骨。def compare_pred_gt(img_path, gt_path, pred_path): img np.array(Image.open(img_path).convert(L)) gt np.array(Image.open(gt_path)) pred np.array(Image.open(pred_path)) img_rgb np.stack([img]*3, axis-1) # 绿色是标注红色是预测黄色是重合 vis img_rgb.copy() vis[gt 0] [0, 255, 0] vis[pred 0] [255, 0, 0] vis[(gt 0) (pred 0)] [255, 255, 0] plt.figure(figsize(8, 8)) plt.imshow(vis); plt.title(绿标注 红预测 黄重合); plt.axis(off) plt.savefig(compare.png, dpi150); plt.show()逻辑说明先涂绿再涂红重合区域最后涂黄这样一眼能看出漏标只有绿和误判只有红。参数上如果类别多需要扩展成多通道配色。这个技巧我一般用在训练中期挑几张指标最差的样本看比盯着 loss 曲线有效得多。6.2 用混淆矩阵定位类别边界问题二分割任务里混淆矩阵能告诉你模型是把背景判成骨多还是把骨判成背景多。前者说明模型太激进后者说明太保守。根据这个调整pos_weight或损失函数里的类别权重。from sklearn.metrics import confusion_matrix def seg_confusion(gt_masks, pred_masks, n_classes2): gt_flat np.concatenate([m.flatten() for m in gt_masks]) pred_flat np.concatenate([m.flatten() for m in pred_masks]) cm confusion_matrix(gt_flat, pred_flat, labelslist(range(n_classes))) print(混淆矩阵:) print(cm) return cm逻辑说明把所有掩膜拉平后算混淆矩阵labels指定类别顺序避免缺失类别导致矩阵错位。参数上n_classes2对应二分割多类时改大。如果背景判成骨的比例高可以在损失里给背景更高权重或者用 Dice Loss 替代交叉熵。6.3 一个我常犯的错忘了检查掩膜的位深最后说一个很隐蔽的坑。有些掩膜保存成 16 位 PNGnp.array读出来是uint16像素值范围 0~65535而你的代码按uint8处理mask 0虽然能工作但mask 255这种判断会失效。我一般会在读掩膜后加一句print(mask.dtype, mask.max())确认位深和最大值。如果是 16 位但实际只用 0 和 1先转成uint8再处理省得后面映射时出玄学问题。这套流程跑下来从拿到数据集到能开始训练大概半天时间。真正花时间的不是写代码而是验证数据质量——掩膜对不对、类别映射对不对、增强有没有破坏解剖结构。希望帮到你。本文还有配套的精品资源点击获取