2. 医学影像数据基础与工程环境

多模态医学影像配准的第一步,不是写配准算法,而是把数据“读对”。如果对 DICOM 与 NIfTI 的几何语义理解有偏差,后续的相似性度量、优化器和变换模型都会建立在错误的坐标基础上。本节围绕三个核心问题展开:影像文件里到底存了什么几何信息、这些信息如何映射到物理空间、以及如何用 SimpleITK/ANTs 把数据正确加载并验证。

一、DICOM 与 NIfTI:两种数据组织方式

DICOM 是临床设备输出的原始格式,本质是“一个序列对应一个三维体数据”。每个切片文件包含像素数据(Pixel Data)和大量元数据标签,其中与配准直接相关的是:(0020,0032) Image Position (Patient) 给出该切片左上角体素在患者坐标系中的位置;(0020,0037) Image Orientation (Patient) 给出图像行、列方向在患者坐标系中的单位向量;(0028,0030) Pixel Spacing 是行、列方向的体素间距;(0018,0050) Slice Thickness 与 (0018,0088) Spacing Between Slices 描述层厚与层间距。注意:层间距不一定等于层厚,重建时必须以实际层间距为准。

NIfTI 则是为神经影像分析设计的单文件格式,把三维(或四维)体数据与一个 4×4 仿射矩阵(sform/qform)打包在一起。这个仿射矩阵直接定义了体素索引到世界坐标的映射,因此 NIfTI 的几何信息比 DICOM 更“紧凑”,但也更容易被误读——很多工具在保存时会把 qform 和 sform 写成不同内容,加载时必须明确以哪一个为准。

二、体素间距、方向余弦与物理坐标

体素间距(spacing)描述相邻体素中心在三个轴上的物理距离,单位通常是毫米。方向余弦(direction cosines)是一个 3×3 正交矩阵,每一列表示体素索引轴在物理坐标系中的方向。原点(origin)是索引 (0,0,0) 体素中心在物理坐标系中的位置。三者共同构成体素索引到物理坐标的仿射变换:

P_physical = Origin + Direction · (Spacing ⊙ Index)

其中 Index = [i, j, k]^T 为体素索引,
Spacing ⊙ Index 表示逐元素相乘。

以轴位 CT 为例,若 origin = (-150.0, -150.0, -200.0),spacing = (0.7, 0.7, 1.5),direction 为单位矩阵,则索引 (100, 100, 50) 对应的物理坐标为:

x = -150.0 + 0.7 × 100 = -80.0
y = -150.0 + 0.7 × 100 = -80.0
z = -200.0 + 1.5 × 50  = -125.0

方向余弦非单位矩阵时,说明扫描平面相对患者坐标轴发生了旋转。例如某些斜位扫描的 MR,direction 矩阵会出现非对角项。此时若忽略方向余弦、直接把索引当物理坐标使用,配准结果会出现系统性偏移。判断方向是否“标准”的简单方法:检查 direction 是否接近单位矩阵,或计算其行列式是否接近 +1(若为 -1,说明存在翻转,需要特别处理左右方向)。

方向余弦矩阵必须是正交矩阵,且行列式为 +1 或 -1。若加载后发现行列式绝对值明显偏离 1,说明数据或元数据可能损坏,应先排查再进入配准流程。

三、工具链搭建:ITK、SimpleITK 与 ANTs 的分工

ITK 是底层 C++ 图像处理库,提供读写、插值、变换等基础能力;SimpleITK 是 ITK 的简化封装,Python 接口友好,适合快速验证和教学;ANTs 则是在 ITK 之上构建的配准与分割工具集,提供命令行程序和 Python 接口,擅长多模态配准。三者的关系可以理解为:ITK 是引擎,SimpleITK 是易用方向盘,ANTs 是整车。工程中常见做法是用 SimpleITK 做数据加载与预处理,用 ANTs 做配准,最后再用 SimpleITK 做结果评估。

环境搭建建议使用 conda 创建独立环境,避免与系统 Python 冲突:

conda create -n medreg python=3.10
conda activate medreg
conda install -c conda-forge simpleitk ants
pip install nibabel pydicom

安装完成后,用以下代码验证 SimpleITK 与 ANTs 是否可用:

import SimpleITK as sitk
print(sitk.Version())
import ants
print(ants.__version__)

四、数据加载实战:从 DICOM 序列到 NIfTI

加载 DICOM 序列时,SimpleITK 提供 ImageSeriesReader,但前提是文件名顺序与切片顺序一致。更稳妥的方式是先用 pydicom 读取每个文件的 ImagePositionPatient 的 z 分量,按 z 排序后再交给 SimpleITK:

import pydicom, glob
files = glob.glob("ct_series/*.dcm")
slices = [pydicom.dcmread(f) for f in files]
slices.sort(key=lambda s: float(s.ImagePositionPatient[2]))
reader = sitk.ImageSeriesReader()
reader.SetFileNames([s.filename for s in slices])
ct = reader.Execute()
print(ct.GetSize(), ct.GetSpacing(), ct.GetOrigin())

加载 NIfTI 则更直接:

mr = sitk.ReadImage("mr_t1.nii.gz")
print(mr.GetSize(), mr.GetSpacing(), mr.GetDirection())

加载后必须做三项检查:一是 GetSize() 是否与预期层数、行列数一致;二是 GetSpacing() 是否合理(如 CT 常见 0.5–1.0 mm 层内、1–3 mm 层间);三是 GetDirection() 是否接近单位矩阵。若方向矩阵非单位阵,可用 sitk.DICOMOrient() 或 ANTs 的 ants.reorient_image2() 统一到标准方向(如 RAS 或 LPS),再进入配准。

多模态配准前,建议把所有数据统一重采样到同一物理空间或至少统一方向约定。否则即使配准算法正确,也可能因为方向定义不一致而得到错误结果。

五、几何信息验证与常见陷阱

一个实用的验证方法:取图像中心体素的索引,用 TransformIndexToPhysicalPoint() 计算物理坐标,再与手动按 origin + direction·spacing 计算的结果对比,两者应一致。若不一致,说明对仿射关系的理解或代码实现有误。

idx = [s//2 for s in ct.GetSize()]
p1 = ct.TransformIndexToPhysicalPoint(idx)
print("ITK 计算:", p1)

常见陷阱包括:DICOM 层间距与层厚混用、NIfTI 的 qform/sform 不一致、方向矩阵含翻转导致左右颠倒、以及不同工具对 LPS/RAS 坐标系的默认约定不同。ITK/SimpleITK 内部使用 LPS 坐标系,而 NIfTI 标准常用 RAS,两者在 x 和 y 方向符号相反。跨工具传递数据时,必须确认坐标系约定,否则会出现“配准看起来对、实际左右反了”的问题。

体素索引 → 物理坐标的仿射映射与工具链数据流 DICOM 序列 ImagePositionPatient ImageOrientationPatient PixelSpacing SpacingBetweenSlices 按 z 排序后读取 → 3D 体数据 NIfTI 文件 体数据 + 4×4 仿射 qform / sform pixdim 体素间距 单文件,几何紧凑 需确认 qform/sform → 3D 体数据 仿射映射核心公式 P = Origin + Direction · (Spacing ⊙ Index) Origin:索引 (0,0,0) 的物理位置 Direction:3×3 方向余弦矩阵 Spacing:三轴体素间距 (mm) Index:体素索引 [i, j, k] 工具链分工 ITK (C++) 底层读写/插值/变换 SimpleITK (Python) 加载/预处理/验证 ANTs 多模态配准/重采样 pydicom/nibabel 元数据解析 加载后三项必查 ① GetSize() 尺寸是否匹配 ② GetSpacing() 间距是否合理 ③ GetDirection() 是否接近单位阵

完成上述加载与验证后,数据才真正具备进入配准流程的条件。下一阶段的多模态配准,将在这个统一的物理坐标基础上,进一步讨论变换模型、相似性度量和优化策略。