Skip to content

Commit 074c9c6

Browse files
authored
Merge pull request #4 from HelenaLC/multichannel
PR #2
2 parents 2151f9d + daf678a commit 074c9c6

File tree

11 files changed

+275
-29
lines changed

11 files changed

+275
-29
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.1
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
@@ -10,9 +10,12 @@ exportMethods(scale)
1010
exportMethods(translation)
1111
import(SpatialData)
1212
importFrom(DelayedArray,realize)
13+
importFrom(Rarr,zarr_overview)
1314
importFrom(S4Vectors,metadata)
1415
importFrom(SingleCellExperiment,int_colData)
1516
importFrom(SingleCellExperiment,int_metadata)
17+
importFrom(SpatialData,channels)
18+
importFrom(SpatialData,getZarrArrayPath)
1619
importFrom(dplyr,mutate)
1720
importFrom(dplyr,select)
1821
importFrom(ggforce,geom_circle)

R/plotImage.R

Lines changed: 98 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
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+
#'
17+
#' @param c plotting aesthetics; color
1318
#'
1419
#' @return ggplot
1520
#'
@@ -31,6 +36,86 @@ NULL
3136
#' @export
3237
plotSpatialData <- \() ggplot() + scale_y_reverse() + .theme
3338

39+
# merge/manage image channels
40+
# if no colors and channels defined, return the first channel
41+
#' @importFrom grDevices col2rgb
42+
#' @noRd
43+
.manage_channels <- \(a, ch, c=NULL){
44+
if (length(ch) > (n <- length(.DEFAULT_COLORS)) && is.null(c))
45+
stop("Only ", n, " default colors available, but",
46+
length(ch), " are needed; please specify 'c'")
47+
if (!is.null(c) || (is.null(c) && length(ch) > 1)) {
48+
if (is.null(c)) c <- .DEFAULT_COLORS[seq_along(ch)]
49+
c <- col2rgb(c)/255
50+
b <- array(0, dim=c(3, dim(a)[-1]))
51+
for (i in seq_len(dim(a)[1])) {
52+
b[1,,] <- b[1,,,drop=FALSE] + a[i,,,drop=FALSE]*c[1,i]
53+
b[2,,] <- b[2,,,drop=FALSE] + a[i,,,drop=FALSE]*c[2,i]
54+
b[3,,] <- b[3,,,drop=FALSE] + a[i,,,drop=FALSE]*c[3,i]
55+
}
56+
a <- pmin(b, 1)
57+
} else {
58+
a <- a[rep(1, 3), , ]
59+
}
60+
return(a)
61+
}
62+
63+
# check if an image is rgb or not
64+
#' @importFrom SpatialData getZarrArrayPath
65+
#' @importFrom Rarr zarr_overview
66+
#' @noRd
67+
.get_image_dtype <- \(a) {
68+
pa <- getZarrArrayPath(a)
69+
df <- zarr_overview(pa, as_data_frame=TRUE)
70+
if (!is.null(dt <- df$data_type)) return(dt)
71+
}
72+
73+
# normalize the image data given its data type
74+
#' @noRd
75+
.normalize_image_array <- \(a, dt){
76+
if (dt %in% names(.DTYPE_MAX_VALUES)) {
77+
a <- a/.DTYPE_MAX_VALUES[dt]
78+
} else if (max(a) > 1) {
79+
for (i in seq_len(dim(a)[1]))
80+
a[i,,] <- a[i,,]/max(a[i,,])
81+
}
82+
return(a)
83+
}
84+
85+
# check if an image is RGB or not
86+
# (NOTE: some RGB channels are named 0, 1, 2)
87+
#' @importFrom methods is
88+
#' @noRd
89+
.is_rgb <- \(x) {
90+
if (is(x, "ImageArray") &&
91+
!is.null(md <- meta(x)))
92+
x <- md$omero$channels$label
93+
if (!is.vector(x)) stop("invalid 'x'")
94+
is_len <- length(x) == 3
95+
is_012 <- setequal(x, seq(0, 2))
96+
is_rgb <- setequal(x, c("r", "g", "b"))
97+
return(is_len && (is_012 || is_rgb))
98+
}
99+
100+
# check if channels are indices or channel names
101+
#' @importFrom SpatialData channels
102+
#' @noRd
103+
.ch_idx <- \(x, ch) {
104+
if (is.null(ch))
105+
return(1)
106+
lbs <- channels(x)
107+
if (all(ch %in% lbs)) {
108+
return(match(ch, lbs))
109+
} else if (!any(ch %in% lbs)) {
110+
warning("Couldn't find some channels; picking first one(s)!")
111+
return(1)
112+
} else {
113+
warning("Couldn't find channels; picking first one(s)!")
114+
return(1)
115+
}
116+
return(NULL)
117+
}
118+
34119
.guess_scale <- \(x, w, h) {
35120
n <- length(dim(x))
36121
i <- ifelse(n == 3, -1, TRUE)
@@ -47,14 +132,17 @@ plotSpatialData <- \() ggplot() + scale_y_reverse() + .theme
47132
#' @importFrom methods as
48133
#' @importFrom grDevices rgb
49134
#' @importFrom DelayedArray realize
50-
.df_i <- \(x, k=NULL) {
135+
.df_i <- \(x, k=NULL, ch=NULL, c=NULL) {
51136
a <- .get_plot_data(x, k)
52-
a <- if (dim(a)[1] == 1) a[rep(1,3),,] else a
137+
ch_i <- .ch_idx(x, ch)
138+
if (!.is_rgb(x))
139+
a <- a[ch_i, , , drop=FALSE]
140+
dt <- .get_image_dtype(a)
53141
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])
142+
a <- .normalize_image_array(a, dt)
143+
if (!.is_rgb(x))
144+
a <- .manage_channels(a, ch_i, c)
145+
apply(a, c(2, 3), \(.) do.call(rgb, as.list(.)))
58146
}
59147

60148
.get_wh <- \(x, i, j) {
@@ -75,13 +163,13 @@ plotSpatialData <- \() ggplot() + scale_y_reverse() + .theme
75163

76164
#' @rdname plotImage
77165
#' @export
78-
setMethod("plotImage", "SpatialData", \(x, i=1, j=1, k=NULL) {
79-
if (is.numeric(i))
166+
setMethod("plotImage", "SpatialData", \(x, i=1, j=1, k=NULL, ch=NULL, c=NULL) {
167+
if (is.numeric(i))
80168
i <- imageNames(x)[i]
81169
y <- image(x, i)
82-
if (is.numeric(j))
170+
if (is.numeric(j))
83171
j <- CTname(y)[j]
84-
df <- .df_i(y, k)
172+
df <- .df_i(y, k, ch, c)
85173
wh <- .get_wh(x, i, j)
86174
.gg_i(df, wh$w, wh$h)
87175
})

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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
changes in version 0.99.2
2+
3+
- in 'plotImage', added support to visualize channels of choice
4+
- updated vignette to include corresponding examples
5+
16
changes in version 0.99.1
27

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

man/plotImage.Rd

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

tests/testthat/test-plotImage.R

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
require(SpatialData, quietly=TRUE)
2+
x <- file.path("extdata", "blobs.zarr")
3+
x <- system.file(x, package="SpatialData")
4+
x <- readSpatialData(x, tables=FALSE)
5+
6+
test_that(".is_rgb()", {
7+
# valid integer vector
8+
expect_false(.is_rgb(c(0, 1, 1)))
9+
expect_true(.is_rgb(. <- seq(0, 2)))
10+
expect_true(.is_rgb(rev(.)))
11+
# valid character vector
12+
expect_false(.is_rgb(c("r", "g", "g")))
13+
expect_true(.is_rgb(. <- c("r", "g", "b")))
14+
expect_true(.is_rgb(rev(.)))
15+
# only works for 'ImageArray'
16+
expect_true(.is_rgb(image(x, 1)))
17+
expect_error(.is_rgb(label(x, 1)))
18+
})
19+
20+
test_that(".ch_idx()", {
21+
# get indices of channels
22+
expect_equal(.ch_idx(image(x,1), ch=c(2,0,1)), c(3,1,2))
23+
# return first if no matching channel
24+
expect_warning(expect_equal(.ch_idx(image(x,1), ch=99), 1))
25+
})
26+
27+
# TODO: any tests for image array normalization ?
28+
test_that(".normalize_image_array", {
29+
skip()
30+
})

vignettes/SpatialData.plot.Rmd

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,53 @@ wrap_plots(nrow=1, lapply(seq(3), \(.)
238238
plot_layout(guides="collect")
239239
```
240240

241+
## CyCIF (MCMICRO)
242+
243+
Small lung adenocarcinoma, 250 MB; 1 image, 2 labels, 2 tables.
244+
245+
```{r mcmicro-read}
246+
dir.create(td <- tempfile())
247+
pa <- unzip_spd_demo(
248+
zipname="mcmicro_io.zip",
249+
dest=td, source="biocOSN")
250+
(x <- readSpatialData(pa, anndataR=FALSE))
251+
```
252+
253+
Getting channel names for the image:
254+
255+
```{r mcmicro-channels}
256+
channels(image(x))
257+
```
258+
259+
Plotting with multiple image channels:
260+
261+
```{r mcmicro-plot}
262+
plotSpatialData() + plotImage(x,
263+
ch=c("DNA_6", "CD45", "CD57"),
264+
c=c("blue", "cyan", "yellow"))
265+
```
266+
267+
## IMC (Steinbock)
268+
269+
4 different cancers (SCCHN, BCC, NSCLC, CRC), 820 MB; 14 images, 14 labels, 1 table.
270+
271+
```{r steinbock-read}
272+
dir.create(td <- tempfile())
273+
pa <- unzip_spd_demo(
274+
zipname="steinbock_io.zip",
275+
dest=td, source="biocOSN")
276+
x <- readSpatialData(pa, anndataR=FALSE)
277+
```
278+
279+
Plotting with multiple image channels.
280+
281+
```{r steinbock-plot}
282+
plotSpatialData() + plotImage(x,
283+
i="Patient3_003_image",
284+
ch=c(6, 22, 39),
285+
c=c("blue", "cyan", "yellow"))
286+
```
287+
241288
# Masking
242289

243290
Back to blobs...

0 commit comments

Comments
 (0)