
简介本资源为心脏CT图像分割数据集面向医学图像分割方向的初学者与算法实践者可用于训练和评估二类分割模型。数据图像分辨率统一为512×512采用png格式存储mask标签中0表示背景、255表示心脏具体类别可在classes文本中查看。压缩包共1907个文件以1905张png图片为主另含1个txt标签说明与1个py可视化脚本整体约111.22MB。数据集划分为训练集与测试集训练集包含667张原图及667张对应mask测试集包含285张原图及285张对应mask目录结构清晰便于直接接入分割网络训练流程。配套可视化脚本无需修改即可运行随机抽取一张图片同时展示原始图像、GT图像以及GT在原图上的蒙板效果并保存至当前目录方便快速检查标注质量。目前已有268人学习适合需要心脏CT分割数据与快速可视化验证的读者参考使用。1. 心脏CT分割数据集从拿到标签到跑通可视化的第一公里你手里有一份心脏CT分割数据集2类标签附带数据可视化代码。听起来像是「下载解压就能用」的节奏但真正动手的人都知道医学图像分割的第一公里往往最磨人图像是HU值还是归一化后的灰度标签里0、1、2分别代表什么层间距不均匀的切片直接送进网络会不会翻车这些问题不解决后面模型结构选得再花哨也是白搭。这份数据集的核心价值在于它把「图像标签可视化代码」打包到了一起省去了从DICOM原始文件开始清洗的重复劳动。它适合三类人刚接触医学图像分割、想找一个干净2类任务练手的新手需要快速验证分割算法在心脏CT上表现的算法工程师以及想理解CT影像特点、但不想从零标注数据的科研入门者。2类分割意味着任务边界清晰——通常是心脏结构与非心脏区域的区分或者心室与心房的区分具体类别定义需要看标签文件的说明。接下来我会按「先看懂数据、再跑通可视化、然后处理成可训练格式、最后避坑」的顺序把这条链路拆开讲清楚。2. 心脏CT数据集的目录结构与标签语义先别急着写DataLoader2.1 拿到数据集先做三件事看目录、读标签、查图像属性很多人拿到数据集的第一反应是直接写Dataset类结果跑起来才发现标签值对不上、图像尺寸不统一、有些切片全是背景。我一般会先花十分钟做三件事。第一看目录结构。常见的心脏CT分割数据集会按病例或按切片组织比如patient_01/下面放image_001.png和label_001.png或者用images/和masks/两个平行目录。目录结构决定了你后面写DataLoader时怎么配对图像和标签。第二读标签的像素值分布。2类分割的标签通常用0表示背景1和2表示两个前景类别但也有的数据集用0和255或者0和1。不确认这一点后面算损失函数时会出现「明明预测对了但loss不降」的玄学现象。第三查图像的基本属性尺寸、位深、像素值范围。心脏CT的原始HU值范围通常在-1000到1000以上但保存成PNG后往往被窗宽窗位截断并归一化到0-255。你需要知道当前数据处于哪个阶段才能决定要不要做额外的预处理。import os import numpy as np from PIL import Image data_root ./cardiac_ct_dataset image_dir os.path.join(data_root, images) label_dir os.path.join(data_root, labels) # 列出前5个文件确认命名规则和配对方式 image_files sorted(os.listdir(image_dir))[:5] label_files sorted(os.listdir(label_dir))[:5] print(图像文件示例:, image_files) print(标签文件示例:, label_files) # 读取一张图像和对应标签查看属性 img np.array(Image.open(os.path.join(image_dir, image_files[0]))) lbl np.array(Image.open(os.path.join(label_dir, label_files[0]))) print(f图像形状: {img.shape}, 数据类型: {img.dtype}) print(f图像像素范围: [{img.min()}, {img.max()}]) print(f标签形状: {lbl.shape}, 数据类型: {lbl.dtype}) print(f标签唯一值: {np.unique(lbl)}) print(f各类别像素占比: {[(v, round((lbl v).sum() / lbl.size * 100, 2)) for v in np.unique(lbl)]})这段代码做了四件事确认图像和标签的文件名是否一一对应、检查图像尺寸是否一致、查看像素值范围判断是否已归一化、统计标签中每个类别的像素占比。最后一项尤其重要——如果某个类别的像素占比低于1%说明存在严重的类别不平衡后面选损失函数时就要考虑Dice Loss或带权重的交叉熵。参数说明data_root指向数据集根目录image_dir和label_dir根据实际目录名调整。如果数据集是按病例分文件夹的需要改成递归遍历的方式。np.unique(lbl)返回标签中所有不同的像素值正常情况下2类分割应该只有3个值背景2个前景类如果出现其他值说明标签文件可能包含未清理的噪声或标注边界。2.2 标签语义确认0、1、2到底代表什么标签值的含义不能靠猜。常见的心脏CT分割任务中标签可能对应0背景1左心室2右心室或者0背景1心脏2大血管。有些数据集会在根目录放一个label_names.txt或README.md说明类别定义如果没有就需要通过可视化来推断。推断的方法很简单把标签中每个类别单独提取出来叠加到原图上看它覆盖的是哪个解剖结构。这一步不需要医学背景只需要观察形状和位置——心脏CT中左心室通常呈圆形且壁厚右心室呈新月形心房在心室上方。import matplotlib.pyplot as plt # 将标签中每个类别单独可视化叠加到原图上 fig, axes plt.subplots(1, 4, figsize(16, 4)) axes[0].imshow(img, cmapgray) axes[0].set_title(原始图像) axes[0].axis(off) for idx, class_val in enumerate([1, 2]): overlay img.copy() mask (lbl class_val) # 用红色高亮当前类别 overlay[mask] [255, 0, 0] if idx 0 else [0, 255, 0] axes[idx 1].imshow(overlay) axes[idx 1].set_title(f类别 {class_val} 覆盖区域) axes[idx 1].axis(off) axes[3].imshow(img, cmapgray) axes[3].imshow(lbl, alpha0.4, cmapjet) axes[3].set_title(标签整体叠加) axes[3].axis(off) plt.tight_layout() plt.savefig(label_visualization.png, dpi150) plt.show()这段代码把每个类别单独高亮并叠加到原图上同时用半透明彩色标签展示整体分割效果。运行后你会看到每个类别覆盖的解剖区域从而确认标签语义。如果发现某个类别覆盖的区域明显不合理比如覆盖了整个图像说明标签值可能不是你以为的那样需要重新检查。注意如果数据集自带的标签是RGB三通道图像而非单通道灰度图需要先转换成单通道索引图。常见做法是取第一个通道或使用np.argmax沿通道维度取最大值的位置作为类别索引。3. 用自带可视化代码跑通第一张图环境、路径与参数调整3.1 可视化代码的典型结构与运行前的环境检查数据集附带的可视化代码通常是一个Python脚本依赖matplotlib、numpy、Pillow等基础库。在运行之前先确认环境里这些库的版本是否兼容。我遇到过因为matplotlib版本差异导致cmap参数行为不一致、标签颜色显示错乱的情况。# 检查关键依赖版本 python -c import numpy; print(numpy:, numpy.__version__) python -c import matplotlib; print(matplotlib:, matplotlib.__version__) python -c import PIL; print(Pillow:, PIL.__version__) python -c import cv2; print(opencv:, cv2.__version__) 2/dev/null || echo opencv未安装如果可视化代码里用了cv2做图像读取或颜色映射而环境里没有装opencv-python直接pip install opencv-python即可。但要注意opencv读取图像默认是BGR通道顺序而matplotlib显示时按RGB处理混用会导致颜色偏蓝。统一用PIL读取、或者读取后做cv2.cvtColor(img, cv2.COLOR_BGR2RGB)转换。可视化脚本通常需要指定数据路径。常见写法是脚本开头有一个DATA_DIR变量或者通过命令行参数传入。如果脚本里写死了作者本地的路径你需要改成自己的实际路径。这一步没有技术含量但最容易卡住新手——路径里的反斜杠、空格、中文目录都可能导致读取失败。# 典型可视化脚本的参数配置区域 import argparse parser argparse.ArgumentParser(description心脏CT分割数据集可视化) parser.add_argument(--data_root, typestr, default./cardiac_ct_dataset, help数据集根目录) parser.add_argument(--case_id, typestr, defaultpatient_01, help要可视化的病例ID) parser.add_argument(--slice_idx, typeint, default10, help要可视化的切片索引) parser.add_argument(--alpha, typefloat, default0.4, help标签叠加透明度) parser.add_argument(--save_path, typestr, default./vis_output, help可视化结果保存目录) args parser.parse_args() import os os.makedirs(args.save_path, exist_okTrue) print(f数据根目录: {args.data_root}) print(f当前病例: {args.case_id}, 切片: {args.slice_idx})把路径和参数抽成命令行参数的好处是换一个病例或换一个切片不需要改代码直接改命令行就行。alpha参数控制标签叠加的透明度0.3到0.5之间通常视觉效果最好——太低看不清标签边界太高会遮住原图的解剖细节。3.2 多切片批量可视化与结果解读单张切片可视化只能看个大概真正要判断数据质量需要批量可视化一个病例的多个切片观察标签在层间的连续性。心脏CT的切片是沿Z轴排列的一个完整的心脏扫描通常有几十到上百层。如果标签在某些层突然消失或形状突变说明标注可能不完整或存在层间不一致。import os import numpy as np from PIL import Image import matplotlib.pyplot as plt def visualize_case(data_root, case_id, num_slices8, save_dir./vis_output): 批量可视化一个病例的多个切片 img_dir os.path.join(data_root, images, case_id) lbl_dir os.path.join(data_root, labels, case_id) img_files sorted(os.listdir(img_dir)) total len(img_files) # 均匀采样num_slices个切片 indices np.linspace(0, total - 1, num_slices, dtypeint) fig, axes plt.subplots(2, num_slices // 2, figsize(20, 6)) axes axes.flatten() for i, idx in enumerate(indices): img np.array(Image.open(os.path.join(img_dir, img_files[idx]))) lbl_name img_files[idx].replace(image, label) lbl np.array(Image.open(os.path.join(lbl_dir, lbl_name))) axes[i].imshow(img, cmapgray) # 用不同颜色叠加两个类别 overlay np.zeros((*lbl.shape, 4)) overlay[lbl 1] [1, 0, 0, 0.5] # 类别1红色 overlay[lbl 2] [0, 1, 0, 0.5] # 类别2绿色 axes[i].imshow(overlay) axes[i].set_title(f切片 {idx}, fontsize9) axes[i].axis(off) plt.suptitle(f病例 {case_id} 多切片可视化, fontsize14) plt.tight_layout() save_path os.path.join(save_dir, f{case_id}_multi_slice.png) plt.savefig(save_path, dpi150, bbox_inchestight) plt.close() print(f已保存: {save_path}) # 调用示例 visualize_case(./cardiac_ct_dataset, patient_01, num_slices8)这段代码均匀采样一个病例的8个切片每个切片上叠加两个类别的彩色掩码。运行后重点观察三件事标签在层间是否连续、两个类别是否有重叠或空洞、边缘切片是否标注完整。如果发现中间某层标签突然消失可能是该层没有标注或标注被遗漏训练时需要考虑是否跳过这些层。参数说明num_slices控制采样切片数量建议设为8到12太少看不出层间变化太多图会太密。overlay数组的第四通道是透明度0.5表示半透明叠加。如果类别数量不是2需要相应调整overlay的赋值行数。提示如果可视化结果中标签边缘呈锯齿状说明原始标注分辨率可能低于图像分辨率训练前需要确认是否要做标签上采样或使用忽略边缘的损失函数。4. 从可视化到可训练图像预处理与标签编码的四个关键步骤4.1 图像归一化CT的HU值不是0-255那么简单心脏CT的原始像素值是HUHounsfield Unit反映组织对X射线的吸收系数。空气约-1000 HU水约0 HU骨骼可达1000 HU以上。但保存成PNG格式后像素值通常被线性映射到0-255这个映射过程会丢失部分信息。如果你拿到的数据已经是PNG需要确认它是否经过了窗宽窗位调整。常见的心脏CT窗宽窗位设置是窗宽400、窗位40对应显示范围-160到240 HU。这个范围内包含了心脏软组织和对比剂增强区域是分割任务最关心的部分。如果数据集没有做窗宽窗位调整你可以自己用原始HU值做一次截断和归一化。def window_normalize(img, window_center40, window_width400): 对CT图像做窗宽窗位归一化到0-1 lower window_center - window_width // 2 upper window_center window_width // 2 img_clipped np.clip(img, lower, upper) img_normalized (img_clipped - lower) / (upper - lower) return img_normalized.astype(np.float32) # 如果图像已经是0-255的PNG先还原到近似HU值再归一化 # 假设原始映射范围是-1000到1000 img_hu_approx img.astype(np.float32) / 255.0 * 2000 - 1000 img_normalized window_normalize(img_hu_approx, window_center40, window_width400) print(f归一化后范围: [{img_normalized.min():.3f}, {img_normalized.max():.3f}])这段代码先把0-255的PNG像素值近似还原到-1000到1000的HU范围再按心脏窗做截断和归一化。归一化后的值在0到1之间适合直接送入神经网络。window_center和window_width可以根据具体任务调整——如果分割目标包含大血管窗位可以适当提高如果主要看心肌窗位可以降低。参数说明window_center40是心脏软组织窗的典型值window_width400覆盖了从脂肪到增强血管的范围。如果数据已经是归一化后的浮点数跳过还原步骤直接做截断即可。4.2 标签编码从灰度图到One-Hot或多通道掩码2类分割的标签在训练时通常有两种编码方式一种是保持单通道索引图用交叉熵损失函数另一种是转成多通道One-Hot掩码用Dice Loss或BCE Loss。两种方式各有适用场景。单通道索引图适合类别互斥且类别数较少的情况内存占用小计算交叉熵时直接传入类别索引即可。多通道One-Hot适合需要独立计算每个类别Dice系数的情况或者类别之间可能存在重叠虽然2类分割通常互斥。def label_to_onehot(label, num_classes3): 将单通道标签转为One-Hot多通道掩码 onehot np.zeros((num_classes, *label.shape), dtypenp.float32) for c in range(num_classes): onehot[c] (label c).astype(np.float32) return onehot def label_to_index(label): 确保标签是单通道索引图去除多余维度 if label.ndim 3: label label[:, :, 0] # 取第一个通道 return label.astype(np.int64) # 示例 lbl_index label_to_index(lbl) lbl_onehot label_to_onehot(lbl_index, num_classes3) print(f索引图形状: {lbl_index.shape}, One-Hot形状: {lbl_onehot.shape}) print(fOne-Hot各类别像素和: {lbl_onehot.sum(axis(1, 2))})label_to_onehot函数把单通道标签转成形状为(num_classes, H, W)的多通道掩码每个通道对应一个类别的二值掩码。num_classes3是因为2类分割加上背景共3个类别。label_to_index处理标签可能带有冗余通道的情况——有些数据集保存的标签是3通道RGB图像但三个通道值相同取第一个通道即可。注意如果标签中出现了不在预期类别范围内的值比如255需要在编码前做一次清理把这些值映射到背景或忽略区域。否则One-Hot编码会漏掉这些像素导致训练时标签和预测不对齐。4.3 数据集划分按病例分还是按切片分医学图像分割的数据集划分有一个容易踩的坑按切片随机划分会导致同一病例的不同切片同时出现在训练集和验证集中造成数据泄露。正确的做法是按病例划分——整个病例的所有切片要么全在训练集要么全在验证集。import os import random def split_by_case(data_root, train_ratio0.8, seed42): 按病例划分训练集和验证集 case_ids sorted(os.listdir(os.path.join(data_root, images))) random.seed(seed) random.shuffle(case_ids) split_idx int(len(case_ids) * train_ratio) train_cases case_ids[:split_idx] val_cases case_ids[split_idx:] print(f总病例数: {len(case_ids)}) print(f训练集病例: {train_cases}) print(f验证集病例: {val_cases}) return train_cases, val_cases train_cases, val_cases split_by_case(./cardiac_ct_dataset, train_ratio0.8)这段代码按病例ID划分数据集seed42保证每次运行结果一致。train_ratio0.8表示80%的病例用于训练20%用于验证。如果病例数很少比如少于10个可以考虑用交叉验证的方式每次留一个病例做验证。参数说明seed固定随机种子方便复现。train_ratio根据数据量调整——数据量少时可以适当降低训练集比例留更多病例做验证。划分完成后建议把划分结果保存成JSON文件训练和验证时直接读取避免每次重新划分导致结果不一致。4.4 数据增强心脏CT能用的和不能用的数据增强在医学图像分割里是把双刃剑。用对了能提升泛化能力用错了会破坏解剖结构的合理性。心脏CT分割中安全且有效的增强方式包括随机旋转±15度以内、随机缩放0.9到1.1倍、弹性变形小幅、亮度对比度微调。不建议使用的包括大角度旋转心脏在胸腔中有固定朝向、水平翻转左右心室位置会互换语义改变、随机裁剪可能裁掉关键结构。import numpy as np from scipy.ndimage import rotate, zoom def augment_safe(img, lbl, angle_range15, scale_range(0.9, 1.1)): 安全的数据增强小角度旋转缩放 # 随机旋转 angle np.random.uniform(-angle_range, angle_range) img_rot rotate(img, angle, reshapeFalse, order1, modeconstant, cval0) lbl_rot rotate(lbl, angle, reshapeFalse, order0, modeconstant, cval0) # 随机缩放 scale np.random.uniform(*scale_range) h, w img.shape img_scaled zoom(img_rot, scale, order1) lbl_scaled zoom(lbl_rot, scale, order0) # 裁剪或填充回原始尺寸 if scale 1: pad_h (h - img_scaled.shape[0]) // 2 pad_w (w - img_scaled.shape[1]) // 2 img_scaled np.pad(img_scaled, ((pad_h, h - img_scaled.shape[0] - pad_h), (pad_w, w - img_scaled.shape[1] - pad_w)), modeconstant) lbl_scaled np.pad(lbl_scaled, ((pad_h, h - lbl_scaled.shape[0] - pad_h), (pad_w, w - lbl_scaled.shape[1] - pad_w)), modeconstant) else: start_h (img_scaled.shape[0] - h) // 2 start_w (img_scaled.shape[1] - w) // 2 img_scaled img_scaled[start_h:start_h h, start_w:start_w w] lbl_scaled lbl_scaled[start_h:start_h h, start_w:start_w w] return img_scaled.astype(np.float32), lbl_scaled.astype(np.int64) # 测试增强 img_aug, lbl_aug augment_safe(img_normalized, lbl_index) print(f增强后图像形状: {img_aug.shape}, 标签形状: {lbl_aug.shape}) print(f增强后标签唯一值: {np.unique(lbl_aug)})这段代码实现了小角度旋转和缩放两种增强旋转时图像用双线性插值order1标签用最近邻插值order0避免标签出现非整数类别值。缩放后通过裁剪或填充恢复原始尺寸保证增强前后尺寸一致。参数说明angle_range15控制旋转角度范围心脏CT建议不超过15度。scale_range(0.9, 1.1)控制缩放比例范围太大会导致解剖结构变形。order0用于标签插值确保标签值始终是整数类别索引。5. 避坑与排查心脏CT分割数据集最常见的5个翻车现场5.1 标签值对不上导致loss不降现象模型训练几个epoch后loss几乎不变预测结果全是背景或全是某一类。原因标签中前景类的像素值不是预期的1和2而是其他值比如255或128导致交叉熵计算时这些像素被当作背景或忽略。解决在Dataset类里加一行print(np.unique(label))确认标签唯一值。如果发现异常值用np.where做映射label np.where(label 255, 1, label)。同时检查标签是否被错误地归一化到了0-1之间——标签必须是整数类别索引不能做归一化。5.2 图像和标签尺寸不一致导致拼接报错现象DataLoader返回的batch中图像和标签形状不匹配或者可视化时叠加错位。原因部分数据集的图像和标签分辨率不同比如图像是512×512标签是256×256。直接配对会导致形状不匹配。解决在Dataset的__getitem__里统一尺寸。如果标签分辨率低用最近邻插值上采样到图像尺寸如果图像分辨率低用双线性插值上采样图像。不要用双线性插值处理标签会产生非整数类别值。5.3 层间距不均匀导致3D重建变形现象把切片堆叠成3D体积后心脏结构在Z轴方向被拉伸或压缩和真实解剖比例不符。原因CT扫描的层间距slice thickness可能不均匀比如靠近心脏的区域层间距2mm远离的区域5mm。直接按切片索引堆叠会忽略物理距离。解决读取DICOM头文件中的SliceThickness或SpacingBetweenSlices按物理距离重采样到统一层间距。如果数据集只提供了PNG切片需要从文件名或附带的元数据文件中获取层间距信息。没有元数据时只能按等间距处理但要在论文或报告中说明这一局限性。5.4 可视化代码路径写死导致换目录就报错现象数据集自带的visualize.py运行时报FileNotFoundError提示找不到某个路径。原因脚本里用了作者本地的绝对路径比如/home/author/data/cardiac/换到你机器上路径不存在。解决把脚本里所有硬编码路径改成相对路径或命令行参数。用os.path.join拼接路径避免手动拼字符串。如果脚本里用了os.chdir切换目录注意切换后相对路径的基准会变建议统一用绝对路径。5.5 训练时显存溢出但不知道哪一步爆的现象训练开始后显存逐渐增长几个batch后报CUDA out of memory。原因常见原因有三个——DataLoader的num_workers设太大导致每个worker都复制了一份数据损失函数里保留了计算图比如没有用torch.no_grad()包裹验证步骤图像尺寸太大导致单张就占满显存。解决先把num_workers设为0排除多进程问题再把batch_size降到1看是否还爆。如果单张也爆说明图像尺寸太大需要降采样或裁剪。验证步骤用with torch.no_grad():包裹避免计算图累积。另外检查是否在训练循环里意外保留了中间变量比如把loss累加到一个列表里但没有detach。6. 进阶技巧用可视化代码反推数据质量与模型表现数据集自带的可视化代码不只是用来看图的它可以改造成数据质量检查和模型预测对比的工具。我一般会在训练前用可视化脚本扫一遍所有病例标记出标签异常比如某层标签面积突然翻倍或减半的切片训练后再用同一套可视化逻辑把模型预测结果和真实标签并排显示直观判断模型在哪些区域容易出错。def compare_prediction(img, gt_label, pred_label, save_pathNone): 并排显示原图、真实标签、预测标签、错误区域 fig, axes plt.subplots(1, 4, figsize(18, 4.5)) axes[0].imshow(img, cmapgray) axes[0].set_title(原始图像) axes[0].axis(off) axes[1].imshow(img, cmapgray) axes[1].imshow(gt_label, alpha0.4, cmapjet, vmin0, vmax2) axes[1].set_title(真实标签) axes[1].axis(off) axes[2].imshow(img, cmapgray) axes[2].imshow(pred_label, alpha0.4, cmapjet, vmin0, vmax2) axes[2].set_title(模型预测) axes[2].axis(off) # 错误区域预测和真实不一致的像素标红 error_mask (pred_label ! gt_label).astype(np.float32) axes[3].imshow(img, cmapgray) axes[3].imshow(error_mask, alpha0.6, cmapReds, vmin0, vmax1) axes[3].set_title(f错误区域 (错误率: {error_mask.mean()*100:.2f}%)) axes[3].axis(off) plt.tight_layout() if save_path: plt.savefig(save_path, dpi150, bbox_inchestight) plt.close() else: plt.show() # 假设pred是模型输出的预测标签已经过argmax # compare_prediction(img_normalized, lbl_index, pred_label, prediction_compare.png)这段代码把原图、真实标签、预测标签和错误区域并排显示。错误区域用红色高亮一眼就能看出模型在哪些解剖结构上容易混淆。如果错误集中在某个类别的边界说明模型对边缘的分辨能力不足可以考虑加边界损失或后处理如果错误集中在某个特定区域说明训练数据中该区域的样本不够需要补充数据或做针对性增强。参数说明vmin0, vmax2固定了标签显示的颜色范围保证真实标签和预测标签用同一套颜色映射方便对比。error_mask.mean()计算的是像素级错误率可以作为模型表现的快速指标但不要替代Dice系数或IoU等标准指标。我自己的习惯是每训练完一个epoch挑验证集中错误率最高的三个切片做一次对比可视化存到debug/目录下。这样不用等训练完全结束就能发现模型是不是在某个类别上完全没学会。这个习惯帮我省了很多次「训练了三天才发现标签用错了」的后悔药。希望帮到你。本文还有配套的精品资源点击获取