Skip to content

Mean Average Precision

Install the metrics extra before using this API:

pip install "supervision[metrics]"

supervision.metrics.mean_average_precision.MeanAveragePrecision

Bases: Metric[MeanAveragePrecisionResult]

Mean Average Precision (mAP) is a metric used to evaluate object detection models. It is the average of the precision-recall curves at different IoU thresholds.

Examples:

>>> import numpy as np
>>> import supervision as sv
>>> from supervision.metrics import MeanAveragePrecision
>>> predictions = sv.Detections(
...     xyxy=np.array([[0, 0, 10, 10]]),
...     class_id=np.array([0]),
...     confidence=np.array([0.9])
... )
>>> targets = sv.Detections(
...     xyxy=np.array([[0, 0, 10, 10]]),
...     class_id=np.array([0])
... )
>>> map_metric = MeanAveragePrecision()
>>> map_result = map_metric.update(predictions, targets).compute()
>>> round(float(map_result.map50), 2)
1.0
>>> print(map_result)
Average Precision (AP) @[ IoU=0.50:0.95 | area=   all | maxDets=100 ] = 1.000
Average Precision (AP) @[ IoU=0.50      | area=   all | maxDets=100 ] = 1.000
Average Precision (AP) @[ IoU=0.75      | area=   all | maxDets=100 ] = 1.000
Average Precision (AP) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = 1.000
Average Precision (AP) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = -1.000
Average Precision (AP) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = -1.000

example_plot

Source code in src/supervision/metrics/mean_average_precision.py
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
class MeanAveragePrecision(Metric[MeanAveragePrecisionResult]):
    """
    Mean Average Precision (mAP) is a metric used to evaluate object detection models.
    It is the average of the precision-recall curves at different IoU thresholds.

    Examples:
        ```pycon
        >>> import numpy as np
        >>> import supervision as sv
        >>> from supervision.metrics import MeanAveragePrecision
        >>> predictions = sv.Detections(
        ...     xyxy=np.array([[0, 0, 10, 10]]),
        ...     class_id=np.array([0]),
        ...     confidence=np.array([0.9])
        ... )
        >>> targets = sv.Detections(
        ...     xyxy=np.array([[0, 0, 10, 10]]),
        ...     class_id=np.array([0])
        ... )
        >>> map_metric = MeanAveragePrecision()
        >>> map_result = map_metric.update(predictions, targets).compute()
        >>> round(float(map_result.map50), 2)
        1.0
        >>> print(map_result)
        Average Precision (AP) @[ IoU=0.50:0.95 | area=   all | maxDets=100 ] = 1.000
        Average Precision (AP) @[ IoU=0.50      | area=   all | maxDets=100 ] = 1.000
        Average Precision (AP) @[ IoU=0.75      | area=   all | maxDets=100 ] = 1.000
        Average Precision (AP) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = 1.000
        Average Precision (AP) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = -1.000
        Average Precision (AP) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = -1.000

        ```

    ![example_plot](
        https://media.roboflow.com/supervision-docs/metrics/mAP_plot_example.png
    ){ align=center width="800" }
    """

    def __init__(
        self,
        metric_target: MetricTarget = MetricTarget.BOXES,
        class_agnostic: bool = False,
        class_mapping: dict[int, int] | None = None,
        image_indices: list[int] | None = None,
    ) -> None:
        """
        Initialize the Mean Average Precision metric.

        Args:
            metric_target: The type of detection data to use.
            class_agnostic: Whether to treat all data as a single class.
            class_mapping: A dictionary to map class IDs to new IDs.
            image_indices: The indices of the images to use.
        """
        self._metric_target = metric_target
        self._class_agnostic = class_agnostic

        self._predictions_list: list[Detections] = []
        self._targets_list: list[Detections] = []
        self._class_mapping = class_mapping
        self._image_indices = image_indices

    def reset(self) -> None:
        """
        Reset the metric to its initial state, clearing all stored data.
        """
        self._predictions_list = []
        self._targets_list = []

    def update(
        self,
        predictions: Detections | list[Detections],
        targets: Detections | list[Detections],
    ) -> MeanAveragePrecision:
        """
        Add new predictions and targets to the metric, but do not compute the result.

        Args:
            predictions: The predicted detections.
            targets: The ground-truth detections.

        Returns:
            The updated metric instance.
        """
        if not isinstance(predictions, list):
            predictions = [predictions]
        if not isinstance(targets, list):
            targets = [targets]

        if len(predictions) != len(targets):
            raise ValueError(
                f"The number of predictions ({len(predictions)}) and"
                f" targets ({len(targets)}) during the update must be the same."
            )

        if self._class_agnostic:
            predictions = deepcopy(predictions)
            targets = deepcopy(targets)

            for prediction in predictions:
                if prediction.class_id is not None:
                    prediction.class_id[:] = -1
            for target in targets:
                if target.class_id is not None:
                    target.class_id[:] = -1

        self._predictions_list.extend(predictions)
        self._targets_list.extend(targets)

        return self

    def _detections_content(self, detections: Detections) -> npt.NDArray[Any] | None:
        """Return per-detection masks or oriented boxes for the metric target,
        or `None` for the box target and for empty detections."""
        if self._metric_target == MetricTarget.BOXES or len(detections) == 0:
            return None
        if self._metric_target == MetricTarget.MASKS:
            if detections.mask is None:
                raise ValueError(
                    "MeanAveragePrecision with `MetricTarget.MASKS` requires"
                    " masks on both predictions and targets."
                )
            return np.asarray(detections.mask).astype(bool)
        if self._metric_target == MetricTarget.ORIENTED_BOUNDING_BOXES:
            obb = detections.data.get(ORIENTED_BOX_COORDINATES)
            if obb is None:
                raise ValueError(
                    "MeanAveragePrecision with"
                    " `MetricTarget.ORIENTED_BOUNDING_BOXES` requires"
                    f" `{ORIENTED_BOX_COORDINATES}` in `data` on both"
                    " predictions and targets."
                )
            return np.asarray(obb, dtype=np.float32).reshape(-1, 4, 2)
        raise ValueError(f"Invalid metric target: {self._metric_target}")

    def _content_area(
        self, xywh: list[float], content: npt.NDArray[Any] | None, idx: int
    ) -> float:
        """Compute the default annotation area for the metric target: bbox area
        for boxes, pixel count for masks, polygon area for oriented boxes."""
        if content is None:
            return float(xywh[2] * xywh[3])
        if self._metric_target == MetricTarget.MASKS:
            return float(np.count_nonzero(content[idx]))
        x, y = content[idx, :, 0], content[idx, :, 1]
        # Shoelace formula
        return float(0.5 * abs(np.sum(x * np.roll(y, -1) - np.roll(x, -1) * y)))

    def _prepare_targets(
        self, targets: list[Detections]
    ) -> dict[str, list[_TypeCocoDict]]:
        """Transform targets into a dictionary that can be used by the COCO evaluator"""
        images: list[_TypeCocoDict] = [{"id": img_id} for img_id in range(len(targets))]
        if self._image_indices is not None:
            images = [{"id": self._image_indices[img["id"]]} for img in images]
        # Annotations list
        annotations: list[_TypeCocoDict] = []
        for image_id, image_targets in enumerate(targets):
            if self._image_indices is not None:
                image_id = self._image_indices[image_id]

            # Ensure xyxy is not None
            if image_targets.xyxy is None:
                continue

            content = self._detections_content(image_targets)
            for target_idx, xyxy in enumerate(image_targets.xyxy):
                xywh = [xyxy[0], xyxy[1], xyxy[2] - xyxy[0], xyxy[3] - xyxy[1]]

                # Default values
                category_id = 0

                if image_targets.class_id is not None:
                    cls_id = image_targets.class_id[target_idx]
                    if self._class_mapping is not None:
                        category_id = self._class_mapping[int(cls_id)]
                    else:
                        category_id = int(cls_id)

                # Use area from data if available, otherwise calculate from the
                # metric target content (bbox, mask or oriented box)
                area = None
                if image_targets.data is not None and "area" in image_targets.data:
                    area_data: npt.NDArray[np.float32] = np.asarray(
                        image_targets.data["area"], dtype=np.float32
                    )
                    area = float(area_data[target_idx])

                if area is None:
                    area = self._content_area(xywh, content, target_idx)

                iscrowd = 0
                if image_targets.data is not None and "iscrowd" in image_targets.data:
                    iscrowd_data: npt.NDArray[np.int64] = np.asarray(
                        image_targets.data["iscrowd"], dtype=np.int64
                    )
                    iscrowd = int(iscrowd_data[target_idx])

                ignore = 0
                if image_targets.data is not None and "ignore" in image_targets.data:
                    ignore_data: npt.NDArray[np.int64] = np.asarray(
                        image_targets.data["ignore"], dtype=np.int64
                    )
                    ignore = int(ignore_data[target_idx])

                dict_annotation: _TypeCocoDict = {
                    "area": area,
                    "iscrowd": iscrowd,
                    "image_id": image_id,
                    "bbox": xywh,
                    "category_id": category_id,
                    "id": len(annotations) + 1,  # Start IDs from 1 (0 means no match)
                    "ignore": ignore,
                }
                if content is not None:
                    dict_annotation["content"] = content[target_idx]
                annotations.append(dict_annotation)
        # Category list
        all_cat_ids = {annotation["category_id"] for annotation in annotations}
        categories: list[_TypeCocoDict] = [{"id": cat_id} for cat_id in all_cat_ids]
        # Create coco dictionary
        return {
            "images": images,
            "annotations": annotations,
            "categories": categories,
        }

    def _prepare_predictions(
        self, predictions: list[Detections]
    ) -> list[_TypeCocoDict]:
        """Transform predictions into a list of predictions that can be used by the COCO
        evaluator."""
        coco_predictions: list[_TypeCocoDict] = []
        for image_id, image_predictions in enumerate(predictions):
            if self._image_indices is not None:
                image_id = self._image_indices[image_id]

            if image_predictions.xyxy is None:
                continue

            content = self._detections_content(image_predictions)
            for pred_idx, xyxy in enumerate(image_predictions.xyxy):
                xywh = [xyxy[0], xyxy[1], xyxy[2] - xyxy[0], xyxy[3] - xyxy[1]]

                category_id = 0
                score = 0.0

                if image_predictions.class_id is not None:
                    cls_id = image_predictions.class_id[pred_idx]
                    if self._class_mapping is not None:
                        category_id = self._class_mapping[int(cls_id)]
                    else:
                        category_id = int(cls_id)

                if image_predictions.confidence is not None:
                    score = float(image_predictions.confidence[pred_idx])

                # Use area from data if available, otherwise calculate from the
                # metric target content (bbox, mask or oriented box)
                area = None
                if (
                    image_predictions.data is not None
                    and "area" in image_predictions.data
                ):
                    area_data: npt.NDArray[np.float32] = np.asarray(
                        image_predictions.data["area"], dtype=np.float32
                    )
                    area = float(area_data[pred_idx])

                if area is None:
                    area = self._content_area(xywh, content, pred_idx)

                dict_prediction: _TypeCocoDict = {
                    "image_id": image_id,
                    "bbox": xywh,
                    "score": score,
                    "category_id": category_id,
                    "area": area,
                    "id": len(coco_predictions) + 1,
                }
                if content is not None:
                    dict_prediction["content"] = content[pred_idx]
                coco_predictions.append(dict_prediction)
        return coco_predictions

    def compute(self) -> MeanAveragePrecisionResult:
        """
        Calculate Mean Average Precision based on predicted and ground-truth
        detections at different thresholds using the COCO evaluation metrics.
        Source: https://github.com/rafaelpadilla/review_object_detection_metrics

        Returns:
            The Mean Average Precision result.
        """
        total_images_predictions = len(self._predictions_list)
        total_images_targets = len(self._targets_list)

        if total_images_predictions != total_images_targets:
            raise ValueError(
                f"The number of predictions ({total_images_predictions}) and"
                f" targets ({total_images_targets}) during the evaluation must be"
                " the same."
            )
        dict_targets = self._prepare_targets(self._targets_list)
        lst_predictions = self._prepare_predictions(self._predictions_list)
        # Create a coco object with the targets
        coco_gt = EvaluationDataset(targets=dict_targets)
        # Include the predictions to coco object
        coco_det = coco_gt.load_predictions(lst_predictions)
        # Create a coco evaluator with the predictions
        cocoEval = COCOEvaluator(coco_gt, coco_det, metric_target=self._metric_target)

        # Evaluate on all images
        cocoEval.evaluate()

        # Create MeanAveragePrecisionResult object for small objects
        mAP_small = MeanAveragePrecisionResult(
            metric_target=self._metric_target,
            is_class_agnostic=self._class_agnostic,
            mAP_scores=np.asarray(
                cocoEval.results["mAP_scores_small"], dtype=np.float64
            ),
            ap_per_class=np.asarray(
                cocoEval.results["ap_per_class_small"], dtype=np.float64
            ),
            iou_thresholds=np.asarray(cocoEval.params.iou_thrs, dtype=np.float64),
            matched_classes=np.asarray(cocoEval.params.cat_ids, dtype=np.int32),
        )
        # Create MeanAveragePrecisionResult object for medium objects
        mAP_medium = MeanAveragePrecisionResult(
            metric_target=self._metric_target,
            is_class_agnostic=self._class_agnostic,
            mAP_scores=np.asarray(
                cocoEval.results["mAP_scores_medium"], dtype=np.float64
            ),
            ap_per_class=np.asarray(
                cocoEval.results["ap_per_class_medium"], dtype=np.float64
            ),
            iou_thresholds=np.asarray(cocoEval.params.iou_thrs, dtype=np.float64),
            matched_classes=np.asarray(cocoEval.params.cat_ids, dtype=np.int32),
        )
        # Create MeanAveragePrecisionResult object for large objects
        mAP_large = MeanAveragePrecisionResult(
            metric_target=self._metric_target,
            is_class_agnostic=self._class_agnostic,
            mAP_scores=np.asarray(
                cocoEval.results["mAP_scores_large"], dtype=np.float64
            ),
            ap_per_class=np.asarray(
                cocoEval.results["ap_per_class_large"], dtype=np.float64
            ),
            iou_thresholds=np.asarray(cocoEval.params.iou_thrs, dtype=np.float64),
            matched_classes=np.asarray(cocoEval.params.cat_ids, dtype=np.int32),
        )

        # Create the final MeanAveragePrecisionResult object
        mAP_result = MeanAveragePrecisionResult(
            metric_target=self._metric_target,
            is_class_agnostic=self._class_agnostic,
            mAP_scores=np.asarray(
                cocoEval.results["mAP_scores_all_sizes"], dtype=np.float64
            ),
            ap_per_class=np.asarray(
                cocoEval.results["ap_per_class_all_sizes"], dtype=np.float64
            ),
            iou_thresholds=np.asarray(cocoEval.params.iou_thrs, dtype=np.float64),
            matched_classes=np.asarray(cocoEval.params.cat_ids, dtype=np.int32),
            small_objects=mAP_small,
            medium_objects=mAP_medium,
            large_objects=mAP_large,
        )
        return mAP_result

Methods:

__init__(metric_target: MetricTarget = MetricTarget.BOXES, class_agnostic: bool = False, class_mapping: dict[int, int] | None = None, image_indices: list[int] | None = None) -> None

Initialize the Mean Average Precision metric.

Parameters:

Name Type Description Default
metric_target
MetricTarget

The type of detection data to use.

BOXES
class_agnostic
bool

Whether to treat all data as a single class.

False
class_mapping
dict[int, int] | None

A dictionary to map class IDs to new IDs.

None
image_indices
list[int] | None

The indices of the images to use.

None
Source code in src/supervision/metrics/mean_average_precision.py
def __init__(
    self,
    metric_target: MetricTarget = MetricTarget.BOXES,
    class_agnostic: bool = False,
    class_mapping: dict[int, int] | None = None,
    image_indices: list[int] | None = None,
) -> None:
    """
    Initialize the Mean Average Precision metric.

    Args:
        metric_target: The type of detection data to use.
        class_agnostic: Whether to treat all data as a single class.
        class_mapping: A dictionary to map class IDs to new IDs.
        image_indices: The indices of the images to use.
    """
    self._metric_target = metric_target
    self._class_agnostic = class_agnostic

    self._predictions_list: list[Detections] = []
    self._targets_list: list[Detections] = []
    self._class_mapping = class_mapping
    self._image_indices = image_indices

compute() -> MeanAveragePrecisionResult

Calculate Mean Average Precision based on predicted and ground-truth detections at different thresholds using the COCO evaluation metrics. Source: https://github.com/rafaelpadilla/review_object_detection_metrics

Returns:

Type Description
MeanAveragePrecisionResult

The Mean Average Precision result.

Source code in src/supervision/metrics/mean_average_precision.py
def compute(self) -> MeanAveragePrecisionResult:
    """
    Calculate Mean Average Precision based on predicted and ground-truth
    detections at different thresholds using the COCO evaluation metrics.
    Source: https://github.com/rafaelpadilla/review_object_detection_metrics

    Returns:
        The Mean Average Precision result.
    """
    total_images_predictions = len(self._predictions_list)
    total_images_targets = len(self._targets_list)

    if total_images_predictions != total_images_targets:
        raise ValueError(
            f"The number of predictions ({total_images_predictions}) and"
            f" targets ({total_images_targets}) during the evaluation must be"
            " the same."
        )
    dict_targets = self._prepare_targets(self._targets_list)
    lst_predictions = self._prepare_predictions(self._predictions_list)
    # Create a coco object with the targets
    coco_gt = EvaluationDataset(targets=dict_targets)
    # Include the predictions to coco object
    coco_det = coco_gt.load_predictions(lst_predictions)
    # Create a coco evaluator with the predictions
    cocoEval = COCOEvaluator(coco_gt, coco_det, metric_target=self._metric_target)

    # Evaluate on all images
    cocoEval.evaluate()

    # Create MeanAveragePrecisionResult object for small objects
    mAP_small = MeanAveragePrecisionResult(
        metric_target=self._metric_target,
        is_class_agnostic=self._class_agnostic,
        mAP_scores=np.asarray(
            cocoEval.results["mAP_scores_small"], dtype=np.float64
        ),
        ap_per_class=np.asarray(
            cocoEval.results["ap_per_class_small"], dtype=np.float64
        ),
        iou_thresholds=np.asarray(cocoEval.params.iou_thrs, dtype=np.float64),
        matched_classes=np.asarray(cocoEval.params.cat_ids, dtype=np.int32),
    )
    # Create MeanAveragePrecisionResult object for medium objects
    mAP_medium = MeanAveragePrecisionResult(
        metric_target=self._metric_target,
        is_class_agnostic=self._class_agnostic,
        mAP_scores=np.asarray(
            cocoEval.results["mAP_scores_medium"], dtype=np.float64
        ),
        ap_per_class=np.asarray(
            cocoEval.results["ap_per_class_medium"], dtype=np.float64
        ),
        iou_thresholds=np.asarray(cocoEval.params.iou_thrs, dtype=np.float64),
        matched_classes=np.asarray(cocoEval.params.cat_ids, dtype=np.int32),
    )
    # Create MeanAveragePrecisionResult object for large objects
    mAP_large = MeanAveragePrecisionResult(
        metric_target=self._metric_target,
        is_class_agnostic=self._class_agnostic,
        mAP_scores=np.asarray(
            cocoEval.results["mAP_scores_large"], dtype=np.float64
        ),
        ap_per_class=np.asarray(
            cocoEval.results["ap_per_class_large"], dtype=np.float64
        ),
        iou_thresholds=np.asarray(cocoEval.params.iou_thrs, dtype=np.float64),
        matched_classes=np.asarray(cocoEval.params.cat_ids, dtype=np.int32),
    )

    # Create the final MeanAveragePrecisionResult object
    mAP_result = MeanAveragePrecisionResult(
        metric_target=self._metric_target,
        is_class_agnostic=self._class_agnostic,
        mAP_scores=np.asarray(
            cocoEval.results["mAP_scores_all_sizes"], dtype=np.float64
        ),
        ap_per_class=np.asarray(
            cocoEval.results["ap_per_class_all_sizes"], dtype=np.float64
        ),
        iou_thresholds=np.asarray(cocoEval.params.iou_thrs, dtype=np.float64),
        matched_classes=np.asarray(cocoEval.params.cat_ids, dtype=np.int32),
        small_objects=mAP_small,
        medium_objects=mAP_medium,
        large_objects=mAP_large,
    )
    return mAP_result

reset() -> None

Reset the metric to its initial state, clearing all stored data.

Source code in src/supervision/metrics/mean_average_precision.py
def reset(self) -> None:
    """
    Reset the metric to its initial state, clearing all stored data.
    """
    self._predictions_list = []
    self._targets_list = []

update(predictions: Detections | list[Detections], targets: Detections | list[Detections]) -> MeanAveragePrecision

Add new predictions and targets to the metric, but do not compute the result.

Parameters:

Name Type Description Default
predictions
Detections | list[Detections]

The predicted detections.

required
targets
Detections | list[Detections]

The ground-truth detections.

required

Returns:

Type Description
MeanAveragePrecision

The updated metric instance.

Source code in src/supervision/metrics/mean_average_precision.py
def update(
    self,
    predictions: Detections | list[Detections],
    targets: Detections | list[Detections],
) -> MeanAveragePrecision:
    """
    Add new predictions and targets to the metric, but do not compute the result.

    Args:
        predictions: The predicted detections.
        targets: The ground-truth detections.

    Returns:
        The updated metric instance.
    """
    if not isinstance(predictions, list):
        predictions = [predictions]
    if not isinstance(targets, list):
        targets = [targets]

    if len(predictions) != len(targets):
        raise ValueError(
            f"The number of predictions ({len(predictions)}) and"
            f" targets ({len(targets)}) during the update must be the same."
        )

    if self._class_agnostic:
        predictions = deepcopy(predictions)
        targets = deepcopy(targets)

        for prediction in predictions:
            if prediction.class_id is not None:
                prediction.class_id[:] = -1
        for target in targets:
            if target.class_id is not None:
                target.class_id[:] = -1

    self._predictions_list.extend(predictions)
    self._targets_list.extend(targets)

    return self

supervision.metrics.mean_average_precision.MeanAveragePrecisionResult dataclass

The result of the Mean Average Precision calculation.

Returns -1 sentinel scores when no detections or targets are present.

Attributes:

Name Type Description
metric_target MetricTarget

the type of data used for the metric - boxes, masks or oriented bounding boxes.

is_class_agnostic bool

When computing class-agnostic results, class ID is set to -1.

mAP_scores NDArray[float64]

the mAP scores at each IoU threshold. Shape: (num_iou_thresholds,)

ap_per_class NDArray[float64]

the average precision scores per class and IoU threshold. Shape: (num_target_classes, num_iou_thresholds)

iou_thresholds NDArray[float64]

the IoU thresholds used in the calculations.

matched_classes NDArray[int32]

the class IDs of all matched classes. Corresponds to the rows of ap_per_class.

small_objects MeanAveragePrecisionResult | None

the mAP results for small objects (area < 32²).

medium_objects MeanAveragePrecisionResult | None

the mAP results for medium objects (32² ≤ area < 96²).

large_objects MeanAveragePrecisionResult | None

the mAP results for large objects (area ≥ 96²).

Source code in src/supervision/metrics/mean_average_precision.py
@dataclass
class MeanAveragePrecisionResult:
    """
    The result of the Mean Average Precision calculation.

    Returns `-1` sentinel scores when no detections or targets are present.

    Attributes:
        metric_target: the type of data used for the metric -
            boxes, masks or oriented bounding boxes.
        is_class_agnostic: When computing class-agnostic results, class ID
            is set to `-1`.
        mAP_scores: the mAP scores at each IoU threshold.
            Shape: `(num_iou_thresholds,)`
        ap_per_class: the average precision scores per
            class and IoU threshold. Shape: `(num_target_classes, num_iou_thresholds)`
        iou_thresholds: the IoU thresholds used in the calculations.
        matched_classes: the class IDs of all matched classes.
            Corresponds to the rows of `ap_per_class`.
        small_objects: the mAP results
            for small objects (area < 32²).
        medium_objects: the mAP results
            for medium objects (32² ≤ area < 96²).
        large_objects: the mAP results
            for large objects (area ≥ 96²).
    """

    metric_target: MetricTarget
    is_class_agnostic: bool

    @property
    def map50_95(self) -> float:
        """the mAP score at IoU thresholds from `0.5` to `0.95`."""
        valid_scores = self.mAP_scores[self.mAP_scores > -1]
        if len(valid_scores) > 0:
            return float(valid_scores.mean())
        else:
            return -1

    @property
    def map50(self) -> float:
        """the mAP score at IoU threshold of `0.5`."""
        return float(self.mAP_scores[0])

    @property
    def map75(self) -> float:
        """the mAP score at IoU threshold of `0.75`."""
        return float(self.mAP_scores[5])

    mAP_scores: npt.NDArray[np.float64]
    ap_per_class: npt.NDArray[np.float64]
    iou_thresholds: npt.NDArray[np.float64]
    matched_classes: npt.NDArray[np.int32]
    small_objects: MeanAveragePrecisionResult | None = None
    medium_objects: MeanAveragePrecisionResult | None = None
    large_objects: MeanAveragePrecisionResult | None = None

    def __str__(self) -> str:
        """
        Formats the evaluation output metrics to match the structure used by pycocotools

        Example:
           ```pycon
           >>> import numpy as np
           >>> import supervision as sv
           >>> from supervision.metrics import MeanAveragePrecision
           >>> predictions = sv.Detections(
           ...     xyxy=np.array([[0, 0, 10, 10]]),
           ...     class_id=np.array([0]),
           ...     confidence=np.array([0.9])
           ... )
           >>> targets = sv.Detections(
           ...     xyxy=np.array([[0, 0, 10, 10]]),
           ...     class_id=np.array([0])
           ... )
           >>> map_metric = MeanAveragePrecision()
           >>> map_result = map_metric.update(predictions, targets).compute()
           >>> print(map_result)  # doctest: +ELLIPSIS
           Average Precision (AP) @[ IoU=0.50:0.95 | area=   all | maxDets=100 ] = ...
           Average Precision (AP) @[ IoU=0.50      | area=   all | maxDets=100 ] = ...
           Average Precision (AP) @[ IoU=0.75      | area=   all | maxDets=100 ] = ...
           Average Precision (AP) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = ...
           Average Precision (AP) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = ...
           Average Precision (AP) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = ...

           ```
        """
        if (
            self.small_objects is None
            or self.medium_objects is None
            or self.large_objects is None
        ):
            return (
                f"Average Precision (AP) @[ IoU=0.50:0.95 | area=   all | "
                f"maxDets=100 ] = {self.map50_95:.3f}\n"
                f"Average Precision (AP) @[ IoU=0.50      | area=   all | "
                f"maxDets=100 ] = {self.map50:.3f}\n"
                f"Average Precision (AP) @[ IoU=0.75      | area=   all | "
                f"maxDets=100 ] = {self.map75:.3f}"
            )

        return (
            f"Average Precision (AP) @[ IoU=0.50:0.95 | area=   all | "
            f"maxDets=100 ] = {self.map50_95:.3f}\n"
            f"Average Precision (AP) @[ IoU=0.50      | area=   all | "
            f"maxDets=100 ] = {self.map50:.3f}\n"
            f"Average Precision (AP) @[ IoU=0.75      | area=   all | "
            f"maxDets=100 ] = {self.map75:.3f}\n"
            f"Average Precision (AP) @[ IoU=0.50:0.95 | area= small | "
            f"maxDets=100 ] = {self.small_objects.map50_95:.3f}\n"
            f"Average Precision (AP) @[ IoU=0.50:0.95 | area=medium | "
            f"maxDets=100 ] = {self.medium_objects.map50_95:.3f}\n"
            f"Average Precision (AP) @[ IoU=0.50:0.95 | area= large | "
            f"maxDets=100 ] = {self.large_objects.map50_95:.3f}"
        )

    def to_pandas(self) -> pd.DataFrame:
        """
        Convert the result to a pandas DataFrame.

        Returns:
            The result as a DataFrame.
        """
        ensure_pandas_installed()
        import pandas as pd

        pandas_data: dict[str, object] = {
            "mAP@50:95": self.map50_95,
            "mAP@50": self.map50,
            "mAP@75": self.map75,
        }

        if self.small_objects is not None:
            small_objects_df = self.small_objects.to_pandas()
            for key, value in small_objects_df.items():
                pandas_data[f"small_objects_{key}"] = value
        if self.medium_objects is not None:
            medium_objects_df = self.medium_objects.to_pandas()
            for key, value in medium_objects_df.items():
                pandas_data[f"medium_objects_{key}"] = value
        if self.large_objects is not None:
            large_objects_df = self.large_objects.to_pandas()
            for key, value in large_objects_df.items():
                pandas_data[f"large_objects_{key}"] = value

        # Average precisions are currently not included in the DataFrame.
        return pd.DataFrame(
            pandas_data,
            index=[0],
        )

    def plot(self) -> None:
        """
        Plot the mAP results.

        ![example_plot](
            https://media.roboflow.com/supervision-docs/metrics/mAP_plot_example.png
        ){ align=center width="800" }
        """
        from matplotlib import pyplot as plt

        labels = ["mAP@50:95", "mAP@50", "mAP@75"]
        values = [self.map50_95, self.map50, self.map75]
        colors = [LEGACY_COLOR_PALETTE[0]] * 3

        if self.small_objects is not None:
            labels += ["Small: mAP@50:95", "Small: mAP@50", "Small: mAP@75"]
            values += [
                self.small_objects.map50_95,
                self.small_objects.map50,
                self.small_objects.map75,
            ]
            colors += [LEGACY_COLOR_PALETTE[3]] * 3

        if self.medium_objects is not None:
            labels += ["Medium: mAP@50:95", "Medium: mAP@50", "Medium: mAP@75"]
            values += [
                self.medium_objects.map50_95,
                self.medium_objects.map50,
                self.medium_objects.map75,
            ]
            colors += [LEGACY_COLOR_PALETTE[2]] * 3

        if self.large_objects is not None:
            labels += ["Large: mAP@50:95", "Large: mAP@50", "Large: mAP@75"]
            values += [
                self.large_objects.map50_95,
                self.large_objects.map50,
                self.large_objects.map75,
            ]
            colors += [LEGACY_COLOR_PALETTE[4]] * 3

        plt.rcParams["font.family"] = "monospace"

        _, ax = plt.subplots(figsize=(10, 6))
        ax.set_ylim(0, 1)
        ax.set_ylabel("Value", fontweight="bold")
        ax.set_title("Mean Average Precision", fontweight="bold")

        x_positions = range(len(labels))
        bars = ax.bar(x_positions, values, color=colors, align="center")

        ax.set_xticks(x_positions)
        ax.set_xticklabels(labels, rotation=45, ha="right")

        for bar in bars:
            y_value = bar.get_height()
            ax.text(
                bar.get_x() + bar.get_width() / 2,
                y_value + 0.02,
                f"{y_value:.2f}",
                ha="center",
                va="bottom",
            )

        plt.rcParams["font.family"] = "sans-serif"

        plt.tight_layout()
        plt.show()

Attributes

map50: float property

the mAP score at IoU threshold of 0.5.

map50_95: float property

the mAP score at IoU thresholds from 0.5 to 0.95.

map75: float property

the mAP score at IoU threshold of 0.75.

Methods:

__str__() -> str

Formats the evaluation output metrics to match the structure used by pycocotools

Example
>>> import numpy as np
>>> import supervision as sv
>>> from supervision.metrics import MeanAveragePrecision
>>> predictions = sv.Detections(
...     xyxy=np.array([[0, 0, 10, 10]]),
...     class_id=np.array([0]),
...     confidence=np.array([0.9])
... )
>>> targets = sv.Detections(
...     xyxy=np.array([[0, 0, 10, 10]]),
...     class_id=np.array([0])
... )
>>> map_metric = MeanAveragePrecision()
>>> map_result = map_metric.update(predictions, targets).compute()
>>> print(map_result)  # doctest: +ELLIPSIS
Average Precision (AP) @[ IoU=0.50:0.95 | area=   all | maxDets=100 ] = ...
Average Precision (AP) @[ IoU=0.50      | area=   all | maxDets=100 ] = ...
Average Precision (AP) @[ IoU=0.75      | area=   all | maxDets=100 ] = ...
Average Precision (AP) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = ...
Average Precision (AP) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = ...
Average Precision (AP) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = ...
Source code in src/supervision/metrics/mean_average_precision.py
def __str__(self) -> str:
    """
    Formats the evaluation output metrics to match the structure used by pycocotools

    Example:
       ```pycon
       >>> import numpy as np
       >>> import supervision as sv
       >>> from supervision.metrics import MeanAveragePrecision
       >>> predictions = sv.Detections(
       ...     xyxy=np.array([[0, 0, 10, 10]]),
       ...     class_id=np.array([0]),
       ...     confidence=np.array([0.9])
       ... )
       >>> targets = sv.Detections(
       ...     xyxy=np.array([[0, 0, 10, 10]]),
       ...     class_id=np.array([0])
       ... )
       >>> map_metric = MeanAveragePrecision()
       >>> map_result = map_metric.update(predictions, targets).compute()
       >>> print(map_result)  # doctest: +ELLIPSIS
       Average Precision (AP) @[ IoU=0.50:0.95 | area=   all | maxDets=100 ] = ...
       Average Precision (AP) @[ IoU=0.50      | area=   all | maxDets=100 ] = ...
       Average Precision (AP) @[ IoU=0.75      | area=   all | maxDets=100 ] = ...
       Average Precision (AP) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = ...
       Average Precision (AP) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = ...
       Average Precision (AP) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = ...

       ```
    """
    if (
        self.small_objects is None
        or self.medium_objects is None
        or self.large_objects is None
    ):
        return (
            f"Average Precision (AP) @[ IoU=0.50:0.95 | area=   all | "
            f"maxDets=100 ] = {self.map50_95:.3f}\n"
            f"Average Precision (AP) @[ IoU=0.50      | area=   all | "
            f"maxDets=100 ] = {self.map50:.3f}\n"
            f"Average Precision (AP) @[ IoU=0.75      | area=   all | "
            f"maxDets=100 ] = {self.map75:.3f}"
        )

    return (
        f"Average Precision (AP) @[ IoU=0.50:0.95 | area=   all | "
        f"maxDets=100 ] = {self.map50_95:.3f}\n"
        f"Average Precision (AP) @[ IoU=0.50      | area=   all | "
        f"maxDets=100 ] = {self.map50:.3f}\n"
        f"Average Precision (AP) @[ IoU=0.75      | area=   all | "
        f"maxDets=100 ] = {self.map75:.3f}\n"
        f"Average Precision (AP) @[ IoU=0.50:0.95 | area= small | "
        f"maxDets=100 ] = {self.small_objects.map50_95:.3f}\n"
        f"Average Precision (AP) @[ IoU=0.50:0.95 | area=medium | "
        f"maxDets=100 ] = {self.medium_objects.map50_95:.3f}\n"
        f"Average Precision (AP) @[ IoU=0.50:0.95 | area= large | "
        f"maxDets=100 ] = {self.large_objects.map50_95:.3f}"
    )

plot() -> None

Plot the mAP results.

example_plot

Source code in src/supervision/metrics/mean_average_precision.py
def plot(self) -> None:
    """
    Plot the mAP results.

    ![example_plot](
        https://media.roboflow.com/supervision-docs/metrics/mAP_plot_example.png
    ){ align=center width="800" }
    """
    from matplotlib import pyplot as plt

    labels = ["mAP@50:95", "mAP@50", "mAP@75"]
    values = [self.map50_95, self.map50, self.map75]
    colors = [LEGACY_COLOR_PALETTE[0]] * 3

    if self.small_objects is not None:
        labels += ["Small: mAP@50:95", "Small: mAP@50", "Small: mAP@75"]
        values += [
            self.small_objects.map50_95,
            self.small_objects.map50,
            self.small_objects.map75,
        ]
        colors += [LEGACY_COLOR_PALETTE[3]] * 3

    if self.medium_objects is not None:
        labels += ["Medium: mAP@50:95", "Medium: mAP@50", "Medium: mAP@75"]
        values += [
            self.medium_objects.map50_95,
            self.medium_objects.map50,
            self.medium_objects.map75,
        ]
        colors += [LEGACY_COLOR_PALETTE[2]] * 3

    if self.large_objects is not None:
        labels += ["Large: mAP@50:95", "Large: mAP@50", "Large: mAP@75"]
        values += [
            self.large_objects.map50_95,
            self.large_objects.map50,
            self.large_objects.map75,
        ]
        colors += [LEGACY_COLOR_PALETTE[4]] * 3

    plt.rcParams["font.family"] = "monospace"

    _, ax = plt.subplots(figsize=(10, 6))
    ax.set_ylim(0, 1)
    ax.set_ylabel("Value", fontweight="bold")
    ax.set_title("Mean Average Precision", fontweight="bold")

    x_positions = range(len(labels))
    bars = ax.bar(x_positions, values, color=colors, align="center")

    ax.set_xticks(x_positions)
    ax.set_xticklabels(labels, rotation=45, ha="right")

    for bar in bars:
        y_value = bar.get_height()
        ax.text(
            bar.get_x() + bar.get_width() / 2,
            y_value + 0.02,
            f"{y_value:.2f}",
            ha="center",
            va="bottom",
        )

    plt.rcParams["font.family"] = "sans-serif"

    plt.tight_layout()
    plt.show()

to_pandas() -> pd.DataFrame

Convert the result to a pandas DataFrame.

Returns:

Type Description
DataFrame

The result as a DataFrame.

Source code in src/supervision/metrics/mean_average_precision.py
def to_pandas(self) -> pd.DataFrame:
    """
    Convert the result to a pandas DataFrame.

    Returns:
        The result as a DataFrame.
    """
    ensure_pandas_installed()
    import pandas as pd

    pandas_data: dict[str, object] = {
        "mAP@50:95": self.map50_95,
        "mAP@50": self.map50,
        "mAP@75": self.map75,
    }

    if self.small_objects is not None:
        small_objects_df = self.small_objects.to_pandas()
        for key, value in small_objects_df.items():
            pandas_data[f"small_objects_{key}"] = value
    if self.medium_objects is not None:
        medium_objects_df = self.medium_objects.to_pandas()
        for key, value in medium_objects_df.items():
            pandas_data[f"medium_objects_{key}"] = value
    if self.large_objects is not None:
        large_objects_df = self.large_objects.to_pandas()
        for key, value in large_objects_df.items():
            pandas_data[f"large_objects_{key}"] = value

    # Average precisions are currently not included in the DataFrame.
    return pd.DataFrame(
        pandas_data,
        index=[0],
    )

supervision.dataset.formats.coco.get_coco_class_index_mapping(annotations_path: str) -> dict[int, int]

Generates a mapping from sequential class indices to original COCO class ids.

This function is essential when working with models that expect class ids to be zero-indexed and sequential (0 to 79), as opposed to the original COCO dataset where category ids are non-contiguous ranging from 1 to 90 but skipping some ids.

Use Cases
  • Evaluating models trained with COCO-style annotations where class ids are sequential ranging from 0 to 79.
  • Ensuring consistent class indexing across training, inference and evaluation, when using different tools or datasets with COCO format.
  • Reproducing results from models that assume sequential class ids (0 to 79).
How it Works
  • Reads the COCO annotation file in its original format (annotations_path).
  • Extracts and sorts all class names by their original COCO id (1 to 90).
  • Builds a mapping from COCO class ids (not sequential with skipped ids) to new class ids (sequential ranging from 0 to 79).
  • Returns a dictionary mapping: {new_class_id: original_COCO_class_id}.

Parameters:

Name Type Description Default

annotations_path

str

Path to COCO JSON annotations file

required

Returns:

Type Description
dict[int, int]

A mapping from new class id (sequential ranging from 0 to 79)

dict[int, int]

to original COCO class id (1 to 90 with skipped ids).

Examples:

>>> import json
>>> import os
>>> import tempfile
>>> from supervision.dataset.formats.coco import get_coco_class_index_mapping
>>> coco_data = {
...     "categories": [
...         {"id": 1, "name": "person"},
...         {"id": 3, "name": "car"},
...     ],
...     "images": [],
...     "annotations": [],
... }
>>> annotations_path = None
>>> try:
...     with tempfile.NamedTemporaryFile(
...         mode="w", suffix=".json", delete=False
...     ) as f:
...         annotations_path = f.name
...         json.dump(coco_data, f)
...     mapping = get_coco_class_index_mapping(annotations_path)
...     print(mapping)
... finally:
...     if annotations_path is not None:
...         os.remove(annotations_path)
{0: 1, 1: 3}
>>> os.path.exists(annotations_path)
False
Source code in src/supervision/dataset/formats/coco.py
def get_coco_class_index_mapping(annotations_path: str) -> dict[int, int]:
    """
    Generates a mapping from sequential class indices to original COCO class ids.

    This function is essential when working with models that expect class ids to be
    zero-indexed and sequential (0 to 79), as opposed to the original COCO
    dataset where category ids are non-contiguous ranging from 1 to 90 but skipping some
    ids.

    Use Cases:
        - Evaluating models trained with COCO-style annotations where class ids
          are sequential ranging from 0 to 79.
        - Ensuring consistent class indexing across training, inference and evaluation,
          when using different tools or datasets with COCO format.
        - Reproducing results from models that assume sequential class ids (0 to 79).

    How it Works:
        - Reads the COCO annotation file in its original format (`annotations_path`).
        - Extracts and sorts all class names by their original COCO id (1 to 90).
        - Builds a mapping from COCO class ids (not sequential with skipped ids) to
          new class ids (sequential ranging from 0 to 79).
        - Returns a dictionary mapping: `{new_class_id: original_COCO_class_id}`.

    Args:
        annotations_path: Path to COCO JSON annotations file
        (e.g., `instances_val2017.json`).

    Returns:
        A mapping from new class id (sequential ranging from 0 to 79)
        to original COCO class id (1 to 90 with skipped ids).

    Examples:
        >>> import json
        >>> import os
        >>> import tempfile
        >>> from supervision.dataset.formats.coco import get_coco_class_index_mapping
        >>> coco_data = {
        ...     "categories": [
        ...         {"id": 1, "name": "person"},
        ...         {"id": 3, "name": "car"},
        ...     ],
        ...     "images": [],
        ...     "annotations": [],
        ... }
        >>> annotations_path = None
        >>> try:
        ...     with tempfile.NamedTemporaryFile(
        ...         mode="w", suffix=".json", delete=False
        ...     ) as f:
        ...         annotations_path = f.name
        ...         json.dump(coco_data, f)
        ...     mapping = get_coco_class_index_mapping(annotations_path)
        ...     print(mapping)
        ... finally:
        ...     if annotations_path is not None:
        ...         os.remove(annotations_path)
        {0: 1, 1: 3}
        >>> os.path.exists(annotations_path)
        False
    """
    coco_data = read_json_file(annotations_path)
    classes = coco_categories_to_classes(coco_categories=coco_data["categories"])
    class_mapping = build_coco_class_index_mapping(
        coco_categories=coco_data["categories"], target_classes=classes
    )
    return {v: k for k, v in class_mapping.items()}

Comments