diff --git a/.github/workflows/covr.yml b/.github/workflows/covr.yml index db981c9f..e8e52a4e 100644 --- a/.github/workflows/covr.yml +++ b/.github/workflows/covr.yml @@ -27,14 +27,6 @@ jobs: with: use-public-rspm: true - - name: Set up dependencies (GiottoData) - run: | - suppressWarnings({ - install.packages("remotes") - remotes::install_github("drieslab/GiottoData", build = FALSE) - }) - shell: Rscript {0} - - name: Set up dependencies (general) uses: r-lib/actions/setup-r-dependencies@v2 env: @@ -42,7 +34,20 @@ jobs: _R_CHECK_RD_XREFS: false with: dependencies: '"hard"' # do not use suggested dependencies - extra-packages: any::rcmdcheck, any::testthat, any::rlang, any::R.utils, any::sp, any::stars, any::raster, any::sf, any::RTriangle, any::geometry, any::covr + install-pandoc: false + extra-packages: | + github::drieslab/GiottoData + any::rcmdcheck + any::testthat + any::rlang + any::R.utils + any::sp + any::stars + any::raster + any::sf + any::RTriangle + any::geometry + any::covr needs: coverage - name: Generate coverage report diff --git a/.github/workflows/dev_check.yml b/.github/workflows/dev_check.yml index 00f49405..af4b57f5 100644 --- a/.github/workflows/dev_check.yml +++ b/.github/workflows/dev_check.yml @@ -42,7 +42,24 @@ jobs: _R_CHECK_RD_XREFS: false with: dependencies: '"hard"' # do not use suggested dependencies - extra-packages: any::rcmdcheck, any::testthat, any::rlang, any::R.utils, any::knitr, any::rmarkdown, any::qs, any::sp, any::stars, any::raster, any::sf, any::scattermore, any::exactextractr, any::RTriangle, any::geometry, github::drieslab/GiottoData + install-pandoc: false + extra-packages: | + any::rcmdcheck + any::testthat + any::rlang + any::R.utils + any::knitr + any::rmarkdown + any::qs + any::sp + any::stars + any::raster + any::sf + any::scattermore + any::exactextractr + any::RTriangle + any::geometry + github::drieslab/GiottoData - name: Run R CMD check uses: r-lib/actions/check-r-package@v2 diff --git a/.github/workflows/main_check.yml b/.github/workflows/main_check.yml index 1e360a70..00a8d733 100644 --- a/.github/workflows/main_check.yml +++ b/.github/workflows/main_check.yml @@ -58,7 +58,24 @@ jobs: _R_CHECK_RD_XREFS: false with: dependencies: '"hard"' # do not use suggested dependencies - extra-packages: any::rcmdcheck, any::testthat, any::rlang, any::R.utils, any::remotes, any::knitr, any::rmarkdown, any::sp, any::stars, any::raster, any::sf, any::scattermore, any::exactextractr, any::RTriangle, any::geometry, github::drieslab/GiottoData + install-pandoc: false + extra-packages: | + any::rcmdcheck + any::testthat + any::rlang + any::R.utils + any::remotes + any::knitr + any::rmarkdown + any::sp + any::stars + any::raster + any::sf + any::scattermore + any::exactextractr + any::RTriangle + any::geometry + github::drieslab/GiottoData - name: Test python env build run: | diff --git a/DESCRIPTION b/DESCRIPTION index c8321e2f..5454b683 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: GiottoClass Title: Giotto Suite object definitions and framework -Version: 0.2.1 +Version: 0.2.2 Authors@R: c( person("Ruben", "Dries", email = "rubendries@gmail.com", role = c("aut", "cre")), diff --git a/NEWS.md b/NEWS.md index 0245cbd4..826c6631 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,18 @@ +# GiottoClass 0.2.2 (2024/03/01) + +## bug fixes +- fix `createGiottoPolygonsFromMask()` IDs being applied out of sync to mask values +- fix `createGiottoPolygon()` `character` method dispatch for `raster` inputs +- remove unused `fix_multipart` param in `createGiottoPolygonsFromMask()` +- fix `giottoPolygon` ID caching after `rbind()` + +## enhancements +- `createGiottoPolygonsFromMask()` now has `ID_fmt` param for finer control of automatic `poly_ID` generation +- `.flip_spatvect()` internal for flipping `SpatVector` across arbitrary x and y vals + + # GiottoClass 0.2.1 (2024/02/28) ## breaking changes diff --git a/R/create.R b/R/create.R index 817d79ed..8f89e23b 100644 --- a/R/create.R +++ b/R/create.R @@ -672,13 +672,13 @@ createGiottoObjectSubcellular <- function( remove_background_polygon = TRUE, background_algo = c("range"), fill_holes = TRUE, + ID_fmt = "cell_", poly_IDs = NULL, flip_vertical = TRUE, shift_vertical_step = TRUE, flip_horizontal = TRUE, shift_horizontal_step = TRUE, - calc_centroids = FALSE, - fix_multipart = TRUE + calc_centroids = FALSE ) } @@ -1917,7 +1917,7 @@ create_giotto_points_object <- function(feat_type = "rna", #' spatial annotations and polygons. Inputs can be from a structured data.frame #' object where three of the columns should correspond to x/y vertices and the #' polygon ID and additional columns are set as attributes, a spatial file -#' such as wkt, .shp, or .GeoJSON, or a mask file (e.g. segmentation results) +#' such as wkt, .shp, or .GeoJSON, or a mask file (e.g. segmentation results). #' @param x input. Filepath to a .GeoJSON or a mask image file. Can also be a #' data.frame with vertex 'x', 'y', and 'poly_ID' information. #' @param name name for polygons @@ -1956,17 +1956,6 @@ setMethod( ) #' @rdname createGiottoPolygon -#' @param mask_method how the mask file defines individual segmentation annotations -#' @param remove_background_polygon try to remove background polygon (default: FALSE) -#' @param background_algo algorithm to remove background polygon -#' @param fill_holes fill holes within created polygons -#' @param poly_IDs unique names for each polygon in the mask file -#' @param flip_vertical flip mask figure in a vertical manner -#' @param shift_vertical_step shift vertical (boolean or numerical) -#' @param flip_horizontal flip mask figure in a horizontal manner -#' @param shift_horizontal_step shift horizontal (boolean or numerical) -#' @param fix_multipart try to split polygons with multiple parts (default: TRUE) -#' @param remove_unvalid_polygons remove unvalid polygons (default: TRUE) #' @export setMethod( "createGiottoPolygon", signature("SpatRaster"), @@ -1977,11 +1966,11 @@ setMethod( background_algo = c("range"), fill_holes = TRUE, poly_IDs = NULL, + ID_fmt = "cell_", flip_vertical = TRUE, shift_vertical_step = TRUE, flip_horizontal = TRUE, shift_horizontal_step = TRUE, - fix_multipart = TRUE, remove_unvalid_polygons = TRUE, calc_centroids = FALSE, verbose = TRUE) { @@ -1995,42 +1984,43 @@ setMethod( background_algo = background_algo, fill_holes = fill_holes, poly_IDs = poly_IDs, + ID_fmt = ID_fmt, flip_vertical = flip_vertical, shift_vertical_step = shift_vertical_step, flip_horizontal = flip_horizontal, shift_horizontal_step = shift_horizontal_step, - fix_multipart = fix_multipart, remove_unvalid_polygons = remove_unvalid_polygons, calc_centroids = calc_centroids ) } ) -#' @rdname createGiottoPolygon -#' @export -setMethod( - "createGiottoPolygon", signature("data.frame"), - function(x, - name = "cell", - calc_centroids = FALSE, - skip_eval_dfr = FALSE, - copy_dt = TRUE, - verbose = TRUE) { - createGiottoPolygonsFromDfr( - segmdfr = x, - name = name, - calc_centroids = calc_centroids, - skip_eval_dfr = skip_eval_dfr, - copy_dt = copy_dt, - verbose = verbose - ) - } -) #' @rdname createGiottoPolygon #' @param \dots additional params to pass. For character method, params pass to #' SpatRaster or SpatVector methods, depending on whether x was a filepath to #' a maskfile or a spatial file (ex: wkt, shp, GeoJSON) respectively. +#' @examples +#' # %%%%%%%%% `createGiottoPolygon()` examples %%%%%%%%% # +#' # ------- create from a mask image ------- # +#' m <- system.file("extdata/toy_mask_multi.tif", package = "GiottoClass") +#' plot(terra::rast(m), col = grDevices::hcl.colors(7)) +#' gp <- createGiottoPolygon( +#' m, +#' flip_vertical = FALSE, flip_horizontal = FALSE, +#' shift_horizontal_step = FALSE, shift_vertical_step = FALSE, +#' ID_fmt = "id_test_%03d", +#' name = "test" +#' ) +#' plot(gp, col = grDevices::hcl.colors(7)) +#' +#' # ------- create from an shp file ------- # +#' shp <- system.file("extdata/toy_poly.shp", package = "GiottoClass") +#' # vector inputs do not have params for flipping and shifting +#' gp2 <- createGiottoPolygon(shp, name = "test") +#' plot(gp2, col = grDevices::hcl.colors(7)) +#' +#' #' @export setMethod( "createGiottoPolygon", signature("character"), @@ -2039,63 +2029,152 @@ setMethod( # try success means it should be mask file # try failure means it should be vector file - try_rast <- try( + try_rast <- tryCatch( { terra::rast(x) }, - silent = TRUE + error = function(e) return(invisible(NULL)), + warning = function(w) {NULL} ) # mask workflow if (inherits(try_rast, "SpatRaster")) { - return(createGiottoPolygon(x, ...)) + return(createGiottoPolygon(try_rast, ...)) } # file workflow - return(createGiottoPolygon( - x = terra::vect(x), - ... - )) + return(createGiottoPolygon(x = terra::vect(x), ...)) + } +) + + +#' @rdname createGiottoPolygon +#' @examples +#' # ------- create from data.frame-like ------- # +#' shp <- system.file("extdata/toy_poly.shp", package = "GiottoClass") +#' gpoly <- createGiottoPolygon(shp, name = "test") +#' plot(gpoly) +#' gpoly_dt <- data.table::as.data.table(gpoly, geom = "XY") +#' needed_cols_dt <- gpoly_dt[, .(geom, part, x, y, hole, poly_ID)] +#' force(needed_cols_dt) +#' +#' out <- createGiottoPolygon(needed_cols_dt, +#' name = "test") +#' plot(out) +#' +#' +#' @export +setMethod( + "createGiottoPolygon", signature("data.frame"), + function(x, + name = "cell", + calc_centroids = FALSE, + skip_eval_dfr = FALSE, + copy_dt = TRUE, + verbose = TRUE) { + createGiottoPolygonsFromDfr( + segmdfr = x, + name = name, + calc_centroids = calc_centroids, + skip_eval_dfr = skip_eval_dfr, + copy_dt = copy_dt, + verbose = verbose + ) } ) -#' @title Create giotto polygons from mask file #' @rdname createGiottoPolygon #' @param maskfile path to mask file -#' @param mask_method how the mask file defines individual segmentation annotations -#' @param name name for polygons +#' @param mask_method how the mask file defines individual segmentation annotations. +#' See *mask_method* section +#' @param name character. Name to assign created `giottoPolygon` #' @param remove_background_polygon try to remove background polygon (default: FALSE) #' @param background_algo algorithm to remove background polygon #' @param fill_holes fill holes within created polygons -#' @param poly_IDs unique names for each polygon in the mask file +#' @param poly_IDs character vector. Default = NULL. Custom unique names for +#' each polygon in the mask file. +#' @param ID_fmt character. Only applied if `poly_IDs = NULL`. Naming scheme for +#' poly_IDs. Default = "cell_". See *ID_fmt* section. #' @param flip_vertical flip mask figure in a vertical manner #' @param shift_vertical_step shift vertical (boolean or numerical) #' @param flip_horizontal flip mask figure in a horizontal manner #' @param shift_horizontal_step shift horizontal (boolean or numerical) -#' @param calc_centroids calculate centroids for polygons -#' @param fix_multipart try to split polygons with multiple parts (default: TRUE) #' @param remove_unvalid_polygons remove unvalid polygons (default: TRUE) -#' @return a giotto polygon object #' @concept mask polygon +#' @section mask_method: +#' One of "single", "multiple", or "guess". +#' \itemize{ +#' \item{*"single"* assumes that the provided mask image is binary, with only +#' polygon vs background being distinct values. With this kind of image, the +#' expected generated polygons is a single multipart polygon. "single" takes +#' this multipart polygon and breaks it apart into individual singlepart +#' polygons. An initial simple `numeric` index as the 'nth' polygon found in +#' the mask image will be applied as an ID (see *ID_fmt* section).} +#' \item{*"multiple"* assumes that the provided mask image has distinct +#' intensity values to specify the IDs of individual polygons. An initial +#' `numeric` ID is applied as the intensity value of the pixels that made up +#' the annotation for that polygon in the mask image (see *ID_fmt* section).} +#' \item{*"guess"* examines the values in the image to pick the most likely +#' appropriate method out of "single" or "multiple".} +#' } +#' @section ID_fmt: +#' Defaults to applying the input as a prefix (using `paste0()`) to the +#' numerical ID values detected by `mask_method`. (ie: `ID_fmt = "cell_"` +#' produces `cell_1`, `cell_2`, `cell_3`, ...)\cr +#' If a "%" character is detected in the input then the input will be treated as +#' a `sprintf()` `fmt` param input instead. (ie: `ID_fmt = "cell_%03d"` produces +#' `cell_001`, `cell_002`, `cell_003`, ...) +#' @return a giotto polygon object +#' @examples +#' # %%%%%%%%% `createGiottoPolygonsFromMask()` examples %%%%%%%%% # +#' mask_multi <- system.file("extdata/toy_mask_multi.tif", +#' package = "GiottoClass") +#' mask_single <- system.file("extdata/toy_mask_single.tif", +#' package = "GiottoClass") +#' plot(terra::rast(mask_multi), col = grDevices::hcl.colors(7)) +#' plot(terra::rast(mask_single)) +#' +#' gpoly1 = createGiottoPolygonsFromMask( +#' mask_multi, +#' flip_vertical = FALSE, flip_horizontal = FALSE, +#' shift_horizontal_step = FALSE, shift_vertical_step = FALSE, +#' ID_fmt = "id_test_%03d", +#' name = "multi_test" +#' ) +#' plot(gpoly1, col = grDevices::hcl.colors(7)) +#' +#' gpoly2 = createGiottoPolygonsFromMask( +#' mask_single, +#' flip_vertical = FALSE, flip_horizontal = FALSE, +#' shift_horizontal_step = FALSE, shift_vertical_step = FALSE, +#' ID_fmt = "id_test_%03d", +#' name = "single_test" +#' ) +#' plot(gpoly2, col = grDevices::hcl.colors(5)) #' @export -createGiottoPolygonsFromMask <- function(maskfile, - mask_method = c("guess", "single", "multiple"), - name = "cell", - remove_background_polygon = FALSE, - background_algo = c("range"), - fill_holes = TRUE, - poly_IDs = NULL, - flip_vertical = TRUE, - shift_vertical_step = TRUE, - flip_horizontal = TRUE, - shift_horizontal_step = TRUE, - calc_centroids = FALSE, - fix_multipart = TRUE, - remove_unvalid_polygons = TRUE) { +createGiottoPolygonsFromMask <- function( + maskfile, + mask_method = c("guess", "single", "multiple"), + name = "cell", + remove_background_polygon = FALSE, + background_algo = c("range"), + fill_holes = TRUE, + poly_IDs = NULL, + ID_fmt = "cell_", + flip_vertical = TRUE, + shift_vertical_step = TRUE, + flip_horizontal = TRUE, + shift_horizontal_step = TRUE, + calc_centroids = FALSE, + remove_unvalid_polygons = TRUE, + verbose = FALSE +) { # data.table vars x <- y <- geom <- part <- NULL + remove_unvalid_polygons <- as.logical(remove_unvalid_polygons) + # select background algo background_algo <- match.arg(background_algo, choices = "range") @@ -2120,57 +2199,88 @@ createGiottoPolygonsFromMask <- function(maskfile, # create polygons from mask rast_dimensions <- dim(terra_rast) + # value = TRUE here means that the intensity value of the mask image + # (which usually encodes the intended polygon ID) is added to the resulting + # SpatVector as the only attribute. terra_polygon <- terra::as.polygons(x = terra_rast, value = TRUE) + val_col <- names(terra_polygon) # the only col should be from the values # fill holes if (isTRUE(fill_holes)) { terra_polygon <- terra::fillHoles(terra_polygon) } - # remove unvalid polygons - if (isTRUE(remove_unvalid_polygons)) { + # handle unvalid polygons ## + # The unvalid polys formed from as.polygons are usually very misshapen and + # artefacted. It is impossible to fix them using `terra::makeValid()` + if (remove_unvalid_polygons) { valid_index <- terra::is.valid(terra_polygon) terra_polygon <- terra_polygon[valid_index] } - - spatVecDT <- .spatvector_to_dt(terra_polygon) - ## flip across axes ## if (isTRUE(flip_vertical)) { - # terra_polygon = terra::flip(terra_polygon, direction = 'vertical') - spatVecDT[, y := -y] + terra_polygon <- .flip_spatvect(terra_polygon) } - if (isTRUE(flip_horizontal)) { - # terra_polygon = terra::flip(terra_polygon, direction = 'horizontal') - spatVecDT[, x := -x] + terra_polygon <- .flip_spatvect(terra_polygon) } - # guess mask method + # convert to DT format since we want to be able to compare number of geoms + # vs polys to determine correct mask method. + # TODO only test a subset of polys here? + spatVecDT <- .spatvector_to_dt(terra_polygon) + + ## guess mask method ## if (mask_method == "guess") { uniq_geoms <- length(unique(spatVecDT$geom)) uniq_parts <- length(unique(spatVecDT$part)) mask_method <- ifelse(uniq_geoms > uniq_parts, "multiple", "single") } + vmsg(.v = verbose, sprintf("parsing mask using mask_method: %s", mask_method)) + + + ## define polys and apply auto IDs ## + naming_fun <- ifelse(grepl("%", ID_fmt), sprintf, paste0) + # If poly_IDs are NOT provided, then terra_polygon IDs created here will be + # `character` and the finalized ID values. + # If poly_IDs ARE provided, the IDs are still temporary and MUST remain + # `numeric`, pending the `poly_IDs` param being applied downstream. + terra_polygon <- switch(mask_method, + "multiple" = { + names(terra_polygon) <- "poly_ID" + if (is.null(poly_IDs)) { + # spatVecDT[, geom := naming_fun(ID_fmt, geom)] + # spatVecDT[, (val_col) := naming_fun(ID_fmt, get(val_col))] + # g_polygon <- createGiottoPolygonsFromDfr( + # segmdfr = spatVecDT[, .(x, y, get(val_col))] + # ) + # g_polygon@spatVector + terra_polygon$poly_ID <- naming_fun(ID_fmt, terra_polygon$poly_ID) + } + terra_polygon + }, + "single" = { + # TODO ordering may be performed based on centroids xy instead of + # converting the full polygon and then ordering on parts + # May improve the speed + if (is.null(poly_IDs)) { + spatVecDT[, part := naming_fun(ID_fmt, part)] + } + g_polygon <- createGiottoPolygonsFromDfr( + segmdfr = spatVecDT[, .(x, y, part)] + ) + if (!is.null(poly_IDs)) { + g_polygon@spatVector$poly_ID <- as.numeric(g_polygon@spatVector$poly_ID) + } - if (mask_method == "multiple") { - if (is.null(poly_IDs)) { - spatVecDT[, geom := paste0(name, geom)] - } - g_polygon <- createGiottoPolygonsFromDfr(segmdfr = spatVecDT[, .(x, y, geom)]) - terra_polygon <- g_polygon@spatVector - } else if (mask_method == "single") { - if (is.null(poly_IDs)) { - spatVecDT[, part := paste0(name, part)] + g_polygon@spatVector } - g_polygon <- createGiottoPolygonsFromDfr(segmdfr = spatVecDT[, .(x, y, part)]) - terra_polygon <- g_polygon@spatVector - } + ) - ## shift values ## + ## apply spatial shifts ## if (identical(shift_vertical_step, TRUE)) { shift_vertical_step <- rast_dimensions[1] # nrows of raster } else if (is.numeric(shift_vertical_step)) { @@ -2193,29 +2303,35 @@ createGiottoPolygonsFromMask <- function(maskfile, ) - # remove background polygon + ## remove background polygon ## if (isTRUE(remove_background_polygon)) { if (background_algo == "range") { backgr_poly_id <- .identify_background_range_polygons(terra_polygon) - # print(backgr_poly_id) uneccessary to print? + vmsg(.v = verbose, sprintf("removed background poly.\n ID was: %s", + backgr_poly_id)) } - terra_polygon <- terra::subset(x = terra_polygon, terra_polygon[["poly_ID"]] != backgr_poly_id) + terra_polygon <- terra::subset( + x = terra_polygon, + terra_polygon[["poly_ID"]] != backgr_poly_id + ) } - # provide own cell_ID name + ## apply custom poly_IDs ## if (!is.null(poly_IDs)) { + # first sort the polys by ID to ensure that custom poly_IDs are applied in a + # meaningful manner + terra_polygon <- terra_polygon[order(terra_polygon$poly_ID)] + if (isTRUE(remove_unvalid_polygons)) { poly_IDs <- poly_IDs[valid_index] } if (length(poly_IDs) != nrow(terra::values(terra_polygon))) { - stop("length cell_IDs does not equal number of found polyongs \n") + stop("length cell_IDs does not equal number of found polygons \n") } terra_polygon$poly_ID <- as.character(poly_IDs) - } else { - terra_polygon$poly_ID <- paste0(name, "_", 1:nrow(terra::values(terra_polygon))) } @@ -2254,7 +2370,7 @@ createGiottoPolygonsFromMask <- function(maskfile, #' @param copy_dt (default TRUE) if segmdfr is provided as dt, this determines #' whether a copy is made #' @param verbose be verbose -#' @details When determining which column within the tabular data is intended to +#' @details When determining which column within tabular data is intended to #' provide polygon information, Giotto first checks the column names for 'x', 'y', #' and 'poly_ID'. If any of these are discovered, they are directly selected. If #' this is not discovered then Giotto checks the data type of the columns and selects diff --git a/R/data_input.R b/R/data_input.R index 33f6d6a5..f33fe799 100644 --- a/R/data_input.R +++ b/R/data_input.R @@ -1710,11 +1710,11 @@ readPolygonData <- function(data_list, background_algo = c("range"), fill_holes = TRUE, poly_IDs = NULL, + ID_fmt = "cell_", flip_vertical = TRUE, shift_vertical_step = TRUE, flip_horizontal = TRUE, - shift_horizontal_step = TRUE, - fix_multipart = TRUE + shift_horizontal_step = TRUE ) } diff --git a/R/methods-flip.R b/R/methods-flip.R index 5da5f3f8..81684fda 100644 --- a/R/methods-flip.R +++ b/R/methods-flip.R @@ -164,7 +164,7 @@ setMethod( ) } } else { - # flip about y0 + # flip about x0 # poly dx_p <- x0 - x_min_p gpoly@spatVector <- terra::shift( @@ -190,6 +190,42 @@ setMethod( +.flip_spatvect <- function( + x, direction = "vertical", x0 = 0, y0 = 0 + ) { + checkmate::assert_class(x, "SpatVector") + if (!is.null(x0)) { + checkmate::assert_numeric(x0) + } + if (!is.null(y0)) { + checkmate::assert_numeric(y0) + } + + # 1. perform flip + e <- terra::ext(x) + x <- terra::flip(x, direction = direction) + + x <- switch(direction, + "vertical" = { + if (!is.null(y0)) { # flip about y0 if not NULL + ymin <- as.numeric(e$ymin) + dy <- y0 - ymin + terra::shift(x, dy = 2 * dy) + } + }, + "horizontal" = { + if (!is.null(x0)) { # flip about x0 if not NULL + xmin <- as.numeric(e$xmin) + dx <- x0 - xmin + terra::shift(x, dx = 2 * dx) + } + } + ) + + # 3. return + return(x) +} + diff --git a/R/methods-rbind.R b/R/methods-rbind.R index 9477713b..f6d8afd6 100644 --- a/R/methods-rbind.R +++ b/R/methods-rbind.R @@ -81,6 +81,8 @@ rbind2_giotto_polygon_homo <- function(x, y) { } else { slot(x, "overlaps") <- rbind(slot(x, "overlaps"), slot(y, "overlaps")) } + + slot(x, "unique_ID_cache") <- unique(c(spatIDs(x), spatIDs(y))) x } @@ -159,7 +161,8 @@ rbind2_giotto_polygon_hetero <- function(x, y, new_name, add_list_ID = TRUE) { name = new_name, spatVector = new_sv, spatVectorCentroids = new_svc, - overlaps = new_ovlp + overlaps = new_ovlp, + unique_IDs = unique(c(spatIDs(x), spatIDs(y))) ) new_poly } diff --git a/inst/extdata/toy_mask_multi.tif b/inst/extdata/toy_mask_multi.tif new file mode 100644 index 00000000..da28db18 Binary files /dev/null and b/inst/extdata/toy_mask_multi.tif differ diff --git a/inst/extdata/toy_mask_single.tif b/inst/extdata/toy_mask_single.tif new file mode 100644 index 00000000..0efec8e8 Binary files /dev/null and b/inst/extdata/toy_mask_single.tif differ diff --git a/inst/extdata/toy_poly.cpg b/inst/extdata/toy_poly.cpg new file mode 100644 index 00000000..3ad133c0 --- /dev/null +++ b/inst/extdata/toy_poly.cpg @@ -0,0 +1 @@ +UTF-8 \ No newline at end of file diff --git a/inst/extdata/toy_poly.dbf b/inst/extdata/toy_poly.dbf new file mode 100644 index 00000000..7aa15a1a Binary files /dev/null and b/inst/extdata/toy_poly.dbf differ diff --git a/inst/extdata/toy_poly.shp b/inst/extdata/toy_poly.shp new file mode 100644 index 00000000..e41424ee Binary files /dev/null and b/inst/extdata/toy_poly.shp differ diff --git a/inst/extdata/toy_poly.shx b/inst/extdata/toy_poly.shx new file mode 100644 index 00000000..8749c44f Binary files /dev/null and b/inst/extdata/toy_poly.shx differ diff --git a/man/createGiottoPolygon.Rd b/man/createGiottoPolygon.Rd index b9683299..8d038cb7 100644 --- a/man/createGiottoPolygon.Rd +++ b/man/createGiottoPolygon.Rd @@ -4,8 +4,8 @@ \alias{createGiottoPolygon} \alias{createGiottoPolygon,SpatVector-method} \alias{createGiottoPolygon,SpatRaster-method} -\alias{createGiottoPolygon,data.frame-method} \alias{createGiottoPolygon,character-method} +\alias{createGiottoPolygon,data.frame-method} \alias{createGiottoPolygonsFromMask} \alias{createGiottoPolygonsFromDfr} \alias{createGiottoPolygonsFromGeoJSON} @@ -21,16 +21,18 @@ background_algo = c("range"), fill_holes = TRUE, poly_IDs = NULL, + ID_fmt = "cell_", flip_vertical = TRUE, shift_vertical_step = TRUE, flip_horizontal = TRUE, shift_horizontal_step = TRUE, - fix_multipart = TRUE, remove_unvalid_polygons = TRUE, calc_centroids = FALSE, verbose = TRUE ) +\S4method{createGiottoPolygon}{character}(x, ...) + \S4method{createGiottoPolygon}{data.frame}( x, name = "cell", @@ -40,8 +42,6 @@ verbose = TRUE ) -\S4method{createGiottoPolygon}{character}(x, ...) - createGiottoPolygonsFromMask( maskfile, mask_method = c("guess", "single", "multiple"), @@ -50,13 +50,14 @@ createGiottoPolygonsFromMask( background_algo = c("range"), fill_holes = TRUE, poly_IDs = NULL, + ID_fmt = "cell_", flip_vertical = TRUE, shift_vertical_step = TRUE, flip_horizontal = TRUE, shift_horizontal_step = TRUE, calc_centroids = FALSE, - fix_multipart = TRUE, - remove_unvalid_polygons = TRUE + remove_unvalid_polygons = TRUE, + verbose = FALSE ) createGiottoPolygonsFromDfr( @@ -85,7 +86,8 @@ data.frame with vertex 'x', 'y', and 'poly_ID' information.} \item{verbose}{be verbose} -\item{mask_method}{how the mask file defines individual segmentation annotations} +\item{mask_method}{how the mask file defines individual segmentation annotations. +See \emph{mask_method} section} \item{remove_background_polygon}{try to remove background polygon (default: FALSE)} @@ -93,7 +95,11 @@ data.frame with vertex 'x', 'y', and 'poly_ID' information.} \item{fill_holes}{fill holes within created polygons} -\item{poly_IDs}{unique names for each polygon in the mask file} +\item{poly_IDs}{character vector. Default = NULL. Custom unique names for +each polygon in the mask file.} + +\item{ID_fmt}{character. Only applied if \code{poly_IDs = NULL}. Naming scheme for +poly_IDs. Default = "cell_". See \emph{ID_fmt} section.} \item{flip_vertical}{flip mask figure in a vertical manner} @@ -103,19 +109,17 @@ data.frame with vertex 'x', 'y', and 'poly_ID' information.} \item{shift_horizontal_step}{shift horizontal (boolean or numerical)} -\item{fix_multipart}{try to split polygons with multiple parts (default: TRUE)} - \item{remove_unvalid_polygons}{remove unvalid polygons (default: TRUE)} +\item{\dots}{additional params to pass. For character method, params pass to +SpatRaster or SpatVector methods, depending on whether x was a filepath to +a maskfile or a spatial file (ex: wkt, shp, GeoJSON) respectively.} + \item{skip_eval_dfr}{(default FALSE) skip evaluation of provided dataframe} \item{copy_dt}{(default TRUE) if segmdfr is provided as dt, this determines whether a copy is made} -\item{\dots}{additional params to pass. For character method, params pass to -SpatRaster or SpatVector methods, depending on whether x was a filepath to -a maskfile or a spatial file (ex: wkt, shp, GeoJSON) respectively.} - \item{maskfile}{path to mask file} \item{segmdfr}{data.frame-like object with polygon coordinate information (x, y, poly_ID) @@ -132,10 +136,10 @@ Create a \code{giottoPolygon} object that is used to represent spatial annotations and polygons. Inputs can be from a structured data.frame object where three of the columns should correspond to x/y vertices and the polygon ID and additional columns are set as attributes, a spatial file -such as wkt, .shp, or .GeoJSON, or a mask file (e.g. segmentation results) +such as wkt, .shp, or .GeoJSON, or a mask file (e.g. segmentation results). } \details{ -When determining which column within the tabular data is intended to +When determining which column within tabular data is intended to provide polygon information, Giotto first checks the column names for 'x', 'y', and 'poly_ID'. If any of these are discovered, they are directly selected. If this is not discovered then Giotto checks the data type of the columns and selects @@ -143,5 +147,94 @@ the first \code{'character'} type column to be 'poly_ID' and the first two \code columns as 'x' and 'y' respectively. If this is also unsuccessful then poly_ID defaults to the 3rd column. 'x' and 'y' then default to the 1st and 2nd columns. } +\section{mask_method}{ + +One of "single", "multiple", or "guess". +\itemize{ +\item{\emph{"single"} assumes that the provided mask image is binary, with only +polygon vs background being distinct values. With this kind of image, the +expected generated polygons is a single multipart polygon. "single" takes +this multipart polygon and breaks it apart into individual singlepart +polygons. An initial simple \code{numeric} index as the 'nth' polygon found in +the mask image will be applied as an ID (see \emph{ID_fmt} section).} +\item{\emph{"multiple"} assumes that the provided mask image has distinct +intensity values to specify the IDs of individual polygons. An initial +\code{numeric} ID is applied as the intensity value of the pixels that made up +the annotation for that polygon in the mask image (see \emph{ID_fmt} section).} +\item{\emph{"guess"} examines the values in the image to pick the most likely +appropriate method out of "single" or "multiple".} +} +} + +\section{ID_fmt}{ + +Defaults to applying the input as a prefix (using \code{paste0()}) to the +numerical ID values detected by \code{mask_method}. (ie: \code{ID_fmt = "cell_"} +produces \code{cell_1}, \code{cell_2}, \code{cell_3}, ...)\cr +If a "\%" character is detected in the input then the input will be treated as +a \code{sprintf()} \code{fmt} param input instead. (ie: \code{ID_fmt = "cell_\%03d"} produces +\code{cell_001}, \code{cell_002}, \code{cell_003}, ...) +} + +\examples{ +# \%\%\%\%\%\%\%\%\% `createGiottoPolygon()` examples \%\%\%\%\%\%\%\%\% # +# ------- create from a mask image ------- # +m <- system.file("extdata/toy_mask_multi.tif", package = "GiottoClass") +plot(terra::rast(m), col = grDevices::hcl.colors(7)) +gp <- createGiottoPolygon( + m, + flip_vertical = FALSE, flip_horizontal = FALSE, + shift_horizontal_step = FALSE, shift_vertical_step = FALSE, + ID_fmt = "id_test_\%03d", + name = "test" +) +plot(gp, col = grDevices::hcl.colors(7)) + +# ------- create from an shp file ------- # +shp <- system.file("extdata/toy_poly.shp", package = "GiottoClass") +# vector inputs do not have params for flipping and shifting +gp2 <- createGiottoPolygon(shp, name = "test") +plot(gp2, col = grDevices::hcl.colors(7)) + + +# ------- create from data.frame-like ------- # +shp <- system.file("extdata/toy_poly.shp", package = "GiottoClass") +gpoly <- createGiottoPolygon(shp, name = "test") +plot(gpoly) +gpoly_dt <- data.table::as.data.table(gpoly, geom = "XY") +needed_cols_dt <- gpoly_dt[, .(geom, part, x, y, hole, poly_ID)] +force(needed_cols_dt) + +out <- createGiottoPolygon(needed_cols_dt, + name = "test") +plot(out) + + +# \%\%\%\%\%\%\%\%\% `createGiottoPolygonsFromMask()` examples \%\%\%\%\%\%\%\%\% # +mask_multi <- system.file("extdata/toy_mask_multi.tif", + package = "GiottoClass") +mask_single <- system.file("extdata/toy_mask_single.tif", + package = "GiottoClass") +plot(terra::rast(mask_multi), col = grDevices::hcl.colors(7)) +plot(terra::rast(mask_single)) + +gpoly1 = createGiottoPolygonsFromMask( + mask_multi, + flip_vertical = FALSE, flip_horizontal = FALSE, + shift_horizontal_step = FALSE, shift_vertical_step = FALSE, + ID_fmt = "id_test_\%03d", + name = "multi_test" +) +plot(gpoly1, col = grDevices::hcl.colors(7)) + +gpoly2 = createGiottoPolygonsFromMask( + mask_single, + flip_vertical = FALSE, flip_horizontal = FALSE, + shift_horizontal_step = FALSE, shift_vertical_step = FALSE, + ID_fmt = "id_test_\%03d", + name = "single_test" +) +plot(gpoly2, col = grDevices::hcl.colors(5)) +} \concept{mask polygon} \concept{polygon} diff --git a/tests/testthat/test-createObject.R b/tests/testthat/test-createObject.R index f6a59433..655d936a 100644 --- a/tests/testthat/test-createObject.R +++ b/tests/testthat/test-createObject.R @@ -48,10 +48,106 @@ test_that("giottoPolygon is created from data.table", { expect_setequal(gp_IDs, spatIDs(gp)) }) -# TODO need the file uploaded to do this easily -# test_that('giottoPolygon is created from maskfile', { -# gp = createGiottoPolygonsFromMask() -# }) + +test_that('giottoPolygon is created from maskfile', { + # make a faux mask (DO NOT DELETE COMMENTED CODE HERE) + # a <- circleVertices(2) + b <- data.table::data.table(sdimx = c(5, 10, 20, 10, 25, 22, 6), + sdimy = c(5, 3, 8, 10, 3, 10, 8), + cell_ID = letters[seq(7)]) + # x <- createGiottoPolygon(polyStamp(a, b))[] + # x$idx <- rev(4:10) + # r <- terra::rast(ncol = 100, nrow = 100) + # ext(r) <- c(0, 30, 0, 13) + # mask_multi <- terra::rasterize(x, r, field = "idx") + # terra::writeRaster(mask_multi, + # filename = "inst/extdata/toy_mask_multi.tif", + # gdal = "COG", + # overwrite = TRUE) + # mask_single <- terra::rasterize(x, r) + # terra::writeRaster(mask_single, + # filename = "inst/extdata/toy_mask_single.tif", + # gdal = "COG", + # overwrite = TRUE) + # terra::writeVector(x, + # filename = "inst/extdata/toy_poly.shp", + # overwrite = TRUE) + + m <- system.file("extdata/toy_mask_multi.tif", package = "GiottoClass") + s <- system.file("extdata/toy_mask_single.tif", package = "GiottoClass") + + # expect all 7 polys + gpm = createGiottoPolygonsFromMask(m, + flip_vertical = FALSE, + flip_horizontal = FALSE, + shift_horizontal_step = FALSE, + shift_vertical_step = FALSE, + ID_fmt = "id_test_%03d", + name = "multi_test", + verbose = FALSE) + expect_equal(nrow(gpm), 7) + gpm_centroids_dt <- data.table::as.data.table(centroids(gpm), geom = "XY") + expect_identical(gpm_centroids_dt$poly_ID, sprintf("id_test_%03d", 4:10)) + # compare against reversed values from spatlocs DT since values were applied + # in reverse (from idx col) + expect_identical(round(gpm_centroids_dt$x), rev(b$sdimx)) + expect_identical(round(gpm_centroids_dt$y), rev(b$sdimy)) + + # expect 5 polys + gps = createGiottoPolygonsFromMask(s, + flip_vertical = FALSE, + flip_horizontal = FALSE, + shift_horizontal_step = FALSE, + shift_vertical_step = FALSE, + ID_fmt = "id_test_%03d", + name = "single_test", + verbose = FALSE) + expect_equal(nrow(gps), 5) + gps_centroids_dt <- data.table::as.data.table(centroids(gps), geom = "XY") + expect_identical(gps_centroids_dt$poly_ID, sprintf("id_test_%03d", seq(1:5))) + # ordering from readin for "single" is ordered first by row then col + data.table::setkeyv(b, c("sdimy", "sdimx")) # note that y ordering is still inverted + singles_x <- c(b$sdimx[6], mean(b$sdimx[c(7, 5)]), mean(b$sdimx[c(3, 4)]), b$sdimx[c(1, 2)]) + singles_y <- c(b$sdimy[6], mean(b$sdimy[c(7, 5)]), mean(b$sdimy[c(3, 4)]), b$sdimy[c(1, 2)]) + + expect_identical(round(gps_centroids_dt$x, digits = 1), singles_x) + expect_identical(round(gps_centroids_dt$y, digits = 1), singles_y) + + # try again with specified poly_ID values --------------------------------- # + + gpm2 = createGiottoPolygonsFromMask(m, + flip_vertical = FALSE, + flip_horizontal = FALSE, + shift_horizontal_step = FALSE, + shift_vertical_step = FALSE, + poly_IDs = letters[1:7], + ID_fmt = "id_test_%03d", # ignored + name = "multi_test", + verbose = FALSE) + expect_identical(gpm2$poly_ID, letters[1:7]) + gpm2_centroids_dt <- data.table::as.data.table(centroids(gpm2), geom = "XY") + data.table::setkey(b, cell_ID) + expect_identical(round(gpm2_centroids_dt$x), rev(b$sdimx)) + expect_identical(round(gpm2_centroids_dt$y), rev(b$sdimy)) + + gps2 = createGiottoPolygonsFromMask(s, + flip_vertical = FALSE, + flip_horizontal = FALSE, + shift_horizontal_step = FALSE, + shift_vertical_step = FALSE, + poly_IDs = LETTERS[1:5], + ID_fmt = "id_test_%03d", # ignored + name = "single_test", + verbose = FALSE) + expect_identical(gps2$poly_ID, LETTERS[1:5]) + gps2_centroids_dt <- data.table::as.data.table(centroids(gps2), geom = "XY") + data.table::setkeyv(b, c("sdimy", "sdimx")) # note that y ordering is still inverted + singles_x <- c(b$sdimx[6], mean(b$sdimx[c(7, 5)]), mean(b$sdimx[c(3, 4)]), b$sdimx[c(1, 2)]) + singles_y <- c(b$sdimy[6], mean(b$sdimy[c(7, 5)]), mean(b$sdimy[c(3, 4)]), b$sdimy[c(1, 2)]) + + expect_identical(round(gps2_centroids_dt$x, digits = 1), singles_x) + expect_identical(round(gps2_centroids_dt$y, digits = 1), singles_y) +})