|
| 1 | +#' @name sd_plot_image |
| 2 | +#' @title Plot `ImageArray` |
| 3 | +#' |
| 4 | +#' @examples |
| 5 | +#' pa <- file.path("extdata", "blobs.zarr") |
| 6 | +#' pa <- system.file(pa, package="SpatialData") |
| 7 | +#' sd <- readSpatialData(pa) |
| 8 | +#' |
| 9 | +#' sd_plot() + sd_plot_image(sd) |
| 10 | +#' |
| 11 | +#' # custom colors |
| 12 | +#' pal <- c("cyan", "magenta", "gold") |
| 13 | +#' sd_plot() + sd_plot_image(sd, c=pal) |
| 14 | +#' |
| 15 | +#' @importFrom methods as |
| 16 | +#' @importFrom grDevices rgb |
| 17 | +#' @importFrom DelayedArray realize |
| 18 | +#' @export |
| 19 | +sd_plot_image <- \(x, i=1, k=NULL, c=NULL, ch=NULL, cl=NULL, w=800, h=800) { |
| 20 | + #x <- sd; i <- 1; w=h=800; ch <- cl <- c <- NULL; ch <- 2 |
| 21 | + ia <- x@images[[i]] |
| 22 | + ch <- .ch_idx(ia, ch) |
| 23 | + if (is.null(k)) |
| 24 | + k <- .guess_scale(ia, w, h) |
| 25 | + a <- data(ia, k) |
| 26 | + if (!.is_rgb(ia)) |
| 27 | + a <- a[ch, , , drop=FALSE] |
| 28 | + dt <- .get_dt(a) |
| 29 | + a <- as(a, "DelayedArray") |
| 30 | + a <- .norm_ia(realize(a), dt) |
| 31 | + # enter when image isn't RGB already, either |
| 32 | + # custom colors or contrasts are specified |
| 33 | + if (!.is_rgb(ia) || !is.null(c) || !is.null(cl)) |
| 34 | + a <- .chs2rgb(a, ch, c, cl) |
| 35 | + a <- apply(a, c(2, 3), \(.) do.call(rgb, as.list(.))) |
| 36 | + w <- c(0, dim(ia)[3]) |
| 37 | + h <- c(0, dim(ia)[2]) |
| 38 | + pal <- if (!.is_rgb(ia) && dim(ia)[1] > 1) { |
| 39 | + nms <- channels(ia)[ch] |
| 40 | + pal <- if (is.null(c)) .DEFAULT_COLORS else c |
| 41 | + setNames(pal[seq_along(ch)], nms) |
| 42 | + } |
| 43 | + lgd <- if (!is.null(pal)) list( |
| 44 | + guides(col=guide_legend(override.aes=list(alpha=1, size=2))), |
| 45 | + scale_color_identity(NULL, guide="legend", labels=names(pal)), |
| 46 | + geom_point(aes(col=.data$foo), data.frame(foo=pal), x=0, y=0, alpha=0)) |
| 47 | + list(lgd, |
| 48 | + scale_x_continuous(limits=w), scale_y_reverse(limits=rev(h)), |
| 49 | + annotation_raster(a, w[2],w[1], h[1],h[2], interpolate=FALSE)) |
| 50 | +} |
| 51 | + |
| 52 | +# default colors (from ImageJ/Fiji) |
| 53 | +.DEFAULT_COLORS <- c("red", "green", "blue", "gray", "cyan", "magenta", "yellow") |
| 54 | + |
| 55 | +# image data type factors (max values) |
| 56 | +# TODO: add more cases from other data types |
| 57 | +# https://doc.embedded-wizard.de/uint-type |
| 58 | +.DTYPE_MAX_VALUES <- c( |
| 59 | + "uint8"=2**8-1, "uint16"=2**16-1, |
| 60 | + "uint32"=2**32-1, "uint64"=2**64-1) |
| 61 | + |
| 62 | +.ch_idx <- \(x, ch) { |
| 63 | + if (is.null(ch)) |
| 64 | + return(1) |
| 65 | + lbs <- channels(x) |
| 66 | + if (is.integer(ch)) { |
| 67 | + return(lbs[ch]) |
| 68 | + } else if (all(ch %in% lbs)) { |
| 69 | + return(match(ch, lbs)) |
| 70 | + } else if (!any(ch %in% lbs)) { |
| 71 | + warning("Couldn't find some channels; picking first one(s)!") |
| 72 | + return(1) |
| 73 | + } else { |
| 74 | + warning("Couldn't find channels; picking first one(s)!") |
| 75 | + return(1) |
| 76 | + } |
| 77 | + return(NULL) |
| 78 | +} |
| 79 | + |
| 80 | +.check_cl <- \(cl, d) { |
| 81 | + if (is.null(cl)) { |
| 82 | + # default to [0, 1] for all channels |
| 83 | + cl <- replicate(d, c(0, 1), FALSE) |
| 84 | + } else { |
| 85 | + # should be a list with as many elements as channels |
| 86 | + if (!is.list(cl)) stop("'cl' should be a list") |
| 87 | + if (length(cl) != d) stop("'cl' should be of length ", d) |
| 88 | + for (. in seq_len(d)) { |
| 89 | + # replace NULL by [0, 1] & n by [0, n] |
| 90 | + if (is.null(cl[[.]])) cl[[.]] <- c(0, 1) |
| 91 | + if (length(cl[[.]]) == 1) { |
| 92 | + if (cl[[.]] < 0) stop("scalar 'cl' can't be < 0") |
| 93 | + cl[[.]] <- c(0, cl[[.]]) |
| 94 | + } |
| 95 | + } |
| 96 | + # elements should be length-2, numeric, non-negative, increasing |
| 97 | + .f <- \(.) length(.) == 2 && is.numeric(.) && all(. >= 0) && .[2] > .[1] |
| 98 | + if (!all(vapply(cl, .f, logical(1)))) |
| 99 | + stop("elements of 'cl' should be length-2,", |
| 100 | + " non-negative, increasing numeric vectors") |
| 101 | + } |
| 102 | + return(cl) |
| 103 | +} |
| 104 | + |
| 105 | +# merge/manage image channels; |
| 106 | +# if no colors and channels defined, |
| 107 | +# return the first channel |
| 108 | +#' @importFrom grDevices col2rgb |
| 109 | +.chs2rgb <- \(a, ch, c=NULL, cl=NULL) { |
| 110 | + cl <- .check_cl(cl, d <- dim(a)[1]) |
| 111 | + if (length(ch) > (n <- length(.DEFAULT_COLORS)) && is.null(c)) |
| 112 | + stop("Only ", n, " default colors available, but", |
| 113 | + length(ch), " are needed; please specify 'c'") |
| 114 | + if (!is.null(c) || (is.null(c) && length(ch) > 1)) { |
| 115 | + if (is.null(c)) c <- .DEFAULT_COLORS[seq_along(ch)] |
| 116 | + c <- col2rgb(c)/255 |
| 117 | + b <- array(0, dim=c(3, dim(a)[-1])) |
| 118 | + for (i in seq_len(d)) { |
| 119 | + for (j in seq_len(3)) { |
| 120 | + rgb <- a[i,,,drop=FALSE]*c[j,i] |
| 121 | + # apply upper contrast lim. |
| 122 | + rgb <- rgb*(1/cl[[i]][2]) |
| 123 | + b[j,,] <- b[j,,,drop=FALSE] + rgb |
| 124 | + # apply lower contrast lim. |
| 125 | + b[j,,][b[j,,] < cl[[i]][1]] <- 0 |
| 126 | + } |
| 127 | + } |
| 128 | + a <- pmin(b, 1) |
| 129 | + } else { |
| 130 | + a <- a[rep(1, 3), , ] |
| 131 | + } |
| 132 | + return(a) |
| 133 | +} |
| 134 | + |
| 135 | +.guess_scale <- \(x, w, h) { |
| 136 | + n <- length(dim(x)) |
| 137 | + i <- ifelse(n == 3, -1, TRUE) |
| 138 | + d <- vapply(x@data, dim, numeric(n)) |
| 139 | + d <- apply(d, 2, \(.) sum(abs(.[i]-c(h, w)))) |
| 140 | + which.min(d) |
| 141 | +} |
| 142 | + |
| 143 | +# check if an image is rgb or not |
| 144 | +#' @importFrom Rarr zarr_overview |
| 145 | +#' @noRd |
| 146 | +.get_dt <- \(a) { |
| 147 | + pa <- a@seed@zarr_array_path |
| 148 | + df <- zarr_overview(pa, as_data_frame=TRUE) |
| 149 | + if (!is.null(dt <- df$data_type)) return(dt) |
| 150 | +} |
| 151 | + |
| 152 | +.norm_ia <- \(a, dt) { |
| 153 | + d <- dim(a)[1] |
| 154 | + if (dt %in% names(.DTYPE_MAX_VALUES)) { |
| 155 | + a <- a / .DTYPE_MAX_VALUES[dt] |
| 156 | + } else if (max(a) > 1) { |
| 157 | + for (i in seq_len(d)) |
| 158 | + a[i,,] <- a[i,,] / max(a[i,,]) |
| 159 | + } |
| 160 | + return(a) |
| 161 | +} |
| 162 | + |
| 163 | +# check if an image is RGB or not |
| 164 | +# (NOTE: some RGB channels are named 0, 1, 2) |
| 165 | +#' @importFrom S7 S7_inherits |
| 166 | +.is_rgb <- \(x) { |
| 167 | + stopifnot("invalid 'x'"=S7_inherits(x, ImageArray) && !is.null(zattrs(x))) |
| 168 | + x <- channels(x) |
| 169 | + is_len <- length(x) == 3 |
| 170 | + is_012 <- setequal(x, seq(0, 2)) |
| 171 | + is_rgb <- setequal(x, c("r", "g", "b")) |
| 172 | + return(is_len && (is_012 || is_rgb)) |
| 173 | +} |
0 commit comments