diff --git a/NAMESPACE b/NAMESPACE index 9a148836..1c826167 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -48,6 +48,11 @@ S3method(item_transform_random_vertical_flip,default) S3method(item_transform_random_vertical_flip,image_with_bounding_box) S3method(item_transform_random_vertical_flip,image_with_rotated_box) S3method(item_transform_random_vertical_flip,image_with_segmentation_mask) +S3method(item_transform_resize,dataset) +S3method(item_transform_resize,default) +S3method(item_transform_resize,image_with_bounding_box) +S3method(item_transform_resize,image_with_rotated_box) +S3method(item_transform_resize,image_with_segmentation_mask) S3method(item_transform_rotate,dataset) S3method(item_transform_rotate,default) S3method(item_transform_rotate,image_with_bounding_box) @@ -179,6 +184,7 @@ export(item_transform_pad) export(item_transform_perspective) export(item_transform_random_horizontal_flip) export(item_transform_random_vertical_flip) +export(item_transform_resize) export(item_transform_rotate) export(item_transform_vflip) export(kmnist_dataset) diff --git a/NEWS.md b/NEWS.md index df7b58a1..d7c99ad5 100644 --- a/NEWS.md +++ b/NEWS.md @@ -23,6 +23,7 @@ * Added `item_transform_affine()` and `target_transform_affine()` for applying an affine transformation to a dataset item and its detection target, with support for detection and segmentation items and datasets (#367). * Added `item_transform_center_crop()` for cropping images from the center to a specified size for dataset items, with support for detection and segmentation item types and datasets (@DerrickUnleashed, #370). * Added `item_transform_random_horizontal_flip()` and `item_transform_random_vertical_flip()` for random geometric augmentation of dataset items with probability `p`, with support for detection and segmentation item types and datasets (@DerrickUnleashed, #381). +* Added `item_transform_resize()` for resizing dataset items, with support for detection and segmentation item types and datasets (@srishtiii28, #362). * `item_transform_rotate()` now supports segmentation items and datasets, rotating the masks alongside the image (@srishtiii28, #379). * Added `item_transform_crop()` for cropping dataset items at a specified location and size, with support for detection and segmentation item types and datasets (@DerrickUnleashed, #371). * Added `item_transform_pad()` for padding dataset items on all sides, with support for detection, segmentation and rotated-box item types and datasets (@DerrickUnleashed, #373). diff --git a/R/item-transforms-geometry.R b/R/item-transforms-geometry.R index 1a2f7dbe..b3c53481 100644 --- a/R/item-transforms-geometry.R +++ b/R/item-transforms-geometry.R @@ -1146,3 +1146,106 @@ item_transform_perspective.image_with_segmentation_mask <- function(x, startpoin x } +# item_transform_resize + +#' Resize a dataset item +#' +#' Resizes the image inside a dataset item. For detection items, bounding box +#' coordinates are rescaled by the same factors as the image. For segmentation +#' items, the masks are resized alongside the image with nearest-neighbour +#' sampling, so that they keep their discrete values. +#' +#' @param x A dataset item, typically an \code{image_with_bounding_box} or +#' \code{image_with_segmentation_mask} object containing an image tensor +#' and associated target data. +#' @inheritParams transform_resize +#' +#' @return A dataset item of the same class with the image and target resized. +#' +#' @examples +#' \dontrun{ +#' url <- "https://upload.wikimedia.org/wikipedia/commons/b/b6/Felis_catus-cat_on_snow.jpg" +#' img <- base_loader(url) |> transform_to_tensor() +#' +#' boxes <- torch_tensor(matrix(c(600, 200, 2880, 1860), ncol = 4), dtype = torch_float32()) +#' +#' before <- list(x = img, y = list(boxes = boxes, labels = "cat")) +#' class(before) <- c("image_with_bounding_box", "list") +#' +#' after <- item_transform_resize(before, size = c(600, 800)) +#' +#' before_plot <- draw_bounding_boxes(before, colors = "blue", width = 10) +#' after_plot <- draw_bounding_boxes(after, colors = "red", width = 10) +#' tensor_image_browse(before_plot) +#' tensor_image_browse(after_plot) +#' } +#' +#' @family item_unitary_transforms +#' +#' @export +item_transform_resize <- function(x, size, interpolation = 2) { + UseMethod("item_transform_resize", x) +} + +#' @export +item_transform_resize.default <- function(x, size, interpolation = 2) { + cli_abort( + "{.fn item_transform_resize} requires a dataset item (a list with {.var x} and {.var y} fields), not {.obj_type_friendly {x}}. + To resize a raw image tensor, use {.fn transform_resize} instead." + ) +} + +#' @export +item_transform_resize.dataset <- function(x, size, interpolation = 2) { + original_getitem <- x$.getitem + unlockBinding(".getitem", as.environment(x)) + x$.getitem <- function(index) { + item <- original_getitem(index) + item_transform_resize(item, size = size, interpolation = interpolation) + } + x +} + +#' @export +item_transform_resize.image_with_bounding_box <- function(x, size, interpolation = 2) { + orig_spatial <- tail(x$x$shape, 2) + + x$x <- transform_resize(x$x, size, interpolation) + + new_spatial <- tail(x$x$shape, 2) + scale_h <- new_spatial[1] / orig_spatial[1] + scale_w <- new_spatial[2] / orig_spatial[2] + + boxes <- x$y$boxes$clone() + if (boxes$size(1) > 0) { + boxes[, 1] <- boxes[, 1] * scale_w + boxes[, 2] <- boxes[, 2] * scale_h + boxes[, 3] <- boxes[, 3] * scale_w + boxes[, 4] <- boxes[, 4] * scale_h + } + x$y$boxes <- boxes + x$y$image_height <- new_spatial[1] + x$y$image_width <- new_spatial[2] + + x +} + +#' @export +item_transform_resize.image_with_segmentation_mask <- function(x, size, interpolation = 2) { + x$x <- transform_resize(x$x, size, interpolation) + + new_spatial <- tail(x$x$shape, 2) + masks <- x$y$masks + x$y$masks <- if (masks$size(1) > 0) { + transform_resize(masks, new_spatial, interpolation = 0) + } else { + torch_zeros(c(0, new_spatial), dtype = masks$dtype, device = masks$device) + } + x$y$image_height <- new_spatial[1] + x$y$image_width <- new_spatial[2] + + x +} + +#' @export +item_transform_resize.image_with_rotated_box <- item_transform_resize.image_with_bounding_box diff --git a/man/item_transform_affine.Rd b/man/item_transform_affine.Rd index 9af56432..32b0f18b 100644 --- a/man/item_transform_affine.Rd +++ b/man/item_transform_affine.Rd @@ -87,6 +87,7 @@ Other item_unitary_transforms: \code{\link[=item_transform_hflip]{item_transform_hflip()}}, \code{\link[=item_transform_pad]{item_transform_pad()}}, \code{\link[=item_transform_perspective]{item_transform_perspective()}}, +\code{\link[=item_transform_resize]{item_transform_resize()}}, \code{\link[=item_transform_rotate]{item_transform_rotate()}}, \code{\link[=item_transform_vflip]{item_transform_vflip()}} } diff --git a/man/item_transform_center_crop.Rd b/man/item_transform_center_crop.Rd index 71fb1d03..80e6bb08 100644 --- a/man/item_transform_center_crop.Rd +++ b/man/item_transform_center_crop.Rd @@ -57,6 +57,7 @@ Other item_unitary_transforms: \code{\link[=item_transform_hflip]{item_transform_hflip()}}, \code{\link[=item_transform_pad]{item_transform_pad()}}, \code{\link[=item_transform_perspective]{item_transform_perspective()}}, +\code{\link[=item_transform_resize]{item_transform_resize()}}, \code{\link[=item_transform_rotate]{item_transform_rotate()}}, \code{\link[=item_transform_vflip]{item_transform_vflip()}} } diff --git a/man/item_transform_crop.Rd b/man/item_transform_crop.Rd index b8e5a4a5..33fce70c 100644 --- a/man/item_transform_crop.Rd +++ b/man/item_transform_crop.Rd @@ -63,6 +63,7 @@ Other item_unitary_transforms: \code{\link[=item_transform_hflip]{item_transform_hflip()}}, \code{\link[=item_transform_pad]{item_transform_pad()}}, \code{\link[=item_transform_perspective]{item_transform_perspective()}}, +\code{\link[=item_transform_resize]{item_transform_resize()}}, \code{\link[=item_transform_rotate]{item_transform_rotate()}}, \code{\link[=item_transform_vflip]{item_transform_vflip()}} } diff --git a/man/item_transform_hflip.Rd b/man/item_transform_hflip.Rd index 65a3f0da..65b2b11f 100644 --- a/man/item_transform_hflip.Rd +++ b/man/item_transform_hflip.Rd @@ -47,6 +47,7 @@ Other item_unitary_transforms: \code{\link[=item_transform_crop]{item_transform_crop()}}, \code{\link[=item_transform_pad]{item_transform_pad()}}, \code{\link[=item_transform_perspective]{item_transform_perspective()}}, +\code{\link[=item_transform_resize]{item_transform_resize()}}, \code{\link[=item_transform_rotate]{item_transform_rotate()}}, \code{\link[=item_transform_vflip]{item_transform_vflip()}} } diff --git a/man/item_transform_pad.Rd b/man/item_transform_pad.Rd index f875e78d..074927fe 100644 --- a/man/item_transform_pad.Rd +++ b/man/item_transform_pad.Rd @@ -77,6 +77,7 @@ Other item_unitary_transforms: \code{\link[=item_transform_crop]{item_transform_crop()}}, \code{\link[=item_transform_hflip]{item_transform_hflip()}}, \code{\link[=item_transform_perspective]{item_transform_perspective()}}, +\code{\link[=item_transform_resize]{item_transform_resize()}}, \code{\link[=item_transform_rotate]{item_transform_rotate()}}, \code{\link[=item_transform_vflip]{item_transform_vflip()}} } diff --git a/man/item_transform_perspective.Rd b/man/item_transform_perspective.Rd index a94e0a31..d41dfc7d 100644 --- a/man/item_transform_perspective.Rd +++ b/man/item_transform_perspective.Rd @@ -77,6 +77,7 @@ Other item_unitary_transforms: \code{\link[=item_transform_crop]{item_transform_crop()}}, \code{\link[=item_transform_hflip]{item_transform_hflip()}}, \code{\link[=item_transform_pad]{item_transform_pad()}}, +\code{\link[=item_transform_resize]{item_transform_resize()}}, \code{\link[=item_transform_rotate]{item_transform_rotate()}}, \code{\link[=item_transform_vflip]{item_transform_vflip()}} } diff --git a/man/item_transform_resize.Rd b/man/item_transform_resize.Rd new file mode 100644 index 00000000..4bf43571 --- /dev/null +++ b/man/item_transform_resize.Rd @@ -0,0 +1,63 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/item-transforms-geometry.R +\name{item_transform_resize} +\alias{item_transform_resize} +\title{Resize a dataset item} +\usage{ +item_transform_resize(x, size, interpolation = 2) +} +\arguments{ +\item{x}{A dataset item, typically an \code{image_with_bounding_box} or +\code{image_with_segmentation_mask} object containing an image tensor +and associated target data.} + +\item{size}{(integer vector or integer): Desired output size. If \code{size} is +an integer vector of length 2 like \code{c(h, w)}, output size will be matched +to this. If \code{size} is a bare integer, smaller edge of the image will be +matched to this number, i.e., if height > width, then image will be +rescaled to \verb{(size * height / width, size)}.} + +\item{interpolation}{(integer, optional): Desired interpolation. An integer +\code{0 = nearest}, \code{2 = bilinear}, and \code{3 = bicubic} or a name from +\code{\link[magick:filter_types]{magick::filter_types()}}.} +} +\value{ +A dataset item of the same class with the image and target resized. +} +\description{ +Resizes the image inside a dataset item. For detection items, bounding box +coordinates are rescaled by the same factors as the image. For segmentation +items, the masks are resized alongside the image with nearest-neighbour +sampling, so that they keep their discrete values. +} +\examples{ +\dontrun{ +url <- "https://upload.wikimedia.org/wikipedia/commons/b/b6/Felis_catus-cat_on_snow.jpg" +img <- base_loader(url) |> transform_to_tensor() + +boxes <- torch_tensor(matrix(c(600, 200, 2880, 1860), ncol = 4), dtype = torch_float32()) + +before <- list(x = img, y = list(boxes = boxes, labels = "cat")) +class(before) <- c("image_with_bounding_box", "list") + +after <- item_transform_resize(before, size = c(600, 800)) + +before_plot <- draw_bounding_boxes(before, colors = "blue", width = 10) +after_plot <- draw_bounding_boxes(after, colors = "red", width = 10) +tensor_image_browse(before_plot) +tensor_image_browse(after_plot) +} + +} +\seealso{ +Other item_unitary_transforms: +\code{\link[=item_transform_affine]{item_transform_affine()}}, +\code{\link[=item_transform_center_crop]{item_transform_center_crop()}}, +\code{\link[=item_transform_crop]{item_transform_crop()}}, +\code{\link[=item_transform_hflip]{item_transform_hflip()}}, +\code{\link[=item_transform_pad]{item_transform_pad()}}, +\code{\link[=item_transform_perspective]{item_transform_perspective()}}, +\code{\link[=item_transform_rotate]{item_transform_rotate()}}, +\code{\link[=item_transform_vflip]{item_transform_vflip()}} +} +\concept{item_unitary_transforms} diff --git a/man/item_transform_rotate.Rd b/man/item_transform_rotate.Rd index aad2d6d7..6afd79b1 100644 --- a/man/item_transform_rotate.Rd +++ b/man/item_transform_rotate.Rd @@ -72,6 +72,7 @@ Other item_unitary_transforms: \code{\link[=item_transform_hflip]{item_transform_hflip()}}, \code{\link[=item_transform_pad]{item_transform_pad()}}, \code{\link[=item_transform_perspective]{item_transform_perspective()}}, +\code{\link[=item_transform_resize]{item_transform_resize()}}, \code{\link[=item_transform_vflip]{item_transform_vflip()}} } \concept{item_unitary_transforms} diff --git a/man/item_transform_vflip.Rd b/man/item_transform_vflip.Rd index 77943a4c..fafc26f7 100644 --- a/man/item_transform_vflip.Rd +++ b/man/item_transform_vflip.Rd @@ -48,6 +48,7 @@ Other item_unitary_transforms: \code{\link[=item_transform_hflip]{item_transform_hflip()}}, \code{\link[=item_transform_pad]{item_transform_pad()}}, \code{\link[=item_transform_perspective]{item_transform_perspective()}}, +\code{\link[=item_transform_resize]{item_transform_resize()}}, \code{\link[=item_transform_rotate]{item_transform_rotate()}} } \concept{item_unitary_transforms} diff --git a/tests/testthat/test-item-transforms-geometry.R b/tests/testthat/test-item-transforms-geometry.R index 53f0f8c2..50d79007 100644 --- a/tests/testthat/test-item-transforms-geometry.R +++ b/tests/testthat/test-item-transforms-geometry.R @@ -1514,3 +1514,185 @@ test_that("item_transform_perspective applies to datasets", { expect_equal_to_r(item2$y$boxes[1, 4], 35) }) +# item_transform_resize + +test_that("item_transform_resize rejects non-item inputs", { + img <- torch_randn(3, 100, 200) + expect_error( + item_transform_resize(img, size = c(50, 100)), + "requires a dataset item" + ) + expect_error( + item_transform_resize(42, size = c(50, 100)), + "requires a dataset item" + ) +}) + +test_that("item_transform_resize resizes detection items and rescales boxes", { + boxes <- matrix(c(120, 70, 180, 130), ncol = 4) + item <- make_detection_item(boxes, image_size = c(200L, 400L)) + original_img <- item$x$clone() + original_class <- class(item) + + result <- item_transform_resize(item, size = c(100L, 200L)) + + expect_s3_class(result, "image_with_bounding_box") + expect_tensor_dtype(result$x, item$x$dtype) + expect_tensor_shape(result$x, c(3, 100, 200)) + expect_equal(result$y$image_height, 100L) + expect_equal(result$y$image_width, 200L) + expect_true(torch_equal(result$x, transform_resize(original_img, size = c(100L, 200L)))) + expect_equal_to_r(result$y$boxes[1, ], boxes[1, ] / 2) + + # a bare integer size matches the smaller edge + bare <- item_transform_resize(item, size = 100) + expect_true(torch_equal(bare$x, result$x)) + expect_true(torch_equal(bare$y$boxes, result$y$boxes)) + + # input is not mutated + expect_equal_to_r(item$x, as_array(original_img)) + expect_equal_to_r(item$y$boxes, boxes) + expect_equal(class(item), original_class) +}) + +test_that("item_transform_resize rescales boxes with an anisotropic size", { + boxes <- matrix(c( + 120, 70, 180, 130, + 0, 0, 400, 200 + ), ncol = 4, byrow = TRUE) + item <- make_detection_item(boxes, image_size = c(200L, 400L)) + + result <- item_transform_resize(item, size = c(100L, 100L)) + + expect_tensor_shape(result$x, c(3, 100, 100)) + expect_tensor_shape(result$y$boxes, c(2, 4)) + expect_equal_to_r(result$y$boxes[1, ], c(30, 35, 45, 65)) + expect_equal_to_r(result$y$boxes[2, ], c(0, 0, 100, 100)) +}) + +test_that("item_transform_resize preserves labels and handles empty boxes", { + labels <- torch_tensor(c(1L, 2L), dtype = torch_long()) + item <- make_detection_item( + matrix(c(120, 70, 180, 130, 210, 80, 280, 120), ncol = 4, byrow = TRUE), + labels = labels, + image_size = c(200L, 400L) + ) + result <- item_transform_resize(item, size = c(100L, 200L)) + + expect_equal_to_r(result$y$labels, as.integer(as_array(labels))) + + item <- make_detection_item( + boxes = matrix(numeric(0), ncol = 4), + labels = torch_zeros(0L, dtype = torch_long()) + ) + result <- item_transform_resize(item, size = c(50L, 100L)) + + expect_tensor_shape(result$y$boxes, c(0, 4)) + expect_tensor_dtype(result$y$boxes, torch_float()) +}) + +test_that("item_transform_resize resizes segmentation items", { + item <- make_segmentation_item(image_size = c(200L, 400L), num_masks = 2L) + original_img <- item$x$clone() + original_masks <- item$y$masks$clone() + original_labels <- as.integer(as_array(item$y$labels)) + + result <- item_transform_resize(item, size = c(100L, 200L)) + + expect_s3_class(result, "image_with_segmentation_mask") + expect_tensor_dtype(result$x, item$x$dtype) + expect_tensor_shape(result$x, c(3, 100, 200)) + expect_equal(result$y$image_height, 100L) + expect_equal(result$y$image_width, 200L) + expect_equal_to_r(result$y$labels, original_labels) + expect_true(torch_equal(result$x, transform_resize(original_img, size = c(100L, 200L)))) + + expected_masks <- transform_resize(original_masks, size = c(100L, 200L), interpolation = 0) + expect_tensor_shape(result$y$masks, c(2, 100, 200)) + expect_tensor_dtype(result$y$masks, torch_bool()) + expect_true(result$y$masks$equal(expected_masks)) +}) + +test_that("item_transform_resize handles items without any mask", { + item <- make_segmentation_item(image_size = c(200L, 400L), num_masks = 0L) + result <- item_transform_resize(item, size = c(100L, 200L)) + + expect_tensor_shape(result$y$masks, c(0, 100, 200)) + expect_tensor_dtype(result$y$masks, torch_bool()) +}) + +test_that("item_transform_resize keeps masks aligned with the image for a bare integer size", { + item <- make_segmentation_item(image_size = c(101L, 199L), num_masks = 2L) + result <- item_transform_resize(item, size = 50) + + expect_equal(result$y$masks$shape[2], result$x$shape[2]) + expect_equal(result$y$masks$shape[3], result$x$shape[3]) + expect_equal(result$y$image_height, result$x$shape[2]) + expect_equal(result$y$image_width, result$x$shape[3]) +}) + +test_that("item_transform_resize handles rotated boxes", { + boxes <- matrix(c(120, 70, 180, 130), ncol = 4) + item <- make_detection_item(boxes, image_size = c(200L, 400L)) + rotated <- item_transform_rotate(item, angle = 30) + original_angle <- as_array(rotated$y$boxes[1, 5]) + + result <- item_transform_resize(rotated, size = c(100L, 200L)) + + expect_s3_class(result, "image_with_rotated_box") + expect_tensor_shape(result$y$boxes, c(1, 5)) + expect_tensor_dtype(result$y$boxes, torch_float()) + expect_equal_to_r(result$y$boxes[1, 5], original_angle) +}) + +test_that("item_transform_resize can be composed", { + boxes <- matrix(c(120, 70, 180, 130, 210, 80, 280, 120), ncol = 4, byrow = TRUE) + labels <- torch_tensor(c(1L, 2L), dtype = torch_long()) + item <- make_detection_item(boxes, labels = labels, image_size = c(200L, 400L)) + + result <- item |> + item_transform_resize(size = c(100L, 200L)) |> + item_transform_hflip() + + expect_s3_class(result, "image_with_bounding_box") + expect_tensor_shape(result$x, c(3, 100, 200)) + expect_equal_to_r(result$y$labels, as.integer(as_array(labels))) + expect_equal_to_r(result$y$boxes[1, ], c(200 - 90, 35, 200 - 60, 65)) + expect_equal_to_r(result$y$boxes[2, ], c(200 - 140, 40, 200 - 105, 60)) +}) + +test_that("item_transform_resize works on detection and segmentation datasets", { + ds <- dataset( + name = "toy_detection", + initialize = function() {}, + .getitem = function(index) { + make_detection_item(matrix(c(120, 70, 180, 130), ncol = 4), image_size = c(200L, 400L)) + }, + .length = function() 1L + )() + + ds <- item_transform_resize(ds, size = c(100L, 200L)) + item <- ds$.getitem(1) + + expect_s3_class(item, "image_with_bounding_box") + expect_tensor_shape(item$x, c(3, 100, 200)) + expect_equal_to_r(item$y$boxes[1, ], c(60, 35, 90, 65)) + expect_equal(item$y$image_height, 100L) + expect_equal(item$y$image_width, 200L) + + ds <- dataset( + name = "toy_segmentation", + initialize = function() {}, + .getitem = function(index) { + make_segmentation_item(image_size = c(200L, 400L), num_masks = 2L) + }, + .length = function() 1L + )() + + ds <- item_transform_resize(ds, size = c(100L, 200L)) + item <- ds$.getitem(1) + + expect_s3_class(item, "image_with_segmentation_mask") + expect_tensor_shape(item$x, c(3, 100, 200)) + expect_tensor_shape(item$y$masks, c(2, 100, 200)) +})