病理图像数据获取与预处理
开始讲数据了。说实话,搞空间分析这个领域,数据预处理占掉我六成以上的时间。你模型再牛,喂进去的是脏数据,出来的就是垃圾。说白了,这章就是教你怎么把一张张病理切片变成干净、可用、还能对齐空间信息的数字资产。
图像来源: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 密度 |
| 空间 | 最近邻距离 | 细胞聚集程度 |
特征抽取完后,建议把 x、y 坐标、切片 ID、特征向量全部存成 Parquet 格式。后续做空间邻域分析时,按坐标索引会快很多。
核心工作流就是这样:扫描 → 预拼接 → 归一化 → 组织分割 → 核分割 → 特征抽取。每一步都有坑,但掌握了这些,你手里的病理图像就能真正进入空间分析的管线了。