ImageArrayThe ImageArray package provides a unified, memory-efficient framework for working with both pyramidal and non-pyramidal images in R by leveraging the DelayedArray package. With ImageArray you can store large images on disk (as HDF5 or Zarr), treat them as array-like objects, perform delayed/lazy operations (e.g., rotate, flip, crop) across all pyramid levels, and seamlessly integrate with other R package and workflows that use large bioimages.
You can install ImageArray from Bioconductor with:
Image pyramids are multi-resolution representations that start with a full resolution image followed by a series of down-sampled levels (e.g., half resolutions). These are common in microscopy, digital pathology and large-scale imaging because:
In our context, an ImageArray object typically contain multiple levels (each a resolution level) internally, and most operations are applied lazily (or delayed) across all levels.
library(ImageArray)
library(BiocFileCache)
library(RBioFormats)
library(EBImage)
library(magick)
library(ggplot2)
library(shiny)We primarily use images via the EBImage package.
# make random EBImage image
f <- system.file("images", "sample.png", package = "EBImage")
# read image with EBImage
img <- readImage(f)Similar to its counterparts (see writeHDF5Array in
HDF5Array),
we use the writeImageArray function to write pyramid images
to preferred locations. Here, n.levels control the amount
of levels that the pyramid image should have.
output_h5 <- tempfile(fileext = ".h5")
imgarray <- writeImageArray(img, output = output_h5, n.levels = 2)
imgarray## ImageArray Object (x,y)
## Scales (2): (768,512) (384,256)
Each level of a pyramid can be rasterized at any time, and thus plotted.
However, the true functionality of ImageArray is to pick pyramid levels in a memory-efficient manner. Packages, applications and workflows that use ImageArray can rasterize images by defining thresholds for pixel dimensions across all levels.
Here, by using the max.pixel.size, we request ImageArray
to return a pyramid level whose both width (X) and height
(Y) are lower than, for example, 400. This approach is
particularly useful when visualizing:
with minimal memory footprint.
## [1] 256 384
Operations such as rotate, flip, flop or negate will be conducted on all levels (without loading the image in the memory) which then can be rasterized and plotted.
## ImageArray Object (x,y)
## Scales (2): (512,768) (256,384)
You can even crop images, either by using crop or
[ methods.
## ImageArray Object (x,y)
## Scales (2): (201,101) (101,51)
# cropping with indices
imgarray_cropped <- crop(imgarray, index = list(300:500, 200:300))
imgarray_cropped## ImageArray Object (x,y)
## Scales (2): (201,101) (101,51)
Cropping can also be performed with named indices, thus allowing
users to define indices for only the target dimensions. For now, ImageArray
only includes x and y dimensions and ‘c’ for
image channels.
# cropping with named indices
imgarray_cropped <- crop(imgarray, index = list(x=300:500, y=200:300))
imgarray_cropped## ImageArray Object (x,y)
## Scales (2): (201,101) (101,51)
The crop function serves as a more flexible alternative
to the traditional subsetting method [. The named list of
index can be of any order while [ requires the
order matching with the order of axis (as revealed in the show method,
i.e. (x,y)).
# index in yx order, results in same slice as xy order
imgarray_cropped <- crop(imgarray, index = list(y=200:300, x=300:500))
imgarray_cropped## ImageArray Object (x,y)
## Scales (2): (201,101) (101,51)
# subsetting with `[` provides a different slice
imgarray_cropped <- imgarray[200:300, 300:500]
imgarray_cropped## ImageArray Object (x,y)
## Scales (2): (101,201) (51,101)
ImageArray is also compatible with the magick package where we retain the dimensionality of the image similar to EBImage.
# make random EBImage image
f <- system.file("images", "sample.png", package = "EBImage")
# read image with magick
img <- image_read(f)
# create image array
output_h5 <- tempfile(fileext = ".h5")
imgarray <- writeImageArray(img, output = output_h5)
imgarray## ImageArray Object (c,x,y)
## Scales (2): (3,768,512) (3,384,256)
You can create ImageArray objects from OME-TIFF files
(or any file compatible with Bio-formats, see RBioFormats)
with already defined layers.
ome.tiff.file <- system.file("extdata", "xy_12bit__plant.ome.tiff",
package = "ImageArray")
read.metadata(ome.tiff.file)## ImageMetadata list of length 2
##
## series res sizeX sizeY sizeC sizeZ sizeT total
## 1 1 512 512 1 1 1 1
## 1 2 256 256 1 1 1 1
##
## globalMetadata:List of 1058
## $ xy_12bit__plant.oir inTrigger triggerNo #5 : chr "0"
## $ xy_12bit__plant.oir inTrigger triggerNo #4 : chr "1"
## $ xy_12bit__plant.oir laser name #7 : chr "LD640"
## $ xy_12bit__plant.oir inTrigger triggerNo #3 : chr "0"
## $ xy_12bit__plant.oir laser name #6 : chr "LD594"
## [list output truncated]
# define ImageArray object
imgarray <- createImageArray(ome.tiff.file, series = 1, resolution = 1:2)
imgarray## ImageArray Object (x,y,c)
## Scales (2): (512,512,1) (256,256,1)
You can again use either max.pixel.size or
min.pixel.size to control the size of the image that loaded
into the memory.
## [1] 256 256
## [1] 512 512
## [1] 512 512
The delayed pyramid scheme introduced by ImageArray
objects can also be used to generate scalable plots and interactive
Shiny application for visualizing large images without loading them into
the memory.
For this example, we will use a large H&E image used by 10x Genomics, generated after a Xenium in Situ platform run (i.e. postXenium H&E).
image_file <- paste(
"https://cf.10xgenomics.com/samples/xenium/1.0.1",
"Xenium_FFPE_Human_Breast_Cancer_Rep1",
"Xenium_FFPE_Human_Breast_Cancer_Rep1_he_image.ome.tif",
sep = "/")
library(BiocFileCache)
bfc <- BiocFileCache()
image_file <- bfcrpath(bfc, image_file)## adding rname 'https://cf.10xgenomics.com/samples/xenium/1.0.1/Xenium_FFPE_Human_Breast_Cancer_Rep1/Xenium_FFPE_Human_Breast_Cancer_Rep1_he_image.ome.tif'
Let us create an ImageArray object first with the image
above. The OME-TIFF file includes multiple resolutions that we can ask
ImageArray object to include.
## ImageArray Object (x,y,c)
## Scales (6): (30786,24241,3) (15393,12120,3) ... (1924,1515,3)
## (962,757,3)
You can visualize subsets of large images really quickly using
crop and as.raster. Again, here we use
max.pixel.size to optimize the level parsed from the
pyramid; that is, as defined below, only resolutions whose width and
height are both under size 800 would be returned, whether it is the
entire image, or a subset.
# crop image
imgarray_sub <- crop(imgarray, index = list(16000:19000, 7000:10000, NULL))
# convert to raster
img_raster <- as.raster(imgarray_sub, max.pixel.size = 800)
dim(img_raster)## [1] 751 751
# plot with ggplot
ggplot(data.frame(x = 0, y = 0), aes(x, y)) +
coord_fixed(expand = FALSE,
xlim = c(0, dim(img_raster)[2]),
ylim = c(0, dim(img_raster)[1])) +
annotation_raster(img_raster,
0, dim(img_raster)[2],
dim(img_raster)[1], 0, interpolate = FALSE)Now let us create a shiny application where we can interactively subset and visualize the OME-TIFF image quickly.
The Shiny application lets the user to define image subsets (or slices) before visualizing without loading large images in memory.
This is because as.raster determines the layer with the
pixel dimensions of he specified subset not exceeding the defined
threshold (max.pixel.size).
if (interactive()) {
# variables
dimimg <- dim(imgarray)
max.pixel.size <- 800
# Define UI
ui <- fluidPage(
sliderInput("x_slider", label = "X", min = 1,
max = dimimg[1], value = c(16000, 19000)),
sliderInput("y_slider", label = "Y", min = 1,
max = dimimg[2], value = c(7000, 10000)),
mainPanel(
plotOutput(outputId = "scatterPlot")
)
)
# Define server logic
server <- function(input, output) {
output$scatterPlot <- renderPlot({
# crop image
indx <- seq(input$x_slider[1], input$x_slider[2])
indy <- seq(input$y_slider[1], input$y_slider[2])
imgarray_sub <- crop(imgarray, index = list(indx, indy, NULL))
# convert to raster
img_raster <- as.raster(imgarray_sub, max.pixel.size = max.pixel.size)
# plot with ggplot
imgggplot <- ggplot(data.frame(x = 0, y = 0), aes(x, y)) +
coord_fixed(expand = FALSE,
xlim = c(0, dim(img_raster)[2]),
ylim = c(0, dim(img_raster)[1])) +
annotation_raster(img_raster,
0, dim(img_raster)[2],
dim(img_raster)[1], 0, interpolate = FALSE)
imgggplot
})
}
# Run the app
shinyApp(ui = ui, server = server)
}## R version 4.6.0 (2026-04-24)
## Platform: x86_64-pc-linux-gnu
## Running under: Ubuntu 24.04.4 LTS
##
## Matrix products: default
## BLAS: /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
## LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so; LAPACK version 3.12.0
##
## locale:
## [1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C
## [3] LC_TIME=en_US.UTF-8 LC_COLLATE=en_US.UTF-8
## [5] LC_MONETARY=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8
## [7] LC_PAPER=en_US.UTF-8 LC_NAME=en_US.UTF-8
## [9] LC_ADDRESS=en_US.UTF-8 LC_TELEPHONE=en_US.UTF-8
## [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=en_US.UTF-8
##
## time zone: Etc/UTC
## tzcode source: system (glibc)
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] shiny_1.13.0 ggplot2_4.0.3 magick_2.9.1
## [4] RBioFormats_1.12.0 BiocFileCache_3.2.0 dbplyr_2.5.2
## [7] ImageArray_1.0.0 EBImage_4.54.0 BiocStyle_2.40.0
##
## loaded via a namespace (and not attached):
## [1] tidyselect_1.2.1 dplyr_1.2.1 farver_2.1.2
## [4] blob_1.3.0 S7_0.2.2 filelock_1.0.3
## [7] R.utils_2.13.0 bitops_1.0-9 fastmap_1.2.0
## [10] RCurl_1.98-1.18 promises_1.5.0 digest_0.6.39
## [13] mime_0.13 lifecycle_1.0.5 paws.storage_0.9.0
## [16] RSQLite_2.4.6 magrittr_2.0.5 compiler_4.6.0
## [19] rlang_1.2.0 sass_0.4.10 tools_4.6.0
## [22] yaml_2.3.12 knitr_1.51 S4Arrays_1.12.0
## [25] htmlwidgets_1.6.4 bit_4.6.0 curl_7.1.0
## [28] DelayedArray_0.38.1 RColorBrewer_1.1-3 abind_1.4-8
## [31] HDF5Array_1.40.0 purrr_1.2.2 withr_3.0.2
## [34] BiocGenerics_0.58.0 sys_3.4.3 R.oo_1.27.1
## [37] grid_4.6.0 stats4_4.6.0 xtable_1.8-8
## [40] Rhdf5lib_2.0.0 scales_1.4.0 cli_3.6.6
## [43] rmarkdown_2.31 crayon_1.5.3 generics_0.1.4
## [46] otel_0.2.0 DBI_1.3.0 cachem_1.1.0
## [49] rhdf5_2.56.0 BiocManager_1.30.27 XVector_0.52.0
## [52] tiff_0.1-12 matrixStats_1.5.0 vctrs_0.7.3
## [55] Matrix_1.7-5 jsonlite_2.0.0 IRanges_2.46.0
## [58] fftwtools_0.9-11 S4Vectors_0.50.0 bit64_4.8.0
## [61] jpeg_0.1-11 maketools_1.3.2 h5mread_1.4.0
## [64] locfit_1.5-9.12 jquerylib_0.1.4 glue_1.8.1
## [67] ZarrArray_1.0.0 rJava_1.0-18 gtable_0.3.6
## [70] later_1.4.8 Rarr_2.0.0 tibble_3.3.1
## [73] pillar_1.11.1 rappdirs_0.3.4 htmltools_0.5.9
## [76] rhdf5filters_1.24.0 R6_2.6.1 httr2_1.2.2
## [79] evaluate_1.0.5 lattice_0.22-9 R.methodsS3_1.8.2
## [82] png_0.1-9 memoise_2.0.1 httpuv_1.6.17
## [85] paws.common_0.8.9 bslib_0.10.0 Rcpp_1.1.1-1.1
## [88] SparseArray_1.12.0 xfun_0.57 MatrixGenerics_1.24.0
## [91] buildtools_1.0.0 pkgconfig_2.0.3