2. DICOM影像数据解析与预处理

在PACS影像AI在线推理链路中,DICOM解析与预处理是决定模型输入质量的第一道关口。模型最终看到的并不是PACS里那个.dcm文件,而是一个归一化后的像素张量。如果解析阶段读错了像素、窗宽窗位映射不一致、多帧序列顺序错乱,后续推理结果再优化也无法挽回。本节围绕DICOM文件结构、元数据读取、像素解码、窗宽窗位调整、多帧多序列处理以及常见伪影与预处理策略展开。

一、DICOM文件结构:从文件头到像素数据

一个标准DICOM文件由文件前言、数据集和像素数据三部分组成。文件前言共128字节,通常为全零,其后紧跟4字节的“DICM”标识,用于快速判断是否为DICOM文件。数据集由一系列数据元素(Data Element)构成,每个元素包含标签(Tag)、值表示(VR)、值长度(Value Length)和值域(Value Field)。标签由组号(Group)和元素号(Element)组成,例如(0028,0010)表示图像行数Rows,(0028,0011)表示列数Columns,(7FE0,0010)则是像素数据Pixel Data。

传输语法(Transfer Syntax UID,标签(0002,0010))决定了像素数据是否压缩、字节序如何排列。常见的有隐式VR小端(1.2.840.10008.1.2)、显式VR小端(1.2.840.10008.1.2.1)、JPEG Baseline、JPEG Lossless、JPEG 2000等。解析器必须先读取传输语法,才能正确解码后续数据。若忽略这一点,把压缩像素当作原始像素读取,会得到完全错误的图像。

DICOM文件结构与解析路径 文件前言 128字节 + "DICM" 数据集(元数据) Tag / VR / Length / Value 像素数据 (7FE0,0010) 传输语法决定解码方式 压缩/未压缩 · 字节序 关键元数据标签 (0028,0010) Rows · (0028,0011) Columns · (0028,0100) BitsAllocated · (0028,0101) BitsStored (0028,0102) HighBit · (0028,0103) PixelRepresentation · (0028,1050) WindowCenter · (0028,1051) WindowWidth (0028,0002) SamplesPerPixel · (0028,0008) NumberOfFrames · (0020,0032) ImagePositionPatient 像素解码与预处理流水线 读取传输语法 解压像素流 应用Rescale Slope/Intercept 窗宽窗位映射 → 8bit显示 归一化 [0,1]或[-1,1] 输出模型张量 NCHW / NHWC

二、元数据读取与像素数据解码

元数据读取的核心是正确解析数据元素。以Python为例,常用pydicom库:

import pydicom
import numpy as np

ds = pydicom.dcmread("CT_001.dcm")
print(ds.TransferSyntaxUID)          # 传输语法
print(ds.Rows, ds.Columns)           # 图像尺寸
print(ds.BitsAllocated, ds.BitsStored, ds.HighBit)
print(ds.PixelRepresentation)        # 0=无符号, 1=有符号
print(ds.RescaleSlope, ds.RescaleIntercept)
print(ds.WindowCenter, ds.WindowWidth)

# 解码像素数据为numpy数组
pixel_array = ds.pixel_array.astype(np.float32)
# 应用Rescale Slope/Intercept,转为物理量(如CT的HU值)
hu = pixel_array * float(ds.RescaleSlope) + float(ds.RescaleIntercept)

这里有一个极易出错的点:BitsStored与HighBit决定了有效位的位置。例如BitsAllocated=16、BitsStored=12、HighBit=11时,有效数据位于低12位,高4位可能是填充位。若直接按16位读取而不做位移或掩码,会引入错误的高位噪声。此外,PixelRepresentation=1表示有符号像素,解码后需按有符号整数解释,否则负值会被误读为大正数。

三、窗宽窗位调整

医学影像的原始像素动态范围通常为12~16位,而显示器和多数模型输入为8位。窗宽(Window Width, WW)和窗位(Window Center, WC)定义了将原始值映射到显示灰度的区间。映射公式为:

下限 = WC − WW/2,上限 = WC + WW/2

将原始值线性截断并缩放到0~255:

def apply_window(hu, wc, ww):
    lower = wc - ww / 2
    upper = wc + ww / 2
    windowed = np.clip(hu, lower, upper)
    windowed = (windowed - lower) / (upper - lower) * 255.0
    return windowed.astype(np.uint8)

以CT为例,肺窗常用WC=−600、WW=1500,用于观察肺实质;纵隔窗常用WC=40、WW=400,用于观察软组织与血管;骨窗常用WC=400、WW=1800。同一份CT数据,选择不同窗宽窗位,模型看到的输入分布差异巨大。如果AI模型训练时使用的是肺窗,推理时却用了纵隔窗,性能会显著下降。因此,预处理阶段必须与模型训练时的窗宽窗位策略严格对齐,或采用多窗融合、自适应窗等策略。

四、多帧与多序列处理

多帧图像(如超声、X射线透视、部分MR序列)在单个DICOM文件中包含NumberOfFrames帧,像素数据按帧连续排列。解码后数组形状为(frames, rows, columns)或(frames, rows, columns, samples)。处理时需注意帧与帧之间的时间或空间顺序,不能简单打乱。

多序列则涉及同一检查下的多个Series,例如MR的T1、T2、DWI、ADC等。每个Series有独立的SeriesInstanceUID,但共享StudyInstanceUID。AI推理前需要根据SeriesDescription、ProtocolName或ImageType筛选目标序列。常见做法是:先按StudyInstanceUID分组,再按SeriesNumber排序,最后根据序列描述匹配模型所需的输入通道。例如一个脑肿瘤分割模型可能需要T1、T1ce、T2、FLAIR四个序列,预处理时必须确保四个序列的空间对齐(同一ImagePositionPatient网格)和像素间距一致。

五、常见伪影与预处理策略

DICOM数据在采集和传输过程中可能引入多种伪影,预处理阶段需要识别并处理:

  • 金属伪影:CT中金属植入物导致条状伪影,HU值异常。可在预处理时对高密度区域做阈值截断,或使用金属伪影校正算法。
  • 运动伪影:患者移动导致图像模糊或重影。可通过图像质量评估指标(如清晰度、信噪比)筛选,低质量帧不送入模型。
  • 截断伪影:FOV外组织被截断,重建时产生亮暗条纹。可对边缘区域做加窗或填充处理。
  • 灰度不均匀:MR表面线圈导致信号强度不均。可应用N4偏置场校正或简单的高斯滤波。
  • 像素填充值:部分设备用PixelPaddingValue标记无效区域,预处理时应将其掩码掉,避免参与归一化统计。
预处理策略的核心原则是:与训练分布对齐。推理时的重采样插值方法、归一化方式、窗宽窗位、通道顺序,都应与模型训练时保持一致。建议将预处理参数固化为配置文件,随模型版本一同管理。
注意:不要对原始HU值直接做全局归一化。CT的HU值有明确物理意义(水=0,空气=−1000,骨=+1000以上),全局归一化会破坏这一语义。正确做法是先按窗宽窗位截断,再缩放到[0,1]或[−1,1]。

综上,DICOM解析与预处理是一个需要精细控制的工程环节。从传输语法识别、元数据校验、像素解码、Rescale映射、窗宽窗位调整,到多帧多序列的组织与伪影处理,每一步都直接影响模型输入的可靠性。在实际集成中,建议将这一环节封装为独立的预处理服务,输入DICOM文件路径或PACS查询结果,输出标准化的NIfTI或NumPy张量,并记录关键参数日志,便于问题追溯与版本对比。