
简介面向心脏CT图像分割任务的数据集配套资源适合医学影像分析、深度学习分割方向的研究者或学习者使用。图像统一为512×512分辨率PNG格式mask标签为两类划分0为背景、255为心脏并附有classes txt说明类别信息。数据集按训练/测试划分其中训练集包含667张原始图像及对应mask测试集285组两套合计1904个图像文件加上标签说明与可视化脚本资源包共1907个文件整体压缩为7z格式大小约111MB。除数据外还提供一个无需修改即可运行的可视化Python脚本可随机抽取样本并展示原始图像、GT以及GT在原图上的叠加效果便于快速核对标签与观察分割目标。资源累计已有260人学习适合作为心脏CT分割模型训练、验证以及入门实践的基准数据。1. 一个 512×512 心脏 CT 分割数据集的真实价值这套数据给我的第一印象是“干净”训练集 667 张、测试集 285 张全部是 512×512 的 PNG掩膜是单通道只区分背景和心脏。相比多器官、多类别数据集它把问题压缩到 2 类分割加上自带可视化脚本拿到手不需要在大模型上反复试错就能验证分割网络在灰度医学图像上的基本表现。适合两类人一是做医学图像分割方向需要一份小规模、可快速跑基准的数据来调试 U-Net 系列模型二是对医学图像标注和标签文件不熟想通过真实 CT 数据理解 Mask 如何存储、如何与原始图配对使用。这份数据补上了一个很容易被忽视的环节标注文件不是直接用类别 ID而是用 0 和 255 两个灰度值表达处理不当会在加载标签时直接造成训练崩溃。下面从目录结构开始拆解。2. 数据集目录与标签协议先看清 images 和 masks 的对应关系心脏 CT 分割最常见的问题是拿到手就开始训练结果模型反复震荡最后发现是 mask 的标签值没有归一化。所以在动手写 DataLoader 前先花十分钟把目录和像素标签读清楚。2.1 训练集、测试集的目录组织方式压缩包解压后的结构基本是下面这种形式heart_ct_seg/ |-- train/ | |-- images/ | | |-- 2286.png | | |-- 2290.png | | -- ... | -- masks/ | |-- 2286.png | |-- 2290.png | -- ... |-- test/ | |-- images/ | -- masks/ -- classes.txt这里的配对规则是images和masks下的同名 PNG 一一对应比如train/images/2290.png对应train/masks/2290.png。训练集 667 对测试集 285 对图上区域即为心脏轮廓。一个小细节是文件名不是肉眼可见顺序比如2290后面直接是2344所以遍历时不要用序号拼接路径直接用glob或os.listdir更稳妥。classes.txt一般记录标签语义内容通常类似0 background、255 foreground。它只起说明作用不代表训练时要直接用这两个数字做分类索引。把 mask 读进来后会看到像素取值只有这两个这在下一节重点解释。2.2 0 和 255 的 mask 取值如何转换为训练标签用 Python 直接读一张 mask打印统计信息from PIL import Image import numpy as np mask np.array(Image.open(train/masks/2290.png).convert(L)) print(mask.shape, mask.dtype) print(unique values:, np.unique(mask)) print(background ratio:, (mask 0).mean().round(4)) print(foreground ratio:, (mask 255).mean().round(4))输出大概率是(512, 512) uint8 unique values: [ 0 255] background ratio: 0.831 foreground ratio: 0.169逻辑说明CT 原始图像是 16 位 DICOM转成 PNG 后通常保留 8 位灰度也就是 0 到 255 的范围。mask 里把心脏区域标记为 255非心脏标记为 0这是图像标注工具存储二值掩膜的常见方式。但在 PyTorch 或 TensorFlow 的交叉熵损失里类别索引必须从 0 开始连续排列如果直接把 255 当作标签会产生越界或梯度异常。所以训练前必须把 mask 除以 255或者用(mask 0).astype(np.float32)把前景转成 1。这里还有一个容易踩的坑Image.open(...).convert(L)会把彩色 PNG 转成灰度但如果某些 mask 保存为带透明通道的 PNGconvert(L)会丢弃 alpha导致透明区域变成背景。稳妥的做法是统一用modeL强制通道一致并在遍历时检查所有 mask 的mode。2.3 配对文件名和尺寸一致性的快速校验当图像和掩膜来自不同标注轮次时可能出现文件名相同但尺寸不一致或者同名缺失。这类问题会在torch.stack时报形状不匹配定位起来很费时间。可以在开头跑一次校验from pathlib import Path from PIL import Image train_img_dir Path(train/images) train_mask_dir Path(train/masks) def inspect_pair(img_path, mask_path): img Image.open(img_path) mask Image.open(mask_path) return img.size, img.mode, mask.size, mask.mode checks [inspect_pair(p, train_mask_dir / p.name) for p in train_img_dir.glob(*.png)] sizes set(c[0] for c in checks) modes set((c[1], c[3]) for c in checks) print(unique sizes:, sizes) print(unique modes:, modes) assert all(p.with_suffix(.png).name in [m.name for m in train_mask_dir.iterdir()] for p in train_img_dir.glob(*.png))预期结果是unique sizes只有(512, 512)unique modes是(L, L)或(RGB, L)。如果出现(RGB, RGB)说明原图可能被误存成彩色三通道后续预处理要统一再转一次灰度。这一遍校验建议直接固化到训练脚本的前置函数里不要每次打开 Jupyter 手工看。把它写成validate_dataset()在训练入口调用一次既避免后期定位数据问题也能确保审计过程可复现。3. 可视化脚本的二次开发从单图对照到批量质量检查原包里附带的可视化脚本可以在不修改参数的情况下随机抽取一张图直接生成原图、GT、GT 在原图上蒙板的图像。这适合快速看一看标注效果但真正要用来排查训练集问题还需要在这个脚本基础上补充几个能力。3.1 三栏拼图脚本标准实现先给出一个可直接替换原脚本的最小版本import matplotlib.pyplot as plt import numpy as np from PIL import Image img_path train/images/2290.png mask_path train/masks/2290.png img np.array(Image.open(img_path).convert(L)) mask np.array(Image.open(mask_path).convert(L)) overlay np.stack([img] * 3, axis-1).astype(np.uint8) overlay[mask 0] (255, 0, 0) fig, axes plt.subplots(1, 3, figsize(15, 5)) axes[0].imshow(img, cmapgray) axes[0].set_title(source CT) axes[1].imshow(mask, cmapgray) axes[1].set_title(GT mask) axes[2].imshow(overlay) axes[2].set_title(overlay) plt.tight_layout() plt.savefig(vis_2290.png, dpi150)逻辑说明先读取灰度图将单通道堆叠成 RGB这样后续叠加红色时不会丢失底色。overlay[mask 0] (255, 0, 0)表示只把 mask 非零区域设成红色半透明效果在这里没有所以适合快速看轮廓是否准确对齐。参数说明cmapgray控制灰度显示不设置时 matplotlib 会默认用viridis伪彩色容易把 CT 看成热力图影响判断。dpi150是为了保存时保留更多细节直接plt.show()在 Jupyter 中没问题但保存下来的图片会更模糊。3.2 半透明叠加与前景占比诊断上面那种纯色覆盖会盖住组织细节不方便观察心脏边缘是否贴合血管。我一般的做法是再加一层 alpha 混合def overlay_semi(img, mask, alpha0.45): rgba np.zeros((img.shape[0], img.shape[1], 4), dtypenp.uint8) rgba[..., :3] np.stack([img] * 3, axis-1) rgba[..., 3] 255 rgba[mask 0] (255, 0, 0, int(alpha * 255)) return rgba这里把红色通道的 alpha 设置成 0.45相当于半透明蒙板源图像的心肌纹理还能透出来。这样做的好处是可以肉眼检查如果心脏区域边缘与图像高亮区域出现错位半透明叠加时看到的不是平滑过渡而是红色边界从组织边缘“漂”出去。更客观的诊断指标是统计每张图的前景占比fg_ratio (mask 0).mean() print(ffg_ratio{fg_ratio:.4f})如果某张图的前景比例明显偏离平均值比如超过 0.5可能 mask 把整片区域都框进去了如果接近 0可能漏标了。建议全量统计所有训练样本的前景占比把异常样本挑出来单独看可视化图。3.3 全量遍历生成拼接大图排查质量问题最快的方式是生成一张包含多组三栏图的大图from glob import glob img_files sorted(glob(train/images/*.png))[:16] fig, axes plt.subplots(len(img_files), 3, figsize(9, len(img_files) * 3)) for row, img_path in enumerate(img_files): mask_path ftrain/masks/{Path(img_path).stem}.png img np.array(Image.open(img_path).convert(L)) mask np.array(Image.open(mask_path).convert(L)) axes[row, 0].imshow(img, cmapgray) axes[row, 1].imshow(mask, cmapgray) axes[row, 2].imshow(overlay_semi(img, mask)) plt.tight_layout() plt.savefig(train_16_slices.png)一次看 16 张的拼接图能快速发现“标签对错位”“同一器官但 mask 只覆盖一半”这两类问题。我见过最大的坑是数据集提供方在拼接 mask 时启用了PIL.Image.paste的mask参数导致部分 mask 边缘自带 1 像素偏移。这种亚像素错位单张看不出来但拼接大图里会表现为所有心脏边界都同一方向偏移。4. 在这个数据集上跑一个 U-Net 分割基线目录结构和可视化都确认无误后下一步就是把数据送进网络。两类的 512×512 灰度图用 U-Net 结构是最稳妥的选择。这里的核心不是模型层数而是输入输出通道、损失函数和评价指标怎么匹配标签格式。4.1 DataLoader 中的预处理管线把原始 CT 和 mask 分别做归一化但归一化方式不一样。图像要除以 255 映射到 0-1 区间mask 要转成 0/1 二值浮点数。PyTorch 的 DataLoader 可以这样写import torch from torch.utils.data import Dataset from PIL import Image import numpy as np class HeartCTDataset(Dataset): def __init__(self, img_dir, mask_dir): self.img_files sorted(Path(img_dir).glob(*.png)) self.mask_dir Path(mask_dir) def __len__(self): return len(self.img_files) def __getitem__(self, idx): img_file self.img_files[idx] mask_file self.mask_dir / img_file.name img Image.open(img_file).convert(L) mask Image.open(mask_file).convert(L) x np.array(img, dtypenp.float32) / 255.0 y (np.array(mask, dtypenp.float32) 0).astype(np.float32) x torch.from_numpy(x).unsqueeze(0) y torch.from_numpy(y).unsqueeze(0) return x, y逻辑说明np.array(img, dtypenp.float32) / 255.0把灰度值变成 0-1 的 float避免后续nn.Conv2d对过大数值敏感。mask 0这一步把 255 变成 True再转 float 后就是 1.0保证了标签只有 0 和 1。最后unsqueeze(0)把(512,512)变成(1,512,512)符合 PyTorch 的通道维度要求。参数说明这里没有做随机裁剪因为数据本身的背景比例约 83%裁剪后容易丢失边缘上下文。如果显存紧张可以先 pad 到 512×512然后再选择性 center-crop。注意不能直接resize到 256×256会让小血管边界信息完全丢失影响分割精度。4.2 损失函数选型BCE 与 Dice Loss 的配合二分类分割通常选择BCEWithLogitsLoss因为模型输出不需要提前接 sigmoid数值更稳定。但只靠 BCE 会在这种背景占 83% 的数据上产生严重的类别不平衡网络很快学会全预测为背景得到很高的像素准确率但 IoU 很低。我常用的做法是加一个 Dice Loss 作为辅助import torch import torch.nn.functional as F def dice_loss(y_pred, y_true, eps1e-7): pred torch.sigmoid(y_pred) inter (pred * y_true).sum(dim(2, 3)) union pred.sum(dim(2, 3)) y_true.sum(dim(2, 3)) return 1.0 - (2.0 * inter eps) / (union eps)y_pred是模型原始 logitsy_true是 0/1 是 mask。先sigmoid把 logits 变成概率再计算重合度。eps防止分子分母都是 0 的情况。在训练循环里这样组合pred model(x) loss_bce F.binary_cross_entropy_with_logits(pred, y) loss_dice dice_loss(pred, y) loss loss_bce loss_dice这样做的逻辑在于BCE 在逐像素上做惩罚对边界像素更敏感Dice Loss 直接关注前景区域的覆盖度能缓解背景占比过高的梯度主导。实际调参时若发现预测结果偏保守可以把 Dice Loss 权重提高比如loss binary_cross_entropy 1.5 * dice_loss。4.3 训练参数与日志指标对 512×512 的输入batch size 不能设太大建议从 4 开始参数项建议值说明input size512×512原始图尺寸不 resizebatch size4-8单卡 24G 显存选 816G 选 4optimizerAdamlr1e-4权重衰减 1e-5epochs50-1002 类分割50 epoch 基本收敛lossBCE Dice权重 1:1 起调eval metricsDice / IoU按(mask 0)计算训练循环中按下面形式记录指标而不是只记录 losswith torch.no_grad(): pred torch.sigmoid(model(x)) 0.5 inter (pred y.bool()).sum().item() union (pred | y.bool()).sum().item() iou inter / (union 1e-7)训练完成后对测试集 285 张图逐个求 IoU统计平均值和中位数。这个中位数很有参考价值如果平均值高但中位数偏低说明模型在多数样本上表现一般只是靠少数简单样本拉高了均值此时要看测试集里是否有大量心电图旁结构干扰。5. 把可视化脚本改造成预测输出与后处理验证工具训练好模型后原来的可视化脚本只适合看 GT无法直接看到模型预测结果。我会把脚本扩展成预测可视化器再加上一个经典的后处理模块用于修正分割输出中细碎斑点和孔洞。5.1 批量预测测试集并保存叠加图from pathlib import Path import numpy as np import torch def preprocess_for_pred(path): img Image.open(path).convert(L) x np.array(img, dtypenp.float32) / 255.0 x torch.from_numpy(x).unsqueeze(0).unsqueeze(0) return x with torch.no_grad(): for img_path in sorted(Path(test/images).glob(*.png)): x preprocess_for_pred(img_path) pred torch.sigmoid(model(x))[0, 0].numpy() 0.5 overlay overlay_semi(img, pred) Image.fromarray(overlay).save(fpred_vis/{img_path.stem}.png)torch.sigmoid是必需的步骤因为模型输出最后一层没有激活函数如果之前训练时已经用 softmax 输出单通道这里要改成pred model(x)[0, 0].numpy()两种方式对应不同训练代码不能混用。5.2 后处理去除小连通域和填充孔洞模型输出的二值图上经常出现两类瑕疵一是背景区域中被误分成心脏的小颗粒二是心脏中间出现小孔。对这些 512×512 的 CT 切片心脏在解剖上是一个连续区域所以后处理可以用连通域分析from scipy.ndimage import binary_fill_holes, label def clean_mask(mask, min_area120): mask binary_fill_holes(mask) labels, n label(mask) out np.zeros_like(mask) total 0 for i in range(1, n 1): area (labels i).sum() if area min_area: out[labels i] True total area return out, total逻辑说明binary_fill_holes填补心脏内部的低置信度区域更适合心肌壁完整、内部有血池的情况。label给每个连通区域编号然后按面积阈值过滤掉小于 120 像素的噪声区域。这个 120 不是固定值对 512×512 图像心脏实际区域通常超过 3000 像素120 以下可以安全过滤。参数说明如果目标是分割心肌而不是心脏“管腔”则要谨慎使用binary_fill_holes因为心肌壁中间的左心室血池本身是空洞不应该被填满。在这种情况下应该把min_area调大只过滤极小的离散噪声不做孔洞补齐。最后一个小技巧把后处理函数接入预测脚本后不要只盯着保存的 PNG 看建议在测试集上对比原始预测和clean_mask 后的 Dice 变化。如果后处理让平均 Dice 上升超过 1 个百分点说明模型输出存在系统性噪声需要回看训练集标注质量如果反而下降说明心脏区域本身有空腔后处理阈值需要放宽。对比不同阈值的 Dice 曲线是最稳妥的评估方式。本文还有配套的精品资源点击获取