4、Seurat空间对象结构:spatial assay、image、坐标与metadata

好,咱们进入第四讲。前几章我们把空间转录组的基本概念和Seurat的安装跑通了,现在该动真格的了——你得搞明白Seurat里那个叫 SeuratObject 的东西,到底长什么样。

说白了,空间数据跟单细胞数据最大的区别在哪?多了一张图像,多了一套坐标。你不能光知道哪个细胞表达了什么基因,你还得知道它在组织切片上的哪个位置。Seurat为了把这个事儿包装得规规矩矩,设计了一套专门的对象结构。我带你看一遍。

核心概念一句话:Seurat的空间对象 = 单细胞assay + 空间影像 + 物理坐标 + 元数据。

我个人习惯把空间对象想象成一个“俄罗斯套娃”——最外面是Seurat对象,里面装着assay,assay旁边挂着一个叫 image 的槽位,这个槽位里又藏着低分辨图、坐标数据、缩放因子这些东西。咱一层层拆开看。

4.1 spatial assay:数据的主心骨

你创建一个空间对象时,第一个接触到的就是 SpatialAssay。这个assay和单细胞里的 RNAAssay 很相似,都存着表达矩阵、特征元数据、细胞元数据。但它多了点私货:spatial 这个slot,专门放每个spot的坐标。

# 看看assay里都有什么
pbmc_spatial[["Spatial"]]

# 如果你用Read10X_Spatial读入数据
# 默认会生成一个 SpatialAssay,名字叫 "Spatial"

关键区别在这:

属性 单细胞 RNAAssay 空间 SpatialAssay
表达矩阵 细胞 × 基因 spot × 基因
坐标信息 无 有(spatial slot)
图像关联 无 有(绑定image对象)
缩放因子 无 有(spot直径、图像缩放等)

我在项目里遇到过一个情况:读入来的数据,GetAssayData 取出来的矩阵是空值。后来发现是因为读数据时没指定 assay 参数。嗯,这种小坑,你只要记住 spatial 参数一定要对得上就行。

4.2 image系统:不仅仅是张图

空间转录组最让人兴奋的就是那张组织切片图。Seurat里,图像不是随便丢进去的JPG,它被封装成一个叫 VisiumV1(或 STARmap、SlideSeq 等)的S4类对象。

这个image对象里装了什么?

  • image:真实的像素图,通常是低分辨率的PNG(你文件夹里那个 tissue_lowres_image.png)
  • scale.factors:缩放因子,告诉你怎么从像素坐标映射回实际物理坐标
  • coordinates:这个比较绕——它存的是原始图像上的坐标,不是那些经过去偏转、对齐后的坐标
  • spot.radius:每个spot在图像上占据的像素半径,用于后面的可视化
# 看看image对象的结构
image_obj <- pbmc_spatial[["slice1"]]  
# 这个 "slice1" 是默认的图像名称

# 提取低分辨图
img <- image_obj@image

# 看缩放因子
image_obj@scale.factors

# 输出的东西长这样:
# lowres: 0.052
# hires : 0.17
# spot  : 13.8

我记得刚用10x Visium数据时,拿到h5文件和space Ranger输出的 tissue_lowres_image.png,还以为低分辨率图只是个预览,不能用于分析。其实Seurat内部做可视化时,依赖的就是这张低分辨图叠加spot。你把它删了,SpatialDimPlot 直接就报错。

注意:不同平台(Visium、Slide-seq、MERFISH)的图像对象类名不一样。Visium是 VisiumV1,Slide-seq可能是 SlideSeq。写代码时别直接写死类名,用 class() 先看一眼。

4.3 坐标系统:像素与物理空间的桥梁

坐标这东西,说简单也简单,说坑也坑。Seurat里至少有两套坐标在打架:

  • 图像像素坐标(tissue_lowres_image 上的x,y)
  • 实际物理坐标(经过缩放、对齐后的x,y)

你最常用的是 GetTissueCoordinates() 这个函数。它会默认帮你取出转换后的坐标。

# 获取坐标
coords <- GetTissueCoordinates(pbmc_spatial)

# 前几行长这样
#              x        y
# AAACAAGTATCTCC-1 5300.93 5234.19
# AAACACCAATAACTG-1 3733.29 5933.85

# 如果想看原始图像坐标,加个参数
raw_coords <- GetTissueCoordinates(pbmc_spatial, which = "slice1", 
                                    use.image = TRUE)

为什么会搞两套?你想想看,组织切片在显微镜下拍的时候,可能位置是歪的,或者做了旋转。Space Ranger在跑流程时已经帮你做了一次对齐。Seurat保存的就是对齐后的坐标,方便你直接画图。

但有一个地方很容易踩坑——坐标的方向。有时候你会看到y轴是反的,这是因为图像坐标系的原点在左上角,而R的默认坐标系原点在左下角。Seurat内部做了处理,但如果你自己去 @coordinates 里取原始值,就可能拿到反向的y。

我建议:任何时候取坐标,都用 GetTissueCoordinates() 这个接口函数,别直接去slots里掏。接口函数会帮你处理好一切潜在的转换问题。

4.4 metadata:你以为只是细胞注释?

单细胞里的 meta.data 是按细胞(barcode)为行存的。空间数据里,这个原则没变——每个spot就是一行。你可以往里面随便加东西:

  • 聚类结果(seurat_clusters)
  • 基因模块分数(AddModuleScore() 的结果)
  • 手动注释的组织区域(比如“皮层”、“髓质”)
  • 质控指标(nCount_Spatial、nFeature_Spatial)
# 加一列自定义信息
pbmc_spatial$tissue_region <- ifelse(
    pbmc_spatial$nCount_Spatial > 5000, 
    "high_density", 
    "low_density"
)

# 查看
head(pbmc_spatial[[]])

但是有一个区别你要注意:空间数据的 meta.data 里,行名是spot的barcode,而barcode的命名是有规律的——它包含了组织切片的坐标信息编码。不同平台barcode的格式差别很大:

平台 barcode示例 说明
10x Visium AAACAAGTATCTCC-1 33个字符 + 编号
Slide-seq ACGTACGTACGT 纯序列,无分隔符
STARmap x_y 直接用物理坐标

4.5 misc:杂物箱,但不要小看

最后这个 misc slot,其实就是个列表,什么都能往里装。Seurat官方文档里对它的定义很模糊——“杂项数据”。但实际用起来,它挺关键的。

比如,你跑 SpatialDimPlot() 时,图例的颜色映射是从哪来的?很多时候就是存在 misc 里。还有像SCT变换的模型参数、自定义的配色方案,都会被塞进去。

# 看看misc里有啥
pbmc_spatial@misc

# 你自己也可以往里存东西
pbmc_spatial@misc$my_custom_colors <- c("red", "blue", "green")

# 或者存一个处理日志
pbmc_spatial@misc$processing_log <- list(
    date = Sys.time(),
    steps = c("filtering", "normalization", "clustering")
)

我曾经在跟同事协作时,把空间坐标的自定义校正信息存在 misc 里,结果对方用另一个版本的Seurat读入,misc 里的格式不兼容,导致分析流程中断。从此我养成了一个习惯:关键数据绝不放在 misc 里,要么另存一个Rds文件,要么写成独立的CSV。

4.6 一张图看懂完整结构

好,到这你可能有点晕——又是assay又是image又是坐标的。我画了张图,把Seurat空间对象的内部结构给串起来。

Seurat空间对象核心结构 SeuratObject SpatialAssay 表达矩阵 (RNA counts) 特征元数据 (feature meta) spot坐标 (spatial slot) 缩放因子 (scale.factors) spot半径 (spot.radius) Image (VisiumV1等) 低分辨图 (tissue_lowres_image) 高分辨图 (tissue_hires_image) 原始像素坐标 缩放因子映射 图像对齐信息 meta.data 聚类注释 (seurat_clusters) 质控指标 (nCount/nFeature) 自定义注释 (区域/模块分数) spot级统计量 misc 配色方案存储 模型参数 (如SCT) 处理日志与备注 自定义杂项数据 关键接口函数:GetTissueCoordinates() · GetAssayData() · FetchData() 取数据走接口,绕开slots,避免版本兼容坑

从上往下看就清楚了——Seurat对象像一棵树,根节点是主对象,四个分支分别是 SpatialAssay、Image、meta.data 和 misc。每个分支负责一类数据,彼此有交叉引用但职责分离。

嗯,这一章的信息量不小。你如果能自己写段代码把新建一个空的空间对象、往里塞一张低分辨图和几个模拟坐标跑通,那这套结构基本就刻在你脑子里了。下次拿到真实的10x Visium数据时,你会发现——熟悉的感觉,全在这里面了。