Repository: Lightning-AI/torchmetrics

Issue:
re-implement mAP instead of using COCOtools
## 🚀 Feature

As Coco is not maintained for a few years can we in the next step have our own implementation?
I have also opened a few fixes to the original repo but no reply for weeks/months

### Alternatives

https://github.com/PyTorchLightning/metrics/pull/467#discussion_r737354204

### Additional context

<!-- Add any other context or screenshots about the feature request here. -->



Discussion:
@Borda 

Is the open images dataset useful it has over 600 Classes so we can find the most important ones or all of 600 classes?

https://storage.googleapis.com/openimages/web/index.html


I started working on this, but I think we should merge the existing implementation first. Hopefully I'll find time next week to open up at least a draft.
> I started working on this, but I think we should merge the existing implementation first. Hopefully I'll find time next week to open up at least a draft.

completely agree, this actual implementation will land in v0.6 and ideally the own version in v0.6.1 :rabbit: 
@twsl how is it going, when could we expect a draft PR? Really looking forward to... :cat:

Resolving patch:
diff --git a/torchmetrics/detection/map.py b/torchmetrics/detection/map.py
--- a/torchmetrics/detection/map.py
+++ b/torchmetrics/detection/map.py
@@ -12,95 +12,79 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 import logging
-import sys
-from dataclasses import dataclass
-from typing import Any, Callable, Dict, List, Optional, Sequence, Union
+from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple
 
 import torch
-from torch import Tensor
+from torch import IntTensor, Size, Tensor
 
 from torchmetrics.metric import Metric
-from torchmetrics.utilities.imports import (
-    _PYCOCOTOOLS_AVAILABLE,
-    _TORCHVISION_AVAILABLE,
-    _TORCHVISION_GREATER_EQUAL_0_8,
-)
+from torchmetrics.utilities.imports import _TORCHVISION_AVAILABLE, _TORCHVISION_GREATER_EQUAL_0_8
 
 if _TORCHVISION_AVAILABLE and _TORCHVISION_GREATER_EQUAL_0_8:
-    from torchvision.ops import box_convert
+    from torchvision.ops import box_area, box_convert, box_iou
 else:
-    box_convert = None
-
-if _PYCOCOTOOLS_AVAILABLE:
-    from pycocotools.coco import COCO
-    from pycocotools.cocoeval import COCOeval
-else:
-    COCO, COCOeval = None, None
+    box_convert = box_iou = box_area = None
 
 log = logging.getLogger(__name__)
 
 
-@dataclass
-class MAPMetricResults:
-    """Dataclass to wrap the final mAP results."""
+class BaseMetricResults(dict):
+    """Base metric class, that allows fields for pre-defined metrics."""
 
-    map: Tensor
-    map_50: Tensor
-    map_75: Tensor
-    map_small: Tensor
-    map_medium: Tensor
-    map_large: Tensor
-    mar_1: Tensor
-    mar_10: Tensor
-    mar_100: Tensor
-    mar_small: Tensor
-    mar_medium: Tensor
-    mar_large: Tensor
-    map_per_class: Tensor
-    mar_100_per_class: Tensor
+    def __getattr__(self, key: str) -> Tensor:
+        # Using this you get the correct error message, an AttributeError instead of a KeyError
+        if key in self:
+            return self[key]
+        raise AttributeError(f"No such attribute: {key}")
 
-    def __getitem__(self, key: str) -> Union[Tensor, List[Tensor]]:
-        return getattr(self, key)
+    def __setattr__(self, key: str, value: Tensor) -> None:
+        self[key] = value
 
+    def __delattr__(self, key: str) -> None:
+        if key in self:
+            del self[key]
+        raise AttributeError(f"No such attribute: {key}")
 
-# noinspection PyMethodMayBeStatic
-class WriteToLog:
-    """Logging class to move logs to log.debug()."""
 
-    def write(self, buf: str) -> None:  # skipcq: PY-D0003, PYL-R0201
-        for line in buf.rstrip().splitlines():
-            log.debug(line.rstrip())
+class MAPMetricResults(BaseMetricResults):
+    """Class to wrap the final mAP results."""
 
-    def flush(self) -> None:  # skipcq: PY-D0003, PYL-R0201
-        for handler in log.handlers:
-            handler.flush()
+    __slots__ = ("map", "map_50", "map_75", "map_small", "map_medium", "map_large")
 
-    def close(self) -> None:  # skipcq: PY-D0003, PYL-R0201
-        for handler in log.handlers:
-            handler.close()
 
+class MARMetricResults(BaseMetricResults):
+    """Class to wrap the final mAR results."""
 
-class _hide_prints:
-    """Internal helper context to suppress the default output of the pycocotools package."""
+    __slots__ = ("mar_1", "mar_10", "mar_100", "mar_small", "mar_medium", "mar_large")
 
-    def __init__(self) -> None:
-        self._original_stdout = None
 
-    def __enter__(self) -> None:
-        self._original_stdout = sys.stdout  # type: ignore
-        sys.stdout = WriteToLog()  # type: ignore
+class COCOMetricResults(BaseMetricResults):
+    """Class to wrap the final COCO metric results including various mAP/mAR values."""
 
-    def __exit__(self, exc_type, exc_val, exc_tb) -> None:  # type: ignore
-        sys.stdout.close()
-        sys.stdout = self._original_stdout  # type: ignore
+    __slots__ = (
+        "map",
+        "map_50",
+        "map_75",
+        "map_small",
+        "map_medium",
+        "map_large",
+        "mar_1",
+        "mar_10",
+        "mar_100",
+        "mar_small",
+        "mar_medium",
+        "mar_large",
+        "map_per_class",
+        "mar_100_per_class",
+    )
 
 
-def _input_validator(preds: List[Dict[str, torch.Tensor]], targets: List[Dict[str, torch.Tensor]]) -> None:
+def _input_validator(preds: Sequence[Dict[str, Tensor]], targets: Sequence[Dict[str, Tensor]]) -> None:
     """Ensure the correct input format of `preds` and `targets`"""
     if not isinstance(preds, Sequence):
-        raise ValueError("Expected argument `preds` to be of type List")
+        raise ValueError("Expected argument `preds` to be of type Sequence")
     if not isinstance(targets, Sequence):
-        raise ValueError("Expected argument `target` to be of type List")
+        raise ValueError("Expected argument `target` to be of type Sequence")
     if len(preds) != len(targets):
         raise ValueError("Expected argument `preds` and `target` to have the same length")
 
@@ -112,16 +96,16 @@ def _input_validator(preds: List[Dict[str, torch.Tensor]], targets: List[Dict[st
         if any(k not in p for p in targets):
             raise ValueError(f"Expected all dicts in `target` to contain the `{k}` key")
 
-    if any(type(pred["boxes"]) is not torch.Tensor for pred in preds):
-        raise ValueError("Expected all boxes in `preds` to be of type torch.Tensor")
-    if any(type(pred["scores"]) is not torch.Tensor for pred in preds):
-        raise ValueError("Expected all scores in `preds` to be of type torch.Tensor")
-    if any(type(pred["labels"]) is not torch.Tensor for pred in preds):
-        raise ValueError("Expected all labels in `preds` to be of type torch.Tensor")
-    if any(type(target["boxes"]) is not torch.Tensor for target in targets):
-        raise ValueError("Expected all boxes in `target` to be of type torch.Tensor")
-    if any(type(target["labels"]) is not torch.Tensor for target in targets):
-        raise ValueError("Expected all labels in `target` to be of type torch.Tensor")
+    if any(type(pred["boxes"]) is not Tensor for pred in preds):
+        raise ValueError("Expected all boxes in `preds` to be of type Tensor")
+    if any(type(pred["scores"]) is not Tensor for pred in preds):
+        raise ValueError("Expected all scores in `preds` to be of type Tensor")
+    if any(type(pred["labels"]) is not Tensor for pred in preds):
+        raise ValueError("Expected all labels in `preds` to be of type Tensor")
+    if any(type(target["boxes"]) is not Tensor for target in targets):
+        raise ValueError("Expected all boxes in `target` to be of type Tensor")
+    if any(type(target["labels"]) is not Tensor for target in targets):
+        raise ValueError("Expected all labels in `target` to be of type Tensor")
 
     for i, item in enumerate(targets):
         if item["boxes"].size(0) != item["labels"].size(0):
@@ -130,15 +114,15 @@ def _input_validator(preds: List[Dict[str, torch.Tensor]], targets: List[Dict[st
                 f" different length (expected {item['boxes'].size(0)} labels, got {item['labels'].size(0)})"
             )
     for i, item in enumerate(preds):
-        if item["boxes"].size(0) != item["labels"].size(0) != item["scores"].size(0):
+        if not (item["boxes"].size(0) == item["labels"].size(0) == item["scores"].size(0)):
             raise ValueError(
-                f"Input boxes, labels and scores of sample {i} in preds have a"
+                f"Input boxes, labels and scores of sample {i} in predictions have a"
                 f" different length (expected {item['boxes'].size(0)} labels and scores,"
                 f" got {item['labels'].size(0)} labels and {item['scores'].size(0)})"
             )
 
 
-def _fix_empty_tensors(boxes: torch.Tensor) -> torch.Tensor:
+def _fix_empty_tensors(boxes: Tensor) -> Tensor:
     """Empty tensors can cause problems in DDP mode, this methods corrects them."""
     if boxes.numel() == 0 and boxes.ndim == 1:
         return boxes.unsqueeze(0)
@@ -147,8 +131,8 @@ def _fix_empty_tensors(boxes: torch.Tensor) -> torch.Tensor:
 
 class MAP(Metric):
     r"""
-    Computes the `Mean-Average-Precision (mAP) and Mean-Average-Recall (mAR)\
-    <https://jonathan-hui.medium.com/map-mean-average-precision-for-object-detection-45c121a31173>`_\
+    Computes the `Mean-Average-Precision (mAP) and Mean-Average-Recall (mAR)
+    <https://jonathan-hui.medium.com/map-mean-average-precision-for-object-detection-45c121a31173>`_
     for object detection predictions.
     Optionally, the mAP and mAR values can be calculated per class.
 
@@ -156,28 +140,33 @@ class MAP(Metric):
     (xmin-top left, ymin-top left, xmax-bottom right, ymax-bottom right).
     See the :meth:`update` method for more information about the input format to this metric.
 
-    For an example on how to use this metric check the `torchmetrics examples\
+    For an example on how to use this metric check the `torchmetrics examples
     <https://github.com/PyTorchLightning/metrics/blob/master/tm_examples/detection_map.py>`_
 
     .. note::
-        This metric is a wrapper for the
+        This metric is following the mAP implementation of
         `pycocotools <https://github.com/cocodataset/cocoapi/tree/master/PythonAPI/pycocotools>`_,
-        which is a standard implementation for the mAP metric for object detection. Using this metric
-        therefore requires you to have `pycocotools` installed. Please install with ``pip install pycocotools`` or
-        ``pip install torchmetrics[detection]``.
+        , a standard implementation for the mAP metric for object detection.
 
     .. note::
         This metric requires you to have `torchvision` version 0.8.0 or newer installed (with corresponding
         version 1.7.0 of torch or newer). Please install with ``pip install torchvision`` or
         ``pip install torchmetrics[detection]``.
 
-    .. note::
-        As the pycocotools library cannot deal with tensors directly, all results have to be transfered
-        to the CPU, this might have an performance impact on your training.
-
     Args:
+        box_format:
+            Input format of given boxes. Supported formats are [‘xyxy’, ‘xywh’, ‘cxcywh’].
+        iou_thresholds:
+            IoU thresholds for evaluation. If set to `None` it corresponds to the stepped range `[0.5,...,0.95]`
+            with step `0.05`. Else provide a list of floats.
+        rec_thresholds:
+            Recall thresholds for evaluation. If set to `None` it corresponds to the stepped range `[0,...,1]`
+            with step `0.01`. Else provide a list of floats.
+        max_detection_thresholds:
+            Thresholds on max detections per image. If set to `None` will use thresholds `[1, 10, 100]`.
+            Else please provide a list of ints.
         class_metrics:
-            Option to enable per-class metrics for mAP and mAR_100. Has a performance impact. default: False
+            Option to enable per-class metrics for mAP and mAR_100. Has a performance impact.
         compute_on_step:
             Forward only calls ``update()`` and return ``None`` if this is set to ``False``.
         dist_sync_on_step:
@@ -191,8 +180,6 @@ class MAP(Metric):
             will be used to perform the allgather
 
     Raises:
-        ImportError:
-            If ``pycocotools`` is not installed
         ImportError:
             If ``torchvision`` is not installed or version installed is lower than 0.8.0
         ValueError:
@@ -201,6 +188,10 @@ class MAP(Metric):
 
     def __init__(
         self,
+        box_format: str = "xyxy",
+        iou_thresholds: Optional[List[float]] = None,
+        rec_thresholds: Optional[List[float]] = None,
+        max_detection_thresholds: Optional[List[int]] = None,
         class_metrics: bool = False,
         compute_on_step: bool = True,
         dist_sync_on_step: bool = False,
@@ -214,21 +205,31 @@ def __init__(
             dist_sync_fn=dist_sync_fn,
         )
 
-        if not _PYCOCOTOOLS_AVAILABLE:
-            raise ImportError(
-                "`MAP` metric requires that `pycocotools` installed."
-                " Please install with `pip install pycocotools` or `pip install torchmetrics[detection]`"
-            )
         if not (_TORCHVISION_AVAILABLE and _TORCHVISION_GREATER_EQUAL_0_8):
             raise ImportError(
                 "`MAP` metric requires that `torchvision` version 0.8.0 or newer is installed."
                 " Please install with `pip install torchvision` or `pip install torchmetrics[detection]`"
             )
 
+        allowed_box_formats = ("xyxy", "xywh", "cxcywh")
+        if box_format not in allowed_box_formats:
+            raise ValueError(f"Expected argument `box_format` to be one of {allowed_box_formats} but got {box_format}")
+        self.box_format = box_format
+        self.iou_thresholds = Tensor(iou_thresholds or torch.linspace(0.5, 0.95, round((0.95 - 0.5) / 0.05) + 1))
+        self.rec_thresholds = Tensor(rec_thresholds or torch.linspace(0.0, 1.00, round(1.00 / 0.01) + 1))
+        self.max_detection_thresholds = IntTensor(max_detection_thresholds or [1, 10, 100])
+        self.max_detection_thresholds, _ = torch.sort(self.max_detection_thresholds)
+        self.bbox_area_ranges = {
+            "all": (0 ** 2, int(1e5 ** 2)),
+            "small": (0 ** 2, 32 ** 2),
+            "medium": (32 ** 2, 96 ** 2),
+            "large": (96 ** 2, int(1e5 ** 2)),
+        }
+
         if not isinstance(class_metrics, bool):
             raise ValueError("Expected argument `class_metrics` to be a boolean")
-        self.class_metrics = class_metrics
 
+        self.class_metrics = class_metrics
         self.add_state("detection_boxes", default=[], dist_reduce_fx=None)
         self.add_state("detection_scores", default=[], dist_reduce_fx=None)
         self.add_state("detection_labels", default=[], dist_reduce_fx=None)
@@ -236,32 +237,34 @@ def __init__(
         self.add_state("groundtruth_labels", default=[], dist_reduce_fx=None)
 
     def update(self, preds: List[Dict[str, Tensor]], target: List[Dict[str, Tensor]]) -> None:  # type: ignore
-        """Add detections and groundtruth to the metric.
+        """Add detections and ground truth to the metric.
 
         Args:
-            preds: A list consisting of dictionaries each containing the key-values\
+            preds: A list consisting of dictionaries each containing the key-values
             (each dictionary corresponds to a single image):
-            - ``boxes``: torch.FloatTensor of shape
+            - ``boxes``: ``torch.FloatTensor`` of shape
                 [num_boxes, 4] containing `num_boxes` detection boxes of the format
+                specified in the contructor. By default, this method expects
                 [xmin, ymin, xmax, ymax] in absolute image coordinates.
-            - ``scores``: torch.FloatTensor of shape
+            - ``scores``: ``torch.FloatTensor`` of shape
                 [num_boxes] containing detection scores for the boxes.
-            - ``labels``: torch.IntTensor of shape
+            - ``labels``: ``torch.IntTensor`` of shape
                 [num_boxes] containing 0-indexed detection classes for the boxes.
 
-            target: A list consisting of dictionaries each containing the key-values\
+            target: A list consisting of dictionaries each containing the key-values
             (each dictionary corresponds to a single image):
-            - ``boxes``: torch.FloatTensor of shape
-                [num_boxes, 4] containing `num_boxes` groundtruth boxes of the format
+            - ``boxes``: ``torch.FloatTensor`` of shape
+                [num_boxes, 4] containing `num_boxes` ground truth boxes of the format
+                specified in the contructor. By default, this method expects
                 [xmin, ymin, xmax, ymax] in absolute image coordinates.
-            - ``labels``: torch.IntTensor of shape
-                [num_boxes] containing 1-indexed groundtruth classes for the boxes.
+            - ``labels``: ``torch.IntTensor`` of shape
+                [num_boxes] containing 1-indexed ground truth classes for the boxes.
 
         Raises:
             ValueError:
-                If ``preds`` is not of type List[Dict[str, torch.Tensor]]
+                If ``preds`` is not of type List[Dict[str, Tensor]]
             ValueError:
-                If ``target`` is not of type List[Dict[str, torch.Tensor]]
+                If ``target`` is not of type List[Dict[str, Tensor]]
             ValueError:
                 If ``preds`` and ``target`` are not of the same length
             ValueError:
@@ -279,20 +282,370 @@ def update(self, preds: List[Dict[str, Tensor]], target: List[Dict[str, Tensor]]
         _input_validator(preds, target)
 
         for item in preds:
-            self.detection_boxes.append(_fix_empty_tensors(item["boxes"]))
-            self.detection_scores.append(item["scores"])
+            self.detection_boxes.append(
+                _fix_empty_tensors(box_convert(item["boxes"], in_fmt=self.box_format, out_fmt="xyxy"))
+                if item["boxes"].size() == Size([1, 4])
+                else _fix_empty_tensors(item["boxes"])
+            )
             self.detection_labels.append(item["labels"])
+            self.detection_scores.append(item["scores"])
 
         for item in target:
-            self.groundtruth_boxes.append(_fix_empty_tensors(item["boxes"]))
+            self.groundtruth_boxes.append(
+                _fix_empty_tensors(box_convert(item["boxes"], in_fmt=self.box_format, out_fmt="xyxy"))
+                if item["boxes"].size() == Size([1, 4])
+                else _fix_empty_tensors(item["boxes"])
+            )
             self.groundtruth_labels.append(item["labels"])
 
+    def _get_classes(self) -> List:
+        """Returns a list of unique classes found in ground truth and detection data."""
+        if len(self.detection_labels) > 0 or len(self.groundtruth_labels) > 0:
+            return torch.cat(self.detection_labels + self.groundtruth_labels).unique().tolist()
+        return []
+
+    def _compute_iou(self, id: int, class_id: int, max_det: int) -> Tensor:
+        """Computes the Intersection over Union (IoU) for ground truth and detection bounding boxes for the given
+        image and class.
+
+        Args:
+            id:
+                Image Id, equivalent to the index of supplied samples
+            class_id:
+                Class Id of the supplied ground truth and detection labels
+            max_det:
+                Maximum number of evaluated detection bounding boxes
+        """
+        gt = self.groundtruth_boxes[id]
+        det = self.detection_boxes[id]
+        gt_label_mask = self.groundtruth_labels[id] == class_id
+        det_label_mask = self.detection_labels[id] == class_id
+        if len(det_label_mask) == 0 or len(det_label_mask) == 0:
+            return Tensor([])
+        gt = gt[gt_label_mask]
+        det = det[det_label_mask]
+        if len(gt) == 0 or len(det) == 0:
+            return Tensor([])
+
+        # Sort by scores and use only max detections
+        scores = self.detection_scores[id]
+        scores_filtered = scores[self.detection_labels[id] == class_id]
+        inds = torch.argsort(scores_filtered, descending=True)
+        det = det[inds]
+        if len(det) > max_det:
+            det = det[:max_det]
+
+        # generalized_box_iou
+        ious = box_iou(det, gt)
+        return ious
+
+    def _evaluate_image(
+        self, id: int, class_id: int, area_range: Tuple[int, int], max_det: int, ious: dict
+    ) -> Optional[dict]:
+        """Perform evaluation for single class and image.
+
+        Args:
+            id:
+                Image Id, equivalent to the index of supplied samples.
+            class_id:
+                Class Id of the supplied ground truth and detection labels.
+            area_range:
+                List of lower and upper bounding box area threshold.
+            max_det:
+                Maximum number of evaluated detection bounding boxes.
+            ious:
+                IoU results for image and class.
+        """
+        gt = self.groundtruth_boxes[id]
+        det = self.detection_boxes[id]
+        gt_label_mask = self.groundtruth_labels[id] == class_id
+        det_label_mask = self.detection_labels[id] == class_id
+        if len(det_label_mask) == 0 or len(det_label_mask) == 0:
+            return None
+        gt = gt[gt_label_mask]
+        det = det[det_label_mask]
+        if len(gt) == 0 and len(det) == 0:
+            return None
+
+        areas = box_area(gt)
+        ignore_area = (areas < area_range[0]) | (areas > area_range[1])
+
+        # sort dt highest score first, sort gt ignore last
+        ignore_area_sorted, gtind = torch.sort(ignore_area)
+        gt = gt[gtind]
+        scores = self.detection_scores[id]
+        scores_filtered = scores[det_label_mask]
+        scores_sorted, dtind = torch.sort(scores_filtered, descending=True)
+        det = det[dtind]
+        if len(det) > max_det:
+            det = det[:max_det]
+        # load computed ious
+        ious = ious[id, class_id][:, gtind] if len(ious[id, class_id]) > 0 else ious[id, class_id]
+
+        nb_iou_thrs = len(self.iou_thresholds)
+        nb_gt = len(gt)
+        nb_det = len(det)
+        gt_matches = torch.zeros((nb_iou_thrs, nb_gt), dtype=torch.bool)
+        det_matches = torch.zeros((nb_iou_thrs, nb_det), dtype=torch.bool)
+        gt_ignore = ignore_area_sorted
+        det_ignore = torch.zeros((nb_iou_thrs, nb_det), dtype=torch.bool)
+        if torch.numel(ious) > 0:
+            for idx_iou, t in enumerate(self.iou_thresholds):
+                for idx_det in range(nb_det):
+                    m = MAP._find_best_gt_match(t, nb_gt, gt_matches, idx_iou, gt_ignore, ious, idx_det)
+                    if m is not -1:
+                        det_ignore[idx_iou, idx_det] = gt_ignore[m]
+                        det_matches[idx_iou, idx_det] = True
+                        gt_matches[idx_iou, m] = True
+        # set unmatched detections outside of area range to ignore
+        det_areas = box_area(det)
+        det_ignore_area = (det_areas < area_range[0]) | (det_areas > area_range[1])
+        ar = det_ignore_area.reshape((1, nb_det))
+        det_ignore = torch.logical_or(
+            det_ignore, torch.logical_and(det_matches == 0, torch.repeat_interleave(ar, nb_iou_thrs, 0))
+        )
+        return {
+            "dtMatches": det_matches,
+            "gtMatches": gt_matches,
+            "dtScores": scores_sorted,
+            "gtIgnore": gt_ignore,
+            "dtIgnore": det_ignore,
+        }
+
+    @staticmethod
+    def _find_best_gt_match(
+        thr: int, nb_gt: int, gt_matches: Tensor, idx_iou: float, gt_ignore: Tensor, ious: Tensor, idx_det: int
+    ) -> int:
+        """Return id of best ground truth match with current detection.
+
+        Args:
+            thr:
+                Current threshold value.
+            nb_gt:
+                Number of ground truth elements.
+            gt_matches:
+                Tensor showing if a ground truth matches for threshold ``t`` exists.
+            idx_iou:
+                Id of threshold ``t``.
+            gt_ignore:
+                Tensor showing if ground truth should be ignored.
+            ious:
+                IoUs for all combinations of detection and ground truth.
+            idx_det:
+                Id of current detection.
+        """
+        # information about best match so far (m=-1 -> unmatched)
+        iou = min([thr, 1 - 1e-10])
+        match_id = -1
+        for idx_gt in range(nb_gt):
+            # if this gt already matched, and not a crowd, continue
+            if gt_matches[idx_iou, idx_gt]:
+                continue
+            # if dt matched to reg gt, and on ignore gt, stop
+            if match_id > -1 and not gt_ignore[match_id] and gt_ignore[idx_gt]:
+                break
+            # continue to next gt unless better match made
+            if ious[idx_det, idx_gt] < iou:
+                continue
+            # if match successful and best so far, store appropriately
+            iou = ious[idx_det, idx_gt]
+            match_id = idx_gt
+        return match_id
+
+    def _summarize(
+        self,
+        results: Dict,
+        avg_prec: bool = True,
+        iou_threshold: Optional[float] = None,
+        area_range: str = "all",
+        max_dets: int = 100,
+    ) -> Tensor:
+        """Perform evaluation for single class and image.
+
+        Args:
+            results:
+                Dictionary including precision, recall and scores for all combinations.
+            avg_prec:
+                Calculate average precision. Else calculate average recall.
+            iou_threshold:
+                IoU threshold. If set to `None` it all values are used. Else results are filtered.
+            area_range:
+                Bounding box area range key.
+            max_dets:
+                Maximum detections.
+        """
+        area_inds = [i for i, k in enumerate(self.bbox_area_ranges.keys()) if k == area_range]
+        mdet_inds = [i for i, k in enumerate(self.max_detection_thresholds) if k == max_dets]
+        if avg_prec:
+            # dimension of precision: [TxRxKxAxM]
+            prec = results["precision"]
+            # IoU
+            if iou_threshold is not None:
+                thr = torch.where(iou_threshold == self.iou_thresholds)[0]
+                prec = prec[thr]
+            prec = prec[:, :, :, area_inds, mdet_inds]
+        else:
+            # dimension of recall: [TxKxAxM]
+            prec = results["recall"]
+            if iou_threshold is not None:
+                thr = torch.where(iou_threshold == self.iou_thresholds)[0]
+                prec = prec[thr]
+            prec = prec[:, :, area_inds, mdet_inds]
+
+        mean_prec = Tensor([-1]) if len(prec[prec > -1]) == 0 else torch.mean(prec[prec > -1])
+        return mean_prec
+
+    def _calculate(self, class_ids: List) -> Tuple[Dict, MAPMetricResults, MARMetricResults]:
+        """Calculate the precision, recall and scores for all supplied label classes to calculate mAP/mAR.
+
+        Args:
+            class_ids:
+                List of label class Ids.
+        """
+        img_ids = torch.arange(len(self.groundtruth_boxes), dtype=torch.int).tolist()
+        max_detections = self.max_detection_thresholds[-1]
+        area_ranges = self.bbox_area_ranges.values()
+
+        ious = {
+            (id, class_id): self._compute_iou(id, class_id, max_detections) for id in img_ids for class_id in class_ids
+        }
+
+        eval_imgs = [
+            self._evaluate_image(img_id, class_id, area, max_detections, ious)
+            for class_id in class_ids
+            for area in area_ranges
+            for img_id in img_ids
+        ]
+
+        nb_iou_thrs = len(self.iou_thresholds)
+        nb_rec_thrs = len(self.rec_thresholds)
+        nb_classes = len(class_ids)
+        nb_bbox_areas = len(self.bbox_area_ranges)
+        nb_max_det_thrs = len(self.max_detection_thresholds)
+        nb_imgs = len(img_ids)
+        precision = -torch.ones((nb_iou_thrs, nb_rec_thrs, nb_classes, nb_bbox_areas, nb_max_det_thrs))
+        recall = -torch.ones((nb_iou_thrs, nb_classes, nb_bbox_areas, nb_max_det_thrs))
+        scores = -torch.ones((nb_iou_thrs, nb_rec_thrs, nb_classes, nb_bbox_areas, nb_max_det_thrs))
+
+        # retrieve E at each category, area range, and max number of detections
+        for idx_cls in range(nb_classes):
+            for idx_bbox_area in range(nb_bbox_areas):
+                for idx_max_det_thrs, max_det in enumerate(self.max_detection_thresholds):
+                    recall, precision, scores = MAP.__calculate_recall_precision_scores(
+                        recall,
+                        precision,
+                        scores,
+                        idx_cls=idx_cls,
+                        idx_bbox_area=idx_bbox_area,
+                        idx_max_det_thrs=idx_max_det_thrs,
+                        eval_imgs=eval_imgs,
+                        rec_thresholds=self.rec_thresholds,
+                        max_det=max_det,
+                        nb_imgs=nb_imgs,
+                        nb_bbox_areas=nb_bbox_areas,
+                    )
+
+        results = {
+            "dimensions": [nb_iou_thrs, nb_rec_thrs, nb_classes, nb_bbox_areas, nb_max_det_thrs],
+            "precision": precision,
+            "recall": recall,
+            "scores": scores,
+        }
+
+        map_metrics = MAPMetricResults()
+        map_metrics.map = self._summarize(results, True)
+        last_max_det_thr = self.max_detection_thresholds[-1]
+        map_metrics.map_50 = self._summarize(results, True, iou_threshold=0.5, max_dets=last_max_det_thr)
+        map_metrics.map_75 = self._summarize(results, True, iou_threshold=0.75, max_dets=last_max_det_thr)
+        map_metrics.map_small = self._summarize(results, True, area_range="small", max_dets=last_max_det_thr)
+        map_metrics.map_medium = self._summarize(results, True, area_range="medium", max_dets=last_max_det_thr)
+        map_metrics.map_large = self._summarize(results, True, area_range="large", max_dets=last_max_det_thr)
+
+        mar_metrics = MARMetricResults()
+        for max_det in self.max_detection_thresholds:
+            mar_metrics[f"mar_{max_det}"] = self._summarize(results, False, max_dets=max_det)
+        mar_metrics.mar_small = self._summarize(results, False, area_range="small", max_dets=last_max_det_thr)
+        mar_metrics.mar_medium = self._summarize(results, False, area_range="medium", max_dets=last_max_det_thr)
+        mar_metrics.mar_large = self._summarize(results, False, area_range="large", max_dets=last_max_det_thr)
+
+        return results, map_metrics, mar_metrics
+
+    @staticmethod
+    def __calculate_recall_precision_scores(
+        recall: Tensor,
+        precision: Tensor,
+        scores: Tensor,
+        idx_cls: int,
+        idx_bbox_area: int,
+        idx_max_det_thrs: int,
+        eval_imgs: list,
+        rec_thresholds: Tensor,
+        max_det: int,
+        nb_imgs: int,
+        nb_bbox_areas: int,
+    ) -> Tuple[Tensor, Tensor, Tensor]:
+        nb_rec_thrs = len(rec_thresholds)
+        idx_cls_pointer = idx_cls * nb_bbox_areas * nb_imgs
+        idx_bbox_area_pointer = idx_bbox_area * nb_imgs
+        # Load all image evals for current class_id and area_range
+        img_eval_cls_bbox = [eval_imgs[idx_cls_pointer + idx_bbox_area_pointer + i] for i in range(nb_imgs)]
+        img_eval_cls_bbox = [e for e in img_eval_cls_bbox if e is not None]
+        if not img_eval_cls_bbox:
+            return recall, precision, scores
+        det_scores = torch.cat([e["dtScores"][:max_det] for e in img_eval_cls_bbox])
+
+        # different sorting method generates slightly different results.
+        # mergesort is used to be consistent as Matlab implementation.
+        inds = torch.argsort(det_scores, descending=True)
+        det_scores_sorted = det_scores[inds]
+
+        det_matches = torch.cat([e["dtMatches"][:, :max_det] for e in img_eval_cls_bbox], axis=1)[:, inds]
+        det_ignore = torch.cat([e["dtIgnore"][:, :max_det] for e in img_eval_cls_bbox], axis=1)[:, inds]
+        gt_ignore = torch.cat([e["gtIgnore"] for e in img_eval_cls_bbox])
+        npig = torch.count_nonzero(gt_ignore == False)  # noqa: E712
+        if npig == 0:
+            return recall, precision, scores
+        tps = torch.logical_and(det_matches, torch.logical_not(det_ignore))
+        fps = torch.logical_and(torch.logical_not(det_matches), torch.logical_not(det_ignore))
+
+        tp_sum = torch.cumsum(tps, axis=1, dtype=torch.float)
+        fp_sum = torch.cumsum(fps, axis=1, dtype=torch.float)
+        for idx, (tp, fp) in enumerate(zip(tp_sum, fp_sum)):
+            nd = len(tp)
+            rc = tp / npig
+            pr = tp / (fp + tp + torch.finfo(torch.float64).eps)
+            prec = torch.zeros((nb_rec_thrs,))
+            score = torch.zeros((nb_rec_thrs,))
+
+            recall[idx, idx_cls, idx_bbox_area, idx_max_det_thrs] = rc[-1] if nd else 0
+
+            # Remove zigzags for AUC
+            for i in range(nd - 1, 0, -1):
+                if pr[i] > pr[i - 1]:
+                    pr[i - 1] = pr[i]
+
+            inds = torch.searchsorted(rc, rec_thresholds, right=False)
+            # TODO: optimize
+            try:
+                for ri, pi in enumerate(inds):  # range(min(len(inds), len(pr))):
+                    # pi = inds[ri]
+                    prec[ri] = pr[pi]
+                    score[ri] = det_scores_sorted[pi]
+            except Exception:
+                pass
+            precision[idx, :, idx_cls, idx_bbox_area, idx_max_det_thrs] = prec
+            scores[idx, :, idx_cls, idx_bbox_area, idx_max_det_thrs] = score
+
+        return recall, precision, scores
+
     def compute(self) -> dict:
-        """Compute the `Mean-Average-Precision (mAP) and Mean-Average-Recall (mAR)` scores. All detections added in
-        the `update()` method are included.
+        """Compute the `Mean-Average-Precision (mAP) and Mean-Average-Recall (mAR)` scores.
 
         Note:
-            Main `map` score is calculated with @[ IoU=0.50:0.95 | area=all | maxDets=100 ]
+            `map` score is calculated with @[ IoU=self.iou_thresholds | area=all | max_dets=max_detection_thresholds ]
+
+            Caution: If the initialization parameters are changed, dictionary keys for mAR can change as well.
+            The default properties are also accessible via fields and will raise an ``AttributeError`` if not available.
 
         Returns:
             dict containing
@@ -312,112 +665,27 @@ def compute(self) -> dict:
             - map_per_class: ``torch.Tensor`` (-1 if class metrics are disabled)
             - mar_100_per_class: ``torch.Tensor`` (-1 if class metrics are disabled)
         """
-        coco_target, coco_preds = COCO(), COCO()
-        coco_target.dataset = self._get_coco_format(self.groundtruth_boxes, self.groundtruth_labels)
-        coco_preds.dataset = self._get_coco_format(self.detection_boxes, self.detection_labels, self.detection_scores)
-
-        with _hide_prints():
-            coco_target.createIndex()
-            coco_preds.createIndex()
-            coco_eval = COCOeval(coco_target, coco_preds, "bbox")
-            coco_eval.evaluate()
-            coco_eval.accumulate()
-            coco_eval.summarize()
-            stats = coco_eval.stats
-
-        map_per_class_values: Tensor = torch.Tensor([-1])
-        mar_100_per_class_values: Tensor = torch.Tensor([-1])
+        overall, map, mar = self._calculate(self._get_classes())
+
+        map_per_class_values: Tensor = Tensor([-1])
+        mar_max_dets_per_class_values: Tensor = Tensor([-1])
+
         # if class mode is enabled, evaluate metrics per class
         if self.class_metrics:
             map_per_class_list = []
-            mar_100_per_class_list = []
-            for class_id in self._get_classes():
-                coco_eval.params.catIds = [class_id]
-                with _hide_prints():
-                    coco_eval.evaluate()
-                    coco_eval.accumulate()
-                    coco_eval.summarize()
-                    class_stats = coco_eval.stats
-
-                map_per_class_list.append(torch.Tensor([class_stats[0]]))
-                mar_100_per_class_list.append(torch.Tensor([class_stats[8]]))
-            map_per_class_values = torch.Tensor(map_per_class_list)
-            mar_100_per_class_values = torch.Tensor(mar_100_per_class_list)
-
-        metrics = MAPMetricResults(
-            map=torch.Tensor([stats[0]]),
-            map_50=torch.Tensor([stats[1]]),
-            map_75=torch.Tensor([stats[2]]),
-            map_small=torch.Tensor([stats[3]]),
-            map_medium=torch.Tensor([stats[4]]),
-            map_large=torch.Tensor([stats[5]]),
-            mar_1=torch.Tensor([stats[6]]),
-            mar_10=torch.Tensor([stats[7]]),
-            mar_100=torch.Tensor([stats[8]]),
-            mar_small=torch.Tensor([stats[9]]),
-            mar_medium=torch.Tensor([stats[10]]),
-            mar_large=torch.Tensor([stats[11]]),
-            map_per_class=map_per_class_values,
-            mar_100_per_class=mar_100_per_class_values,
-        )
-        return metrics.__dict__
-
-    def _get_coco_format(
-        self, boxes: List[torch.Tensor], labels: List[torch.Tensor], scores: Optional[List[torch.Tensor]] = None
-    ) -> Dict:
-        """Transforms and returns all cached targets or predictions in COCO format.
-
-        Format is defined at https://cocodataset.org/#format-data
-        """
-        images = []
-        annotations = []
-        annotation_id = 1  # has to start with 1, otherwise COCOEval results are wrong
-
-        boxes = [
-            box_convert(box, in_fmt="xyxy", out_fmt="xywh") if box.ndim > 1 and box.size(1) == 4 else box
-            for box in boxes
-        ]
-        for image_id, (image_boxes, image_labels) in enumerate(zip(boxes, labels)):
-            image_boxes = image_boxes.cpu().tolist()
-            image_labels = image_labels.cpu().tolist()
-
-            images.append({"id": image_id})
-            for k, (image_box, image_label) in enumerate(zip(image_boxes, image_labels)):
-                if len(image_box) != 4:
-                    raise ValueError(
-                        f"Invalid input box of sample {image_id}, element {k} (expected 4 values, got {len(image_box)})"
-                    )
-
-                if type(image_label) != int:
-                    raise ValueError(
-                        f"Invalid input class of sample {image_id}, element {k}"
-                        f" (expected value of type integer, got type {type(image_label)})"
-                    )
+            mar_max_dets_per_class_list = []
 
-                annotation = {
-                    "id": annotation_id,
-                    "image_id": image_id,
-                    "bbox": image_box,
-                    "category_id": image_label,
-                    "area": image_box[2] * image_box[3],
-                    "iscrowd": 0,
-                }
-                if scores is not None:
-                    score = scores[image_id][k].cpu().tolist()
-                    if type(score) != float:
-                        raise ValueError(
-                            f"Invalid input score of sample {image_id}, element {k}"
-                            f" (expected value of type float, got type {type(score)})"
-                        )
-                    annotation["score"] = score
-                annotations.append(annotation)
-                annotation_id += 1
-
-        classes = [{"id": i, "name": str(i)} for i in self._get_classes()]
-        return {"images": images, "annotations": annotations, "categories": classes}
-
-    def _get_classes(self) -> list:
-        """Get list of unique classes depending on groundtruth_labels and detection_labels."""
-        if len(self.detection_labels) > 0 or len(self.groundtruth_labels) > 0:
-            return torch.cat(self.detection_labels + self.groundtruth_labels).unique().cpu().tolist()
-        return []
+            for class_id in self._get_classes():
+                _, cls_map, cls_mar = self._calculate([class_id])
+                map_per_class_list.append(cls_map.map)
+                mar_max_dets_per_class_list.append(cls_mar[f"mar_{self.max_detection_thresholds[-1]}"])
+
+            map_per_class_values = Tensor(map_per_class_list)
+            mar_max_dets_per_class_values = Tensor(mar_max_dets_per_class_list)
+
+        metrics = COCOMetricResults()
+        metrics.update(map)
+        metrics.update(mar)
+        metrics.map_per_class = map_per_class_values
+        metrics[f"mar_{self.max_detection_thresholds[-1]}_per_class"] = mar_max_dets_per_class_values
+        return metrics


Test patch:
diff --git a/tests/detection/test_map.py b/tests/detection/test_map.py
--- a/tests/detection/test_map.py
+++ b/tests/detection/test_map.py
@@ -19,11 +19,7 @@
 
 from tests.helpers.testers import MetricTester
 from torchmetrics.detection.map import MAP
-from torchmetrics.utilities.imports import (
-    _PYCOCOTOOLS_AVAILABLE,
-    _TORCHVISION_AVAILABLE,
-    _TORCHVISION_GREATER_EQUAL_0_8,
-)
+from torchmetrics.utilities.imports import _TORCHVISION_AVAILABLE, _TORCHVISION_GREATER_EQUAL_0_8
 
 Input = namedtuple("Input", ["preds", "target"])
 
@@ -59,7 +55,7 @@
             ),  # coco image id 74
             dict(
                 boxes=torch.Tensor([[0.00, 2.87, 601.00, 421.52]]),
-                scores=torch.Tensor([0.699, 0.423]),
+                scores=torch.Tensor([0.699]),
                 labels=torch.IntTensor([5]),
             ),  # coco image id 133
         ],
@@ -164,10 +160,10 @@ def _compare_fn(preds, target) -> dict:
     }
 
 
-_pytest_condition = not (_PYCOCOTOOLS_AVAILABLE and _TORCHVISION_AVAILABLE and _TORCHVISION_GREATER_EQUAL_0_8)
+_pytest_condition = not (_TORCHVISION_AVAILABLE and _TORCHVISION_GREATER_EQUAL_0_8)
 
 
-@pytest.mark.skipif(_pytest_condition, reason="test requires that pycocotools and torchvision=>0.8.0 is installed")
+@pytest.mark.skipif(_pytest_condition, reason="test requires that torchvision=>0.8.0 is installed")
 class TestMAP(MetricTester):
     """Test the MAP metric for object detection predictions.
 
@@ -194,7 +190,7 @@ def test_map(self, ddp):
 
 
 # noinspection PyTypeChecker
-@pytest.mark.skipif(_pytest_condition, reason="test requires that pycocotools and torchvision=>0.8.0 is installed")
+@pytest.mark.skipif(_pytest_condition, reason="test requires that torchvision=>0.8.0 is installed")
 def test_error_on_wrong_init():
     """Test class raises the expected errors."""
     MAP()  # no error
@@ -203,20 +199,11 @@ def test_error_on_wrong_init():
         MAP(class_metrics=0)
 
 
-@pytest.mark.skipif(_pytest_condition, reason="test requires that pycocotools and torchvision=>0.8.0 is installed")
+@pytest.mark.skipif(_pytest_condition, reason="test requires that torchvision=>0.8.0 is installed")
 def test_empty_preds():
     """Test empty predictions."""
     metric = MAP()
 
-    metric.update(
-        [
-            dict(boxes=torch.Tensor([[]]), scores=torch.Tensor([]), labels=torch.IntTensor([])),
-        ],
-        [
-            dict(boxes=torch.Tensor([[214.1500, 41.2900, 562.4100, 285.0700]]), labels=torch.IntTensor([4])),
-        ],
-    )
-
     metric.update(
         [
             dict(boxes=torch.Tensor([]), scores=torch.Tensor([]), labels=torch.IntTensor([])),
@@ -235,17 +222,17 @@ def test_empty_metric():
     metric.compute()
 
 
-@pytest.mark.skipif(_pytest_condition, reason="test requires that pycocotools and torchvision=>0.8.0 is installed")
+@pytest.mark.skipif(_pytest_condition, reason="test requires that torchvision=>0.8.0 is installed")
 def test_error_on_wrong_input():
     """Test class input validation."""
     metric = MAP()
 
     metric.update([], [])  # no error
 
-    with pytest.raises(ValueError, match="Expected argument `preds` to be of type List"):
+    with pytest.raises(ValueError, match="Expected argument `preds` to be of type Sequence"):
         metric.update(torch.Tensor(), [])  # type: ignore
 
-    with pytest.raises(ValueError, match="Expected argument `target` to be of type List"):
+    with pytest.raises(ValueError, match="Expected argument `target` to be of type Sequence"):
         metric.update([], torch.Tensor())  # type: ignore
 
     with pytest.raises(ValueError, match="Expected argument `preds` and `target` to have the same length"):
@@ -281,31 +268,31 @@ def test_error_on_wrong_input():
             [dict(boxes=torch.IntTensor())],
         )
 
-    with pytest.raises(ValueError, match="Expected all boxes in `preds` to be of type torch.Tensor"):
+    with pytest.raises(ValueError, match="Expected all boxes in `preds` to be of type Tensor"):
         metric.update(
             [dict(boxes=[], scores=torch.Tensor(), labels=torch.IntTensor())],
             [dict(boxes=torch.Tensor(), labels=torch.IntTensor())],
         )
 
-    with pytest.raises(ValueError, match="Expected all scores in `preds` to be of type torch.Tensor"):
+    with pytest.raises(ValueError, match="Expected all scores in `preds` to be of type Tensor"):
         metric.update(
             [dict(boxes=torch.Tensor(), scores=[], labels=torch.IntTensor())],
             [dict(boxes=torch.Tensor(), labels=torch.IntTensor())],
         )
 
-    with pytest.raises(ValueError, match="Expected all labels in `preds` to be of type torch.Tensor"):
+    with pytest.raises(ValueError, match="Expected all labels in `preds` to be of type Tensor"):
         metric.update(
             [dict(boxes=torch.Tensor(), scores=torch.Tensor(), labels=[])],
             [dict(boxes=torch.Tensor(), labels=torch.IntTensor())],
         )
 
-    with pytest.raises(ValueError, match="Expected all boxes in `target` to be of type torch.Tensor"):
+    with pytest.raises(ValueError, match="Expected all boxes in `target` to be of type Tensor"):
         metric.update(
             [dict(boxes=torch.Tensor(), scores=torch.Tensor(), labels=torch.IntTensor())],
             [dict(boxes=[], labels=torch.IntTensor())],
         )
 
-    with pytest.raises(ValueError, match="Expected all labels in `target` to be of type torch.Tensor"):
+    with pytest.raises(ValueError, match="Expected all labels in `target` to be of type Tensor"):
         metric.update(
             [dict(boxes=torch.Tensor(), scores=torch.Tensor(), labels=torch.IntTensor())],
             [dict(boxes=torch.Tensor(), labels=[])],