Skip to content

Feat: Add SAHI (Slicing Aided Hyper Inference) Transform#329

Open
DerrickUnleashed wants to merge 71 commits into
mlverse:mainfrom
DerrickUnleashed:feat/sahiTransform
Open

Feat: Add SAHI (Slicing Aided Hyper Inference) Transform#329
DerrickUnleashed wants to merge 71 commits into
mlverse:mainfrom
DerrickUnleashed:feat/sahiTransform

Conversation

@DerrickUnleashed

@DerrickUnleashed DerrickUnleashed commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

This PR Adds:

  • The implementation of transform_sahi_crop()
  • The implementation of target_transform_sahi_crop()
  • Adds test suites for the same
  • Adds export command to fix this error
transforms-tensor.R:480: S3 method
  `get_image_size.torch_tensor` needs
  @export or @exportS3Method tag.

@DerrickUnleashed DerrickUnleashed marked this pull request as ready for review June 11, 2026 20:02
@DerrickUnleashed

Copy link
Copy Markdown
Contributor Author

@cregouby, I'm still a bit unclear about the usage of target_transform_sahi_crop() However, the transform_sahi_crop() seems to be working as expected

@cregouby cregouby left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise I think that the cropping values logic is there.
todo design all transform_ functions shall return a object of the same type as the input (except transform to tensor) as they will be piped. Your only freedom is to change the shape of the tensor, and move the crops into a sequential batch, as transform_five_crop() do.
ex for x$shape [1, 3, 10, 10], transform_sahi_crop(x, c(4,4) , c(.2, .2)) shall output a tensor of shape [25, 3, 3, 3] (see

test_that("five_crop", {
x <- torch_randn(3, 10, 12)
o <- transform_five_crop(x, c(3, 3))
expect_length(o, 5)
expect_tensor_shape(o[[1]], c(3,3,3))
ob <- transform_five_crop(x$unsqueeze(1), c(3, 3))
expect_length(ob, 5)
expect_tensor_shape(ob[[1]], c(1,3,3,3))
})
)
suggestion you should rely on the existing crop function to crop the tensor, saving you a lot of unit tests.
todo software design this transform shall be an S3 transform so shall have an entry in transform-generics.R for the dispatch, one in transforms-defaults.R, one in transforms-magick.R, one in transforms-tensor.R, as you never know what transform goes before, what transform is piped after.

todo code relocation Please keep (the badly named, my fault) transform-segmentation.R file for target_transforms
suggestion for clarity, you may rename the transform-segmentation.R into target-transform-segmentation.R (and the test file accordingly)

thought as SAHI intimately rely on input image size and coco bbox (so both x and y at the same time), and as there is no way to pass information from transform_sahi_crop to target_transform_sahi_crop, I think maybe a prepare_sahi_crop(dataset, size, overlap_size_ratio, ... ) function gathering all the needed context into a specific class object sahi_preparation and having both functions using it a the only input parameter transform_sahi_crop(x, sahi_preparation) and target_transform_sahi_crop(x, sahi_preparation) could be a nice API to SAHI.

Comment thread .vscode/settings.json Outdated
Comment thread R/transforms-tensor.R
Comment thread R/transforms-segmentation.R Outdated
Comment thread R/transforms-segmentation.R Outdated
@DerrickUnleashed

Copy link
Copy Markdown
Contributor Author

todo software design this transform shall be an S3 transform so shall have an entry in transform-generics.R for the dispatch, one in transforms-defaults.R, one in transforms-magick.R, one in transforms-tensor.R, as you never know what transform goes before, what transform is piped after.

I didn't understand properly, Do I need to move the code of transform_sahi and target_transform_sahi to transform-generics ?

@cregouby

cregouby commented Jun 15, 2026 via email

Copy link
Copy Markdown
Collaborator

Comment thread tests/testthat/test-transforms-array.R Outdated
Comment thread tests/testthat/test-transforms-tensor.R
Comment thread tests/testthat/test-transforms-tensor.R Outdated

@cregouby cregouby left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise thanks for your modifications.
todo a small effort again for completness.

Comment thread tests/testthat/test-target-transforms-segmentation.R

@cregouby cregouby left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise thanks for those improvements.
todo I've add the proper output data model in here : #324 (comment). Sorry for doing that so late, but I think this is the only way to have target_transform_sahi_crop transparent to upstream target transform (like the young target_transform_resize) and eventually downstream.

Comment thread tests/testthat/test-transforms-array.R Outdated
Comment thread tests/testthat/test-transforms-magick.R
Comment thread tests/testthat/test-transforms-tensor.R Outdated
@cregouby

cregouby commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

@DerrickUnleashed ,
I've merge the latest addition in main so that you don't have to worry about file names and functions family.
You will now find target_transform_sahi_crop() into target_transform_detection.R
Hope it helps.

@DerrickUnleashed

Copy link
Copy Markdown
Contributor Author

@DerrickUnleashed , I've merge the latest addition in main so that you don't have to worry about file names and functions family. You will now find target_transform_sahi_crop() into target_transform_detection.R Hope it helps.

Awesome thanks for this

Comment thread R/target_transform_detection.R Outdated
Comment on lines +203 to +311
#' @rdname transform_sahi_crop
#' @family target_transforms_detection
#' @export
target_transform_sahi_crop <- function(y, sahi_split, min_area_ratio = 0.1) {

# Detect batch input: list of target lists
if (is.list(y) && !"boxes" %in% names(y)) {
if (is.list(sahi_split) && !inherits(sahi_split, "sahi_split")) {
return(Map(function(yi, sp) target_transform_sahi_crop(yi, sp, min_area_ratio),
y, sahi_split))
}
return(lapply(y, function(yi) target_transform_sahi_crop(yi, sahi_split, min_area_ratio)))
}

boxes <- y$boxes
labels <- y$labels
labels_is_tensor <- inherits(labels, "torch_tensor")

n <- boxes$size(1)

crop_windows <- sahi_split$crop_windows

shape_y <- function(n_boxes, boxes_dtype, labels_val, labels_dtype, crop_h, crop_w) {
out <- y
out$boxes <- torch_zeros(c(n_boxes, 4), dtype = boxes_dtype)
if (labels_is_tensor)
out$labels <- torch_tensor(labels_val, dtype = labels_dtype)
else
out$labels <- labels_val
if (!is.null(y$area))
out$area <- torch_zeros(n_boxes, dtype = y$area$dtype)
if (!is.null(y$image_height))
out$image_height <- crop_h
if (!is.null(y$image_width))
out$image_width <- crop_w
out$iscrowd <- NULL
out
}

results <- lapply(crop_windows, function(cw) {

# Convert 1-based crop window coordinates to 0-based for box clipping
top <- cw$top - 1
left <- cw$left - 1
crop_h <- cw$height
crop_w <- cw$width

if (n == 0) {
labels_val <- if (labels_is_tensor) integer(0) else vector(typeof(labels), 0)
labels_dtype <- if (labels_is_tensor) labels$dtype else NULL
return(shape_y(0, boxes$dtype, labels_val, labels_dtype, crop_h, crop_w))
}

x1 <- boxes[, 1]
y1 <- boxes[, 2]
x2 <- boxes[, 3]
y2 <- boxes[, 4]

orig_area <- (x2 - x1) * (y2 - y1)

x1_clip <- torch_clamp(x1, min = left, max = left + crop_w)
y1_clip <- torch_clamp(y1, min = top, max = top + crop_h)
x2_clip <- torch_clamp(x2, min = left, max = left + crop_w)
y2_clip <- torch_clamp(y2, min = top, max = top + crop_h)

keep_w <- x2_clip - x1_clip
keep_h <- y2_clip - y1_clip
keep_area <- keep_w * keep_h

mask <- (keep_area > 0) & ((keep_area / orig_area) >= min_area_ratio)

mask_idx <- which(as.logical(mask))

if (length(mask_idx) == 0) {
labels_val <- if (labels_is_tensor) integer(0) else vector(typeof(labels), 0)
labels_dtype <- if (labels_is_tensor) labels$dtype else NULL
return(shape_y(0, boxes$dtype, labels_val, labels_dtype, crop_h, crop_w))
}

n_keep <- length(mask_idx)
new_boxes <- torch_zeros(n_keep, 4, dtype = boxes$dtype)
new_boxes[, 1] <- x1_clip[mask_idx] - left
new_boxes[, 2] <- y1_clip[mask_idx] - top
new_boxes[, 3] <- x2_clip[mask_idx] - left
new_boxes[, 4] <- y2_clip[mask_idx] - top

out_y <- y
out_y$boxes <- new_boxes

if (labels_is_tensor)
out_y$labels <- labels[mask_idx]
else
out_y$labels <- labels[mask_idx]

if (!is.null(y$area))
out_y$area <- keep_area[mask_idx]

if (!is.null(y$image_height))
out_y$image_height <- crop_h
if (!is.null(y$image_width))
out_y$image_width <- crop_w

out_y$iscrowd <- NULL

out_y
})

results
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo code duplicate. Please remove

if (!is.null(y$image_width))
out_y$image_width <- crop_w

out_y$iscrowd <- NULL

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
out_y$iscrowd <- NULL
out_y$iscrowd <- y$iscrowd[mask_idx]

Comment thread R/sahi-split.R
Comment on lines +35 to +53
#' @export
prepare_sahi_split.dataset <- function(x, size = c(512L, 512L), overlap_size_ratio = c(0.2, 0.2)) {
meta <- x$image_metadata
if (!is.null(meta) && is.list(meta)) {
first <- meta[[1]]
image_height <- first$height
image_width <- first$width
if (!is.null(image_height) && !is.null(image_width))
return(compute_sahi_split(image_height, image_width, size, overlap_size_ratio))
}
item <- x$.getitem(1)
im <- item$x
if (inherits(im, "torch_tensor")) {
image_height <- im$size(-2)
image_width <- im$size(-1)
} else if (inherits(im, "array")) {
image_height <- dim(im)[1]
image_width <- dim(im)[2]
} else {

@cregouby cregouby Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo ordering logic dataset image metadata shall not be the first way to retrieve image shape, but the last resort one. This is because usual (coco) dataset have varying image size and will be first resized by end-user like via transform = transform_resize() or via target_transform = target_transform_resize(), or both if it is unclear witch is considered. The first one will change the item$x image size, the second will change the item$y image_wdth and image_heigh metadata values. Those are the primary value to consider for actual size.
Typical workflow with dataset :

ds <- coco_detection_dataset(
  train=FALSE, year = "2017", download = TRUE,
  transform = . %>%
   transform_to_tensor() %>%
   transform_resize(c(500, 400)),
  target_transform = . %>% target_transform_resize(c(500, 400))
  )

item <- ds[1]

sahi_prep <- prepare_sahi_split(ds, size = c(200L, 200L), overlap_size_ratio = c(0.2, 0.2)) 

ds_sahi <- coco_detection_dataset(
  train=FALSE, year = "2017", download = FALSE,
  transform = . %>%
   transform_to_tensor() %>%
   transform_resize(c(500, 400)) %>%
   transform_sahi_crop(sahi_prep),
  target_transform = . %>%
   target_transform_resize(c(500, 400)) %>%
   target_transform_sahi_crop(sahi_prep, min_area_ratio = 0.2) 
)

This currently fails with

Erreur : stack expects each tensor to be equal size, but got [3, 200, 200] at entry 0 and [3, 200, 80] at entry 2
Exception raised from check_stack_inputs at /pytorch/aten/src/ATen/native/TensorShape.cpp:3345 (most recent call first):

suggestion Add this workflow as a documentation exemple

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo missing Are all the created bbox xyxy ?
suggestion use expect_bbox_is_xyxy(out[[1]]$boxes) extensively

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement SAHI: (Slicing Aided Hyper Inference) as both transform_ and target_transform_

2 participants