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): (1,768,512) (1,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 <- ImageArray(ome.tiff.file, series = 1, resolution = 1:2)
imgarray## ImageArray Object (x,y)
## Scales (2): (512,512) (256,256)
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
ImageArray can also read images with all five dimensions (XYZCT) available in the OME (Open Microscopy Environment) specifications. See OME specifications webpage for more information here.
ome.tiff.file <- system.file("extdata", "4D-series.ome.tiff",
package = "ImageArray")
# read metadata
read.metadata(ome.tiff.file, proprietary.metadata = TRUE,
filter.metadata = TRUE)## ImageMetadata
## $ coreMetadata:List of 18
## ..$ sizeX : int 439
## ..$ sizeY : int 167
## ..$ sizeZ : int 5
## ..$ sizeC : int 1
## ..$ sizeT : int 7
## ..$ pixelType: chr "int8"
## .. [list output truncated]
# define ImageArray object
imgarray <- ImageArray(ome.tiff.file, series = 1, resolution = 1)
imgarray## ImageArray Object (x,y,z,t)
## Scales (1): (439,167,5,7)
ImageArray is also capable of reading other BioFormats compatible image formats (e.g. qptiff, svs). For more information, visit the Supported Formats webpage of BioFormats.
ImageArray provides image transformation functions that operate consistently across all pyramid levels. Translation, scaling, rotation, and affine transformations are recorded as lazy operations rather than being immediately applied to the underlying image data array. This keeps the object memory-efficient while still allowing complex image-processing workflows to be expressed clearly, and most importantly, it allows cropping operations efficient since transformations are only applied to queried image slices.
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 <- ImageArray(ome.tiff.file, series = 1, resolution = 1:2)
imgarray## ImageArray Object (x,y)
## Scales (2): (512,512) (256,256)
Translation transformation will not alter the pixel dimensionality of
the image, but will change the physical space that the image occupies.
You can view the physical space that the image spans using the
extent function.
## ImageArray Object (x,y)
## Scales (2): (512,512) (256,256)
## $x
## [1] 100 612
##
## $y
## [1] 20 532
Scale transformations are performed providing the dimensionality of
the new scaled ImageArray object,
i.e. output.dim for x and y
dimensions. Here, we use the filter argument to determine
the method of interpolation of pixel intensity values after
transformations. Regular images typically use bilinear
whereas none invokes a nearest neighbor interpolation
approach more suitable for image masks.
## ImageArray Object (x,y)
## Scales (2): (200,300) (100,150)
Rotation requires specifying the angle that rotates the
image in clock-wise fashion and filter for interpolation
method.
## ImageArray Object (x,y)
## Scales (2): (724,724) (362,362)
Affine transformations are also available where users have to provide
a 2x3 matrix that applies an affine transformation on
x and y axes.
m <- matrix(c(1, -.5, 128, 0, 1, 0), nrow=3, ncol=2)
imgarray_affine <- affine(imgarray,
m = m,
filter = "bilinear")
imgarray_affine## ImageArray Object (x,y)
## Scales (2): (768,512) (384,256)
Because the transformations are delayed, they can also be performed in sequence. This is useful when an image must first be then rotated, scaled and finally shifted onto a new coordinate system.
m <- matrix(c(1, -.5, 128, 0, 1, 0), nrow=3, ncol=2)
imgarray <- affine(imgarray,
m = m,
filter = "bilinear")
imgarray <- scale(imgarray,
output.dim = c(200,300),
filter = "bilinear")
imgarray <- translation(imgarray,
shift = c(100,20))
plot(as.raster(imgarray))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] 752 752
# 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.1 (2026-06-24)
## Platform: x86_64-pc-linux-gnu
## Running under: Ubuntu 26.04 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.32.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.14.0 ggplot2_4.0.3 magick_2.9.1
## [4] RBioFormats_1.13.0 BiocFileCache_3.3.0 dbplyr_2.6.0
## [7] ImageArray_1.1.7 EBImage_4.55.2 BiocStyle_2.41.0
##
## loaded via a namespace (and not attached):
## [1] tidyselect_1.2.1 grumpy_0.1.1 dplyr_1.2.1
## [4] farver_2.1.2 blob_1.3.0 S7_0.2.2
## [7] filelock_1.0.3 R.utils_2.13.0 bitops_1.1-0
## [10] fastmap_1.2.0 RCurl_1.98-1.19 promises_1.5.0
## [13] digest_0.6.39 mime_0.13 lifecycle_1.0.5
## [16] paws.storage_0.10.0 RSQLite_3.53.3 magrittr_2.0.5
## [19] compiler_4.6.1 rlang_1.3.0 sass_0.4.10
## [22] tools_4.6.1 yaml_2.3.12 knitr_1.51
## [25] S4Arrays_1.13.0 htmlwidgets_1.6.4 bit_4.6.0
## [28] curl_7.1.0 DelayedArray_0.39.4 RColorBrewer_1.1-3
## [31] abind_1.4-8 HDF5Array_1.41.1 purrr_1.2.2
## [34] withr_3.0.3 BiocGenerics_0.59.11 sys_3.4.3
## [37] R.oo_1.27.1 grid_4.6.1 stats4_4.6.1
## [40] xtable_1.8-8 Rhdf5lib_2.1.0 scales_1.4.0
## [43] cli_3.6.6 rmarkdown_2.31 crayon_1.5.3
## [46] generics_0.1.4 otel_0.2.0 DBI_1.3.0
## [49] cachem_1.1.0 rhdf5_2.57.9 BiocManager_1.30.27
## [52] XVector_0.53.0 tiff_0.1-12 matrixStats_1.5.0
## [55] vctrs_0.7.3 Matrix_1.7-6 jsonlite_2.0.0
## [58] IRanges_2.47.2 fftwtools_0.9-11 S4Vectors_0.51.6
## [61] bit64_4.8.2 jpeg_0.1-11 maketools_1.3.2
## [64] h5mread_1.5.0 locfit_1.5-9.12 jquerylib_0.1.4
## [67] glue_1.8.1 ZarrArray_1.1.5 rJava_1.0-18
## [70] gtable_0.3.6 later_1.4.8 Rarr_2.1.32
## [73] tibble_3.3.1 pillar_1.11.1 htmltools_0.5.9
## [76] rhdf5filters_1.25.4 R6_2.6.1 httr2_1.3.0
## [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.10 bslib_0.12.0 Rcpp_1.1.2
## [88] SparseArray_1.13.2 xfun_0.60 MatrixGenerics_1.25.0
## [91] buildtools_1.0.0 pkgconfig_2.0.3