Skip to content

Feat: add transform_to_rotated_box and box_xyxy_to_xyxyr#342

Open
DerrickUnleashed wants to merge 19 commits into
mlverse:mainfrom
DerrickUnleashed:feat/rotation-transform
Open

Feat: add transform_to_rotated_box and box_xyxy_to_xyxyr#342
DerrickUnleashed wants to merge 19 commits into
mlverse:mainfrom
DerrickUnleashed:feat/rotation-transform

Conversation

@DerrickUnleashed

@DerrickUnleashed DerrickUnleashed commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Summary

item_transform_bbox_rotate() converts image_with_bounding_box items to image_with_rotated_box with $boxes in xyxyr format. draw_bounding_boxes.image_with_rotated_box() renders them as rotated polygons.

Changes

R/ops-box_convert.Rbox_xyxy_to_xyxyr(boxes, angle) now rotates boxes around their center by the given angle (radians) and returns the enclosing axis-aligned box + angle column.

R/target_transform_detection.Ritem_transform_bbox_rotate(x, angle) S3 generic with methods for image_with_bounding_box and dataset.

R/vision_utils.R — new draw_bounding_boxes.image_with_rotated_box S3 method that draws rotated rectangles as polygons via graphics::polygon().

tests/testthat/test-ops-box_convert.R — 5 new tests for rotation (pi/2, pi/4, per-box angles, edge cases).

tests/testthat/test-target_transform_detection.R — 6 new tests for the item transform (basic, field preservation, empty, multiple, non-mutation, non-zero angle).

Closes #338

Convert axis-aligned xyxy bounding boxes to xyxyr format (with 0 rotation)
for rotated object detection pipelines. Includes an item-level transform
that works on image_with_bounding_box objects and detection datasets.
…to_xyxyr

- Fix broadcasting bug: cx/cy/hw/hh now reshaped to (n, 1) to match 2D angle
- Fix dim arg: use dim=-1 (last axis) instead of dim=2 for min/max on corners
- Fix dimension mismatch in torch_cat: reshape min/max results back to (n, 1)
- Use torch_cat instead of torch_stack for corners to avoid 3D tensor
- Handle empty boxes edge case (n=0) before any operations
- Handle scalar angle broadcast to multiple boxes via expand
- Accept per-box angle tensors
Draws rotated bounding boxes as polygons instead of axis-aligned rects.
Computes the 4 corners of the rotated rectangle from xyxyr format and
draws them with graphics::polygon.
@DerrickUnleashed

DerrickUnleashed commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author
url <- "https://upload.wikimedia.org/wikipedia/commons/9/9a/Aeroflot_Airbus_A330_Kustov.jpg"

img <- base_loader(url) |>
  transform_to_tensor() |>
  transform_resize(c(300, 500))

item <- list(
  x = img,
  y = list(
    boxes = torch_tensor(matrix(c(40, 95, 475, 180), ncol = 4), dtype = torch_float32()),
    labels = "airplane",
    image_height = 300L,
    image_width = 500L
  )
)
class(item) <- c("image_with_bounding_box", "list")

angle_deg <- 355  
angle_rad <- angle_deg * pi / 180

item_rot <- item_transform_bbox_rotate(item, angle = angle_rad)

before <- draw_bounding_boxes(item, colors = "blue", width = 4)
after <- draw_bounding_boxes(item_rot, colors = "red", width = 4)

grid <- vision_make_grid(torch_stack(list(before, after), dim = 1)$to(dtype = torch_float32()))
tensor_image_browse(grid)
filecb057dc1b55f
img <- torch_ones(3, 400, 400)  # white canvas

angles_deg <- seq(0, 340, by = 20)
angles_rad <- angles_deg * pi / 180
colors <- grDevices::hcl.colors(length(angles_rad), rev = TRUE)

base_box <- torch_tensor(matrix(c(180, 50, 220, 350), ncol = 4), dtype = torch_float32())

all_boxes <- torch_cat(lapply(angles_rad, function(a) {
  box_xyxy_to_xyxyr(base_box, angle = a)
}), dim = 1L)

all_labels <- paste0(angles_deg)

item <- list(
  x = img,
  y = list(
    boxes = all_boxes,
    labels = all_labels,
    image_height = 400L,
    image_width = 400L
  )
)
class(item) <- c("image_with_rotated_box", "list")

result <- draw_bounding_boxes(item, colors = colors, width = 2)
tensor_image_browse(result)
filecb057b36e147 filecb053f433e7d

@DerrickUnleashed DerrickUnleashed marked this pull request as ready for review July 10, 2026 18:19
@DerrickUnleashed

Copy link
Copy Markdown
Contributor Author

@cregouby , I just wanted to know if I'm going in the right direction.

@cregouby

cregouby commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

Hello @DerrickUnleashed,

This is the good direction, indeed, as a step toward implementing pytorch v2 transform.
Few remarks:
todo the spirit of item_transform_* is to have an item as input but also an item as output, because it will modify both $x and $y. So item_transform_bbox_rotate() should be named item_transform_rotate() and shall rotate both image and bboxes (like in the pytoch/vision vignette).
todo I made a mistake in the angle parameter : pytorch vision expect the angle in degrees (which is more accessible to human understanding) so could we switch back to angle in degrees, please ?
suggestion code organization We shouldn't mix target_transform and item_transform, so I would move all item_transform_bbox_* into a dedicated item_transforms_detection.R file
thought we may also using file organization that mimic the pytorch/vision code organization for v2 transforms and thus name it item_transform_geometry.R. Up to you to decide.

Comment thread tests/testthat/test-ops-box_convert.R Outdated
Comment thread tests/testthat/test-ops-box_convert.R Outdated
cregouby and others added 9 commits July 11, 2026 13:55
…egrees

Align with pytorch/vision convention where angles are expressed in
degrees for better human readability. Internally converts to radians
for trigonometric computations.
…rendering

The 5th column of xyxyr boxes is now in degrees after the preceding
refactor. Convert to radians internally for trigonometric rendering.
…mage and boxes

Rename item_transform_bbox_rotate() to item_transform_rotate() and move
to a dedicated item_transform_geometry.R file (aligning with pytorch/vision
file organization for geometric transforms).

The function now rotates both the image (via transform_rotate) and the
bounding boxes (via box_xyxy_to_xyr) together, matching the spirit of
item_transform_* which should modify both  and . Supports canvas
expansion via the expand parameter.

Angle parameter is in degrees (aligning with pytorch/vision convention).
The function converts bounding boxes to xyxyr format without modifying
the image tensor, matching the original item_transform_bbox_rotate
behavior.
@DerrickUnleashed

Copy link
Copy Markdown
Contributor Author
url <- "https://upload.wikimedia.org/wikipedia/commons/9/9a/Aeroflot_Airbus_A330_Kustov.jpg"

img <- base_loader(url) |>
  transform_to_tensor() |>
  transform_resize(c(300, 500))

item <- list(
  x = img,
  y = list(
    boxes = torch_tensor(matrix(c(40, 95, 475, 180), ncol = 4), dtype = torch_float32()),
    labels = "airplane",
    image_height = 300L,
    image_width = 500L
  )
)
class(item) <- c("image_with_bounding_box", "list")

item_rot <- item_transform_rotate(item, angle = 355)

before <- draw_bounding_boxes(item, colors = "blue", width = 4)
after <- draw_bounding_boxes(item_rot, colors = "red", width = 4)

grid <- vision_make_grid(torch_stack(list(before, after), dim = 1)$to(dtype = torch_float32()))
tensor_image_browse(grid)
fileda6e1c7025c4
img <- torch_ones(3, 400, 400)  # white canvas

angles_deg <- seq(0, 340, by = 5)
colors <- grDevices::hcl.colors(length(angles_deg), rev = TRUE)

base_box <- torch_tensor(matrix(c(180, 50, 220, 350), ncol = 4), dtype = torch_float32())

all_boxes <- torch_cat(lapply(angles_deg, function(a) {
  box_xyxy_to_xyxyr(base_box, angle = a)
}), dim = 1L)

all_labels <- paste0(angles_deg)

item <- list(
  x = img,
  y = list(
    boxes = all_boxes,
    labels = all_labels,
    image_height = 400L,
    image_width = 400L
  )
)
class(item) <- c("image_with_rotated_box", "list")

result <- draw_bounding_boxes(item, colors = colors, width = 2)
tensor_image_browse(result)
fileda6e22dc0b65

@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.

todo some small chrore

Comment thread tests/testthat/test-ops-box_convert.R Outdated
Comment thread tests/testthat/test-ops-box_convert.R Outdated
Comment thread tests/testthat/test-ops-box_convert.R Outdated
Comment thread tests/testthat/test-ops-box_convert.R Outdated
Comment thread tests/testthat/test-ops-box_convert.R Outdated
Comment thread tests/testthat/test-target_transform_detection.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.

todo Sorry I missed that : transform is intended for dataset augmentation, and shall rotate the image and the bbox, together, without bbox expansion.
This mean we have to use an additionnal box_rotate function and not the (to-be-kept) box_xyxy_to_xyxyr() function to rotate the box.

expect_equal_to_r(result$y$boxes[1, 5], 0)
})

test_that("item_transform_rotate does not modify the image", {

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 we DO expect the image to be modified !

expect_equal(torch::as_array(result$y$boxes[, 5]), c(0, 0, 0))
})

test_that("item_transform_rotate does not mutate input", {

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 we DO expect the image to be modified !

Comment on lines +6 to +7
#' \eqn{r} is the rotation angle in degrees (counter-clockwise). The image
#' is left unchanged. For axis-aligned boxes, \eqn{r = 0}.

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.


#' @export
item_transform_rotate.image_with_bounding_box <- function(x, angle = 0) {
x$y$boxes <- box_xyxy_to_xyxyr(x$y$boxes, angle = angle)

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.

question I guess the required rotation of the box is much simpler here, as there is no need to take the axis-aligned bounding box of the rotated corners..

@DerrickUnleashed

DerrickUnleashed commented Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

Apologies for the oversight I seem to have missed that the image is also modified that is rotated will implment this and update tests accordingly

Hey, @cregouby , I have some questions to make sure the implementation is alright.

https://developer.nvidia.com/blog/detecting-rotated-objects-using-the-odtk/ -> this just rotates the bbox

The current task is just to rotate the image ? Do we need to rotate the bbox relative to the object.

If we are gonna go by using PyTorch docs then do we utilise the cxcywhr format instead ?

@cregouby

cregouby commented Jul 13, 2026 via email

Copy link
Copy Markdown
Collaborator

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.

Add a item_transform_bbox_rotate() that rotate the boxes.

2 participants