diff --git a/.Rbuildignore b/.Rbuildignore index d452576..436d224 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -3,3 +3,5 @@ LICENSE.md .github .git +^\.positai$ +^\.claude$ diff --git a/.gitignore b/.gitignore index 93b9e0f..42f5c48 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ .Ruserdata inst/.DS_Store *.Rproj +.positai diff --git a/DESCRIPTION b/DESCRIPTION index 6436d3a..61d085d 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -27,7 +27,8 @@ Imports: Rarr, ZarrArray, magick, - tools + tools, + BiocGenerics Suggests: testthat (>= 3.0.0), knitr, diff --git a/NAMESPACE b/NAMESPACE index 21568b1..c4bab35 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -12,10 +12,12 @@ exportMethods("[") exportMethods("[[") exportMethods("[[<-") exportMethods("path<-") +exportMethods(affine) exportMethods(aperm) exportMethods(axes) exportMethods(crop) exportMethods(dim) +exportMethods(extent) exportMethods(flip) exportMethods(flop) exportMethods(length) @@ -25,8 +27,11 @@ exportMethods(negate) exportMethods(path) exportMethods(realize) exportMethods(rotate) +exportMethods(scale) +exportMethods(translation) exportMethods(type) import(DelayedArray) +importFrom(BiocGenerics,scale) importFrom(DelayedArray,DelayedArray) importFrom(DelayedArray,path) importFrom(EBImage,flip) diff --git a/R/ImageArray.R b/R/ImageArray.R index ef89f3f..a3be1a9 100644 --- a/R/ImageArray.R +++ b/R/ImageArray.R @@ -14,7 +14,6 @@ #' } #' } #' @param drop ignored -#' @param angle value between 0 and 360 for degrees to rotate #' @param brightness the brightness of the new image in percentage, e.g. 120 #' @param perm perm #' @param index a named or unnamed list of indices for cropping/subsetting the @@ -30,14 +29,8 @@ #' @aliases #' [[,ImageArray,numeric-method #' [[<-,ImageArray,numeric-method -#' rotate -#' rotate,ImageArray-method #' crop #' crop,ImageArray-method -#' flip -#' flip,ImageArray-method -#' flop -#' flop,ImageArray-method #' negate #' negate,ImageArray-method #' modulate @@ -52,6 +45,8 @@ #' as.raster,ImageArray-method #' path #' path,ImageArray-method +#' extent +#' extent,ImageArray-method #' #' @examples #' # get image @@ -89,6 +84,9 @@ #' #' # realize #' imgarray <- realize(imgarray) +#' +#' # extent +#' extent(imgarray) NULL #' @describeIn ImageArray-methods subset and crop @@ -99,12 +97,6 @@ setMethod( f = "[", signature = c("ImageArray"), function(x, i, j, ..., drop = FALSE) { - if (missing(x)) { - stop("'x' is missing") - } - if (!.isTRUEorFALSE(drop)) { - stop("'drop' must be TRUE or FALSE") - } Nindex <- S4Arrays:::extract_Nindex_from_syscall(sys.call(), parent.frame()) crop(x, index = Nindex) } @@ -580,17 +572,10 @@ writeImageArray <- function( if(format == "hdf5") format <- "h5" if(fileext != format && format != "in-memory") { warning( - sprintf( - paste( - "The file extension of the output path%s does", - "not match the specified format (%s),", - "The object will be written as (%s).", - sep = " " - ), - if (fileext == "") "" else paste0(" (", fileext, ")"), - format, - format - ) + "The file extension of the output path", + if (fileext == "") "" else sprintf(" '%s'", fileext), + " does not match the specified format (", format, "). ", + "The object will be saved as ", format, " format. " ) } } diff --git a/R/allgenerics.R b/R/allgenerics.R index 85c6eeb..d695e24 100644 --- a/R/allgenerics.R +++ b/R/allgenerics.R @@ -1,11 +1,20 @@ +#' @importFrom BiocGenerics scale + # generics of ImageArray setGeneric("crop", function(object, ...) standardGeneric("crop")) setGeneric("negate", function(object, ...) standardGeneric("negate")) setGeneric("modulate", function(object, ...) standardGeneric("modulate")) setGeneric("meta", function(object, ...) standardGeneric("meta")) setGeneric("axes", function(object, ...) standardGeneric("axes")) +setGeneric("extent", \(x, ...) standardGeneric("extent")) # generics from EBImage setGeneric("rotate") setGeneric("flip") setGeneric("flop") + +# transformations +setGeneric("affine", \(x, ...) standardGeneric("affine")) +setGeneric("translation", \(x, ...) standardGeneric("translation")) +setGeneric("rotate_transform", + \(x, ...) standardGeneric("rotate_transform")) \ No newline at end of file diff --git a/R/manipulation.R b/R/manipulation.R index f9e1456..e507ada 100644 --- a/R/manipulation.R +++ b/R/manipulation.R @@ -1,43 +1,3 @@ -#' @importFrom EBImage rotate flip flop -NULL - -#' @describeIn ImageArray-methods rotate image array to 90, 180, 270 degrees -#' @export -setMethod("rotate", signature = "ImageArray", function(x, angle) { - # validate rotation - if (!angle %in% c(0, 90, 180, 270, 360)) { - stop("Only rotations of 0,90,180,270,360 degrees are supported!") - } - - # check dimensions - .check_dim(x) - dim_img <- dim(x[[1]]) - ax <- axes(x) - - # array perm. - if (angle %in% c(90, 270)) { - cur_perm <- .swap( - seq_along(dim_img), - which(ax == "x"), - which(ax == "y") - ) - x <- aperm(x, perm = cur_perm) - } - - # flop - if (angle %in% c(90, 180)) { - x <- flop(x) - } - - # flip - if (angle %in% c(180, 270)) { - x <- flip(x) - } - - # return - x -}) - #' @describeIn ImageArray-methods permute image #' @exportMethod aperm setMethod("aperm", signature = "ImageArray", function(a, perm) { @@ -74,37 +34,6 @@ setMethod("modulate", signature = "ImageArray", function(object, brightness) { object }) -#' @importFrom stats setNames -#' @noRd -.flipflop <- function(object, direction = "x") { - ax <- axes(object) - - # check dim - .check_dim(object) - - # flip all - for (i in seq_along(object@levels)) { - img <- object[[i]] - dim_img <- stats::setNames(dim(img), ax) - cur_ind <- stats::setNames(lapply(dim_img, seq_len), ax) - cur_ind[[direction]] <- rev(cur_ind[[direction]]) - object[[i]] <- .subset_array(object[[i]], cur_ind, drop = FALSE) - } - object -} - -#' @describeIn ImageArray-methods vertical flipping image -#' @export -setMethod("flip", signature = "ImageArray", function(x) { - .flipflop(x, direction = "y") -}) - -#' @describeIn ImageArray-methods horizontal flipping image -#' @export -setMethod("flop", signature = "ImageArray", function(x) { - .flipflop(x, direction = "x") -}) - #' @describeIn ImageArray-methods cropping image #' @importFrom utils head tail #' @importFrom stats setNames diff --git a/R/transformations.R b/R/transformations.R new file mode 100644 index 0000000..ace7526 --- /dev/null +++ b/R/transformations.R @@ -0,0 +1,744 @@ +#' @importFrom EBImage rotate flip flop +NULL + +#' @name trans +#' @rdname trans +#' @title Transformations +#' @aliases scale translation affine +#' +#' @description +#' Transformation methods for objects of ImageArray class. By default, +#' these operations are delayed thus transformations are only performed +#' when realized into the memory +#' +#' @param x an ImageArray object +#' @inheritParams EBImage::affine +#' @inheritParams EBImage::rotate +#' @param axes axes +#' @param shift translation shift parameter, a vector of length 2 +#' @param ... arguments passed to other methods +#' +#' @returns an \code{ImageArray} object. +#' +#' @examples +#' ome.tiff.file <- system.file("extdata", "xy_12bit__plant.ome.tiff", +#' package = "ImageArray") +#' read.metadata(ome.tiff.file) +#' +#' # define ImageArray object +#' imgarray <- createImageArray(ome.tiff.file, series = 1, resolution = 1:2) +#' +#' # translate image +#' imgarray_trans <- translation(imgarray, shift = c(100,20)) +#' +#' # scale image +#' imgarray_scale <- scale(imgarray, output.dim = c(200,300), +#' filter = "bilinear") +#' imgarray_scale +#' +#' # rotate image +#' imgarray_rotate <- rotate(imgarray, angle = 45, filter = "bilinear") +#' imgarray_rotate +#' +#' # image with affine transformation +#' m <- matrix(c(1, -.5, 128, 0, 1, 0), nrow=3, ncol=2) +#' imgarray_affine <- affine(imgarray, +#' m = m, +#' filter = "bilinear") +#' imgarray_affine +#' +#' # get extent of the transformed image +#' extent(imgarray_affine) +#' +#' # a sequence of transformations +#' m <- matrix(c(1, -.5, 128, 0, 1, 0), nrow=3, ncol=2) +#' imgarray_trans <- affine(imgarray, +#' m = m, +#' filter = "bilinear") +#' imgarray_trans +#' +#' imgarray_trans <- scale(imgarray_trans, +#' output.dim = c(200,300), +#' filter = "bilinear") +#' imgarray_trans <- translation(imgarray_trans, +#' shift = c(100,20)) +#' +NULL + +#### +# Classes #### +#### + +#' @noRd +#' @rdname trans +#' @keywords internal +setClass( + "DelayedAffineSeed", + contains = "DelayedUnaryOp", + slots = c( + seed = "ANY", + dim = "integer", + dimnames = "list", + m = "matrix", + axes = "character", + filter = "character", + bg.col = "character", + antialias = "logical" + ) +) + +#' @noRd +#' @rdname trans +#' @keywords internal +setClass( + "DelayedTranslateSeed", + contains = "DelayedUnaryOp", + slots = c( + seed = "ANY", + dim = "integer", + dimnames = "list", + shift = "numeric", + axes = "character" + ) +) + +setClassUnion("DelayedTransformSeed", + c("DelayedAffineSeed", "DelayedTranslateSeed")) + + +#' @noRd +#' @rdname trans +#' @keywords internal +setMethod("dim", "DelayedTransformSeed", function(x) { + x@dim +}) + +#' @noRd +#' @rdname trans +#' @keywords internal +setMethod("dimnames", "DelayedTransformSeed", function(x) { + x@dimnames +}) + +#' @noRd +#' @rdname trans +#' @keywords internal +setMethod("type", "DelayedTransformSeed", function(x) { + DelayedArray::type(x@seed) +}) + +#### +# Utils #### +#### + +.as_homogeneous <- function(m) { + if (!is.matrix(m) || !identical(dim(m), c(3L, 2L))) { + stop("'m' must be a 3 x 2 EBImage affine matrix") + } + + cbind(m, c(0, 0, 1)) +} + +.from_homogeneous <- function(M) { + M[, seq_len(2), drop = FALSE] +} + +.translate_h <- function(dx, dy) { + matrix( + c( + 1, 0, 0, + 0, 1, 0, + dx, dy, 1 + ), + nrow = 3, + byrow = TRUE + ) +} + +.default_axes <- function(ndim) { + c( + "x", + "y", + if (ndim > 2L) paste0("d", seq.int(3L, ndim)) else character() + ) +} + +.axes_perm <- function(ndim, axes) { + xy <- match(c("x", "y"), axes) + c(xy, setdiff(seq_len(ndim), xy)) +} + +.axes_first <- function(a, axes) { + p <- .axes_perm(length(dim(a)), axes) + + if (identical(p, seq_along(p))) { + a + } else { + aperm(a, p) + } +} + +.axes_back <- function(a, axes) { + ndim <- length(dim(a)) + p <- .axes_perm(ndim, axes) + inv <- order(p) + + if (identical(inv, seq_len(ndim))) { + a + } else { + aperm(a, inv) + } +} + +.normalize_index <- function(index, dim) { + if (length(index) == 0L) { + index <- vector("list", length(dim)) + } + + if (length(index) != length(dim)) { + stop("'index' must have one entry per dimension") + } + + Map( + function(i, n) { + if (is.null(i)) { + seq_len(n) + } else { + as.integer(i) + } + }, + index, + dim + ) +} + +.is_full_index <- function(index, dim) { + all(vapply( + seq_along(dim), + function(k) identical(index[[k]], seq_len(dim[k])), + logical(1) + )) +} + +.bg_array <- function(x, dim) { + type <- tryCatch( + DelayedArray::type(x@seed), + error = function(e) "double" + ) + + proto <- switch( + type, + logical = logical(prod(dim)), + integer = integer(prod(dim)), + double = numeric(prod(dim)), + numeric = numeric(prod(dim)), + complex = complex(prod(dim)), + character = character(prod(dim)), + raw = raw(prod(dim)), + numeric(prod(dim)) + ) + + array(proto, dim = dim) +} + +.source_bbox_from_output_query <- function(m, + out_x, + out_y, + source_dim_xy, + filter = "bilinear") { + M <- .as_homogeneous(m) + Minv <- solve(M) + + ## EBImage-style pixel coordinates are treated as zero-based here. + ## R index 1 corresponds to coordinate 0. + x0 <- min(out_x) - 1 + x1 <- max(out_x) - 1 + y0 <- min(out_y) - 1 + y1 <- max(out_y) - 1 + + output_corners <- rbind( + c(x0, y0, 1), + c(x1, y0, 1), + c(x0, y1, 1), + c(x1, y1, 1) + ) + + source_corners <- output_corners %*% Minv + + sx_min <- min(source_corners[, 1]) + sx_max <- max(source_corners[, 1]) + sy_min <- min(source_corners[, 2]) + sy_max <- max(source_corners[, 2]) + + pad <- if (identical(filter, "bilinear")) 2L else 1L + + sx0 <- floor(sx_min) + 1L - pad + sx1 <- ceiling(sx_max) + 1L + pad + sy0 <- floor(sy_min) + 1L - pad + sy1 <- ceiling(sy_max) + 1L + pad + + sx0 <- max(1L, sx0) + sy0 <- max(1L, sy0) + sx1 <- min(source_dim_xy[1], sx1) + sy1 <- min(source_dim_xy[2], sy1) + + list( + x = if (sx0 <= sx1) seq.int(sx0, sx1) else integer(0), + y = if (sy0 <= sy1) seq.int(sy0, sy1) else integer(0) + ) +} + +.localize_affine <- function(m, + source_origin_index, + output_origin_index) { + M <- .as_homogeneous(m) + + ## Convert R indices to zero-based EBImage coordinates. + source_origin0 <- source_origin_index - 1 + output_origin0 <- output_origin_index - 1 + + M_local <- + .translate_h(source_origin0[1], source_origin0[2]) %*% + M %*% + .translate_h(-output_origin0[1], -output_origin0[2]) + + .from_homogeneous(M_local) +} + +.collect_affine_transform_seed <- function(x) { + full_index <- vector("list", length(dim(x@seed))) + + arr <- S4Arrays::extract_array(x@seed, full_index) + arr <- .axes_first(arr, x@axes) + + xy <- match(c("x", "y"), x@axes) + + out <- EBImage::affine( + arr, + m = x@m, + filter = x@filter, + output.dim = x@dim[xy], + bg.col = x@bg.col, + antialias = x@antialias + ) + + .axes_back(as.array(out), x@axes) +} + +.get_extent_from_box <- function(bboxmin, bboxmax) { + list(x = c(bboxmin[1], bboxmax[1]), + y = c(bboxmin[2], bboxmax[2])) +} + +.get_bbox_from_extent <- function(ext,m, adjust = TRUE){ + px <- as.matrix(expand.grid(ext[[1]], ext[[2]])) + transformed <- px %*% m[seq_len(2),] + if(adjust) transformed <- sweep(transformed, 2L, m[3,], "+") + bbox.min <- apply(transformed, 2L, min) + bbox.max <- apply(transformed, 2L, max) + list(min = bbox.min, max = bbox.max) +} + +.adjust_affine_matrix <- function(dim, axes, m, output.dim = NULL){ + dim <- setNames(dim, axes) + bbox <- .get_bbox_from_extent( + list(x = c(0,dim[["x"]]), y = c(0,dim[["y"]])), + m = m) + m[3, ] <- m[3,] - bbox$min + newdim <- bbox$max - bbox$min + if(!is.null(output.dim)) newdim <- output.dim + dim[c("x", "y")] <- as.integer(round(newdim)) + names(dim) <- NULL + list(m=m, output.dim=dim) +} + +.check_outputdim <- function(output.dim){ + if(!is.null(output.dim)) + if (length(output.dim) != 2L || !is.numeric(output.dim)) + stop("'output.dim' must be a numeric vector of length 2") +} + +# extract_array #### + +setMethod( + "extract_array", + "DelayedAffineSeed", + function(x, index) { + out_dim <- dim(x) + index <- .normalize_index(index, out_dim) + + ans_dim <- vapply(index, length, integer(1)) + + if (any(ans_dim == 0L)) { + return(.bg_array(x, ans_dim)) + } + + if (.is_full_index(index, out_dim)) { + return(.collect_affine_transform_seed(x)) + } + + xdim <- match("x", x@axes) + ydim <- match("y", x@axes) + + out_x <- index[[xdim]] + out_y <- index[[ydim]] + + ## Render a contiguous output window, then subset/reorder at the end. + out_x_window <- seq.int(min(out_x), max(out_x)) + out_y_window <- seq.int(min(out_y), max(out_y)) + + source_dim <- dim(x@seed) + + bbox <- .source_bbox_from_output_query( + m = x@m, + out_x = out_x_window, + out_y = out_y_window, + source_dim_xy = source_dim[c(xdim, ydim)], + filter = x@filter + ) + + if (length(bbox$x) == 0L || length(bbox$y) == 0L) { + return(.bg_array(x, ans_dim)) + } + + source_index <- index + source_index[[xdim]] <- bbox$x + source_index[[ydim]] <- bbox$y + + source_patch <- S4Arrays::extract_array(x@seed, source_index) + source_patch <- .axes_first(source_patch, x@axes) + + m_local <- .localize_affine( + m = x@m, + source_origin_index = c(min(bbox$x), min(bbox$y)), + output_origin_index = c(min(out_x_window), min(out_y_window)) + ) + + rendered <- EBImage::affine( + source_patch, + m = m_local, + filter = x@filter, + output.dim = c(length(out_x_window), length(out_y_window)), + bg.col = x@bg.col, + antialias = x@antialias + ) + + rendered <- .axes_back(as.array(rendered), x@axes) + + local_index <- vector("list", length(out_dim)) + local_index[[xdim]] <- match(out_x, out_x_window) + local_index[[ydim]] <- match(out_y, out_y_window) + + for (k in seq_along(out_dim)) { + if (k != xdim && k != ydim) { + local_index[[k]] <- seq_along(index[[k]]) + } + } + + do.call( + `[`, + c(list(rendered), local_index, list(drop = FALSE)) + ) + } +) + +setMethod( + "extract_array", + "DelayedTranslateSeed", + function(x, index) { + out_dim <- dim(x) + index <- .normalize_index(index, out_dim) + S4Arrays::extract_array(x@seed, index) + }) + +# extent #### + +#' @export +#' @describeIn ImageArray-methods extent +setMethod("extent", "ImageArray", function(x){ + dims <- c("x", "y") + xy <- match(dims, axes(x)) + setNames(extent(x[[1]])[xy], dims) +}) + +#' @keywords internal +#' @noRd +setMethod("extent", "DelayedArray", function(x){ + extent(x@seed) +}) + +#' @keywords internal +#' @noRd +setMethod("extent", "DelayedAffineSeed", function(x){ + ext <- extent(x@seed) + xy <- match(c("x", "y"), x@axes) + bbox <- .get_bbox_from_extent(ext[xy], x@m, adjust = FALSE) + ext[xy] <- .get_extent_from_box(bbox$min, bbox$max) + ext +}) + +#' @keywords internal +#' @noRd +setMethod("extent", "DelayedTranslateSeed", function(x){ + ext <- extent(x@seed) + xy <- match(c("x", "y"), x@axes) + ext[xy] <- Map(\(i,j){ + i+j + }, ext[xy],x@shift) + ext +}) + +.extent_array <- function(x) { + lapply(dim(x), \(.){ + c(0,.) + }) +} + +#' @keywords internal +#' @noRd +setMethod("extent", "Array", .extent_array) + +#' @keywords internal +#' @noRd +setMethod("extent", "array", .extent_array) + +#' @keywords internal +#' @noRd +setMethod("extent", "matrix", .extent_array) + +# lazy transformations #### + +.affine_transform <- function(x, + m, + axes = NULL, + output.dim = NULL, + filter = c("bilinear", "none"), + bg.col = "black", + antialias = TRUE) { + filter <- match.arg(filter) + + adj <- .adjust_affine_matrix(dim(x), axes, m, output.dim) + output.dim <- as.integer(adj$output.dim) + m <- adj$m + dn <- vector("list", length(output.dim)) + + seed <- new( + "DelayedAffineSeed", + seed = x@seed, + dim = as.integer(output.dim), + dimnames = dn, + m = m, + axes = axes, + filter = filter, + bg.col = bg.col, + antialias = isTRUE(antialias) + ) + + DelayedArray::DelayedArray(seed) +} + +#' @export +#' @describeIn trans affine transformation +setMethod("affine", + signature = "ImageArray", + function(x, + m, + axes = NULL, + output.dim = NULL, + filter = c("bilinear", "none"), + bg.col = "black", + antialias = TRUE) { + ax <- axes(x) + for (i in seq_along(x@levels)) { + scl <- rep(2^(i - 1), 2) + m <- solve(diag(c(scl, 1))) %*% m %*% diag(scl) + if(!is.null(output.dim)){ + cur_output.dim <- output.dim / 2^(i - 1) + } else { + cur_output.dim <- output.dim + } + .check_outputdim(cur_output.dim) + x[[i]] <- .affine_transform( + x[[i]], + m = m, + axes = ax, + output.dim = cur_output.dim, + filter = filter, + bg.col = bg.col, + antialias = antialias + ) + } + x + }) + + +.rotate_transform <- function(x, + angle, + axes = NULL, + filter = c("bilinear", "none"), + bg.col = "black", + antialias = TRUE) { + if ((angle%%90) == 0) + filter <- "none" + angle <- angle * pi/180 + xy <- match(c("x", "y"), axes) + d <- dim(x)[xy] + cos <- cos(angle) + sin <- sin(angle) + output.dim = c(d[1] * abs(cos) + d[2] * abs(sin), d[1] * abs(sin) + + d[2] * abs(cos)) + offset = c(d[1] * max(0, -cos) + d[2] * max(0, sin), d[1] * + max(0, -sin) + d[2] * max(0, -cos)) + m <- matrix(c(cos, -sin, offset[1], sin, cos, offset[2]), + 3L, 2L) + affine(x, + m, + axes = axes, + output.dim = output.dim, + filter = filter, + bg.col = bg.col, + antialias = antialias) +} + +#' @describeIn trans rotation transformation +#' @export +setMethod("rotate", + signature = "ImageArray", + function(x, + angle, + filter = c("bilinear", "none"), + bg.col = "black", + antialias = TRUE) { + .rotate_transform(x, + angle = angle, + axes = axes(x), + filter = filter, + bg.col = bg.col, + antialias = antialias) + }) + +.scale_transform <- function(x, + output.dim = NULL, + output.origin = c(0,0), + axes = NULL, + filter = c("bilinear", "none"), + bg.col = "black", + antialias = TRUE, + ...) { + if (length(output.origin) != 2L || !is.numeric(output.origin)) + stop("'output.origin' must be a numeric vector of length 2") + xy <- match(c("x", "y"), axes) + ratio <- output.dim/dim(x)[xy] + m <- matrix(c(ratio[1], 0, (1 - ratio[1]) * output.origin[1], + 0, ratio[2], (1 - ratio[2]) * output.origin[2]), 3L, + 2L) + .affine_transform(x, + m, + axes = axes, + filter = filter, + bg.col = bg.col, + antialias = antialias) +} + +#' @export +#' @describeIn trans scale transformation +setMethod("scale", + signature = "ImageArray", + function(x, + output.dim = NULL, + output.origin = c(0,0), + axes = NULL, + filter = c("bilinear", "none"), + bg.col = "black", + antialias = TRUE, + ...) { + ax <- axes(x) + .check_outputdim(output.dim) + for (i in seq_along(x@levels)) { + cur_output.dim <- output.dim / 2^(i - 1) + x[[i]] <- .scale_transform( + x[[i]], + output.dim = cur_output.dim, + output.origin = output.origin, + axes = ax, + filter = filter, + bg.col = bg.col, + antialias = antialias, + ... + ) + } + x + }) + +.translate_transform <- function(x, + shift = c(0,0), + axes = NULL) { + if (length(shift) != 2L || !is.numeric(shift)) + stop("'shift' must be a numeric vector of length 2, ", + "associated with x,y dimenions of an image!") + + d1 <- dim(x) + dn <- vector("list", length(d1)) + + seed <- new( + "DelayedTranslateSeed", + seed = x@seed, + dim = as.integer(d1), + dimnames = dn, + shift = shift, + axes = axes + ) + + DelayedArray::DelayedArray(seed) +} + +#' @export +#' @describeIn trans translation transformation +setMethod("translation", + signature = "ImageArray", + function(x, + shift = c(0,0), + axes = NULL) { + ax <- axes(x) + for (i in seq_along(x@levels)) { + x[[i]] <- .translate_transform( + x[[i]], + shift = shift, + axes = ax + ) + } + x + }) + +# other transformations #### + +#' @importFrom stats setNames +#' @noRd +.flipflop <- function(object, direction = "x") { + ax <- axes(object) + + # check dim + .check_dim(object) + + # flip all + for (i in seq_along(object@levels)) { + img <- object[[i]] + dim_img <- stats::setNames(dim(img), ax) + cur_ind <- stats::setNames(lapply(dim_img, seq_len), ax) + cur_ind[[direction]] <- rev(cur_ind[[direction]]) + object[[i]] <- .subset_array(object[[i]], cur_ind, drop = FALSE) + } + object +} + +#' @describeIn trans vertical flipping +#' @export +setMethod("flip", signature = "ImageArray", function(x) { + .flipflop(x, direction = "y") +}) + +#' @describeIn trans horizontal flipping +#' @export +setMethod("flop", signature = "ImageArray", function(x) { + .flipflop(x, direction = "x") +}) \ No newline at end of file diff --git a/man/ImageArray-methods.Rd b/man/ImageArray-methods.Rd index 8fa2fae..511d7a1 100644 --- a/man/ImageArray-methods.Rd +++ b/man/ImageArray-methods.Rd @@ -1,18 +1,12 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/ImageArray.R, R/conversion.R, -% R/manipulation.R, R/utils.R +% R/manipulation.R, R/transformations.R, R/utils.R \name{ImageArray-methods} \alias{ImageArray-methods} \alias{[[,ImageArray,numeric-method} \alias{[[<-,ImageArray,numeric-method} -\alias{rotate} -\alias{rotate,ImageArray-method} \alias{crop} \alias{crop,ImageArray-method} -\alias{flip} -\alias{flip,ImageArray-method} -\alias{flop} -\alias{flop,ImageArray-method} \alias{negate} \alias{negate,ImageArray-method} \alias{modulate} @@ -27,7 +21,11 @@ \alias{as.raster,ImageArray-method} \alias{path} \alias{path,ImageArray-method} +\alias{extent} +\alias{extent,ImageArray-method} \alias{[,ImageArray,ANY,ANY,ANY-method} +\alias{[[,ImageArray,numeric,ANY-method} +\alias{[[<-,ImageArray,numeric,ANY,ANY-method} \alias{dim,ImageArray-method} \alias{type,ImageArray-method} \alias{length,ImageArray-method} @@ -39,9 +37,9 @@ \usage{ \S4method{[}{ImageArray,ANY,ANY,ANY}(x, i, j, ..., drop = FALSE) -\S4method{[[}{ImageArray,numeric}(x, i) +\S4method{[[}{ImageArray,numeric,ANY}(x, i) -\S4method{[[}{ImageArray,numeric}(x, i, j, ...) <- value +\S4method{[[}{ImageArray,numeric,ANY,ANY}(x, i, j, ...) <- value \S4method{dim}{ImageArray}(x) @@ -55,24 +53,20 @@ ImageArray(meta, levels) \method{as.raster}{ImageArray}(x, level = NULL, max.pixel.size = NULL, min.pixel.size = NULL, ...) -\S4method{rotate}{ImageArray}(x, angle) - \S4method{aperm}{ImageArray}(a, perm) \S4method{negate}{ImageArray}(object) \S4method{modulate}{ImageArray}(object, brightness) -\S4method{flip}{ImageArray}(x) - -\S4method{flop}{ImageArray}(x) - \S4method{crop}{ImageArray}(object, index) \S4method{axes}{ImageArray}(object) \S4method{meta}{ImageArray}(object) +\S4method{extent}{ImageArray}(x) + \S4method{path}{ImageArray}(object) \S4method{path}{ImageArray}(object) <- value @@ -106,8 +100,6 @@ starting with 1} \item{min.pixel.size}{minimum pixel size} -\item{angle}{value between 0 and 360 for degrees to rotate} - \item{perm}{perm} \item{brightness}{the brightness of the new image in percentage, e.g. 120} @@ -134,7 +126,7 @@ for \code{ImageArray} objects \item \code{x[[i}: Layer access for \code{ImageArray} objects -\item \code{`[[`(x = ImageArray, i = numeric) <- value}: Layer access +\item \code{`[[`(x = ImageArray, i = numeric, j = ANY) <- value}: Layer access for \code{ImageArray} objects \item \code{dim(ImageArray)}: dimensions of an ImageArray @@ -151,24 +143,20 @@ A function for creating objects of ImageArray class \item \code{as.raster(ImageArray)}: create a raster object -\item \code{rotate(ImageArray)}: rotate image array to 90, 180, 270 degrees - \item \code{aperm(ImageArray)}: permute image \item \code{negate(ImageArray)}: negate image \item \code{modulate(ImageArray)}: modulate image -\item \code{flip(ImageArray)}: vertical flipping image - -\item \code{flop(ImageArray)}: horizontal flipping image - \item \code{crop(ImageArray)}: cropping image \item \code{axes(ImageArray)}: get axes metadata of the ImageArray object \item \code{meta(ImageArray)}: get metadata of the ImageArray object +\item \code{extent(ImageArray)}: extent + \item \code{path(ImageArray)}: path of an ImageArray object \item \code{path(ImageArray) <- value}: replace method for path(ImageArray) @@ -210,4 +198,7 @@ imgarray_raster <- as.raster(imgarray) # realize imgarray <- realize(imgarray) + +# extent +extent(imgarray) } diff --git a/man/trans.Rd b/man/trans.Rd new file mode 100644 index 0000000..29741c0 --- /dev/null +++ b/man/trans.Rd @@ -0,0 +1,142 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/transformations.R +\name{trans} +\alias{trans} +\alias{scale} +\alias{translation} +\alias{affine} +\alias{affine,ImageArray-method} +\alias{rotate,ImageArray-method} +\alias{scale,ImageArray-method} +\alias{translation,ImageArray-method} +\alias{flip,ImageArray-method} +\alias{flop,ImageArray-method} +\title{Transformations} +\usage{ +\S4method{affine}{ImageArray}( + x, + m, + axes = NULL, + output.dim = NULL, + filter = c("bilinear", "none"), + bg.col = "black", + antialias = TRUE +) + +\S4method{rotate}{ImageArray}( + x, + angle, + filter = c("bilinear", "none"), + bg.col = "black", + antialias = TRUE +) + +\S4method{scale}{ImageArray}( + x, + output.dim = NULL, + output.origin = c(0, 0), + axes = NULL, + filter = c("bilinear", "none"), + bg.col = "black", + antialias = TRUE, + ... +) + +\S4method{translation}{ImageArray}(x, shift = c(0, 0), axes = NULL) + +\S4method{flip}{ImageArray}(x) + +\S4method{flop}{ImageArray}(x) +} +\arguments{ +\item{x}{an ImageArray object} + +\item{m}{A 3x2 matrix describing the affine transformation. See Details.} + +\item{axes}{axes} + +\item{output.dim}{A vector of 2 numbers indicating the dimension of the output image. + For \code{affine} and \code{translate} the default is \code{dim(x)}, for \code{resize} it equals \code{c(w, h)}, and for \code{rotate} it defaults to the bounding box size of the rotated image.} + +\item{filter}{A character string indicating the interpolating sampling filter. + Valid values are 'none' or 'bilinear'. See Details.} + +\item{bg.col}{Color used to fill the background pixels, defaults to "black". In the case of multi-frame images the value is recycled, and individual background for each frame can be specified by providing a vector.} + +\item{antialias}{If \code{TRUE}, perform bilinear sampling at image edges using \code{bg.col}.} + +\item{angle}{A numeric specifying the image rotation angle in degrees.} + +\item{output.origin}{A vector of 2 numbers indicating the output coordinates of the origin in pixels.} + +\item{...}{arguments passed to other methods} + +\item{shift}{translation shift parameter, a vector of length 2} +} +\value{ +an \code{ImageArray} object. +} +\description{ +Transformation methods for objects of ImageArray class. By default, +these operations are delayed thus transformations are only performed +when realized into the memory +} +\section{Functions}{ +\itemize{ +\item \code{affine(ImageArray)}: affine transformation + +\item \code{rotate(ImageArray)}: rotation transformation + +\item \code{scale(ImageArray)}: scale transformation + +\item \code{translation(ImageArray)}: translation transformation + +\item \code{flip(ImageArray)}: vertical flipping + +\item \code{flop(ImageArray)}: horizontal flipping + +}} +\examples{ +ome.tiff.file <- system.file("extdata", "xy_12bit__plant.ome.tiff", +package = "ImageArray") +read.metadata(ome.tiff.file) + +# define ImageArray object +imgarray <- createImageArray(ome.tiff.file, series = 1, resolution = 1:2) + +# translate image +imgarray_trans <- translation(imgarray, shift = c(100,20)) + +# scale image +imgarray_scale <- scale(imgarray, output.dim = c(200,300), + filter = "bilinear") +imgarray_scale + +# rotate image +imgarray_rotate <- rotate(imgarray, angle = 45, filter = "bilinear") +imgarray_rotate + +# image with affine transformation +m <- matrix(c(1, -.5, 128, 0, 1, 0), nrow=3, ncol=2) +imgarray_affine <- affine(imgarray, + m = m, + filter = "bilinear") +imgarray_affine + +# get extent of the transformed image +extent(imgarray_affine) + +# a sequence of transformations +m <- matrix(c(1, -.5, 128, 0, 1, 0), nrow=3, ncol=2) +imgarray_trans <- affine(imgarray, + m = m, + filter = "bilinear") +imgarray_trans + +imgarray_trans <- scale(imgarray_trans, + output.dim = c(200,300), + filter = "bilinear") +imgarray_trans <- translation(imgarray_trans, + shift = c(100,20)) + +} diff --git a/tests/testthat/helper-transformations.R b/tests/testthat/helper-transformations.R new file mode 100644 index 0000000..153673b --- /dev/null +++ b/tests/testthat/helper-transformations.R @@ -0,0 +1,51 @@ +# test affine +test_affine <- function(img, axes, m){ + actual_dim <- dim(img) + xy <- match(c("x", "y"), axes) + ap <- c(xy, setdiff(seq_along(actual_dim), xy)) + img <- aperm(img, perm = ap) + img <- EBImage::Image(img) + adj <- .adjust_affine_matrix(actual_dim, axes, m) + img_affine <- EBImage::affine(EBImage::Image(img), m = adj$m, output.dim = adj$output.dim[xy]) + img_affine <- EBImage::imageData(img_affine) + aperm(img_affine, perm = order(ap)) +} + +# test affine +test_rotate <- function(img, axes, angle){ + actual_dim <- dim(img) + xy <- match(c("x", "y"), axes) + ap <- c(xy, setdiff(seq_along(actual_dim), xy)) + img <- aperm(img, perm = ap) + img <- EBImage::Image(img) + img_rotate <- EBImage::rotate(EBImage::Image(img), angle = angle) + img_rotate <- EBImage::imageData(img_rotate) + aperm(img_rotate, perm = order(ap)) +} + +# test scale +test_scale <- function(img, axes, output.dim){ + actual_dim <- dim(img) + xy <- match(c("x", "y"), axes) + ap <- c(xy, setdiff(seq_along(actual_dim), xy)) + img <- aperm(img, perm = ap) + img <- EBImage::Image(img) + img_scale <- EBImage::resize(EBImage::Image(img), + w = output.dim[1], h = output.dim[2]) + img_scale <- EBImage::imageData(img_scale) + aperm(img_scale, perm = order(ap)) +} + +# test scale +test_subset <- function(img, axes, index){ + actual_dim <- dim(img) + xy <- match(c("x", "y"), axes) + ap <- c(xy, setdiff(seq_along(actual_dim), xy)) + img <- aperm(img, perm = ap) + if(length(dim(img)) == 2){ + img_subset <- img[index[[1]], index[[2]]] + } else { + img_subset <- img[index[[1]], index[[2]], , drop = FALSE] + } + aperm(img_subset, perm = order(ap)) +} \ No newline at end of file diff --git a/tests/testthat/test-delayedarray.R b/tests/testthat/test-delayedarray.R index 2e0107e..3fd5193 100644 --- a/tests/testthat/test-delayedarray.R +++ b/tests/testthat/test-delayedarray.R @@ -17,8 +17,7 @@ mat_raster <- as.raster(mat, max = 255) # read as magick object mat_image <- magick::image_read(mat_raster) -test_that("path hdf5", { - # h5 +test_that("path HDF5", { mat_list <- writeImageArray( mat_image, output = output_h5, @@ -39,8 +38,7 @@ test_that("path hdf5", { expect_equal(path(mat_list), output_h5_replace) }) -test_that("path zarr", { - # zarr +test_that("path Zarr", { mat_list <- writeImageArray( mat_image, output = output_zarr, diff --git a/tests/testthat/test-manipulation.R b/tests/testthat/test-manipulation.R index 481af5c..93ec6f9 100644 --- a/tests/testthat/test-manipulation.R +++ b/tests/testthat/test-manipulation.R @@ -44,15 +44,6 @@ test_that("manipulate h5 ImageArray", { expect_equal(unique(as.vector(tmp)), 255) expect_equal(type(mat_list_negated[[1]]), "integer") - # rotate - mat_list_rotated <- rotate(mat_list, angle = 90) - expect_equal(dim(mat_list_rotated), c(3, 2000, 5000)) - mat_list_rotated <- rotate(mat_list, angle = 180) - expect_equal(dim(mat_list_rotated), c(3, 5000, 2000)) - mat_list_rotated <- rotate(mat_list, angle = 270) - expect_equal(dim(mat_list_rotated), c(3, 2000, 5000)) - expect_error(mat_list_rotated <- rotate(mat_list, angle = 20)) - # flip flop mat_list_flipflop <- flip(mat_list) expect_equal( @@ -119,15 +110,6 @@ test_that("manipulate zarr ImageArray", { tmp <- realize(mat_list[[1]]) + realize(mat_list_negated[[1]]) expect_equal(unique(as.vector(tmp)), 255) - # rotate - mat_list_rotated <- rotate(mat_list, angle = 90) - expect_equal(dim(mat_list_rotated), c(3, 2000, 5000)) - mat_list_rotated <- rotate(mat_list, angle = 180) - expect_equal(dim(mat_list_rotated), c(3, 5000, 2000)) - mat_list_rotated <- rotate(mat_list, angle = 270) - expect_equal(dim(mat_list_rotated), c(3, 2000, 5000)) - expect_error(mat_list_rotated <- rotate(mat_list, angle = 20)) - # flip flop mat_list_flipflop <- flip(mat_list) expect_equal( diff --git a/tests/testthat/test-transformations.R b/tests/testthat/test-transformations.R new file mode 100644 index 0000000..bce93d4 --- /dev/null +++ b/tests/testthat/test-transformations.R @@ -0,0 +1,208 @@ +library(magick) +library(rhdf5) +library(HDF5Array) +library(Rarr) + +# images +ome.tiff.file <- system.file("extdata", "xy_12bit__plant.ome.tiff", + package = "ImageArray") +f <- system.file("images", "sample.png", package = "EBImage") + +# array list +img_list <- list() + +# OME TIFF +imgarray <- createImageArray(ome.tiff.file, series = 1, resolution = 1:2) +img_list[["tiff"]] <- imgarray + +# HDF5 +img <- image_read(f) +output_h5 <- tempfile(fileext = ".h5") +imgarray <- writeImageArray(img, output = output_h5) +img_list[["hdf5"]] <- imgarray + +# Zarr +img <- image_read(f) +output_h5 <- tempfile(fileext = ".zarr") +imgarray <- writeImageArray(img, output = output_h5) +img_list[["zarr"]] <- imgarray + +for(ni in names(img_list)) { + + imgarray <- img_list[[ni]] + xy <- match(c("x", "y"), axes(imgarray)) + + test_that(paste0("translation transformation for ", ni), { + + # translate image + shift <- c(100,20) + imgarray_trans <- translation(imgarray, shift = shift) + + # check seed + expect_s4_class(imgarray_trans, "ImageArray") + for(i in seq_along(imgarray_trans)){ + expect_s4_class(imgarray_trans[[i]]@seed, "DelayedTranslateSeed") + expect_s4_class(imgarray_trans[[i]]@seed, "DelayedUnaryOp") + } + + # check extent + expect_equal( + extent(imgarray_trans), + mapply(\(i,j) i+j, extent(imgarray), shift, SIMPLIFY = FALSE) + ) + }) + + test_that(paste0("scaling transformation for ", ni), { + + # scale image + output.dim <- c(200,300) + imgarray_scale <- scale(imgarray, output.dim = output.dim) + + # check seed + expect_s4_class(imgarray_scale, "ImageArray") + for(i in seq_along(imgarray_scale)){ + expect_s4_class(imgarray_scale[[i]]@seed, "DelayedAffineSeed") + expect_s4_class(imgarray_scale[[i]]@seed, "DelayedUnaryOp") + } + + # check dim + actual.dim <- dim(imgarray) + od <- actual.dim + od[xy] <- output.dim + expect_equal(dim(imgarray_scale), od) + for(i in seq_along(imgarray)){ + od[xy] <- od[xy] / 2^(i-1) + expect_equal(dim(imgarray_scale[[i]]), od) + } + + # check scale + img <- realize(imgarray) + img_scale <- test_scale(img, axes(imgarray), output.dim) + dimnames(img_scale) <- vector("list", length(dim(img_scale))) + expect_equal( + realize(imgarray_scale[[1]]), + img_scale + ) + + # check subset + index <- list(x = 100:140, y = 230:240) + imgarray_subset <- crop(imgarray_scale, index = index) + img_subset <- test_subset(img_scale, axes(imgarray), index) + dimnames(img_subset) <- NULL + expect_equal( + realize(imgarray_subset[[1]]), + img_subset + ) + }) + + test_that(paste0("affine transformation for ", ni), { + + # affine transform image + m <- matrix(c(1, -.5, 128, 0, 1, 0), nrow=3, ncol=2) + imgarray_affine <- affine(imgarray, m = m) + + # check seed + expect_s4_class(imgarray_affine, "ImageArray") + for(i in seq_along(imgarray_affine)){ + expect_s4_class(imgarray_affine[[i]]@seed, "DelayedAffineSeed") + expect_s4_class(imgarray_affine[[i]]@seed, "DelayedUnaryOp") + } + + # check affine + img <- realize(imgarray) + img_affine <- test_affine(img, axes(imgarray), m) + dimnames(img_affine) <- vector("list", length(dim(img_affine))) + expect_equal( + realize(imgarray_affine[[1]]), + img_affine + ) + + # check subset + index <- list(x = 100:140, y = 230:240) + imgarray_subset <- crop(imgarray_affine, index = index) + img_subset <- test_subset(img_affine, axes(imgarray), index) + dimnames(img_subset) <- NULL + expect_equal( + realize(imgarray_subset[[1]]), + img_subset + ) + }) + + # test a lot of angles + for(angle in c(seq(2, 360, 20), c(90, 180, 270))){ + + # test a lot of angles + test_that(paste0("rotate (", angle, " degrees) transformation for ", ni), { + + # rotate image + imgarray_rotate <- ImageArray::rotate(imgarray, angle = angle) + + # check seed + expect_s4_class(imgarray_rotate, "ImageArray") + for(i in seq_along(imgarray_rotate)){ + expect_s4_class(imgarray_rotate[[i]]@seed, "DelayedAffineSeed") + expect_s4_class(imgarray_rotate[[i]]@seed, "DelayedUnaryOp") + } + + # check affine + img <- realize(imgarray) + img_rotate <- test_rotate(img, axes(imgarray), angle) + dimnames(img_rotate) <- vector("list", length(dim(img_rotate))) + expect_equal( + realize(imgarray_rotate[[1]]), + img_rotate + ) + + # check subset + index <- list(x = 100:140, y = 230:240) + imgarray_subset <- crop(imgarray_rotate, index = index) + img_subset <- test_subset(img_rotate, axes(imgarray), index) + dimnames(img_subset) <- NULL + expect_equal( + realize(imgarray_subset[[1]]), + img_subset + ) + }) + } + + test_that(paste0("sequence transformation for ", ni), { + m <- matrix(c(1, -.5, 128, 0, 1, 0), nrow=3, ncol=2) + output.dim <- c(200,300) + shift <- c(100,20) + img <- realize(imgarray) + + # affine + imgarray_affine <- affine(imgarray, m = m) + img_affine <- test_affine(img, axes(imgarray), m) + + # scale + imgarray_scale <- scale(imgarray_affine, output.dim = output.dim) + img_scale <- test_scale(img_affine, axes(imgarray), output.dim = output.dim) + + # check equal + dimnames(img_scale) <- vector("list", length(dim(img_scale))) + expect_equal( + realize(imgarray_scale[[1]]), + img_scale + ) + + # translate + imgarray_trans <- translation(imgarray_scale, shift = shift) + + # check extent + expect_equal( + extent(imgarray_trans), + mapply(\(i,j) i+j, extent(imgarray_scale), shift, SIMPLIFY = FALSE) + ) + + # check subset + index <- list(x = 30:47, y = 53:58) + imgarray_subset <- crop(imgarray_trans, index = index) + img_subset <- test_subset(img_scale, axes(imgarray), index) + dimnames(img_subset) <- NULL + expect_equal( + realize(imgarray_subset[[1]]), + img_subset + ) + }) +} \ No newline at end of file diff --git a/vignettes/ImageArray.Rmd b/vignettes/ImageArray.Rmd index d229a37..6860bdf 100644 --- a/vignettes/ImageArray.Rmd +++ b/vignettes/ImageArray.Rmd @@ -239,6 +239,77 @@ imgarray <- createImageArray(ome.tiff.file, series = 1, resolution = 1) imgarray ``` +# Image Transformations + +```{r delayed} +ome.tiff.file <- system.file("extdata", "xy_12bit__plant.ome.tiff", + package = "ImageArray") +read.metadata(ome.tiff.file) + +# define ImageArray object +imgarray <- createImageArray(ome.tiff.file, series = 1, resolution = 1:2) +imgarray +``` + +```{r delayed_magick} +# 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") +imgarray2 <- writeImageArray(img, output = output_h5) +imgarray2 +``` + +## translation + +```{r translation} +imgarray_trans <- translation(imgarray2, + shift = c(100,20)) +``` + +## scale transformation + +```{r scale} +imgarray_scale <- scale(imgarray2, + output.dim = c(200,300), + filter = "bilinear") +``` + +## affine transformation + +```{r affine} +m <- matrix(c(1, -.5, 128, 0, 1, 0), nrow=3, ncol=2) +imgarray_affine <- affine(imgarray2, + m = m, + filter = "bilinear") +``` + +## sequence transformation + +```{r sequence} +m <- matrix(c(1, -.5, 128, 0, 1, 0), nrow=3, ncol=2) +imgarray_affine <- affine(imgarray2, + m = m, + filter = "bilinear") +imgarray_scale <- scale(imgarray_affine, + output.dim = c(200,300), + filter = "bilinear") +imgarray_trans <- translation(imgarray_scale, + shift = c(100,20)) +``` + +## rotate transformation + +```{r rotate_lazy} +imgarray_rotate <- rotate(imgarray, + angle = 45, + filter = "bilinear") +``` + # Use cases The delayed pyramid scheme introduced by `ImageArray` objects can also