Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Boxes #40

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions R/extension.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
has_ops <- function() {
not_implemented_error("has_ops() Not implemented yet. https://github.com/pytorch/vision/blob/b266c2f1a5c10f5caf22f5aea7418acc392a5075/torchvision/extension.py#L60")
}

assert_has_ops <- function() {
if(!has_ops()) {
runtime_error(
"Couldn't load custom C++ ops. This can happen if your torch and torchvision
versions are incompatible, or if you had errors while compiling torchvision
from source. Please reinstall torchvision so that it matches your torch install."
)
}
}
74 changes: 74 additions & 0 deletions R/ops-box_convert.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#' box_cxcywh_to_xyxy
#'
#' Converts bounding boxes from (cx, cy, w, h) format to (x1, y1, x2, y2) format.
#' (cx, cy) refers to center of bounding box
#' (w, h) are width and height of bounding box
#'
#' @param boxes (Tensor[N, 4]): boxes in (cx, cy, w, h) format which will be converted.
#'
#' @return boxes (Tensor(N, 4)): boxes in (x1, y1, x2, y2) format.
box_cxcywh_to_xyxy <- function(boxes) {
# We need to change all 4 of them so some temporary variable is needed.
c(cx, cy, w, h) %<-% boxes$unbind(-1)
x1 = cx - 0.5 * w
y1 = cy - 0.5 * h
x2 = cx + 0.5 * w
y2 = cy + 0.5 * h

boxes = torch::torch_stack(list(x1, y1, x2, y2), dim=-1)

return(boxes)
}

#' box_xyxy_to_cxcywh
#'
#' Converts bounding boxes from (x1, y1, x2, y2) format to (cx, cy, w, h) format.
#' (x1, y1) refer to top left of bounding box
#' (x2, y2) refer to bottom right of bounding box
#'
#' @param boxes (Tensor[N, 4]): boxes in (x1, y1, x2, y2) format which will be converted.
#'
#' @return boxes (Tensor(N, 4)): boxes in (cx, cy, w, h) format.
box_xyxy_to_cxcywh <- function(boxes) {
c(x1, y1, x2, y2) %<-% boxes$unbind(-1)
cx = (x1 + x2) / 2
cy = (y1 + y2) / 2
w = x2 - x1
h = y2 - y1

boxes = torch::torch_stack(list(cx, cy, w, h), dim=-1)

return(boxes)
}

#' box_xywh_to_xyxy
#'
#' Converts bounding boxes from (x, y, w, h) format to (x1, y1, x2, y2) format.
#' (x, y) refers to top left of bouding box.
#' (w, h) refers to width and height of box.
#'
#' @param boxes (Tensor[N, 4]): boxes in (x, y, w, h) which will be converted.
#'
#' @return boxes (Tensor[N, 4]): boxes in (x1, y1, x2, y2) format.
box_xywh_to_xyxy <- function(boxes) {
c(x, y, w, h) %<-% boxes$unbind(-1)
boxes = torch::torch_stack(list(x, y, x + w, y + h), dim=-1)
return(boxes)
}

#' box_xyxy_to_xywh
#'
#' Converts bounding boxes from (x1, y1, x2, y2) format to (x, y, w, h) format.
#' (x1, y1) refer to top left of bounding box
#' (x2, y2) refer to bottom right of bounding box
#'
#' @param boxes (Tensor[N, 4]): boxes in (x1, y1, x2, y2) which will be converted.
#'
#' @return boxes (Tensor[N, 4]): boxes in (x, y, w, h) format.
box_xyxy_to_xywh <- function(boxes) {
c(x1, y1, x2, y2) %<-% boxes$unbind(-1)
w = x2 - x1 # x2 - x1
h = y2 - y1 # y2 - y1
boxes = torch::torch_stack(list(x1, y1, w, h), dim=-1)
return(boxes)
}
266 changes: 266 additions & 0 deletions R/ops-boxes.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
#' Non-maximum Suppression (NMS)
#'
#' Performs non-maximum suppression (NMS) on the boxes according
#' to their intersection-over-union (IoU). NMS iteratively removes
#' lower scoring boxes which have an IoU greater than iou_threshold
#' with another (higher scoring) box.
#'
#' @param boxes (Tensor[N, 4])): boxes to perform NMS on. They are
#' expected to be in `` (x1, y1, x2, y2)`` format with
#' ``0 <= x1 < x2`` and ``0 <= y1 < y2``.
#' @param scores (Tensor[N]): scores for each one of the boxes
#' @param iou_threshold (float): discards all overlapping boxes with IoU > iou_threshold
#'
#' @details
#' If multiple boxes have the exact same score and satisfy the IoU
#' criterion with respect to a reference box, the selected box is
#' not guaranteed to be the same between CPU and GPU. This is similar
#' to the behavior of argsort in torch when repeated values are present.
#'
#' @return keep (Tensor): int64 tensor with the indices of the elements that
#' have been kept by NMS, sorted in decreasing order of scores.
#'
#' @export
nms <- function(boxes, scores, iou_threshold) {
assert_has_ops()
return(torch::torch_nms(boxes, scores, iou_threshold))
}

#' Batched Non-maximum Suppression (NMS)
#'
#' Performs non-maximum suppression in a batched fashion.
#' Each index value correspond to a category, and NMS
#' will not be applied between elements of different categories.
#'
#' @param boxes (Tensor[N, 4]): boxes where NMS will be performed. They are expected to be
#' in `` (x1, y1, x2, y2)`` format with ``0 <= x1 < x2`` and ``0 <= y1 < y2``.
#' @param scores (Tensor[N]): scores for each one of the boxes
#' @param idxs (Tensor[N]): indices of the categories for each one of the boxes.
#' @param iou_threshold (float): discards all overlapping boxes with IoU > iou_threshold
#'
#'
#' @return keep (Tensor): int64 tensor with the indices of
#' the elements that have been kept by NMS, sorted
#' in decreasing order of scores
#'
#' @export
batched_nms <- function(
boxes,
scores,
idxs,
iou_threshold
) {
boxes_dtype = boxes$dtype
boxes_device = boxes$device

# strategy: in order to perform NMS independently per class.
# we add an offset to all the boxes. The offset is dependent
# only on the class idx, and is large enough so that boxes
# from different classes do not overlap

if(boxes$numel() == 0) {
return(torch::torch_empty(0, dtype=torch::torch_int64(), device = boxes_device))
} else {
max_coordinate = boxes$max()
offsets = idxs$to(device = boxes_device, dtype = boxes_dtype) * (max_coordinate + torch::torch_tensor(1)$to(device = boxes_device, dtype = boxes_dtype))
boxes_for_nms = boxes + offsets[, NULL]
keep = nms(boxes_for_nms, scores, iou_threshold)
return(keep)
}
}

#' Remove Small Boxes
#'
#' Remove boxes which contains at least one side smaller than min_size.
#'
#' @param boxes (Tensor[N, 4]): boxes in ``(x1, y1, x2, y2)`` format
#' with ``0 <= x1 < x2`` and ``0 <= y1 < y2``.
#' @param min_size (float): minimum size
#'
#' @return keep (Tensor[K]): indices of the boxes that have both sides
#' larger than min_size
#'
#' @export
remove_small_boxes <- function(boxes, min_size) {
c(ws, hs) %<-% c(boxes[, 3] - boxes[, 1], boxes[, 4] - boxes[, 2])
keep = (ws >= min_size) & (hs >= min_size)
keep = torch::torch_where(keep)[[1]]
return(keep)
}

#' Clip Boxes to Image
#'
#' Clip boxes so that they lie inside an image of size `size`.
#'
#' @param boxes (Tensor[N, 4]): boxes in ``(x1, y1, x2, y2)`` format
#' with ``0 <= x1 < x2`` and ``0 <= y1 < y2``.
#' @param size (Tuple[height, width]): size of the image
#'
#' @return clipped_boxes (Tensor[N, 4])
#'
#' @export
clip_boxes_to_image <- function(boxes, size) {
dim = boxes$dim()
boxes_x = boxes[.., seq(1, boxes$shape[2], 2)]
boxes_y = boxes[.., seq(2, boxes$shape[2], 2)]
c(height, width) %<-% size

# if(torchvision$_is_tracing()) {
# boxes_x = torch::torch_max(boxes_x, other = torch::torch_tensor(0, dtype=boxes$dtype, device=boxes$device))
# boxes_x = torch::torch_min(boxes_x, other = torch::torch_tensor(width, dtype=boxes$dtype, device=boxes$device))
# boxes_y = torch::torch_max(boxes_y, other = torch::torch_tensor(0, dtype=boxes$dtype, device=boxes$device))
# boxes_y = torch::torch_min(boxes_y, other = torch::torch_tensor(height, dtype=boxes$dtype, device=boxes$device))
# } else {
boxes_x = boxes_x$clamp(min=0, max=width)
boxes_y = boxes_y$clamp(min=0, max=height)

clipped_boxes = torch::torch_stack(c(boxes_x, boxes_y), dim=dim+1)
return(clipped_boxes$reshape(boxes$shape))
}

#' Box Convert
#'
#' Converts boxes from given in_fmt to out_fmt.
#'
#' @param boxes (Tensor[N, 4]): boxes which will be converted.
#' @param in_fmt (str): Input format of given boxes. Supported formats are ['xyxy', 'xywh', 'cxcywh'].
#' @param out_fmt (str): Output format of given boxes. Supported formats are ['xyxy', 'xywh', 'cxcywh']
#' @return boxes (Tensor[N, 4]): Boxes into converted format.
#'
#' @details
#' Supported in_fmt and out_fmt are:
#' 'xyxy': boxes are represented via corners, x1, y1 being top left and x2, y2 being bottom right.
#' 'xywh' : boxes are represented via corner, width and height, x1, y2 being top left, w, h being width and height.
#' 'cxcywh' : boxes are represented via centre, width and height, cx, cy being center of box, w, h
#' being width and height.
#'
#' @export
box_convert <- function(boxes, in_fmt, out_fmt) {
allowed_fmts = c("xyxy", "xywh", "cxcywh")
if((!in_fmt %in% allowed_fmts) | (!out_fmt %in% allowed_fmts))
value_error("Unsupported Bounding Box Conversions for given in_fmt and out_fmt")

if(in_fmt == out_fmt)
return(boxes$clone())

if(in_fmt != 'xyxy' & out_fmt != 'xyxy') {
# convert to xyxy and change in_fmt xyxy
if(in_fmt == "xywh") {
boxes = box_xywh_to_xyxy(boxes)
} else if(in_fmt == "cxcywh") {
boxes = box_cxcywh_to_xyxy(boxes)
}
in_fmt = 'xyxy'
}
if(in_fmt == "xyxy") {
if(out_fmt == "xywh") {
boxes = box_xyxy_to_xywh(boxes)
} else if(out_fmt == "cxcywh") {
boxes = box_xyxy_to_cxcywh(boxes)
}
} else if(out_fmt == "xyxy") {
if(in_fmt == "xywh") {
boxes = box_xywh_to_xyxy(boxes)
} else if(in_fmt == "cxcywh") {
boxes = box_cxcywh_to_xyxy(boxes)
}
}
return(boxes)
}

upcast <- function(t) {
t_dtype <- as.character(t$dtype)
# Protects from numerical overflows in multiplications by upcasting to the equivalent higher type
if(t$is_floating_point()) {
return(if(t_dtype %in% c("Float", "Double")) t else t$to(device = torch::torch_float()))
} else {
return(if(t_dtype %in% c("Int", "Long")) t else t$to(device = torch::torch_int()))
}
}

#' Box Area
#'
#' Computes the area of a set of bounding boxes, which are specified by its
#' (x1, y1, x2, y2) coordinates.
#'
#' @param boxes (Tensor[N, 4]): boxes for which the area will be computed. They
#' are expected to be in (x1, y1, x2, y2) format with ``0 <= x1 < x2`` and ``0 <= y1 < y2``.
#'
#' @return area (Tensor[N]): area for each box
#'
#' @export
box_area <- function(boxes) {
boxes = upcast(boxes)
return((boxes[, 3] - boxes[, 1]) * (boxes[, 4] - boxes[, 2]))
}

box_inter_union <- function(boxes1, boxes2) {
# implementation from https://github.com/kuangliu/torchcv/blob/master/torchcv/utils/box.py
# with slight modifications
area1 = box_area(boxes1)
area2 = box_area(boxes2)

lt = torch::torch_max(boxes1[, NULL, 1:2], other = boxes2[, 1:2]) # [N,M,2]
rb = torch::torch_min(boxes1[, NULL, 3:N], other = boxes2[, 3:N]) # [N,M,2]

wh = upcast(rb - lt)$clamp(min=0) # [N,M,2]
inter = wh[, , 1] * wh[, , 2] # [N,M]

union = area1[, NULL] + area2 - inter

return(list(inter, union))
}

#' Box IoU
#'
#' Return intersection-over-union (Jaccard index) of boxes.
#' Both sets of boxes are expected to be in `` (x1, y1, x2, y2)`` format with
#' ``0 <= x1 < x2`` and ``0 <= y1 < y2``.
#'
#' @param boxes1 (Tensor[N, 4])
#' @param boxes2 (Tensor[M, 4])
#'
#' @return iou (Tensor[N, M]): the NxM matrix containing the pairwise IoU values for every element in boxes1 and boxes2
#'
#' @export
box_iou <- function(boxes1, boxes2) {
c(inter, union) %<-% box_inter_union(boxes1, boxes2)
iou = inter / union
return(iou)
}

#' Generalized Box IoU
#'
#' Return generalized intersection-over-union (Jaccard index) of boxes.
#' Both sets of boxes are expected to be in `` (x1, y1, x2, y2)`` format with
#' ``0 <= x1 < x2`` and ``0 <= y1 < y2``.
#'
#' @param boxes1 (Tensor[N, 4])
#' @param boxes2 (Tensor[M, 4])
#'
#' @details
#' Implementation adapted from https://github.com/facebookresearch/detr/blob/master/util/box_ops.py
#'
#' @return generalized_iou (Tensor[N, M]): the NxM matrix containing the pairwise generalized_IoU values
#' for every element in boxes1 and boxes2
#'
#' @export
generalized_box_iou <- function(boxes1, boxes2) {
# degenerate boxes gives inf / nan results
# so do an early check
if(as.numeric((boxes1[, 3:N] >= boxes1[, 1:2])$all()) != 1)
value_error("(boxes1[, 3:N] >= boxes1[, 1:2])$all() not TRUE")
if(as.numeric((boxes2[, 3:N] >= boxes2[, 1:2])$all()) != 1)
value_error("(boxes2[, 3:N] >= boxes2[, 1:2])$all() not TRUE")

c(inter, union) %<-% box_inter_union(boxes1, boxes2)
iou = inter / union

lti = torch::torch_min(boxes1[, NULL, 1:2], other = boxes2[, 1:2])
rbi = torch::torch_max(boxes1[, NULL, 3:N], other = boxes2[, 3:N])

whi = upcast(rbi - lti)$clamp(min=0) # [N,M,2]
areai = whi[, , 1] * whi[, , 2]

return(iou - (areai - union) / areai)
}
17 changes: 17 additions & 0 deletions tests/testthat/test-ops-box_convert.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
x1 = torch::torch_ones(5)
y1 = torch::torch_ones(5)
x2 = x1 + 1
y2 = y1 + 1
xyxy = torch::torch_stack(list(x1,y1,x2,y2))$transpose(2,1)

test_that("box_cxcywh_to_xyxy box_xyxy_to_cxcywh box_xywh_to_xyxy box_xyxy_to_xywh", {
# from xyxy
cxcywh <- box_cxcywh_to_xyxy(box_xyxy_to_cxcywh(xyxy))
xywh <- box_xywh_to_xyxy(box_xyxy_to_xywh(xyxy))

expect_tensor(cxcywh)
expect_tensor(xywh)
expect_equal(torch::as_array(cxcywh), torch::as_array(xyxy))
expect_equal(torch::as_array(xywh), torch::as_array(xyxy))
})

Loading