From 4c928eda2e7592b00bc6d202ccd79d62a887adad Mon Sep 17 00:00:00 2001 From: Soham Date: Thu, 10 Oct 2024 15:22:55 +0530 Subject: [PATCH 01/11] feat: add detection metadata and update related methods --- supervision/detection/core.py | 20 +++++++++++++++++++- supervision/detection/utils.py | 25 ++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index 113948fc..a970cc2d 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -34,6 +34,7 @@ from supervision.detection.utils import ( is_data_equal, mask_to_xyxy, merge_data, + merge_metadata, process_roboflow_result, xywh_to_xyxy, ) @@ -125,6 +126,9 @@ class Detections: data (Dict[str, Union[np.ndarray, List]]): A dictionary containing additional data where each key is a string representing the data type, and the value is either a NumPy array or a list of corresponding data. + metadata (Dict[str, Any]): A dictionary containing collection-level metadata + that applies to the entire set of detections. This may include information such + as the video name, camera parameters, timestamp, or other global metadata. """ # noqa: E501 // docs xyxy: np.ndarray @@ -133,6 +137,7 @@ class Detections: class_id: Optional[np.ndarray] = None tracker_id: Optional[np.ndarray] = None data: Dict[str, Union[np.ndarray, List]] = field(default_factory=dict) + metadata: Dict[str, Any] = field(default_factory=dict) def __post_init__(self): validate_detections_fields( @@ -185,6 +190,7 @@ class Detections: np.array_equal(self.confidence, other.confidence), np.array_equal(self.tracker_id, other.tracker_id), is_data_equal(self.data, other.data), + self.metadata == other.metadata ] ) @@ -985,6 +991,7 @@ class Detections: """ empty_detections = Detections.empty() empty_detections.data = self.data + empty_detections.metadata = self.metadata return self == empty_detections @classmethod @@ -1041,12 +1048,18 @@ class Detections: array([0.1, 0.2, 0.3]) ``` """ + metadata_list = [ + detections.metadata for detections in detections_list + ] + detections_list = [ detections for detections in detections_list if not detections.is_empty() ] + + metadata = merge_metadata(metadata_list) if len(detections_list) == 0: - return Detections.empty() + return Detections.empty(metadata=metadata) for detections in detections_list: validate_detections_fields( @@ -1085,6 +1098,7 @@ class Detections: class_id=class_id, tracker_id=tracker_id, data=data, + metadata=metadata ) def get_anchors_coordinates(self, anchor: Position) -> np.ndarray: @@ -1198,6 +1212,7 @@ class Detections: class_id=self.class_id[index] if self.class_id is not None else None, tracker_id=self.tracker_id[index] if self.tracker_id is not None else None, data=get_data_item(self.data, index), + metadata=self.metadata ) def __setitem__(self, key: str, value: Union[np.ndarray, List]): @@ -1458,6 +1473,8 @@ def merge_inner_detection_object_pair( winning_detection = detections_1 else: winning_detection = detections_2 + + metadata = merge_metadata([detections_1.metadata, detections_2.metadata]) return Detections( xyxy=merged_xyxy, @@ -1466,6 +1483,7 @@ def merge_inner_detection_object_pair( class_id=winning_detection.class_id, tracker_id=winning_detection.tracker_id, data=winning_detection.data, + metadata=metadata ) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 43fcec5a..efc98099 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -1,5 +1,5 @@ from itertools import chain -from typing import Dict, List, Optional, Tuple, Union +from typing import Dict, List, Optional, Tuple, Union, Any import cv2 import numpy as np @@ -865,6 +865,29 @@ def merge_data( return merged_data +def merge_metadata(metadata_list: List[Dict[str, Any]]) -> Dict[str, Any]: + """ + Merge metadata from a list of metadata dictionaries. + + This function combines the metadata dictionaries. If a key appears in more than one + dictionary, the values must be identical for the merge to succeed. + + Args: + metadata_list (List[Dict[str, Any]]): A list of metadata dictionaries to merge. + + Returns: + Dict[str, Any]: A single merged metadata dictionary. + """ + merged_metadata = {} + for metadata in metadata_list: + for key, value in metadata.items(): + if key in merged_metadata: + if merged_metadata[key] != value: + raise ValueError(f"Conflicting metadata for key: {key}") + else: + merged_metadata[key] = value + return merged_metadata + def get_data_item( data: Dict[str, Union[np.ndarray, List]], From b59f867bcb70776aac60dbfcd908b3d906523861 Mon Sep 17 00:00:00 2001 From: Soham Date: Thu, 10 Oct 2024 16:02:20 +0530 Subject: [PATCH 02/11] format with precommit --- supervision/detection/core.py | 18 ++++++++---------- supervision/detection/utils.py | 3 ++- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index a970cc2d..8216d054 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -190,7 +190,7 @@ class Detections: np.array_equal(self.confidence, other.confidence), np.array_equal(self.tracker_id, other.tracker_id), is_data_equal(self.data, other.data), - self.metadata == other.metadata + self.metadata == other.metadata, ] ) @@ -1048,14 +1048,12 @@ class Detections: array([0.1, 0.2, 0.3]) ``` """ - metadata_list = [ - detections.metadata for detections in detections_list - ] - + metadata_list = [detections.metadata for detections in detections_list] + detections_list = [ detections for detections in detections_list if not detections.is_empty() ] - + metadata = merge_metadata(metadata_list) if len(detections_list) == 0: @@ -1098,7 +1096,7 @@ class Detections: class_id=class_id, tracker_id=tracker_id, data=data, - metadata=metadata + metadata=metadata, ) def get_anchors_coordinates(self, anchor: Position) -> np.ndarray: @@ -1212,7 +1210,7 @@ class Detections: class_id=self.class_id[index] if self.class_id is not None else None, tracker_id=self.tracker_id[index] if self.tracker_id is not None else None, data=get_data_item(self.data, index), - metadata=self.metadata + metadata=self.metadata, ) def __setitem__(self, key: str, value: Union[np.ndarray, List]): @@ -1473,7 +1471,7 @@ def merge_inner_detection_object_pair( winning_detection = detections_1 else: winning_detection = detections_2 - + metadata = merge_metadata([detections_1.metadata, detections_2.metadata]) return Detections( @@ -1483,7 +1481,7 @@ def merge_inner_detection_object_pair( class_id=winning_detection.class_id, tracker_id=winning_detection.tracker_id, data=winning_detection.data, - metadata=metadata + metadata=metadata, ) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index efc98099..e9060edb 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -1,5 +1,5 @@ from itertools import chain -from typing import Dict, List, Optional, Tuple, Union, Any +from typing import Any, Dict, List, Optional, Tuple, Union import cv2 import numpy as np @@ -865,6 +865,7 @@ def merge_data( return merged_data + def merge_metadata(metadata_list: List[Dict[str, Any]]) -> Dict[str, Any]: """ Merge metadata from a list of metadata dictionaries. From a988b7282256e3867498d18d302eca7b3014c375 Mon Sep 17 00:00:00 2001 From: Soham Date: Thu, 10 Oct 2024 17:44:52 +0530 Subject: [PATCH 03/11] fix: merge_metadata function --- supervision/detection/utils.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index e9060edb..074f409c 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -884,9 +884,13 @@ def merge_metadata(metadata_list: List[Dict[str, Any]]) -> Dict[str, Any]: for key, value in metadata.items(): if key in merged_metadata: if merged_metadata[key] != value: - raise ValueError(f"Conflicting metadata for key: {key}") + if not isinstance(merged_metadata[key], list): + merged_metadata[key] = [merged_metadata[key]] + if value not in merged_metadata[key]: + merged_metadata[key].append(value) else: merged_metadata[key] = value + return merged_metadata From 6b3735001b95fe56a216fdacdca5926e06f6704b Mon Sep 17 00:00:00 2001 From: Soham Date: Thu, 10 Oct 2024 18:59:50 +0530 Subject: [PATCH 04/11] fix: merge_metadata for conflicting keys --- supervision/detection/utils.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 074f409c..71e67c49 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -878,16 +878,17 @@ def merge_metadata(metadata_list: List[Dict[str, Any]]) -> Dict[str, Any]: Returns: Dict[str, Any]: A single merged metadata dictionary. + + Raises: + ValueError: If there are conflicting values for the same key. """ merged_metadata = {} + for metadata in metadata_list: for key, value in metadata.items(): if key in merged_metadata: if merged_metadata[key] != value: - if not isinstance(merged_metadata[key], list): - merged_metadata[key] = [merged_metadata[key]] - if value not in merged_metadata[key]: - merged_metadata[key].append(value) + raise ValueError(f"Conflicting metadata for key: {key}.") else: merged_metadata[key] = value From 68a1052cfc01307d524ab9fc61bf9942c28a00dc Mon Sep 17 00:00:00 2001 From: Soham Date: Fri, 11 Oct 2024 01:14:00 +0530 Subject: [PATCH 05/11] fix: is_empty no longer compares metadata directly --- supervision/detection/core.py | 1 - 1 file changed, 1 deletion(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index 8216d054..8b470d60 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -991,7 +991,6 @@ class Detections: """ empty_detections = Detections.empty() empty_detections.data = self.data - empty_detections.metadata = self.metadata return self == empty_detections @classmethod From c7611e384bd8e1d12faa641bf5227bfe449c0405 Mon Sep 17 00:00:00 2001 From: Soham Date: Fri, 11 Oct 2024 01:44:47 +0530 Subject: [PATCH 06/11] fix: update empty and is_empty methods --- supervision/detection/core.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index 8b470d60..6ef52a03 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -964,7 +964,7 @@ class Detections: ) @classmethod - def empty(cls) -> Detections: + def empty(cls, metadata: Optional[Dict[str, Any]] = None) -> Detections: """ Create an empty Detections object with no bounding boxes, confidences, or class IDs. @@ -983,15 +983,21 @@ class Detections: xyxy=np.empty((0, 4), dtype=np.float32), confidence=np.array([], dtype=np.float32), class_id=np.array([], dtype=int), + metadata=metadata if metadata is not None else {}, ) def is_empty(self) -> bool: """ Returns `True` if the `Detections` object is considered empty. """ - empty_detections = Detections.empty() - empty_detections.data = self.data - return self == empty_detections + return ( + len(self.xyxy) == 0 + and (self.mask is None or len(self.mask) == 0) + and (self.class_id is None or len(self.class_id) == 0) + and (self.confidence is None or len(self.confidence) == 0) + and (self.tracker_id is None or len(self.tracker_id) == 0) + and not self.data + ) @classmethod def merge(cls, detections_list: List[Detections]) -> Detections: From 4ce033c4d7cada60b2493d56eb4dada82bdaf39f Mon Sep 17 00:00:00 2001 From: Soham Date: Wed, 16 Oct 2024 11:51:13 +0530 Subject: [PATCH 07/11] fix: made required changes --- supervision/detection/core.py | 17 ++++++++--------- supervision/detection/utils.py | 28 +++++++++++++++++++++++++--- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index 6ef52a03..8e2d06e2 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -32,6 +32,7 @@ from supervision.detection.utils import ( extract_ultralytics_masks, get_data_item, is_data_equal, + is_metadata_equal, mask_to_xyxy, merge_data, merge_metadata, @@ -190,7 +191,7 @@ class Detections: np.array_equal(self.confidence, other.confidence), np.array_equal(self.tracker_id, other.tracker_id), is_data_equal(self.data, other.data), - self.metadata == other.metadata, + is_metadata_equal(self.metadata, other.metadata), ] ) @@ -979,6 +980,9 @@ class Detections: empty_detections = Detections.empty() ``` """ + if metadata is not None and not isinstance(metadata, dict): + raise TypeError("Metadata must be a dictionary.") + return cls( xyxy=np.empty((0, 4), dtype=np.float32), confidence=np.array([], dtype=np.float32), @@ -990,14 +994,9 @@ class Detections: """ Returns `True` if the `Detections` object is considered empty. """ - return ( - len(self.xyxy) == 0 - and (self.mask is None or len(self.mask) == 0) - and (self.class_id is None or len(self.class_id) == 0) - and (self.confidence is None or len(self.confidence) == 0) - and (self.tracker_id is None or len(self.tracker_id) == 0) - and not self.data - ) + empty_detections = Detections.empty() + empty_detections.data = self.data + return self == empty_detections @classmethod def merge(cls, detections_list: List[Detections]) -> Detections: diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 71e67c49..421c4a6d 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -808,6 +808,21 @@ def is_data_equal(data_a: Dict[str, np.ndarray], data_b: Dict[str, np.ndarray]) ) +def is_metadata_equal(metadata_a: Dict[str, Any], metadata_b: Dict[str, Any]) -> bool: + """ + Compares the metadata payloads of two Detections instances. + + Args: + metadata_a, metadata_b: The metadata payloads of the instances. + + Returns: + True if the metadata payloads are equal, False otherwise. + """ + return set(metadata_a.keys()) == set(metadata_b.keys()) and all( + np.array_equal(metadata_a[key], metadata_b[key]) for key in metadata_a + ) + + def merge_data( data_list: List[Dict[str, Union[npt.NDArray[np.generic], List]]], ) -> Dict[str, Union[npt.NDArray[np.generic], List]]: @@ -880,15 +895,22 @@ def merge_metadata(metadata_list: List[Dict[str, Any]]) -> Dict[str, Any]: Dict[str, Any]: A single merged metadata dictionary. Raises: - ValueError: If there are conflicting values for the same key. + ValueError: If there are conflicting values for the same key or if + dictionaries have different keys. """ - merged_metadata = {} + if not metadata_list: + return {} + all_keys_sets = [set(metadata.keys()) for metadata in metadata_list] + if not all(keys_set == all_keys_sets[0] for keys_set in all_keys_sets): + raise ValueError("All metadata dictionaries must have the same keys to merge.") + + merged_metadata = {} for metadata in metadata_list: for key, value in metadata.items(): if key in merged_metadata: if merged_metadata[key] != value: - raise ValueError(f"Conflicting metadata for key: {key}.") + raise ValueError(f"Conflicting metadata for key: '{key}'.") else: merged_metadata[key] = value From c6e701be1c702b22d57ddfd103de80e9085b1471 Mon Sep 17 00:00:00 2001 From: Soham Date: Wed, 16 Oct 2024 19:30:28 +0530 Subject: [PATCH 08/11] fix: added check if not array --- supervision/detection/utils.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 421c4a6d..d80a4dcb 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -819,7 +819,11 @@ def is_metadata_equal(metadata_a: Dict[str, Any], metadata_b: Dict[str, Any]) -> True if the metadata payloads are equal, False otherwise. """ return set(metadata_a.keys()) == set(metadata_b.keys()) and all( - np.array_equal(metadata_a[key], metadata_b[key]) for key in metadata_a + np.array_equal(metadata_a[key], metadata_b[key]) + if isinstance(metadata_a[key], np.ndarray) + and isinstance(metadata_b[key], np.ndarray) + else metadata_a[key] == metadata_b[key] + for key in metadata_a ) From 5cce8a159934b4c97d390dfdd30bf1c0a54bb4d0 Mon Sep 17 00:00:00 2001 From: LinasKo Date: Mon, 4 Nov 2024 12:23:56 +0200 Subject: [PATCH 09/11] Remoe Detections.empty metadata field --- supervision/detection/core.py | 16 ++++++---------- supervision/detection/utils.py | 14 +++++++++++--- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/supervision/detection/core.py b/supervision/detection/core.py index 8e2d06e2..32753a30 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -965,7 +965,7 @@ class Detections: ) @classmethod - def empty(cls, metadata: Optional[Dict[str, Any]] = None) -> Detections: + def empty(cls) -> Detections: """ Create an empty Detections object with no bounding boxes, confidences, or class IDs. @@ -980,14 +980,10 @@ class Detections: empty_detections = Detections.empty() ``` """ - if metadata is not None and not isinstance(metadata, dict): - raise TypeError("Metadata must be a dictionary.") - return cls( xyxy=np.empty((0, 4), dtype=np.float32), confidence=np.array([], dtype=np.float32), class_id=np.array([], dtype=int), - metadata=metadata if metadata is not None else {}, ) def is_empty(self) -> bool: @@ -996,6 +992,7 @@ class Detections: """ empty_detections = Detections.empty() empty_detections.data = self.data + empty_detections.metadata = self.metadata return self == empty_detections @classmethod @@ -1052,16 +1049,12 @@ class Detections: array([0.1, 0.2, 0.3]) ``` """ - metadata_list = [detections.metadata for detections in detections_list] - detections_list = [ detections for detections in detections_list if not detections.is_empty() ] - metadata = merge_metadata(metadata_list) - if len(detections_list) == 0: - return Detections.empty(metadata=metadata) + return Detections.empty() for detections in detections_list: validate_detections_fields( @@ -1093,6 +1086,9 @@ class Detections: data = merge_data([d.data for d in detections_list]) + metadata_list = [detections.metadata for detections in detections_list] + metadata = merge_metadata(metadata_list) + return cls( xyxy=xyxy, mask=mask, diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index d80a4dcb..c5990b42 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -820,8 +820,10 @@ def is_metadata_equal(metadata_a: Dict[str, Any], metadata_b: Dict[str, Any]) -> """ return set(metadata_a.keys()) == set(metadata_b.keys()) and all( np.array_equal(metadata_a[key], metadata_b[key]) - if isinstance(metadata_a[key], np.ndarray) - and isinstance(metadata_b[key], np.ndarray) + if ( + isinstance(metadata_a[key], np.ndarray) + and isinstance(metadata_b[key], np.ndarray) + ) else metadata_a[key] == metadata_b[key] for key in metadata_a ) @@ -833,6 +835,9 @@ def merge_data( """ Merges the data payloads of a list of Detections instances. + Warning: Assumes that empty detections were filtered-out before passing data to + this function. + Args: data_list: The data payloads of the Detections instances. Each data payload is a dictionary with the same keys, and the values are either lists or @@ -892,6 +897,9 @@ def merge_metadata(metadata_list: List[Dict[str, Any]]) -> Dict[str, Any]: This function combines the metadata dictionaries. If a key appears in more than one dictionary, the values must be identical for the merge to succeed. + Warning: Assumes that empty detections were filtered-out before passing metadata to + this function. + Args: metadata_list (List[Dict[str, Any]]): A list of metadata dictionaries to merge. @@ -909,7 +917,7 @@ def merge_metadata(metadata_list: List[Dict[str, Any]]) -> Dict[str, Any]: if not all(keys_set == all_keys_sets[0] for keys_set in all_keys_sets): raise ValueError("All metadata dictionaries must have the same keys to merge.") - merged_metadata = {} + merged_metadata: Dict[str, Any] = {} for metadata in metadata_list: for key, value in metadata.items(): if key in merged_metadata: From d645bd816e454594adf972594ae2dea980392cd9 Mon Sep 17 00:00:00 2001 From: LinasKo Date: Mon, 4 Nov 2024 12:41:16 +0200 Subject: [PATCH 10/11] Fix: Add metadata to public var retrieval test --- test/utils/test_internal.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/utils/test_internal.py b/test/utils/test_internal.py index eee614e6..ff9825e2 100644 --- a/test/utils/test_internal.py +++ b/test/utils/test_internal.py @@ -121,7 +121,7 @@ class MockDataclass: ( Detections.empty(), False, - {"xyxy", "class_id", "confidence", "mask", "tracker_id", "data"}, + {"xyxy", "class_id", "confidence", "mask", "tracker_id", "data", "metadata"}, DoesNotRaise(), ), ( @@ -134,6 +134,7 @@ class MockDataclass: "mask", "tracker_id", "data", + "metadata", "area", "box_area", }, @@ -149,6 +150,7 @@ class MockDataclass: "mask", "tracker_id", "data", + "metadata" }, DoesNotRaise(), ), @@ -169,13 +171,14 @@ class MockDataclass: "mask", "tracker_id", "data", + "metadata" }, DoesNotRaise(), ), ( Detections.empty(), False, - {"xyxy", "class_id", "confidence", "mask", "tracker_id", "data"}, + {"xyxy", "class_id", "confidence", "mask", "tracker_id", "data", "metadata"}, DoesNotRaise(), ), ], From 9ee1b5dc5ab691a552f0e67e6f4ad5bfea9c2ea2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 10:41:52 +0000 Subject: [PATCH 11/11] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20auto?= =?UTF-8?q?=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/utils/test_internal.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/test/utils/test_internal.py b/test/utils/test_internal.py index ff9825e2..872822a7 100644 --- a/test/utils/test_internal.py +++ b/test/utils/test_internal.py @@ -121,7 +121,15 @@ class MockDataclass: ( Detections.empty(), False, - {"xyxy", "class_id", "confidence", "mask", "tracker_id", "data", "metadata"}, + { + "xyxy", + "class_id", + "confidence", + "mask", + "tracker_id", + "data", + "metadata", + }, DoesNotRaise(), ), ( @@ -150,7 +158,7 @@ class MockDataclass: "mask", "tracker_id", "data", - "metadata" + "metadata", }, DoesNotRaise(), ), @@ -171,14 +179,22 @@ class MockDataclass: "mask", "tracker_id", "data", - "metadata" + "metadata", }, DoesNotRaise(), ), ( Detections.empty(), False, - {"xyxy", "class_id", "confidence", "mask", "tracker_id", "data", "metadata"}, + { + "xyxy", + "class_id", + "confidence", + "mask", + "tracker_id", + "data", + "metadata", + }, DoesNotRaise(), ), ],