简介本资源是面向医学图像分割初学者与算法工程师的高质量2D脾脏分割数据集源自公开3D脾脏分割数据在冠状面与矢状面两个解剖视角下精细抽取并严格筛选剔除前景占比低于0.1%的样本专用于训练和验证轻量级2D分割模型如U-Net、SegNet等适用于课程实验、Kaggle式入门项目及模型baseline构建。资源共1664个文件含1663张PNG格式的原始图像与对应掩膜coronal平面60对、sagittal平面771对以及1个即用型Python可视化脚本——无需修改参数运行即可随机加载样本同步展示原图、真值掩膜及叠加蒙版效果并自动保存结果图显著降低数据探查门槛。压缩包仅23.45MB结构清晰、标注精准、前景区域丰富已获461人学习下载是兼顾专业性与易用性的医学影像入门级实践资源。1. 从3D医学影像里“切片”出高质量2D脾脏分割数据为什么冠状面只有60张、矢状面却有771张你手头有一套3D脾脏CT扫描数据想训练一个轻量级2D分割模型快速验证算法——但直接拿原始体数据喂U-Net显存炸、训练慢、泛化差。这时候有人把3D体积数据沿解剖平面“切”成二维切片并人工筛掉无效区域比如脾脏只占图像0.05%的薄层留下冠状面60张 矢状面771张带精确标注的2D样本。这不是简单截图而是按临床解剖逻辑抽帧冠状面切得少因为脾脏在该方向跨度小矢状面切得多因脾脏前后径长、信息更密集。数据集共34MB开箱即用含完整目录结构images/masks、二值标注0背景1脾脏、以及一个零配置可视化脚本——运行即弹窗显示原图GT叠加蒙版连路径都不用改。适合刚入坑医学图像分割的新手练手也适合老手做baseline对比或数据增强前的数据探查。别被“34MB”骗了这尺寸背后是严格的质量过滤剔除所有前景像素占比0.001的切片确保每张图都有足够脾脏纹理可学。2. 数据结构与加载逻辑如何用PyTorch DataLoader正确读取coronal/sagittal双平面数据2.1 目录结构解析为什么必须区分coronal和sagittal两个子集项目明确将数据分为coronal和sagittal两个独立子集而非混放。这不是为了文件管理方便而是解剖学约束冠状面frontal plane垂直于人体长轴显示左右对称结构矢状面sagittal plane平行于中线呈现前后层次。脾脏在矢状面上的投影面积更大、形态变化更丰富从脾门到脾尖渐变因此切片数量771远超冠状面60。若强行合并加载会破坏数据的空间分布特性导致模型混淆解剖维度。实际工程中我通常为二者分别构建Dataset类再用ConcatDataset组合——这样既能保留平面特异性又支持统一训练。# dataset_loader.py import os from torch.utils.data import Dataset, DataLoader from PIL import Image import numpy as np class Spleen2DSliceDataset(Dataset): def __init__(self, root_dir: str, plane: str coronal, transformNone): :param root_dir: 数据集根目录如 ./spleen_2d/ :param plane: coronal 或 sagittal :param transform: 图像预处理pipeline如ToTensor self.plane plane self.image_dir os.path.join(root_dir, plane, images) self.mask_dir os.path.join(root_dir, plane, masks) # 确保images和masks文件名严格一一对应 self.image_files sorted([f for f in os.listdir(self.image_dir) if f.lower().endswith((.png, .jpg))]) self.mask_files sorted([f for f in os.listdir(self.mask_dir) if f.lower().endswith((.png, .jpg))]) # 校验配对完整性 assert len(self.image_files) len(self.mask_files), \ fPlane {plane}: image/mask count mismatch ({len(self.image_files)} vs {len(self.mask_files)}) for img, mask in zip(self.image_files, self.mask_files): assert img.split(.)[0] mask.split(.)[0], \ fFilename mismatch at {img} / {mask} def __len__(self): return len(self.image_files) def __getitem__(self, idx): img_path os.path.join(self.image_dir, self.image_files[idx]) mask_path os.path.join(self.mask_dir, self.mask_files[idx]) image Image.open(img_path).convert(RGB) # 统一转RGB兼容多数预训练模型 mask Image.open(mask_path).convert(L) # 灰度图像素值0/1 if self.transform: image self.transform(image) mask self.transform(mask) # 注意mask需用相同transform但避免RandomRotation等破坏标签 return image, mask.squeeze(0) # 去掉channel维度返回 [C,H,W], [H,W]提示mask.convert(L)是关键。原始mask虽为二值但可能以P模式调色板存储直接转tensor会出错。convert(L)强制转为灰度确保像素值为0-255整数后续torch.tensor(mask)才能正确映射为0/1。2.2 构建双平面DataLoader按比例采样避免矢状面主导训练矢状面771张 vs 冠状面60张若直接合并模型93%的batch都看到矢状面切片严重偏置。常见做法是分层采样先为每个plane构建独立DataLoader再用WeightedRandomSampler按反比权重分配——让coronal样本被采样概率提升12.85倍771/60≈12.85使两个平面在epoch内出现频次接近。# train_dataloader.py from torch.utils.data import WeightedRandomSampler coronal_ds Spleen2DSliceDataset(./spleen_2d/, planecoronal, transformtrain_transform) sagittal_ds Spleen2DSliceDataset(./spleen_2d/, planesagittal, transformtrain_transform) # 计算采样权重coronal权重 1/60, sagittal权重 1/771 → 归一化后coronal权重≈0.93 weights [1/len(coronal_ds)] * len(coronal_ds) [1/len(sagittal_ds)] * len(sagittal_ds) sampler WeightedRandomSampler(weights, num_sampleslen(weights), replacementTrue) # 合并数据集 combined_ds torch.utils.data.ConcatDataset([coronal_ds, sagittal_ds]) train_loader DataLoader(combined_ds, batch_size16, samplersampler, num_workers4)2.3 可视化脚本深度拆解为什么它能“零配置”运行摘要提到“代码无需更改直接运行”这依赖于脚本对目录结构的强约定。其核心逻辑是自动遍历当前目录下所有*/images/子目录对每个子目录匹配同名*/masks/随机选一张图片读取image/mask用OpenCV叠加蒙版绿色半透明保存三图拼接结果到./viz_output/。# viz_script.py 精简版 import os import random import cv2 import numpy as np from pathlib import Path def find_image_mask_pairs(): 自动发现所有plane子目录下的images/masks配对 pairs [] for plane_dir in Path(.).glob(*/): if not plane_dir.is_dir() or plane_dir.name in [viz_output, __pycache__]: continue images_dir plane_dir / images masks_dir plane_dir / masks if images_dir.exists() and masks_dir.exists(): img_files list(images_dir.glob(*.png)) list(images_dir.glob(*.jpg)) for img_path in img_files: mask_path masks_dir / img_path.name if mask_path.exists(): pairs.append((img_path, mask_path)) return pairs if __name__ __main__: pairs find_image_mask_pairs() if not pairs: raise FileNotFoundError(No image-mask pairs found! Check directory structure.) img_path, mask_path random.choice(pairs) image cv2.imread(str(img_path)) mask cv2.imread(str(mask_path), cv2.IMREAD_GRAYSCALE) # 叠加蒙版mask为1处画绿色alpha0.4 overlay image.copy() overlay[mask 1] [0, 255, 0] # BGR顺序 blended cv2.addWeighted(image, 0.6, overlay, 0.4, 0) # 拼接三图原图 | GT | 叠加图 gt_colored cv2.cvtColor(mask * 255, cv2.COLOR_GRAY2BGR) # 0/1→0/255 viz np.hstack([image, gt_colored, blended]) output_dir Path(viz_output) output_dir.mkdir(exist_okTrue) cv2.imwrite(str(output_dir / fviz_{img_path.stem}.png), viz) print(fVisualization saved to {output_dir / fviz_{img_path.stem}.png})注意此脚本不依赖任何外部配置文件靠目录名隐式识别plane类型。若你重命名了coronal为frontal脚本仍能工作——只要images和masks子目录存在且同级。3. 数据质量验证如何用统计指标确认“标注效果极好”是否属实3.1 前景占比分布分析验证“剔除0.001区域”的实际效果摘要强调“去除了前景区域不足0.001的数据”但需实证。我们遍历所有mask计算每个mask中像素值为1的比例绘制直方图import matplotlib.pyplot as plt from tqdm import tqdm def analyze_foreground_ratio(root_dir: str): ratios [] for plane in [coronal, sagittal]: mask_dir Path(root_dir) / plane / masks for mask_path in tqdm(list(mask_dir.glob(*.png)), descfAnalyzing {plane}): mask cv2.imread(str(mask_path), cv2.IMREAD_GRAYSCALE) ratio np.sum(mask 1) / mask.size ratios.append(ratio) plt.hist(ratios, bins50, alpha0.7, labelAll masks) plt.axvline(0.001, colorr, linestyle--, labelThreshold (0.001)) plt.xlabel(Foreground pixel ratio) plt.ylabel(Count) plt.legend() plt.title(Foreground ratio distribution across all slices) plt.show() print(fMin ratio: {min(ratios):.6f}, Max: {max(ratios):.4f}, Mean: {np.mean(ratios):.4f}) analyze_foreground_ratio(./spleen_2d/)预期结果直方图左边界应紧贴0.001红线无低于该值的点——证明过滤生效。若发现大量0.001~0.01的样本说明脾脏在这些切片中确实很薄但仍有学习价值若峰值集中在0.1~0.3则说明数据富含典型脾脏形态。3.2 标注一致性检查用Dice系数量化GT间相似度单张图标注质量难判断但多张图间若存在系统性偏差如某操作员总把脾脏边缘标粗可通过计算相邻切片mask的Dice相似度来探测。理想情况下同一3D体积中相邻矢状面切片的脾脏形状应高度相似Dice 0.85。def dice_coefficient(mask1, mask2): intersection np.sum(mask1 mask2) union np.sum(mask1) np.sum(mask2) return 2. * intersection / (union 1e-8) # 对sagittal平面计算相邻切片Dice sagittal_masks sorted(Path(./spleen_2d/sagittal/masks).glob(*.png)) dice_scores [] for i in range(len(sagittal_masks)-1): m1 cv2.imread(str(sagittal_masks[i]), cv2.IMREAD_GRAYSCALE) 0 m2 cv2.imread(str(sagittal_masks[i1]), cv2.IMREAD_GRAYSCALE) 0 dice_scores.append(dice_coefficient(m1, m2)) print(fSagittal adjacent Dice: mean{np.mean(dice_scores):.3f}, std{np.std(dice_scores):.3f}) # 合格数据集应输出mean≈0.82~0.92std0.13.3 解剖合理性验证用最小外接矩形Bounding Box检查脾脏位置脾脏位于左上腹其mask的最小外接矩形应满足y坐标行集中在图像上半部y_min height/3x坐标列偏左x_max width*0.6若大量mask的bbox落在右下角可能是标注错误或数据裁剪异常。def check_bbox_anatomy(root_dir: str): invalid_count 0 total 0 for plane in [coronal, sagittal]: mask_dir Path(root_dir) / plane / masks for mask_path in mask_dir.glob(*.png): mask cv2.imread(str(mask_path), cv2.IMREAD_GRAYSCALE) if np.sum(mask) 0: continue # 跳过全黑mask理论上已过滤但防万一 total 1 # 计算bbox coords np.where(mask 0) y_min, y_max coords[0].min(), coords[0].max() x_min, x_max coords[1].min(), coords[1].max() h, w mask.shape # 解剖规则脾脏应在图像上半部且偏左 if y_max h/2 or x_max w*0.7: invalid_count 1 print(fAnatomically suspicious masks: {invalid_count}/{total} ({invalid_count/total*100:.1f}%)) check_bbox_anatomy(./spleen_2d/)血泪经验我在早期项目中曾发现某批数据因DICOM窗宽窗位预处理错误导致脾脏在CT上显示为低密度区标注员误标为“无脾脏”结果bbox全空。这个检查能在训练前揪出此类硬伤。4. 模型训练实战用nnUNet风格预处理适配2D U-Net4.1 输入尺寸归一化为什么固定为512×512而非原始尺寸原始图片尺寸未在摘要中说明但医学图像常为512×512或更高。若保持原尺寸不同切片分辨率不一如coronal切片可能被插值放大影响模型收敛。统一缩放到512×512是平衡显存与细节的黄金选择小于512的图像用cv2.INTER_CUBIC插值放大避免锯齿大于512的用cv2.INTER_AREA缩小保留结构mask必须用cv2.INTER_NEAREST防止0/1像素被插值污染。def resize_to_512(image: np.ndarray, mask: np.ndarray) - tuple: h, w image.shape[:2] if h 512 and w 512: return image, mask # 图像缩放 if h 512 or w 512: image cv2.resize(image, (512, 512), interpolationcv2.INTER_CUBIC) mask cv2.resize(mask, (512, 512), interpolationcv2.INTER_NEAREST) else: image cv2.resize(image, (512, 512), interpolationcv2.INTER_AREA) mask cv2.resize(mask, (512, 512), interpolationcv2.INTER_NEAREST) return image, mask4.2 数据增强策略针对医学图像的保守增强组合医学图像增强需克制——过度旋转/弹性变形会扭曲解剖结构。推荐组合RandomHorizontalFlip(p0.5)脾脏左右不对称但水平翻转在2D切片中可接受RandomAffine(degrees0, translate(0.1, 0.1), scale(0.95, 1.05))微平移微缩放模拟定位误差ColorJitter(brightness0.1, contrast0.1)仅调亮度/对比度不碰饱和度CT无色彩禁用RandomRotation破坏解剖朝向、ElasticTransformCT纹理不适用。from torchvision import transforms train_transform transforms.Compose([ transforms.ToTensor(), transforms.RandomHorizontalFlip(p0.5), transforms.RandomAffine( degrees0, translate(0.1, 0.1), scale(0.95, 1.05), interpolationtransforms.InterpolationMode.BILINEAR ), transforms.ColorJitter(brightness0.1, contrast0.1), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) # ImageNet均值 ])4.3 损失函数选择Dice Loss BCE Loss的加权组合二值分割任务中单纯BCE Loss对前景稀疏场景如脾脏占图10%敏感度低。Dice Loss直接优化交并比但梯度不稳定。加权组合Dice:0.5 BCE:0.5是业界标配import torch import torch.nn as nn import torch.nn.functional as F class DiceBCELoss(nn.Module): def __init__(self, weight_bce0.5): super().__init__() self.weight_bce weight_bce def forward(self, logits, targets): # BCE部分 bce_loss F.binary_cross_entropy_with_logits(logits, targets.float(), reductionmean) # Dice部分 probs torch.sigmoid(logits) intersection (probs * targets).sum() dice (2. * intersection 1e-8) / (probs.sum() targets.sum() 1e-8) dice_loss 1 - dice return self.weight_bce * bce_loss (1 - self.weight_bce) * dice_loss criterion DiceBCELoss(weight_bce0.5)5. 避坑指南训练时踩过的5个真实坑及解决方案5.1 现象训练loss下降但验证Dice不升反降原因模型过拟合coronal平面仅60张在sagittal上泛化差。验证集若随机采样大概率抽到sagittal样本导致Dice骤降。解决验证集必须按plane分层采样确保coronal和sagittal各占50%。用SubsetRandomSampler指定索引而非全局shuffle。5.2 现象mask叠加可视化时出现灰色斑点非纯绿原因原始mask为8位PNG像素值是0/255但cv2.imread默认读为uint8mask 1永远为False因值为255。解决读mask后强制二值化mask (cv2.imread(str(mask_path), 0) 128).astype(np.uint8)。5.3 现象DataLoader报错OSError: broken data stream when reading image file原因部分PNG文件损坏常见于解压不完整或传输中断尤其spleen_18_154.png这类编号连续的文件易成片损坏。解决在Dataset__getitem__中加健壮读取try: image Image.open(img_path).convert(RGB) except Exception as e: print(fCorrupted image {img_path}, skipping...) return self.__getitem__((idx 1) % len(self)) # 递归重试5.4 现象训练初期loss为nan原因DiceBCELoss中分母为0某batch内targets全01e-8补偿不足。解决在loss计算中加batch级保护# Dice计算前 if targets.sum() 0 and probs.sum() 0: dice torch.tensor(1.0) # 完全匹配 elif targets.sum() 0 or probs.sum() 0: dice torch.tensor(0.0) # 完全不匹配 else: intersection (probs * targets).sum() dice (2. * intersection) / (probs.sum() targets.sum())5.5 现象可视化脚本保存的图片全黑原因OpenCV的cv2.imwrite要求输入为uint8但blended数组是float64因addWeighted输出浮点。解决保存前转换cv2.imwrite(..., blended.astype(np.uint8))。6. 进阶技巧用Grad-CAM定位模型“看哪里”验证是否聚焦脾脏解剖区6.1 Grad-CAM实现无需修改模型结构的热力图生成Grad-CAM通过反向传播最后一层卷积的梯度加权求和特征图生成类激活图。对2D U-Net我们取decoder最后输出的特征假设为64通道计算其对脾脏类别logits[:,1]的梯度import torch import torch.nn.functional as F class GradCAM: def __init__(self, model, target_layer): self.model model self.target_layer target_layer self.gradients None self.features None self.target_layer.register_forward_hook(self._save_features) self.target_layer.register_backward_hook(self._save_gradients) def _save_features(self, module, input, output): self.features output def _save_gradients(self, module, grad_in, grad_out): self.gradients grad_out[0] def __call__(self, input_tensor, target_classNone): self.model.eval() input_tensor input_tensor.requires_grad_(True) output self.model(input_tensor) # [1,2,H,W] for binary segmentation if target_class is None: target_class 1 # spleen class # 取脾脏通道的logits全局平均池化作为分类得分 score output[:, target_class].mean() self.model.zero_grad() score.backward() gradients self.gradients features self.features weights torch.mean(gradients, dim(2, 3), keepdimTrue) # [1,C,1,1] cam torch.sum(weights * features, dim1, keepdimTrue) # [1,1,H,W] cam F.relu(cam) cam F.interpolate(cam, sizeinput_tensor.shape[2:], modebilinear) # 上采样到原图 cam cam - cam.min() cam cam / (cam.max() 1e-8) return cam.squeeze().cpu().detach().numpy() # 使用示例 model UNet2D(in_channels3, num_classes2) # 假设已加载权重 grad_cam GradCAM(model, model.decoder[-1]) # 取decoder最后一层 # 加载一张sagittal切片 image, mask sagittal_ds[0] # [3,512,512], [512,512] image_batch image.unsqueeze(0) # [1,3,512,512] cam_map grad_cam(image_batch) # [512,512] plt.imshow(image.permute(1,2,0).cpu().numpy()) plt.imshow(cam_map, cmapjet, alpha0.4) plt.title(Grad-CAM: Model attention on spleen) plt.show()6.2 解剖可信度评估表用热力图验证模型是否“懂医学”评估维度合格标准不合格表现检查方法空间定位热力图峰值在脾脏解剖区左肋缘下峰值在胃/胰腺/左肾区域叠加CT解剖图比对形态覆盖热力图覆盖脾脏整体轮廓非局部碎片热力图呈多个孤立斑点未连通计算热力图连通域数量应≤3边缘锐度热力图边界与mask边缘基本重合热力图明显溢出mask边界假阳性关注计算热力图与mask的IoU应0.6平面特异性coronal切片热力图呈椭圆横向分布sagittal呈纵向条带两平面热力图形态无差异分别统计主轴方向PCA6.3 一个硬核习惯每次训练前必跑的3行验证脚本我从不直接开训。在train.py最开头插入这三行它会在加载数据后立即打印关键统计5秒内告诉你数据有没有硬伤# 在DataLoader创建后立即执行 sample_batch next(iter(train_loader)) print(fBatch shape: {sample_batch[0].shape}) # 应为 [B,3,512,512] print(fMask unique values: {torch.unique(sample_batch[1])}) # 应为 tensor([0, 1]) print(fForeground ratio in batch: {sample_batch[1].float().mean():.4f}) # 应在0.05~0.3之间如果unique values出现2或-1说明mask有非法值如果foreground ratio恒为0说明路径错了或mask全黑。这三行省去80%的debug时间——毕竟模型不会说谎但数据会撒谎。从那以后我每次新建实验都强制走一遍这个检查哪怕只是临时调试。希望帮到你。本文还有配套的精品资源点击获取