diff --git a/supervision/metrics/__init__.py b/supervision/metrics/__init__.py index c8b01dbc..e452d66c 100644 --- a/supervision/metrics/__init__.py +++ b/supervision/metrics/__init__.py @@ -1,2 +1,2 @@ -from supervision.metrics.core import UnsupportedMetricTargetError, Metric, MetricTarget -from supervision.metrics.intersection_over_union import IntersectionOverUnion \ No newline at end of file +from supervision.metrics.core import Metric, MetricTarget, UnsupportedMetricTargetError +from supervision.metrics.intersection_over_union import IntersectionOverUnion diff --git a/supervision/metrics/core.py b/supervision/metrics/core.py index 1011ab5b..beb3ba76 100644 --- a/supervision/metrics/core.py +++ b/supervision/metrics/core.py @@ -1,4 +1,5 @@ from __future__ import annotations + from abc import ABC, abstractmethod from enum import Enum from typing import Any, Dict, Iterator, Optional, Tuple, Union @@ -12,6 +13,7 @@ from supervision.detection.core import Detections """Used by metrics module as class ID, when none is present""" CLASS_ID_NONE = -1 + class Metric(ABC): """ The base class for all supervision metrics. @@ -43,7 +45,7 @@ class Metric(ABC): Compute the metric from the internal state and return the result. """ raise NotImplementedError - + # TODO: determine if this is necessary. # @abstractmethod # def to_pandas(self, *args, **kwargs) -> Any: @@ -91,11 +93,7 @@ class InternalMetricDataStore: * Provides iteration by class """ - def __init__( - self, - metric_target: MetricTarget, - class_agnostic: bool - ): + def __init__(self, metric_target: MetricTarget, class_agnostic: bool): self._metric_target = metric_target self._class_agnostic = class_agnostic self._data_1: Dict[int, npt.NDArray] @@ -133,8 +131,10 @@ class InternalMetricDataStore: if class_id not in self._data_1: self._data_1[class_id] = content_of_class continue - self._data_1[class_id] = np.vstack((self._data_1[class_id], content_of_class)) - + self._data_1[class_id] = np.vstack( + (self._data_1[class_id], content_of_class) + ) + if content_2 is not None and len(content_2) > 0: assert len(content_2) == len(class_ids_2) for class_id in set(class_ids_2): @@ -142,17 +142,26 @@ class InternalMetricDataStore: if class_id not in self._data_2: self._data_2[class_id] = content_of_class continue - self._data_2[class_id] = np.vstack((self._data_2[class_id], content_of_class)) + self._data_2[class_id] = np.vstack( + (self._data_2[class_id], content_of_class) + ) - def __iter__(self) -> Iterator[Tuple[int, Optional[npt.NDArray], Optional[npt.NDArray]]]: - class_ids = sorted(set.union( - set(self._data_1.keys()), - set(self._data_2.keys()) - )) + def __iter__( + self, + ) -> Iterator[Tuple[int, Optional[npt.NDArray], Optional[npt.NDArray]]]: + class_ids = sorted( + set.union(set(self._data_1.keys()), set(self._data_2.keys())) + ) for class_id in class_ids: - yield class_id, self._data_1.get(class_id, None), self._data_2.get(class_id, None) + yield ( + class_id, + self._data_1.get(class_id, None), + self._data_2.get(class_id, None), + ) - def _get_content(self, data: Union[npt.NDArray, Detections]) -> Optional[npt.NDArray]: + def _get_content( + self, data: Union[npt.NDArray, Detections] + ) -> Optional[npt.NDArray]: """Return boxes, masks or oriented bounding boxes from the data.""" if isinstance(data, np.ndarray): return data @@ -169,7 +178,9 @@ class InternalMetricDataStore: return obb raise ValueError(f"Invalid metric target: {self._metric_target}") - def _get_class_ids(self, data: Union[npt.NDArray, Detections]) -> npt.NDArray[np.int_]: + def _get_class_ids( + self, data: Union[npt.NDArray, Detections] + ) -> npt.NDArray[np.int_]: if self._class_agnostic or isinstance(data, np.ndarray): return np.array([CLASS_ID_NONE] * len(data), dtype=int) assert isinstance(data, Detections) @@ -180,7 +191,9 @@ class InternalMetricDataStore: def _validate_class_ids(self, class_id: npt.NDArray[np.int_]) -> None: class_set = set(class_id) if len(class_set) >= 2 and -1 in class_set: - raise ValueError("Metrics store received results with partially defined classes.") + raise ValueError( + "Metrics store received results with partially defined classes." + ) def _validate_shape(self, data: npt.NDArray) -> None: if self._datapoint_shape is None: diff --git a/supervision/metrics/intersection_over_union.py b/supervision/metrics/intersection_over_union.py index 322329dd..2cb232ff 100644 --- a/supervision/metrics/intersection_over_union.py +++ b/supervision/metrics/intersection_over_union.py @@ -1,14 +1,13 @@ -from typing import Dict, Union, TYPE_CHECKING +from typing import TYPE_CHECKING, Dict, Union import numpy as np import numpy.typing as npt -import supervision.config as config from supervision.detection.core import Detections from supervision.metrics.core import InternalMetricDataStore, Metric, MetricTarget if TYPE_CHECKING: - import pandas as pd + pass class IntersectionOverUnion(Metric): diff --git a/test/metrics/test_core.py b/test/metrics/test_core.py index 0b5ec8b6..d7781c07 100644 --- a/test/metrics/test_core.py +++ b/test/metrics/test_core.py @@ -1,5 +1,4 @@ from contextlib import ExitStack as DoesNotRaise -from test.test_utils import mock_detections from typing import List, Optional, Tuple, Union import numpy as np @@ -8,144 +7,117 @@ import pytest from supervision.detection.core import Detections from supervision.metrics.core import ( + CLASS_ID_NONE, InternalMetricDataStore, MetricTarget, - CLASS_ID_NONE ) -# Boxes, class-agnostic + +# Boxes, class-agnostic @pytest.mark.parametrize( "data_1, data_2, expected_result, exception", [ # Empty - ( - Detections.empty(), - Detections.empty(), - [], - DoesNotRaise() - ), + (Detections.empty(), Detections.empty(), [], DoesNotRaise()), ( np.empty((0, 4), dtype=np.float32), np.empty((0, 4), dtype=np.float32), [], - DoesNotRaise() + DoesNotRaise(), ), - ( - Detections.empty(), - np.empty((0, 4), dtype=np.float32), - [], - DoesNotRaise() - ), - + (Detections.empty(), np.empty((0, 4), dtype=np.float32), [], DoesNotRaise()), # Single box + Empty ( - Detections( - xyxy=np.array([[0, 0, 1, 1]], dtype=np.float32) - ), + Detections(xyxy=np.array([[0, 0, 1, 1]], dtype=np.float32)), Detections.empty(), - [ - ( - CLASS_ID_NONE, - np.array([[0, 0, 1, 1]], dtype=np.float32), - None - ) - ], - DoesNotRaise() + [(CLASS_ID_NONE, np.array([[0, 0, 1, 1]], dtype=np.float32), None)], + DoesNotRaise(), ), ( Detections.empty(), - Detections( - xyxy=np.array([[0, 0, 1, 1]], dtype=np.float32) - ), - [ - ( - CLASS_ID_NONE, - None, - np.array([[0, 0, 1, 1]], dtype=np.float32) - ) - ], - DoesNotRaise() + Detections(xyxy=np.array([[0, 0, 1, 1]], dtype=np.float32)), + [(CLASS_ID_NONE, None, np.array([[0, 0, 1, 1]], dtype=np.float32))], + DoesNotRaise(), ), - # Multiple boxes ( - Detections( - xyxy=np.array([[0, 0, 1, 1], [0, 0, 2, 2]], dtype=np.float32) - ), + Detections(xyxy=np.array([[0, 0, 1, 1], [0, 0, 2, 2]], dtype=np.float32)), Detections.empty(), [ ( CLASS_ID_NONE, np.array([[0, 0, 1, 1], [0, 0, 2, 2]], dtype=np.float32), - None + None, ) ], - DoesNotRaise() + DoesNotRaise(), ), ( - Detections( - xyxy=np.array([[0, 0, 1, 1], [0, 0, 2, 2]], dtype=np.float32) - ), - Detections( - xyxy=np.array([[0, 0, 1, 1], [0, 0, 2, 2]], dtype=np.float32) - ), + Detections(xyxy=np.array([[0, 0, 1, 1], [0, 0, 2, 2]], dtype=np.float32)), + Detections(xyxy=np.array([[0, 0, 1, 1], [0, 0, 2, 2]], dtype=np.float32)), [ ( CLASS_ID_NONE, np.array([[0, 0, 1, 1], [0, 0, 2, 2]], dtype=np.float32), - np.array([[0, 0, 1, 1], [0, 0, 2, 2]], dtype=np.float32) + np.array([[0, 0, 1, 1], [0, 0, 2, 2]], dtype=np.float32), ) ], - DoesNotRaise() + DoesNotRaise(), ), ( - Detections( - xyxy=np.array([[0, 0, 1, 1], [0, 0, 2, 2]], dtype=np.float32) - ), + Detections(xyxy=np.array([[0, 0, 1, 1], [0, 0, 2, 2]], dtype=np.float32)), np.array([[0, 0, 1, 1], [0, 0, 2, 2]], dtype=np.float32), [ ( CLASS_ID_NONE, np.array([[0, 0, 1, 1], [0, 0, 2, 2]], dtype=np.float32), - np.array([[0, 0, 1, 1], [0, 0, 2, 2]], dtype=np.float32) + np.array([[0, 0, 1, 1], [0, 0, 2, 2]], dtype=np.float32), ) ], - DoesNotRaise() + DoesNotRaise(), ), - # with classes - should be ignored. ( Detections( xyxy=np.array([[0, 0, 1, 1], [0, 0, 2, 2]], dtype=np.float32), - class_id=np.array([1, 2], dtype=int) + class_id=np.array([1, 2], dtype=int), ), np.array([[0, 0, 1, 1], [0, 0, 2, 2]], dtype=np.float32), [ ( CLASS_ID_NONE, np.array([[0, 0, 1, 1], [0, 0, 2, 2]], dtype=np.float32), - np.array([[0, 0, 1, 1], [0, 0, 2, 2]], dtype=np.float32) + np.array([[0, 0, 1, 1], [0, 0, 2, 2]], dtype=np.float32), ) ], - DoesNotRaise() + DoesNotRaise(), ), - ] + ], ) def test_store_boxes_class_agnostic( data_1: Union[npt.NDArray, Detections], data_2: Union[npt.NDArray, Detections], expected_result: List[Tuple[int, Optional[npt.NDArray], Optional[npt.NDArray]]], - exception: Exception + exception: Exception, ) -> None: store = InternalMetricDataStore(MetricTarget.BOXES, class_agnostic=True) store.update(data_1, data_2) result = [result for result in store] assert len(result) == len(expected_result) with exception: - for (class_id, content_1, content_2), (expected_class_id, expected_content_1, expected_content_2) in zip(result, expected_result): + for (class_id, content_1, content_2), ( + expected_class_id, + expected_content_1, + expected_content_2, + ) in zip(result, expected_result): assert class_id == expected_class_id - assert (content_1 is None and expected_content_1 is None) or np.array_equal(content_1, expected_content_1) - assert (content_2 is None and expected_content_2 is None) or np.array_equal(content_2, expected_content_2) + assert (content_1 is None and expected_content_1 is None) or np.array_equal( + content_1, expected_content_1 + ) + assert (content_2 is None and expected_content_2 is None) or np.array_equal( + content_2, expected_content_2 + ) + # Boxes, by-class @pytest.mark.parametrize( @@ -155,128 +127,104 @@ def test_store_boxes_class_agnostic( ( Detections( xyxy=np.array([[0, 0, 1, 1]], dtype=np.float32), - class_id=np.array([1], dtype=int) + class_id=np.array([1], dtype=int), ), Detections.empty(), - [ - ( - 1, - np.array([[0, 0, 1, 1]], dtype=np.float32), - None - ) - ], - DoesNotRaise() + [(1, np.array([[0, 0, 1, 1]], dtype=np.float32), None)], + DoesNotRaise(), ), ( Detections.empty(), Detections( xyxy=np.array([[0, 0, 1, 1]], dtype=np.float32), - class_id=np.array([1], dtype=int) + class_id=np.array([1], dtype=int), ), - [ - ( - 1, - None, - np.array([[0, 0, 1, 1]], dtype=np.float32) - ) - ], - DoesNotRaise() + [(1, None, np.array([[0, 0, 1, 1]], dtype=np.float32))], + DoesNotRaise(), ), - # Multiple classes ( Detections( xyxy=np.array([[0, 0, 1, 1], [0, 0, 2, 2]], dtype=np.float32), - class_id=np.array([1, 2], dtype=int) + class_id=np.array([1, 2], dtype=int), ), Detections.empty(), [ - ( - 1, - np.array([[0, 0, 1, 1]], dtype=np.float32), - None - ), - ( - 2, - np.array([[0, 0, 2, 2]], dtype=np.float32), - None - ) + (1, np.array([[0, 0, 1, 1]], dtype=np.float32), None), + (2, np.array([[0, 0, 2, 2]], dtype=np.float32), None), ], - DoesNotRaise() + DoesNotRaise(), ), ( Detections( xyxy=np.array([[0, 0, 1, 1], [0, 0, 2, 2]], dtype=np.float32), - class_id=np.array([1, 2], dtype=int) + class_id=np.array([1, 2], dtype=int), ), Detections( xyxy=np.array([[0, 0, 1, 1], [0, 0, 2, 2]], dtype=np.float32), - class_id=np.array([2, 3], dtype=int) + class_id=np.array([2, 3], dtype=int), ), [ - ( - 1, - np.array([[0, 0, 1, 1]], dtype=np.float32), - None - ), + (1, np.array([[0, 0, 1, 1]], dtype=np.float32), None), ( 2, np.array([[0, 0, 2, 2]], dtype=np.float32), - np.array([[0, 0, 1, 1]], dtype=np.float32) + np.array([[0, 0, 1, 1]], dtype=np.float32), ), - ( - 3, - None, - np.array([[0, 0, 2, 2]], dtype=np.float32) - ) + (3, None, np.array([[0, 0, 2, 2]], dtype=np.float32)), ], - DoesNotRaise() + DoesNotRaise(), ), ( - Detections( - xyxy=np.array([[0, 0, 1, 1], [0, 0, 2, 2]], dtype=np.float32) - ), + Detections(xyxy=np.array([[0, 0, 1, 1], [0, 0, 2, 2]], dtype=np.float32)), np.array([[0, 0, 1, 1], [0, 0, 2, 2]], dtype=np.float32), [ ( CLASS_ID_NONE, np.array([[0, 0, 1, 1], [0, 0, 2, 2]], dtype=np.float32), - np.array([[0, 0, 1, 1], [0, 0, 2, 2]], dtype=np.float32) + np.array([[0, 0, 1, 1], [0, 0, 2, 2]], dtype=np.float32), ) ], - DoesNotRaise() + DoesNotRaise(), ), - # with classes - should be ignored. ( Detections( xyxy=np.array([[0, 0, 1, 1], [0, 0, 2, 2]], dtype=np.float32), - class_id=np.array([1, 2], dtype=int) + class_id=np.array([1, 2], dtype=int), ), np.array([[0, 0, 1, 1], [0, 0, 2, 2]], dtype=np.float32), [ ( CLASS_ID_NONE, np.array([[0, 0, 1, 1], [0, 0, 2, 2]], dtype=np.float32), - np.array([[0, 0, 1, 1], [0, 0, 2, 2]], dtype=np.float32) + np.array([[0, 0, 1, 1], [0, 0, 2, 2]], dtype=np.float32), ) ], - DoesNotRaise() + DoesNotRaise(), ), - ] + ], ) def test_store_boxes_by_class( data_1: Union[npt.NDArray, Detections], data_2: Union[npt.NDArray, Detections], expected_result: List[Tuple[int, Optional[npt.NDArray], Optional[npt.NDArray]]], - exception: Exception + exception: Exception, ) -> None: store = InternalMetricDataStore(MetricTarget.BOXES, class_agnostic=False) store.update(data_1, data_2) result = [result for result in store] assert len(result) == len(expected_result) with exception: - for (class_id, content_1, content_2), (expected_class_id, expected_content_1, expected_content_2) in zip(result, expected_result): + for (class_id, content_1, content_2), ( + expected_class_id, + expected_content_1, + expected_content_2, + ) in zip(result, expected_result): assert class_id == expected_class_id - assert (content_1 is None and expected_content_1 is None) or np.array_equal(content_1, expected_content_1) - assert (content_2 is None and expected_content_2 is None) or np.array_equal(content_2, expected_content_2) + assert (content_1 is None and expected_content_1 is None) or np.array_equal( + content_1, expected_content_1 + ) + assert (content_2 is None and expected_content_2 is None) or np.array_equal( + content_2, expected_content_2 + )