3、空间坐标与组织图像:解析spatial字段、H&E图像对齐

拿到一个Squidpy能用的Visium数据后,我最先做的事不是跑邻域分析,而是先看空间坐标和H&E图像有没有对齐。你想想看,坐标偏了一个像素,后面算出来的邻域关系全是错的。这章我们就来解剖一下spatial字段里到底装了啥,以及怎么把图像和坐标严格对上。

3.1 spatial字段到底长什么样?

在AnnData对象里,空间坐标藏在adata.obsm['spatial']。我每次打开新数据集,第一件事就是看它的shape:

import scanpy as sc
import squidpy as sq

adata = sq.datasets.visium_hne_adata()
print(adata.obsm['spatial'].shape)   # 通常是 (n_spots, 2)
print(adata.obsm['spatial'][:5])

打印出来你会看到两列数字,单位是像素。第一列是X,第二列是Y。注意⚠️ 这个坐标系的Y轴方向跟图像显示是反的——图像里Y向下增长,但这里Y向上增长。我早期没注意这个,直接画图发现点全跑到图像外面去了。

字段名维度含义
obsm['spatial'](n_spots, 2)每个spot的像素坐标 (x, y)
obsm['spatial_neighbors'](n_spots, n_spots)邻接矩阵(后续章节讲)
uns['spatial']dict包含图像、缩放信息、组织检测结果等

3.2 从像素坐标到真实物理坐标?别急

很多人以为spatial里面的坐标就是微米级真实坐标。翻车了。它只是图像上的像素位置。要换算真实距离,得靠adata.uns['spatial'][library_id]['scalefactors']里的信息。举个例子:

scalefactors = adata.uns['spatial']['V1_Human_Lymph_Node']['scalefactors']
print(scalefactors['spot_diameter_fullres'])  # 全分辨率下spot直径(像素)
print(scalefactors['tissue_hires_scalef'])     # 高分辨率图像相对于全图的缩放比

Visium的spot直径在全分辨图上大约是89像素,相邻spot中心间距约100像素。但不同组织样本的缩放因子不一样,千万别硬编码。我曾经有一个项目,跑了三天邻域分析,最后发现图像缩放因子用错了,所有距离都偏了20%,重头再来。所以我建议你在预处理阶段就把缩放因子存起来,一劳永逸:

adata.uns['spot_size_fullres'] = scalefactors['spot_diameter_fullres']
adata.uns['scalef_hires'] = scalefactors['tissue_hires_scalef']

3.3 H&E图像如何与坐标对齐?

这是新手最容易懵的地方。Visium官方提供的是全分辨率(fullres)图像和低分辨率(hires)图像。坐标默认是按全分辨率算的。但实际你更可能用hires来显示,因为全图太大。对齐的关键三步:

  1. 读取hires图像:sq.datasets.visium_hne_image() 或自己加载tissue_hires_image.png
  2. 缩放坐标:将spatial坐标乘以tissue_hires_scalef(注意不是除以!)
  3. 翻转Y轴:因为matplotlib/imshow的Y轴从上往下,而空间坐标Y向上,所以需要y = image_height - y

代码实现:

import matplotlib.pyplot as plt
import numpy as np

image = adata.uns['spatial']['V1_Human_Lymph_Node']['images']['hires']
scalef = adata.uns['spatial']['V1_Human_Lymph_Node']['scalefactors']['tissue_hires_scalef']
coords = adata.obsm['spatial'] * scalef  # 缩放

# 翻转Y
h, w, _ = image.shape
coords[:, 1] = h - coords[:, 1]

plt.figure(figsize=(8,8))
plt.imshow(image)
plt.scatter(coords[:,0], coords[:,1], s=5, c='red', alpha=0.5)
plt.axis('off')
plt.show()

你会发现红色的spatial点完美覆盖在组织切片上。如果点全部漂移到图像外部或者只集中在某个角落,那就是Y轴翻转或缩放因子错了。

经验总结: 对齐测试用1-2个典型Spot手动核对。比如你找一个位于组织边缘的Spot,在图像上用肉眼确认其位置,再对比代码打印的坐标是否一致。这一步花不了5分钟,但能救你半天的debug时间。

3.4 可视化比对:检查组织检测掩膜

除了手动画散点图,Squidpy提供了更专业的可视化工具sq.pl.spatial_scatter()。你看看:

sq.pl.spatial_scatter(adata, shape=None, color='total_counts', 
                      library_id='V1_Human_Lymph_Node',
                      img=True, img_channel=None)

这个函数自动处理了Y轴翻转和缩放,背后调用了我们刚才手动做的那一套。不过我还是建议你至少自己写一次原始坐标映射,这样出问题的时候能快速定位。

另外,adata.uns['spatial'][library_id]['images']['hires']里存的图像是RGB 8位无符号整型,最大值255。如果你要自己做图像处理(比如分割、染色分离),记得先归一化到[0,1]:

image_norm = image.astype(np.float32) / 255.0

3.5 SVG: 空间坐标与图像对齐流程

下面这张图把整个对齐路径画清楚了,我建议你收藏起来,以后做任何空间转录组项目都能复用这个流程。

空间坐标与H&E图像对齐流程 AnnData 对象 obsm['spatial'] 提取像素坐标 (X, Y) 单位:像素 缩放因子 tissue_hires_scalef 坐标缩放 coords * scalef Y轴翻转 y = h - y 加载H&E图像 hires / fullres 可视化验证 散点图叠加或 sq.pl.spatial_scatter 提示: 实际项目中缩放因子和图像大小都来自 uns['spatial'],不要自己猜。

3.6 常见坑与我的经验

我曾经遇到的一个坑: 用sc.pl.spatial()画图时,如果设置了img_alpha=0,发现点位置飘了。后来发现是library_id参数写错了,导致它用了另一个样本的缩放因子。所以每次切换样本时,记得先打印list(adata.uns['spatial'].keys())确认id。
  • 坐标单位不一致: Visium是像素,但有些自有平台(如Slide-seq)给的是微米。记得先看文档。
  • 全分辨图 vs hires: 如果你要自己算spot间的欧氏距离,必须用全分辨坐标,不能用hires缩放后的,否则距离单位乱了。
  • 组织边缘的spot: 有时候tissue_hires_scalef是0.5,但实际hires图只有全图的一半大小,没问题。但偶尔官方数据包里有bug,hires图和fullres图的长宽比例不匹配(极少见,但我遇到过两次),这时候需要手动用PIL或cv2重新缩放。
一个小技巧: 如果你想快速验证对齐,可以把坐标存成GeoJSON,用QGIS或AnyChart打开看。但我更推荐用sq.pl.spatial_scatter一行搞定。