3. MONAI Transform系统与数据预处理
在医学影像 AI 开发中,原始数据往往来自不同厂商、不同扫描协议、不同存储格式,直接送入网络训练几乎必然失败。MONAI 的 transform 系统正是为解决这一问题而设计的:它把“读取—几何校正—强度归一化—增强—转张量”这一整条预处理链拆成可组合、可复用、可序列化的原子变换,再通过 Compose 串成流水线。理解这套系统,关键不是记住每个类的名字,而是理解数据在变换链中如何被逐步改写,以及每一步的工程参数如何影响最终张量。
一、Compose:变换链的调度器
Compose 本身不做任何像素运算,它只负责按顺序调用子变换,并把每个变换的返回值传递给下一个。MONAI 的变换有两种调用约定:字典式(MapTransform)和数组式(Transform)。工程中推荐字典式,因为影像、标签、元数据可以放在同一个字典里同步变换,避免图像和分割掩膜错位。
from monai.transforms import Compose, LoadImaged, EnsureChannelFirstd, Spacingd, Orientationd, ScaleIntensityRanged, EnsureTyped
train_transforms = Compose([
LoadImaged(keys=["image", "label"]),
EnsureChannelFirstd(keys=["image", "label"]),
Spacingd(keys=["image", "label"], pixdim=(1.0, 1.0, 1.0), mode=("bilinear", "nearest")),
Orientationd(keys=["image", "label"], axcodes="RAS"),
ScaleIntensityRanged(keys=["image"], a_min=-175, a_max=250, b_min=0.0, b_max=1.0, clip=True),
EnsureTyped(keys=["image", "label"]),
])
这段配置的顺序不是随意的:LoadImage 必须先执行,否则没有数据可处理;EnsureChannelFirst 要在几何变换前完成,保证通道维语义正确;Spacing 和 Orientation 都属于几何重采样,通常放在强度变换之前,避免插值把归一化后的数值再次平滑;EnsureTyped 放在最后,把 NumPy 数组统一转成 PyTorch 张量。
二、LoadImage:把文件变成带元数据的数组
LoadImaged 封装了 ITK、Nibabel、PIL 等后端,能读取 NIfTI、DICOM、PNG 等格式。它最重要的工程价值是同时产出数组和元数据:image.meta["affine"] 保存体素到世界坐标的仿射矩阵,meta["spacing"] 保存原始体素间距,meta["original_channel_dim"] 记录通道维位置。这些元数据是后续 Spacing、Orientation 能正确工作的前提。
实操中常见的一个坑是 DICOM 序列读取。若一个目录下有多层 DICOM,应使用 LoadImaged(reader="PydicomReader") 并配合 LoadImage 的 image_only=False,否则可能只读到单帧。另一个坑是 EnsureChannelFirstd 必须紧跟 LoadImage,因为不同后端的通道维位置不一致,延迟处理会让后续变换误判维度。
三、Spacing:统一体素物理尺寸
不同设备扫描出的体素间距可能相差数倍,例如 CT 常见 0.7×0.7×1.25 mm,而某些 MRI 可能是 1×1×3 mm。若直接训练,网络会把“物理上相同大小的病灶”看成不同像素规模,严重损害泛化。Spacingd 通过重采样把体素间距统一到目标值 pixdim。
工程配置要点有三:其一,pixdim 应参考数据集统计中位数,而不是拍脑袋取 1×1×1,过细会显著增加显存和计算量;其二,图像用 mode="bilinear",标签必须用 mode="nearest",否则分割类别会被插值成小数;其三,align_corners=True 在多数医学影像场景下更符合物理对应关系。重采样后,meta["spacing"] 会更新为目标间距,仿射矩阵同步缩放。
四、Orientation:统一解剖方向
医学影像的轴方向由仿射矩阵决定,常见的有 RAS、LPS、LAS 等。如果训练集里混有不同方向的数据,网络学到的“上下左右”就是混乱的。Orientationd(axcodes="RAS") 会把所有影像重定向到统一解剖坐标系:R 表示从左到右,A 表示从后到前,S 表示从下到上。
需要注意,Orientation 只改变轴的排列和翻转,不改变体素间距,因此它和 Spacing 是互补的。两者顺序上,先 Spacing 再 Orientation 或反之在数学上等价,但工程上建议先 Spacing,因为重采样后再翻转轴,计算量更可控。执行后应检查 meta["original_affine"] 与当前 affine 的关系,确认方向确实被改写。
五、IntensityNormalization:让数值分布可比
强度归一化决定网络能否稳定收敛。MONAI 提供多种选择:ScaleIntensityRanged 做线性截断缩放,适合 CT(如把 -175~250 HU 映射到 0~1);NormalizeIntensityd 做零均值单位方差,适合 MRI;ScaleIntensityd 做简单 min-max。选择依据是模态的物理含义:CT 的 HU 有绝对意义,应保留窗宽窗位逻辑;MRI 强度无绝对单位,更适合按非零区域统计做 z-score。
一个容易被忽略的工程细节是:归一化应只对图像执行,标签不参与;同时 clip=True 能防止极端值把动态范围拉偏。若使用 NormalizeIntensityd,建议设置 nonzero=True,只统计前景体素,避免大量背景零值拉低均值。
六、工程化配置的检查清单
把上述变换组装成可复现的流水线后,建议在正式训练前做一次“单样本可视化验证”:取一个样本,依次打印每个变换后的 shape、spacing、affine 和强度范围,确认图像与标签的几何关系始终一致。常见故障包括:标签被插值成非整数类别、通道维位置错误导致 Spacing 把通道当成空间维、归一化把背景零值纳入统计导致前景对比度被压缩。
| 变换 | 关键参数 | 作用对象 | 典型取值 |
|---|---|---|---|
| LoadImaged | keys, reader | image, label | keys=["image","label"] |
| EnsureChannelFirstd | keys | image, label | 紧跟 LoadImage |
| Spacingd | pixdim, mode | image, label | (1,1,1) / bilinear+nearest |
| Orientationd | axcodes | image, label | "RAS" |
| ScaleIntensityRanged | a_min,a_max,b_min,b_max | 仅 image | -175,250,0,1 |
| NormalizeIntensityd | nonzero, channel_wise | 仅 image | nonzero=True |
Compose 支持嵌套,可以把公共部分抽成 base_transforms,再分别拼接增强分支。最后强调一点:Compose 的顺序不是“随便排”,而是由数据依赖和物理语义共同决定的。LoadImage 提供元数据,ChannelFirst 固定维度语义,Spacing 和 Orientation 统一几何参照,IntensityNormalization 统一数值分布,EnsureTyped 完成框架对接。每一步都在改写数据的某个属性,理解这些属性的变化,比记住 API 名称更重要。