The submission-branch API uses existing Bioconductor S4 containers.
Raw, unaligned spectra remain in Cardinal; a common feature-by-pixel
matrix belongs in SpatialExperiment. No knowledge of
internal S4 slots is needed to read, modify or subset these objects. The
historical Seurat tutorials remain on the published-workflow branch;
their code is not the submission-branch API.
| Data to access | Public interface |
|---|---|
| Expression/intensity matrices | assay(), assayNames() |
| Feature m/z and annotations | rowData() |
| Pixel metadata and groups | colData(), colLabels() |
| Pixel coordinates | spatialCoords(), spatialCoordsNames() |
| Optical images | imgData(), imgRaster(),
addImg() |
| Paired modalities | altExp(), altExpNames() |
| PCA and other embeddings | reducedDim(), reducedDimNames() |
| Analysis provenance | metadata() |
These accessors also have replacement forms. For example,
colData(spe)$region <- values modifies pixel metadata
through the container’s public API. A slot = "counts"
argument retained by some SpaMTP functions is a compatibility spelling
for the expression assay name; it is not an instruction to inspect an S4
slot.
The two selections are distinct: assay = "transcriptome"
selects altExp(spe, "transcriptome"), while
slot = "logcounts" selects the matrix within that
experiment. Use assay = "main" for the primary experiment
and exact character matrix names; numeric positions are not accepted by
these SpaMTP selectors. See
help("experimentAccess", package = "SpaMTP") for the shared
convention and compatibility aliases.
The CSV contains one row per pixel, x and y
coordinate columns, and one column per measured m/z. This runnable
example writes a small temporary input file. Replace it with your own
path for real data. Raw imzML processing is demonstrated in the native workflow.
input <- data.frame(
x = rep(1:3, each = 2), y = rep(1:2, 3),
check.names = FALSE,
"100.01" = c(0, 2, 3, 5, 7, 9),
"200.02" = c(4, 1, 6, 2, 8, 3),
"300.03" = c(2, 3, 1, 4, 2, 6)
)
spe <- local({
path <- tempfile(fileext = ".csv")
on.exit(unlink(path))
utils::write.csv(input, path, row.names = FALSE)
readSMMatrix(path, project.name = "section1", verbose = FALSE)
})
stopifnot(methods::is(spe, "SpatialExperiment"), nrow(spe) == 3, ncol(spe) == 6)
SummarizedExperiment::assayNames(spe)
## [1] "counts"
SummarizedExperiment::assay(spe, "counts")
## 3 x 6 sparse Matrix of class "dgCMatrix"
## 1_1 1_2 2_1 2_2 3_1 3_2
## mz-100.01 . 2 3 5 7 9
## mz-200.02 4 1 6 2 8 3
## mz-300.03 2 3 1 4 2 6
SummarizedExperiment::rowData(spe)$mz
## [1] 100.01 200.02 300.03Use feature identifiers to index matrix rows, and pixel identifiers to index columns. m/z is explicit feature metadata, not something to recover by editing feature names. For discrete plotting groups, use a factor even if labels were originally stored as integer cluster numbers.
SummarizedExperiment::colData(spe)$region <- factor(rep(c("edge", "core"), 3))
SingleCellExperiment::colLabels(spe) <- spe$region
SummarizedExperiment::rowData(spe)$feature_label <- c("peak A", "peak B", "peak C")
intensity <- SummarizedExperiment::assay(spe, "counts")
SummarizedExperiment::colData(spe)$total_intensity <- Matrix::colSums(intensity)
SummarizedExperiment::colData(spe)$detected_features <- Matrix::colSums(intensity > 0)
as.data.frame(SummarizedExperiment::colData(spe))
## orig.ident sample_id region label total_intensity detected_features
## 1_1 section1 section1 edge edge 6 2
## 1_2 section1 section1 core core 6 3
## 2_1 section1 section1 edge edge 10 3
## 2_2 section1 section1 core core 11 3
## 3_1 section1 section1 edge edge 17 3
## 3_2 section1 section1 core core 18 3QC values above are summaries of MSI intensities, not UMI counts. They are computed from the selected matrix instead of assuming that historical Seurat-generated QC columns still describe the current features and assay.
spe <- normalizeSMData(spe, "LogNormalize", verbose = FALSE)
SummarizedExperiment::assayNames(spe)
## [1] "counts" "logcounts"
scater::plotExpression(
spe, features = rownames(spe)[1], x = "region", exprs_values = "logcounts"
)spatialCoords() is the coordinate source for native
spatial analysis. Historical x_coord/y_coord
metadata may describe an earlier alignment; retaining these columns for
provenance does not make them authoritative. SpaMTP expects named
x and y axes in a shared coordinate system. If
an importer uses names such as pxl_col_in_fullres and
pxl_row_in_fullres, map the known column axis to
x and row axis to y with
spatialCoordsNames(). Renaming does not convert units,
reverse an axis or perform image registration.
xy <- SpatialExperiment::spatialCoords(spe)
SpatialExperiment::spatialCoordsNames(spe)
## [1] "x" "y"
shifted <- spe
SpatialExperiment::spatialCoords(shifted)[, "x"] <- xy[, "x"] + 10
stopifnot(identical(SpatialExperiment::spatialCoords(spe), xy))
# Demonstrate an explicit name mapping on a copy, without changing the values.
imported <- spe
SpatialExperiment::spatialCoordsNames(imported) <-
c("pxl_col_in_fullres", "pxl_row_in_fullres")
axisNames <- SpatialExperiment::spatialCoordsNames(imported)
axisNames[match(c("pxl_col_in_fullres", "pxl_row_in_fullres"), axisNames)] <- c("x", "y")
SpatialExperiment::spatialCoordsNames(imported) <- axisNames
stopifnot(identical(SpatialExperiment::spatialCoords(imported), xy))An image has its own sample ID, image ID and scale factor in
imgData(). Adding a raster does not register it to the MSI
pixels. This small artificial image illustrates storage only. With
load = TRUE, the raster remains usable after the temporary
PNG is removed; a path-backed image requires a lasting path.
spe <- local({
path <- tempfile(fileext = ".png")
on.exit(unlink(path))
png::writePNG(matrix(seq(0, 1, length.out = 12), nrow = 3), path)
addSpatialImage(spe, imageSource = path, sampleId = "section1",
imageId = "demo", scaleFactor = 1, load = TRUE)
})
SpatialExperiment::imgData(spe)[, c("sample_id", "image_id", "scaleFactor")]
## DataFrame with 1 row and 3 columns
## sample_id image_id scaleFactor
## <character> <character> <numeric>
## 1 section1 demo 1
raster <- SpatialExperiment::imgRaster(spe, sample_id = "section1", image_id = "demo")
stopifnot(inherits(raster, "raster"))Only measurements of the same pixels belong in an
altExp(). First align and map independent MSI pixels and
transcriptomic spots with mapSpatialOmics(); do not rename
unrelated spots to force a match. addTranscriptome()
validates pixel names and reorders an already paired matrix.
transcriptome <- matrix(seq_len(12), nrow = 2,
dimnames = list(c("gene1", "gene2"), rev(colnames(spe))))
spe <- addTranscriptome(spe, transcriptome)
rna <- SingleCellExperiment::altExp(spe, "transcriptome")
stopifnot(identical(colnames(rna), colnames(spe)))
SummarizedExperiment::assay(rna, "counts")
## 1_1 1_2 2_1 2_2 3_1 3_2
## gene1 11 9 7 5 3 1
## gene2 12 10 8 6 4 2
selected <- spe[, spe$region == "edge"]
stopifnot(identical(colnames(SingleCellExperiment::altExp(selected, "transcriptome")),
colnames(selected)))
stopifnot(nrow(SpatialExperiment::spatialCoords(selected)) == ncol(selected))Feature subsetting affects the primary experiment’s rows; each alternative experiment retains its own feature space. Standard column subsetting keeps coordinates and alternative experiments aligned to the selected pixels.
S4Vectors::metadata(spe)$analysis <- list(
input_type = "binned MSI", normalization = "LogNormalize")
S4Vectors::metadata(spe)$analysis
## $input_type
## [1] "binned MSI"
##
## $normalization
## [1] "LogNormalize"For full annotation examples, see the indexed annotation
pipeline. It reads candidate summaries from rowData()
and stores ranked candidates and annotation provenance in
metadata().
SeuratObject is optional and is needed only for explicit conversion.
In Seurat 5, LayerData() reads an exact expression layer,
double brackets read cell or feature metadata, and
GetTissueCoordinates() reads image/FOV coordinates.
SpaMTP’s converters use these public APIs. Older serialized objects that
cannot be read by current accessors should be updated explicitly with
SeuratObject::UpdateSeuratObject() in the external Seurat
workflow; SpaMTP does not patch their internal slots.
if (requireNamespace("SeuratObject", quietly = TRUE)) {
seurat <- spatialExperimentToSeurat(spe)
values <- SeuratObject::LayerData(seurat, assay = "Spatial", layer = "counts")
pixel_metadata <- seurat[[]]
feature_metadata <- seurat[["Spatial"]][[]]
centres <- SeuratObject::GetTissueCoordinates(seurat, image = "fov", full = FALSE)
converted <- seuratToSpatialExperiment(seurat, image = "fov", layer = "counts")
stopifnot(identical(colnames(converted), colnames(spe)))
stopifnot(isTRUE(all.equal(
as.matrix(SummarizedExperiment::assay(converted, "counts")), as.matrix(values))))
S4Vectors::metadata(converted)$seurat_interoperability
}
## $primary_assay
## [1] "Spatial"
##
## $layer
## [1] "counts"
##
## $image
## [1] "fov"
##
## $coordinate_source
## [1] "image"
##
## $coordinate_columns
## [1] "x" "y"When no image is specified, a single image/FOV is authoritative over
any historical metadata coordinates. Metadata is used automatically only
if there are no images. With multiple images, select image
or supply explicit named coordinates; the converter never
guesses which slice to use. Centroids are read as one centre per pixel,
not as expanded boundary vertices.
Only the requested exact layer is transferred. Join split Seurat layers first if the complete dataset is intended. Unpaired assays are skipped with a warning. Optical rasters, graphs and reductions are not transferred. After conversion, continue with native accessors; Seurat is not used for SpaMTP analysis.
sessionInfo()
## R version 4.6.1 (2026-06-24)
## Platform: x86_64-pc-linux-gnu
## Running under: Ubuntu 26.04.1 LTS
##
## Matrix products: default
## BLAS: /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
## LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.32.so; LAPACK version 3.12.0
##
## locale:
## [1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C LC_TIME=en_US.UTF-8
## [4] LC_COLLATE=en_US.UTF-8 LC_MONETARY=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8
## [7] LC_PAPER=en_US.UTF-8 LC_NAME=C LC_ADDRESS=C
## [10] LC_TELEPHONE=C LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C
##
## time zone: Etc/UTC
## tzcode source: system (glibc)
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] SpaMTP_0.99.11 BiocStyle_2.41.0
##
## loaded via a namespace (and not attached):
## [1] splines_4.6.1 later_1.4.8 filelock_1.0.3
## [4] tibble_3.3.1 lifecycle_1.0.5 httr2_1.3.0
## [7] sf_1.1-3 edgeR_4.99.7 globals_0.20.0
## [10] lattice_0.23-1 crosstalk_1.2.2 magrittr_2.0.5
## [13] SpaMTPdb_0.99.4 limma_3.99.2 plotly_4.12.1
## [16] sass_0.4.10 rmarkdown_2.32 jquerylib_0.1.4
## [19] yaml_2.3.12 metapod_1.21.0 httpuv_1.6.17
## [22] otel_0.2.0 spam_2.11-4 sp_2.2-4
## [25] cowplot_1.2.0 DBI_1.3.0 buildtools_1.0.0
## [28] RColorBrewer_1.1-3 abind_1.4-8 GenomicRanges_1.65.4
## [31] purrr_1.2.2 BiocGenerics_0.59.12 rappdirs_0.3.4
## [34] SpaMTPData_0.99.5 IRanges_2.47.5 S4Vectors_0.51.10
## [37] ggrepel_0.9.8 irlba_2.4.1 listenv_1.1.0
## [40] maketools_1.3.2 pheatmap_1.0.13 units_1.0-1
## [43] RSpectra_0.16-2 matter_2.15.0 dqrng_0.4.1
## [46] parallelly_1.48.0 codetools_0.2-20 DelayedArray_0.39.8
## [49] scuttle_1.23.2 tidyselect_1.2.1 farver_2.1.2
## [52] ScaledMatrix_1.21.0 viridis_0.6.5 matrixStats_1.5.0
## [55] stats4_4.6.1 BiocFileCache_3.3.0 Seqinfo_1.3.2
## [58] jsonlite_2.0.0 CardinalIO_1.11.0 BiocNeighbors_2.7.3
## [61] e1071_1.7-17 progressr_1.0.0 scater_1.41.2
## [64] tools_4.6.1 Rcpp_1.1.2 glue_1.8.1
## [67] gridExtra_2.3.1 SparseArray_1.13.4 BiocBaseUtils_1.15.1
## [70] xfun_0.61 MatrixGenerics_1.25.0 dplyr_1.2.1
## [73] withr_3.0.3 BiocManager_1.30.27 fastmap_1.2.0
## [76] bluster_1.23.1 digest_0.6.39 rsvd_1.0.5
## [79] R6_2.6.1 mime_0.13 RSQLite_3.53.3
## [82] tidyr_1.3.2 generics_0.1.4 data.table_1.18.6.1
## [85] FNN_1.1.4.1 class_7.3-24 httr_1.4.9
## [88] htmlwidgets_1.6.4 S4Arrays_1.13.2 ontologyIndex_2.12
## [91] pkgconfig_2.0.3 gtable_0.3.6 blob_1.3.0
## [94] S7_0.2.2 SingleCellExperiment_1.35.2 XVector_0.53.0
## [97] sys_3.4.3 htmltools_0.5.9 dotCall64_1.2
## [100] fgsea_1.39.2 ProtGenerics_1.45.0 SeuratObject_5.4.0
## [103] scales_1.4.0 Biobase_2.73.2 png_0.1-9
## [106] SpatialExperiment_1.23.0 scran_1.41.1 knitr_1.52
## [109] rjson_0.2.23 nlme_3.1-171 curl_8.0.0
## [112] proxy_0.4-29 cachem_1.1.0 stringr_1.6.0
## [115] Cardinal_3.15.0 BiocVersion_3.24.0 KernSmooth_2.23-27
## [118] parallel_4.6.1 vipor_0.4.7 AnnotationDbi_1.75.2
## [121] pillar_1.11.1 grid_4.6.1 vctrs_0.7.3
## [124] promises_1.5.0 BiocSingular_1.29.2 dbplyr_2.6.0
## [127] beachmat_2.29.3 xtable_1.8-8 cluster_2.1.8.3
## [130] beeswarm_0.4.0 evaluate_1.0.5 magick_2.9.1
## [133] cli_3.6.6 locfit_1.5-9.12 compiler_4.6.1
## [136] rlang_1.3.0 crayon_1.5.3 future.apply_1.20.2
## [139] labeling_0.4.3 mclust_6.1.3 classInt_0.4-11
## [142] naturalsort_0.1.3 ggbeeswarm_0.7.3 stringi_1.8.9
## [145] viridisLite_0.4.3 BiocParallel_1.47.0 Biostrings_2.81.9
## [148] Matrix_1.7-6 ExperimentHub_3.3.2 bit64_4.8.6
## [151] future_1.76.0 ggplot2_4.0.3 KEGGREST_1.53.6
## [154] statmod_1.5.2 shiny_1.14.0 SummarizedExperiment_1.43.0
## [157] AnnotationHub_4.3.2 igraph_2.3.4 memoise_2.0.1
## [160] bslib_0.12.0 fastmatch_1.1-8 bit_4.6.0