病理图像数据获取与预处理

开始讲数据了。说实话,搞空间分析这个领域,数据预处理占掉我六成以上的时间。你模型再牛,喂进去的是脏数据,出来的就是垃圾。说白了,这章就是教你怎么把一张张病理切片变成干净、可用、还能对齐空间信息的数字资产。

图像来源:H&E 与 IF

先分清两类片子。

类型 原理 我常用的场景
H&E 苏木精染核(蓝紫),伊红染胞浆/基质(粉红) 组织学形态、核分级、腺体结构
IF(免疫荧光) 抗体标记特定蛋白,多通道荧光成像 标记 CD8、PD-L1 等,做细胞表型定量

我个人习惯是:做空间分析时优先把两种都拿到。H&E 给结构,IF 给分子。但接下来的预处理管线,两者的痛点不太一样。

全切片扫描与图像拼接

现在的数字病理扫描仪,扫一张 H&E 切片能产出几十 GB 的 SVS 文件。为什么这么大?因为分辨率到了 0.5 微米/像素,你想想看,一个细胞核才 10 微米宽,细节全在里面了。

SVS 文件实质上是多分辨率金字塔,顶层是缩略图,底层是 40x 原图。我曾在项目里直接用 OpenSlide 库读取,这段代码你直接拿去用:

import openslide
slide = openslide.OpenSlide("sample.svs")
# 读取全分辨率
tissue_wsi = slide.read_region((0, 0), 0, slide.dimensions)
# 读取20x下采样
tissue_20x = slide.read_region((0, 0), 1, slide.level_dimensions[1])

有时扫描仪会扫出 Tile,你得自己拼。这里的关键是 overlap 区域怎么融合。我踩过一个坑:直接用平均融合,结果拼接边缘出现双影。后来改用线性融合,权重从 overlap 起始处 1 渐变为 0,效果好了很多。

# 简单权重融合示意
alpha = np.linspace(1, 0, overlap_width)
tile1[:, -overlap_width:, :] = tile1[:, -overlap_width:, :] * alpha
tile2[:, :overlap_width, :] = tile2[:, :overlap_width, :] * (1 - alpha)

拼接顺序别搞错。务必先配准再融合,否则组织撕裂了后面全部白做。

颜色归一化

为什么要做?不同医院的染色批次不同,切片的颜色深浅差别很大。如果你用深度学习模型直接跑,模型很容易学到染色批次特征,而不是组织特征。

我以前做乳腺癌数据集时吃过亏。模型在训练集上准确率 95%,换了一批切片掉到 60%。查了半天,发现是染色深浅不同,模型学会了看颜色深浅判断良恶性。

主流做法是色彩反卷积。把 RGB 图像转换成苏木精、伊红、DAB(IF 里)三个通道的光密度值。然后做归一化。

# 使用 StainTools 库
from staintools import StainNormalizer

target = staintools.read_image("target.png")
normalizer = StainNormalizer(method='macenko')
normalizer.fit(target)

# 对每张图做标准化
source = staintools.read_image("source.png")
source_norm = normalizer.transform(source)

建议选一张染色均匀、结构清晰的切片作为模板。不要用坏死区域多的片子做 target。

组织分割

图像大了,不可能全图处理。第一步是先找出哪些区域是组织,哪些是背景(白片或玻璃)。

手动框?不现实。一张全切片 10 万 × 10 万像素,框到你手断。我一般用 Otsu 阈值法 + 形态学开闭运算,又快又稳。

import cv2
import numpy as np

def segment_tissue(rgb_image):
    # 转灰度
    gray = cv2.cvtColor(rgb_image, cv2.COLOR_RGB2GRAY)
    # Otsu
    _, tissue_mask = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    # 形态学去噪
    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
    tissue_mask = cv2.morphologyEx(tissue_mask, cv2.MORPH_CLOSE, kernel)
    return tissue_mask

如果你需要更精细的,可以用 QuPath。这个开源软件特别好用。我的管线通常是:先用 Otsu 粗筛,再用 QuPath 的可视化界面微调边界,尤其是在肿瘤与间质交错的位置。

QuPath 使用小记:先导入 SVS,在「Auto」菜单里找「Cell detection」,选「Positive cell detection」能快速标记阳性细胞。不过别全自动,记得手动检查。

核分割与特征提取

核分割是空间分析的基石。你后面算细胞密度、邻域关系、距离,都依赖这一步骤。

传统方法用分水岭算法或形态学重建。我早期项目里用过,但遇到核密集区域效果很差。后来转向深度学习,用了 U-Net 变体。

HoVer-Net 是我比较推荐的。它同时输出核分割和核分类(肿瘤、炎性、基质等)。

# 假设 HoVer-Net 已训练好
from hovernet_inference import run_hovernet

instances_map, nuclei_types = run_hovernet("tissue_patch.png")
# instances_map: 每个像素的实例ID
# nuclei_types: 每个核的类型标签

核分割之后做什么?特征抽取。每个核可以算几十个特征:

  • 形态特征:面积、周长、圆度、核质比
  • 纹理特征:灰度共生矩阵、局部二值模式
  • 染色特征:平均光密度、染色强度分散度
  • 空间特征:与最近邻核的距离、核密度估计

我自己整理的核特征表,可以参考:

特征类别 具体特征 生物学含义
形态 面积、圆度 核异型性
纹理 熵、对比度 染色质分布
染色强度 平均 OD DNA 密度
空间 最近邻距离 细胞聚集程度
病理图像预处理流程 原始WSI SVS/TIF格式 扫描/拼接 Tile融合 颜色归一化 Macenko法 组织分割 Otsu + QuPath 核分割 神经网络 特征提取 形态/纹理/空间 可分析的特征矩阵 每个细胞核 → 多维特征向量 预处理核心原则 1. 保留空间信息 2. 消除批次差异 3. 确保核分割准确 4. 特征兼顾形态与空间 5. 可溯源性(保留原始坐标)

特征抽取完后,建议把 x、y 坐标、切片 ID、特征向量全部存成 Parquet 格式。后续做空间邻域分析时,按坐标索引会快很多。

核心工作流就是这样:扫描 → 预拼接 → 归一化 → 组织分割 → 核分割 → 特征抽取。每一步都有坑,但掌握了这些,你手里的病理图像就能真正进入空间分析的管线了。