2. 医学影像数据格式与MONAI数据加载

在医学影像AI开发中,数据格式决定了我们如何读取体素、如何解释空间方向、以及如何把图像与标签对齐。MONAI本身并不重新发明影像IO,而是建立在ITK、NiBabel等成熟库之上,通过monai.data中的Reader与Dataset把“文件”变成“可训练的数组”。理解DICOM、NIfTI与NIfTI压缩格式的差异,以及ITKReader、NibabelReader、PILReader等读取器的分工,是后续做变换、缓存和DataLoader的前提。

三种常见格式的定位

DICOM是临床设备与PACS的通用交换格式。一次CT或MR检查通常由多个切片文件组成,每个文件既包含像素数据,也包含患者信息、层厚、像素间距、ImagePositionPatient、ImageOrientationPatient等元数据。它的优势是信息完整、符合临床工作流;缺点是单次检查是“多文件序列”,读取后需要按空间位置排序并重建为三维体数据,且不同厂商的私有字段可能影响解析。

NIfTI(.nii)通常把三维或四维数据存放在单个文件中,配合仿射矩阵(affine)描述体素索引到世界坐标的映射。它更适合研究场景下的配准、分割与深度学习训练,因为一个文件即可表示整个volume,标签与图像容易成对管理。NIfTI压缩格式(.nii.gz)是对.nii做gzip压缩,磁盘占用更小、传输更快,读取时由库自动解压,对上层代码几乎透明,但会带来少量CPU开销,且不适合频繁随机写入。

格式典型扩展名组织方式关键元数据常见用途
DICOM.dcm / 无扩展名每切片一个文件Patient/Study/Series、PixelSpacing、ImagePositionPatient临床采集、PACS归档
NIfTI.nii单文件volumeaffine、qform/sform、pixdim研究数据、训练集
NIfTI压缩.nii.gz单文件gzip压缩同.nii,读取时解压公开数据集分发

MONAI读取器的分工与集成

MONAI的读取器都实现ImageReader接口,核心方法是read()与get_data(),返回图像数组和元数据字典。常用组合如下:

  • ITKReader:基于ITK,支持DICOM序列、NIfTI、NRRD、MHA等,能读取方向、间距、原点,并可通过series_meta、affine_lps_to_ras等参数控制坐标约定。处理DICOM目录时最稳妥。
  • NibabelReader:基于NiBabel,专注NIfTI/NIfTI.gz,读取快、affine语义清晰,适合.nii.gz训练集。
  • PILReader:读取PNG/JPG等2D图像,常用于病理或X光片,不保留三维空间信息。
  • LoadImage:MONAI的变换算子,内部可指定reader,把读取器接入Compose流水线。

一个常见的集成写法是:对DICOM目录使用ITKReader,对.nii.gz使用NibabelReader,再统一交给LoadImaged(keys=["image","label"], reader=...)。这样上层变换(如EnsureChannelFirstd、Spacingd、Orientationd)无需关心底层格式差异。

import monai
from monai.data import Dataset, DataLoader
from monai.transforms import Compose, LoadImaged, EnsureChannelFirstd, Orientationd, Spacingd

train_files = [
    {"image": "data/case001/img.nii.gz", "label": "data/case001/seg.nii.gz"},
    {"image": "data/dicom_case002",      "label": "data/case002/seg.nii.gz"},
]

transforms = Compose([
    LoadImaged(keys=["image", "label"]),          # 自动按扩展名选择Reader
    EnsureChannelFirstd(keys=["image", "label"]),
    Orientationd(keys=["image", "label"], axcodes="RAS"),
    Spacingd(keys=["image", "label"], pixdim=(1.0, 1.0, 1.0), mode=("bilinear", "nearest")),
])

ds = Dataset(data=train_files, transform=transforms)
dl = DataLoader(ds, batch_size=2, num_workers=2)
batch = next(iter(dl))
print(batch["image"].shape, batch["image"].meta["affine"].shape)

上面示例中,LoadImaged会根据路径自动选择读取器:目录或.dcm走ITKReader,.nii.gz走NibabelReader。若希望显式控制,可写成LoadImaged(keys="image", reader=ITKReader())。读取完成后,meta中会保留affine、original_affine、spatial_shape等字段,供后续重采样和逆变换使用。

DICOM序列读取时,ITKReader会依据ImagePositionPatient沿法向量排序切片。若发现层序颠倒,可检查ImageOrientationPatient与SliceThickness,必要时在读取后做一次Orientationd统一到RAS,避免左右或前后翻转。

方向、仿射与重采样的一致性

医学影像最容易出错的地方不是“读不出来”,而是“读出来方向不对”。DICOM的像素坐标通常遵循LPS(Left-Posterior-Superior)约定,而NIfTI的affine常用RAS(Right-Anterior-Superior)。MONAI的ITKReader默认会把LPS转为RAS,使不同来源的数据在同一坐标系下对齐。若自行用SimpleITK读取后再转MONAI,需要确认是否重复转换。

仿射矩阵是一个4×4矩阵,把体素索引(i,j,k)映射到世界坐标(x,y,z)。重采样时,Spacingd修改的是体素间距,Orientationd修改的是轴方向,两者都会更新affine。标签必须使用最近邻插值(mode="nearest"),否则会出现不存在的类别值。图像可用双线性或三线性插值。判断重采样是否成功,可以观察meta["affine"]对角线是否接近目标pixdim,以及meta["spatial_shape"]是否按预期变化。

医学影像格式 → MONAI Reader → 统一张量与元数据 DICOM 序列 多文件 / 每切片一个 PixelSpacing · IPP · IOP LPS 坐标约定 需按位置排序重建3D NIfTI (.nii) 单文件 volume affine / qform / sform RAS 坐标约定 NIfTI 压缩 (.nii.gz) gzip 压缩单文件 读取时自动解压 省磁盘 · 略增CPU ITKReader DICOM / NIfTI / NRRD / MHA LPS → RAS 转换 序列排序 · 方向/间距/原点 适合 DICOM 目录 NibabelReader .nii / .nii.gz affine 语义清晰 读取快 · 适合训练集 PILReader PNG / JPG 等 2D 图像 无三维空间信息 LoadImaged 变换 按扩展名/目录自动选 Reader 也可显式指定 reader=... 输出:image / label 数组 meta: affine · spatial_shape original_affine · pixdim 后续统一变换 EnsureChannelFirstd Orientationd(axcodes="RAS") Spacingd(pixdim=1,1,1) 标签用 nearest 插值 关键一致性:DICOM(LPS) 与 NIfTI(RAS) 经 Reader 统一到同一世界坐标系;affine 随 Orientationd / Spacingd 同步更新

实操中的检查点

读取完成后,建议先做三项检查再进入训练。第一,看形状:image.shape应为[C, H, W, D]或[C, H, W],若出现[H, W, D, C]说明通道维位置不对,需用EnsureChannelFirstd。第二,看affine:meta["affine"]的对角线应接近体素间距,若全为1而实际层厚是3mm,说明间距信息丢失。第三,看标签取值:分割标签应只包含预期的类别整数,若出现0.5、1.7等值,通常是插值方式用错。

对于DICOM目录,还要注意一个目录中可能混有定位像(Localizer)或不同序列。ITKReader会尝试按SeriesInstanceUID分组,但更稳妥的做法是在构建文件列表时先用pydicom筛选目标Series,再交给MONAI。对于.nii.gz,若数据来自不同站点,affine方向可能不一致,统一执行Orientationd(axcodes="RAS")能显著降低后续配准和增强的出错概率。

不要对标签使用双线性插值。分割标签是离散类别,插值会产生无意义的中间值,导致损失计算和评估指标异常。图像与标签的mode必须分别指定,例如mode=("bilinear","nearest")。

把格式解析、Reader选择、方向统一和重采样串起来,就形成了MONAI数据加载的基本链路。后续的缓存(CacheDataset)、随机增强和Patch采样,都建立在这条链路输出的、带有正确affine和meta的统一数组之上。