2. 医学影像数据格式与读取

在 nnU-Net 的工程链路里,数据格式与读取不是“顺手做一下”的前置步骤,而是决定后续预处理、训练和推理是否正确的根基。医学影像常见的三种格式——DICOM、NIfTI、NRRD——各自承载的信息结构不同,读取工具的行为也不同。如果在这一步把方向、spacing 或原点理解错,后面所有基于体素的操作都会系统性偏移。

三种格式的定位与结构差异

DICOM 是临床设备输出的原始格式,通常一个序列由成百上千个文件组成,每个文件对应一层切片。它把像素数据放在 PixelData 标签里,把几何信息分散在 ImagePositionPatient、ImageOrientationPatient、PixelSpacing、SliceThickness、SpacingBetweenSlices 等标签中。DICOM 的强项是保留采集参数和患者信息,弱项是几何信息分散、层间距可能不均匀,且不同厂商的私有标签会带来兼容问题。

NIfTI 是神经影像与科研分割中最常见的格式,通常一个 .nii 或 .nii.gz 文件就包含完整三维体数据。它的几何信息集中在头信息里:pixdim 记录体素尺寸,qform 和 sform 记录体素坐标到世界坐标的仿射变换。NIfTI 的强项是单文件、几何紧凑,弱项是 qform 与 sform 可能不一致,需要明确以哪个为准。

NRRD 常用于医学影像处理工具链,支持多通道、多组件和自定义元数据,头信息以键值对形式保存,空间方向通过 space directions 和 space origin 描述。它在保留方向信息方面比较直观,适合作为中间交换格式。

SimpleITK 与 nibabel 的读取差异

SimpleITK 读取后返回的是 sitk.Image,其数组索引顺序是 (z, y, x),而 NumPy 习惯的数组顺序是 (x, y, z)。因此从 SimpleITK 转到 NumPy 时,必须用 GetArrayFromImage(),它会把轴顺序调整为 (z, y, x) 对应的 NumPy 数组;如果直接用 GetArrayViewFromImage() 再手动转置,容易在后续处理中把轴顺序弄反。SimpleITK 的 GetSpacing() 返回的是 (x, y, z) 方向的体素尺寸,GetOrigin() 返回世界坐标原点,GetDirection() 返回方向余弦矩阵。

nibabel 读取 NIfTI 后返回 Nifti1Image,通过 get_fdata() 得到 NumPy 数组,轴顺序天然是 (x, y, z)。它的 header.get_zooms() 返回体素尺寸,affine 属性给出 4×4 仿射矩阵,把体素索引映射到世界坐标。nibabel 不直接提供“方向余弦”概念,而是通过 affine 的前 3×3 子矩阵表达旋转、缩放和剪切。

两者最关键的差异在于:SimpleITK 把方向、spacing、origin 分开存储,nibabel 把它们统一进 affine。理解这一点,才能在两种工具之间安全转换。

DICOM / NIfTI / NRRD 读取与几何信息映射 DICOM 序列 • 多文件,每文件一层切片 • PixelData 存像素 • ImagePositionPatient • ImageOrientationPatient • PixelSpacing / SliceThickness • 层间距可能不均匀 NIfTI 单文件 • .nii / .nii.gz • pixdim 记录体素尺寸 • qform / sform 仿射 • 轴顺序 (x, y, z) • qform 与 sform 可能不一致 • 单文件便于分发 NRRD 头信息 • 键值对头信息 • space directions • space origin • 支持多通道 / 多组件 • 方向描述直观 • 适合中间交换 SimpleITK 读取 • sitk.ReadImage() → sitk.Image • GetArrayFromImage() → NumPy (z, y, x) • GetSpacing() → (x, y, z) 体素尺寸 • GetOrigin() → 世界坐标原点 • GetDirection() → 方向余弦矩阵 nibabel 读取 • nib.load() → Nifti1Image • get_fdata() → NumPy (x, y, z) • header.get_zooms() → 体素尺寸 • affine → 4×4 体素到世界矩阵 • 方向、spacing、origin 统一在 affine 关键一致性检查: 轴顺序 (z,y,x) 与 (x,y,z) 不可混用;spacing 顺序必须与数组轴对应;affine 与 direction+spacing+origin 应能互相推导。

方向与 spacing 的处理原则

方向处理的核心是“世界坐标一致性”。DICOM 的 ImageOrientationPatient 给出图像行、列方向余弦,ImagePositionPatient 给出每层左上角体素的世界坐标。SimpleITK 读取 DICOM 序列后,会把这些信息整合成 direction、spacing、origin 三件套。NIfTI 的 affine 则把这三者合并成一个矩阵。无论用哪种工具,最终都要回答同一个问题:体素索引 (i, j, k) 对应的世界坐标是什么。

spacing 处理要特别注意顺序。SimpleITK 的 GetSpacing() 返回 (sx, sy, sz),对应数组的 x、y、z 轴;而 GetArrayFromImage() 得到的数组轴顺序是 (z, y, x),所以数组第 0 轴对应的是 sz,第 2 轴对应的是 sx。nnU-Net 在预处理时会统一重采样到目标 spacing,如果这里把 spacing 顺序弄反,重采样后的解剖比例就会失真。

一个实用的检查方法是:读取后打印 image.GetSize()、image.GetSpacing()、image.GetDirection(),再用 GetArrayFromImage() 打印数组 shape,确认 shape 与 size 的轴顺序对应关系。对于 NIfTI,打印 img.shape、img.header.get_zooms() 和 img.affine,检查 affine 对角线元素是否与 zooms 一致(允许旋转和剪切时非对角元素不为零)。

实际读取示例

下面给出一个可参考的读取与几何信息检查片段,示例参数为合理假设,实际运行时以数据为准。

import SimpleITK as sitk
import nibabel as nib
import numpy as np

# SimpleITK 读取 NIfTI
sitk_img = sitk.ReadImage("case_001.nii.gz")
arr_sitk = sitk.GetArrayFromImage(sitk_img)   # (z, y, x)
print("size:", sitk_img.GetSize())            # (x, y, z)
print("spacing:", sitk_img.GetSpacing())      # (sx, sy, sz)
print("origin:", sitk_img.GetOrigin())
print("direction:\n", sitk_img.GetDirection())
print("array shape:", arr_sitk.shape)         # (z, y, x)

# nibabel 读取同一文件
nib_img = nib.load("case_001.nii.gz")
arr_nib = nib_img.get_fdata()                 # (x, y, z)
print("shape:", nib_img.shape)                # (x, y, z)
print("zooms:", nib_img.header.get_zooms())   # (sx, sy, sz)
print("affine:\n", nib_img.affine)

# 轴顺序转换:nibabel (x,y,z) -> SimpleITK 数组 (z,y,x)
arr_from_nib = np.transpose(arr_nib, (2, 1, 0))
assert arr_from_nib.shape == arr_sitk.shape

这段代码的关键不是“能读出来”,而是通过打印和断言确认两种工具读到的几何信息一致。如果 arr_from_nib 与 arr_sitk 的 shape 不一致,说明轴顺序理解有误;如果 spacing 与 affine 对角线不匹配,说明方向或缩放信息在转换中丢失。

常见陷阱与判断依据

第一类陷阱是 qform 与 sform 不一致。nibabel 默认优先使用 sform,但某些数据可能只设置了 qform。此时应显式检查 header['qform_code'] 和 header['sform_code'],必要时用 nib.aff2axcodes() 查看方向编码,确认是否与预期解剖方向一致。

第二类陷阱是 DICOM 层间距不均匀。SimpleITK 读取时如果发现层间距不一致,会给出警告或按平均间距处理。对于 nnU-Net,建议先检查 SpacingBetweenSlices 与 SliceThickness 是否一致,必要时在重采样前做层间插值。

第三类陷阱是方向翻转。不同工具对 RAS、LPS 坐标系的约定不同,DICOM 使用 LPS,NIfTI 使用 RAS。SimpleITK 内部统一为 LPS,nibabel 的 affine 通常对应 RAS。跨工具转换时,必须明确当前坐标系,否则左右、前后方向可能整体翻转。判断依据是:用 nib.aff2axcodes(img.affine) 得到方向编码,再与解剖学预期比对;如果出现 L 与 R 互换,说明坐标系约定没有对齐。

在 nnU-Net 工程中,建议统一以 SimpleITK 的 direction、spacing、origin 作为几何基准,在数据转换层完成 NIfTI 与 NRRD 的读写,避免在训练代码里混用两套轴顺序。

把格式解析、读取工具差异、方向与 spacing 处理这三件事做扎实,后续的归一化、重采样、滑窗推理才有可靠的空间基础。读取阶段多打印、多断言、多比对,远比训练阶段反复调参更省时间。