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

医学影像数据治理的第一步,不是写脱敏规则,而是把“数据长什么样”这件事彻底搞清楚。不同模态的影像来自不同设备、不同厂商、不同采集协议,落到磁盘上的文件格式也完全不同。如果工程师把 DICOM 当成普通图片、把 NIfTI 当成带头的数组、把病理切片当成超大 TIFF,后续的匿名化、脱敏、去标识、质量校验都会在错误的前提上展开。本节从工程视角梳理三类最常见的数据来源及其格式结构,重点讲清“元数据与像素数据如何分离”,并给出多模态影像进入处理流水线时的统一入口设计。

一、三类主流格式的结构差异

DICOM(.dcm)是临床影像的事实标准,覆盖 CT、MR、US、CR、MG、PET 等。一个 DICOM 文件由文件头(128 字节前导 + “DICM”魔数)和若干数据元素(Data Element)组成,每个元素用标签(Group, Element)标识,例如 (0010,0010) 是患者姓名,(7FE0,0010) 是像素数据。DICOM 的关键特征是:元数据与像素数据同文件共存,且元数据以显式或隐式 VR(Value Representation)编码,还可能是未压缩或压缩(JPEG、JPEG-LS、JPEG2000、RLE)的像素流。一个检查(Study)通常由多个序列(Series)组成,每个序列包含多张切片(Instance),它们通过 StudyInstanceUID、SeriesInstanceUID、SOPInstanceUID 三级标识串联。

NIfTI(.nii / .nii.gz)主要出现在科研与神经影像处理中,常见于 fMRI、DTI、结构 MRI。它由 348 字节固定头(header)和紧随其后的三维/四维体数据(voxel data)构成,头中记录 dim、pixdim、datatype、scl_slope/scl_inter、qform/sform 等空间信息。与 DICOM 不同,NIfTI 是单文件承载一个体数据,元数据字段数量远少于 DICOM,患者身份信息通常不在文件内,但可能残留在扩展字段或伴随的 JSON 侧车文件中。

病理全切片(WSI,.svs / .ndpi / .mrxs / .tiff)是数字病理的核心格式,单张切片可达数 GB 至数十 GB。其结构通常是“金字塔 + 瓦片”:底层为最高分辨率图像,向上逐级降采样形成多级金字塔,每级再切成固定大小的瓦片(tile),并配有索引。元数据包括扫描仪型号、放大倍率、MPP(微米每像素)、扫描时间、染色信息等,部分格式还内嵌标签图像(label/macro)。WSI 的元数据与像素数据在物理上分离得更彻底,但标签图中常含手写患者信息,是脱敏的高风险点。

三类影像格式的元数据 / 像素数据分离结构 DICOM (.dcm) 128B 前导 + "DICM" 魔数 元数据元素 (Group,Element) 患者/检查/序列/设备/采集参数 像素数据 (7FE0,0010) 未压缩 / JPEG / JPEG-LS / J2K / RLE 元数据与像素同文件,UID 三级串联 NIfTI (.nii / .nii.gz) 348B 固定头 (header) dim / pixdim / datatype / sform 体数据 (voxel data) 3D / 4D 连续数组 scl_slope / scl_inter 线性映射 扩展字段可能残留标识 单文件承载体数据,身份信息少但需查侧车 病理全切片 WSI 金字塔层级 L0 / L1 / L2 … 逐级降采样 瓦片 (tile) + 索引 固定尺寸分块存储 标签图 label / macro 高风险:常含手写患者信息 元数据与像素物理分离,标签图需单独脱敏 多模态统一工程处理入口 格式识别 魔数 / 扩展名 / 头字段 元数据抽取 标签字典 / 头解析 像素/体数据解码 解压 / 重采样 / 归一化 脱敏与输出 去标识 / 重编码 / 校验 统一入口将三类格式归一为「元数据记录 + 像素/体数据数组 + 空间信息」三元组,供后续匿名化与治理复用

二、元数据与像素数据的分离原则

工程上必须建立一条铁律:元数据与像素数据在逻辑上分离,在物理上按需重组。原因有三。第一,脱敏主要作用于元数据,像素数据通常不动或仅做重编码;第二,元数据体量小、可索引、可审计,适合放入数据库;第三,像素数据体量大,适合对象存储或分块存储。以 DICOM 为例,抽取 (0010,xxxx) 患者信息、(0008,xxxx) 检查信息、(0020,xxxx) 序列信息后,像素数据 (7FE0,0010) 可单独落盘为 .raw 或 .npy,并记录偏移、长度、传输语法(Transfer Syntax UID)。NIfTI 则直接读 348 字节头,体数据按 datatype 映射为 numpy 数组。WSI 通过 OpenSlide 或 tiffslide 读取层级与瓦片,元数据以属性字典形式返回。

需要特别注意的是像素数据中可能隐含标识:超声图像常烧录患者姓名与检查时间,CT 定位像可能带体位文字,病理标签图更是手写信息的重灾区。因此“像素不动”只是默认策略,遇到烧录文字必须走 OCR 检测 + 遮盖或重绘流程。

三、多模态统一处理入口的设计

面对三类格式,最忌讳为每种格式写一套独立流水线。推荐做法是设计一个统一入口适配器,把任意输入归一为三元组:metadata_record(键值对字典)、pixel_array(numpy 数组或瓦片迭代器)、spatial_info(spacing、origin、direction、MPP、层级)。后续的匿名化、质量校验、格式转换都只面向这个三元组,不再关心原始格式。

一个可参考的入口伪代码如下,示例参数仅用于说明结构,不代表真实数据:

def open_medical_image(path):
    fmt = detect_format(path)          # 魔数优先,扩展名兜底
    if fmt == "DICOM":
        ds = pydicom.dcmread(path)
        meta = extract_tags(ds, whitelist=PATIENT_TAGS)
        pix  = ds.pixel_array          # 触发解码
        spa  = {"spacing": ds.PixelSpacing,
                "origin": ds.ImagePositionPatient,
                "ts": ds.file_meta.TransferSyntaxUID}
    elif fmt == "NIfTI":
        img = nib.load(path)
        meta = {"descrip": img.header["descrip"]}
        pix  = img.get_fdata()
        spa  = {"pixdim": img.header["pixdim"],
                "sform": img.affine.tolist()}
    elif fmt == "WSI":
        sl = openslide.OpenSlide(path)
        meta = dict(sl.properties)     # 含 scanner、MPP、level_count
        pix  = TileIterator(sl)        # 惰性瓦片迭代
        spa  = {"mpp": sl.properties.get("openslide.mpp-x"),
                "levels": sl.level_count}
    return NormalizedImage(meta, pix, spa)

入口设计还要处理两个工程细节。其一是惰性加载:WSI 动辄数十 GB,不能一次性读入内存,必须用瓦片迭代器按需读取;NIfTI 4D 数据也应支持内存映射(nib.load(path).dataobj)。其二是传输语法与压缩:DICOM 的 JPEG2000 解码依赖 GDCM 或 pylibjpeg,若环境缺失会直接抛异常,入口应捕获并记录为“待转码”状态,而不是让整批任务失败。

四、工程处理入口的检查清单

在把数据送入匿名化流水线之前,建议逐项确认:格式识别是否基于魔数而非扩展名;DICOM 是否已读取 Transfer Syntax 并确认解码器可用;NIfTI 是否检查了 sform/qform 一致性;WSI 是否确认了层级数与 MPP;元数据抽取是否覆盖了患者、检查、设备、采集四类标签;像素数据是否检测了烧录文字;输出三元组是否带有来源路径与校验和(如 SHA-256)。这份清单不追求形式,而是确保后续每一步都有可靠输入。

实践建议:先用 10~20 例覆盖 CT、MR、US、WSI 的小样本跑通入口,打印每例的格式、元数据字段数、像素形状、spacing/MPP,确认无异常后再扩展到全量。入口阶段的错误若被带入脱敏阶段,排查成本会成倍上升。

把格式结构、元数据与像素的分离关系、统一入口这三件事做扎实,后续的匿名化规则、去标识策略、质量校验才有稳定的立足点。多模态影像治理的难点从来不在算法,而在于是否真正理解了数据本身的组织方式。