1. WSI基础与格式解析
全切片图像(Whole Slide Image,WSI)是病理数字化工作流的起点。它并不是一张“超大照片”,而是由扫描仪在显微镜光路上逐行采集、再拼接压缩得到的多分辨率图像集合。理解它的成像原理、文件格式与金字塔结构,是后续分割、定量分析能否正确读取像素、坐标和物理尺度的前提。
一、WSI是怎么来的:从玻片到像素
数字切片扫描仪的核心是“光学显微 + 线阵/面阵相机 + 精密位移平台”。以常见的20×物镜扫描为例,其光学分辨率约为0.25 μm/pixel,即一个像素对应0.25微米组织。扫描时,平台按固定步进移动玻片,相机逐条带(strip)或逐视野(tile)曝光,系统记录每个视野的位置,再通过图像配准与拼接消除重叠区,形成一张覆盖整张玻片的“全分辨率”图像。由于单张全分辨率图可达数万×数万像素(例如40×下约100000×80000像素,未压缩约24 GB),必须做分块压缩与多级降采样,于是产生了金字塔层级。
这里有一个关键约定:WSI中的坐标通常以“层级0(level 0)”为基准,即最高分辨率层。其他层级都是level 0按2的幂次降采样得到。例如level 1是level 0的1/2,level 2是1/4,依此类推。OpenSlide等库读取时,返回的坐标默认是level 0坐标,这一点在后续做分割掩膜映射时必须牢记。
二、常见格式:SVS、NDPI、MRXS等
不同厂商的扫描仪输出不同容器格式,但底层大多遵循“金字塔 + 瓦片(tile)+ 元数据”的结构。常见格式对比如下:
| 格式 | 典型厂商/来源 | 容器基础 | 特点 |
|---|---|---|---|
| SVS | Aperio(Leica) | TIFF | 兼容性好,OpenSlide支持成熟,常含JPEG压缩瓦片 |
| NDPI | Hamamatsu | TIFF | 含宏图与标签图,瓦片常为JPEG,元数据字段较丰富 |
| MRXS | 3DHISTECH | 文件夹+索引 | 由多个文件组成,需整体目录读取,不能只拷贝单文件 |
| SCN | Leica | TIFF | 部分版本需专用读取路径 |
| DICOM WSI | 通用标准 | DICOM | 标准化程度高,适合归档与交换,读取需DICOM库 |
格式差异主要体现在:瓦片尺寸(如256×256或512×512)、压缩方式(JPEG/JPEG2000/WebP)、金字塔层数、是否内嵌缩略图(thumbnail)与标签图(label/macro)。实际项目中,若只处理SVS,可直接用OpenSlide;若涉及MRXS,必须保证整个文件夹完整。
三、金字塔层级与瓦片结构
金字塔的本质是“同一张切片的多级缓存”。以一张SVS为例,常见层级为:level 0(20×,0.25 μm/pixel)、level 1(10×,0.5 μm/pixel)、level 2(5×,1.0 μm/pixel)、level 3(2.5×,2.0 μm/pixel),以及一张缩略图。每一层内部再按瓦片切分,读取时按需加载,避免一次性载入整图。
下面这张SVG展示了WSI的金字塔层级、瓦片切分与元数据读取路径的关系,帮助建立“层级—坐标—物理尺度”的整体印象。
四、元数据读取与OpenSlide实战
OpenSlide是读取WSI最常用的开源库,支持SVS、NDPI、MRXS、SCN等格式。安装后,核心操作只有几步:打开文件、读取元数据、按层级读取区域、关闭文件。下面给出一个可直接运行的Python示例,用于读取一张SVS并输出关键元数据。
import openslide
slide = openslide.OpenSlide("sample.svs")
# 1. 金字塔层数
print("level_count:", slide.level_count)
# 2. 每层尺寸与降采样因子
for i in range(slide.level_count):
dims = slide.level_dimensions[i]
ds = slide.level_downsamples[i]
print(f"level {i}: size={dims}, downsample={ds:.2f}")
# 3. 关键元数据
props = slide.properties
print("mpp-x:", props.get("openslide.mpp-x"))
print("mpp-y:", props.get("openslide.mpp-y"))
print("objective-power:", props.get("openslide.objective-power"))
print("vendor:", props.get("openslide.vendor"))
# 4. 读取Level 2的一块区域(坐标以Level 0为基准)
region = slide.read_region((4000, 3200), 2, (512, 512))
region = region.convert("RGB")
region.save("level2_region.png")
# 5. 读取缩略图
thumb = slide.get_thumbnail((1024, 1024))
thumb.save("thumbnail.png")
slide.close()
运行后,典型输出可能如下(示例数据,实际以文件为准):
level_count: 4
level 0: size=(100352, 79872), downsample=1.00
level 1: size=(50176, 39936), downsample=2.00
level 2: size=(25088, 19968), downsample=4.00
level 3: size=(6272, 4992), downsample=16.00
mpp-x: 0.25
mpp-y: 0.25
objective-power: 20
vendor: aperio
这里有两个容易踩坑的点。第一,read_region的坐标参数是Level 0坐标,而不是目标层级的坐标;第二个参数才是层级。第二,level_dimensions返回的是该层级的宽高,若直接用该层坐标去读,会得到错误区域。正确做法是:先确定目标层级,再把Level 0坐标传入,由OpenSlide内部完成换算。
最后强调一个贯穿本课程的原则:WSI的所有分析都建立在“层级—坐标—物理尺度”三者一致的基础上。分割模型在Level 2上推理,掩膜要映射回Level 0;定量结果若以微米为单位,必须用mpp换算;可视化时若用缩略图,要按比例还原。把这一章的金字塔结构和元数据读取掌握牢固,后续的像素级分割与定量分析才不会在坐标和尺度上出错。