Skip to content

box_convert from xyxyxyxy to xywhr returns inf for float16 boxes with a side of 256 pixels or more (the squares of the sides overflow) #9695

Open

Description

@Nicholas022400701

🐛 Describe the bug

torchvision.ops.box_convert(boxes, "xyxyxyxy", "xywhr") squares the side vectors in the input dtype. For float16 boxes every side of 256 pixels or more comes back as inf (256² = 65536 is above the float16 maximum 65504), and with out_fmt="cxcywhr" the centre becomes inf, nan as well. Narrow integer dtypes wrap instead: a uint8 box with a side of 30 comes back with 11 (900 mod 256 = 132, sqrt 11.5) and an int16 box with a side of 200 with 0 (40000 wraps negative, sqrt gives nan, round and cast give 0). The transforms v2 kernel for the same conversion, _xyxyxyxy_to_xywhr in torchvision/transforms/v2/functional/_meta.py, has upcast to float32 since #9175 and returns the right box for the same input.

import torch
from torchvision import tv_tensors
from torchvision.ops import box_convert
from torchvision.transforms.v2 import functional as F

box = torch.tensor([[0., 0., 300., 0., 300., 100., 0., 100.]], dtype=torch.float16)  # 300 x 100, no rotation
print(box_convert(box, "xyxyxyxy", "xywhr"))    # tensor([[  0.,   0.,  inf, 100.,   0.]], dtype=torch.float16)
print(box_convert(box, "xyxyxyxy", "cxcywhr"))  # tensor([[ inf,  nan,  inf, 100.,   0.]], dtype=torch.float16)
print(F.convert_bounding_box_format(box, old_format=tv_tensors.BoundingBoxFormat.XYXYXYXY,
                                    new_format=tv_tensors.BoundingBoxFormat.XYWHR))
                                                # tensor([[  0.,   0., 300., 100.,   0.]], dtype=torch.float16)

for side in (255, 256, 257, 1024):
    b = torch.tensor([[0., 0., side, 0., side, 100., 0., 100.]], dtype=torch.float16)
    print(side, box_convert(b, "xyxyxyxy", "xywhr")[0, 2].item())   # 255.0, inf, inf, inf

print(box_convert(torch.tensor([[0, 0, 30, 0, 30, 10, 0, 10]], dtype=torch.uint8), "xyxyxyxy", "xywhr"))
                                                # tensor([[ 0,  0, 11, 10,  0]], dtype=torch.uint8)
print(box_convert(torch.tensor([[0, 0, 200, 0, 200, 10, 0, 10]], dtype=torch.int16), "xyxyxyxy", "xywhr"))
                                                # tensor([[ 0,  0,  0, 10,  0]], dtype=torch.int16)

main (7a7520f) has the same _box_xyxyxyxy_to_xywhr in torchvision/ops/_box_convert.py. The other rotated conversions in that file (cxcywhr, xywhr, xywhr to xyxyxyxy) only add and multiply by cos and sin and are fine in float16 for ordinary image sizes.

Fix. Do what #9175 did for the transforms v2 kernel: when the dtype is not float32 or float64, compute in float32 and cast the result back to the input dtype, keeping the existing rounding for integer input. With the patch the float16 and bfloat16 boxes above come back as [0, 0, 300, 100, 0], the uint8 and int16 ones as [0, 0, 30, 10, 0] and [0, 0, 200, 10, 0], float32, float64, int32 and int64 results are unchanged, and torch.jit.script(box_convert) still scripts and gives the same float16 result. The new TestBoxConvert::test_bbox_xyxyxyxy_to_xywhr_half_precision (a 400 x 200 box rotated by 90 degrees, in float16 and bfloat16) fails on the current code for float16 with inf in w and h and passes with the patch; test/test_ops.py -k BoxConvert is 35 passed with the patch, including test_bbox_convert_jit.

Patch

The complete change is below and on the branch https://github.com/Nicholas022400701/vision/tree/fix/box-convert-xyxyxyxy-half (commit 8f7b5d7); a draft PR against it follows.

git diff, 2 files, +15
diff --git a/test/test_ops.py b/test/test_ops.py
index ecc4378..4f1924b 100644
--- a/test/test_ops.py
+++ b/test/test_ops.py
@@ -1666,6 +1666,16 @@ class TestBoxConvert:
         box_xywhr = ops.box_convert(box_xyxyxyxy, in_fmt="xyxyxyxy", out_fmt="xywhr")
         torch.testing.assert_close(box_xywhr, box_tensor)
 
+    @pytest.mark.parametrize("dtype", (torch.float16, torch.bfloat16))
+    def test_bbox_xyxyxyxy_to_xywhr_half_precision(self, dtype):
+        # The squares of the side lengths overflow float16 above 256 pixels, so the conversion has to upcast
+        box_tensor = torch.tensor([[400, 500, 400, 100, 600, 100, 600, 500]], dtype=dtype)
+        exp_xywhr = torch.tensor([[400, 500, 400, 200, 90]], dtype=dtype)
+
+        box_xywhr = ops.box_convert(box_tensor, in_fmt="xyxyxyxy", out_fmt="xywhr")
+        assert box_xywhr.dtype == dtype
+        torch.testing.assert_close(box_xywhr, exp_xywhr)
+
     @pytest.mark.parametrize("inv_infmt", ["xwyh", "cxwyh", "xwyhr", "cxwyhr", "xxxxyyyy"])
     @pytest.mark.parametrize("inv_outfmt", ["xwcx", "xhwcy", "xwcxr", "xhwcyr", "xyxyxxyy"])
     def test_bbox_invalid(self, inv_infmt, inv_outfmt):
diff --git a/torchvision/ops/_box_convert.py b/torchvision/ops/_box_convert.py
index 8140624..4c10f78 100644
--- a/torchvision/ops/_box_convert.py
+++ b/torchvision/ops/_box_convert.py
@@ -193,6 +193,10 @@ def _box_xyxyxyxy_to_xywhr(boxes: Tensor) -> Tensor:
     """
     dtype = boxes.dtype
     need_cast = not boxes.is_floating_point()
+    acceptable_dtypes = [torch.float32, torch.float64]  # Ensure consistency between CPU and GPU.
+    if dtype not in acceptable_dtypes:
+        # Up-cast to avoid overflow for square operations
+        boxes = boxes.to(torch.float32)
     x1, y1, x2, y2, x3, y3, x4, y4 = boxes.unbind(-1)
     r_rad = torch.atan2(y1 - y2, x2 - x1)
     r = r_rad * 180 / torch.pi
@@ -203,5 +207,6 @@ def _box_xyxyxyxy_to_xywhr(boxes: Tensor) -> Tensor:
     boxes = torch.stack((x1, y1, w, h, r), dim=-1)
     if need_cast:
         boxes.round_()
+    if dtype not in acceptable_dtypes:
         boxes = boxes.to(dtype)
     return boxes

AI disclosure: I used an AI coding agent to help write this patch, the tests and this description. I have read the change and the tests myself and I will answer review comments personally.

Versions

torchvision 0.29.0+cpu (main 7a7520f has the same _box_xyxyxyxy_to_xywhr)
torch 2.14.0+cpu
Python 3.13.14, Linux x86_64 (glibc 2.34), CPU only, no CUDA

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions