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 | 单文件volume | affine、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等字段,供后续重采样和逆变换使用。
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"]是否按预期变化。
实操中的检查点
读取完成后,建议先做三项检查再进入训练。第一,看形状: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的统一数组之上。