---
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(SpatialExperiment, 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 = "<sample>"`, `image_id = TRUE`  
will drop all images for a given sample
- `NULL` defaults to the first entry available, e.g.  
`sample_id = "<sample>"`, `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, `r BiocStyle::Biocpkg("VisiumIO")`'s `TENxVisium/List()` 
functions facilitate 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 `sampleFolders` argument, or can be ignored; if ignored, 
it will be added automatically. (Note that `tissue_positions.csv` was renamed 
in Space Ranger v2.0.0; the `tissuePattern` argument can be used to specify a
pattern to match the tissue positions file.)

```{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 `TENxVisiumList()`, the above code to construct the same SPE is reduced to:

```{r}
library(VisiumIO)
dir <- list.dirs(system.file(
    file.path("extdata", "10xVisium"), 
    package = "SpatialExperiment"), 
    recursive = FALSE, full.names = TRUE)
(ids <- basename(dir))
(vl <- TENxVisiumList(
    sampleFolders = dir, sample_ids = ids, 
    processing = "raw", image = "lowres"))
(spe10x <- import(vl))
```

## 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()
```

