14
14
import contextlib
15
15
import io
16
16
import json
17
- from typing import Any , Callable , Dict , List , Optional , Sequence , Tuple , Union
17
+ from types import ModuleType
18
+ from typing import Any , Callable , ClassVar , Dict , List , Optional , Sequence , Tuple , Union
18
19
19
20
import numpy as np
20
21
import torch
27
28
from torchmetrics .metric import Metric
28
29
from torchmetrics .utilities import rank_zero_warn
29
30
from torchmetrics .utilities .imports import (
31
+ _FASTER_COCO_EVAL_AVAILABLE ,
30
32
_MATPLOTLIB_AVAILABLE ,
31
33
_PYCOCOTOOLS_AVAILABLE ,
32
34
_TORCHVISION_GREATER_EQUAL_0_8 ,
48
50
"MeanAveragePrecision.coco_to_tm" ,
49
51
]
50
52
51
-
52
- if _PYCOCOTOOLS_AVAILABLE :
53
- import pycocotools .mask as mask_utils
54
- from pycocotools .coco import COCO
55
- from pycocotools .cocoeval import COCOeval
56
- else :
57
- COCO , COCOeval = None , None
58
- mask_utils = None
53
+ if not _PYCOCOTOOLS_AVAILABLE :
59
54
__doctest_skip__ = [
60
55
"MeanAveragePrecision.plot" ,
61
56
"MeanAveragePrecision" ,
64
59
]
65
60
66
61
62
+ def _load_backend_tools (backend : Literal ["pycocotools" , "faster_coco_eval" ]) -> Tuple [object , object , ModuleType ]:
63
+ """Load the backend tools for the given backend."""
64
+ if backend == "pycocotools" :
65
+ if not _PYCOCOTOOLS_AVAILABLE :
66
+ raise ModuleNotFoundError (
67
+ "Backend `pycocotools` in metric `MeanAveragePrecision` metric requires that `pycocotools` is"
68
+ " installed. Please install with `pip install pycocotools` or `pip install torchmetrics[detection]`"
69
+ )
70
+ import pycocotools .mask as mask_utils
71
+ from pycocotools .coco import COCO
72
+ from pycocotools .cocoeval import COCOeval
73
+
74
+ return COCO , COCOeval , mask_utils
75
+
76
+ if not _FASTER_COCO_EVAL_AVAILABLE :
77
+ raise ModuleNotFoundError (
78
+ "Backend `faster_coco_eval` in metric `MeanAveragePrecision` metric requires that `faster-coco-eval` is"
79
+ " installed. Please install with `pip install faster-coco-eval`."
80
+ )
81
+ from faster_coco_eval import COCO
82
+ from faster_coco_eval import COCOeval_faster as COCOeval
83
+ from faster_coco_eval .core import mask as mask_utils
84
+
85
+ return COCO , COCOeval , mask_utils
86
+
87
+
67
88
class MeanAveragePrecision (Metric ):
68
89
r"""Compute the `Mean-Average-Precision (mAP) and Mean-Average-Recall (mAR)`_ for object detection predictions.
69
90
@@ -142,9 +163,16 @@ class MeanAveragePrecision(Metric):
142
163
Caution: If the initialization parameters are changed, dictionary keys for mAR can change as well.
143
164
144
165
.. note::
145
- This metric utilizes the official `pycocotools` implementation as its backend. This means that the metric
146
- requires you to have `pycocotools` installed. In addition we require `torchvision` version 0.8.0 or newer.
147
- Please install with ``pip install torchmetrics[detection]``.
166
+ This metric supports, at the moment, two different backends for the evaluation. The default backend is
167
+ ``"pycocotools"``, which either require the official `pycocotools`_ implementation or this
168
+ `fork of pycocotools`_ to be installed. We recommend using the fork as it is better maintained and easily
169
+ available to install via pip: `pip install pycocotools`. It is also this fork that will be installed if you
170
+ install ``torchmetrics[detection]``. The second backend is the `faster-coco-eval`_ implementation, which can be
171
+ installed with ``pip install faster-coco-eval``. This implementation is a maintained open-source implementation
172
+ that is faster and corrects certain corner cases that the official implementation has. Our own testing has shown
173
+ that the results are identical to the official implementation. Regardless of the backend we also require you to
174
+ have `torchvision` version 0.8.0 or newer installed. Please install with ``pip install torchvision>=0.8`` or
175
+ ``pip install torchmetrics[detection]``.
148
176
149
177
Args:
150
178
box_format:
@@ -188,7 +216,9 @@ class MeanAveragePrecision(Metric):
188
216
of max detections per image.
189
217
190
218
average:
191
- Method for averaging scores over labels. Choose between "``macro``"" and "``micro``". Default is "macro"
219
+ Method for averaging scores over labels. Choose between "``"macro"`` and ``"micro"``.
220
+ backend:
221
+ Backend to use for the evaluation. Choose between ``"pycocotools"`` and ``"faster_coco_eval"``.
192
222
193
223
kwargs: Additional keyword arguments, see :ref:`Metric kwargs` for more info.
194
224
@@ -323,6 +353,19 @@ class MeanAveragePrecision(Metric):
323
353
324
354
warn_on_many_detections : bool = True
325
355
356
+ __jit_unused_properties__ : ClassVar [List [str ]] = [
357
+ "is_differentiable" ,
358
+ "higher_is_better" ,
359
+ "plot_lower_bound" ,
360
+ "plot_upper_bound" ,
361
+ "plot_legend_name" ,
362
+ "metric_state" ,
363
+ # below is added for specifically for this metric
364
+ "coco" ,
365
+ "cocoeval" ,
366
+ "mask_utils" ,
367
+ ]
368
+
326
369
def __init__ (
327
370
self ,
328
371
box_format : Literal ["xyxy" , "xywh" , "cxcywh" ] = "xyxy" ,
@@ -333,6 +376,7 @@ def __init__(
333
376
class_metrics : bool = False ,
334
377
extended_summary : bool = False ,
335
378
average : Literal ["macro" , "micro" ] = "macro" ,
379
+ backend : Literal ["pycocotools" , "faster_coco_eval" ] = "pycocotools" ,
336
380
** kwargs : Any ,
337
381
) -> None :
338
382
super ().__init__ (** kwargs )
@@ -387,6 +431,12 @@ def __init__(
387
431
raise ValueError (f"Expected argument `average` to be one of ('macro', 'micro') but got { average } " )
388
432
self .average = average
389
433
434
+ if backend not in ("pycocotools" , "faster_coco_eval" ):
435
+ raise ValueError (
436
+ f"Expected argument `backend` to be one of ('pycocotools', 'faster_coco_eval') but got { backend } "
437
+ )
438
+ self .backend = backend
439
+
390
440
self .add_state ("detection_box" , default = [], dist_reduce_fx = None )
391
441
self .add_state ("detection_mask" , default = [], dist_reduce_fx = None )
392
442
self .add_state ("detection_scores" , default = [], dist_reduce_fx = None )
@@ -397,6 +447,24 @@ def __init__(
397
447
self .add_state ("groundtruth_crowds" , default = [], dist_reduce_fx = None )
398
448
self .add_state ("groundtruth_area" , default = [], dist_reduce_fx = None )
399
449
450
+ @property
451
+ def coco (self ) -> object :
452
+ """Returns the coco module for the given backend, done in this way to make metric picklable."""
453
+ coco , _ , _ = _load_backend_tools (self .backend )
454
+ return coco
455
+
456
+ @property
457
+ def cocoeval (self ) -> object :
458
+ """Returns the coco eval module for the given backend, done in this way to make metric picklable."""
459
+ _ , cocoeval , _ = _load_backend_tools (self .backend )
460
+ return cocoeval
461
+
462
+ @property
463
+ def mask_utils (self ) -> object :
464
+ """Returns the mask utils object for the given backend, done in this way to make metric picklable."""
465
+ _ , _ , mask_utils = _load_backend_tools (self .backend )
466
+ return mask_utils
467
+
400
468
def update (self , preds : List [Dict [str , Tensor ]], target : List [Dict [str , Tensor ]]) -> None :
401
469
"""Update metric state.
402
470
@@ -454,7 +522,7 @@ def compute(self) -> dict:
454
522
for anno in coco_preds .dataset ["annotations" ]:
455
523
anno ["area" ] = anno [f"area_{ i_type } " ]
456
524
457
- coco_eval = COCOeval (coco_target , coco_preds , iouType = i_type )
525
+ coco_eval = self . cocoeval (coco_target , coco_preds , iouType = i_type )
458
526
coco_eval .params .iouThrs = np .array (self .iou_thresholds , dtype = np .float64 )
459
527
coco_eval .params .recThrs = np .array (self .rec_thresholds , dtype = np .float64 )
460
528
coco_eval .params .maxDets = self .max_detection_thresholds
@@ -482,7 +550,7 @@ def compute(self) -> dict:
482
550
# since micro averaging have all the data in one class, we need to reinitialize the coco_eval
483
551
# object in macro mode to get the per class stats
484
552
coco_preds , coco_target = self ._get_coco_datasets (average = "macro" )
485
- coco_eval = COCOeval (coco_target , coco_preds , iouType = i_type )
553
+ coco_eval = self . cocoeval (coco_target , coco_preds , iouType = i_type )
486
554
coco_eval .params .iouThrs = np .array (self .iou_thresholds , dtype = np .float64 )
487
555
coco_eval .params .recThrs = np .array (self .rec_thresholds , dtype = np .float64 )
488
556
coco_eval .params .maxDets = self .max_detection_thresholds
@@ -516,7 +584,7 @@ def compute(self) -> dict:
516
584
517
585
return result_dict
518
586
519
- def _get_coco_datasets (self , average : Literal ["macro" , "micro" ]) -> Tuple [COCO , COCO ]:
587
+ def _get_coco_datasets (self , average : Literal ["macro" , "micro" ]) -> Tuple [object , object ]:
520
588
"""Returns the coco datasets for the target and the predictions."""
521
589
if average == "micro" :
522
590
# for micro averaging we set everything to be the same class
@@ -526,7 +594,7 @@ def _get_coco_datasets(self, average: Literal["macro", "micro"]) -> Tuple[COCO,
526
594
groundtruth_labels = self .groundtruth_labels
527
595
detection_labels = self .detection_labels
528
596
529
- coco_target , coco_preds = COCO (), COCO ()
597
+ coco_target , coco_preds = self . coco (), self . coco ()
530
598
531
599
coco_target .dataset = self ._get_coco_format (
532
600
labels = groundtruth_labels ,
@@ -571,6 +639,7 @@ def coco_to_tm(
571
639
coco_preds : str ,
572
640
coco_target : str ,
573
641
iou_type : Union [Literal ["bbox" , "segm" ], List [str ]] = "bbox" ,
642
+ backend : Literal ["pycocotools" , "faster_coco_eval" ] = "pycocotools" ,
574
643
) -> Tuple [List [Dict [str , Tensor ]], List [Dict [str , Tensor ]]]:
575
644
"""Utility function for converting .json coco format files to the input format of this metric.
576
645
@@ -581,6 +650,7 @@ def coco_to_tm(
581
650
coco_preds: Path to the json file containing the predictions in coco format
582
651
coco_target: Path to the json file containing the targets in coco format
583
652
iou_type: Type of input, either `bbox` for bounding boxes or `segm` for segmentation masks
653
+ backend: Backend to use for the conversion. Either `pycocotools` or `faster_coco_eval`.
584
654
585
655
Returns:
586
656
A tuple containing the predictions and targets in the input format of this metric. Each element of the
@@ -599,9 +669,10 @@ def coco_to_tm(
599
669
600
670
"""
601
671
iou_type = _validate_iou_type_arg (iou_type )
672
+ coco , _ , _ = _load_backend_tools (backend )
602
673
603
674
with contextlib .redirect_stdout (io .StringIO ()):
604
- gt = COCO (coco_target )
675
+ gt = coco (coco_target )
605
676
dt = gt .loadRes (coco_preds )
606
677
607
678
gt_dataset = gt .dataset ["annotations" ]
@@ -748,7 +819,7 @@ def _get_safe_item_values(
748
819
if "segm" in self .iou_type :
749
820
masks = []
750
821
for i in item ["masks" ].cpu ().numpy ():
751
- rle = mask_utils .encode (np .asfortranarray (i ))
822
+ rle = self . mask_utils .encode (np .asfortranarray (i ))
752
823
masks .append ((tuple (rle ["size" ]), rle ["counts" ]))
753
824
output [1 ] = tuple (masks )
754
825
if (output [0 ] is not None and len (output [0 ]) > self .max_detection_thresholds [- 1 ]) or (
@@ -819,10 +890,12 @@ def _get_coco_format(
819
890
if area is not None and area [image_id ][k ].cpu ().tolist () > 0 :
820
891
area_stat = area [image_id ][k ].cpu ().tolist ()
821
892
else :
822
- area_stat = mask_utils .area (image_mask ) if "segm" in self .iou_type else image_box [2 ] * image_box [3 ]
893
+ area_stat = (
894
+ self .mask_utils .area (image_mask ) if "segm" in self .iou_type else image_box [2 ] * image_box [3 ]
895
+ )
823
896
if len (self .iou_type ) > 1 :
824
897
area_stat_box = image_box [2 ] * image_box [3 ]
825
- area_stat_mask = mask_utils .area (image_mask )
898
+ area_stat_mask = self . mask_utils .area (image_mask )
826
899
827
900
annotation = {
828
901
"id" : annotation_id ,
0 commit comments