ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

三切面五类别骶骨腰痛数据集构建与医学图像分割实战指南

三切面五类别骶骨腰痛数据集构建与医学图像分割实战指南 简介面向医学影像分割与腰痛脊椎分析任务这份数据集以CTSpine1K为基础按轴位面、冠状面、矢状面三个方向切分出2D图像剔除ROI占比不足3%的切片后经windowing对比度增强并统一缩放为512×512。图像为jpg标签为png8bit灰度、uint8共5个类别非0区域为前景、0为背景类别定义见classes.txt。压缩包共2000个文件以1998张png标签图为主附带1个类别说明txt和1个可视化脚本show.py整体大小472.47MB。数据集按x、y、z三个切面分别组织对应2113、2272、6404组图像与mask方便按需加载。已有224人浏览学习。借助可视化代码可快速检查mask与原始影像的叠加效果适合入门或进阶医学图像分割、需要多切面训练样本的研究者直接使用。1. 为什么拿到骶骨腰痛数据要先按3个切面重新划分做骶骨腰痛相关的医学图像分割时最头疼的往往不是模型结构而是数据集本身。这个标题里的医学图像分割数据集核心不是「有一堆图」而是把同一个人同一套扫描重建成矢状面、冠状面、横断面三个切面每个切面都配上像素级5类别标签再送你一份可视化代码。没有这套东西你连「数据长什么样」都说不清后面训练、调参、写论文全都踩在沙子上。我处理过几套腰椎、骶骨相关的分割数据这类任务天然适合按切面拆开矢状面看腰椎序列曲度和滑脱横断面看椎间盘与神经根压迫冠状面看侧弯和骶髂关节。与其让模型在三维体素里盲人摸象不如先把三个方向的图像分别喂给分割网络每个方向都有自己的分辨率、间距和感受野设定。这篇就按「数据组织 → 标签处理 → 可视化 → 踩坑 → 验证」的顺序把一套能直接落地的做法讲清楚。新手照着能跑通第一个实验熟手可以重点看第5章的边界条件和第6章的评估口径。2. 3个切面5个类别怎么装进文件夹目录约定与切面重采样2.1 为什么医学图像分割的数据集要按切面重采样医学图像原始扫描通常是从CT或MRI设备输出的DICOM序列体素是各向异性的横断面层内分辨率往往是0.5mm左右层间距却可能是1mm、2mm甚至5mm。这种数据直接做三维分割横断面方向的感受野和其他两个方向完全不对称模型很容易学到方向相关的伪影。常见做法是先按三个切面把体素重采样成接近各向同性再逐一切片保存。矢状面适合看L1到S1的整体序列主要关注椎体前缘连线、滑脱程度横断面适合看椎间盘、硬膜囊和神经根出口冠状面适合看椎体侧方和骶髂关节。三个切面的重采样间距不完全一样我一般按下面的参数做默认值切面重建方向推荐重采样间距主要观察目标矢状面Sagittal沿左右方向逐层切片0.81.0mm 等间距腰椎序列、椎间盘高度、骶骨倾斜角横断面Axial沿头脚方向逐层切片层内0.60.8mm椎间盘、硬膜囊面积、神经根压迫冠状面Coronal沿前后方向逐层切片1.01.5mm侧弯、骶髂关节、椎体横向偏移重采样工具上我习惯用SimpleITK。它读NIfTI或DICOM后可以直接拿到spacing和direction矩阵做Resample时不至于把方向搞反。重采样有两个参数必须显式指定一个是输出spacing一个是对齐原点。很多人只改spacing不改origin结果切面重建出来整体平移了一个体素最后标签和原图悄悄错位。2.2 从原始扫描到三切面数据集的目录编排拿到原始扫描后先统一格式我一般全部转成NIfTI再处理。原始DICOM转NIfTI用dcm2niix最省事一条命令就能把整个序列导出还能顺带生成JSON侧car文件记录spacing和方位信息。注意转换时加上-o指定输出目录-f指定文件名模板不然文件名会是乱序数字后面配对标签时非常痛苦。dcm2niix -z y -o ./nifti_raw -f %p_%s ./dicom_input转换完成后NIfTI的spacing、origin、direction都写进了文件头接下来用SimpleITK读进来做切面重采样。这里有个很关键的约定切面方向是按解剖方位命名的不是按数组维度命名的。横断面在NIfTI数组里通常是第2维i方向和j方向构成的平面矢状面是第0维或第1维具体取决于direction矩阵。写代码时不要写死img_array[0]是矢状面一定要先打印img.GetDirection()确认。import SimpleITK as sitk img sitk.ReadImage(nifti_raw/patient01_T1.nii.gz) print(Spacing:, img.GetSpacing()) print(Direction:, img.GetDirection()) # 统一重采样到各向同性这里以矢状面为例 resampler sitk.ResampleImageFilter() resampler.SetOutputSpacing([1.0, 1.0, 1.0]) resampler.SetSize([0] * 3) # 下面根据spacing计算 original_spacing img.GetSpacing() original_size img.GetSize() new_size [ int(round(original_size[0] * original_spacing[0] / 1.0)), int(round(original_size[1] * original_spacing[1] / 1.0)), int(round(original_size[2] * original_spacing[2] / 1.0)), ] resampler.SetSize(new_size) resampler.SetOutputOrigin(img.GetOrigin()) resampler.SetOutputDirection(img.GetDirection()) resampler.SetInterpolator(sitk.sitkLinear) resampled resampler.Execute(img)参数这里要注意的是矢状面重采样到1mm等间距主要是为了让后续能直接按切片顺序做组间对比横断面我会保留层内0.60.8mm因为椎间盘和神经根这些小结构在高分辨率下边界更清楚。重采样的插值方式只对图像用sitkLinear对标签图必须换sitkNearestNeighbor否则会出现新的灰度类别这个在第5章踩坑里细说。重采样完成后按切面拆切片保存。目录结构我强烈建议从一开始就这么定别等标注完了再改dataset_root/ ├── images/ │ ├── axial/patient01_000.png │ ├── axial/patient01_001.png │ ├── coronal/patient01_000.png │ ├── sagittal/patient01_000.png ├── labels/ │ ├── axial/patient01_000.png │ ├── coronal/patient01_000.png │ ├── sagittal/patient01_000.png └── visualization/ ├── overlay/ └── slice_browser.pyimages和labels用完全相同的文件名这样训练时按文件名配对就行。每个切面单独一个子目录方便后面用不同的预处理参数。不要把所有切片混在一个目录里——骶骨腰痛任务里横断面和矢状面的类别分布差异非常大分开目录才能对症下药。2.3 5个类别到底分割什么标签值与解剖结构的对应关系标题里明确了5类别结合骶骨腰痛的实际临床需求常见的类别划分是腰椎椎体、骶骨、椎间盘、椎管硬膜囊和棘突附件。背景通常是01到5分别给前五类。这个划分的好处是腰椎椎体和骶骨是退行性疾病的主要承载结构椎间盘是腰痛的核心病灶椎管直接影响神经压迫症状棘突附件则是手术规划的参考点。标签值结构分割难点0背景与软组织相近容易误检1腰椎椎体L1-L5边缘骨皮质与骨髓信号差异大2骶骨与髂骨连接处边界模糊3椎间盘体积小、类别不平衡严重4椎管/硬膜囊低信号区域对比度差5棘突/附件形态不规则标签面积小在NIfTI标签文件里每个体素存一个0到5的整数保存在一张灰度PNG里或一个三维NIfTI里都行。关键是要保证标签与图像有完全相同的shape、spacing、origin和direction。很多人图省事把标签存成RGB图后面转灰度时颜色映射错乱属于给自己挖坑。我的习惯中间过程全部用NIfTI保留空间信息只在训练前按切片导出为PNG这样最不容易丢坐标。3. 标签文件怎么处理才不翻车格式、对齐与类别合并3.1 灰阶标签图与One-Hot编码的选择标签文件最稳妥的存储格式是单通道灰度图每个像素的值就是类别编号。不要存成三通道的彩色PNG也不要存成JPEGJPEG压缩会在边缘生成不存在的灰度值轻则训练时报错重则把错误类别直接教给模型。横断面、矢状面、冠状面各切一张图就是标题配的「3个切面划分的数据集」的常见形态。有些公开数据集喜欢把标签做成One-Hot编码也就是5个类别的mask分5张图存每张图只存0和1。这种做法不是不行但要注意很多分割网络直接读单通道标签图你喂5通道的One-Hot进去输出层要跟着改推理时还要做argmax流程上多绕一圈。我一般只在做多任务或者类别之间有重叠时才用One-Hot常规的骶骨腰痛分割用单通道整数图就够了。还有一个容易忽略的细节标签文件的位深。0到5这个取值范围用uint8足够但别用有符号的int8有的框架读int8时会把大于127的值当负数5不会出错但标注工具偶尔会写出255这种值一但出现int8读出来是-1损失的是一次训练。规规矩矩用np.uint8保存和读取这是最没有歧义的做法。3.2 把多类别标注合并成5类的批处理脚本标注工具导出的标签往往比5类多比如把L1到L5分开标成了5个类别加上骶骨就是6类。这种情况需要合并。下面这个脚本就是把腰椎椎体合并、保留骶骨、处理椎间盘和椎管的通用写法。核心思路是先读标签数组再用numpy的索引重新映射。import numpy as np import SimpleITK as sitk def merge_labels(label_array): 将细粒度标签合并为5类 背景。 约定L1-L5椎体 - 1骶骨 - 2椎间盘 - 3 椎管/硬膜囊 - 4棘突/附件 - 5其余 - 0 merged np.zeros_like(label_array, dtypenp.uint8) # 假设原始标签: 1,2,3,4,5是L1-L5椎体, 6是骶骨, # 7-10是椎间盘, 11是椎管, 12是棘突 merged[label_array 1] 1 merged[label_array 2] 1 merged[label_array 3] 1 merged[label_array 4] 1 merged[label_array 5] 1 merged[label_array 6] 2 merged[label_array 7] 3 merged[label_array 8] 3 merged[label_array 9] 3 merged[label_array 10] 3 merged[label_array 11] 4 merged[label_array 12] 5 return merged img sitk.ReadImage(training_labels/patient01_label.nii.gz) label_arr sitk.GetArrayFromImage(img) # shape: (z, y, x) merged_arr merge_labels(label_arr) merged_img sitk.GetImageFromArray(merged_arr) merged_img.CopyInformation(img) # 保留空间信息 sitk.WriteImage(merged_img, training_labels/patient01_label_5class.nii.gz)代码逻辑说明合并的映射关系必须和标注协议一一对应如果你的标注工具把L1到L5分别标为101到105就把条件改成label_array 101。最关键的一行是CopyInformation(img)它把原图的origin、spacing、direction原样带进新标签这一步漏了后面所有配准都会串位。合并完成后一定要跑一遍类别直方图确认0到5六个值都有且没有被误合并成别的数字。参数说明合并脚本的输入是NIfTI输出也是NIfTI。这个阶段不要把标签切成分片保存因为NIfTI在三维层面能保持解剖结构完整性便于做空间一致性检查。等训练前再按切片切分这样如果你某个切面的标签错了还能回到三维体素里重新来。3.3 标签与图像的空间对齐spacing、origin、direction三个参数医学图像分割数据集最常见的翻车现场就是图像和标签明明看着形状一样叠加后却像是两张图在错位。这个问题十有八九出在spacing、origin、direction三个参数的某一项上。图像重采样了但标签没重采样spacing自然不一致两个文件来自不同次扫描origin差了几个毫米dcm2niix转换时direction不同导致图像是RAS方向而标签是LPS方向叠加时左右翻转。我在拿到任何一组图像和标签时第一件事不是看一眼效果而是打印四个值img sitk.ReadImage(images/sagittal/patient01.nii.gz) label sitk.ReadImage(labels/sagittal/patient01.nii.gz) print(img :, img.GetSize(), img.GetSpacing(), img.GetOrigin(), img.GetDirection()) print(label :, label.GetSize(), label.GetSpacing(), label.GetOrigin(), label.GetDirection())如果size、spacing、origin、direction四项有任何一项不一样绝对不要强行用先把label重采样到与图像一致再做后续。重采样标签用最近邻插值这段代码应该是所有医学图像分割项目里的常驻工具def resample_label_to_image(label, ref_img): resampler sitk.ResampleImageFilter() resampler.SetReferenceImage(ref_img) # 直接以图像为空间模板 resampler.SetInterpolator(sitk.sitkNearestNeighbor) return resampler.Execute(label)这里只说一点SetReferenceImage把图像的所有空间属性全部套到label上不需要你手动指定size和spacing是最不容易出错的写法。唯一要注意的是它不会重采样灰度值所以只适用于标签图用在CT图像上会把密度值插出奇怪的伪影。4. 可视化代码最小实现叠mask、存PNG、查穿层4.1 最小可用的可视化脚本数据集里配套的可视化代码通常要解决两个问题一是把分割结果叠加原图看一眼合不合理二是快速把某个病人的全部切片导出来便于发现标注错误。下面这个脚本就是典型的超轻量实现只依赖numpy、SimpleITK和matplotlib。它读取一个NIfTI图像和对应标签在三个切面方向各取中间几张切片把标签作为彩色半透明mask叠加在灰度图上保存为PNG。import numpy as np import SimpleITK as sitk import matplotlib.pyplot as plt def visualize_overlay(image_path, label_path, output_path, slice_idxNone, opacity0.4): img sitk.ReadImage(image_path) label sitk.ReadImage(label_path) arr sitk.GetArrayFromImage(img) # (z, y, x) lab sitk.GetArrayFromImage(label).astype(np.uint8) # 归一化图像到[0, 255] arr arr - arr.min() arr arr / (arr.max() 1e-8) * 255.0 # 默认取三个方向的中间切片 if slice_idx is None: slice_idx { axial: arr.shape[0] // 2, coronal: arr.shape[1] // 2, sagittal: arr.shape[2] // 2, } colors np.array([ [0, 0, 0], # 背景 [1, 0, 0], # 椎体 [0, 1, 0], # 骶骨 [0, 0, 1], # 椎间盘 [1, 1, 0], # 椎管 [1, 0, 1], # 棘突 ], dtypenp.float32) fig, axes plt.subplots(1, 3, figsize(15, 5)) views [] # 横断面: (z, y, x) 取第z层显示y-x平面 z slice_idx[axial] views.append((arr[z, :, :], lab[z, :, :], Axial z str(z))) # 冠状面: 取第y层显示z-x平面 y slice_idx[coronal] views.append((arr[:, y, :], lab[:, y, :], Coronal y str(y))) # 矢状面: 取第x层显示z-y平面 x slice_idx[sagittal] views.append((arr[:, :, x], lab[:, :, x], Sagittal x str(x))) for ax, (img_slice, lab_slice, title) in zip(axes, views): img_display np.stack([img_slice] * 3, axis-1) mask colors[lab_slice] alpha (lab_slice 0).astype(np.float32) * opacity overlay img_display * (1 - alpha[..., None]) mask * alpha[..., None] ax.imshow(overlay.astype(np.uint8)) ax.set_title(title) ax.axis(off) plt.tight_layout() plt.savefig(output_path, dpi150) plt.close() visualize_overlay( images/sagittal/patient01.nii.gz, labels/sagittal/patient01_5class.nii.gz, visualization/patient01_overlay.png )代码逻辑说明sitk.GetArrayFromImage返回的数组维度顺序是(z, y, x)也就是第一个维度是横断面方向中间是冠状面方向最后是矢状面方向。很多人在这一步把维度搞反把横断面当成矢状面来看看半天还觉得标签没毛病。切片索引slice_idx是三个方向各取一层的默认设定实际使用时建议传一个列表把关键解剖位置都扫一遍。colors数组的索引直接对应标签值1到5背景全透明其余类别分别用不同颜色标出。参数说明opacity控制mask透明度0.4在多数情况下既能看清覆盖区域又不会遮住底层解剖结构如果是对比图可以调到0.2。dpi150导出的图够写报告用不用为了图清晰而无限调大否则上百张切片会等得血压升高。这段代码可以直接引用成数据集配套的可视化代码稍微改造就能用于推理结果评估。4.2 用切片浏览器检查标签有没有穿层一个NIfTI文件几十层到几百层不等用上面那张三视图只能看三个中间层骶骨腰痛这种任务里病灶往往在L4-S1区域中间层未必是重点。更实际的做法是写一个简单的切片浏览器用鼠标滚轮切换切片逐层看。这里用matplotlib的event接口就能实现不必上专门标注软件。import matplotlib.pyplot as plt import SimpleITK as sitk import numpy as np class SliceBrowser: def __init__(self, image_path, label_pathNone, axis0): self.img sitk.GetArrayFromImage(sitk.ReadImage(image_path)) self.lab sitk.GetArrayFromImage(sitk.ReadImage(label_path)) if label_path else None self.axis axis self.idx self.img.shape[axis] // 2 self.fig, self.ax plt.subplots(figsize(8, 8)) self.fig.canvas.mpl_connect(scroll_event, self.on_scroll) self.fig.canvas.mpl_connect(key_press_event, self.on_key) self.update() def get_slice(self, data, idx): if self.axis 0: return data[idx, :, :] elif self.axis 1: return data[:, idx, :] else: return data[:, :, idx] def update(self): self.ax.clear() img_slice self.get_slice(self.img, self.idx) img_slice (img_slice - img_slice.min()) / (img_slice.max() - img_slice.min() 1e-8) self.ax.imshow(img_slice, cmapgray) if self.lab is not None: lab_slice self.get_slice(self.lab, self.idx) self.ax.imshow(lab_slice, cmapjet, alpha0.4, vmin0, vmax5) self.ax.set_title(fAxis {self.axis}, Slice {self.idx}/{self.img.shape[self.axis] - 1}) self.fig.canvas.draw() def on_scroll(self, event): if event.button up: self.idx min(self.idx 1, self.img.shape[self.axis] - 1) elif event.button down: self.idx max(self.idx - 1, 0) self.update() def on_key(self, event): if event.key a: self.axis (self.axis 1) % 3 self.idx min(self.idx, self.img.shape[self.axis] - 1) self.update() SliceBrowser(images/axial/patient01.nii.gz, labels/axial/patient01_5class.nii.gz) plt.show()这段代码比上一段更接近「干活」的形态滚轮逐层扫按a键在三个切面之间切换。判断标签穿层主要靠扫的时候眼盯四件事一是椎体和骶骨是否连接在一起二是椎间盘是否被错误标进椎体区域三是横断面上椎管是否连续四是切片最上下边缘有没有出现孤立杂乱的小色块。如果某个类别的区域在连续几层间突然断开又出现多半是标注断层不是模型的问题。4.3 三切面同一解剖点如何联动核对分割标注质量检查里最容易被忽视的是跨切面一致性。横断面显示L4椎体的中心位置转到矢状面应该是同一个病人的L4上缘如果两个切面在解剖结构上对不上多数是重采样或标注时某个环节改变了origin。联动核对的做法不复杂在三个切面各取指定索引锁定同一个体素坐标然后对比三张图上该点的结构。import numpy as np # 假设体素坐标 (z, y, x)可以在SliceBrowser中点击获取 voxel_coord (120, 180, 160) # 横断面看 (z, :, :) axial_slice arr[voxel_coord[0], :, :] # 冠状面看 (:, y, :) 对应z-x平面 coronal_slice arr[:, voxel_coord[1], :] # 矢状面看 (:, :, x) 对应z-y平面 sagittal_slice arr[:, :, voxel_coord[2]]这三个切面共享同一个体素坐标点解剖结构应该同时出现在三张图的交叉位置。如果三张图里该点的类别不一致或者只有两个切面能看到结构基本可以判定标签是分切面单独标注后没有做三维一致性融合。这种情况修复成本极高最好的办法是开始标注之前就把重采样参数锁死让三个切面共享同一个三维体素空间分工才能守住。5. 骶骨腰痛分割数据集避坑指南5条血泪经验5.1 切面名字对不上训练时找不到label现象训练脚本读images/axial/patient01_000.png却去labels/axial/找同名文件报错FileNotFoundError。检查了半天发现图像的命名规则是patient01_000.png标签却是patient01_000_mask.png。原因数据集在切分时images目录和labels目录不是同一套生成脚本一个带了_mask后缀另一个没带。或者两个目录分别来自不同的标注人员文件编号规则没有约定好。解决在训练代码里做一次文件名规范化。不要靠人去检查几百个文件名直接在数据加载入口统一处理——把*_mask.png、*_label.png、*_gt.png全部重命名为和图像名完全一致。用glob找出两个目录的文件列表对集合做差集把不匹配的全部打印出来再统一重命名。import glob import os import shutil img_files glob.glob(dataset_root/images/axial/*.png) label_files glob.glob(dataset_root/labels/axial/*.png) img_names {os.path.basename(f) for f in img_files} label_keys {} for f in label_files: name os.path.basename(f) key name.replace(_mask, ).replace(_label, ).replace(_gt, ) label_keys[key] f missing [n for n in img_names if n not in label_keys] if missing: print(Missing labels:, missing[:10])这类问题通常在第一个epoch就暴露但如果你用了shuffleTrue而数据量又大可能到第3个epoch才触发那时候已经白白等了几个小时。所以训练前用这个脚本强制做一次全量配对比出错了再查省心得多。5.2 mask边缘断层把插值开到了标签上现象图像重采样后标签边缘出现大量不属于任何类别的灰度值比如标签值变成255、128或者类别1的边缘多出一圈类别3。杏色区域看着像把标签糊了。原因重采样标签时用了线性插值。线性插值在两个相近类别编号之间会生成中间值位于3和4之间的体素被插成3.5四舍五入后随机分配到3或4边缘就出现锯齿和错乱类别。解决标签重采样强制使用sitkNearestNeighbor这一点怎么强调都不过分。如果已经用了线性插值别想靠后处理修复重新做一次重采样比清洗污染标签便宜得多。另外还要注意某些框架的DataLoader里如果对图像和标签一并做了resize图像用了双线性而标签忘了改成最近邻同样会触发这个坑。去DataLoader里检查一下interpolation参数图像和标签必须分开指定。5.3 类别不平衡骶骨目标太大、椎间盘太小现象训练出的模型骶骨分割结果接近完美椎间盘却几乎全预测成背景整体mDice看着还行但单独看椎间盘的Dice只有0.2。投影到临床场景就是模型报的椎间盘区域完全不可用。原因5个类别里椎间盘和棘突的区域占比远小于背景和椎体。以矢状面为例椎间盘在一个切片里可能只占几十个像素背景却占了几万个。模型的损失函数被大类别主导稀疏类别几乎学不到特征。解决常见做法是从损失函数下手用带类别权重的Dice Loss或Combo Loss给类别3和类别5更高的权重。另一个更直接的办法是在数据增强里做类别相关的crop保证每个训练patch都包含椎间盘区域。还有一条路径是把椎间盘单独拎出来做二分类检测再分割但成本高先试权重调整更划算。class_weights torch.tensor([0.1, 1.0, 1.0, 3.0, 2.0, 3.0]) # 背景权重压低椎间盘和棘突权重拉高 criterion nn.CrossEntropyLoss(weightclass_weights)这里权重具体怎么设要看你数据集里各类别的像素占比先统计类别直方图让权重和频率成反比再根据验证集dice微调。不要一上来就给椎间盘10倍权重容易让模型把别的区域误判成椎间盘Dice反而更差。5.4 标签文件黑匣子用直方图快速定位断裂类别现象训练时模型输出只有背景和类别1其他类别参与度极低翻遍了代码以为是网络结构的问题最后发现标签文件里类别3、4、5一个像素都没有。原因合并脚本的映射关系写错了或者标注工具导出时把某些类别的值统一写成了0。这类问题不跑代码是看不出来的一张图叠上去视觉上可能只是少了几个区域你不会注意到某个类别彻底消失。解决数据加载完成后第一件事是打印训练集标签的类别直方图。别等训练完再核对在数据集类初始化时顺带统计一次。下面这段代码跑出来如果某个类别是0立即停下查标注源文件。import numpy as np import SimpleITK as sitk def inspect_label_distribution(label_path): label sitk.ReadImage(label_path) arr sitk.GetArrayFromImage(label) unique, counts np.unique(arr, return_countsTrue) dist dict(zip(unique.tolist(), counts.tolist())) print(label_path.split(/)[-1], dist) return dist直方图只能告诉你有没有类别不能告诉你边界对不对。边界检查还是得回到第4章的切片浏览器或者三视图可视化两者结合起来数据质量才算真正过了一道关。注意不能只看一个病人至少要随机抽5个以上病人做同样的检查否则恰好抽到一个标注完整的样本还是会被误导。5.5 把背景当作第0类还是255训练框架的甜蜜陷阱现象标签图在保存时背景是0前景是1到5用某个框架训练时却报错number of classes mismatch或者预测时背景变成白色、前景变成黑色。原因有些图像处理库读PNG时会把单通道uint8当作灰度图但是某些标注工具保存时把背景写成了255或者用了带透明通道的RGBA PNG导致读出来的shape是(H, W, 4)而不是(H, W)后续处理全乱了。解决统一在数据集初始化时强制转成np.uint8的单通道数组并对像素值做一个显式的映射把大于5的值全部归到背景再把所有值裁剪到0到5之间。def sanitize_label(arr): arr arr.astype(np.uint8) arr[arr 5] 0 # 把异常的255、6等值清零 return arr这一步看着像多此一举实际能拦截掉相当一部分标注质量问题。如果你用PyTorch的torchvision.datasets.ImageFolder这种通用接口它默认做三通道图读取单通道标签图会被复制成3通道标签值变成三个一样的值CrossEntropyLoss虽然也能算但语义上已经错了。所以数据集类里要单独写标签读取逻辑别和图像共用一条读取管线。6. 训练前最后一道验证切片级Dice与gif核图6.1 用切片级Dice替代全图Dice很多人评估医学图像分割模型时只算一个全图Dice这个值对骶骨腰痛任务不够用。原因很简单矢状面切片里大部分层是背景只有中间几层包含椎体和骶骨全图Dice被大量背景切片拉高或拉低掩盖了关键层的问题。正确做法是只统计包含前景的切片并且按类别分开算。import numpy as np def dice_per_class(pred, label, num_classes6): dices {} for c in range(1, num_classes): pred_c (pred c) label_c (label c) intersection (pred_c label_c).sum() denom pred_c.sum() label_c.sum() dices[c] (2.0 * intersection) / (denom 1e-8) if denom 0 else 0.0 foreground_mask label 0 pred_fg pred 0 inter_fg (pred_fg foreground_mask).sum() denom_fg pred_fg.sum() foreground_mask.sum() dices[foreground] (2.0 * inter_fg) / (denom_fg 1e-8) return dices dices dice_per_class(pred_arr, label_arr) print(Per-class Dice:, dices)说明按切片统计时不要把无前景切片放进类别Dice的计算否则类别1到5的结果会被大量0除以小数的案例污染。正确做法是只在label_c.sum() 0的切片上累计Dice最后求平均。参数上num_classes6对应背景加5个前景类别如果你的项目里第0类是背景循环从1开始没有问题。这个评估口径写进实验记录里比一个笼统的全图Dice更能暴露模型在椎间盘、棘突上的短板。6.2 让可视化代码承担「gif核图」的职责静态三视图只能给你一个总览骶骨腰痛分割真正要看的是结构在连续切片间的形态变化。椎间盘厚度从L1到S1逐层递减椎管在L4-L5处有无受压这些都藏在序列里。把整个轴位或矢状位序列导出成一个gif用播放速度模拟滚动查看往往比一帧帧点鼠标更高效。import numpy as np import SimpleITK as sitk import imageio def save_slices_gif(image_path, label_path, output_path, axis0, step1, fps6): img_arr sitk.GetArrayFromImage(sitk.ReadImage(image_path)) lab_arr sitk.GetArrayFromImage(sitk.ReadImage(label_path)).astype(np.uint8) n_slices img_arr.shape[axis] frames [] for idx in range(0, n_slices, step): if axis 0: img_slice img_arr[idx, :, :] lab_slice lab_arr[idx, :, :] elif axis 1: img_slice img_arr[:, idx, :] lab_slice lab_arr[:, idx, :] else: img_slice img_arr[:, :, idx] lab_slice lab_arr[:, :, idx] img_slice (img_slice - img_slice.min()) / (img_slice.max() - img_slice.min() 1e-8) # 用第4章的overlay逻辑生成叠加图 rgb np.stack([img_slice] * 3, axis-1) colors np.array([[1,0,0],[0,1,0],[0,0,1],[1,1,0],[1,0,1]]) mask colors[lab_slice] alpha (lab_slice 0)[..., None] * 0.4 frame rgb * (1 - alpha) mask * alpha frames.append((frame * 255).astype(np.uint8)) imageio.mimsave(output_path, frames, fpsfps) save_slices_gif( images/sagittal/patient01.nii.gz, labels/sagittal/patient01_5class.nii.gz, visualization/patient01_sagittal.gif, axis2, step1, fps6 )代码逻辑说明step1导出的gif帧数最多相当于逐片浏览代价是文件大、生成慢对于矢状面这种切片数量在30到80之间的方向用小步长完全可行。横断面动辄两三百层建议step2或step3只保留重点结构区域不然文件上百MB发给谁都不方便。fps用6到8比较接近人工翻片的节奏超过10反而看不清楚。6.3 训练后回头重新审视数据而不是只盯着网络模型效果不好时先不要急着换网络结构。我处理这类医学图像分割数据集的习惯是训练前的视觉核图花掉时间预算的一半训练结束后再花半天把bad case翻出来和原图gif对照。很多所谓网络能力不足的问题实际是标签边界标错了或者在重采样时embedding偏移累积导致的解码错位。保留每个病人的三切面可视化结果把它当作项目资产固定下来改一次数据处理流程就重新生成一遍。实际踩过最值回票价的一个习惯是每次导出可视化结果时顺手在文件名里写上spacing和是否有标签三个月后翻旧项目不用重新读代码也能知道这批数据当时是怎么重采样的。希望帮到你。本文还有配套的精品资源点击获取
返回列表