2. WSI文件格式解析

全切片图像(Whole Slide Image,WSI)并不是一张“超大 JPEG”,而是一个带有层级结构、分块存储、附带丰富元数据的容器。理解它的内部组织方式,是后续做切片、染色归一化、模型训练和可视化浏览的前提。本节围绕四个核心问题展开:金字塔层级如何组织分辨率、tile 如何切分与寻址、元数据描述了什么、以及面对各厂商私有格式时如何用通用接口统一读取。

一、金字塔层级:一张切片的多分辨率表示

扫描仪在最高分辨率下采集的整张玻片,单张图像往往达到数万×数万像素,例如 100000×80000,直接解码会占用数十 GB 内存。因此 WSI 普遍采用图像金字塔:底层是最高分辨率的 Level 0,向上逐级降采样,每一级约为下一级的一半宽高。常见的层级关系可以写成:

Level 0: 100000 x 80000   (40x, downsample = 1)
Level 1:  50000 x 40000   (20x, downsample = 2)
Level 2:  25000 x 20000   (10x, downsample = 4)
Level 3:  12500 x 10000   ( 5x, downsample = 8)
Level 4:   6250 x  5000   (2.5x, downsample = 16)

这里的 downsample 表示该层相对于 Level 0 的降采样倍数。需要特别注意:层号不等于放大倍数,不同厂商可能从 0 开始编号,也可能存在非 2 的整数倍降采样。判断某一层实际对应多少倍,应结合元数据中的 mpp(microns per pixel,每像素微米数)计算,而不是凭层号猜测。例如 Level 0 的 mpp 为 0.25 μm/px,则 Level 2 的 mpp 约为 1.0 μm/px,对应约 10x 的等效分辨率。

金字塔的意义在于:浏览时用低层快速渲染缩略图,分析时按需读取高层局部区域。做病理 AI 时,通常先确定目标物理分辨率(如 0.5 μm/px),再反查应读取哪一层,而不是固定读 Level 0。

WSI 金字塔层级与 tile 寻址关系 Level 0 · 100000×80000 mpp 0.25 μm/px · 40x downsample = 1 tile 网格:512×512 列数 196 · 行数 157 Level 1 · 50000×40000 mpp 0.5 μm/px · 20x downsample = 2 Level 2 · 25000×20000 mpp 1.0 · 10x Level 3 · 12500×10000 mpp 2.0 · 5x Level 0 局部 tile 网格 tile (3,2) tile 坐标 (col,row) → 像素偏移 (col×512, row×512) 元数据与通用读取接口 • 尺寸 / 层数 / tile 大小 / mpp • 厂商私有标签(XML / 二进制) • 染色、扫描仪、放大倍数 • 通用接口:openslide / tiffslide • 统一输出:level_count / dimensions read_region(loc, level, size)

二、tile 组织:分块存储与坐标寻址

金字塔的每一层并不是按行连续存储的整幅位图,而是切成固定大小的 tile(常见 256×256 或 512×512),每个 tile 独立压缩(JPEG、JPEG2000、WebP 等)。这样做的直接好处是:读取某个局部区域时,只需解码覆盖该区域的少数 tile,而不必解压整层。

tile 的寻址依赖两个坐标:层号 level 和 tile 网格坐标 (col, row)。给定像素坐标 (x, y),其所在 tile 为:

col = x // tile_width
row = y // tile_height
tile_index = row * tiles_across + col

以 Level 0 为 100000×80000、tile 为 512×512 为例,tiles_across = ceil(100000/512) = 196,tiles_down = ceil(80000/512) = 157。像素点 (1300, 900) 落在 col=2、row=1 的 tile 内。注意边缘 tile 可能不足 512×512,读取时需要做边界填充或裁剪。

实际工程中,我们很少直接操作 tile 索引,而是通过 read_region(location, level, size) 这类接口按像素区域读取。但理解 tile 组织有助于解释两个常见现象:一是读取跨 tile 的大区域时耗时并非线性增长;二是某些格式在 tile 边界处存在轻微接缝或压缩伪影。

三、元数据:描述切片的关键信息

元数据是 WSI 的“说明书”,通常包括:图像尺寸与层数、每层 downsample、tile 大小、mpp、物镜放大倍数、染色方式(H&E、IHC 等)、扫描仪型号、扫描时间、以及厂商自定义字段。其中 mpp 和 downsample 直接决定物理尺度换算,是分析流程中最常用的参数。

物理尺寸换算关系为:

物理长度(μm) = 像素数 × mpp(μm/px)
目标层 mpp = Level0_mpp × downsample

例如要在 0.5 μm/px 下读取 1024×1024 的区域,若 Level 0 的 mpp 为 0.25,则应选择 downsample=2 的层,读取 1024×1024 像素,对应物理区域为 512 μm × 512 μm。若元数据缺失 mpp,可借助厂商标签或已知放大倍数估算,但需在报告中注明假设。

四、厂商私有标签与通用读取接口

不同厂商的 WSI 格式差异很大:Aperio 的 .svs 基于 TIFF,私有信息放在 ImageDescription 的 XML 中;Hamamatsu 的 .ndpi 也是 TIFF 变体,但标签布局不同;Leica 的 .scn 使用自有容器;Philips 的 .isyntax 则是完全不同的二进制格式。这些私有标签往往包含扫描参数、聚焦信息、甚至部分区域的质量标记。

如果为每种格式单独写解析器,维护成本极高。通用读取接口的价值就在于屏蔽差异,对外暴露统一 API。目前主流方案是 OpenSlide 与 tiffslide:前者支持多种厂商格式,后者基于 tifffile,对 TIFF 系格式更轻量。它们通常提供以下统一能力:

  • level_count:金字塔层数;
  • level_dimensions:每层宽高;
  • level_downsamples:每层降采样倍数;
  • properties:mpp、vendor、objective-power 等键值;
  • read_region(location, level, size):按区域读取并返回 RGB 图像。

下面是一段典型的读取示例,用于确认切片基本信息并读取指定区域:

from openslide import OpenSlide

slide = OpenSlide("sample.svs")
print(slide.level_count)              # 例如 5
print(slide.level_dimensions)         # [(100000,80000), ...]
print(slide.level_downsamples)        # (1.0, 2.0, 4.0, ...)
print(slide.properties.get("openslide.mpp-x"))  # 0.25

# 在 Level 2 读取 1024x1024 区域
region = slide.read_region((20000, 16000), 2, (1024, 1024))
region = region.convert("RGB")
读取前先检查 level_count 与 level_downsamples,再根据目标 mpp 选择层号,可避免误读低分辨率层导致模型输入模糊。
部分格式的 read_region 坐标以 Level 0 为基准,size 以目标层像素为单位;不同库对坐标语义可能略有差异,跨库迁移时务必用已知区域验证。

五、实践中的判断与验证

拿到一个未知 WSI 文件时,建议按以下顺序确认:先用通用接口打开,打印层数、每层尺寸与 downsample;再读取 properties,确认 mpp 与放大倍数是否存在;然后选取一个已知组织区域,分别在 Level 0 和目标层读取同一物理位置,比较清晰度与尺寸是否符合预期;最后检查边缘 tile 是否正常,避免边界填充引入黑边。若 mpp 缺失,可结合扫描仪型号与放大倍数做合理假设,并在数据说明中标注。

理解金字塔、tile、元数据与通用接口这四层关系后,后续的切片采样、缓存策略、多尺度融合和格式转换都会变得有据可依。真正决定工程稳定性的,往往不是模型结构,而是这些底层读取细节是否被正确处理。