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来显示,因为全图太大。对齐的关键三步:
- 读取hires图像:
sq.datasets.visium_hne_image()或自己加载tissue_hires_image.png - 缩放坐标:将
spatial坐标乘以tissue_hires_scalef(注意不是除以!) - 翻转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轴翻转或缩放因子错了。
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: 空间坐标与图像对齐流程
下面这张图把整个对齐路径画清楚了,我建议你收藏起来,以后做任何空间转录组项目都能复用这个流程。
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重新缩放。
sq.pl.spatial_scatter一行搞定。