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