资讯动态

医学图像三切面分割数据集处理全攻略:从标签解析到模型避坑

发布时间:2026/10/5 3:43:02 来源:尧图企业网站定制
简介面向医学图像分割任务整理的骶骨腰痛脊椎分割数据集基于CTSpine1K脊柱数据适合医学影像算法工程师、科研人员和计算机视觉学习者用于模型训练与验证。资源按轴位面、冠状面、矢状面三个切面划分为三个子集分别包含2113张、2272张、6404张图像及对应mask并统一完成windowing对比度增强和512×512重采样还剔除了ROI比例不足3%的无效切片免除基础预处理工作。图像以jpg格式保存原始数据mask为8bit灰度png非0前景、0背景附classes.txt类别说明与show.py可视化脚本可快速查看分割标注。压缩包共2000个文件以png标签文件为主另有txt和py各1个整体大小472.47MB已有226人浏览学习。下载后可直接用于脊椎结构分割、腰痛病灶定位等研究也可用于比较不同切面对分割效果的影响。1. 骶骨腰痛脊椎分割为什么一个5类别数据集要同时给3个切面做腰痛相关的影像AI最头疼的不是网络结构而是数据长什么样。医学图像分割数据集里骶骨腰痛脊椎分割这类带三切面、5类别标签的集合表面上是把同一批患者的CT或MRI切成了轴位、冠状位、矢状位三套2D图片实际上它解决了模型「只认一个视角」的毛病腰痛患者的疼痛点往往要从横断面看椎间盘从矢状面看滑脱从冠状面看侧弯单独喂一个切面很难覆盖临床阅片习惯。你在本地跑通这类数据集等于同时拿到了数据组织规范、标签文件解析范式和可视化代码的试错样本。对刚转医学影像的算法工程师来说这是一套能直接拿来练手的现成素材对有经验的团队来说它也是验证三切面融合策略的好起点。2. 拆解数据集结构3个切面、5类别标签文件和文件命名规则2.1 一个典型的三切面分割数据集里装了什么常见做法的目录结构长这样三个切面文件夹分别叫 axial、coronal、sagittal每个文件夹里再按 images 和 labels 各分一层。images 存原始切片labels 存对应的分割掩膜。掩膜里的像素值不是随随便便的0和255而是按 label_mapping.json 里约定的类别编号填进去的。实际解压后你会发现有些切面图像是 PNG有些是 NIfTI少数情况直接给两张 DICOM。不管哪种第一步都应该先列文件别急着训练。文件用途常见格式说明原始切面图像PNG / NIfTI (.nii.gz)按切面分开存放文件名常带患者ID和切片序号分割掩膜PNG / NIfTI像素值对应类别编号背景为0前景为1、2、3、4标签文件JSON / YAML记录每个类别编号对应的解剖结构名称和RGB颜色可视化代码Python (.py)用于把掩膜叠加到原图上检查对齐和标注质量我这里说的5类别通常约定为0对应背景、1对应腰椎椎骨、2对应骶骨、3对应椎间盘、4对应脊髓硬膜囊。不同数据集会用别的顺序但绝大多数会保留背景类并把解剖结构放在1到4。你拿到手第一件事不是写网络而是把标签文件打开确认。5类别里最容易踩雷的是骶骨和椎骨交界处有些标注把骶骨上关节突画进腰椎编号里这种错在单张截图里看不出来叠加到矢状位大图后边界就会歪。2.2 标签文件是黑匣子先读JSON再谈训练我习惯把标签文件当作整个数据集的说明书。一个标准的 label_mapping.json 大概是这样的{ 0: {name: background, color: [0, 0, 0]}, 1: {name: lumbar_vertebra, color: [255, 0, 0]}, 2: {name: sacrum, color: [0, 255, 0]}, 3: {name: intervertebral_disc, color: [0, 0, 255]}, 4: {name: spinal_canal, color: [255, 255, 0]} }读取这个文件不要手写正则直接一行json加载然后打印每一个键值对。下面这段是我每次拿到新数据集都要跑的第一段诊断代码import json import numpy as np with open(label_mapping.json, r, encodingutf-8) as f: label_map json.load(f) for label_id, info in label_map.items(): print(编号:, label_id, 名称:, info[name], 颜色:, info[color]) mask np.load(sample_label.npy) if False else None # 这里如果手头有掩膜用 np.unique(mask) 查看实际像素值逻辑说明json.load 会把键都变成字符串所以后续做掩膜比较时记得 int(label_id) 或者把掩膜转成 str。参数说明color 列表里三个值分别代表 RGB上面用的 [255,0,0] 是纯红显示时如果原始图像已经是伪彩色颜色叠加会被覆盖需要先归一化原始图像到0-1再叠加。为什么强烈建议先读标签文件因为很多公开或者自制的医学图像分割数据集的掩膜并不是连续编号0到4有的会把“背景”定义为2把“病变区域”定义为255。你按常规语义直接处理可视化时就会看到整片整片的红色覆盖在软组织上。我遇到过一次 label_mapping.json 里写的是 BGR 顺序而可视化代码按 RGB 解析结果椎间盘在屏幕上从蓝色变成黄色。这类问题不看文件根本定位不了训练出来的模型自然也是错的。3. 把NIfTI切成三切面2D数据采样步长与标签对齐的细节3.1 为什么医学图像切面要单独存轴位、冠状位、矢状位的坐标系差异医学图像的标准坐标系和自然图像不一样。轴位axial是垂直于人体头尾方向的横截面冠状位coronal是从前到后的方向矢状位sagittal是从左到右的方向。同一个三维体积数据沿着不同轴切片你看到的是完全不同形态的解剖结构。CT和MRI的默认存储通常是轴位序列但很多数据集中为了建模方便直接把三个切面都渲染成2D PNG。这样做的好处是训练时可以独立加载某一个切面不必在DataLoader里做重采样缺点是如果不保留原始体素之间的Spacing后续融合3D时各向异性会让人头疼。如果你的数据集给的是 NIfTI 而不是图片通常要自己切。同一个.nii.gz文件包含完整的体数据我用SimpleITK来抽取三个切面这样标签和图像同步变换不容易错位。核心代码如下import SimpleITK as sitk import numpy as np def extract_sagittal_slices(image_path, label_path): image sitk.ReadImage(image_path) label sitk.ReadImage(label_path) # 转换为numpy数组shape为(深度, 高度, 宽度) img_arr sitk.GetArrayFromImage(image) lbl_arr sitk.GetArrayFromImage(label) # 矢状位沿着宽度方向取切片步长设为每隔4张取一张 num_width img_arr.shape[2] for idx in range(0, num_width, 4): img_slice img_arr[:, :, idx] lbl_slice lbl_arr[:, :, idx] # 保存时用相同索引断命名确保后续图-掩膜配对 np.savez_compressed(fslice_{idx:04d}.npz, imageimg_slice, labellbl_slice)逻辑说明GetArrayFromImage返回的数组顺序是轴位、冠状位、矢状位也就是 z, y, x。上述代码沿第三个维度取切片得到的是矢状位图像。参数说明步长取4是为了减少冗余切片因为矢状位连续相邻的切片内容重叠度很高如果你关注椎间盘的细小信号步长建议缩到2否则会漏掉小结构。切片索引必须显式写进文件名因为单独保存图像和标签时如果文件名不一致后面对齐就是一场灾难。类似的取轴位切片时沿第一个维度取冠状位时沿第二个维度。这里有个容易翻车的点有些NIfTI文件经过设置后方向矩阵不是单位阵直接取数组维度可能会导致图像左右翻转。因此我强烈建议读取后先用sitk.GetDirection打印方向矩阵确认不是斜向切层。如果方向矩阵不是 [1,0,0;0,1,0;0,0,1]先调用sitk.DICOMOrient重定向到标准方向再切片。3.2 标签如何跟着图像走重采样与Spacing对齐三切面数据集若本身是3D重采样的2D图像就需要在保存时固定物理尺寸。我在处理这类数据时第一步是从DICOM头里读取患者Spacing然后以毫米为单位把图像统一重采样到例如 1mm x 1mm 的平面分辨率。标签的插值方式必须是最近邻否则椎间盘和骶骨边界会出现介于两个类别之间的渐变像素训练时这些伪像素会直接教坏模型。target_spacing [1.0, 1.0] def resample_slice(image_np, label_np, original_spacing): while image_np.shape[0] ! 256: # 这里只是演示真正做法是用SimpleITK的Resample new_shape [int(round(image_np.shape[0] * original_spacing[0] / 1.0)), int(round(image_np.shape[1] * original_spacing[1] / 1.0))] # 后续填充sitk.Resample break上面的代码只给出了思路实际项目中我用下面的方式更稳妥import SimpleITK as sitk def resample_to_spacing(image_sitk, label_sitk, target_spacing[1.0, 1.0]): original_spacing image_sitk.GetSpacing() original_size image_sitk.GetSize() new_size [int(round(orig * spc / target)) for orig, spc, target in zip(original_size, original_spacing, target_spacing)] resampler sitk.ResampleImageFilter() resampler.SetOutputSpacing(target_spacing) resampler.SetSize(new_size) resampler.SetInterpolator(sitk.sitkLinear) resampled_image resampler.Execute(image_sitk) resampler.SetInterpolator(sitk.sitkNearestNeighbor) resampled_label resampler.Execute(label_sitk) return resampled_image, resampled_label参数说明target_spacing 设为 [1,1] 是让每个像素代表实际1平方毫米如果原图一直是各向同性你可以不重采样但训练时统一尺寸能让batch更稳定。标签重采样必须用最近邻这一点没有商量余地。线性插值用在标签上会创造出类似像素值2.7之类的不存在类别导致标签文件和5类别定义直接冲突。做完这些切片和重采样你得到的才是真正能喂给U-Net这类分割网络的2D训练样本。每个切片配对的时候先用np.shape核对图像和标签尺寸再用np.unique(lbl_slice)检查类别有没有在某个切片里突然消失。座椅姿势不同、病人体型不同椎间盘在有些矢状位切片里可能只占三五个像素这种情况就不能用固定步长抽样要按类别像素比例决定是否保留该切片。4. 用可视化代码第一版就能发现标注错位重叠显示与类别统计4.1 最小可视化脚本把预测轮廓叠到原图上拿到图像和标签文件后最紧急的是写一个可视化代码尽快看到标注长什么样。我习惯把原始灰度图转成三通道RGB再把分割掩膜转成彩色图最后用透明度把两者叠加。这样背景仍然是黑白椎骨是红色骶骨是绿色椎间盘是蓝色硬膜囊是黄色一眼就能看出有没有大范围偏位。import cv2 import numpy as np def overlay_mask(image_path, mask_path, color_map): image cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) image_rgb cv2.cvtColor(image, cv2.COLOR_GRAY2RGB) mask cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) mask_color np.zeros_like(image_rgb) colors { 1: [255, 0, 0], # 腰椎椎骨 2: [0, 255, 0], # 骶骨 3: [0, 0, 255], # 椎间盘 4: [255, 255, 0] # 硬膜囊 } for label_id, color in colors.items(): mask_color[mask label_id] color overlay cv2.addWeighted(image_rgb, 0.6, mask_color, 0.4, 0) cv2.imwrite(overlay_result.png, overlay)逻辑说明cv2.addWeighted的权重设置为原图0.6、掩膜0.4能让解剖背景保持可辨认掩膜轮廓也清晰。参数说明如果你的标签文件里有类别5记得在 colors 字典里补上否则该类别在可视化里完全透明。如果掩膜路径读出来是彩色PNG直接读灰度会得到像素值0-255但这里5类别标签的像素值1、2、3、4如果重叠着读会出现把多个通道的像素混合的情况所以始终按单通道读掩膜。另外一个关键点是图像旋转问题。医学图像切面保存时可能自带方向信息但用OpenCV读PNG时横竖方向完全看存储时的矩阵顺序。我碰到过一个坑矢状位图像看起来是左脑朝前但分割标注是右脑朝前视觉上整个脊柱轮廓错位。要避免这个问题在可视化代码里先打印两对坐标左右缘的解剖点。如果图像中心和掩膜中心明显不重合说明有镜像翻转。用np.fliplr翻转后重新对比直到边缘一致再继续训练。4.2 统计每个类别的像素占比是训练前必做的体检可视化只能看几个样本想全面体检还得统计整个数据集的类别分布。三切面、5类别标签很容易变成极端不平衡问题背景可能占95%椎间盘不到1%硬膜囊有时候只有几个像素。这时候如果你的损失函数直接套用自然图像分割的CrossEntropy网络大概率只学会背景。以下代码统计单张掩膜类别占比import numpy as np from collections import Counter def count_class_percent(mask_path): mask np.load(mask_path)[label] if mask_path.endswith(.npz) \ else cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) total mask.size class_counts Counter(mask.flatten()) percent_map {cls: cnt / total for cls, cnt in class_counts.items()} for cls in [0, 1, 2, 3, 4]: print(f类别{cls}: {class_counts.get(cls, 0)} 像素, {percent_map.get(cls, 0):.4%})逻辑说明统计完单张后要把整个数据集的占比加权平均而不是简单加起来再除以文件数因为不同病人切片数量不一致。参数说明np.load后取label是假设保存的npz文件里包含了label数组如果你的标签文件是PNG直接走imread分支即可。若发现某个类别的占比在所有切面里都低于1%建议训练时使用带权重Dice Loss或Focal Loss而不是普通CrossEntropy。这些统计结果也能反过来校验标签文件。比如JSON里写了硬膜囊类别4但统计结果在整个数据集里类别4从未出现说明某个切面或某一部分切片的标注可能没跑全。如果三个切面之中只有轴位出现了类别4而矢状位完全没有那很可能是标注阶段只用轴位勾了硬膜囊其余切面漏标。这种信息可视化看不出统计后能立刻发现属于数据体检里最有价值的一步。5. 避坑三切面分割数据集的常见问题与排查路径5.1 标签像素值与JSON映射不一致现象可视化代码用JSON里定义的红色显示腰椎椎骨但屏幕上看到骶骨区域变成了红色或者几乎所有像素都落在类别255。 原因掩膜文件里实际存储的像素值不是JSON里标定的0-4可能是255代表前景也可能是不同标注工具导出了不同的通道顺序。 解决先跑统计代码打印np.unique(mask)把所有出现过的像素值和JSON里的编号做差集。如果出现255最常见做法是把255改成1再对原JSON做一次字典反转。不要手动逐个像素改直接用numpy的where替换import numpy as np mask np.array([[0, 255, 0], [1, 255, 2]]) mask_fixed np.where(mask 255, 1, mask) # 这样就把255归一到腰椎椎骨类别参数说明np.where的三个参数分别是condition、替换值、原数组。如果255已经是背景那就替换成0。关键是先确认255对应的结构不要在没统计前瞎换。标签文件和可视化代码同时错位的时候肉眼真的看不出来需要把原始切片单独显示一遍再叠加彩色掩膜看解剖位置是否吻合。5.2 三切面图像尺寸与Spacing不一致现象轴位图像是512x512冠状位是384x480矢状位是300x400直接用同一个DataLoader报维度错误。 原因数据集为了节省空间把每个切面按各自的最大包围盒裁剪了没有统一到同一个物理视场。 解决先查询每个切面的Spacing再重采样到统一的毫米分辨率。常见做法是让所有切面都按照轴位的Spacing为基准统一到1x1毫米这样既能对齐全又不会损失椎间盘的小细节。如果某些切面裁剪得太小采用中心裁剪后再pad到固定尺寸而不是直接resize否则椎骨比例被拉伸后训练出的模型泛化到新病人时会产生假的弧线条纹。def pad_or_crop(slice_arr, target_h, target_w): h, w slice_arr.shape[:2] # 先裁剪到目标尺寸以内 if h target_h: start_h (h - target_h) // 2 slice_arr slice_arr[start_h:start_h target_h, :] if w target_w: start_w (w - target_w) // 2 slice_arr slice_arr[:, start_w:start_w target_w] # 再补零到目标尺寸 pad_h max(target_h - slice_arr.shape[0], 0) pad_w max(target_w - slice_arr.shape[1], 0) return np.pad(slice_arr, ((pad_h // 2, pad_h - pad_h // 2)[:min(len(slice_arr.shape), 2)], (pad_w // 2, pad_w - pad_w // 2)), modeconstant)说明这段代码演示了中心裁剪加补零的思路真正写的时候要分别处理图像和标签但不要补零后在3D方向再切割否则两个切面之间空间关系会丢。Spacing问题一定在整个训练前解决不要在模型里加resize层那是给自然图像用的医学数据里给椎骨做非等比放缩会影响诊断信息。5.3 训练/验证划分泄漏现象验证集Dice看起来很高但放到真实病人单切面上效果差得离谱。 原因没有按患者ID划分数据集同一个病人的三切面切片被随机分到了训练和验证里。 解决在文件列表中提取患者标识用患者ID做分组。下面的伪代码表示这个逻辑# 按患者ID分组确保同一患者所有切面在同一条分割内 patient_to_files {} for path in all_files: pid path.split(/)[0] # 假设第一位是患者ID patient_to_files.setdefault(pid, []).append(path) patient_ids list(patient_to_files.keys()) train_patients patient_ids[:int(0.8 * len(patient_ids))] val_patients patient_ids[int(0.8 * len(patient_ids)):]原因说明医学图像分割数据集的验证集划分核心原则不是文件级别随机而是病人级别隔离。因为同一病人的三切面切片之间高度相关把其中一部分放进训练集模型相当于记住了这个病人的纹理验证时遇到剩余切片自然效果好。使用“按病人划分”能避免这种虚假精度。参数说明训练比例8:2常见但如果数据集里病人数量小于10建议改成K折交叉验证防止验证集人数太少导致抖动太大。5.4 可视化代码跑不起来掩膜通道数不对现象OpenCV读出来的mask是三维数组shape为(H,W,3)直接用mask 1判断会得到HxWx3的布尔数组叠加时报维度不匹配。 原因标注工具导出彩色位图时把三个通道都写了相同的灰度值或者本身存成BGR的伪彩色掩膜。 解决先用mask.shape看看通道数如果是3通道直接转换成单通道。如果三个通道完全相同取任意通道若不同说明是伪彩色标注需要用标签文件里的颜色做反向映射。我经常用的反向代码mask cv2.imread(mask_path, cv2.IMREAD_COLOR) gray_mask np.zeros(mask.shape[:2], dtypenp.uint8) for label_id, color in colors.items(): # 颜色是RGB但OpenCV读出来是BGR bgr color[::-1] gray_mask[np.all(mask bgr, axis-1)] label_id逻辑说明这一步能把彩色掩膜转换成语义编号。参数说明OpenCV默认读入格式是BGR所以需要把colors里定义的RGB反转成BGR再匹配。如果标签文件里颜色没有严格按照RGB顺序写这里匹配会不中这时候先打印掩膜中最多的三个BGR值再比对JSON。5.5 背景类远大于前景导致验证指标虚高现象整体Dice达到0.95但单独看椎间盘的Dice只有0.3腰痛关键结构完全没分割出来。 原因模型把所有像素预测成背景因为背景占比95%Sensitivity的权重完全倾向于背景。 解决用逐类Dice评估模型不要只看average。训练时优先使用Dice Loss或带类别权重的Tversky Loss。下面这个损失函数可以替换CrossEntropydef tversky_loss(y_true, y_pred, alpha0.7, beta0.3): smooth 1.0 tp (y_true * y_pred).sum() fp ((1 - y_true) * y_pred).sum() fn (y_true * (1 - y_pred)).sum() return 1 - (tp smooth) / (tp alpha * fn beta * fp smooth)参数说明alpha和beta分别控制假阴性和假阳性的惩罚对于椎间盘这类小目标把alpha设得比beta大模型会更倾向于召回小结构而不是把它们并进背景。实测时如果图像里背景占比极高配合按类别频率计算的权重效果更好。每次调完参数要用可视化代码重新叠加预测结果不要迷信指标。6. 把三切面结果融合回3D体空间小目标与边缘修正的进阶处理当你的三切面模型分别训练完毕后如何合成一个完整的3D分割结果一个常见做法是把轴位、冠状位、矢状位的预测概率各自映射回原体素的三个方向然后用多数投票决定每个体素的最终类别。由于同一体素在三个切面中可能被预测成不同类别投票时需要把椎间盘和硬膜囊这类小结构优先级提高如果任意两个切面都预测为类别3即便第三切面预测为背景也要保留类别3。这样做可以显著减少小目标在单一视角下被噪声吞掉的概率。在此基础上我习惯加一步形态学后处理。先对预测结果中的骶骨和腰椎椎骨区域做闭运算填补椎体内部因噪声产生的空洞再用连通成分分析去掉和主区域不连通的孤立点。实际操作中我用SciPy的ndimage.binary_closing配合label就够了不需要复杂的3D卷积网络。处理完后再重新做一次逐类Dice检查你会发现边缘改善幅度不大但背景中的假阳性点能下降好几个百分点。有一个教训值得分享融合前先确认三个切面是否都覆盖了完整解剖范围。某次我的矢状位数据只拍到L3没拍到骶骨融合时该区域必然缺少预测导致骶骨整体缺失。如果你遇到这种情况宁可只用有覆盖的两个切面做融合也不要强行把缺失区域交给模型脑补。最后提醒一句所有后处理参数都要在验证集上调不要根据测试集反复试希望这套落地流程能帮你在三切面分割任务上少走弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑