简介:面向医学图像分割研究者和算法工程师的骶骨腰痛脊椎分割数据集,涵盖轴位面、冠状面、矢状面三个切面,共5个类别,并提供类别说明文件与可视化脚本。图像统一为512×512尺寸,采用医学影像常用窗宽窗位增强处理,切片时已剔除ROI面积不足3%的样本;标签以8位灰度PNG格式存储,便于直接用于模型训练与评估。整个压缩包共2000个文件,其中1998张PNG图片对应CT切片及掩膜,1个TXT文件记录类别映射,1个PY脚本用于快速预览分割效果,包体大小472.47MB。数据集按x、y、z轴分别整理,图片与mask一一对应,x轴2113对、y轴2272对、z轴6404对,结构清晰可直接加载。目前已有226人学习浏览,适合作为脊柱分割、CT影像分析等任务的训练与验证数据。
1. 医学图像分割数据集:骶骨腰痛脊椎分割从哪一步开始最省力
医学图像分割数据集是把CT、MRI里看不见的解剖结构与病灶变成像素级标签的基础资源。标题里这组骶骨腰痛脊椎分割数据,典型形态是:一份NIfTI或PNG格式的原始影像,配一套同分辨率标签文件,外加按轴位、矢状位、冠状位三个切面划分好的子集和可视化代码。它解决的是临床中一个很具体的痛点:腰痛患者的椎间盘退变、骶髂关节炎、神经根压迫,都需要先精确分割出椎体、椎间盘、骶骨和神经结构,才能做后续测量与手术规划。适合用它的人群很明确:不想从零采集影像的医学影像算法工程师、需要快速做基线实验的研究生、以及要验证分割模型泛化性的医疗AI团队。但这类数据集真正值钱的地方,不只是标签本身,而是你读数据时能不能绕过方向轴序这些隐蔽的坑。
2. 三个切面划分与5个类别:读透数据集前最关键的认知
2.1 轴位、矢状位、冠状位:三维影像的三个解剖观察平面
拿到这组数据集,第一件事不是看模型,而是理解“三个切面划分”在医学影像里指什么。MRI和CT原始数据是一个三维体素数组,可以想象成一个由无数小方块堆成的长方体,每个小方块记录一个灰度值。解剖学上习惯用三个互相垂直的平面去裁剪这个长方体,得到临床读片用的二维图像。
轴位,也叫横断面,垂直于人体长轴,从头侧往脚侧看,是CT读取椎体和椎间盘最常用的视角。矢状位从人体侧面切开,能完整看到脊柱前后序列,适合观察腰椎前凸、椎体滑脱和椎间盘突出。冠状位从正前方切开,对观察左右对称结构和骶髂关节很有用。这三个切面在同一份三维数据上切片,得到的二维图像内容完全不同,所以整理型数据集会把它们分开组织,避免用户混淆。
实际数据组织上的差异比想象中大。一个三维volume的shape若是(384, 384, 120),轴位切片数量约120张,矢状位384张,冠状位384张。如果体素间距各向同性,三个方向切片数量很接近;但MRI矢状位序列常常层厚2.5mm、平面内分辨率0.8mm,重采样后矢状位层面数远少于轴位。这有个直接后果:按切面划分的数据集三份子集大小通常不一样,训练时如果不按各切面自身epoch均衡,模型会对切片多的那个切面过拟合,而对切片少的切面欠拟合。
采用3D模型时,三个切面会被网络天然融合,不存在这个问题;但用2D模型按切面训练,椎体在轴位是圆形轮廓,在矢状位是矩形轮廓,同一个网络很难同时拟合两种形态。所以标题里专门标注“3个切面划分”暗含一个使用建议:三个切面最好分开做训练或验证,而不是直接混在一个训练集里。我实际做的时候,一般先按切面各跑一个2D模型,把三个模型对同一病例的预测结果做投票融合,而不是一上来就堆3D网络。
2.2 5个类别标签:不要想当然,先打印np.unique
“5类别”是标签文件里语义类别的数量。最常见设计是0为背景,1到4分别对应四类组织结构。具体到骶骨腰痛场景,不同数据集语义划分差异很大:有的把椎体和椎间盘分开,把骶骨和腰椎分列;有的按节段划分,L3、L4、L5各自一类;还有的会把硬膜囊、神经根、椎管算进去。因此第一步永远是用np.unique打印标签值,并去README或标签说明文件里找类别映射表。标签文件里写的是整数,不会直接告诉你哪个整数代表椎间盘,这个映射信息是使用这套数据的第一道关卡。
如果标签说明缺失,还有一个排查方法:用matplotlib逐个画mask,从颜色和形状猜解剖结构。椎体在轴位上有明显的皮质骨高信号环包围松质骨,椎间盘呈扁椭圆,骶骨呈倒三角形态且位于脊柱末端。靠形态学判断虽然不严谨,但对确认标签语义足够有效。我在以前的项目里遇到过标签说明文件写反的情况,class ID 1写的是椎体,实际mask画出来是椎间盘,这种错位只有靠可视化才能发现。
5个类别的实际意义在于损失函数设计。若按体素数统计,椎体和背景占绝对多数,椎间盘和神经根占比可能只有百分之几,直接用softmax cross entropy,小类别几乎不可能学到。我在项目里给这5类做加权Dice Loss,权重按类别体素占比的倒数归一化,小类权重通常落在5到15区间。如果GPU资源有限、不想调太多超参数,至少用Tversky Loss,把alpha和beta分别设成0.3和0.7,对假阴性更敏感,适合骶骨边界这类小目标。
如果业务只要求“骨性结构 vs 非骨性结构”,完全可以把5类重映射成2类再训练。保留原始5类标签文件不动,在dataloader里做重映射:
def remap_label(label, keep_ids=(1, 2, 3, 4), target_id=1): out = np.zeros_like(label, dtype=np.uint8) for idx in keep_ids: out[label == idx] = target_id return out这里要注意:不要预处理时覆盖原始标签文件,否则后悔药就没了。重映射代码最好写在数据加载阶段,让原始5类标签始终保留在磁盘。参数keep_ids根据你查到的类别映射决定,比如想把椎体和骶骨合并成骨头类,就传对应的两个class id。
2.3 标签文件的两种组织形式:整卷掩码与切片掩码
“标签文件”在不同数据集里指代差别很大。从标题和常见落地方式看,这组数据的标签文件很可能是整卷NIfTI掩码,也可能已被切成单张PNG。这两种组织形式各有适用场景,处理方式完全不同:
| 组织形式 | 典型文件 | 读入方式 | 优势 | 隐患 |
|---|---|---|---|---|
| 整卷掩码 | case001_img.nii.gz + case001_label.nii.gz | nibabel读取整个volume | 保留三维结构,任意切面可重新抽取 | 需要自己写切片逻辑 |
| 切片掩码 | images/case001_sag_123.png + masks/case001_sag_123.png | PIL/OpenCV逐张读取 | 2D训练开箱即用 | 切面轴序被固化,转切面易错 |
判断方法很简单:看每个case目录里label文件有几个。如果只有一个和img同尺寸的同名文件,就是整卷掩码;如果有几十上百个png,就是切片掩码。整卷掩码对重新划分三切面更友好;切片掩码则省去了抽取步骤。我一般倾向拿到手先转成整卷nii的中间形态,哪怕原始文件是png。原因很现实:以后换模型换切面,只需从nii重新切一遍,不用去找png命名对应关系。
还有一个细节:有些整理型数据集会另放一个label_info.yaml或classes.txt,里面写每个class id对应的解剖名称。这个文件就是标签文件的文档,务必在训练前读一遍。如果里面写的类别数和标题“5类别”对不上,先不要怀疑自己,可能是组织版本不一致,去README的历史版本记录里查更新说明。
3. 读入数据集并核验标签文件:最小脚本与轴序确认
3.1 先确认目录结构,再决定用哪种读取管线
解压数据集以后,最忌讳一上来就glob所有文件进训练脚本。医学影像数据集最常见的翻车点就是目录结构比想象中复杂。正确顺序是:先看顶层目录,再用脚本统计文件类型和数量,最后边读边检查文件命名。常见目录结构有:
- 按case组织:case001/ 下放 img.nii.gz 和 seg.nii.gz,case002/ 同理。
- 按切面组织:axial/、sagittal/、coronal/ 三个文件夹,每个文件夹下包含对应切片图与标签。
- 按训练/验证组织:train/、val/、test/ 下再套上述两种结构。
用tree列出两层目录后,还要统计每个子目录下文件数量是否吻合三切面预期。一条bash命令就能完成扩展名统计:
find . -type f | sed 's|.*\.||' | sort | uniq -c这个命令把所有文件扩展名提取出来计数。如果输出里既有nii.gz又有png,说明可能是混合交付,训练脚本要分别处理。如果某切面目录下文件数量是0,说明交付时漏了该切面,不能默认“三个切面都能用”。
这步做完还有一个容易漏的检查:文件名里的切面标记是否统一。有的数据用ax,有的用axial,有的用t1_ax_001.png这种带序列名的格式。我一般会把所有文件名扫一遍,找出所有不匹配规律的文件,单独列出来人工确认。尤其是那种把矢状位误命名为“cor”的文件,一旦混进训练集,等于给模型喂错误标签。
3.2 NIfTI读取、标签unique值检查与orientation标准化
读取NIfTI时,我习惯把图像读取、标签检查和方向标准化写在一个体检脚本里,每次拿到新数据先跑一遍。最小代码像这样:
import numpy as np import nibabel as nib img_nii = nib.load("case001_img.nii.gz") seg_nii = nib.load("case001_label.nii.gz") img = img_nii.get_fdata() seg = seg_nii.get_fdata().astype(np.uint8) print("image shape:", img.shape) print("seg unique:", np.unique(seg)) # 标签文件里到底有哪些类别 print("voxel spacing:", img_nii.header.get_zooms())get_zooms返回体素间距,单位是毫米。这个信息在后面重采样和物理坐标换算时非常重要。seg转成uint8是为了后续保存和计算Dice时不出现类型不匹配;float类型的mask在做np.sum时不会报错,但数字精度和显存占用都更差。unique输出应该是0到4的连续整数,如果发现跳号,比如只有0、1、2、4而没有3,说明标签文件可能有缺失或压缩损坏。
方向标准化是NIfTI读取里最容易被忽视的一步。NIfTI的affine矩阵记录了体素数组到解剖坐标的映射,直接按数组索引取切片,图像可能被旋转或翻转。统一到RAS坐标系的标准做法是:
from nibabel.orientations import axcodes2ornt, apply_orientation def to_ras(vol, affine): ornt = nib.orientations.io_orientation(affine) # 当前方向 ras_ornt = axcodes2ornt(("R", "A", "S")) # 目标方向 transform = nib.orientations.ornt_transform(ornt, ras_ornt) return apply_orientation(vol, transform), transform这段代码不重采样,只是做数组翻转和转置,把方向统一成RAS后,img和seg做同样的变换,两者仍保持体素级对齐。以后所有切片抽取都基于标准化后的数组,就不会出现“轴位图像左右翻转”这种低级错误。
3.3 DICOM序列读取与SeriesInstanceUID匹配
另一类常见情形是图像给的是DICOM原始序列,标签文件却是NIfTI。这时不能用SimpleITK的默认series reader一把梭,因为一个目录下可能混着多个序列:定位像、T1、T2各有各的SeriesInstanceUID。正确做法是先按UID分组,再逐组读取。
import SimpleITK as sitk reader = sitk.ImageSeriesReader() # 先列出目录下所有序列UID,确认目标序列 uids = reader.GetGDCMSeriesIDs("/path/to/dicom_dir") print("available series:", uids) dicom_names = reader.GetGDCMSeriesFileNames( "/path/to/dicom_dir", seriesID=uids[0], useSeriesDetails=True ) reader.SetFileNames(dicom_names) image = reader.Execute() # SimpleITK Image,含spacing和direction参数useSeriesDetails=True会让匹配更严格,把同一序列的多个子序列区分开,避免合并错乱。读取后必须检查image.GetSpacing(),因为DICOM头部的spacing单位是毫米,且z方向层间距可能在不同序列间差别很大。如果直接按numpy索引切片而忽略spacing,训练出来的模型对物理世界中的椎体大小没有概念,换一台扫描设备可能直接失效。
这里有一个容易翻车的点:SimpleITK把numpy数组按z、y、x顺序排列,和NIfTI的x、y、z顺序刚好相反。同样的代码从NIfTI读和从DICOM读,取第三维索引含义完全不同。我在项目里会用SimpleITK的GetDirection和origin打印出来,和NIfTI的affine对齐一次,确认空间位置匹配后再做后续处理,避免两个模态的数据在混合训练时轴序错乱。
3.4 从整卷掩码抽三切面并保存索引CSV
如果你拿到的是整卷NIfTI,下一步就是按三个切面抽片,并把切片索引保存下来。常见做法是写一个抽取函数,统一处理三个方向:
def extract_slice(vol, mask, axis, idx): if axis == "axial": return vol[:, :, idx], mask[:, :, idx] elif axis == "sagittal": return vol[:, idx, :], mask[:, idx, :] elif axis == "coronal": return vol[idx, :, :], mask[idx, :, :] else: raise ValueError("axis must be axial/sagittal/coronal")抽取后建议把所有切片导出成npy或png,同时生成一个CSV记录每个切片的来源索引和非空标签状态:
import csv, os meta_path = "slice_index.csv" with open(meta_path, "w", newline="") as f: writer = csv.writer(f) writer.writerow(["case_id", "axis", "slice_idx", "has_label"]) for axis in ["axial", "sagittal", "coronal"]: num = {"axial": img.shape[2], "sagittal": img.shape[1], "coronal": img.shape[0]}[axis] for idx in range(num): img_s, seg_s = extract_slice(img, seg, axis, idx) has_label = int(np.count_nonzero(seg_s) > 0) writer.writerow([case_id, axis, idx, has_label])这个CSV在训练时的价值很大:可以按has_label过滤切片,也可以按axis统计各切面的标注覆盖密度。很多医学分割数据集只标注了部分切片,没有这个索引文件,你训练时只能全量读入,再在内存里做判断;有了CSV,数据加载阶段就能直接跳过无标注切片,IO和显存开销都小很多。npy格式保存的切片读起来比每次从nii.gz切快不少,代价是磁盘占用变大,适合以2D训练为主的项目。
4. 可视化代码跑通三切面:叠加mask、颜色映射与批量导出
4.1 方向统一后,再做灰度窗口调整
可视化不是为了好看,是为了用眼睛快速发现标签和图像的对齐错误。读入图像后第一步是检查方向;第二步是调整灰度显示范围。CT图像的HU值范围巨大,脊柱CT可能在-1024到+3000HU,不加窗直接imshow,看到的只是一团白或一团黑。MRI没有标准HU值,但也需要按百分位截断。
p_low, p_high = np.percentile(img_slice, [0.5, 99.5]) img_slice_clipped = np.clip(img_slice, p_low, p_high)percentile取0.5和99.5是在多数医学影像上的稳妥区间,能压掉极端的金属伪影和空气噪声。不要用0和100,那等于没截断。clip只影响显示,不改变mask,也不改变训练数据。如果数据集本身就是已经归一化好的npy,比如0到255的uint8,这步可以省略;但可视化脚本里保留clip无害,训练pipeline里则要保持和可视化一致的数据预处理,避免可视化与训练看到不同灰度分布。
4.2 固定类别颜色映射,避免连续colormap误导
多类别mask如果用matplotlib默认的viridis,第一类低亮度、第五类高亮度,人眼会误把类别值当成强度。正确做法是自定义离散颜色表,让每个类别从视觉上就能区分:骨骼用黄色系、椎间盘用蓝色系、神经根用绿色系,背景始终透明。
CMAP = { 0: (0, 0, 0, 0), # 背景透明 1: (1.0, 0.8, 0.2, 0.5), # 黄 2: (0.2, 0.6, 1.0, 0.5), # 蓝 3: (0.3, 0.9, 0.3, 0.5), # 绿 4: (0.9, 0.3, 0.7, 0.5), # 紫 }key是标签文件里的class id,value是RGBA四元组。用matplotlib的ListedColormap把这个dict转换成可用的colormap,再传给imshow。alpha统一0.5,既能看到底下的解剖细节,又不会让类别边界发虚。若某个类别占比过低,比如第四类只有几百个像素,叠加在图上几乎看不清,可以单独对该类别生成一个二值mask放大显示,而不是提高全局alpha,否则其他类别会糊成一片。
实际调试中我发现一个规律:当mask边缘和图像灰度边缘错开两三个像素时,不需要去算指标,肉眼就能看出来。所以可视化代码里不要加平滑或插值,imshow的interpolation必须设成"none",否则mask边缘被平滑后,本来对不齐的问题会被糊掉,看起来反而“差不多对齐了”。
4.3 一次性输出三切面九宫格并批量导出
单张单张画图效率太低。我在项目里常写一个自动选层函数,从每个切面里挑出“非空标签面积最大”的三张切片,拼成3行3列的九宫格,一次输出一个case的完整概览。
def pick_best_slices(seg, axis, k=3): scores = [] num = {"axial": seg.shape[2], "sagittal": seg.shape[1], "coronal": seg.shape[0]}[axis] for idx in range(num): img_s, seg_s = extract_slice(seg, seg, axis, idx) # 仅取mask count = np.count_nonzero(seg_s) if count > 0: scores.append((count, idx)) scores.sort(reverse=True) return [idx for _, idx in scores[:k]]这里有个偷懒的小技巧:extract_slice的第一个参数传seg而不是img,因为选层只看标签面积,没必要读图像数据。选完层后再从原始volume里抽图像,能省一次IO。九宫格用matplotlib的subplots(3, 3, figsize=(12, 12))排版,每个子图显示一个切面的一张切片。figsize设成12x12在普通屏幕上能完整显示,字体也够大。
批量导出时固定三个参数:dpi=200、bbox_inches="tight"、facecolor="black"。dpi太低时压缩伪影明显,太高文件体积大;facecolor设成黑可以让灰度图像视觉更接近阅片环境。命名规则建议用case001_axial_123.png这种带切片索引的格式,方便回溯到原始volume的具体位置。
4.4 用交互式滑块快速做单病例三切面检查
脚本稳定后,在Jupyter或PyQt上可以加一个滑动条交互式浏览切片,这对快速浏览一个case所有标注特别有用。核心代码是基于matplotlib的Slider:
from matplotlib.widgets import Slider def browse_case(img, seg, axis): num = {"axial": img.shape[2], "sagittal": img.shape[1], "coronal": img.shape[0]}[axis] fig, ax = plt.subplots(figsize=(8, 8)) plt.subplots_adjust(bottom=0.15) initial_img, initial_seg = extract_slice(img, seg, axis, num // 2) im = ax.imshow(initial_img, cmap="gray", interpolation="none") overlay = ax.imshow(np.ma.masked_where(initial_seg == 0, initial_seg), cmap=my_colormap, alpha=0.5, interpolation="none") slider_ax = plt.axes([0.2, 0.05, 0.6, 0.03]) slider = Slider(slider_ax, "slice", 0, num - 1, valinit=num // 2, valstep=1) def update(val): idx = int(val) img_s, seg_s = extract_slice(img, seg, axis, idx) im.set_data(img_s) overlay.set_data(np.ma.masked_where(seg_s == 0, seg_s)) fig.canvas.draw_idle() slider.on_changed(update) plt.show()参数valstep=1保证滑块每次移动一个整数切片,避免浮点索引导致的重复显示。这个交互式查看的实用价值在于:一次拖完所有切片,能快速发现标签文件里哪些层面没有标注、哪些层面标注明显画错。如果一个case几乎每张切片都全黑,那就要怀疑标签文件读入方向出错了。
5. 骶骨腰痛分割数据集使用避坑:切面混淆、稀疏标注与类别权重
5.1 切面混淆:轴位当成了矢状位,模型学到的全是翻转特征
现象:train loss下降正常,验证集Dice却不低,但可视化结果里mask整体偏移半个椎体,边缘像碎纸片。重新检查数据发现训练图像全部来自sagittal文件夹,而你自己以为拿的是axial。
原因:数据集目录命名不统一,有的叫sag,有的叫sagittal;或者按文件名排序时被字符串排序打乱,导致无法对应到解剖切面。更深层的原因是分割模型训练阶段没有校验图像和mask的orientation是否一致,2D网络不具备三维旋转不变性。
解决:在训练pipeline里加一道orientation检查函数,读入图片时统一转成RAS坐标系,并重新生成三个切面,不直接信任原始目录分类。我习惯把第一次生成的mask和第二次生成的mask用np.array_equal比较,不一致就打印warning。更稳妥的办法是把三切面目录的名字规范成ax/sag/cor三种,统一命名规则后再进训练。
5.2 标签稀疏:一个三维volume里只有部分切片有标注
现象:训练时卡顿不明显,但模型很快过拟合;可视化时发现大量切片mask全为0,只有病灶附近少数切片有标注。某些切面抽取后,非背景像素占比低于0.1%。
原因:医学分割数据集为了降低标注成本,常常只标注部分层面,而非整个volume。标题里的“标签文件”可能只覆盖每5层里选1层,或者只覆盖一个ROI区域。直接拿全切片训练,模型会大量看到“只有背景”的样本,优化方向被带偏。
解决:先扫描CSV里has_label字段,统计每个切面的标注覆盖比例,只取有标注的切片做监督训练。无标注切片要么丢弃,要么作为无监督一致性正则的输入。我一般把min_foreground_ratio设成0.005,小于这个值的切片直接不进训练集。如果你只有200张有标注切片,batch size就不要开大,32以内比较稳,否则每个batch里几乎没有前景样本。
5.3 类别不平衡:5个类别里有两个类几乎从不出现
现象:按类别统计Dice时,背景和大的椎体轻松0.95,椎间盘0.5,神经根只有0.1。整体Dice看“还不错”,真实临床场景根本不能用。
原因:多类别标签按体素数分布极不均匀。常规softmax交叉熵默认每个体素权重相同,小类别处在梯度消失边缘,尤其当它们出现在椎体边缘,很容易被邻近大类别吃掉。医学分割里更隐蔽的是,单类Dice上去了,但混淆发生在椎间盘和骶骨软骨之间,解剖边界本来就是连续的,肉眼难以察觉。
解决:损失函数层面,用类别加权Dice Loss或Tversky Loss,权重按体素占比的倒数归一化。如果神经根太小,权重给到5到10也不夸张。数据层面,对稀有类别所在切片做过采样,让每个epoch里稀有类别切片重复2到4次。训练后验证时只report按类别的Dice,不要只报平均Dice,否则等于被背景类洗地。
5.4 方向翻转导致标签错位:图像数组是LAS,标签文件却是RAS
现象:可视化看单张mask和原图对得上,但叠加后发现mask总在图像左边偏移10个像素;沿着切面扫描时,第20张才和原图第18张重合。
原因:NIfTI的affine记录了方向信息,但部分处理脚本直接丢弃affine,用numpy的flip或rot90重排数组,却没有同步更新方向信息。或者图像用了LAS方向,标签用了RAS方向,两者叠加时不翻转一致就会偏。
解决:统一用nibabel的orientation把img和seg转成同一个标准方向,再做切片。不要用image[:, ::-1]这种直接翻转操作,除非你能同时改写affine。验证方法很朴素:找一张标签里明显能看到左右不对称的结构,比如侧隐窝或神经根管,翻转方向看左右是否错位。一次翻转看不出问题,三个切面各抽一张对比就能暴露。
5.5 不要为了复用检测工具箱强行转换标签格式
现象:有人为了把这份医学分割数据塞进现有的目标检测框架,比如用YOLO系列训练自己的数据集,就把切片mask转成polygon格式,结果训练出来轮廓全是锯齿,模型还时不时把一个椎体拆成两半。
原因:医学分割标签是像素级密集掩码,而检测框架的标注是稀疏框或多边形。椎体和椎间盘在相邻切片上形态连续变化,转成polygon后丢失了大量边界细节,尤其骶骨这种不规则形态,polygon的顶点数永远不够。标签文件格式转换本身没错,错在场景不匹配。
解决:如果一定要用检测框架,只把mask转成bbox做检测辅助任务,不能当成分割训练的监督信号。真正做分割还是在nnU-Net、MONAI这类医学分割框架里跑。这个坑见过太多次,本质上是想省写数据加载代码的时间,结果花在调试格式兼容上的时间多十倍。
6. 进阶:三切面数据如何组织训练与快速验证Dice
三个切面都训练还是只取一个切面?我的结论是先用轴位跑通基线,轴位层数通常最多、标注覆盖最全。轴位数据量不够,再把矢状位和冠状位作为补充。最终验证时,同一组测试集三个切面分别预测,取Dice最高的那个切面结果作为该case的最终输出。
Dice计算是分割验证里最常用的指标,但写不好会虚高。这个版本是我一直在用的:
def dice_score(pred, gt, smooth=1e-6): pred = (pred > 0.5).astype(np.uint8) gt = (gt > 0.5).astype(np.uint8) intersection = np.sum(pred * gt) union = np.sum(pred) + np.sum(gt) return (2 * intersection + smooth) / (union + smooth)先二值化再算,不要用浮点概率直接算,否则结果虚高。smooth取1e-6防止0除以0,但要注意:对全背景切片,pred和gt都是0,这行代码输出1.0,会让验证集Dice被空标签污染。所以验证脚本必须过滤掉没有前景的切片,只对有标签切片计算Dice,并单独报出“有标签切片数”和“空标签切片数”,这两个数能反映数据质量。
训练管线最后值得做的一件事,是按切面分别配置数据增强:轴位允许上下翻转,矢状位禁止左右翻转,冠状位限制旋转角度。理由很直接:解剖结构有方向性,矢状位翻转会让左右神经根位置全乱。我在真实项目中因为随意翻转矢状位导致验证Dice下降约5个百分点,最后把这个教训记进了数据检查清单,每次换数据集都先确认增强策略里有没有违反解剖对称性的操作。
这套骶骨腰痛数据集最适合的落地方式是先跑通轴位2D基线,验证数据链路无误后,再上3D模型或跨切面融合。可视化输出永远是第一验证手段:随机抽20个切片人眼比对,再算统计指标。很多翻车不是模型不行,而是轴序、标签文件、类别命名这些前置步骤里出了一处小错。希望这些踩坑经验能帮到你,让你在这套三切面医学图像分割数据上少走一段弯路。
本文还有配套的精品资源,点击获取