Skip to content

Commit 4651716

Browse files
committed
Merge branch 'main' into vince_geoms
2 parents 2ca7d9c + c9607a2 commit 4651716

File tree

12 files changed

+454
-37
lines changed

12 files changed

+454
-37
lines changed

.Rbuildignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
^\.github$
2+
^.*\.Rproj$
3+
^\.Rproj\.user$

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.Rproj.user
2+
.Rhistory
3+
.RData
4+
.Ruserdata
5+
*.Rproj

DESCRIPTION

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Package: SpatialData.plot
22
Title: SpatialData visualization
33
Depends: R (>= 4.4), SpatialData
4-
Version: 0.99.101
4+
Version: 0.99.2
55
Description: Visualization suit for 'SpatialData' (R). Current functionality
66
includes handling of multiscale 'images', visualizing 'labels', 'points',
77
and 'shapes'. For the latter, POINT, POLYGON, and MULTIPOLYGON geometries
@@ -41,7 +41,8 @@ Imports:
4141
rlang,
4242
sf,
4343
S4Vectors,
44-
SingleCellExperiment
44+
SingleCellExperiment,
45+
Rarr
4546
Suggests:
4647
BiocStyle,
4748
ggnewscale,

NAMESPACE

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,12 @@ exportMethods(scale)
1313
exportMethods(translation)
1414
import(SpatialData)
1515
importFrom(DelayedArray,realize)
16+
importFrom(Rarr,zarr_overview)
1617
importFrom(S4Vectors,metadata)
1718
importFrom(SingleCellExperiment,int_colData)
1819
importFrom(SingleCellExperiment,int_metadata)
20+
importFrom(SpatialData,channels)
21+
importFrom(SpatialData,getZarrArrayPath)
1922
importFrom(dplyr,mutate)
2023
importFrom(dplyr,select)
2124
importFrom(ggforce,geom_circle)

R/plotImage.R

Lines changed: 146 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@
1010
#' @param k index of the scale of an image; by default (NULL), will auto-select
1111
#' scale in order to minimize memory-usage and blurring for a target size of
1212
#' 800 x 800px; use Inf to plot the lowest resolution available.
13+
#' @param ch image channel(s) to be used for plotting (defaults to
14+
#' the first channel(s) available); use \code{channels()} to see
15+
#' which channels are available for a given \code{ImageArray}
16+
#' @param c character vector; colors to use for each channel.
17+
#' @param cl list of length-2 numeric vectors (non-negative, increasing);
18+
#' specifies channel-wise contrast limits - defaults to [0, 1] for all
19+
#' (ignored when \code{image(x, i)} is an RGB image;
20+
#' for convenience, any NULL = [0, 1], and n = [0, n]).
1321
#'
1422
#' @return ggplot
1523
#'
@@ -23,6 +31,14 @@
2331
#' plotImage(x, i=2, k=.))
2432
#' patchwork::wrap_plots(ms)
2533
#'
34+
#' # custom colors
35+
#' cmy <- c("cyan", "magenta", "yellow")
36+
#' plotSpatialData() + plotImage(x, c=cmy)
37+
#'
38+
#' # contrast limits
39+
#' plotSpatialData() + plotImage(x, c=cmy,
40+
#' cl=list(c(0.2,1), c(0,0.8), c(0,1)))
41+
#'
2642
#' @import SpatialData
2743
NULL
2844

@@ -31,6 +47,118 @@ NULL
3147
#' @export
3248
plotSpatialData <- \() ggplot() + scale_y_reverse() + .theme
3349

50+
.check_cl <- \(cl, d) {
51+
if (is.null(cl)) {
52+
# default to [0, 1] for all channels
53+
cl <- replicate(d, c(0, 1), FALSE)
54+
} else {
55+
# should be a list with as many elements as channels
56+
if (!is.list(cl)) stop("'cl' should be a list")
57+
if (length(cl) != d) stop("'cl' should be of length ", d)
58+
for (. in seq_len(d)) {
59+
# replace NULL by [0, 1] & n by [0, n]
60+
if (is.null(cl[[.]])) cl[[.]] <- c(0, 1)
61+
if (length(cl[[.]]) == 1) {
62+
if (cl[[.]] < 0) stop("scalar 'cl' can't be < 0")
63+
cl[[.]] <- c(0, cl[[.]])
64+
}
65+
}
66+
# elements should be length-2, numeric, non-negative, increasing
67+
.f <- \(.) length(.) == 2 && is.numeric(.) && all(. >= 0) && .[2] > .[1]
68+
if (!all(vapply(cl, .f, logical(1))))
69+
stop("elements of 'cl' should be length-2,",
70+
" non-negative, increasing numeric vectors")
71+
}
72+
return(cl)
73+
}
74+
75+
# merge/manage image channels
76+
# if no colors and channels defined, return the first channel
77+
#' @importFrom grDevices col2rgb
78+
#' @noRd
79+
.chs2rgb <- \(a, ch, c=NULL, cl=NULL) {
80+
cl <- .check_cl(cl, d <- dim(a)[1])
81+
if (length(ch) > (n <- length(.DEFAULT_COLORS)) && is.null(c))
82+
stop("Only ", n, " default colors available, but",
83+
length(ch), " are needed; please specify 'c'")
84+
if (!is.null(c) || (is.null(c) && length(ch) > 1)) {
85+
if (is.null(c)) c <- .DEFAULT_COLORS[seq_along(ch)]
86+
c <- col2rgb(c)/255
87+
b <- array(0, dim=c(3, dim(a)[-1]))
88+
for (i in seq_len(d)) {
89+
for (j in seq_len(3)) {
90+
rgb <- a[i,,,drop=FALSE]*c[j,i]
91+
# apply upper contrast lim.
92+
rgb <- rgb*(1/cl[[i]][2])
93+
b[j,,] <- b[j,,,drop=FALSE] + rgb
94+
# apply lower contrast lim.
95+
b[j,,][b[j,,] < cl[[i]][1]] <- 0
96+
}
97+
}
98+
a <- pmin(b, 1)
99+
} else {
100+
a <- a[rep(1, 3), , ]
101+
}
102+
return(a)
103+
}
104+
105+
# check if an image is rgb or not
106+
#' @importFrom SpatialData getZarrArrayPath
107+
#' @importFrom Rarr zarr_overview
108+
#' @noRd
109+
.get_img_dt <- \(a) {
110+
pa <- getZarrArrayPath(a)
111+
df <- zarr_overview(pa, as_data_frame=TRUE)
112+
if (!is.null(dt <- df$data_type)) return(dt)
113+
}
114+
115+
# normalize the image data given its data type
116+
#' @noRd
117+
.norm_ia <- \(a, dt) {
118+
d <- dim(a)[1]
119+
if (dt %in% names(.DTYPE_MAX_VALUES)) {
120+
a <- a / .DTYPE_MAX_VALUES[dt]
121+
} else if (max(a) > 1) {
122+
for (i in seq_len(d))
123+
a[i,,] <- a[i,,] / max(a[i,,])
124+
}
125+
return(a)
126+
}
127+
128+
# check if an image is RGB or not
129+
# (NOTE: some RGB channels are named 0, 1, 2)
130+
#' @importFrom methods is
131+
#' @noRd
132+
.is_rgb <- \(x) {
133+
if (is(x, "ImageArray") &&
134+
!is.null(md <- meta(x)))
135+
x <- md$omero$channels$label
136+
if (!is.vector(x)) stop("invalid 'x'")
137+
is_len <- length(x) == 3
138+
is_012 <- setequal(x, seq(0, 2))
139+
is_rgb <- setequal(x, c("r", "g", "b"))
140+
return(is_len && (is_012 || is_rgb))
141+
}
142+
143+
# check if channels are indices or channel names
144+
#' @importFrom SpatialData channels
145+
#' @noRd
146+
.ch_idx <- \(x, ch) {
147+
if (is.null(ch))
148+
return(1)
149+
lbs <- channels(x)
150+
if (all(ch %in% lbs)) {
151+
return(match(ch, lbs))
152+
} else if (!any(ch %in% lbs)) {
153+
warning("Couldn't find some channels; picking first one(s)!")
154+
return(1)
155+
} else {
156+
warning("Couldn't find channels; picking first one(s)!")
157+
return(1)
158+
}
159+
return(NULL)
160+
}
161+
34162
.guess_scale <- \(x, w, h) {
35163
n <- length(dim(x))
36164
i <- ifelse(n == 3, -1, TRUE)
@@ -39,22 +167,27 @@ plotSpatialData <- \() ggplot() + scale_y_reverse() + .theme
39167
which.min(d)
40168
}
41169

42-
.get_plot_data <- \(x, k=NULL, w=800, h=800) {
170+
.get_img_data <- \(x, k=NULL, w=800, h=800) {
43171
if (!is.null(k)) return(data(x, k))
44172
data(x, .guess_scale(x, w, h))
45173
}
46174

47175
#' @importFrom methods as
48176
#' @importFrom grDevices rgb
49177
#' @importFrom DelayedArray realize
50-
.df_i <- \(x, k=NULL) {
51-
a <- .get_plot_data(x, k)
52-
a <- if (dim(a)[1] == 1) a[rep(1,3),,] else a
53-
a <- realize(as(a, "DelayedArray"))
54-
img <- rgb(
55-
maxColorValue=max(a),
56-
c(a[1,,]), c(a[2,,]), c(a[3,,]))
57-
array(img, dim(a)[-1])
178+
.df_i <- \(x, k=NULL, ch=NULL, c=NULL, cl=NULL) {
179+
a <- .get_img_data(x, k)
180+
ch <- .ch_idx(x, ch)
181+
if (!.is_rgb(x))
182+
a <- a[ch, , , drop=FALSE]
183+
dt <- .get_img_dt(a)
184+
a <- as(a, "DelayedArray")
185+
a <- .norm_ia(realize(a), dt)
186+
# enter when image isn't RGB already, either
187+
# custom colors or contrasts are specified
188+
if (!.is_rgb(x) || !is.null(c) || !is.null(cl))
189+
a <- .chs2rgb(a, ch, c, cl)
190+
apply(a, c(2, 3), \(.) do.call(rgb, as.list(.)))
58191
}
59192

60193
.get_wh <- \(x, i, j) {
@@ -75,13 +208,13 @@ plotSpatialData <- \() ggplot() + scale_y_reverse() + .theme
75208

76209
#' @rdname plotImage
77210
#' @export
78-
setMethod("plotImage", "SpatialData", \(x, i=1, j=1, k=NULL) {
79-
if (is.numeric(i))
211+
setMethod("plotImage", "SpatialData", \(x, i=1, j=1, k=NULL, ch=NULL, c=NULL, cl=NULL) {
212+
if (is.numeric(i))
80213
i <- imageNames(x)[i]
81214
y <- image(x, i)
82-
if (is.numeric(j))
215+
if (is.numeric(j))
83216
j <- CTname(y)[j]
84-
df <- .df_i(y, k)
217+
df <- .df_i(y, k, ch, c, cl)
85218
wh <- .get_wh(x, i, j)
86219
.gg_i(df, wh$w, wh$h)
87220
})

R/utils.R

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,15 @@
3333
plot.title=element_text(hjust=0.5),
3434
axis.text=element_text(color="grey"),
3535
axis.ticks=element_line(color="grey"))
36-
)
36+
)
37+
38+
# default colors (from ImageJ/Fiji)
39+
.DEFAULT_COLORS <- c("red", "green", "blue", "gray", "cyan", "magenta", "yellow")
40+
41+
# image data type factors (max values)
42+
# TODO: add more cases from other data types
43+
# https://doc.embedded-wizard.de/uint-type
44+
.DTYPE_MAX_VALUES <- c("uint8" = 255,
45+
"uint16" = 65535,
46+
"uint32" = 4294967295,
47+
"uint64" = 2^64 - 1)

inst/NEWS

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
changes in version 0.99.2
2+
3+
- in 'plotImage', added support to visualize channels of choice,
4+
as well as 'cl' argument to control constrast limits
5+
- updated vignette to include corresponding examples
6+
17
changes in version 0.99.1
28

39
- various fixes related to moving 'instance/region_key' to 'int_colData'

man/plotImage.Rd

Lines changed: 20 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/test-plotArray.R

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ test_that(".guess_scale", {
2222
dim <- lapply(c(6, 3), \(.) c(3, rep(., 2))), \(.)
2323
array(sample(seq_len(255), prod(.), replace=TRUE), dim=.)))
2424
# manual scale
25-
expect_identical(.get_plot_data(img, k=1), lys[[1]])
26-
expect_identical(.get_plot_data(img, k=2), lys[[2]])
25+
expect_identical(.get_img_data(img, k=1), lys[[1]])
26+
expect_identical(.get_img_data(img, k=2), lys[[2]])
2727
# automatic scale
28-
expect_identical(.get_plot_data(img, k=NULL, w=5, h=7), lys[[1]])
29-
expect_identical(.get_plot_data(img, k=NULL, w=2, h=2), lys[[2]])
28+
expect_identical(.get_img_data(img, k=NULL, w=5, h=7), lys[[1]])
29+
expect_identical(.get_img_data(img, k=NULL, w=2, h=2), lys[[2]])
3030
})
3131

3232
test_that("plotImage()", {

0 commit comments

Comments
 (0)