-
Notifications
You must be signed in to change notification settings - Fork 10
utilities (query, mask, etc.) #148
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Changes from all commits
Commits
Show all changes
35 commits
Select commit
Hold shift + click to select a range
d9e6ff7
use sf_crop for query on shapes
HelenaLC 9a07c57
import sf::st_bbox
HelenaLC a867005
fix comm typo
HelenaLC 27a1b5c
+polygon query for point/shape
HelenaLC 699897d
tests for poly query
HelenaLC 5578788
cleaner box/pol validity
HelenaLC ba6f8e0
bug fix test edge case
HelenaLC 9c083b9
make copi happier
HelenaLC 374a066
more tests; more validity; documentation
HelenaLC 29f51a3
more docs
HelenaLC d8130ed
cleaner docs
HelenaLC 319b0bb
track changes
HelenaLC 5676dd0
v0.99.27 bump
HelenaLC 3348417
init mask revision
HelenaLC 2e70594
more docs
HelenaLC ecca7df
fix doc typo
HelenaLC 4d9539d
some progress on masking with tables
HelenaLC 2216dae
masking tests
HelenaLC 8c8e4ef
+Imports:scuttle
HelenaLC d9b2a20
note on masking
HelenaLC 00fc7ee
bug fix doc
HelenaLC c04939f
fix roxy imports
HelenaLC 4843dfd
fix R CMD check error/warnings
HelenaLC 60c8c46
make copi happier
HelenaLC 0ad8f95
added roxy imports
HelenaLC 8505a1f
rephrase comment
HelenaLC febdf4c
bug fix: check for row-wise duplicates to assure polygon vertices are…
HelenaLC 73539d9
fix method export
HelenaLC 5ba1055
assure bb is within bounds; add tests of query,labelArray; fractorize…
HelenaLC 1c147d7
code rearrangement; fix typo
HelenaLC 2c48a05
scuttle -> scrapper
HelenaLC fa2b29e
omit deps by aggregating via mtx mul
HelenaLC 6847822
bug fix; code cleaning
HelenaLC 60e0ddb
remove duplicated factoring
HelenaLC 0fd9fe3
expose 'assay' arg
HelenaLC File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,104 +1,166 @@ | ||
| #' @name mask | ||
| #' @title Masking | ||
| #' | ||
| #' @description ... | ||
| #' @description | ||
| #' Masking operations serve to aggregate data across layers, e.g., | ||
| #' counting points in shapes, averaging image channels by labels, etc. | ||
| #' For added flexibility, these may be carried out directly between elements, | ||
| #' or using an input \code{SpatialData} object and specifying element names. | ||
| #' | ||
| #' @param x \code{\link{SpatialData}} object. | ||
| #' @param i,j character string; names of elements to mask, | ||
| #' specifically, \code{i} will be masked by \code{j}, | ||
| #' adding a \code{table} for \code{j} in \code{x}. | ||
| #' @param how character string; statistic to use for masking. | ||
| #' @param name function use to generate the new \code{table}'s name. | ||
| #' @param ... optional arguments passed to and from other methods. | ||
| #' | ||
| #' @return \code{\link{SingleCellExperiment}} | ||
| #' @return Input \code{SpatialData} object \code{x} with an additional table. | ||
| #' | ||
| #' @examples | ||
| #' library(SingleCellExperiment) | ||
| #' x <- file.path("extdata", "blobs.zarr") | ||
| #' x <- system.file(x, package="SpatialData") | ||
| #' x <- readSpatialData(x, tables=FALSE) | ||
| #' | ||
| #' # count points in circles | ||
| #' x <- mask(x, "blobs_points", "blobs_circles") | ||
| #' x <- mask(x, "blobs_image", "blobs_labels") | ||
| #' tables(x) | ||
| #' # count points in shapes | ||
| #' y <- mask(x, "blobs_points", "blobs_circles") | ||
| #' tail(tables(y), 1) | ||
| #' | ||
| #' # average image channels by labels | ||
| #' y <- mask(x, "blobs_image", "blobs_labels") | ||
| #' tail(tables(y), 1) | ||
| #' | ||
| #' library(SpatialData.data) | ||
| #' x <- get_demo_SDdata("merfish") | ||
| #' x <- readSpatialData(x) | ||
| #' | ||
| #' # sum table counts by shapes | ||
| #' y <- mask(x, "cells", "anatomical") | ||
| #' tail(tables(y), 1) | ||
| #' | ||
| #' @export | ||
| NULL | ||
|
|
||
| # TODO: table from point + shape, image + label etc. etc. etc. | ||
| .check_ij <- \(x, .) stopifnot(length(.) == 1, is.character(.), . %in% unlist(colnames(x))) | ||
|
|
||
| #' @rdname mask | ||
| #' @importFrom methods as | ||
| #' @importFrom SummarizedExperiment assay assay<- | ||
| #' @importFrom SingleCellExperiment int_colData int_colData<- int_metadata<- | ||
| #' @export | ||
| setMethod("mask", "SpatialData", \(x, i, j, ...) { | ||
| stopifnot(length(i) == 1, is.character(i), i %in% unlist(colnames(x))) | ||
| stopifnot(length(j) == 1, is.character(j), j %in% unlist(colnames(x))) | ||
| # get element types | ||
| ls <- vapply( | ||
| list(i, j), \(e) rownames(x)[vapply(colnames(x), | ||
| \(es) e %in% es, logical(1))], character(1)) | ||
| a <- element(x, ls[[1]], i) | ||
| b <- element(x, ls[[2]], j) | ||
| t <- .mask(a, b, ...) | ||
| md <- list(region=j, | ||
| region_key="region", | ||
| instance_key="instance") | ||
| int_metadata(t)$spatialdata_attrs <- md | ||
| cd <- data.frame(region=j, instance=colnames(t)) | ||
| int_colData(t) <- cbind(int_colData(t), cd) | ||
| nm <- paste0(i, "_masked_by_", j) | ||
| `table<-`(x, nm, value=t) | ||
| setMethod("mask", c("SpatialData", "ANY", "ANY"), \(x, i, j, | ||
| how=NULL, name=\(i, j) sprintf("%s_by_%s", i, j), ...) { | ||
| .check_ij(x, i); .check_ij(x, j) | ||
| #if (!is.null(how)) how <- match.arg(how, c("sum", "mean")) | ||
| ok <- is.character(name) && length(name) == 1 && !name %in% tableNames(x) | ||
| nm <- if (is.function(name)) name(i, j) else if (ok) name else stop( | ||
| "Invalid 'name'; should be a function or a ", | ||
| "character string not yet in 'tableNames(x)'") | ||
| f <- \(i) names(which(rapply(colnames(x), \(.) i %in% ., "character"))) | ||
| .i <- element(x, f(i), i) | ||
| .j <- element(x, f(j), j) | ||
| t <- tryCatch(error=\(.) NULL, getTable(x, i)) | ||
| se <- .mask(.i, .j, how=how, table=t, ...) | ||
| md <- list(region=j, region_key="region", instance_key="instance") | ||
| int_metadata(se)$spatialdata_attrs <- md | ||
| assay(se) <- as(assay(se), "dgCMatrix") | ||
| cd <- int_colData(se) | ||
| cd$region <- j | ||
| cd$instance <- colnames(se) | ||
| int_colData(se) <- cd | ||
| `table<-`(x, nm, value=se) | ||
| }) | ||
|
|
||
| setGeneric(".mask", \(a, b, ...) standardGeneric(".mask")) | ||
| setGeneric(".mask", \(i, j, ...) standardGeneric(".mask")) | ||
|
|
||
| #' @noRd | ||
| #' @importFrom methods as | ||
| #' @importFrom Matrix rowSums sparseVector t | ||
| #' @importFrom Matrix sparseVector | ||
| #' @importFrom SummarizedExperiment assayNames<- | ||
| #' @importFrom SingleCellExperiment SingleCellExperiment | ||
| #' @importFrom sf st_as_sf st_geometry_type st_sfc st_point st_distance | ||
| setMethod(".mask", c("PointFrame", "ShapeFrame"), \(a, b) { | ||
| n <- nrow(b <- st_as_sf(data(b))) | ||
| fk <- meta(a)$spatialdata_attrs$feature_key | ||
| switch(paste(st_geometry_type(b)[1]), | ||
| POINT={ | ||
| # realize one feature at a time | ||
| is <- split(seq_len(length(a)), a[[fk]]) | ||
| ns <- lapply(is, \(.) { | ||
| # make points 'sf'-compliant | ||
| xy <- as.data.frame(a[., c("x", "y")]) | ||
| ps <- st_sfc(lapply(asplit(xy, 1), st_point)) | ||
| # for each circle, count points within radius | ||
| z <- rowSums(st_distance(b, ps) < b$radius) | ||
| # sparsify counts | ||
| sv <- sparseVector(z[i <- z > 0], which(i), n) | ||
| sm <- as(sv, "sparseMatrix") | ||
| }) | ||
| # collect intro matrix w/ dim. features x circles | ||
| ns <- t(as(do.call(cbind, ns), "dgCMatrix")) | ||
| rownames(ns) <- names(is) | ||
| colnames(ns) <- seq(ncol(ns)) | ||
| }) | ||
| SingleCellExperiment(list(counts=ns)) | ||
| setMethod(".mask", c("ImageArray", "LabelArray"), \(i, j, how=NULL, ...) { | ||
| if (is.null(how)) { how <- "mean"; message("Missing 'how'; defaulting to 'mean'") } | ||
| stopifnot(dim(i)[-1] == dim(j)) | ||
| .j <- as(data(j), "sparseVector") | ||
| .j <- as.vector(.j[ok <- .j > 0]) | ||
| mx <- apply(data(i), 1, \(.i) { | ||
| .i <- as(.i, "sparseVector") | ||
| .i <- as.vector(.i[ok]) | ||
| tapply(.i, .j, how) | ||
| }) | ||
| colnames(mx) <- channels(i) | ||
| se <- SingleCellExperiment(list(t(mx))) | ||
| assayNames(se) <- how | ||
| return(se) | ||
| }) | ||
|
|
||
| #' @noRd | ||
| #' @importFrom methods as | ||
| #' @importFrom DelayedArray realize | ||
| #' @importFrom S4Arrays as.array.Array | ||
| #' @importFrom Matrix t rowSums sparseVector sparseMatrix | ||
| #' @importFrom SingleCellExperiment SingleCellExperiment | ||
| setMethod(".mask", c("ImageArray", "LabelArray"), \(a, b, fun=mean) { | ||
| # TODO: somehow rewrite w/o realizing everything | ||
| # at once (maybe w/ 'DelayedArray::blockApply'?) | ||
| .a2v <- \(.) as.vector(as.array.Array(.)) | ||
| stopifnot(dim(a)[-1] == dim(b)) | ||
| w <- .a2v(data(b)); w[w == 0] <- NA | ||
| n <- length(i <- unique(w[!is.na(w)])) | ||
| ns <- vapply(seq_len(dim(a)[1]), \(.) { | ||
| v <- .a2v(data(a, 1)[., , ]) | ||
| tapply(v, w, sum, na.rm=TRUE) | ||
| }, numeric(n)) | ||
| ns <- t(as(ns, "dgCMatrix")) | ||
| dimnames(ns) <- list(seq(dim(a)[1]), i) | ||
| #' @importFrom sf st_as_sf st_geometry_type st_distance | ||
| setMethod(".mask", c("PointFrame", "ShapeFrame"), \(i, j, how=NULL, ...) { | ||
| if (!is.null(how)) warning("Can only count when masking points; ignoring 'how'") | ||
| n <- nrow(j <- st_as_sf(data(j))) | ||
| fun <- switch(as.character(st_geometry_type(j[1, ])), | ||
| POINT=\(i, j) rowSums(st_distance(j, i) <= j$radius), | ||
| \(i, j) vapply(st_intersects(j, i), length, integer(1))) | ||
| # realize one feature at i time | ||
| is <- split(seq_len(length(i)), i[[feature_key(i)]]) | ||
| ns <- lapply(is, \(.) { | ||
| # make points 'sf'-compliant | ||
| i <- as.data.frame(i[., c("x", "y")]) | ||
| i <- st_as_sf(i, coords=c("x", "y")) | ||
| # for each shape, count intersecting points | ||
| z <- fun(i, j) | ||
| # sparsify counts | ||
| sv <- sparseVector(z[i <- z > 0], which(i), n) | ||
| sm <- as(sv, "sparseMatrix") | ||
| }) | ||
| # collect into matrix w/ dim. features x shapes | ||
| ns <- t(do.call(cbind, ns)) | ||
| rownames(ns) <- names(is) | ||
| colnames(ns) <- seq(ncol(ns)) | ||
| SingleCellExperiment(list(counts=ns)) | ||
| }) | ||
| setMethod(".mask", c("ANY", "ANY"), \(a, b) | ||
| stop("'mask'ing between these element types not supported.")) | ||
|
|
||
| #' @noRd | ||
| #' @importFrom methods as | ||
| #' @importFrom Matrix sparseMatrix | ||
| #' @importFrom SummarizedExperiment assay | ||
| #' @importFrom SingleCellExperiment SingleCellExperiment | ||
| setMethod(".mask", c("ShapeFrame", "ShapeFrame"), \(i, j, how=NULL, table=NULL, value=NULL, assay=1, ...) { | ||
| # validity | ||
| if (is.null(table)) stop("Missing 'table'; can't mask shapes without") | ||
| ok <- is.null(value) || (is.character(value) && all(value %in% rownames(table))) | ||
| if (!ok) stop("Invalid 'value'; should be in 'rownames(table(x, i))'") | ||
| if (is.null(how)) { how <- "sum"; message("Missing 'how'; defaulting to 'sum'") } | ||
| if (is.character(how)) how <- match.arg(how, c("sum", "mean", "detected", "prop.detected")) | ||
| # grouping | ||
| js <- st_intersects(st_as_sf(data(j)), st_as_sf(data(i))) | ||
| is <- factor(integer(nrow(i)), seq(0, nrow(j))) | ||
| is[unlist(js)] <- rep(seq_along(js), lengths(js)) | ||
| ns <- tabulate(is, ni <- nlevels(is)) | ||
| # aggregation | ||
| mx <- assay(table, assay) | ||
| if (grepl("detected$", how)) mx <- mx > 0 | ||
| my <- sparseMatrix( | ||
| x=rep(1, length(is)), | ||
| i=seq_along(is), j=is, | ||
| dims=c(ncol(table), ni)) | ||
| mx <- mx %*% my | ||
| if (grepl("mean|prop", how)) mx <- t(t(mx)/ns) | ||
| # wrangling | ||
| mx <- as(mx, "dgCMatrix") | ||
| colnames(mx) <- levels(is) | ||
| mx <- list(mx); names(mx) <- how | ||
| se <- SingleCellExperiment(mx) | ||
| nm <- paste0("n_", meta(table)$region) | ||
| se[[nm]] <- ns | ||
| return(se) | ||
| }) | ||
|
|
||
| #' @noRd | ||
| setMethod(".mask", c("ANY", "ANY"), \(i, j, ...) | ||
| stop("'mask'ing between these element types not yet supported")) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.