Repository: Lightning-AI/torchmetrics

Issue:
MeanAveragePrecision does not handle empty predictions or ground truth boxes not in xyxy format
## 🐛 Bug

When empty ground truth or prediction `boxes` tensor are provided, `MeanAveragePrecision` crashes during the conversion to `xyxy` from other formats.

### To Reproduce

Steps to reproduce the behavior...

1. Have empty `boxes` tensor in ground truth or prediction
2. instantiate `MeanAveragePrecision` with `box_format='xywh'`
3. run `update` with the ground truth and prediction boxes
4. conversion error

#### Code sample

```python
import torch
from torchmetrics.detection.mean_ap import MeanAveragePrecision
gts = [
        dict(boxes=torch.Tensor([[10, 20, 15, 25]]), labels=torch.IntTensor([0])),
    ]
preds = [
    dict(boxes=torch.Tensor([]), scores=torch.Tensor([]), labels=torch.IntTensor([])),
]
metric = MeanAveragePrecision(box_format='xywh')
metric.update(preds, gts)
```

### Expected behavior

torchmetrics should skip the conversion for empty boxes.

### Environment

- TorchMetrics version (and how you installed TM, e.g. `conda`, `pip`, build from source): `0.9.1` from `nix`
- Python & PyTorch Version (e.g., 1.0): Python 3.9, PyTorch 1.11.0
- Any other relevant information such as OS (e.g., Linux): Linux

### Additional context

<!-- Add any other context about the problem here. -->



Discussion:
Hi! thanks for your contribution!, great first issue!

Resolving patch:
diff --git a/src/torchmetrics/detection/mean_ap.py b/src/torchmetrics/detection/mean_ap.py
--- a/src/torchmetrics/detection/mean_ap.py
+++ b/src/torchmetrics/detection/mean_ap.py
@@ -416,7 +416,8 @@ def _get_safe_item_values(self, item: Dict[str, Any]) -> Union[Tensor, Tuple]:
 
         if self.iou_type == "bbox":
             boxes = _fix_empty_tensors(item["boxes"])
-            boxes = box_convert(boxes, in_fmt=self.box_format, out_fmt="xyxy")
+            if boxes.numel() > 0:
+                boxes = box_convert(boxes, in_fmt=self.box_format, out_fmt="xyxy")
             return boxes
         elif self.iou_type == "segm":
             masks = []


Test patch:
diff --git a/tests/unittests/detection/test_map.py b/tests/unittests/detection/test_map.py
--- a/tests/unittests/detection/test_map.py
+++ b/tests/unittests/detection/test_map.py
@@ -56,7 +56,6 @@
     ],
 )
 
-
 _inputs = Input(
     preds=[
         [
@@ -299,7 +298,6 @@ class TestMAP(MetricTester):
 
     @pytest.mark.parametrize("ddp", [False, True])
     def test_map_bbox(self, compute_on_cpu, ddp):
-
         """Test modular implementation for correctness."""
         self.run_class_metric_test(
             ddp=ddp,
@@ -344,12 +342,8 @@ def test_empty_preds():
     metric = MeanAveragePrecision()
 
     metric.update(
-        [
-            dict(boxes=Tensor([]), scores=Tensor([]), labels=IntTensor([])),
-        ],
-        [
-            dict(boxes=Tensor([[214.1500, 41.2900, 562.4100, 285.0700]]), labels=IntTensor([4])),
-        ],
+        [dict(boxes=Tensor([]), scores=Tensor([]), labels=IntTensor([]))],
+        [dict(boxes=Tensor([[214.1500, 41.2900, 562.4100, 285.0700]]), labels=IntTensor([4]))],
     )
     metric.compute()
 
@@ -360,16 +354,56 @@ def test_empty_ground_truths():
     metric = MeanAveragePrecision()
 
     metric.update(
-        [
-            dict(
-                boxes=Tensor([[214.1500, 41.2900, 562.4100, 285.0700]]),
-                scores=Tensor([0.5]),
-                labels=IntTensor([4]),
-            ),
-        ],
-        [
-            dict(boxes=Tensor([]), labels=IntTensor([])),
-        ],
+        [dict(boxes=Tensor([[214.1500, 41.2900, 562.4100, 285.0700]]), scores=Tensor([0.5]), labels=IntTensor([4]))],
+        [dict(boxes=Tensor([]), labels=IntTensor([]))],
+    )
+    metric.compute()
+
+
+@pytest.mark.skipif(_pytest_condition, reason="test requires that torchvision=>0.8.0 is installed")
+def test_empty_ground_truths_xywh():
+    """Test empty ground truths in xywh format."""
+    metric = MeanAveragePrecision(box_format="xywh")
+
+    metric.update(
+        [dict(boxes=Tensor([[214.1500, 41.2900, 348.2600, 243.7800]]), scores=Tensor([0.5]), labels=IntTensor([4]))],
+        [dict(boxes=Tensor([]), labels=IntTensor([]))],
+    )
+    metric.compute()
+
+
+@pytest.mark.skipif(_pytest_condition, reason="test requires that torchvision=>0.8.0 is installed")
+def test_empty_preds_xywh():
+    """Test empty predictions in xywh format."""
+    metric = MeanAveragePrecision(box_format="xywh")
+
+    metric.update(
+        [dict(boxes=Tensor([]), scores=Tensor([]), labels=IntTensor([]))],
+        [dict(boxes=Tensor([[214.1500, 41.2900, 348.2600, 243.7800]]), labels=IntTensor([4]))],
+    )
+    metric.compute()
+
+
+@pytest.mark.skipif(_pytest_condition, reason="test requires that torchvision=>0.8.0 is installed")
+def test_empty_ground_truths_cxcywh():
+    """Test empty ground truths in cxcywh format."""
+    metric = MeanAveragePrecision(box_format="cxcywh")
+
+    metric.update(
+        [dict(boxes=Tensor([[388.2800, 163.1800, 348.2600, 243.7800]]), scores=Tensor([0.5]), labels=IntTensor([4]))],
+        [dict(boxes=Tensor([]), labels=IntTensor([]))],
+    )
+    metric.compute()
+
+
+@pytest.mark.skipif(_pytest_condition, reason="test requires that torchvision=>0.8.0 is installed")
+def test_empty_preds_cxcywh():
+    """Test empty predictions in cxcywh format."""
+    metric = MeanAveragePrecision(box_format="cxcywh")
+
+    metric.update(
+        [dict(boxes=Tensor([]), scores=Tensor([]), labels=IntTensor([]))],
+        [dict(boxes=Tensor([[388.2800, 163.1800, 348.2600, 243.7800]]), labels=IntTensor([4]))],
     )
     metric.compute()
 
@@ -467,16 +501,8 @@ def test_segm_iou_empty_gt_mask():
     metric = MeanAveragePrecision(iou_type="segm")
 
     metric.update(
-        [
-            dict(
-                masks=torch.randint(0, 1, (1, 10, 10)).bool(),
-                scores=Tensor([0.5]),
-                labels=IntTensor([4]),
-            ),
-        ],
-        [
-            dict(masks=Tensor([]), labels=IntTensor([])),
-        ],
+        [dict(masks=torch.randint(0, 1, (1, 10, 10)).bool(), scores=Tensor([0.5]), labels=IntTensor([4]))],
+        [dict(masks=Tensor([]), labels=IntTensor([]))],
     )
 
     metric.compute()
@@ -488,16 +514,8 @@ def test_segm_iou_empty_pred_mask():
     metric = MeanAveragePrecision(iou_type="segm")
 
     metric.update(
-        [
-            dict(
-                masks=torch.BoolTensor([]),
-                scores=Tensor([]),
-                labels=IntTensor([]),
-            ),
-        ],
-        [
-            dict(masks=torch.randint(0, 1, (1, 10, 10)).bool(), labels=IntTensor([4])),
-        ],
+        [dict(masks=torch.BoolTensor([]), scores=Tensor([]), labels=IntTensor([]))],
+        [dict(masks=torch.randint(0, 1, (1, 10, 10)).bool(), labels=IntTensor([4]))],
     )
 
     metric.compute()