简介:本资源是面向医学图像分割初学者与算法工程师的轻量级2D脾脏分割数据集,源自公开3D脾脏数据,在冠状面与矢状面精准切片生成,专用于二分类(背景/脾脏)模型训练与验证。数据集共1664个文件,含1663张PNG格式图像(60张冠状面+771张矢状面原始图及对应mask)和1个Python可视化脚本,总大小23.45MB;其中images与masks目录结构清晰,便于直接接入PyTorch或TensorFlow数据加载流程。已有462人学习下载,说明其在入门实践与baseline实验中具备良好接受度。随包附带的可视化脚本开箱即用,可自动随机抽取样本,同步展示原始图像、真值掩膜及叠加蒙板效果,显著降低数据理解门槛;所有切片均经前景区域过滤(剔除面积<0.001的低信息样本),标注质量高、脾脏轮廓完整、解剖结构丰富,适合数据预处理教学、U-Net等模型快速验证及跨平面分割泛化性研究。
1. 这不是“随便切几刀”的2D数据集:从3D脾脏CT重建中精准剥离冠状/矢状面切片,专为医学图像分割模型验证而生
你手头那套UNet或TransUNet在LiverSeg上跑得飞起,一换到脾脏就dice掉0.15?不是模型不行,很可能是训练数据在“空间语义”上根本没对齐——3D器官的连续性被粗暴打散成孤立2D切片,而切片方向(冠状/矢状)又没被显式建模。这个数据集就是冲着这个痛点来的:它不直接提供原始3D体数据,而是从已标注的3D脾脏分割体中,沿解剖学标准平面(coronal/sagittal)逐层抽取、严格过滤、人工复核后的2D切片对。60张冠状面+771张矢状面,共831对image-mask,全部满足“前景像素占比≥0.001”硬约束,mask边缘锐利、无模糊标注、无半像素偏移。它不是拿来即用的“玩具数据”,而是专为验证模型在不同解剖视角下的泛化鲁棒性设计的基准子集——比如测试你的模型能否在矢状面(长轴视图)识别出被肋骨遮挡的脾尾部,或在冠状面(横断视图)区分紧邻的胃底与脾上极。如果你正在做医学图像分割的消融实验、跨视角迁移研究,或者需要一份轻量但解剖学可信的验证集,这个34MB的数据包比下载整个LiTS或MSD更高效、更聚焦。
2. 数据结构解析与加载:为什么必须区分coronal/sagittal目录,以及如何避免通道错位
2.1 目录结构与文件命名逻辑:解剖学方向即数据契约
数据包解压后呈现严格分层结构:
spleen_2d_dataset/ ├── coronal_plane/ │ ├── images/ # 60张.png,命名如 spleen_18_153.png │ └── masks/ # 60张.png,同名,如 spleen_18_153.png └── sagittal_plane/ ├── images/ # 771张.png └── masks/ # 771张.png关键点在于:spleen_18_153.png中的18指代原始3D病例编号(Case ID),153是该病例在对应平面中的切片序号。这意味着同一Case ID在coronal和sagittal目录下可能有不同数量的切片(因器官在不同方向投影面积差异大),且绝对不可跨目录混用——把coronal的image和sagittal的mask配对,会导致解剖结构完全错位。我曾见过有人用glob通配符批量读取,结果训练时loss震荡剧烈,查了三天才发现是路径拼接时漏了coronal_plane/前缀。
2.2 图像与mask加载实操:PIL vs OpenCV的通道陷阱
直接用cv2.imread()读取mask会触发经典翻车:
import cv2 mask_cv2 = cv2.imread("coronal_plane/masks/spleen_18_153.png", cv2.IMREAD_GRAYSCALE) # ✅ 正确 # mask_cv2.shape → (512, 512) mask_pil = cv2.imread("coronal_plane/masks/spleen_18_153.png") # ❌ 错误!默认BGR三通道 # mask_pil.shape → (512, 512, 3),后续转tensor会报维度不匹配更隐蔽的坑是PIL的mode:
from PIL import Image mask_pil = Image.open("coronal_plane/masks/spleen_18_153.png") print(mask_pil.mode) # 输出 'L'(灰度),但若用convert('RGB')再转numpy,值域会从[0,1]变成[0,255] # 正确做法: mask_np = np.array(mask_pil) # 直接转,保持0/1二值 assert np.unique(mask_np).tolist() == [0, 1], "mask must be binary"提示:所有mask均为单通道PNG,像素值严格为0(背景)或1(脾脏),无需归一化到[0,255]。若用torchvision.transforms.ToTensor(),它会自动将uint8转为float32并除以255,此时需手动
mask = (mask * 255).long()还原二值标签。
2.3 数据集类封装:按解剖方向分组采样
为支持对比实验(如训练用sagittal,验证用coronal),需在Dataset中显式保留方向信息:
import os from torch.utils.data import Dataset class Spleen2DDataset(Dataset): def __init__(self, root_dir, plane='coronal', transform=None): self.plane = plane # 'coronal' or 'sagittal' self.img_dir = os.path.join(root_dir, plane + '_plane', 'images') self.mask_dir = os.path.join(root_dir, plane + '_plane', 'masks') self.filenames = [f for f in os.listdir(self.img_dir) if f.endswith('.png')] self.transform = transform def __len__(self): return len(self.filenames) def __getitem__(self, idx): fname = self.filenames[idx] img_path = os.path.join(self.img_dir, fname) mask_path = os.path.join(self.mask_dir, fname) # 同名mask image = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) # 确保单通道 mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) if self.transform: # 注意:Albumentations要求mask为uint8,且transform需指定mask参数 augmented = self.transform(image=image, mask=mask) image, mask = augmented['image'], augmented['mask'] return image, mask, self.plane # 返回方向标签,用于后续分组统计此设计让train_loader = DataLoader(Spleen2DDataset('data/', 'sagittal'))和val_loader = DataLoader(Spleen2DDataset('data/', 'coronal'))天然隔离,避免数据泄露。
3. 可视化脚本深度拆解:不只是“随机看一张”,而是验证标注质量的诊断工具
3.1 脚本核心逻辑:三图同屏的医学合理性校验
提供的visualize.py本质是一个标注质量快速筛查器。它不只展示GT,更通过蒙版叠加揭示临床细节:
import numpy as np import matplotlib.pyplot as plt import random import os from PIL import Image def visualize_sample(data_root, plane='coronal'): # 随机选一个case img_dir = os.path.join(data_root, f"{plane}_plane", "images") mask_dir = os.path.join(data_root, f"{plane}_plane", "masks") files = [f for f in os.listdir(img_dir) if f.endswith('.png')] sample_file = random.choice(files) # 加载 img = np.array(Image.open(os.path.join(img_dir, sample_file))) mask = np.array(Image.open(os.path.join(mask_dir, sample_file))) # 创建叠加图:原图+半透明红色mask(脾脏区域高亮) overlay = np.stack([img, img, img], axis=-1) # RGB副本 overlay[mask == 1, 0] = 255 # R通道设为255(红色) overlay[mask == 1, 1] = 0 # G通道0 overlay[mask == 1, 2] = 0 # B通道0 overlay = np.clip(overlay, 0, 255).astype(np.uint8) # 三图并排 fig, axes = plt.subplots(1, 3, figsize=(15, 5)) axes[0].imshow(img, cmap='gray') axes[0].set_title(f'Original: {sample_file}') axes[1].imshow(mask, cmap='gray') axes[1].set_title('Ground Truth Mask') axes[2].imshow(overlay) axes[2].set_title('Overlay (Red=Spleen)') plt.tight_layout() plt.savefig(f"visualize_{plane}_{sample_file}", dpi=300, bbox_inches='tight') plt.show() if __name__ == "__main__": visualize_sample("spleen_2d_dataset", plane="sagittal")关键参数说明:
overlay[mask == 1, 0] = 255将脾脏区域染成纯红,不是为了美观,而是为了肉眼判断mask是否覆盖了脾脏所有解剖亚区(如脾门、脾切迹)。若发现红色区域在CT上明显存在低密度灶(囊肿)却被标为1,或脾周脂肪间隙被误标,说明标注需复核。
3.2 三图诊断法:用可视化反向验证数据可靠性
运行脚本后,重点观察三图关系:
| 图像类型 | 诊断要点 | 典型问题 |
|---|---|---|
| Original CT | 脾脏边界是否清晰?有无运动伪影? | 若图像模糊,mask边缘却锐利→标注者可能过度平滑 |
| Ground Truth Mask | 是否存在孤立噪点?边缘是否呈阶梯状(锯齿)? | 孤立噪点→标注时误点;阶梯边缘→未用抗锯齿绘制 |
| Overlay | 红色区域是否完全包裹脾实质?是否侵入邻近器官? | 红色溢出至胃/胰→标注过界;红色遗漏脾下极→标注欠完整 |
我习惯每次新加载数据集必跑10次visualize.py,尤其关注sagittal_plane中Case ID=18的切片(因摘要提到spleen_18_153.png等),这些是原始3D数据中脾脏形态最典型的样本,能快速建立对标注风格的信任感。
4. 训练适配指南:如何把这831张图喂给主流分割模型而不崩
4.1 输入尺寸与归一化:CT值范围决定预处理策略
该数据集图像为uint8格式,但原始CT值已被窗宽窗位(WW/WL)映射为视觉友好的灰度。查看典型图像直方图:
img = cv2.imread("coronal_plane/images/spleen_18_153.png", cv2.IMREAD_GRAYSCALE) print(f"Min: {img.min()}, Max: {img.max()}, Mean: {img.mean():.1f}") # 输出示例:Min: 0, Max: 255, Mean: 112.3因此无需复杂CT窗宽调整,直接按常规医学图像处理:
- 归一化:
image = (image.astype(np.float32) - 128.0) / 128.0(中心化到[-1,1]) - 尺寸:原始512×512,UNet建议resize到512×512(无crop,保全解剖结构);若用Swin-Unet等ViT架构,可center-crop到384×384减少显存。
注意:
mask保持uint8二值,不要归一化!否则损失函数(如DiceLoss)计算会失效。
4.2 损失函数选择:为什么Dice Loss比BCE更适合此数据集
脾脏在CT中与周围组织(肌肉、脂肪)对比度中等,且mask前景占比小(平均约5%-15%),导致BCE Loss易受背景主导:
# 错误示范:BCE with logits,未加权 criterion = nn.BCEWithLogitsLoss() # 在脾脏区域稀疏时,梯度几乎全来自背景 # 正确方案:Dice Loss + Focal Loss组合 from monai.losses import DiceLoss, FocalLoss dice_loss = DiceLoss(include_background=False, to_onehot_y=True, softmax=True) focal_loss = FocalLoss(to_onehot_y=True, gamma=2.0) total_loss = 0.7 * dice_loss(pred, mask) + 0.3 * focal_loss(pred, mask)include_background=False强制Dice只计算脾脏(label=1)区域,避免背景主导;gamma=2.0的Focal Loss抑制易分类背景像素的梯度,聚焦难例(如脾边缘模糊区)。
4.3 数据增强策略:解剖学约束下的安全增强
医学图像增强必须尊重解剖不变性:
| 增强类型 | 推荐参数 | 禁用原因 |
|---|---|---|
| RandomRotation | degrees=(-5, 5) | 脾脏在冠状/矢状面有固定朝向,旋转>10°会破坏解剖合理性 |
| ElasticTransform | alpha=25, sigma=3 | 模拟轻微形变,但sigma>5会导致脾轮廓扭曲 |
| GridDistortion | num_steps=2, distort_limit=0.05 | 轻微扰动,避免网格畸变破坏器官边界 |
| 禁用 | RandomBrightnessContrast,HueSaturationValue | CT图像是线性衰减系数,非RGB色彩空间,调色会引入伪影 |
使用Albumentations时,务必设置p=0.5并确保mask同步变换:
train_transform = A.Compose([ A.RandomRotation(degrees=(-5, 5), p=0.5), A.ElasticTransform(alpha=25, sigma=3, p=0.3), A.GridDistortion(num_steps=2, distort_limit=0.05, p=0.3), A.Normalize(mean=[0.0], std=[1.0]), # 归一化放最后 ], additional_targets={'mask': 'mask'})5. 避坑指南:831张图里藏着的5个血泪经验
5.1 现象:训练初期Dice系数卡在0.3-0.4,loss下降缓慢
原因:mask被错误读取为三通道(如用cv2.imread(path)未加cv2.IMREAD_GRAYSCALE),导致模型输入mask shape为(512,512,3),而网络期望(512,512)。PyTorch在计算loss时会静默广播,但梯度回传失效。
解决:在Dataset的__getitem__中加入断言:
assert len(mask.shape) == 2, f"Mask {fname} has {len(mask.shape)} dims, expected 2" assert np.unique(mask).tolist() in [[0,1], [0]], f"Mask {fname} contains invalid values {np.unique(mask)}"5.2 现象:验证时某几张图Dice骤降,但训练loss平稳
原因:sagittal_plane中部分切片(如spleen_12_001.png)脾脏仅占图像左下角极小区域(<0.5%),模型在全局池化时丢失空间信息。
解决:对sagittal数据启用焦点采样(Focal Sampling):
# 在DataLoader前,统计每张mask的前景占比 foreground_ratio = [] for f in sagittal_files: m = cv2.imread(f"sagittal_plane/masks/{f}", cv2.IMREAD_GRAYSCALE) ratio = m.sum() / (m.shape[0] * m.shape[1]) foreground_ratio.append(ratio) # 构建加权sampler,使低ratio样本采样概率提升3倍 weights = [1.0 if r > 0.01 else 3.0 for r in foreground_ratio] sampler = WeightedRandomSampler(weights, num_samples=len(weights), replacement=True)5.3 现象:可视化overlay图中红色区域“漂移”,不贴合脾脏实际位置
原因:cv2.imread()读取PNG时,若图像含alpha通道,cv2.IMREAD_GRAYSCALE会错误解释为灰度,导致mask错位。
解决:统一用PIL读取并强制转换:
mask = np.array(Image.open(mask_path).convert('L')) # 'L'确保灰度 mask = (mask > 128).astype(np.uint8) # 二值化,防PNG保存时的灰度抖动5.4 现象:跨plane训练时,coronal模型在sagittal验证集上Dice暴跌20%
原因:未在训练时注入“plane”作为条件特征。冠状面切片显示脾脏横截面(圆形),矢状面显示长轴(椭圆形),纹理分布差异大。
解决:修改网络输入,将plane编码为one-hot并拼接到encoder输出:
plane_emb = torch.zeros(batch_size, 2) # [coronal, sagittal] plane_emb[:, 0] = 1 if plane == 'coronal' else 0 plane_emb[:, 1] = 1 if plane == 'sagittal' else 0 x = torch.cat([encoder_features, plane_emb.unsqueeze(-1).unsqueeze(-1)], dim=1) # 拼接至channel维度5.5 现象:visualize.py保存的图片名称含中文路径报错
原因:脚本中plt.savefig()在Windows系统下对中文路径支持不佳。
解决:修改保存路径为绝对英文路径,并添加异常捕获:
save_path = os.path.join(os.getcwd(), "vis_results", f"vis_{plane}_{sample_file}") os.makedirs(os.path.dirname(save_path), exist_ok=True) try: plt.savefig(save_path, dpi=300, bbox_inches='tight') except Exception as e: print(f"Save failed: {e}, fallback to PNG without path") plt.savefig(f"fallback_vis_{plane}.png", dpi=300)6. 进阶技巧:用coronal/sagittal数据构建“解剖一致性”验证协议
6.1 为什么需要解剖一致性验证?
3D器官的2D切片不是独立样本——同一病例的coronal和sagittal切片在空间上必然存在几何约束。例如:脾脏在coronal面的上下径(cranio-caudal)应与sagittal面的前后径(anterior-posterior)呈正相关。若模型在两个平面预测结果违背此约束,说明其未学到真实解剖先验。
6.2 实施步骤:三步构建一致性指标
Step 1:提取每个切片的脾脏质心坐标
def get_centroid(mask): y_coords, x_coords = np.where(mask == 1) if len(y_coords) == 0: return (0, 0) return (int(np.mean(y_coords)), int(np.mean(x_coords))) # 对coronal_plane所有切片计算质心y坐标(代表上下位置) coronal_ys = [] for f in os.listdir("coronal_plane/masks"): mask = cv2.imread(f"coronal_plane/masks/{f}", cv2.IMREAD_GRAYSCALE) cy, cx = get_centroid(mask) coronal_ys.append(cy) # 对sagittal_plane所有切片计算质心x坐标(代表前后位置) sagittal_xs = [] for f in os.listdir("sagittal_plane/masks"): mask = cv2.imread(f"sagittal_plane/masks/{f}", cv2.IMREAD_GRAYSCALE) cy, cx = get_centroid(mask) sagittal_xs.append(cx)Step 2:拟合解剖相关性曲线
# 假设Case ID=18在coronal有10张切片,在sagittal有85张,取重叠Case的质心序列 # 用多项式拟合:coronal_y = a * sagittal_x^2 + b * sagittal_x + c from numpy.polynomial import Polynomial coefs = Polynomial.fit(sagittal_xs, coronal_ys, deg=2) # 得到拟合曲线coefficientsStep 3:定义一致性损失项
在验证阶段,对每个batch计算预测mask的质心,代入拟合曲线,计算残差:
def anatomical_consistency_loss(pred_mask, plane, coefs): cy_pred, cx_pred = get_centroid(pred_mask) if plane == 'coronal': # pred_cy 应接近 coefs(sagittal_x) —— 但此处需跨plane关联,故需存储case_id return 0 # 实际需在dataloader中返回case_id,此处简化 else: # sagittal return torch.abs(torch.tensor(cy_pred) - torch.tensor(coefs(cx_pred))) # 最终验证指标 = Dice + 0.1 * consistency_loss6.3 我的落地习惯:每次提交论文前必跑的3个检查
- 方向隔离检查:确认
coronal和sagittal的DataLoader绝对不共享sampler seed,避免隐式数据泄露; - mask完整性检查:用
np.count_nonzero(mask)统计所有mask前景像素,剔除<50像素的切片(防标注噪声); - 可视化存档:运行
visualize.py生成20张图,按plane_caseID_sliceNum命名,存入/figures/,作为审稿人质疑标注质量时的“后悔药”。
从那以后我每次构建医学图像数据集,都强制走一遍coronal/sagittal质心相关性分析——不是为了发论文,而是确保模型学到的不是数据集的偶然噪声,而是人体真实的解剖规律。希望帮到你。
本文还有配套的精品资源,点击获取