---
title: "Working with MultiAssaySpatialExperiment"
author:
- name: Patrick Aboyoun
  email: aboyounp@gene.com
  affiliation: Genentech, Inc.
date: "Compiled: `r BiocStyle::doc_date()`; Modified: 6 July 2026"
package: MultiAssaySpatialExperiment
output:
  BiocStyle::html_document:
    toc: true
    number_sections: true
    toc_depth: 3
    toc_float:
      collapsed: true
vignette: >
  %\VignetteIndexEntry{2. Working with MultiAssaySpatialExperiment}
  %\VignetteEncoding{UTF-8}
  %\VignetteEngine{knitr::rmarkdown}
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  cache = TRUE
)
```

# Introduction

This vignette covers the core operations for working with
`MultiAssaySpatialExperiment` (MASE) objects:

1. **Construction**: Building MASE objects from matrices, existing Bioconductor
   objects, or vendor output (see *MultiAssaySpatialExperiment use cases*)
2. **Subsetting**: Filtering by specimens, assays, columns and spatial regions
3. **Spatial operations**: Annotation and aggregation by spatial regions
4. **Labels ↔ shapes**: Converting between raster masks and vector polygons

**Prerequisite**: If you are new to MASE, read the **Introduction to
MultiAssaySpatialExperiment** vignette first to understand the MASE structure and
components.

```{r load_packages, message = FALSE, warning = FALSE}
library(MultiAssaySpatialExperiment)
library(SummarizedExperiment)
library(SingleCellExperiment)
library(S4Vectors)
library(sf)
```

# Building MASE objects

There are several approaches to constructing a MASE object:

1. **Minimal MASE**: Start with assays + metadata, add spatial elements later
2. **From scratch**: Build all components manually
3. **From existing objects**: Convert `SpatialExperiment` or wrap
   `SingleCellExperiment` objects in an `ExperimentList`
4. **From vendor output**: Use platform readers (`readXeniumMASE()`, etc.), 
   covered in the *MultiAssaySpatialExperiment use cases* vignette
5. **With prepMASE**: Wrap `prepMultiAssay()` for name harmonization and spatial
   FK cleanup (see below)

Throughout, the `sampleMap` is the three-column `DataFrame` that says which specimen
each assay column belongs to, with one row per column: `assay` names the element of
the `ExperimentList`, `primary` is a row name of `colData`, and `colname` is the
column name within that assay. The *Introduction to MultiAssaySpatialExperiment*
vignette covers `sampleMap` and `spatialMap` in more detail.

## Optional: lazy Parquet I/O via BiocDuckDB

`MultiAssaySpatialExperiment` does not import BiocDuckDB. For large on-disk
datasets, the **BiocDuckDB** package registers methods on MASE generics
(`readParquetForMASE`, lazy `spatialPoints` / `spatialShapes`, etc.) so you can
persist and query MASE objects without loading full layers into memory. See the
BiocDuckDB documentation for Parquet read/write workflows.

## Minimal MASE

The simplest MASE has one experiment, specimen metadata (`colData`), and a
`sampleMap`. No spatial elements are required initially. In the example below,
each cell is treated as its own specimen for simplicity; in practice,
`colData` rows usually represent biological replicates or tissue sections.

```{r minimal_mase}
# Create a simple expression matrix
counts <- matrix(rpois(50, lambda = 10), nrow = 10, ncol = 5,
                dimnames = list(paste0("Gene", 1:10),
                               paste0("Cell", 1:5)))

# Specimen metadata
specimens <- DataFrame(
  patient_id = c("P1", "P1", "P1", "P2", "P2"),
  tissue = c("cortex", "cortex", "cortex", "medulla", "medulla"),
  row.names = paste0("Cell", 1:5)
)

# Sample map: link assay columns to specimens
sample_map <- DataFrame(
  assay = factor("rna", "rna"),
  primary = paste0("Cell", 1:5),
  colname = paste0("Cell", 1:5)
)

# Construct MASE
mase_minimal <- MultiAssaySpatialExperiment(
  experiments = ExperimentList(rna = counts),
  colData = specimens,
  sampleMap = sample_map
)

mase_minimal
```

## Adding spatial coordinates

To add point coordinates (e.g., cell centroids), create a `PointsLayerList` and
a `spatialMap` linking assay columns to spatial instances.

```{r add_points_with_map}
# Suppose we have two assays: RNA and protein
rna_counts <- matrix(rpois(50, 10), nrow = 10, ncol = 5,
                    dimnames = list(paste0("Gene", 1:10), paste0("Cell", 1:5)))
prot_counts <- matrix(rpois(15, 5), nrow = 3, ncol = 5,
                     dimnames = list(paste0("Protein", 1:3), paste0("Cell", 1:5)))

# Both assays share the same specimens
specimens2 <- DataFrame(
  patient_id = c("P1", "P1", "P1", "P2", "P2"),
  row.names = paste0("Cell", 1:5)
)

# Sample map for two assays
sample_map2 <- DataFrame(
  assay = factor(rep(c("rna", "protein"), each = 5), c("rna", "protein")),
  primary = rep(paste0("Cell", 1:5), 2),
  colname = rep(paste0("Cell", 1:5), 2)
)

# Point coordinates (shared by both assays)
coords2 <- DataFrame(
  x = runif(5, 0, 100),
  y = runif(5, 0, 100),
  instance_id = paste0("Cell", 1:5)
)

# Spatial map: link assay columns to spatial points
spatial_map2 <- DataFrame(
  assay = factor(rep(c("rna", "protein"), each = 5), c("rna", "protein")),
  colname = rep(paste0("Cell", 1:5), 2),
  element_type = "points",
  region = factor(rep("coords", 10), "coords"),
  instance_id = rep(paste0("Cell", 1:5), 2)
)

# Construct MASE with spatialMap
mase_with_map <- MultiAssaySpatialExperiment(
  experiments = ExperimentList(rna = rna_counts, protein = prot_counts),
  colData = specimens2,
  sampleMap = sample_map2,
  points = PointsLayerList(coords = coords2),
  spatialMap = spatial_map2
)

mase_with_map
```

## Adding spatial shapes

Shapes (polygons) typically represent cell boundaries, tissue regions, or
annotation areas. Use the `sf` package to create geometries.

```{r add_shapes}
# Create simple circular boundaries around each point
centroids <- data.frame(
  x = runif(5, 0, 100),
  y = runif(5, 0, 100),
  instance_id = paste0("Cell", 1:5)
)

# Convert to sf points, then buffer to circles
centroids_sf <- st_as_sf(centroids, coords = c("x", "y"))
circles <- st_buffer(centroids_sf, dist = 5)

# Convert back to DataFrame with geometry column
boundaries <- DataFrame(
  geometry = st_geometry(circles),
  instance_id = paste0("Cell", 1:5)
)

# Add to MASE
mase_with_shapes <- mase_with_map
spatialShapes(mase_with_shapes) <- ShapesLayerList(boundaries = boundaries)

mase_with_shapes
```

## Building from multiple SingleCellExperiment objects

Combine multiple `SingleCellExperiment` objects into a MASE:

```{r from_sce}
# Create two SingleCellExperiment objects (simulating different technologies)
sce1 <- SingleCellExperiment(
  assays = list(counts = matrix(rpois(40, 10), nrow = 10, ncol = 4,
                               dimnames = list(paste0("Gene", 1:10),
                                              paste0("Cell", 1:4)))),
  colData = DataFrame(tech = "rna", cell_id = paste0("Cell", 1:4))
)

sce2 <- SingleCellExperiment(
  assays = list(counts = matrix(rpois(12, 5), nrow = 3, ncol = 4,
                               dimnames = list(paste0("Protein", 1:3),
                                              paste0("Cell", 1:4)))),
  colData = DataFrame(tech = "protein", cell_id = paste0("Cell", 1:4))
)

# Specimen metadata (shared)
specimens_sce <- DataFrame(
  patient = rep("P1", 4),
  tissue = rep("cortex", 4),
  row.names = paste0("Cell", 1:4)
)

# Sample maps
samplemap_sce <- DataFrame(
  assay = factor(rep(c("rna", "protein"), each = 4), c("rna", "protein")),
  primary = rep(paste0("Cell", 1:4), 2),
  colname = rep(paste0("Cell", 1:4), 2)
)

# Construct MASE
mase_from_sce <- MultiAssaySpatialExperiment(
  experiments = ExperimentList(rna = sce1, protein = sce2),
  colData = specimens_sce,
  sampleMap = samplemap_sce
)

mase_from_sce
```

## prepMASE and buildSpatialMap

For manual construction, `buildSpatialMap()` assembles a `spatialMap` from
`sampleMap` (analogous to MAE's `listToMap()`), and `prepMASE()` wraps
`prepMultiAssay()` while harmonizing spatial slots:

```{r prep_mase, eval = FALSE}
spmap <- buildSpatialMap(sample_map, region = "cells", element_type = "shapes")
prepared <- prepMASE(
  ExperimentList(rna = counts, protein = protein_counts),
  colData = specimens,
  sample_map,
  shapes = ShapesLayerList(cells = cell_boundaries),
  spatialMap = spmap
)
mase <- do.call(MultiAssaySpatialExperiment, prepared)
drops(prepared$metadata$drops)  # rows removed during harmonization, if any
```

## Validation checklist

Before constructing a MASE, ensure:

1. **ExperimentList**:
   - All experiments have unique column names
   
2. **colData**:
   - Row names are specimen identifiers (primary IDs)
   - No duplicate row names

3. **sampleMap**:
   - `assay` column matches names in ExperimentList
   - `primary` column matches row names in colData
   - `colname` column matches column names in experiments
   - No duplicate (assay, colname) pairs

4. **points/shapes** (if provided):
   - `instance_id` column exists
   - For points: `x`, `y` columns exist
   - For shapes: `geometry` column exists (sfc)

5. **spatialMap** (if provided):
   - `assay` + `colname` exist in sampleMap
   - `element_type` is "points" or "shapes"
   - `region` matches names in points or shapes
   - `instance_id` exists in the corresponding spatial element

Check validity manually:

```{r check_validity}
validObject(mase_with_map)
```

Most of these are checked at construction time, not just by `validObject()`.
`sampleMap` rows that fail the `colname`/`primary` checks are dropped with a
message rather than an error, since a partial mismatch is common when
building a `sampleMap` by hand:

```{r validation_harmonize}
bad_sample_map <- sample_map2
bad_sample_map$colname[1] <- "NotARealColumn"
mase_dropped <- MultiAssaySpatialExperiment(
  experiments = ExperimentList(rna = rna_counts, protein = prot_counts),
  colData = specimens2,
  sampleMap = bad_sample_map
)
```

A broken `spatialMap`, by contrast, fails `validObject()` outright, since
there is no reasonable way to silently drop a dangling geometry reference:

```{r validation_error, error = TRUE}
bad_spatial_map <- spatial_map2
bad_spatial_map$instance_id[1] <- "NotACell"
MultiAssaySpatialExperiment(
  experiments = ExperimentList(rna = rna_counts, protein = prot_counts),
  colData = specimens2,
  sampleMap = sample_map2,
  points = PointsLayerList(coords = coords2),
  spatialMap = bad_spatial_map
)
```

# Subsetting operations

MASE extends MultiAssayExperiment with spatial slots (points, shapes, images,
labels, `imgData`, and `spatialMap`). When you subset a MASE, these slots are
automatically updated to stay consistent with the assays and specimen metadata.

The examples below use a **separate** MASE object from the construction section
above, so you can run them independently.

## Overview of subsetting operations

| Operation | What it filters | Spatial propagation |
|-----------|-----------------|---------------------|
| `subsetByColData(x, y)` | Specimens (primary identifiers) | `spatialMap`, `imgData` |
| `subsetByRow(x, y, ...)` | Rows of experiments | Spatial layers unchanged |
| `subsetByColumn(x, y)` | Assay columns (names, indices, or logical vectors per assay) | `spatialMap`, `imgData`, points, shapes, images, labels |
| `subsetByAssay(x, y)` | Assays | Points, shapes, images, labels and `spatialMap` rows tied to retained assays |
| `x[i, j, k, ..., drop]` | Row, column, assay indices | Composes the above |
| `subsetByBoundingBox(x, xmin, xmax, ymin, ymax, ...)` | Points/shapes by rectangle | Assays via `spatialMap` |
| `subsetByPolygon(x, polygon, ...)` | Points/shapes by polygon | Assays via `spatialMap` |

**Note:** `subsetByAssay()` keeps only spatial layers referenced in `spatialMap`
for the retained assays. Auxiliary shape layers added for annotation (but not
linked in `spatialMap`) are dropped; run `annotateWithRegions()` on the full
MASE first if you need those layers.

## Basic subsetting

Two assays are enough to show what the different subsetting axes actually do. Here `P1`
and `P2` are the specimens, the rows of `colData`; `S1` to `S4` are observations, the
assay columns. Both assays measure the same four cells, and both are mapped onto one
shared points layer.

```{r basic-subset-setup}
make_assay <- function() {
  SummarizedExperiment(
    assays = list(counts = matrix(rnorm(20), nrow = 5, ncol = 4,
      dimnames = list(paste0("G", 1:5), paste0("S", 1:4)))),
    # "zone", not "region": this is an arbitrary per-column covariate on the
    # assay's own colData, unrelated to spatialMap's "region" column below,
    # which names a points/shapes layer
    colData = DataFrame(zone = rep(c("core", "margin"), length.out = 4)))
}

pts <- DataFrame(x = 1:4, y = 1:4, instance_id = paste0("S", 1:4))

mase <- MultiAssaySpatialExperiment(
  experiments = ExperimentList(rna = make_assay(), protein = make_assay()),
  colData = DataFrame(row.names = c("P1", "P2")),
  sampleMap = DataFrame(
    assay = factor(rep(c("rna", "protein"), each = 4)),
    primary = rep(c("P1", "P1", "P2", "P2"), 2),
    colname = rep(paste0("S", 1:4), 2)
  ),
  points = PointsLayerList(coords = pts),
  spatialMap = DataFrame(
    assay = factor(rep(c("rna", "protein"), each = 4)),
    colname = rep(paste0("S", 1:4), 2),
    element_type = "points",
    region = "coords",
    instance_id = rep(paste0("S", 1:4), 2)
  )
)

vapply(experiments(mase), ncol, integer(1))
```

### Subset by specimen

Naming specimens cuts across the whole object. `P1` owns `S1` and `S2` in both assays,
so both come back with two columns:

```{r subsetByColData}
by_specimen <- mase[, "P1"]
vapply(experiments(by_specimen), ncol, integer(1))
```

### Subset by assay column

Passing a *list* instead targets one assay by name. Only `rna` is cut here; `protein`
keeps all four columns, which is the difference from the previous example:

```{r subsetByColumn}
by_column <- mase[, list(rna = c("S1", "S3"))]
vapply(experiments(by_column), ncol, integer(1))
```

The same list interface filters on observation metadata, which is the idiomatic
replacement for ad hoc "query by metadata" patterns:

```{r subsetByColumnColData}
cdf <- colData(experiments(mase)[["rna"]])
mase_core <- subsetByColumn(mase, list(rna = cdf$zone == "core"))
ncol(experiments(mase_core)[["rna"]])
nrow(spatialPoints(mase_core)[["coords"]])
```

### Bracket notation

The `[i, j, k, drop]` operator composes feature, specimen and assay subsetting in one
call:

```{r bracket}
mase[1:3, "P1", "rna", drop = FALSE]
```

## Spatial subsetting

When you have `spatialMap` linking assay columns to spatial elements, you can
subset by **bounding box** or **polygon**. Points and shapes within the region
are kept; assays are filtered to retained instance_ids via `spatialMap`.

### Subset by bounding box

Filter by a rectangle `(xmin, xmax, ymin, ymax)`:

```{r subsetByBoundingBox}
m2 <- subsetByBoundingBox(mase, xmin = 1.5, xmax = 4.5, ymin = 1.5, ymax = 4.5)
m2
spatialPoints(m2)[["coords"]]
ncol(experiments(m2)[["assay1"]])
```

### Subset by polygon

Filter by an sf polygon:

```{r subsetByPolygon}
poly <- st_polygon(list(matrix(
  c(1.5, 1.5, 4.5, 1.5, 4.5, 4.5, 1.5, 4.5, 1.5, 1.5),
  ncol = 2, byrow = TRUE)))
m3 <- subsetByPolygon(mase, poly)
nrow(spatialPoints(m3)[["coords"]])
ncol(experiments(m3)[["assay1"]])
```

### Select data from multiple non-contiguous regions

A region of interest need not be a single connected shape. Subsetting once per region
and keeping the results separate lets you compare them directly:

```{r multiregion}
# Define two regions of interest
region1 <- st_polygon(list(matrix(c(1, 1, 2, 1, 2, 2, 1, 2, 1, 1), ncol = 2, byrow = TRUE)))
region2 <- st_polygon(list(matrix(c(4, 4, 5, 4, 5, 5, 4, 5, 4, 4), ncol = 2, byrow = TRUE)))

# Subset by each region
m_r1 <- subsetByPolygon(mase, region1)
m_r2 <- subsetByPolygon(mase, region2)

c(region1 = ncol(experiments(m_r1)[[1]]),
  region2 = ncol(experiments(m_r2)[[1]]))
```

### Buffer-based subsets

Find all points within a distance from a reference point:

```{r buffer_subset}
# Reference point
ref_point <- st_point(c(3, 3))

# Create buffer (e.g., 1.5 units radius)
buffer_region <- st_buffer(ref_point, dist = 1.5)

# Subset by buffer
m_buffer <- subsetByPolygon(mase, buffer_region)
m_buffer
```

# Spatial annotation and aggregation

Spatial omics often measures signals at discrete locations (spots, cell
centroids) that lie within larger regions (cells, tissue segments). A common
workflow is to:

1. **Annotate** each measurement point with the region it falls in
2. **Aggregate** assay values by region (sum, mean, count)

MASE provides `annotateWithRegions` for step 1 and `aggregateByRegion` for
step 2. Both use the central `spatialMap` table to link assay columns to
spatial elements.

## Example setup

We construct a minimal MASE with four spots (transcriptomic measurements),
coordinates, two cell polygons, and a gene expression assay. The goal is to
annotate each spot with its containing cell and then aggregate expression per
cell.

```{r setup-annotation}
# Four spots at coordinates
pts_agg <- DataFrame(
  x = c(1.5, 2.5, 2.5, 3.5),
  y = c(1.5, 1.5, 2.5, 2.5),
  instance_id = paste0("S", 1:4))

# Three cell polygons
cell1 <- st_polygon(list(matrix(c(1, 1, 2, 1, 2, 2, 1, 2, 1, 1), ncol = 2, byrow = TRUE)))
cell2 <- st_polygon(list(matrix(c(2, 1, 3, 1, 3, 2, 2, 2, 2, 1), ncol = 2, byrow = TRUE)))
cell3 <- st_polygon(list(matrix(c(2, 2, 3, 2, 3, 3, 2, 3, 2, 2), ncol = 2, byrow = TRUE)))

shp_df <- DataFrame(
  instance_id = c("cell1", "cell2", "cell3"),
  geometry = st_sfc(cell1, cell2, cell3))

# Gene expression assay (3 genes × 4 spots)
expr <- matrix(c(10, 20, 5,  15,
                 30, 10, 25, 5,
                 5,  15, 20, 30),
  nrow = 3, ncol = 4,
  dimnames = list(paste0("Gene", 1:3), paste0("S", 1:4)))

# Construct MASE
mase_agg <- MultiAssaySpatialExperiment(
  experiments = ExperimentList(rna = expr),
  colData = DataFrame(row.names = "P1"),
  sampleMap = DataFrame(
    assay = factor("rna"),
    primary = rep("P1", 4),
    colname = paste0("S", 1:4)
  ),
  points = PointsLayerList(centroids = pts_agg),
  shapes = ShapesLayerList(cells = shp_df),
  spatialMap = DataFrame(
    assay = factor("rna"),
    colname = paste0("S", 1:4),
    element_type = "points",
    region = "centroids",
    instance_id = paste0("S", 1:4)
  )
)
mase_agg
```

## Annotate with regions

`annotateWithRegions` performs a spatial join: for each point, it finds which
shape (if any) matches and records the shape's `instance_id` in a new column
in `spatialMap`.

```{r annotate}
mase_agg_orig <- mase_agg
mase_agg <- annotateWithRegions(mase_agg, points = "centroids", shapes = "cells")
spatialMap(mase_agg)
```

The new column `cells` shows: S1 → cell1, S2 → cell2, S3 → cell3, S4 → NA
(no containing cell). To assign S4 to its nearest cell instead, use
`join = sf::st_nearest_feature`.

## Aggregate by region

With the annotation in place, `aggregateByRegion` groups assay columns by
region and applies an aggregation function.

### Count points per region

`FUN = "count"` returns the number of points in each shape:

```{r aggregate-count}
agg_count <- aggregateByRegion(mase_agg, by = "cells", FUN = "count")
agg_count
```

### Sum expression per region

`FUN = "sum"` sums each gene's expression over spots in the same cell:

```{r aggregate-sum}
agg_sum <- aggregateByRegion(mase_agg, by = "cells", FUN = "sum")
agg_sum[["rna"]]
```

### Mean expression per region

`FUN = "mean"` averages expression across spots in each cell:

```{r aggregate-mean}
agg_mean <- aggregateByRegion(mase_agg, by = "cells", FUN = "mean")
agg_mean[["rna"]]
```

## Lower-level spatial joins

`annotateWithRegions()` wraps a point-in-polygon join and writes results to
`spatialMap`. For ad hoc layer-to-layer joins, use `spatialJoin()` on
`DataFrame` layers with `geometry` columns.

## Aggregation strategies

Different strategies serve different purposes: sum preserves total signal (useful
for RNA-seq or ATAC-seq counts), mean allows comparing regions with different
numbers of points (a normalized comparison), and count supports quality control,
density analysis, or use as a normalization factor.

```{r compare_strategies}
# Compare sum vs mean for Gene1
cells <- colnames(agg_sum[["rna"]])
data.frame(
  cell = cells,
  sum = agg_sum[["rna"]]["Gene1", ],
  mean = agg_mean[["rna"]]["Gene1", ],
  n_spots = agg_count$count[match(cells, agg_count$cells)]
)
```

## Visualizing aggregated data

After aggregation, visualize results spatially:

```{r vis_aggregated, fig.width = 8, fig.height = 4}
# Get aggregated data for Gene1
gene1_expr <- agg_sum[["rna"]]["Gene1", ]

# Get cell polygons as sf
cells_sf <- st_sf(
  instance_id = shp_df$instance_id,
  geometry = shp_df$geometry
)

# Add expression values
cells_sf$Gene1_sum <- gene1_expr[cells_sf$instance_id]

# Plot
par(mfrow = c(1, 2))

# Original points
plot(pts_agg$x, pts_agg$y, 
     pch = 16, cex = 2,
     col = rainbow(4)[rank(expr["Gene1", ])],
     main = "Gene1: Original spots",
     xlab = "x", ylab = "y")
text(pts_agg$x, pts_agg$y, paste0("S", 1:4), pos = 3, cex = 0.7)

# Aggregated cells
plot(st_geometry(cells_sf), 
     col = rainbow(3)[rank(cells_sf$Gene1_sum)],
     main = "Gene1: Aggregated by cell",
     xlab = "x", ylab = "y")
text(st_coordinates(st_centroid(cells_sf)), 
     labels = round(cells_sf$Gene1_sum, 1),
     cex = 0.8)
```

# Labels ↔ shapes interoperability

MASE supports both **labels** (raster segmentation masks) and **shapes**
(vector polygons). The package stores both slot types; converting between them,
rasterization from shapes to labels, and vectorization from labels back to
shapes, is a manual workflow using external tools, covered in the next two
sections.

## Rasterization: shapes → labels

Convert cell boundary polygons to a segmentation mask:

```{r rasterize}
# Example: 3 cell polygons
cell1_rast <- st_polygon(list(matrix(c(0, 0, 1, 0, 1, 1, 0, 1, 0, 0), ncol = 2, byrow = TRUE)))
cell2_rast <- st_polygon(list(matrix(c(1, 0, 2, 0, 2, 1, 1, 1, 1, 0), ncol = 2, byrow = TRUE)))
cell3_rast <- st_polygon(list(matrix(c(0, 1, 1, 1, 1, 2, 0, 2, 0, 1), ncol = 2, byrow = TRUE)))

shapes_df <- DataFrame(
  instance_id = c("cell1", "cell2", "cell3"),
  geometry = st_sfc(cell1_rast, cell2_rast, cell3_rast)
)
```

MASE stores the result of rasterization rather than performing it. The steps are to
choose a target grid extent and resolution, determine which pixels each polygon covers,
write the polygon identifier into those pixels, and store the resulting raster as a
`RasterLayerList` in the `labels` slot. `stars::st_rasterize()` and `terra::rasterize()`
both do the middle two steps.

## Vectorization: labels → shapes

Extract polygon boundaries from a segmentation mask:

```{r vectorize}
# Example label matrix (3 cells)
label_matrix <- matrix(c(
  1, 1, 2, 2,
  1, 1, 2, 2,
  3, 3, 0, 0,
  3, 3, 0, 0
), nrow = 4, byrow = TRUE)
```

Vectorization runs the same path in reverse: read the label matrix, trace the boundary
of each distinct non-zero value, convert those boundaries to `sf` polygons carrying the
label as `instance_id`, and store them as a `ShapesLayerList` in the `shapes` slot.
`stars::st_as_sf()` and `sf::st_polygonize()` cover the tracing step.

## Use cases

**Rasterization (shapes → labels)**:

- Generate training data for deep learning models (requires raster masks)
- Perform fast pixel-based operations (convolution, morphology)
- Memory-efficient storage for dense segmentations

**Vectorization (labels → shapes)**:

- Perform spatial operations (area, perimeter, centroid)
- Spatial subsetting (point-in-polygon, overlap)
- Visualization with ggplot2 + sf

# See also

- *Introduction to MultiAssaySpatialExperiment*: a worked example from
  construction through aggregation
- *MultiAssaySpatialExperiment use cases*: platform readers and complete
  analysis workflows
- *Design of MultiAssaySpatialExperiment*: slots, mapping tables and the
  relational schema
- *MultiAssaySpatialExperiment cheatsheet*: one-page API reference

# Session info

```{r sessionInfo}
sessionInfo()
```
