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 为例,抽取 (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)。这份清单不追求形式,而是确保后续每一步都有可靠输入。
把格式结构、元数据与像素的分离关系、统一入口这三件事做扎实,后续的匿名化规则、去标识策略、质量校验才有稳定的立足点。多模态影像治理的难点从来不在算法,而在于是否真正理解了数据本身的组织方式。