2. 医学影像数据格式解析

医学影像开发中,真正让工程变复杂的往往不是算法本身,而是“同一份影像在不同格式里长得不一样”。DICOM、NIfTI、NRRD、MetaImage 这四种格式分别服务于临床设备、神经影像研究、通用科研数据和 ITK 生态,它们在元数据组织、像素数据排布、方向与原点表达上各有约定。SimpleITK 的 Image 对象是这些差异的“统一收口层”:无论读入哪种格式,最终都要落到 size、spacing、origin、direction 和像素缓冲这五个核心要素上。理解格式与 Image 对象的映射关系,是后续配准、重采样、分割不“翻车”的前提。

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

DICOM 是临床标准,一个序列通常由多个文件组成,每个文件携带 Patient、Study、Series、Image Position Patient、Image Orientation Patient、Pixel Spacing、Slice Thickness 等标签。它的元数据最丰富,但也最“碎”:几何信息分散在多个标签中,且不同厂商、不同设备可能存在私有标签或缺失字段。NIfTI 常见于脑影像研究,单文件(.nii)或双文件(.hdr/.img)存储,头信息用 qform/sform 描述空间变换,体素数据按三维数组连续存放。NRRD 以“头+数据”分离或合并方式存储,头信息是键值对文本,支持 space directions、space origin、kinds 等字段,对方向表达非常明确。MetaImage(.mhd/.raw 或 .mha)是 ITK 的原生格式之一,头文件用 TransformMatrix、Offset、ElementSpacing 等字段描述几何,结构清晰、解析稳定,常用于中间结果和测试数据。

从工程视角看,这四种格式的差异集中在三处:元数据是分散还是集中、像素数据是单文件还是多文件、方向与原点是用矩阵还是用标签表达。SimpleITK 读取时,会把这些差异统一转换为 Image 对象的几何属性,但转换过程中存在“信息丢失”和“约定差异”两个风险点,必须提前识别。

四种格式 → SimpleITK Image 对象映射关系 DICOM 多文件序列 / 标签分散 IP、IOP、PixelSpacing NIfTI 单文件 .nii / 双文件 qform / sform 矩阵 NRRD 头+数据 / 键值对 space directions/origin MetaImage .mhd/.raw 或 .mha TransformMatrix/Offset SimpleITK 读取 ReadImage() ImageFileReader 元数据 → 几何属性 像素数据 → 缓冲 方向约定统一 类型转换 注意:DICOM 方向 需按切片顺序重建 SimpleITK Image 对象 size (X, Y, Z) 体素数量 spacing (sx, sy, sz) mm/体素 origin 世界坐标起点 (mm) direction 3×3 方向余弦矩阵 pixel buffer 连续内存 / 类型 物理坐标 = origin + direction · (index × spacing)

元数据到几何属性的映射细节

SimpleITK 读取 DICOM 序列时,spacing 的前两维来自 PixelSpacing(行、列方向),第三维来自相邻切片 Image Position Patient 的欧氏距离,而不是简单取 Slice Thickness。这一点非常关键:如果切片间距不均匀,SimpleITK 会按实际位置计算,但若序列缺失位置信息,则可能退化为厚度值。方向矩阵由 Image Orientation Patient 的前三个分量(行方向)和后三个分量(列方向)叉乘得到第三方向,再按切片位置排序确定正负。原点取第一张切片位置与方向矩阵的对应关系。NIfTI 则直接使用 sform 或 qform 矩阵分解出 spacing、origin 和 direction;NRRD 从 space directions 的列向量取模长得到 spacing,归一化后得到 direction;MetaImage 从 TransformMatrix 和 Offset 直接映射。

像素数据方面,DICOM 的 BitsAllocated、PixelRepresentation、RescaleSlope、RescaleIntercept 决定了原始值到物理值(如 HU)的转换。SimpleITK 默认读取原始像素值,若需要 HU,必须手动应用 RescaleSlope 和 RescaleIntercept,或使用 ReadImage 后调用 Cast 与线性变换。NIfTI、NRRD、MetaImage 通常直接存储物理值或已标定的强度值,但要注意 NIfTI 的 scl_slope/scl_inter 字段,SimpleITK 在读取时会自动应用。像素类型上,DICOM 常见 int16、uint16,NIfTI 常见 float32、int16,NRRD 和 MetaImage 支持更丰富的类型,读取后可通过 GetPixelID() 查看。

方向与坐标系的工程陷阱

医学影像中最容易出错的是方向。DICOM 的 Image Orientation Patient 描述的是图像行、列方向在患者坐标系中的方向余弦,而 SimpleITK 的 direction 矩阵列向量对应的是索引轴方向。两者在 LPS(DICOM 患者坐标)与 RAS(NIfTI 常用)之间存在符号差异。SimpleITK 内部统一使用 LPS 作为物理坐标约定,读取 NIfTI 时会自动将 RAS 转换为 LPS。如果直接混用不同来源的数组和几何信息,就会出现左右翻转、上下颠倒。判断方法很简单:读取后打印 GetDirection(),检查对角线元素符号;再用 TransformIndexToPhysicalPoint 把体素索引转成物理坐标,与已知解剖点比对。

DICOM 序列读取时,不要假设文件名顺序等于切片顺序。应按 Image Position Patient 沿切片法向投影排序,否则 direction 第三列符号可能错误,导致体积镜像。

实操:读取、检查与转换

下面示例演示读取 DICOM 序列并检查关键几何属性,再转换为 NIfTI 保存。示例参数为合理假设,实际运行时以数据为准。

import SimpleITK as sitk

# 读取 DICOM 序列(目录内为同一 Series 的切片)
reader = sitk.ImageSeriesReader()
series_ids = reader.GetGDCMSeriesIDs("data/dicom_series")
file_names = reader.GetGDCMSeriesFileNames("data/dicom_series", series_ids[0])
reader.SetFileNames(file_names)
image = reader.Execute()

print("size:", image.GetSize())
print("spacing:", image.GetSpacing())
print("origin:", image.GetOrigin())
print("direction:\n", image.GetDirection())
print("pixel type:", image.GetPixelIDTypeAsString())

# 将索引 (0,0,0) 与 (10,10,5) 转为物理坐标
p0 = image.TransformIndexToPhysicalPoint((0, 0, 0))
p1 = image.TransformIndexToPhysicalPoint((10, 10, 5))
print("physical p0:", p0)
print("physical p1:", p1)

# 保存为 NIfTI,SimpleITK 会自动处理 LPS→RAS 的方向转换
sitk.WriteImage(image, "output/volume.nii.gz")

运行后重点观察三点:spacing 第三维是否与切片间距一致;direction 矩阵是否接近单位阵或带符号的对角阵;TransformIndexToPhysicalPoint 的结果是否符合解剖方位。若发现方向异常,可用 sitk.DICOMOrient 将图像重定向到标准方向(如 LPS 或 RAS),再继续后续处理。

格式选择与工程建议

临床数据入口优先用 DICOM,保留原始标签用于溯源;算法中间结果建议用 MetaImage 或 NRRD,头信息清晰、读写稳定;与神经影像工具链交互时用 NIfTI,但要注意 RAS/LPS 转换。无论哪种格式,读入后都应先做一次几何自检:size 是否与预期体素规模一致、spacing 是否为正、direction 行列式是否接近 +1(若为 -1 说明存在镜像)。把这一步固化成读取后的检查函数,能避免大量“看起来对、实际左右反”的隐蔽错误。

建议在项目中统一以 SimpleITK Image 作为内存中的唯一表示,格式转换只发生在读写边界。这样几何约定、像素类型和方向处理都收敛到一处,后续配准和重采样才有稳定的输入基础。