--- title: "Introduction to the SpatialExperiment class" author: "Dario Righelli, Helena L. Crowell, Lukas M. Weber" date: "`r format(Sys.Date(), '%b %d, %Y')`" output: BiocStyle::html_document: toc: true number_sections: true toc_depth: 3 toc_float: collapsed: true vignette: > %\VignetteIndexEntry{Introduction to the SpatialExperiment class} %\VignetteEncoding{UTF-8} %\VignetteEngine{knitr::rmarkdown} editor_options: chunk_output_type: inline --- ```{r setup, include = FALSE} knitr::opts_chunk$set(cache = TRUE, autodep = TRUE, cache.lazy = FALSE) ``` # Class structure ## Introduction The `SpatialExperiment` class is an R/Bioconductor S4 class for storing data from spatial -omics experiments. The class extends the `SingleCellExperiment` class for single-cell data to support storage and retrieval of additional information from spot-based and molecule-based platforms, including spatial coordinates, images, and image metadata. A specialized constructor function is included for data from the 10x Genomics Visium platform. The following schematic illustrates the `SpatialExperiment` class structure. ```{r, echo=FALSE, out.width = "100%", fig.cap="Overview of the SpatialExperiment class structure."} knitr::include_graphics("SPE.png") ``` As shown, an object consists of: (i) `assays` containing expression counts, (ii) `rowData` containing information on features, i.e. genes, (iii) `colData` containing information on spots or cells, including nonspatial and spatial metadata, (iv) `spatialCoords` containing spatial coordinates, and (v) `imgData` containing image data. For spot-based ST data (e.g. 10x Genomics Visium), a single `assay` named `counts` is used. For molecule-based ST data (e.g. seqFISH), two `assays` named `counts` and `molecules` can be used. Additional details on the class structure are provided in our [preprint](https://www.biorxiv.org/content/10.1101/2021.01.27.428431v3). ## Load data For demonstration of the general class structure, we load an example `SpatialExperiment` (abbreviated as SPE) object (variable `spe`): ```{r message = FALSE} library(SpatialExperiment) example(read10xVisium, echo = FALSE) spe ``` ## `spatialCoords` In addition to observation metadata stored inside the `colData` slot, the `SpatialExperiment` class stores spatial coordinates as: - `spatialCoords`, a numeric matrix of spatial coordinates (e.g. `x` and `y`) `spatialCoords` are stored inside the `int_colData`, and are directly accessible via the corresponding accessor: ```{r} head(spatialCoords(spe)) ``` The corresponding column names can be also accessed with `spatialCoordsNames()`: ```{r} spatialCoordsNames(spe) ``` ## `imgData` All image related data are stored inside the `int_metadata`'s `imgData` field as a `DataFrame` of the following structure: * each row corresponds to one image for a given sample and with a given unique image identifier (e.g. its resolutions) * for each image, columns specify: * which `sample_id` the image belongs to * a unique `image_id` in order to accommodate multiple images for a given sample (e.g. of different resolutions) * the image's `data` (a `SpatialImage` object) * the `scaleFactor` that adjusts pixel positions of the original, full-resolution image to pixel positions in the image The `imgData()` accessor can be used to retrieve the image data stored within the object: ```{r} imgData(spe) ``` ### The `SpatialImage` class Images are stored inside the `data` field of the `imgData` as a list of `SpatialImage`s. Each image may be of one of the following sub-classes: * `LoadedSpatialImage` * represents an image that is fully realized into memory as a `raster` object * `@image` contains a `raster` object: a matrix of RGB colors for each pixel in the image * `StoredSpatialImage` * represents an image that is stored in a local file (e.g., as .png, .jpg or .tif), and loaded into memory only on request * `@path` specifies a local file from which to retrieve the image * `RemoteSpatialImage` * represents an image that is remotely hosted (under some URL), and retrieved only on request * `@url` specifies where to retrieve the image from A `SpatialImage` can be accessed using `getImg()`, or retrieved directly from the `imgData()`: ```{r} (spi <- getImg(spe)) identical(spi, imgData(spe)$data[[1]]) ``` Data available in an object of class `SpatialImage` may be accessed via the `imgRaster()` and `imgSource()` accessors: ```{r fig.small = TRUE} plot(imgRaster(spe)) ``` ### Adding or removing images Image entries may be added or removed from a `SpatialExperiment`'s `imgData` `DataFrame` using `addImg()` and `rmvImg()`, respectively. Besides a path or URL to source the image from and a numeric scale factor, `addImg()` requires specification of the `sample_id` the new image belongs to, and an `image_id` that is not yet in use for that sample: ```{r fig.small = TRUE, eval = TRUE} url <- "https://i.redd.it/3pw5uah7xo041.jpg" spe <- addImg(spe, sample_id = "section1", image_id = "pomeranian", imageSource = url, scaleFactor = NA_real_, load = TRUE) img <- imgRaster(spe, sample_id = "section1", image_id = "pomeranian") plot(img) ``` The `rmvImg()` function is more flexible in the specification of the `sample_id` and `image_id` arguments. Specifically: - `TRUE` is equivalent to *all*, e.g. `sample_id = ""`, `image_id = TRUE` will drop all images for a given sample - `NULL` defaults to the first entry available, e.g. `sample_id = ""`, `image_id = NULL` will drop the first image for a given sample For example, `sample_id = TRUE`, `image_id = TRUE` will specify all images; `sample_id = NULL`, `image_id = NULL` corresponds to the first image entry in the `imgData`; `sample_id = TRUE`, `image_id = NULL` equals the first image for all samples; and `sample_id = NULL`, `image_id = TRUE` matches all images for the first sample. Here, we remove `section1`'s `pomeranian` image added in the previous code chunk; the image is now completely gone from the `imgData`: ```{r} imgData(spe <- rmvImg(spe, "section1", "pomeranian")) ``` # Object construction ## Manually The `SpatialExperiment` constructor provides several arguments to give maximum flexibility to the user. In particular, these include: - `spatialCoords`, a numeric `matrix` containing spatial coordinates - `spatialCoordsNames`, a character vector specifying which `colData` fields correspond to spatial coordinates `spatialCoords` can be supplied via `colData` by specifying the column names that correspond to spatial coordinates with `spatialCoordsNames`: ```{r} n <- length(z <- letters) y <- matrix(nrow = n, ncol = n) cd <- DataFrame(x = seq(n), y = seq(n), z) spe1 <- SpatialExperiment( assay = y, colData = cd, spatialCoordsNames = c("x", "y")) ``` Alternatively, `spatialCoords` may be supplied separately as a `matrix`, e.g.: ```{r} xy <- as.matrix(cd[, c("x", "y")]) spe2 <- SpatialExperiment( assay = y, colData = cd["z"], spatialCoords = xy) ``` Importantly, both of the above `SpatialExperiment()` function calls lead to construction of the exact same object: ```{r} identical(spe1, spe2) ``` Finally, `spatialCoords(Names)` are optional, i.e., we can construct a SPE using only a subset of the above arguments: ```{r} spe <- SpatialExperiment( assays = y) isEmpty(spatialCoords(spe)) ``` In general, `spatialCoordsNames` takes precedence over `spatialCoords`, i.e., if both are supplied, the latter will be ignored. In other words, `spatialCoords` are preferentially extracted from the `DataFrame` provided via `colData`. E.g., in the following function call, `spatialCoords` will be ignored, and xy-coordinates are instead extracted from the input `colData` according to the specified `spatialCoordsNames`. In this case, a message is also provided: ```{r results = "hide"} n <- 10; m <- 20 y <- matrix(nrow = n, ncol = m) cd <- DataFrame(x = seq(m), y = seq(m)) xy <- matrix(nrow = m, ncol = 2) colnames(xy) <- c("x", "y") SpatialExperiment( assay = y, colData = cd, spatialCoordsNames = c("x", "y"), spatialCoords = xy) ``` ## Spot-based When working with spot-based ST data, such as *10x Genomics Visium* or other platforms providing images, it is possible to store the image information in the dedicated `imgData` structure. Also, the `SpatialExperiment` class stores a `sample_id` value in the `colData` structure, which is possible to set with the `sample_id` argument (default is "sample_01"). Here we show how to load the default *Space Ranger* data files from a 10x Genomics Visium experiment, and build a `SpatialExperiment` object. In particular, the `readImgData()` function is used to build an `imgData` `DataFrame` to be passed to the `SpatialExperiment` constructor. The `sample_id` used to build the `imgData` object must be the same one used to build the `SpatialExperiment` object, otherwise an error is returned. ```{r} dir <- system.file( file.path("extdata", "10xVisium", "section1", "outs"), package = "SpatialExperiment") # read in counts fnm <- file.path(dir, "raw_feature_bc_matrix") sce <- DropletUtils::read10xCounts(fnm) # read in image data img <- readImgData( path = file.path(dir, "spatial"), sample_id = "foo") # read in spatial coordinates fnm <- file.path(dir, "spatial", "tissue_positions_list.csv") xyz <- read.csv(fnm, header = FALSE, col.names = c( "barcode", "in_tissue", "array_row", "array_col", "pxl_row_in_fullres", "pxl_col_in_fullres")) # construct observation & feature metadata rd <- S4Vectors::DataFrame( symbol = rowData(sce)$Symbol) # construct 'SpatialExperiment' (spe <- SpatialExperiment( assays = list(counts = assay(sce)), rowData = rd, colData = DataFrame(xyz), spatialCoordsNames = c("pxl_col_in_fullres", "pxl_row_in_fullres"), imgData = img, sample_id = "foo")) ``` Alternatively, the `read10xVisium()` function facilitates the import of *10x Genomics Visium* data to handle one or more samples organized in folders reflecting the default *Space Ranger* folder tree organization, as illustrated below (where "raw/filtered" refers to either "raw" or "filtered" to match the `data` argument). Note that the base directory "outs/" from Space Ranger can either be included manually in the paths provided in the `samples` argument, or can be ignored; if ignored, it will be added automatically. The `.h5` files are used if `type = "HDF5"`. (Note that `tissue_positions.csv` was renamed in Space Ranger v2.0.0.) ```{bash, eval = FALSE} sample . | — outs · · | — raw/filtered_feature_bc_matrix.h5 · · | — raw/filtered_feature_bc_matrix · · · | — barcodes.tsv.gz · · · | — features.tsv.gz · · · | — matrix.mtx.gz · · | — spatial · · · | — scalefactors_json.json · · · | — tissue_lowres_image.png · · · | — tissue_positions.csv ``` Using `read10xVisium()`, the above code to construct the same SPE is reduced to: ```{r} dir <- system.file( file.path("extdata", "10xVisium"), package = "SpatialExperiment") sample_ids <- c("section1", "section2") samples <- file.path(dir, sample_ids, "outs") (spe10x <- read10xVisium(samples, sample_ids, type = "sparse", data = "raw", images = "lowres", load = FALSE)) ``` Or alternatively, omitting the base directory `outs/` from Space Ranger: ```{r} samples2 <- file.path(dir, sample_ids) (spe10x2 <- read10xVisium(samples2, sample_ids, type = "sparse", data = "raw", images = "lowres", load = FALSE)) ``` ## Molecule-based To demonstrate how to accommodate molecule-based ST data (e.g. *seqFISH* platform) inside a `SpatialExperiment` object, we generate some mock data of 1000 molecule coordinates across 50 genes and 20 cells. These should be formatted into a `data.frame` where each row corresponds to a molecule, and columns specify the xy-positions as well as which gene/cell the molecule has been assigned to: ```{r message = FALSE, warning = FALSE} n <- 1e3 # number of molecules ng <- 50 # number of genes nc <- 20 # number of cells # sample xy-coordinates in [0, 1] x <- runif(n) y <- runif(n) # assign each molecule to some gene-cell pair gs <- paste0("gene", seq(ng)) cs <- paste0("cell", seq(nc)) gene <- sample(gs, n, TRUE) cell <- sample(cs, n, TRUE) # assure gene & cell are factors so that # missing observations aren't dropped gene <- factor(gene, gs) cell <- factor(cell, cs) # construct data.frame of molecule coordinates df <- data.frame(gene, cell, x, y) head(df) ``` Next, it is possible to re-shape the above table into a `r BiocStyle::Biocpkg("BumpyMatrix")` using `splitAsBumpyMatrix()`, which takes as input the xy-coordinates, as well as arguments specifying the row and column index of each observation: ```{r message = FALSE, warning = FALSE} # construct 'BumpyMatrix' library(BumpyMatrix) mol <- splitAsBumpyMatrix( df[, c("x", "y")], row = gene, col = cell) ``` Finally, it is possible to construct a `SpatialExperiment` object with two data slots: - The `counts` assay stores the number of molecules per gene and cell (equivalent to transcript counts in spot-based data) - The `molecules` assay holds the spatial molecule positions (xy-coordinates) Each entry in the `molecules` assay is a `DFrame` that contains the positions of all molecules from a given gene that have been assigned to a given cell. ```{r message = FALSE, warning = FALSE} # get count matrix y <- with(df, table(gene, cell)) y <- as.matrix(unclass(y)) y[1:5, 1:5] # construct SpatialExperiment spe <- SpatialExperiment( assays = list( counts = y, molecules = mol)) spe ``` The `BumpyMatrix` of molecule locations can be accessed using the dedicated `molecules()` accessor: ```{r message = FALSE, warning = FALSE} molecules(spe) ``` # Common operations ## Subsetting Subsetting objects is automatically defined to synchronize across all attributes, as for any other Bioconductor *Experiment* class. For example, it is possible to `subset` by `sample_id` as follows: ```{r} sub <- spe10x[, spe10x$sample_id == "section1"] ``` Or to retain only observations that map to tissue via: ```{r} sub <- spe10x[, colData(spe10x)$in_tissue] sum(colData(spe10x)$in_tissue) == ncol(sub) ``` ## Combining samples To work with multiple samples, the `SpatialExperiment` class provides the `cbind` method, which assumes unique `sample_id`(s) are provided for each sample. In case the `sample_id`(s) are duplicated across multiple samples, the `cbind` method takes care of this by appending indices to create unique sample identifiers. ```{r} spe1 <- spe2 <- spe spe3 <- cbind(spe1, spe2) unique(spe3$sample_id) ``` Alternatively (and preferentially), we can create unique `sample_id`(s) prior to `cbind`ing as follows: ```{r} # make sample identifiers unique spe1 <- spe2 <- spe spe1$sample_id <- paste(spe1$sample_id, "A", sep = ".") spe2$sample_id <- paste(spe2$sample_id, "B", sep = ".") # combine into single object spe3 <- cbind(spe1, spe2) ``` ## Sample ID replacement In particular, when trying to replace the `sample_id`(s) of a `SpatialExperiment` object, these must map uniquely with the already existing ones, otherwise an error is returned. ```{r, error=TRUE} new <- spe3$sample_id new[1] <- "section2.A" spe3$sample_id <- new new[1] <- "third.one.of.two" spe3$sample_id <- new ``` Importantly, the `sample_id` `colData` field is *protected*, i.e., it will be retained upon attempted removal (= replacement by `NULL`): ```{r} # backup original sample IDs tmp <- spe$sample_id # try to remove sample IDs spe$sample_id <- NULL # sample IDs remain unchanged identical(tmp, spe$sample_id) ``` ## Image transformations Both the `SpatialImage` (SpI) and `SpatialExperiment` (SpE) class currently support two basic image transformations, namely, rotation (via `rotateImg()`) and mirroring (via `mirrorImg()`). Specifically, for a SpI/E `x`: * `rotateImg(x, degrees)` expects as `degrees` a single numeric in +/-[0,90,...,360]. Here, a (negative) positive value corresponds to (counter-)clockwise rotation. * `mirrorImg(x, axis)` expects as `axis` a character string specifying whether to mirror horizontally (`"h"`) or vertically (`"v"`). Here, we apply various transformations using both a SpI (`spi`) and SpE (`spe`) as input, and compare the resulting images to the original: ### Rotation ```{r} # extract first image spi <- getImg(spe10x) # apply counter-/clockwise rotation spi1 <- rotateImg(spi, -90) spi2 <- rotateImg(spi, +90) # visual comparison par(mfrow = c(1, 3)) plot(as.raster(spi)) plot(as.raster(spi1)) plot(as.raster(spi2)) ``` ```{r} # specify sample & image identifier sid <- "section1" iid <- "lowres" # counter-clockwise rotation tmp <- rotateImg(spe10x, sample_id = sid, image_id = iid, degrees = -90) # visual comparison par(mfrow = c(1, 2)) plot(imgRaster(spe10x, sid, iid)) plot(imgRaster(tmp, sid, iid)) ``` ### Mirroring ```{r} # extract first image spi <- getImg(spe10x) # mirror horizontally/vertically spi1 <- mirrorImg(spi, "h") spi2 <- mirrorImg(spi, "v") # visual comparison par(mfrow = c(1, 3)) plot(as.raster(spi)) plot(as.raster(spi1)) plot(as.raster(spi2)) ``` ```{r} # specify sample & image identifier sid <- "section2" iid <- "lowres" # mirror horizontally tmp <- mirrorImg(spe10x, sample_id = sid, image_id = iid, axis = "h") # visual comparison par(mfrow = c(1, 2)) plot(imgRaster(spe10x, sid, iid)) plot(imgRaster(tmp, sid, iid)) ``` # Session Info {.smaller} ```{r tidy = TRUE} sessionInfo() ```