Skip to content

Commit d49e46d

Browse files
authored
Merge pull request #10215 from satijalab/update-visium-spatial
Update loading & visualization for Visium objects
2 parents ee207eb + 1e44554 commit d49e46d

26 files changed

+275
-315
lines changed

.github/workflows/merge_checks.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ jobs:
4444
- name: Install Dependencies
4545
run: installDeps.r -s
4646

47+
# Install SeuratObject from CRAN
48+
# This helps check updates to SeuratObject and Seurat in rapid succession
49+
- name: Install SeuratObject from CRAN (source)
50+
run: Rscript -e "install.packages('SeuratObject', type = 'source', repos = 'https://cloud.r-project.org')"
51+
4752
# Run CRAN checks, if any ERRORs or WARNINGs are raised the check fails.
4853
# Certain tests are skipped when running as CRAN—skip all tests so they
4954
# can be run together in a subsequent step.

NAMESPACE

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,7 @@ importFrom(SeuratObject,"%NA%")
530530
importFrom(SeuratObject,"%iff%")
531531
importFrom(SeuratObject,"%||%")
532532
importFrom(SeuratObject,"DefaultAssay<-")
533+
importFrom(SeuratObject,"DefaultBoundary<-")
533534
importFrom(SeuratObject,"DefaultLayer<-")
534535
importFrom(SeuratObject,"Idents<-")
535536
importFrom(SeuratObject,"Index<-")
@@ -635,6 +636,7 @@ importFrom(ggplot2,GeomPolygon)
635636
importFrom(ggplot2,GeomViolin)
636637
importFrom(ggplot2,aes)
637638
importFrom(ggplot2,alpha)
639+
importFrom(ggplot2,annotation_custom)
638640
importFrom(ggplot2,annotation_raster)
639641
importFrom(ggplot2,coord_cartesian)
640642
importFrom(ggplot2,coord_fixed)
@@ -705,6 +707,7 @@ importFrom(ggplot2,scale_x_log10)
705707
importFrom(ggplot2,scale_y_continuous)
706708
importFrom(ggplot2,scale_y_discrete)
707709
importFrom(ggplot2,scale_y_log10)
710+
importFrom(ggplot2,scale_y_reverse)
708711
importFrom(ggplot2,stat_density2d)
709712
importFrom(ggplot2,stat_qq)
710713
importFrom(ggplot2,sym)

R/objects.R

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1731,14 +1731,21 @@ GetTissueCoordinates.VisiumV2 <- function(
17311731
scale = NULL,
17321732
...
17331733
) {
1734-
# make call to GetTissueCoordiantes.FOV
1734+
# make call to GetTissueCoordinates.FOV
17351735
coordinates <- NextMethod(object, ...)
1736+
dots <- list(...)
1737+
which <- dots$which %||% DefaultBoundary(object = object)
1738+
which <- match.arg(arg = which, choices = names(x = object))
17361739
# do some cleanup of the resulting data.frame to make it play nice
17371740
# with `SpatialPlot` - namely set rownames and re-order the columns so
17381741
# that the actual position values appear first
1739-
rownames(coordinates) <- coordinates[["cell"]]
1742+
1743+
# avoid extra processing for Segmentation objects,
1744+
# otherwise "duplicate 'row.names' are not allowed" error occurs
1745+
if (!(inherits(object[[which]], "Segmentation"))) {
1746+
rownames(coordinates) <- coordinates[["cell"]]
1747+
}
17401748
coordinates <- coordinates[, c("x", "y", "cell")]
1741-
17421749
if (!is.null(scale)) {
17431750
# scale the coordinates by the specified factor
17441751
scale <- match.arg(scale, choices = c("lowres", "hires"))

R/preprocessing.R

Lines changed: 58 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -506,13 +506,17 @@ GetResidual <- function(
506506
#' passed in it should be co-indexed with \code{`bin.size`}
507507
#' @param segmentation.type Which segmentations to load (cell or nucleus) when bin.size includes "polygons".
508508
#' Defaults to "cell".
509+
#' @param compact Whether to store segmentations in \emph{only} the \code{sf.data} slot
510+
#' in the corresponding Segmentation object (default TRUE) to save memory and processing time.
511+
#' If FALSE, segmentations are also stored in \code{\link[sp]{sp}} format in addition to the \code{sf.data} slot.
509512
#' @param image.name Name of the tissue image to be plotted. Defaults to tissue_lowres_image.png
510513
#' @param ... Arguments passed to \code{\link{Read10X_h5}}
511514
#'
512515
#' @return A \code{Seurat} object
513516
#'
514517
#' @importFrom png readPNG
515518
#' @importFrom jsonlite fromJSON
519+
#' @importFrom SeuratObject DefaultBoundary<-
516520
#'
517521
#' @export
518522
#' @concept preprocessing
@@ -536,6 +540,7 @@ Load10X_Spatial <- function (
536540
image = NULL,
537541
image.name = "tissue_lowres_image.png",
538542
segmentation.type = NULL,
543+
compact = TRUE,
539544
...
540545
) {
541546
# if more than one directory is passed in
@@ -669,6 +674,11 @@ Load10X_Spatial <- function (
669674

670675
# read segmentation data if requested
671676
if (load.segmentations) {
677+
# Check for required packages, stop with clear message if missing
678+
if (!requireNamespace("sf", quietly = TRUE)) {
679+
stop("The 'sf' package must be installed to load segmentation data.")
680+
}
681+
672682
segmentation.assay.name <- paste0(assay, ".Polygons")
673683
seg.data.dir <- file.path(data.dir, "segmented_outputs")
674684

@@ -710,49 +720,22 @@ Load10X_Spatial <- function (
710720
image.dir = file.path(seg.data.dir, "spatial"),
711721
data.dir = data.dir,
712722
image.name = image.name,
713-
segmentation.type = segmentation.type
714-
)
715-
716-
# Holds the segmentation object
717-
segmentation_obj <- visium.segmentation@boundaries$segmentation
718-
719-
# Get sf data
720-
sf_data <- visium.segmentation@boundaries$segmentation@sf.data
721-
722-
# Set the attribute-geometry relationship to constant
723-
# See https://r-spatial.github.io/sf/reference/sf.html#details
724-
st_agr(sf_data) <- "constant"
725-
726-
# Create a dataframe from sf data to hold centroids
727-
centroids_obj <- visium.segmentation@boundaries$centroids
728-
centroid_coords <- sf::st_coordinates(sf::st_centroid(sf_data))
729-
centroids_df <- data.frame(
730-
x = centroid_coords[, "X"],
731-
y = centroid_coords[, "Y"],
732-
row.names = sf_data$barcodes
723+
segmentation.type = segmentation.type,
724+
compact = compact
733725
)
734726

735-
# Create centroids object
736-
centroids <- CreateCentroids(centroids_df,
737-
nsides = Inf,
738-
radius = NULL,
739-
theta = 0)
740-
741-
# Add centroids to the Visium object
742-
visium.segmentation@boundaries$centroids <- centroids
743-
744727
# Create a new Seurat object with the raw counts
745728
segmentation.object <- CreateSeuratObject(
746729
segmentation.counts,
747730
assay = segmentation.assay.name
748731
)
749-
750-
# Make sure the list of cell names between segmentations & raw counts matches exactly
732+
733+
common_cells <- unique(Cells(visium.segmentation)[Cells(visium.segmentation) %in% Cells(segmentation.object)])
751734
visium.segmentation <- subset(
752735
x = visium.segmentation,
753-
cells = intersect(Cells(segmentation.object), Cells(visium.segmentation))
736+
cells = common_cells
754737
)
755-
738+
756739
# Set the default boundary type to centroids for plotting
757740
DefaultBoundary(object = visium.segmentation) <- "centroids"
758741

@@ -1426,23 +1409,31 @@ Read10X_Image <- function(
14261409

14271410
# Create an `sp` compatible `FOV` instance.
14281411
fov <- CreateFOV(
1429-
coordinates[, c("imagerow", "imagecol")],
1412+
coordinates[, c("imagecol", "imagerow")],
14301413
type = "centroids",
14311414
radius = scale.factors[["spot"]],
14321415
assay = assay,
14331416
key = key
14341417
)
14351418

1436-
# Build the final `VisiumV2` instance, essentially just adding `image` and
1437-
# `scale.factors` to the `fov`.
1419+
#### NOTE ####
1420+
# The Visium coordinate system takes the origin to be in the top-left corner,
1421+
# where the x-axis is horizontal and associated with the image column.
1422+
# We mark this with the coords_x_orientation flag.
1423+
# Older Visium objects in Seurat have a different system (x-axis vertical, etc),
1424+
# which is updated after checking whether the flag is set (SeuratObject::UpdateSeuratObject).
1425+
###############
1426+
1427+
# Build the final `VisiumV2` instance
14381428
visium.v2 <- new(
14391429
Class = "VisiumV2",
14401430
boundaries = fov@boundaries,
14411431
molecules = fov@molecules,
14421432
assay = fov@assay,
14431433
key = fov@key,
14441434
image = image,
1445-
scale.factors = scale.factors
1435+
scale.factors = scale.factors,
1436+
coords_x_orientation = "horizontal"
14461437
)
14471438

14481439
return(visium.v2)
@@ -1548,6 +1539,7 @@ Read10X_ScaleFactors <- function(filename) {
15481539
#' @param assay Name of assay to associate segmentations to
15491540
#' @param slice Name of the slice to associate the segmentations to
15501541
#' @param segmentation.type Which segmentations to load, cell or nucleus. If using nucleus the full matrix from cells is still used
1542+
#' @param compact Whether to store segmentations in only the \code{sf.data} slot; see \code{\link{Load10X_Spatial}} for details
15511543
#'
15521544
#'
15531545
#' @return A VisiumV2 object with segmentations
@@ -1560,27 +1552,30 @@ Read10X_Segmentations <- function (image.dir,
15601552
image.name = "tissue_lowres_image.png",
15611553
assay = "Spatial.Polygons",
15621554
slice = "slice1.polygons",
1563-
segmentation.type = "cell") {
1564-
1565-
image <- png::readPNG(source = file.path(image.dir, image.name))
1566-
1567-
scale.factors <- Read10X_ScaleFactors(filename = file.path(image.dir,
1568-
"scalefactors_json.json"))
1569-
key <- Key(slice, quiet = TRUE)
1555+
segmentation.type = "cell",
1556+
compact = TRUE) {
15701557

1571-
# Pass proper scale.factor based on whether image is lowres or hires
1572-
# We assume if not lowres it is hires - image has been validated above
1573-
scale.factor <- if (grepl("lowres", image.name)) "lowres" else "hires"
15741558

1575-
sf.data <- Read10X_HD_GeoJson(data.dir = data.dir,
1576-
image.dir = image.dir,
1559+
sf.obj <- Read10X_HD_GeoJson(data.dir = data.dir,
15771560
segmentation.type = segmentation.type)
1561+
1562+
# Create a Segmentation object; populate it based on the coordinates from the sf object
1563+
segmentations <- CreateSegmentation(sf.obj, compact = compact)
15781564

1579-
# Create a Segmentation object based on sf, populate sf.data and polygons
1580-
segmentation <- CreateSegmentation(sf.data)
1565+
# Create a Centroids object; populate it based on the centroids from the sf object
1566+
centroids <- CreateCentroids(sf.obj,
1567+
nsides = Inf,
1568+
radius = NULL,
1569+
theta = 0)
15811570

1582-
# Named list with segmentation
1583-
boundaries <- list(segmentation = segmentation)
1571+
# Named list with segmentations and centroids
1572+
boundaries <- list(segmentations = segmentations, centroids = centroids)
1573+
1574+
# Get image, scale factors, key
1575+
image <- png::readPNG(source = file.path(image.dir, image.name))
1576+
scale.factors <- Read10X_ScaleFactors(filename = file.path(image.dir,
1577+
"scalefactors_json.json"))
1578+
key <- Key(slice, quiet = TRUE)
15841579

15851580
# Build VisiumV2 object
15861581
visium.v2 <- new(
@@ -1589,11 +1584,13 @@ Read10X_Segmentations <- function (image.dir,
15891584
assay = assay,
15901585
key = key,
15911586
image = image,
1592-
scale.factors = scale.factors
1587+
scale.factors = scale.factors,
1588+
coords_x_orientation = "horizontal"
15931589
)
15941590

15951591
return(visium.v2)
15961592
}
1593+
15971594
#' Format 10X Genomics GeoJson cell IDs
15981595
#'
15991596
#' @param ids Vector of cell IDs to format
@@ -1619,25 +1616,24 @@ Format10X_GeoJson_CellID <- function(ids, prefix = "cellid_", suffix = "-1", dig
16191616
#' Load 10X Genomics GeoJson
16201617
#'
16211618
#' @param data.dir Path to the directory containing matrix data
1622-
#' @param image.dir Path to the directory with spatial GeoJSON data
16231619
#' @param segmentation.type Which segmentations to load, cell or nucleus. If using nucleus the full matrix from cells is still used
16241620
#'
1625-
#' @return A FOV
1621+
#' @return An \code{sf} object containing polygon segmentations from the GeoJSON provided by 10x, formatted for downstream coordinate retrieval
16261622
#'
16271623
#' @export
16281624
#' @concept preprocessing
16291625
#'
1630-
Read10X_HD_GeoJson <- function(data.dir, image.dir, segmentation.type = "cell") {
1631-
segmentation_polygons <- read_sf(file.path(data.dir,"segmented_outputs", paste0(segmentation.type, "_segmentations.geojson")))
1626+
Read10X_HD_GeoJson <- function(data.dir, segmentation.type = "cell") {
1627+
segmentation_polygons <- sf::read_sf(file.path(data.dir,"segmented_outputs", paste0(segmentation.type, "_segmentations.geojson")))
16321628

16331629
# Restructure sf geometry for downstream compatibility
1634-
segmentation_polygons$geometry <- lapply(
1630+
segmentation_polygons$geometry <- sf::st_sfc(lapply(
16351631
segmentation_polygons$geometry,
16361632
function(geom) {
16371633
coords <- geom[[1]]
1638-
st_polygon(list(coords))
1634+
sf::st_polygon(list(coords))
16391635
}
1640-
) %>% st_sfc(crs = st_crs(NA))
1636+
), crs = sf::st_crs(NA))
16411637

16421638
segmentation_polygons$barcodes <- Format10X_GeoJson_CellID(segmentation_polygons$cell_id)
16431639
segmentation_polygons
@@ -3747,7 +3743,7 @@ RunMoransI <- function(data, pos, verbose = TRUE) {
37473743
#'
37483744
#' @examples
37493745
#' data("pbmc_small")
3750-
#' counts = as.matrix(x = GetAssayData(object = pbmc_small, assay = "RNA", slot = "counts"))
3746+
#' counts = as.matrix(x = GetAssayData(object = pbmc_small, assay = "RNA", layer = "counts"))
37513747
#' downsampled = SampleUMI(data = counts)
37523748
#' head(x = downsampled)
37533749
#'

R/utilities.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -760,7 +760,7 @@ CreateAnn <- function(name, ndim) {
760760
#' # Define custom distance matrix
761761
#' manhattan.distance <- function(x, y) return(sum(abs(x-y)))
762762
#'
763-
#' input.data <- GetAssayData(pbmc_small, assay.type = "RNA", slot = "scale.data")
763+
#' input.data <- GetAssayData(pbmc_small, assay.type = "RNA", layer = "scale.data")
764764
#' cell.manhattan.dist <- CustomDistance(input.data, manhattan.distance)
765765
#'
766766
CustomDistance <- function(my.mat, my.function, ...) {

0 commit comments

Comments
 (0)