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

在医学影像 AI 训练流水线中,数据格式解析是第一个真正接触原始数据的环节。它决定了后续预处理、标注对齐、增强和模型输入的正确性。医学影像领域存在多种格式,其中 DICOM 是临床采集与归档的事实标准,NIfTI 是神经影像研究的主流格式,NRRD 则常见于影像组学和部分开源工具链。理解它们的元数据组织方式、像素数据布局以及常用解析库的读写行为,是构建稳定数据管线的必要基础。

三种格式的核心差异

DICOM 以“单次检查、单序列、单帧”为基本单位,一个 CT 或 MR 序列通常由几十到几百个独立文件组成,每个文件都携带完整的患者信息、设备参数、采集几何和像素数据。它的元数据以标签(Tag)形式组织,例如 (0028,0010) 表示行数,(0028,0011) 表示列数,(0020,0032) 表示图像位置,(0028,1052) 表示截距,(0028,1053) 表示斜率。像素值需要经过 HU = pixel * RescaleSlope + RescaleIntercept 转换才能得到具有物理意义的 CT 值。

NIfTI 将整个三维或四维体数据存放在单个文件中,元数据集中在头部(Header),包括维度、体素尺寸、数据类型、仿射矩阵等。它的优势是读写简单、适合体数据整体处理,但缺少 DICOM 那样丰富的采集上下文。NRRD 同样以单文件存储体数据,头部为可读文本,支持多种编码方式,在 3D Slicer 和 PyRadiomics 等工具中广泛使用。

DICOM / NIfTI / NRRD 解析路径与像素数据流 DICOM 序列 多文件 · 单帧 IM-0001.dcm … IM-0120.dcm 元数据标签 (0028,0010) Rows (0020,0032) ImagePosition (0028,1052/1053) Rescale 像素数据 int16 / uint16 原始值 HU 转换 HU = px × slope + intercept 几何排序 按 ImagePosition 沿法向排序 NIfTI 体数据 单文件 · 3D/4D volume.nii / volume.nii.gz Header 元数据 dim / pixdim / datatype scl_slope / scl_inter qform / sform 仿射矩阵 像素数据 连续内存块 · 按维度展开 仿射映射 体素坐标 → 世界坐标 方向一致性 RAS / LAS 轴方向约定 NRRD 体数据 单文件 · 文本头 volume.nrrd Header 字段 type / dimension / sizes space directions / origin encoding: raw / gzip 像素数据 按 encoding 解码后读取 空间信息 origin + space directions 工具链兼容 3D Slicer / PyRadiomics

DICOM 解析实战:从序列到体数据

使用 Python 生态中最常用的 pydicom 库读取单个 DICOM 文件非常直接:

import pydicom
import numpy as np

ds = pydicom.dcmread("IM-0001.dcm")
rows = ds.Rows
cols = ds.Columns
pixel = ds.pixel_array.astype(np.float32)
hu = pixel * float(ds.RescaleSlope) + float(ds.RescaleIntercept)
print(rows, cols, hu.min(), hu.max())

但实际训练中面对的是一个序列,需要把多个切片按空间位置排序后堆叠成三维体数据。关键步骤是读取每个文件的 ImagePositionPatient 和 ImageOrientationPatient,计算切片沿法向的投影位置,再按该位置升序排列。如果忽略这一步,直接按文件名排序,在部分设备导出的数据中会出现层序错乱,导致三维重建和病灶定位出现严重偏差。

另一个常见问题是像素间距不一致。DICOM 中的 PixelSpacing 给出层内两个方向的实际物理尺寸,SliceThickness 或相邻层位置差给出层间间距。当层间距远大于层内间距时,体数据在 Z 方向被“拉长”,直接输入各向同性假设的模型会产生几何畸变。工程上通常先重采样到统一间距,例如将 0.7 mm × 0.7 mm × 5 mm 重采样为 1 mm × 1 mm × 1 mm,再进入后续流程。

NIfTI 与 NRRD 的读写要点

NIfTI 常用 nibabel 读写。读取时除了拿到 dataobj,还要关注 header.get_zooms() 和 affine。仿射矩阵定义了体素索引到世界坐标的映射,做多模态配准、标注坐标转换时必须使用它,而不能假设原点在图像角点。

import nibabel as nib

img = nib.load("volume.nii.gz")
data = img.get_fdata()          # float64,已应用 scl_slope/inter
zooms = img.header.get_zooms()  # 体素物理尺寸
affine = img.affine             # 4x4 仿射矩阵
print(data.shape, zooms)

NRRD 可以用 pynrrd 读取,其头部字段 space directions 与 NIfTI 的仿射矩阵作用类似,但表达方式不同。跨格式转换时,最容易出错的地方是轴顺序和方向约定:NIfTI 常用 RAS 约定,而部分 DICOM 和 NRRD 数据可能使用 LPS 或 LAS。若不做方向统一,左右侧结构可能被镜像,这在脑部或肺部病灶分析中是不可接受的错误。

方向一致性检查:在把 DICOM 序列转为 NIfTI 后,建议用 nib.aff2axcodes(img.affine) 查看轴方向编码,确认与预期一致。若出现左右翻转,需要在仿射矩阵或数据轴上做显式修正,而不是靠模型“自己学”。

元数据与像素数据的分离管理

在流水线设计中,建议把元数据和像素数据分开管理。元数据表可以包含:患者/检查匿名 ID、序列描述、层厚、像素间距、图像方向、重采样后间距、窗宽窗位、数据来源格式等字段。像素数据则统一转换为 NumPy 数组或内存映射文件,按固定命名规则存放。这样做的直接好处是:预处理参数可以按序列批量配置,数据增强不会误改几何信息,训练时的数据加载也不必反复解析原始格式。

格式 典型解析库 元数据关键字段 像素数据注意点
DICOM pydicom Rows/Columns、PixelSpacing、ImagePositionPatient、RescaleSlope/Intercept 需按位置排序、应用 HU 转换、注意层间距
NIfTI nibabel dim、pixdim、affine、scl_slope/inter get_fdata 已应用缩放,注意轴方向 RAS/LAS
NRRD pynrrd dimension、sizes、space directions、origin、encoding 按 encoding 解码,注意 space directions 与仿射的对应

一个可复用的解析检查清单

在把任何一批医学影像数据接入训练流水线之前,建议按以下顺序做一次解析检查:

  1. 确认格式与解析库版本,读取一个样本文件,打印形状、数据类型和值范围。
  2. 对 DICOM 序列,检查切片数量、层间距是否均匀、是否存在缺失层或重复层。
  3. 对 NIfTI/NRRD,检查仿射矩阵或 space directions,确认体素尺寸和方向编码。
  4. 验证像素值转换:CT 的 HU 范围是否合理(如空气约 -1000,水约 0,骨密度可到 1000 以上)。
  5. 检查方向一致性,必要时统一到 RAS 或 LPS,并记录转换关系。
  6. 将元数据写入结构化表,像素数据转为统一数组格式,完成一次端到端读取测试。

完成这些检查后,数据解析环节才算真正稳定。后续的预处理、增强和模型训练都建立在这一步的正确性之上,任何方向、间距或值域上的疏漏都会在训练结果中被放大。