From 6a8f3fc03bfbfe4ea7abd436f571a6df9e1e9c76 Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Tue, 4 Jul 2023 21:47:56 +0200 Subject: [PATCH 01/49] add detection module --- supervision/metrics/__init__.py | 0 supervision/metrics/detection.py | 229 +++++++++++++++++++++++++++++++ test/metrics/__init__.py | 0 test/metrics/test_detection.py | 0 4 files changed, 229 insertions(+) create mode 100644 supervision/metrics/__init__.py create mode 100644 supervision/metrics/detection.py create mode 100644 test/metrics/__init__.py create mode 100644 test/metrics/test_detection.py diff --git a/supervision/metrics/__init__.py b/supervision/metrics/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/supervision/metrics/detection.py b/supervision/metrics/detection.py new file mode 100644 index 00000000..760b63c4 --- /dev/null +++ b/supervision/metrics/detection.py @@ -0,0 +1,229 @@ +from dataclasses import dataclass +from typing import Callable, List, Optional + +import matplotlib.pyplot as plt +import numpy as np +import seaborn as sn +from matplotlib import rcParams + +import supervision as sv +from supervision.detection.utils import box_iou_batch + + +@dataclass +class ConfusionMatrix: + matrix: np.ndarray + classes: List[str] + num_classes: int + conf_threshold: float + iou_threshold: float + + @classmethod + def from_detections( + cls, + predictions: List[sv.Detections], + target: List[sv.Detections], + classes: List[str], + conf_threshold: float = 0.3, + iou_threshold: float = 0.5, + ) -> "ConfusionMatrix": + """ + Calculate confusion matrix based on ground-true and detected objects across all images in concerned dataset. + + Args: + target: representing ground-truth objects across all images in concerned dataset. Each element of `target` list describe single image and has `shape = (N, 5)` where `N` is number of ground-truth objects. Each row is an sv.Detections object + predictions: representing detected objects across all images in concerned dataset. Each element of `detection_batches` list describe single image and has `shape = (M, 1)` where `M` is number of detected objects. Each row is an sv.Detections object + classes: all known classes. + conf_threshold: detection confidence threshold between 0 and 1. Detections with lower confidence will be excluded. + iou_threshold: detection iou threshold between 0 and 1. Detections with lower iou will be classified as FP. + + Returns: + confusion_matrix: `ConfusionMatrix` object raw confusion matrix 2d `np.ndarray`. + + Example: + ``` + >>> import numpy as np + + >>> from onemetric.cv.object_detection import ConfusionMatrix + + >>> target = [ + ... np.array([ + ... [0.0, 0.0, 3.0, 3.0, 1], + ... [2.0, 2.0, 5.0, 5.0, 1], + ... [6.0, 1.0, 8.0, 3.0, 2], + ... ]), + ... np.array([ + ... [1.0, 1.0, 2.0, 2.0, 2], + ... ]), + ... ] + + >>> detection_batches = [ + ... np.array([ + ... [0.0, 0.0, 3.0, 3.0, 1, 0.9], + ... [0.1, 0.1, 3.0, 3.0, 0, 0.9], + ... [6.0, 1.0, 8.0, 3.0, 1, 0.8], + ... [1.0, 6.0, 2.0, 7.0, 1, 0.8], + ... ]), + ... np.array([ + ... [1.0, 1.0, 2.0, 2.0, 2, 0.8], + ... ]), + ... ] + + >>> confusion_matrix = ConfusionMatrix.from_detections( + ... target=target, + ... detection_batches=detection_batches, + ... num_classes=3 + ... ) + + >>> confusion_matrix.matrix + ... array([ + ... [0., 0., 0., 0.], + ... [0., 1., 0., 1.], + ... [0., 1., 1., 0.], + ... [1., 1., 0., 0.] + ... ]) + ``` + """ + # validate_detections( + # target=target, detection_batches=detection_batches + # ) + num_classes = len(classes) + matrix = np.zeros((num_classes + 1, num_classes + 1)) + for true_batch, detection_batch in zip(target, predictions): + matrix += ConfusionMatrix._evaluate_detection_batch( + true_detections=true_batch, + pred_detections=detection_batch, + num_classes=num_classes, + conf_threshold=conf_threshold, + iou_threshold=iou_threshold, + ) + return cls( + matrix=matrix, + num_classes=num_classes, + classes=classes, + conf_threshold=conf_threshold, + iou_threshold=iou_threshold, + ) + + @staticmethod + def _evaluate_detection_batch( + true_detections: sv.Detections, + pred_detections: sv.Detections, + num_classes: int, + conf_threshold: float, + iou_threshold: float, + ) -> np.ndarray: + # validate_true_batch(true_batch=true_batch) + # validate_detection_batch(detection_batch=detection_batch) + + result_matrix = np.zeros((num_classes + 1, num_classes + 1)) + detection_batch_filtered = pred_detections[ + pred_detections.confidence > conf_threshold + ] + true_classes = true_detections.class_id + detection_classes = detection_batch_filtered.class_id + true_boxes = true_detections.xyxy + detection_boxes = detection_batch_filtered.xyxy + + # ConfusionMatrix._validate_classes_idx_values(classes=true_classes, num_classes=num_classes) + # ConfusionMatrix._validate_classes_idx_values(classes=detection_classes, num_classes=num_classes) + + iou_batch = box_iou_batch( + boxes_true=true_boxes, boxes_detection=detection_boxes + ) + matched_idx = np.asarray(iou_batch > iou_threshold).nonzero() + + if matched_idx[0].shape[0]: + matches = np.stack( + (matched_idx[0], matched_idx[1], iou_batch[matched_idx]), axis=1 + ) + # matches = ConfusionMatrix._drop_extra_matches(matches=matches) + else: + matches = np.zeros((0, 3)) + + matched_true_idx, matched_detection_idx, _ = matches.transpose().astype( + np.int16 + ) + + for i, true_class_value in enumerate(true_classes): + j = matched_true_idx == i + if matches.shape[0] > 0 and sum(j) == 1: + result_matrix[ + true_class_value, detection_classes[matched_detection_idx[j]] + ] += 1 # TP + else: + result_matrix[true_class_value, num_classes] += 1 # FN + + for i, detection_class_value in enumerate(detection_classes): + if not any(matched_detection_idx == i): + result_matrix[num_classes, detection_class_value] += 1 # FP + + return result_matrix + + def plot( + self, + target_path: str, + title: Optional[str] = None, + class_names: Optional[List[str]] = None, + normalize: bool = True, + ) -> None: + """ + Create confusion matrix plot and save it at selected location. + + Args: + target_path: `str` selected target location of confusion matrix plot. + title: `Optional[str]` title displayed at the top of the confusion matrix plot. Default `None`. + class_names: `Optional[List[str]]` list of class names detected my model. If non given class indexes will be used. Default `None`. + normalize: `bool` if set to `False` chart will display absolute number of detections falling into given category. Otherwise percentage of detections will be displayed. + """ + plt.rcParams["font.family"] = "sans-serif" + plt.rcParams["font.sans-serif"] = ["Verdana"] + + array = self.matrix.copy() + + if normalize: + array = array / (array.sum(0).reshape(1, self.num_classes + 1) + EPSILON) + + array[array < 0.005] = np.nan + + fig = plt.figure(figsize=(12, 10), tight_layout=True, facecolor="white") + sn.set(font_scale=1.0 if self.num_classes < 50 else 0.8) + + labels = ( + class_names is not None + and (0 < len(class_names) < 99) + and len(class_names) == self.num_classes + ) + x_tick_labels = class_names + ["FN"] if labels else "auto" + y_tick_labels = class_names + ["FP"] if labels else "auto" + sn.heatmap( + array, + annot=self.num_classes < 30, + annot_kws={"size": 8}, + fmt=".2f", + square=True, + vmin=0, + cmap="Blues", + xticklabels=x_tick_labels, + yticklabels=y_tick_labels, + ).set_facecolor((1, 1, 1)) + + if title: + fig.axes[0].set_title(title, fontsize=20) + + fig.axes[0].set_xlabel("Predicted") + fig.axes[0].set_ylabel("True") + fig.axes[0].set_facecolor("white") + fig.savefig( + target_path, dpi=250, facecolor=fig.get_facecolor(), transparent=True + ) + + @classmethod + def benchmark( + cls, + dataset: sv.DetectionDataset, + callback: Callable[[np.ndarray], sv.Detections], + conf_threshold: float = 0.3, + iou_threshold: float = 0.5, + ) -> "ConfusionMatrix": + pass diff --git a/test/metrics/__init__.py b/test/metrics/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/metrics/test_detection.py b/test/metrics/test_detection.py new file mode 100644 index 00000000..e69de29b From e1ee80cb37e540ece2241a31f46b120897d94ac8 Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Tue, 4 Jul 2023 22:03:10 +0200 Subject: [PATCH 02/49] add docstring to from_detections --- supervision/metrics/detection.py | 58 +++++++++++++++++--------------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/supervision/metrics/detection.py b/supervision/metrics/detection.py index 760b63c4..06c6b27d 100644 --- a/supervision/metrics/detection.py +++ b/supervision/metrics/detection.py @@ -42,36 +42,41 @@ class ConfusionMatrix: Example: ``` - >>> import numpy as np - - >>> from onemetric.cv.object_detection import ConfusionMatrix + >>> from supervision.metrics.detection import ConfusionMatrix >>> target = [ - ... np.array([ - ... [0.0, 0.0, 3.0, 3.0, 1], - ... [2.0, 2.0, 5.0, 5.0, 1], - ... [6.0, 1.0, 8.0, 3.0, 2], - ... ]), - ... np.array([ - ... [1.0, 1.0, 2.0, 2.0, 2], - ... ]), + ... Detections(xyxy=array([ + ... [ 0.0, 0.0, 3.0, 3.0 ], + ... [ 2.0, 2.0, 5.0, 5.0 ], + ... [ 6.0, 1.0, 8.0, 3.0 ], + ... ]), confidence=array([ 1.0, 1.0, 1.0, 1.0 ]), class_id=array([1, 1, 2])), + ... Detections(xyxy=array([ + ... [ 1.0, 1.0, 2.0, 2.0 ], + ... ]), confidence=array([ 1.0 ]), class_id=array([2])) ... ] - - >>> detection_batches = [ - ... np.array([ - ... [0.0, 0.0, 3.0, 3.0, 1, 0.9], - ... [0.1, 0.1, 3.0, 3.0, 0, 0.9], - ... [6.0, 1.0, 8.0, 3.0, 1, 0.8], - ... [1.0, 6.0, 2.0, 7.0, 1, 0.8], - ... ]), - ... np.array([ - ... [1.0, 1.0, 2.0, 2.0, 2, 0.8], - ... ]), + >>> predictions = [ + ... Detections( + ... xyxy=array([ + ... [ 0.0, 0.0, 3.0, 3.0 ], + ... [ 0.1, 0.1, 3.0, 3.0 ], + ... [ 6.0, 1.0, 8.0, 3.0 ], + ... [ 1.0, 6.0, 2.0, 7.0 ], + ... ]), + ... confidence=array([ 0.9, 0.9, 0.8, 0.8 ]), + ... class_id=array([1, 0, 1, 1]) + ... ), + ... Detections( + ... xyxy=array([ + ... [ 1.0, 1.0, 2.0, 2.0 ] + ... ]), + ... confidence=array([ 0.8 ]), + ... class_id=array([2]) + ... ) ... ] >>> confusion_matrix = ConfusionMatrix.from_detections( + ... predictions=predictions, ... target=target, - ... detection_batches=detection_batches, ... num_classes=3 ... ) @@ -84,9 +89,8 @@ class ConfusionMatrix: ... ]) ``` """ - # validate_detections( - # target=target, detection_batches=detection_batches - # ) + # TODO: add validation for inputs + num_classes = len(classes) matrix = np.zeros((num_classes + 1, num_classes + 1)) for true_batch, detection_batch in zip(target, predictions): @@ -113,8 +117,6 @@ class ConfusionMatrix: conf_threshold: float, iou_threshold: float, ) -> np.ndarray: - # validate_true_batch(true_batch=true_batch) - # validate_detection_batch(detection_batch=detection_batch) result_matrix = np.zeros((num_classes + 1, num_classes + 1)) detection_batch_filtered = pred_detections[ From 43cca52a58824fc50784418a4bfba06f1ad75110 Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Tue, 4 Jul 2023 22:04:38 +0200 Subject: [PATCH 03/49] add _drop_extra_matches from onemetric --- supervision/metrics/detection.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/supervision/metrics/detection.py b/supervision/metrics/detection.py index 06c6b27d..ebe3a95f 100644 --- a/supervision/metrics/detection.py +++ b/supervision/metrics/detection.py @@ -127,9 +127,6 @@ class ConfusionMatrix: true_boxes = true_detections.xyxy detection_boxes = detection_batch_filtered.xyxy - # ConfusionMatrix._validate_classes_idx_values(classes=true_classes, num_classes=num_classes) - # ConfusionMatrix._validate_classes_idx_values(classes=detection_classes, num_classes=num_classes) - iou_batch = box_iou_batch( boxes_true=true_boxes, boxes_detection=detection_boxes ) @@ -139,7 +136,7 @@ class ConfusionMatrix: matches = np.stack( (matched_idx[0], matched_idx[1], iou_batch[matched_idx]), axis=1 ) - # matches = ConfusionMatrix._drop_extra_matches(matches=matches) + matches = ConfusionMatrix._drop_extra_matches(matches=matches) else: matches = np.zeros((0, 3)) @@ -162,6 +159,15 @@ class ConfusionMatrix: return result_matrix + @staticmethod + def _drop_extra_matches(matches: np.ndarray) -> np.ndarray: + if matches.shape[0] > 0: + matches = matches[matches[:, 2].argsort()[::-1]] + matches = matches[np.unique(matches[:, 1], return_index=True)[1]] + matches = matches[matches[:, 2].argsort()[::-1]] + matches = matches[np.unique(matches[:, 0], return_index=True)[1]] + return matches + def plot( self, target_path: str, From c9d2b52900a4ba442b1ed660bfa0a3760adc2fb4 Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Tue, 4 Jul 2023 22:07:18 +0200 Subject: [PATCH 04/49] minor doc update --- supervision/metrics/detection.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/supervision/metrics/detection.py b/supervision/metrics/detection.py index ebe3a95f..51a9da5a 100644 --- a/supervision/metrics/detection.py +++ b/supervision/metrics/detection.py @@ -3,7 +3,6 @@ from typing import Callable, List, Optional import matplotlib.pyplot as plt import numpy as np -import seaborn as sn from matplotlib import rcParams import supervision as sv @@ -184,13 +183,16 @@ class ConfusionMatrix: class_names: `Optional[List[str]]` list of class names detected my model. If non given class indexes will be used. Default `None`. normalize: `bool` if set to `False` chart will display absolute number of detections falling into given category. Otherwise percentage of detections will be displayed. """ - plt.rcParams["font.family"] = "sans-serif" - plt.rcParams["font.sans-serif"] = ["Verdana"] + # TODO: replace seaborn with matplotlib + import seaborn as sn + + rcParams["font.family"] = "sans-serif" + rcParams["font.sans-serif"] = ["Verdana"] array = self.matrix.copy() if normalize: - array = array / (array.sum(0).reshape(1, self.num_classes + 1) + EPSILON) + array = array / (array.sum(0).reshape(1, self.num_classes + 1) + 1e-8) array[array < 0.005] = np.nan From 35d9ee7bad2197479b082a79c0120ae53cd2d686 Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Wed, 5 Jul 2023 14:08:39 +0200 Subject: [PATCH 05/49] non-int ticks --- supervision/metrics/detection.py | 93 ++++++++++++++++++++++---------- 1 file changed, 64 insertions(+), 29 deletions(-) diff --git a/supervision/metrics/detection.py b/supervision/metrics/detection.py index 51a9da5a..9d6977e1 100644 --- a/supervision/metrics/detection.py +++ b/supervision/metrics/detection.py @@ -60,15 +60,15 @@ class ConfusionMatrix: ... [ 0.1, 0.1, 3.0, 3.0 ], ... [ 6.0, 1.0, 8.0, 3.0 ], ... [ 1.0, 6.0, 2.0, 7.0 ], - ... ]), - ... confidence=array([ 0.9, 0.9, 0.8, 0.8 ]), + ... ]), + ... confidence=array([ 0.9, 0.9, 0.8, 0.8 ]), ... class_id=array([1, 0, 1, 1]) ... ), ... Detections( ... xyxy=array([ ... [ 1.0, 1.0, 2.0, 2.0 ] - ... ]), - ... confidence=array([ 0.8 ]), + ... ]), + ... confidence=array([ 0.8 ]), ... class_id=array([2]) ... ) ... ] @@ -89,7 +89,7 @@ class ConfusionMatrix: ``` """ # TODO: add validation for inputs - + num_classes = len(classes) matrix = np.zeros((num_classes + 1, num_classes + 1)) for true_batch, detection_batch in zip(target, predictions): @@ -116,7 +116,6 @@ class ConfusionMatrix: conf_threshold: float, iou_threshold: float, ) -> np.ndarray: - result_matrix = np.zeros((num_classes + 1, num_classes + 1)) detection_batch_filtered = pred_detections[ pred_detections.confidence > conf_threshold @@ -178,16 +177,18 @@ class ConfusionMatrix: Create confusion matrix plot and save it at selected location. Args: - target_path: `str` selected target location of confusion matrix plot. - title: `Optional[str]` title displayed at the top of the confusion matrix plot. Default `None`. - class_names: `Optional[List[str]]` list of class names detected my model. If non given class indexes will be used. Default `None`. - normalize: `bool` if set to `False` chart will display absolute number of detections falling into given category. Otherwise percentage of detections will be displayed. + target_path: save location of confusion matrix plot. + title: title displayed at the top of the confusion matrix plot. Default `None`. + class_names: list of class names detected my model. If non given class indexes will be used. Default `None`. + normalize: chart will display absolute number of detections falling into given category. Otherwise percentage of detections will be displayed. """ # TODO: replace seaborn with matplotlib + import matplotlib.pyplot as plt import seaborn as sn + from matplotlib import rcParams - rcParams["font.family"] = "sans-serif" - rcParams["font.sans-serif"] = ["Verdana"] + # rcParams["font.family"] = "sans-serif" + # rcParams["font.sans-serif"] = ["Verdana"] array = self.matrix.copy() @@ -196,8 +197,8 @@ class ConfusionMatrix: array[array < 0.005] = np.nan - fig = plt.figure(figsize=(12, 10), tight_layout=True, facecolor="white") - sn.set(font_scale=1.0 if self.num_classes < 50 else 0.8) + fig, ax = plt.subplots(figsize=(12, 10), tight_layout=True, facecolor="white") + # sn.set(font_scale=1.0 if self.num_classes < 50 else 0.8) labels = ( class_names is not None @@ -206,24 +207,58 @@ class ConfusionMatrix: ) x_tick_labels = class_names + ["FN"] if labels else "auto" y_tick_labels = class_names + ["FP"] if labels else "auto" - sn.heatmap( - array, - annot=self.num_classes < 30, - annot_kws={"size": 8}, - fmt=".2f", - square=True, - vmin=0, - cmap="Blues", - xticklabels=x_tick_labels, - yticklabels=y_tick_labels, - ).set_facecolor((1, 1, 1)) + # sn.heatmap( + # array, + # annot=self.num_classes < 30, + # annot_kws={"size": 8}, + # fmt=".2f", + # square=True, + # vmin=0, + # cmap="Blues", + # xticklabels=x_tick_labels, + # yticklabels=y_tick_labels, + # ).set_facecolor((1, 1, 1)) + im = ax.imshow(array, cmap="Blues") + # Create colorbar + cbar_kw = {} + cbarlabel = "" + cbar = ax.figure.colorbar(im, ax=ax, **cbar_kw) + cbar.ax.set_ylabel(cbarlabel, rotation=-90, va="bottom") + cbar.mappable.set_clim(vmin=0, vmax=np.nanmax(array)) + # Disable grid lines in the axis + ax.grid(False) + + # Adjust the z-order of the grid lines + ax.set_axisbelow(True) + ax.yaxis.grid(color='gray', zorder=0) + ax.xaxis.grid(color='gray', zorder=0) + + # Show all ticks and label them with the respective list entries. + ax.set_xticks(np.arange(array.shape[1])-0.5) + ax.set_yticks(np.arange(array.shape[0])-0.5) + # ax.grid(which="major", color="gray", linestyle="-", linewidth=1) + # ax.tick_params(which="minor", bottom=False, left=False) + + # Let the horizontal axes labeling appear on top. + # ax.tick_params(top=True, bottom=False, labeltop=True, labelbottom=False) + + # Rotate the tick labels and set their alignment. + plt.setp(ax.get_xticklabels(), rotation=90, ha="right", rotation_mode="anchor") + + # Turn spines off and create white grid. + # ax.spines[:].set_visible(False) + + # ax.set_xticks(x_tick_labels, minor=True) + # ax.set_yticks(y_tick_labels, minor=True) + # ax.grid(which="minor", color="w", linestyle="-", linewidth=1) + # ax.tick_params(which="minor", bottom=False, left=False) if title: - fig.axes[0].set_title(title, fontsize=20) + ax.set_title(title, fontsize=20) - fig.axes[0].set_xlabel("Predicted") - fig.axes[0].set_ylabel("True") - fig.axes[0].set_facecolor("white") + ax.set_xlabel("Predicted") + ax.set_ylabel("True") + ax.set_facecolor("white") fig.savefig( target_path, dpi=250, facecolor=fig.get_facecolor(), transparent=True ) From 4375e1924536f2c415799f2e7b299a42e787e726 Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Thu, 6 Jul 2023 09:37:00 +0200 Subject: [PATCH 06/49] add benchmark() --- supervision/metrics/detection.py | 59 +++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/supervision/metrics/detection.py b/supervision/metrics/detection.py index 9d6977e1..21fc1559 100644 --- a/supervision/metrics/detection.py +++ b/supervision/metrics/detection.py @@ -21,7 +21,7 @@ class ConfusionMatrix: def from_detections( cls, predictions: List[sv.Detections], - target: List[sv.Detections], + targets: List[sv.Detections], classes: List[str], conf_threshold: float = 0.3, iou_threshold: float = 0.5, @@ -92,7 +92,7 @@ class ConfusionMatrix: num_classes = len(classes) matrix = np.zeros((num_classes + 1, num_classes + 1)) - for true_batch, detection_batch in zip(target, predictions): + for true_batch, detection_batch in zip(targets, predictions): matrix += ConfusionMatrix._evaluate_detection_batch( true_detections=true_batch, pred_detections=detection_batch, @@ -166,6 +166,39 @@ class ConfusionMatrix: matches = matches[np.unique(matches[:, 0], return_index=True)[1]] return matches + @classmethod + def benchmark( + cls, + dataset: sv.DetectionDataset, + callback: Callable[[np.ndarray], sv.Detections], + conf_threshold: float = 0.3, + iou_threshold: float = 0.5, + ) -> "ConfusionMatrix": + """ + Create confusion matrix from dataset and callback function. + + Args: + dataset: an annotated dataset. + callback: a function that takes an image as input and returns detections. + conf_threshold: see ConfusionMatrix.from_detections. + iou_threshold: see ConfusionMatrix.from_detections. + + Returns: + ConfusionMatrix object. + """ + predictions = [] + targets = [] + for img_name, img in dataset.images.items(): + predictions.append(callback(img)) + targets.append(dataset.annotations[img_name]) + return cls.from_detections( + predictions=predictions, + targets=targets, + classes=dataset.classes, + conf_threshold=conf_threshold, + iou_threshold=iou_threshold, + ) + def plot( self, target_path: str, @@ -182,10 +215,6 @@ class ConfusionMatrix: class_names: list of class names detected my model. If non given class indexes will be used. Default `None`. normalize: chart will display absolute number of detections falling into given category. Otherwise percentage of detections will be displayed. """ - # TODO: replace seaborn with matplotlib - import matplotlib.pyplot as plt - import seaborn as sn - from matplotlib import rcParams # rcParams["font.family"] = "sans-serif" # rcParams["font.sans-serif"] = ["Verdana"] @@ -230,12 +259,12 @@ class ConfusionMatrix: # Adjust the z-order of the grid lines ax.set_axisbelow(True) - ax.yaxis.grid(color='gray', zorder=0) - ax.xaxis.grid(color='gray', zorder=0) + ax.yaxis.grid(color="gray", zorder=0) + ax.xaxis.grid(color="gray", zorder=0) # Show all ticks and label them with the respective list entries. - ax.set_xticks(np.arange(array.shape[1])-0.5) - ax.set_yticks(np.arange(array.shape[0])-0.5) + ax.set_xticks(np.arange(array.shape[1]) - 0.5) + ax.set_yticks(np.arange(array.shape[0]) - 0.5) # ax.grid(which="major", color="gray", linestyle="-", linewidth=1) # ax.tick_params(which="minor", bottom=False, left=False) @@ -262,13 +291,3 @@ class ConfusionMatrix: fig.savefig( target_path, dpi=250, facecolor=fig.get_facecolor(), transparent=True ) - - @classmethod - def benchmark( - cls, - dataset: sv.DetectionDataset, - callback: Callable[[np.ndarray], sv.Detections], - conf_threshold: float = 0.3, - iou_threshold: float = 0.5, - ) -> "ConfusionMatrix": - pass From 6784c78b4f889e59e75dcfc52d40bf279627f817 Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Thu, 6 Jul 2023 22:27:21 +0200 Subject: [PATCH 07/49] refine plot() --- supervision/metrics/detection.py | 86 +++++++++++++++----------------- 1 file changed, 41 insertions(+), 45 deletions(-) diff --git a/supervision/metrics/detection.py b/supervision/metrics/detection.py index 21fc1559..dd482643 100644 --- a/supervision/metrics/detection.py +++ b/supervision/metrics/detection.py @@ -201,7 +201,7 @@ class ConfusionMatrix: def plot( self, - target_path: str, + save_path: str, title: Optional[str] = None, class_names: Optional[List[str]] = None, normalize: bool = True, @@ -216,71 +216,69 @@ class ConfusionMatrix: normalize: chart will display absolute number of detections falling into given category. Otherwise percentage of detections will be displayed. """ - # rcParams["font.family"] = "sans-serif" - # rcParams["font.sans-serif"] = ["Verdana"] + rcParams["font.family"] = "sans-serif" + rcParams["font.sans-serif"] = ["Verdana"] array = self.matrix.copy() + # num_classes = self.num_classes + 1 + num_classes = 21 + class_names = class_names[:20] + array = array[:20, :20] + array[0, 0] = 10 + array[1, 8] = 15 + array[2, 9] = 20 + array[3, 10] = 25 + array[4, 11] = 10 + array[5, 12] = 15 + array[8, 12] = 35 + if normalize: - array = array / (array.sum(0).reshape(1, self.num_classes + 1) + 1e-8) + eps = 1e-8 + array = array / (array.sum(0).reshape(1, -1) + eps) array[array < 0.005] = np.nan fig, ax = plt.subplots(figsize=(12, 10), tight_layout=True, facecolor="white") - # sn.set(font_scale=1.0 if self.num_classes < 50 else 0.8) labels = ( class_names is not None and (0 < len(class_names) < 99) and len(class_names) == self.num_classes ) - x_tick_labels = class_names + ["FN"] if labels else "auto" - y_tick_labels = class_names + ["FP"] if labels else "auto" - # sn.heatmap( - # array, - # annot=self.num_classes < 30, - # annot_kws={"size": 8}, - # fmt=".2f", - # square=True, - # vmin=0, - # cmap="Blues", - # xticklabels=x_tick_labels, - # yticklabels=y_tick_labels, - # ).set_facecolor((1, 1, 1)) + x_tick_labels = class_names + ["FN"] if labels else None + y_tick_labels = class_names + ["FP"] if labels else None im = ax.imshow(array, cmap="Blues") - # Create colorbar + cbar_kw = {} cbarlabel = "" cbar = ax.figure.colorbar(im, ax=ax, **cbar_kw) cbar.ax.set_ylabel(cbarlabel, rotation=-90, va="bottom") cbar.mappable.set_clim(vmin=0, vmax=np.nanmax(array)) - # Disable grid lines in the axis - ax.grid(False) - # Adjust the z-order of the grid lines - ax.set_axisbelow(True) - ax.yaxis.grid(color="gray", zorder=0) - ax.xaxis.grid(color="gray", zorder=0) + if x_tick_labels is None: + tick_interval = 2 + else: + tick_interval = 1 + ax.set_xticks(np.arange(0, num_classes, tick_interval), labels=x_tick_labels) + ax.set_yticks(np.arange(0, num_classes, tick_interval), labels=y_tick_labels) - # Show all ticks and label them with the respective list entries. - ax.set_xticks(np.arange(array.shape[1]) - 0.5) - ax.set_yticks(np.arange(array.shape[0]) - 0.5) - # ax.grid(which="major", color="gray", linestyle="-", linewidth=1) - # ax.tick_params(which="minor", bottom=False, left=False) + plt.setp(ax.get_xticklabels(), rotation=90, ha="right", rotation_mode="default") - # Let the horizontal axes labeling appear on top. - # ax.tick_params(top=True, bottom=False, labeltop=True, labelbottom=False) + labelsize = 10 if self.num_classes < 50 else 8 + ax.tick_params(axis="both", which="both", labelsize=labelsize) - # Rotate the tick labels and set their alignment. - plt.setp(ax.get_xticklabels(), rotation=90, ha="right", rotation_mode="anchor") - - # Turn spines off and create white grid. - # ax.spines[:].set_visible(False) - - # ax.set_xticks(x_tick_labels, minor=True) - # ax.set_yticks(y_tick_labels, minor=True) - # ax.grid(which="minor", color="w", linestyle="-", linewidth=1) - # ax.tick_params(which="minor", bottom=False, left=False) + if (num_classes) < 30: + for i in range(array.shape[0]): + for j in range(array.shape[1]): + ax.text( + j, + i, + f"{array[i, j]:.2f}" if normalize else f"{array[i, j]:.0f}", + ha="center", + va="center", + color="white", + ) if title: ax.set_title(title, fontsize=20) @@ -288,6 +286,4 @@ class ConfusionMatrix: ax.set_xlabel("Predicted") ax.set_ylabel("True") ax.set_facecolor("white") - fig.savefig( - target_path, dpi=250, facecolor=fig.get_facecolor(), transparent=True - ) + fig.savefig(save_path, dpi=250, facecolor=fig.get_facecolor(), transparent=True) From e01e495c92d05c4b48ef240956d3f47cab0b0163 Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Thu, 6 Jul 2023 22:27:53 +0200 Subject: [PATCH 08/49] rm test setup from plot() --- supervision/metrics/detection.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/supervision/metrics/detection.py b/supervision/metrics/detection.py index dd482643..4f660d22 100644 --- a/supervision/metrics/detection.py +++ b/supervision/metrics/detection.py @@ -221,17 +221,7 @@ class ConfusionMatrix: array = self.matrix.copy() - # num_classes = self.num_classes + 1 - num_classes = 21 - class_names = class_names[:20] - array = array[:20, :20] - array[0, 0] = 10 - array[1, 8] = 15 - array[2, 9] = 20 - array[3, 10] = 25 - array[4, 11] = 10 - array[5, 12] = 15 - array[8, 12] = 35 + num_classes = self.num_classes + 1 if normalize: eps = 1e-8 From ad06937653925d1c772cfc9661fe84abe5c09d38 Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Thu, 6 Jul 2023 22:29:22 +0200 Subject: [PATCH 09/49] set figsize as plot() arg --- supervision/metrics/detection.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/supervision/metrics/detection.py b/supervision/metrics/detection.py index 4f660d22..ef84e688 100644 --- a/supervision/metrics/detection.py +++ b/supervision/metrics/detection.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Callable, List, Optional +from typing import Callable, List, Optional, Tuple import matplotlib.pyplot as plt import numpy as np @@ -205,6 +205,7 @@ class ConfusionMatrix: title: Optional[str] = None, class_names: Optional[List[str]] = None, normalize: bool = True, + figsize: Tuple[int, int] = (12, 10), ) -> None: """ Create confusion matrix plot and save it at selected location. @@ -229,7 +230,7 @@ class ConfusionMatrix: array[array < 0.005] = np.nan - fig, ax = plt.subplots(figsize=(12, 10), tight_layout=True, facecolor="white") + fig, ax = plt.subplots(figsize=figsize, tight_layout=True, facecolor="white") labels = ( class_names is not None From 85c4d5a6448ff4fcb038a862327a17d9c81eb6e9 Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Thu, 6 Jul 2023 22:30:37 +0200 Subject: [PATCH 10/49] rm extra colorbar configs --- supervision/metrics/detection.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/supervision/metrics/detection.py b/supervision/metrics/detection.py index ef84e688..bdd2d630 100644 --- a/supervision/metrics/detection.py +++ b/supervision/metrics/detection.py @@ -241,10 +241,7 @@ class ConfusionMatrix: y_tick_labels = class_names + ["FP"] if labels else None im = ax.imshow(array, cmap="Blues") - cbar_kw = {} - cbarlabel = "" - cbar = ax.figure.colorbar(im, ax=ax, **cbar_kw) - cbar.ax.set_ylabel(cbarlabel, rotation=-90, va="bottom") + cbar = ax.figure.colorbar(im, ax=ax) cbar.mappable.set_clim(vmin=0, vmax=np.nanmax(array)) if x_tick_labels is None: From 9c5c296fb623f4a7e41872ccb4b285ad805ecf4c Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Thu, 6 Jul 2023 22:43:43 +0200 Subject: [PATCH 11/49] docs --- supervision/metrics/detection.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/supervision/metrics/detection.py b/supervision/metrics/detection.py index bdd2d630..b27b5db1 100644 --- a/supervision/metrics/detection.py +++ b/supervision/metrics/detection.py @@ -27,17 +27,15 @@ class ConfusionMatrix: iou_threshold: float = 0.5, ) -> "ConfusionMatrix": """ - Calculate confusion matrix based on ground-true and detected objects across all images in concerned dataset. + Calculate confusion matrix based on predicted and ground-truth detections. Args: - target: representing ground-truth objects across all images in concerned dataset. Each element of `target` list describe single image and has `shape = (N, 5)` where `N` is number of ground-truth objects. Each row is an sv.Detections object - predictions: representing detected objects across all images in concerned dataset. Each element of `detection_batches` list describe single image and has `shape = (M, 1)` where `M` is number of detected objects. Each row is an sv.Detections object + target: Detections objects from ground-truth. + predictions: Detections objects predicted by the model. classes: all known classes. conf_threshold: detection confidence threshold between 0 and 1. Detections with lower confidence will be excluded. iou_threshold: detection iou threshold between 0 and 1. Detections with lower iou will be classified as FP. - Returns: - confusion_matrix: `ConfusionMatrix` object raw confusion matrix 2d `np.ndarray`. Example: ``` @@ -116,6 +114,15 @@ class ConfusionMatrix: conf_threshold: float, iou_threshold: float, ) -> np.ndarray: + """ + Calculate confusion matrix for a batch of detections for a single image. + + Args: + See ConfusionMatrix.from_detections + + Returns: + confusion matrix based on a single image. + """ result_matrix = np.zeros((num_classes + 1, num_classes + 1)) detection_batch_filtered = pred_detections[ pred_detections.confidence > conf_threshold @@ -159,6 +166,10 @@ class ConfusionMatrix: @staticmethod def _drop_extra_matches(matches: np.ndarray) -> np.ndarray: + """ + Deduplicate matches. If there are multiple matches for the same true or predicted box, + only the one with the highest IoU is kept. + """ if matches.shape[0] > 0: matches = matches[matches[:, 2].argsort()[::-1]] matches = matches[np.unique(matches[:, 1], return_index=True)[1]] @@ -211,7 +222,7 @@ class ConfusionMatrix: Create confusion matrix plot and save it at selected location. Args: - target_path: save location of confusion matrix plot. + save_path: save location of confusion matrix plot. title: title displayed at the top of the confusion matrix plot. Default `None`. class_names: list of class names detected my model. If non given class indexes will be used. Default `None`. normalize: chart will display absolute number of detections falling into given category. Otherwise percentage of detections will be displayed. From d4ea295f09edd27242c7e7796068440c21f8bc41 Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Thu, 6 Jul 2023 22:46:45 +0200 Subject: [PATCH 12/49] add empty test templates --- test/metrics/test_detection.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/metrics/test_detection.py b/test/metrics/test_detection.py index e69de29b..81cb0531 100644 --- a/test/metrics/test_detection.py +++ b/test/metrics/test_detection.py @@ -0,0 +1,14 @@ +def test_from_detections(): + ... + + +def test__evaluate_detection_batch(): + ... + + +def test__drop_extra_matches(): + ... + + +def test_benchmark(): + ... From 476be527e5e9cdf9a933678819aa18b355d9e542 Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Fri, 7 Jul 2023 22:28:21 +0200 Subject: [PATCH 13/49] add test_from_detections --- test/metrics/test_detection.py | 87 +++++++++++++++++++++++++++++++--- 1 file changed, 81 insertions(+), 6 deletions(-) diff --git a/test/metrics/test_detection.py b/test/metrics/test_detection.py index 81cb0531..4055bd42 100644 --- a/test/metrics/test_detection.py +++ b/test/metrics/test_detection.py @@ -1,12 +1,87 @@ -def test_from_detections(): +from contextlib import ExitStack as DoesNotRaise +from typing import Optional + +import numpy as np +import pytest + +from supervision import Detections +from supervision.metrics.detection import ConfusionMatrix + +PREDICTIONS = np.array( + [ + [2254, 906, 2447, 1353, 0.90538, 0], + [2049, 1133, 2226, 1371, 0.59002, 56], + [727, 1224, 838, 1601, 0.51119, 39], + [808, 1214, 910, 1564, 0.45287, 39], + [6, 52, 1131, 2133, 0.45057, 72], + [299, 1225, 512, 1663, 0.45029, 39], + [529, 874, 645, 945, 0.31101, 39], + [8, 47, 1935, 2135, 0.28192, 72], + [2265, 813, 2328, 901, 0.2714, 62], + ], + dtype=np.float32, +) + +DETECTIONS = Detections( + xyxy=PREDICTIONS[:, :4], + confidence=PREDICTIONS[:, 4], + class_id=PREDICTIONS[:, 5].astype(int), +) +CERTAIN_DETECTIONS = Detections( + xyxy=PREDICTIONS[:, :4], + confidence=np.ones_like(PREDICTIONS[:, 4]), + class_id=PREDICTIONS[:, 5].astype(int), +) + +IDEAL_RESULT = np.zeros((81, 81)) +for class_id, count in zip(*np.unique(PREDICTIONS[:, 5], return_counts=True)): + class_id = int(class_id) + IDEAL_RESULT[class_id, class_id] = count + +classes = np.arange(80) + + +@pytest.mark.parametrize( + "predictions, targets, classes, conf_threshold, iou_threshold, expected_result, exception", + [ + ( + [CERTAIN_DETECTIONS], + [DETECTIONS], + classes, + 0.3, + 0.5, + IDEAL_RESULT, + DoesNotRaise(), + ) + ], +) +def test_from_detections( + predictions, + targets, + classes, + conf_threshold, + iou_threshold, + expected_result: Optional[np.ndarray], + exception: Exception, +): + with exception: + result = ConfusionMatrix.from_detections( + predictions=predictions, + targets=targets, + classes=classes, + conf_threshold=conf_threshold, + iou_threshold=iou_threshold, + ) + + assert result.matrix.diagonal().sum() == result.matrix.sum() + assert np.array_equal(result.matrix, expected_result) + + +def test_evaluate_detection_batch(): ... -def test__evaluate_detection_batch(): - ... - - -def test__drop_extra_matches(): +def test_drop_extra_matches(): ... From 2b876bdd64e07298d0a514cc58d7fa6654c09ee2 Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Fri, 7 Jul 2023 22:29:17 +0200 Subject: [PATCH 14/49] add from_matrix() --- supervision/metrics/detection.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/supervision/metrics/detection.py b/supervision/metrics/detection.py index b27b5db1..5dd488b4 100644 --- a/supervision/metrics/detection.py +++ b/supervision/metrics/detection.py @@ -17,6 +17,32 @@ class ConfusionMatrix: conf_threshold: float iou_threshold: float + @classmethod + def from_matrix( + cls, + matrix, + conf_threshold: float, + iou_threshold: float, + classes: Optional[List[str]], + ) -> "ConfusionMatrix": + """ + Create ConfusionMatrix from matrix. + + Args: + matrix: confusion matrix. + classes: all known classes. + conf_threshold: detection confidence threshold between 0 and 1. Detections with lower confidence will be excluded. + iou_threshold: detection iou threshold between 0 and 1. Detections with lower iou will be classified as FP. + """ + classes = classes if classes is not None else [str(x) for x in (range(len(matrix)))] + return cls( + matrix=matrix, + classes=classes, + num_classes=len(classes), + conf_threshold=conf_threshold, + iou_threshold=iou_threshold, + ) + @classmethod def from_detections( cls, From d480ff5be22d2c125bbb6883fcdf62a3030055d6 Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Fri, 7 Jul 2023 22:34:23 +0200 Subject: [PATCH 15/49] add test_evaluate_detection_batch --- test/metrics/test_detection.py | 41 ++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/test/metrics/test_detection.py b/test/metrics/test_detection.py index 4055bd42..00559638 100644 --- a/test/metrics/test_detection.py +++ b/test/metrics/test_detection.py @@ -39,6 +39,7 @@ for class_id, count in zip(*np.unique(PREDICTIONS[:, 5], return_counts=True)): IDEAL_RESULT[class_id, class_id] = count classes = np.arange(80) +num_classes = len(classes) @pytest.mark.parametrize( @@ -77,8 +78,40 @@ def test_from_detections( assert np.array_equal(result.matrix, expected_result) -def test_evaluate_detection_batch(): - ... +@pytest.mark.parametrize( + "predictions, targets, num_classes, conf_threshold, iou_threshold, expected_result, exception", + [ + ( + CERTAIN_DETECTIONS, + DETECTIONS, + num_classes, + 0.3, + 0.5, + IDEAL_RESULT, + DoesNotRaise(), + ) + ], +) +def test_evaluate_detection_batch( + predictions, + targets, + num_classes, + conf_threshold, + iou_threshold, + expected_result: Optional[np.ndarray], + exception: Exception, +): + with exception: + result = ConfusionMatrix._evaluate_detection_batch( + true_detections=targets, + pred_detections=predictions, + num_classes=num_classes, + conf_threshold=conf_threshold, + iou_threshold=iou_threshold, + ) + + assert result.diagonal().sum() == result.sum() + assert np.array_equal(result, expected_result) def test_drop_extra_matches(): @@ -87,3 +120,7 @@ def test_drop_extra_matches(): def test_benchmark(): ... + + +def test_from_matrix(): + ... From 5107dd359fe4a52182283fe4e8b65616ab63020e Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Fri, 7 Jul 2023 22:49:47 +0200 Subject: [PATCH 16/49] add test_drop_extra_matches --- test/metrics/test_detection.py | 37 ++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/test/metrics/test_detection.py b/test/metrics/test_detection.py index 00559638..cf111481 100644 --- a/test/metrics/test_detection.py +++ b/test/metrics/test_detection.py @@ -33,10 +33,18 @@ CERTAIN_DETECTIONS = Detections( class_id=PREDICTIONS[:, 5].astype(int), ) -IDEAL_RESULT = np.zeros((81, 81)) +IDEAL_MATCHES = np.stack( + [ + np.arange(len(PREDICTIONS)), + np.arange(len(PREDICTIONS)), + np.ones(len(PREDICTIONS)), + ], + axis=1, +) +IDEAL_CONF_MATRIX = np.zeros((81, 81)) for class_id, count in zip(*np.unique(PREDICTIONS[:, 5], return_counts=True)): class_id = int(class_id) - IDEAL_RESULT[class_id, class_id] = count + IDEAL_CONF_MATRIX[class_id, class_id] = count classes = np.arange(80) num_classes = len(classes) @@ -51,7 +59,7 @@ num_classes = len(classes) classes, 0.3, 0.5, - IDEAL_RESULT, + IDEAL_CONF_MATRIX, DoesNotRaise(), ) ], @@ -87,7 +95,7 @@ def test_from_detections( num_classes, 0.3, 0.5, - IDEAL_RESULT, + IDEAL_CONF_MATRIX, DoesNotRaise(), ) ], @@ -114,8 +122,25 @@ def test_evaluate_detection_batch( assert np.array_equal(result, expected_result) -def test_drop_extra_matches(): - ... +@pytest.mark.parametrize( + "matches, expected_result, exception", + [ + ( + IDEAL_MATCHES, + IDEAL_MATCHES, + DoesNotRaise(), + ) + ], +) +def test_drop_extra_matches( + matches, + expected_result: Optional[np.ndarray], + exception: Exception, +): + with exception: + result = ConfusionMatrix._drop_extra_matches(matches) + + assert np.array_equal(result, expected_result) def test_benchmark(): From 8f39df2fc04a03d59f80d4a5524043368c58a904 Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Fri, 7 Jul 2023 22:54:22 +0200 Subject: [PATCH 17/49] add test_from_matrix --- test/metrics/test_detection.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/test/metrics/test_detection.py b/test/metrics/test_detection.py index cf111481..89d32c4c 100644 --- a/test/metrics/test_detection.py +++ b/test/metrics/test_detection.py @@ -148,4 +148,27 @@ def test_benchmark(): def test_from_matrix(): - ... + matrix = np.array( + [ + [0, 1, 0], + [1, 4, 0], + [0, 1, 3], + ] + ) + conf_threshold = 0.3 + iou_threshold = 0.5 + classes = ["a", "b", "c"] + + result = ConfusionMatrix.from_matrix( + matrix, + conf_threshold=conf_threshold, + iou_threshold=iou_threshold, + classes=classes, + ) + + assert ( + np.array_equal(result.matrix, matrix) + and result.conf_threshold == conf_threshold + and result.iou_threshold == iou_threshold + and result.classes == classes + ) From 141428310a83a1a9536e14f07b542f258b018afd Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Sat, 8 Jul 2023 10:05:52 +0200 Subject: [PATCH 18/49] add dummy_detection_dataset to utils --- test/utils.py | 54 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/test/utils.py b/test/utils.py index 60ae7763..800fed85 100644 --- a/test/utils.py +++ b/test/utils.py @@ -1,8 +1,8 @@ -from typing import List +from typing import Dict, List import numpy as np -from supervision import Detections +from supervision import DetectionDataset, Detections def mock_detections( @@ -17,3 +17,53 @@ def mock_detections( class_id=class_id if class_id is None else np.array(class_id, dtype=int), tracker_id=tracker_id if tracker_id is None else np.array(tracker_id, dtype=int) ) + + +def mock_detection_dataset( + images: Dict[str, np.ndarray], + annotations: Dict[str, Detections], + classes: List[str], +) -> DetectionDataset: + return DetectionDataset(classes=classes, images=images, annotations=annotations) + + +def dummy_detection_dataset(): + img_paths = ["a.png", "b.png", "c.png"] + classes = ["a", "b", "c"] + imgs = [ + np.random.randint(0, 255, size=(28, 28, 3), dtype=np.uint8), + np.random.randint(0, 255, size=(28, 28, 3), dtype=np.uint8), + np.random.randint(0, 255, size=(28, 28, 3), dtype=np.uint8), + ] + detections = [ + mock_detections( + xyxy=[[10, 10, 20, 20], [20, 20, 25, 25]], + class_id=[0, 1], + confidence=np.ones(2), + ), + mock_detections( + xyxy=[[10, 10, 20, 20], [20, 20, 25, 25]], + class_id=[2, 1], + confidence=np.ones(2), + ), + mock_detections( + xyxy=[[10, 10, 20, 20], [20, 20, 25, 25], [10, 10, 15, 15]], + class_id=[0, 2, 2], + confidence=np.ones(3), + ), + ] + annotations = dict(zip(img_paths, detections)) + images = dict(zip(img_paths, imgs)) + dataset = mock_detection_dataset(images, annotations, classes) + return dataset + + +def dummy_detection_dataset_with_map_img_to_annotation(): + dataset = dummy_detection_dataset() + # dataset.img2ann = { + # tuple(img): dataset.annotations[img_key] for img_key, img in dataset.images.items() + # } + dataset.map_img_to_annotation = lambda img: dataset.annotations[ + [k for k, v in dataset.images.items() if np.array_equal(v, img)][0] + ] + return dataset From b7f72b79de4b583b86a853ba96e0d2b441be9fa5 Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Sat, 8 Jul 2023 10:06:14 +0200 Subject: [PATCH 19/49] add test_benchmark --- test/metrics/test_detection.py | 39 ++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/test/metrics/test_detection.py b/test/metrics/test_detection.py index 89d32c4c..e30692a7 100644 --- a/test/metrics/test_detection.py +++ b/test/metrics/test_detection.py @@ -1,4 +1,5 @@ from contextlib import ExitStack as DoesNotRaise +from test.utils import dummy_detection_dataset_with_map_img_to_annotation from typing import Optional import numpy as np @@ -41,13 +42,14 @@ IDEAL_MATCHES = np.stack( ], axis=1, ) + IDEAL_CONF_MATRIX = np.zeros((81, 81)) for class_id, count in zip(*np.unique(PREDICTIONS[:, 5], return_counts=True)): class_id = int(class_id) IDEAL_CONF_MATRIX[class_id, class_id] = count -classes = np.arange(80) -num_classes = len(classes) +CLASSES = np.arange(80) +NUM_CLASSES = len(CLASSES) @pytest.mark.parametrize( @@ -56,7 +58,7 @@ num_classes = len(classes) ( [CERTAIN_DETECTIONS], [DETECTIONS], - classes, + CLASSES, 0.3, 0.5, IDEAL_CONF_MATRIX, @@ -92,7 +94,7 @@ def test_from_detections( ( CERTAIN_DETECTIONS, DETECTIONS, - num_classes, + NUM_CLASSES, 0.3, 0.5, IDEAL_CONF_MATRIX, @@ -143,8 +145,33 @@ def test_drop_extra_matches( assert np.array_equal(result, expected_result) -def test_benchmark(): - ... +@pytest.mark.parametrize( + "dataset, conf_threshold, iou_threshold, expected_result, exception", + [ + ( + dummy_detection_dataset_with_map_img_to_annotation(), + 0.3, + 0.5, + IDEAL_CONF_MATRIX, + DoesNotRaise(), + ) + ], +) +def test_benchmark(dataset, conf_threshold, iou_threshold, expected_result, exception): + with exception: + + def callback(img): + return dataset.map_img_to_annotation(img) + + result = ConfusionMatrix.benchmark( + dataset=dataset, + callback=callback, + conf_threshold=conf_threshold, + iou_threshold=iou_threshold, + ) + + assert result.matrix.diagonal().sum() == result.matrix.sum() + assert np.array_equal(result, expected_result) def test_from_matrix(): From 36b48def89444fe8e3cfad4dafb8590341f5193a Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Sat, 8 Jul 2023 11:40:07 +0200 Subject: [PATCH 20/49] rename vars, refactor ternary --- supervision/metrics/detection.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/supervision/metrics/detection.py b/supervision/metrics/detection.py index 5dd488b4..f9bf09b0 100644 --- a/supervision/metrics/detection.py +++ b/supervision/metrics/detection.py @@ -269,13 +269,15 @@ class ConfusionMatrix: fig, ax = plt.subplots(figsize=figsize, tight_layout=True, facecolor="white") - labels = ( - class_names is not None - and (0 < len(class_names) < 99) - and len(class_names) == self.num_classes - ) - x_tick_labels = class_names + ["FN"] if labels else None - y_tick_labels = class_names + ["FP"] if labels else None + use_labels_for_ticks = class_names is not None and (0 < len(class_names) < 99) + if use_labels_for_ticks: + x_tick_labels = class_names + ["FN"] + y_tick_labels = class_names + ["FP"] + num_ticks = len(x_tick_labels) + else: + x_tick_labels = None + y_tick_labels = None + num_ticks = len(array) im = ax.imshow(array, cmap="Blues") cbar = ax.figure.colorbar(im, ax=ax) From 1b013f85e50aaa05c24c424f4cf880edd4c71b3d Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Sat, 8 Jul 2023 11:40:35 +0200 Subject: [PATCH 21/49] remove num_classes attr of ConfusionMatrix --- supervision/metrics/detection.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/supervision/metrics/detection.py b/supervision/metrics/detection.py index f9bf09b0..a7bd21db 100644 --- a/supervision/metrics/detection.py +++ b/supervision/metrics/detection.py @@ -13,7 +13,6 @@ from supervision.detection.utils import box_iou_batch class ConfusionMatrix: matrix: np.ndarray classes: List[str] - num_classes: int conf_threshold: float iou_threshold: float @@ -34,11 +33,12 @@ class ConfusionMatrix: conf_threshold: detection confidence threshold between 0 and 1. Detections with lower confidence will be excluded. iou_threshold: detection iou threshold between 0 and 1. Detections with lower iou will be classified as FP. """ - classes = classes if classes is not None else [str(x) for x in (range(len(matrix)))] + classes = ( + classes if classes is not None else [str(x) for x in (range(len(matrix)))] + ) return cls( matrix=matrix, classes=classes, - num_classes=len(classes), conf_threshold=conf_threshold, iou_threshold=iou_threshold, ) @@ -126,7 +126,6 @@ class ConfusionMatrix: ) return cls( matrix=matrix, - num_classes=num_classes, classes=classes, conf_threshold=conf_threshold, iou_threshold=iou_threshold, @@ -219,9 +218,6 @@ class ConfusionMatrix: callback: a function that takes an image as input and returns detections. conf_threshold: see ConfusionMatrix.from_detections. iou_threshold: see ConfusionMatrix.from_detections. - - Returns: - ConfusionMatrix object. """ predictions = [] targets = [] @@ -259,8 +255,6 @@ class ConfusionMatrix: array = self.matrix.copy() - num_classes = self.num_classes + 1 - if normalize: eps = 1e-8 array = array / (array.sum(0).reshape(1, -1) + eps) @@ -287,15 +281,15 @@ class ConfusionMatrix: tick_interval = 2 else: tick_interval = 1 - ax.set_xticks(np.arange(0, num_classes, tick_interval), labels=x_tick_labels) - ax.set_yticks(np.arange(0, num_classes, tick_interval), labels=y_tick_labels) + ax.set_xticks(np.arange(0, num_ticks, tick_interval), labels=x_tick_labels) + ax.set_yticks(np.arange(0, num_ticks, tick_interval), labels=y_tick_labels) plt.setp(ax.get_xticklabels(), rotation=90, ha="right", rotation_mode="default") - labelsize = 10 if self.num_classes < 50 else 8 + labelsize = 10 if num_ticks < 50 else 8 ax.tick_params(axis="both", which="both", labelsize=labelsize) - if (num_classes) < 30: + if num_ticks < 30: for i in range(array.shape[0]): for j in range(array.shape[1]): ax.text( From 21a480b483c7457f63b87249e57dc466aa754928 Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Sat, 8 Jul 2023 11:54:25 +0200 Subject: [PATCH 22/49] refactor, remove duplicated --- test/metrics/test_detection.py | 38 ++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/test/metrics/test_detection.py b/test/metrics/test_detection.py index e30692a7..85cc47e8 100644 --- a/test/metrics/test_detection.py +++ b/test/metrics/test_detection.py @@ -8,6 +8,9 @@ import pytest from supervision import Detections from supervision.metrics.detection import ConfusionMatrix +CLASSES = np.arange(80) +NUM_CLASSES = len(CLASSES) + PREDICTIONS = np.array( [ [2254, 906, 2447, 1353, 0.90538, 0], @@ -43,13 +46,30 @@ IDEAL_MATCHES = np.stack( axis=1, ) -IDEAL_CONF_MATRIX = np.zeros((81, 81)) -for class_id, count in zip(*np.unique(PREDICTIONS[:, 5], return_counts=True)): - class_id = int(class_id) - IDEAL_CONF_MATRIX[class_id, class_id] = count -CLASSES = np.arange(80) -NUM_CLASSES = len(CLASSES) +def create_empty_conf_matrix(num_classes: int, do_add_dummy_class: bool = True): + if do_add_dummy_class: + num_classes += 1 + return np.zeros((num_classes, num_classes)) + + +def update_ideal_conf_matrix(conf_matrix: np.ndarray, class_ids: np.ndarray): + for class_id, count in zip(*np.unique(class_ids, return_counts=True)): + class_id = int(class_id) + conf_matrix[class_id, class_id] += count + return conf_matrix + + +IDEAL_CONF_MATRIX = create_empty_conf_matrix(NUM_CLASSES) +IDEAL_CONF_MATRIX = update_ideal_conf_matrix(IDEAL_CONF_MATRIX, PREDICTIONS[:, 5]) + + +DUMMY_DET_DATASET = dummy_detection_dataset_with_map_img_to_annotation() +DUMMY_DET_IDEAL_CONF_MATRIX = create_empty_conf_matrix(len(DUMMY_DET_DATASET.classes)) +for det in DUMMY_DET_DATASET.annotations.values(): + DUMMY_DET_IDEAL_CONF_MATRIX = update_ideal_conf_matrix( + DUMMY_DET_IDEAL_CONF_MATRIX, det.class_id + ) @pytest.mark.parametrize( @@ -149,10 +169,10 @@ def test_drop_extra_matches( "dataset, conf_threshold, iou_threshold, expected_result, exception", [ ( - dummy_detection_dataset_with_map_img_to_annotation(), + DUMMY_DET_DATASET, 0.3, 0.5, - IDEAL_CONF_MATRIX, + DUMMY_DET_IDEAL_CONF_MATRIX, DoesNotRaise(), ) ], @@ -171,7 +191,7 @@ def test_benchmark(dataset, conf_threshold, iou_threshold, expected_result, exce ) assert result.matrix.diagonal().sum() == result.matrix.sum() - assert np.array_equal(result, expected_result) + assert np.array_equal(result.matrix, expected_result) def test_from_matrix(): From fdd8127293d3a068dc298c04b0a2b21e3eb29238 Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Sat, 8 Jul 2023 11:58:51 +0200 Subject: [PATCH 23/49] add source --- supervision/metrics/detection.py | 1 + 1 file changed, 1 insertion(+) diff --git a/supervision/metrics/detection.py b/supervision/metrics/detection.py index a7bd21db..b58679a3 100644 --- a/supervision/metrics/detection.py +++ b/supervision/metrics/detection.py @@ -111,6 +111,7 @@ class ConfusionMatrix: ... [1., 1., 0., 0.] ... ]) ``` + Source: https://github.com/SkalskiP/onemetric/blob/master/onemetric/cv/object_detection/confusion_matrix.py """ # TODO: add validation for inputs From 40853350fd65a30854f6b436314c0a6f43d2bb97 Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Sat, 8 Jul 2023 12:11:24 +0200 Subject: [PATCH 24/49] make plot() return figure --- supervision/metrics/detection.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/supervision/metrics/detection.py b/supervision/metrics/detection.py index b58679a3..218f3d5f 100644 --- a/supervision/metrics/detection.py +++ b/supervision/metrics/detection.py @@ -1,9 +1,9 @@ from dataclasses import dataclass from typing import Callable, List, Optional, Tuple +import matplotlib import matplotlib.pyplot as plt import numpy as np -from matplotlib import rcParams import supervision as sv from supervision.detection.utils import box_iou_batch @@ -235,28 +235,29 @@ class ConfusionMatrix: def plot( self, - save_path: str, + save_path: Optional[str] = None, title: Optional[str] = None, class_names: Optional[List[str]] = None, - normalize: bool = True, + do_normalize: bool = True, figsize: Tuple[int, int] = (12, 10), - ) -> None: + ) -> matplotlib.figure.Figure: """ Create confusion matrix plot and save it at selected location. Args: save_path: save location of confusion matrix plot. title: title displayed at the top of the confusion matrix plot. Default `None`. - class_names: list of class names detected my model. If non given class indexes will be used. Default `None`. - normalize: chart will display absolute number of detections falling into given category. Otherwise percentage of detections will be displayed. + class_names: custom classes to be displayed on the plot. If not provided, original classes will be used. + do_normalize: chart will display fraction of detections in a given class instead of absolute numbers. + figsize: size of the plot. """ - rcParams["font.family"] = "sans-serif" - rcParams["font.sans-serif"] = ["Verdana"] + matplotlib.rcParams["font.family"] = "sans-serif" + matplotlib.rcParams["font.sans-serif"] = ["Verdana"] array = self.matrix.copy() - if normalize: + if do_normalize: eps = 1e-8 array = array / (array.sum(0).reshape(1, -1) + eps) @@ -264,6 +265,7 @@ class ConfusionMatrix: fig, ax = plt.subplots(figsize=figsize, tight_layout=True, facecolor="white") + class_names = class_names if class_names is not None else self.classes use_labels_for_ticks = class_names is not None and (0 < len(class_names) < 99) if use_labels_for_ticks: x_tick_labels = class_names + ["FN"] @@ -296,7 +298,7 @@ class ConfusionMatrix: ax.text( j, i, - f"{array[i, j]:.2f}" if normalize else f"{array[i, j]:.0f}", + f"{array[i, j]:.2f}" if do_normalize else f"{array[i, j]:.0f}", ha="center", va="center", color="white", @@ -308,4 +310,6 @@ class ConfusionMatrix: ax.set_xlabel("Predicted") ax.set_ylabel("True") ax.set_facecolor("white") - fig.savefig(save_path, dpi=250, facecolor=fig.get_facecolor(), transparent=True) + if save_path: + fig.savefig(save_path, dpi=250, facecolor=fig.get_facecolor(), transparent=True) + return fig From fd717ea749965792061eee0c5bbb323555035f22 Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Sat, 8 Jul 2023 12:29:16 +0200 Subject: [PATCH 25/49] extend docs --- docs/metrics/detection.md | 3 +++ supervision/metrics/detection.py | 9 +++++++++ 2 files changed, 12 insertions(+) create mode 100644 docs/metrics/detection.md diff --git a/docs/metrics/detection.md b/docs/metrics/detection.md new file mode 100644 index 00000000..d0fa4615 --- /dev/null +++ b/docs/metrics/detection.md @@ -0,0 +1,3 @@ +## ConfusionMatrix + +:::supervision.metrics.detection.ConfusionMatrix diff --git a/supervision/metrics/detection.py b/supervision/metrics/detection.py index 218f3d5f..ce75ff1d 100644 --- a/supervision/metrics/detection.py +++ b/supervision/metrics/detection.py @@ -11,6 +11,15 @@ from supervision.detection.utils import box_iou_batch @dataclass class ConfusionMatrix: + """ + Data class containing information about classification results in form of a confusion matrix. + Attributes: + matrix: An array of shape (len(classes) + 1, len(classes) + 1) containing the number of TP, FP, FN and TN for each class. + classes: all known class names. + conf_threshold: detection confidence threshold between 0 and 1. Detections with lower confidence will be excluded from the matrix. + iou_threshold: detection iou threshold between 0 and 1. Detections with lower iou will be classified as FP. + """ + matrix: np.ndarray classes: List[str] conf_threshold: float From 2c0905ba28f2c8f390fcad0c65dd48456ed3e387 Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Sat, 8 Jul 2023 12:29:50 +0200 Subject: [PATCH 26/49] reformat --- supervision/metrics/detection.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/supervision/metrics/detection.py b/supervision/metrics/detection.py index ce75ff1d..95f2c62f 100644 --- a/supervision/metrics/detection.py +++ b/supervision/metrics/detection.py @@ -320,5 +320,7 @@ class ConfusionMatrix: ax.set_ylabel("True") ax.set_facecolor("white") if save_path: - fig.savefig(save_path, dpi=250, facecolor=fig.get_facecolor(), transparent=True) + fig.savefig( + save_path, dpi=250, facecolor=fig.get_facecolor(), transparent=True + ) return fig From 950744111aa614e560bb70157b63ed52cc7e4deb Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Wed, 19 Jul 2023 22:54:39 +0200 Subject: [PATCH 27/49] fix type annotations --- supervision/metrics/detection.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/supervision/metrics/detection.py b/supervision/metrics/detection.py index 95f2c62f..8025b1d4 100644 --- a/supervision/metrics/detection.py +++ b/supervision/metrics/detection.py @@ -1,7 +1,9 @@ +from __future__ import annotations + from dataclasses import dataclass from typing import Callable, List, Optional, Tuple -import matplotlib +import matplotlib import matplotlib.pyplot as plt import numpy as np @@ -32,7 +34,7 @@ class ConfusionMatrix: conf_threshold: float, iou_threshold: float, classes: Optional[List[str]], - ) -> "ConfusionMatrix": + ) -> ConfusionMatrix: """ Create ConfusionMatrix from matrix. @@ -60,7 +62,7 @@ class ConfusionMatrix: classes: List[str], conf_threshold: float = 0.3, iou_threshold: float = 0.5, - ) -> "ConfusionMatrix": + ) -> ConfusionMatrix: """ Calculate confusion matrix based on predicted and ground-truth detections. @@ -219,7 +221,7 @@ class ConfusionMatrix: callback: Callable[[np.ndarray], sv.Detections], conf_threshold: float = 0.3, iou_threshold: float = 0.5, - ) -> "ConfusionMatrix": + ) -> ConfusionMatrix: """ Create confusion matrix from dataset and callback function. From abbc0e44b0e74665ad3a4781d75142eb31aa943f Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Wed, 19 Jul 2023 23:01:50 +0200 Subject: [PATCH 28/49] fixes patch --- mkdocs.yml | 2 ++ supervision/__init__.py | 1 + supervision/metrics/detection.py | 17 +++++++++-------- test/metrics/test_detection.py | 2 +- test/utils.py | 6 ++---- 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index c2349df6..823a09fb 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -39,6 +39,8 @@ nav: - Polygon Zone: detection/tools/polygon_zone.md - Dataset: - Core: dataset/core.md + - Metrics: + - Detection: metrics/detection.md - Draw: - Utils: draw/utils.md - Utils: diff --git a/supervision/__init__.py b/supervision/__init__.py index 30694450..d0335281 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -23,6 +23,7 @@ from supervision.draw.color import Color, ColorPalette from supervision.draw.utils import draw_filled_rectangle, draw_polygon, draw_text from supervision.geometry.core import Point, Position, Rect from supervision.geometry.utils import get_polygon_center +from supervision.metrics.detection import ConfusionMatrix from supervision.utils.file import list_files_with_extensions from supervision.utils.image import ImageSink, crop from supervision.utils.notebook import plot_image, plot_images_grid diff --git a/supervision/metrics/detection.py b/supervision/metrics/detection.py index 8025b1d4..38f23fc8 100644 --- a/supervision/metrics/detection.py +++ b/supervision/metrics/detection.py @@ -7,7 +7,8 @@ import matplotlib import matplotlib.pyplot as plt import numpy as np -import supervision as sv +from supervision.dataset.core import DetectionDataset +from supervision.detection.core import Detections from supervision.detection.utils import box_iou_batch @@ -57,8 +58,8 @@ class ConfusionMatrix: @classmethod def from_detections( cls, - predictions: List[sv.Detections], - targets: List[sv.Detections], + predictions: List[Detections], + targets: List[Detections], classes: List[str], conf_threshold: float = 0.3, iou_threshold: float = 0.5, @@ -67,7 +68,7 @@ class ConfusionMatrix: Calculate confusion matrix based on predicted and ground-truth detections. Args: - target: Detections objects from ground-truth. + targets: Detections objects from ground-truth. predictions: Detections objects predicted by the model. classes: all known classes. conf_threshold: detection confidence threshold between 0 and 1. Detections with lower confidence will be excluded. @@ -145,8 +146,8 @@ class ConfusionMatrix: @staticmethod def _evaluate_detection_batch( - true_detections: sv.Detections, - pred_detections: sv.Detections, + true_detections: Detections, + pred_detections: Detections, num_classes: int, conf_threshold: float, iou_threshold: float, @@ -217,8 +218,8 @@ class ConfusionMatrix: @classmethod def benchmark( cls, - dataset: sv.DetectionDataset, - callback: Callable[[np.ndarray], sv.Detections], + dataset: DetectionDataset, + callback: Callable[[np.ndarray], Detections], conf_threshold: float = 0.3, iou_threshold: float = 0.5, ) -> ConfusionMatrix: diff --git a/test/metrics/test_detection.py b/test/metrics/test_detection.py index 85cc47e8..1d3dbee6 100644 --- a/test/metrics/test_detection.py +++ b/test/metrics/test_detection.py @@ -5,7 +5,7 @@ from typing import Optional import numpy as np import pytest -from supervision import Detections +from supervision.detection.core import Detections from supervision.metrics.detection import ConfusionMatrix CLASSES = np.arange(80) diff --git a/test/utils.py b/test/utils.py index 800fed85..aca30b9f 100644 --- a/test/utils.py +++ b/test/utils.py @@ -2,7 +2,8 @@ from typing import Dict, List import numpy as np -from supervision import DetectionDataset, Detections +from supervision.detection.core import Detections +from supervision.dataset.core import DetectionDataset def mock_detections( @@ -60,9 +61,6 @@ def dummy_detection_dataset(): def dummy_detection_dataset_with_map_img_to_annotation(): dataset = dummy_detection_dataset() - # dataset.img2ann = { - # tuple(img): dataset.annotations[img_key] for img_key, img in dataset.images.items() - # } dataset.map_img_to_annotation = lambda img: dataset.annotations[ [k for k, v in dataset.images.items() if np.array_equal(v, img)][0] ] From 8df5c5b0c464e0f2b9fbbc71c1a7deab517ef1a0 Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Wed, 19 Jul 2023 23:07:28 +0200 Subject: [PATCH 29/49] rm sans-serif from CM plot --- supervision/metrics/detection.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/supervision/metrics/detection.py b/supervision/metrics/detection.py index 38f23fc8..dcd457c0 100644 --- a/supervision/metrics/detection.py +++ b/supervision/metrics/detection.py @@ -264,9 +264,6 @@ class ConfusionMatrix: figsize: size of the plot. """ - matplotlib.rcParams["font.family"] = "sans-serif" - matplotlib.rcParams["font.sans-serif"] = ["Verdana"] - array = self.matrix.copy() if do_normalize: From dc1083711ad82a3ec7878b9e992d23d6fa301e0b Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Wed, 19 Jul 2023 23:13:19 +0200 Subject: [PATCH 30/49] remove ConfusionMatrix.from_matrix --- supervision/metrics/detection.py | 27 --------------------------- test/metrics/test_detection.py | 27 --------------------------- 2 files changed, 54 deletions(-) diff --git a/supervision/metrics/detection.py b/supervision/metrics/detection.py index dcd457c0..269f834d 100644 --- a/supervision/metrics/detection.py +++ b/supervision/metrics/detection.py @@ -28,33 +28,6 @@ class ConfusionMatrix: conf_threshold: float iou_threshold: float - @classmethod - def from_matrix( - cls, - matrix, - conf_threshold: float, - iou_threshold: float, - classes: Optional[List[str]], - ) -> ConfusionMatrix: - """ - Create ConfusionMatrix from matrix. - - Args: - matrix: confusion matrix. - classes: all known classes. - conf_threshold: detection confidence threshold between 0 and 1. Detections with lower confidence will be excluded. - iou_threshold: detection iou threshold between 0 and 1. Detections with lower iou will be classified as FP. - """ - classes = ( - classes if classes is not None else [str(x) for x in (range(len(matrix)))] - ) - return cls( - matrix=matrix, - classes=classes, - conf_threshold=conf_threshold, - iou_threshold=iou_threshold, - ) - @classmethod def from_detections( cls, diff --git a/test/metrics/test_detection.py b/test/metrics/test_detection.py index 1d3dbee6..c66d8c95 100644 --- a/test/metrics/test_detection.py +++ b/test/metrics/test_detection.py @@ -192,30 +192,3 @@ def test_benchmark(dataset, conf_threshold, iou_threshold, expected_result, exce assert result.matrix.diagonal().sum() == result.matrix.sum() assert np.array_equal(result.matrix, expected_result) - - -def test_from_matrix(): - matrix = np.array( - [ - [0, 1, 0], - [1, 4, 0], - [0, 1, 3], - ] - ) - conf_threshold = 0.3 - iou_threshold = 0.5 - classes = ["a", "b", "c"] - - result = ConfusionMatrix.from_matrix( - matrix, - conf_threshold=conf_threshold, - iou_threshold=iou_threshold, - classes=classes, - ) - - assert ( - np.array_equal(result.matrix, matrix) - and result.conf_threshold == conf_threshold - and result.iou_threshold == iou_threshold - and result.classes == classes - ) From 1b3af74756d6cd08500fc126bef8952b8ff22d34 Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Wed, 19 Jul 2023 23:57:05 +0200 Subject: [PATCH 31/49] add ConfusionMatrix.from_tensors --- supervision/metrics/detection.py | 95 +++++++++++++++++++++++++++++++- 1 file changed, 94 insertions(+), 1 deletion(-) diff --git a/supervision/metrics/detection.py b/supervision/metrics/detection.py index 269f834d..bd5ae6a3 100644 --- a/supervision/metrics/detection.py +++ b/supervision/metrics/detection.py @@ -84,7 +84,100 @@ class ConfusionMatrix: >>> confusion_matrix = ConfusionMatrix.from_detections( ... predictions=predictions, - ... target=target, + ... targets=target, + ... num_classes=3 + ... ) + + >>> confusion_matrix.matrix + ... array([ + ... [0., 0., 0., 0.], + ... [0., 1., 0., 1.], + ... [0., 1., 1., 0.], + ... [1., 1., 0., 0.] + ... ]) + ``` + Source: https://github.com/SkalskiP/onemetric/blob/master/onemetric/cv/object_detection/confusion_matrix.py + """ + # TODO: add validation for inputs + + prediction_tensors = [] + target_tensors = [] + for prediction, target in zip(predictions, targets): + prediction_tensors.append( + np.concatenate( + [ + prediction.xyxy, + np.expand_dims(prediction.class_id, 1), + np.expand_dims(prediction.confidence, 1), + ], + axis=1, + ) + ) + target_tensors.append( + np.concatenate( + [target.xyxy, np.expand_dims(target.class_id, 1)], axis=1 + ) + ) + return cls.from_tensors( + predictions=predictions, + targets=targets, + classes=classes, + conf_threshold=conf_threshold, + iou_threshold=iou_threshold, + ) + + @classmethod + def from_tensors( + cls, + predictions: List[np.ndarray], + targets: List[np.ndarray], + classes: List[str], + conf_threshold: float = 0.3, + iou_threshold: float = 0.5, + ) -> ConfusionMatrix: + """ + Calculate confusion matrix based on predicted and ground-truth detections. + + Args: + predictions: detected objects. Each element of the list describes a single image and has shape = (M, 6) where M is the number of detected objects. Each row is expected to be in (x_min, y_min, x_max, y_max, class, conf) format. + target: ground-truth objects. Each element of the list describes a single image and has shape = (N, 5) where N is the number of ground-truth objects. Each row is expected to be in (x_min, y_min, x_max, y_max, class) format. + classes: all known classes. + conf_threshold: detection confidence threshold between 0 and 1. Detections with lower confidence will be excluded. + iou_threshold: detection iou threshold between 0 and 1. Detections with lower iou will be classified as FP. + + + Example: + ``` + >>> from supervision.metrics.detection import ConfusionMatrix + + >>> target = ( + ... [ + ... array( + ... [ + ... [0.0, 0.0, 3.0, 3.0, 1], + ... [2.0, 2.0, 5.0, 5.0, 1], + ... [6.0, 1.0, 8.0, 3.0, 2], + ... ] + ... ), + ... array([1.0, 1.0, 2.0, 2.0, 2]), + ... ] + ... ) + ... + >>> predictions = [ + ... array( + ... [ + ... [0.0, 0.0, 3.0, 3.0, 1, 0.9], + ... [0.1, 0.1, 3.0, 3.0, 0, 0.9], + ... [6.0, 1.0, 8.0, 3.0, 1, 0.8], + ... [1.0, 6.0, 2.0, 7.0, 1, 0.8], + ... ] + ... ), + ... array([[1.0, 1.0, 2.0, 2.0, 2, 0.8]]) + ... ] + + >>> confusion_matrix = ConfusionMatrix.from_tensors( + ... predictions=predictions, + ... targets=targets, ... num_classes=3 ... ) From d21154e882ad141afcee08e6db83fb80324764dc Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Thu, 20 Jul 2023 00:21:06 +0200 Subject: [PATCH 32/49] fix _evaluate_detection_batch. use np.ndarray --- supervision/metrics/detection.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/supervision/metrics/detection.py b/supervision/metrics/detection.py index bd5ae6a3..1d8c0e91 100644 --- a/supervision/metrics/detection.py +++ b/supervision/metrics/detection.py @@ -119,8 +119,8 @@ class ConfusionMatrix: ) ) return cls.from_tensors( - predictions=predictions, - targets=targets, + predictions=prediction_tensors, + targets=target_tensors, classes=classes, conf_threshold=conf_threshold, iou_threshold=iou_threshold, @@ -212,8 +212,8 @@ class ConfusionMatrix: @staticmethod def _evaluate_detection_batch( - true_detections: Detections, - pred_detections: Detections, + true_detections: np.ndarray, + pred_detections: np.ndarray, num_classes: int, conf_threshold: float, iou_threshold: float, @@ -228,13 +228,16 @@ class ConfusionMatrix: confusion matrix based on a single image. """ result_matrix = np.zeros((num_classes + 1, num_classes + 1)) + conf_idx = 5 + confidence = pred_detections[:, conf_idx] detection_batch_filtered = pred_detections[ - pred_detections.confidence > conf_threshold + confidence > conf_threshold ] - true_classes = true_detections.class_id - detection_classes = detection_batch_filtered.class_id - true_boxes = true_detections.xyxy - detection_boxes = detection_batch_filtered.xyxy + class_id_idx = 4 + true_classes = np.array(true_detections[:, class_id_idx], dtype=np.int16) + detection_classes = np.array(detection_batch_filtered[:, class_id_idx], dtype=np.int16) + true_boxes = true_detections[:, :class_id_idx] + detection_boxes = detection_batch_filtered[:, :class_id_idx] iou_batch = box_iou_batch( boxes_true=true_boxes, boxes_detection=detection_boxes From fccbe6baa2f1063731f177a294feddc78582077b Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Thu, 20 Jul 2023 00:22:12 +0200 Subject: [PATCH 33/49] add test_from_tensors --- test/metrics/test_detection.py | 59 ++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/test/metrics/test_detection.py b/test/metrics/test_detection.py index c66d8c95..a25c7857 100644 --- a/test/metrics/test_detection.py +++ b/test/metrics/test_detection.py @@ -37,6 +37,25 @@ CERTAIN_DETECTIONS = Detections( class_id=PREDICTIONS[:, 5].astype(int), ) +DETECTION_TENSORS = [ + np.concatenate( + [ + det.xyxy, + np.expand_dims(det.class_id, 1), + np.expand_dims(det.confidence, 1), + ], + axis=1, + ) + for det in [DETECTIONS] +] +CERTAIN_DETECTION_TENSORS = [ + np.concatenate( + [det.xyxy, np.expand_dims(det.class_id, 1)], + axis=1, + ) + for det in [CERTAIN_DETECTIONS] +] + IDEAL_MATCHES = np.stack( [ np.arange(len(PREDICTIONS)), @@ -108,12 +127,48 @@ def test_from_detections( assert np.array_equal(result.matrix, expected_result) +@pytest.mark.parametrize( + "predictions, targets, classes, conf_threshold, iou_threshold, expected_result, exception", + [ + ( + DETECTION_TENSORS, + CERTAIN_DETECTION_TENSORS, + CLASSES, + 0.3, + 0.5, + IDEAL_CONF_MATRIX, + DoesNotRaise(), + ) + ], +) +def test_from_tensors( + predictions, + targets, + classes, + conf_threshold, + iou_threshold, + expected_result: Optional[np.ndarray], + exception: Exception, +): + with exception: + result = ConfusionMatrix.from_tensors( + predictions=predictions, + targets=targets, + classes=classes, + conf_threshold=conf_threshold, + iou_threshold=iou_threshold, + ) + + assert result.matrix.diagonal().sum() == result.matrix.sum() + assert np.array_equal(result.matrix, expected_result) + + @pytest.mark.parametrize( "predictions, targets, num_classes, conf_threshold, iou_threshold, expected_result, exception", [ ( - CERTAIN_DETECTIONS, - DETECTIONS, + DETECTION_TENSORS[0], + CERTAIN_DETECTION_TENSORS[0], NUM_CLASSES, 0.3, 0.5, From fc1e899575ee31d70eef616d4891ca0610e32fc0 Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Thu, 20 Jul 2023 09:17:47 +0200 Subject: [PATCH 34/49] fix failing tests (reduce threshold to 0.2) --- test/metrics/test_detection.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/metrics/test_detection.py b/test/metrics/test_detection.py index a25c7857..fe51630a 100644 --- a/test/metrics/test_detection.py +++ b/test/metrics/test_detection.py @@ -95,10 +95,10 @@ for det in DUMMY_DET_DATASET.annotations.values(): "predictions, targets, classes, conf_threshold, iou_threshold, expected_result, exception", [ ( - [CERTAIN_DETECTIONS], [DETECTIONS], + [CERTAIN_DETECTIONS], CLASSES, - 0.3, + 0.2, 0.5, IDEAL_CONF_MATRIX, DoesNotRaise(), @@ -134,7 +134,7 @@ def test_from_detections( DETECTION_TENSORS, CERTAIN_DETECTION_TENSORS, CLASSES, - 0.3, + 0.2, 0.5, IDEAL_CONF_MATRIX, DoesNotRaise(), @@ -170,7 +170,7 @@ def test_from_tensors( DETECTION_TENSORS[0], CERTAIN_DETECTION_TENSORS[0], NUM_CLASSES, - 0.3, + 0.2, 0.5, IDEAL_CONF_MATRIX, DoesNotRaise(), From ac39bdd487fa45b03e71008f338c539f8c65d62e Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Thu, 20 Jul 2023 09:27:29 +0200 Subject: [PATCH 35/49] add empty (preds,targets) test --- test/metrics/test_detection.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/test/metrics/test_detection.py b/test/metrics/test_detection.py index fe51630a..ac35cdd6 100644 --- a/test/metrics/test_detection.py +++ b/test/metrics/test_detection.py @@ -138,7 +138,16 @@ def test_from_detections( 0.5, IDEAL_CONF_MATRIX, DoesNotRaise(), - ) + ), + ( + [], + [], + CLASSES, + 0.2, + 0.5, + create_empty_conf_matrix(NUM_CLASSES), + DoesNotRaise(), + ), ], ) def test_from_tensors( From 314454c7a717083ecd0c8a2b7253cca2fbeb2413 Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Thu, 20 Jul 2023 09:57:44 +0200 Subject: [PATCH 36/49] add test cases to test_from_tensors --- test/metrics/test_detection.py | 148 +++++++++++++++++++++++---------- 1 file changed, 102 insertions(+), 46 deletions(-) diff --git a/test/metrics/test_detection.py b/test/metrics/test_detection.py index ac35cdd6..b3cc95a7 100644 --- a/test/metrics/test_detection.py +++ b/test/metrics/test_detection.py @@ -1,6 +1,6 @@ from contextlib import ExitStack as DoesNotRaise from test.utils import dummy_detection_dataset_with_map_img_to_annotation -from typing import Optional +from typing import Optional, Union import numpy as np import pytest @@ -25,6 +25,29 @@ PREDICTIONS = np.array( ], dtype=np.float32, ) +CERTAIN_PREDICTIONS = np.concatenate( + [ + PREDICTIONS[:, :4], + np.ones((len(PREDICTIONS), 1)), + ], + axis=1, +) + +TARGET_TENSORS = [ + np.array( + [ + [2254, 906, 2447, 1353, 0], + [2049, 1133, 2226, 1371, 56], + [727, 1224, 838, 1601, 39], + [808, 1214, 910, 1564, 39], + [6, 52, 1131, 2133, 72], + [299, 1225, 512, 1663, 39], + [529, 874, 645, 945, 39], + [8, 47, 1935, 2135, 72], + [2265, 813, 2328, 901, 62], + ] + ) +] DETECTIONS = Detections( xyxy=PREDICTIONS[:, :4], @@ -79,9 +102,24 @@ def update_ideal_conf_matrix(conf_matrix: np.ndarray, class_ids: np.ndarray): return conf_matrix +def worsen_ideal_conf_matrix( + conf_matrix: np.ndarray, class_ids: Union[np.ndarray, list] +): + for class_id in class_ids: + class_id = int(class_id) + conf_matrix[class_id, class_id] -= 1 + conf_matrix[class_id, 80] += 1 + return conf_matrix + + IDEAL_CONF_MATRIX = create_empty_conf_matrix(NUM_CLASSES) IDEAL_CONF_MATRIX = update_ideal_conf_matrix(IDEAL_CONF_MATRIX, PREDICTIONS[:, 5]) +GOOD_CONF_MATRIX = worsen_ideal_conf_matrix(IDEAL_CONF_MATRIX.copy(), [62, 72]) + +BAD_CONF_MATRIX = worsen_ideal_conf_matrix( + IDEAL_CONF_MATRIX.copy(), [62, 72, 72, 39, 39, 39, 39, 56] +) DUMMY_DET_DATASET = dummy_detection_dataset_with_map_img_to_annotation() DUMMY_DET_IDEAL_CONF_MATRIX = create_empty_conf_matrix(len(DUMMY_DET_DATASET.classes)) @@ -91,6 +129,69 @@ for det in DUMMY_DET_DATASET.annotations.values(): ) +@pytest.mark.parametrize( + "predictions, targets, classes, conf_threshold, iou_threshold, expected_result, exception", + [ + ( + DETECTION_TENSORS, + TARGET_TENSORS, + CLASSES, + 0.2, + 0.5, + IDEAL_CONF_MATRIX, + DoesNotRaise(), + ), + ( + [], + [], + CLASSES, + 0.2, + 0.5, + create_empty_conf_matrix(NUM_CLASSES), + DoesNotRaise(), + ), + ( + DETECTION_TENSORS, + TARGET_TENSORS, + CLASSES, + 0.3, + 0.5, + GOOD_CONF_MATRIX, + DoesNotRaise(), + ), + ( + DETECTION_TENSORS, + TARGET_TENSORS, + CLASSES, + 0.6, + 0.5, + BAD_CONF_MATRIX, + DoesNotRaise(), + ), + ], +) +def test_from_tensors( + predictions, + targets, + classes, + conf_threshold, + iou_threshold, + expected_result: Optional[np.ndarray], + exception: Exception, +): + with exception: + result = ConfusionMatrix.from_tensors( + predictions=predictions, + targets=targets, + classes=classes, + conf_threshold=conf_threshold, + iou_threshold=iou_threshold, + ) + + assert result.matrix.diagonal().sum() == expected_result.diagonal().sum() + assert np.array_equal(result.matrix, expected_result) + + @pytest.mark.parametrize( "predictions, targets, classes, conf_threshold, iou_threshold, expected_result, exception", [ @@ -127,51 +228,6 @@ def test_from_detections( assert np.array_equal(result.matrix, expected_result) -@pytest.mark.parametrize( - "predictions, targets, classes, conf_threshold, iou_threshold, expected_result, exception", - [ - ( - DETECTION_TENSORS, - CERTAIN_DETECTION_TENSORS, - CLASSES, - 0.2, - 0.5, - IDEAL_CONF_MATRIX, - DoesNotRaise(), - ), - ( - [], - [], - CLASSES, - 0.2, - 0.5, - create_empty_conf_matrix(NUM_CLASSES), - DoesNotRaise(), - ), - ], -) -def test_from_tensors( - predictions, - targets, - classes, - conf_threshold, - iou_threshold, - expected_result: Optional[np.ndarray], - exception: Exception, -): - with exception: - result = ConfusionMatrix.from_tensors( - predictions=predictions, - targets=targets, - classes=classes, - conf_threshold=conf_threshold, - iou_threshold=iou_threshold, - ) - - assert result.matrix.diagonal().sum() == result.matrix.sum() - assert np.array_equal(result.matrix, expected_result) - - @pytest.mark.parametrize( "predictions, targets, num_classes, conf_threshold, iou_threshold, expected_result, exception", [ From 37b72243807c919742fa1619b58e4ac32ea41100 Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Thu, 20 Jul 2023 10:13:58 +0200 Subject: [PATCH 37/49] black --- supervision/metrics/detection.py | 8 ++++---- test/utils.py | 10 +++++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/supervision/metrics/detection.py b/supervision/metrics/detection.py index 1d8c0e91..912f0ad6 100644 --- a/supervision/metrics/detection.py +++ b/supervision/metrics/detection.py @@ -230,12 +230,12 @@ class ConfusionMatrix: result_matrix = np.zeros((num_classes + 1, num_classes + 1)) conf_idx = 5 confidence = pred_detections[:, conf_idx] - detection_batch_filtered = pred_detections[ - confidence > conf_threshold - ] + detection_batch_filtered = pred_detections[confidence > conf_threshold] class_id_idx = 4 true_classes = np.array(true_detections[:, class_id_idx], dtype=np.int16) - detection_classes = np.array(detection_batch_filtered[:, class_id_idx], dtype=np.int16) + detection_classes = np.array( + detection_batch_filtered[:, class_id_idx], dtype=np.int16 + ) true_boxes = true_detections[:, :class_id_idx] detection_boxes = detection_batch_filtered[:, :class_id_idx] diff --git a/test/utils.py b/test/utils.py index aca30b9f..44b173f6 100644 --- a/test/utils.py +++ b/test/utils.py @@ -10,13 +10,17 @@ def mock_detections( xyxy: List[List[float]], confidence: List[float] = None, class_id: List[int] = None, - tracker_id: List[int] = None + tracker_id: List[int] = None, ) -> Detections: return Detections( xyxy=np.array(xyxy, dtype=np.float32), - confidence=confidence if confidence is None else np.array(confidence, dtype=np.float32), + confidence=confidence + if confidence is None + else np.array(confidence, dtype=np.float32), class_id=class_id if class_id is None else np.array(class_id, dtype=int), - tracker_id=tracker_id if tracker_id is None else np.array(tracker_id, dtype=int) + tracker_id=tracker_id + if tracker_id is None + else np.array(tracker_id, dtype=int), ) From fb211a6160ae4771113903a150ab1f4cd1d41634 Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Thu, 20 Jul 2023 10:14:06 +0200 Subject: [PATCH 38/49] add IoU tests --- test/metrics/test_detection.py | 48 ++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/test/metrics/test_detection.py b/test/metrics/test_detection.py index b3cc95a7..b1c6286b 100644 --- a/test/metrics/test_detection.py +++ b/test/metrics/test_detection.py @@ -168,6 +168,54 @@ for det in DUMMY_DET_DATASET.annotations.values(): BAD_CONF_MATRIX, DoesNotRaise(), ), + ( + [np.array([ + [0.0, 0.0, 3.0, 3.0, 0, 0.9], # correct detection of [0] + [0.1, 0.1, 3.0, 3.0, 0, 0.9], # additional detection of [0] - FP + [6.0, 1.0, 8.0, 3.0, 1, 0.8], # correct detection with incorrect class + [1.0, 6.0, 2.0, 7.0, 1, 0.8], # incorrect detection - FP + [1.0, 2.0, 2.0, 4.0, 1, 0.8], # incorrect detection with low IoU - FP + ])], + [np.array([ + [0.0, 0.0, 3.0, 3.0, 0], # [0] detected + [2.0, 2.0, 5.0, 5.0, 1], # [1] undetected - FN + [6.0, 1.0, 8.0, 3.0, 2], # [2] correct detection with incorrect class + ])], + CLASSES[:3], + 0.6, + 0.5, + np.array([ + [1, 0, 0, 0], + [0, 0, 0, 1], + [0, 1, 0, 0], + [1, 2, 0, 0] + ]), + DoesNotRaise(), + ), + ( + [np.array([ + [0.0, 0.0, 3.0, 3.0, 0, 0.9], # correct detection of [0] + [0.1, 0.1, 3.0, 3.0, 0, 0.9], # additional detection of [0] - FP + [6.0, 1.0, 8.0, 3.0, 1, 0.8], # correct detection with incorrect class + [1.0, 6.0, 2.0, 7.0, 1, 0.8], # incorrect detection - FP + [1.0, 2.0, 2.0, 4.0, 1, 0.8], # incorrect detection with low IoU - FP + ])], + [np.array([ + [0.0, 0.0, 3.0, 3.0, 0], # [0] detected + [2.0, 2.0, 5.0, 5.0, 1], # [1] undetected - FN + [6.0, 1.0, 8.0, 3.0, 2], # [2] correct detection with incorrect class + ])], + CLASSES[:3], + 0.6, + 1.0, + np.array([ + [0, 0, 0, 1], + [0, 0, 0, 1], + [0, 0, 0, 1], + [2, 3, 0, 0] + ]), + DoesNotRaise(), + ), ], ) def test_from_tensors( From bc9fe6f690d4db2efda92f2c85209675a15a4b58 Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Thu, 20 Jul 2023 13:24:25 +0200 Subject: [PATCH 39/49] fix CM plot colors --- supervision/metrics/detection.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/supervision/metrics/detection.py b/supervision/metrics/detection.py index 912f0ad6..3a9ab80a 100644 --- a/supervision/metrics/detection.py +++ b/supervision/metrics/detection.py @@ -228,9 +228,11 @@ class ConfusionMatrix: confusion matrix based on a single image. """ result_matrix = np.zeros((num_classes + 1, num_classes + 1)) + conf_idx = 5 confidence = pred_detections[:, conf_idx] detection_batch_filtered = pred_detections[confidence > conf_threshold] + class_id_idx = 4 true_classes = np.array(true_detections[:, class_id_idx], dtype=np.int16) detection_classes = np.array( @@ -373,14 +375,18 @@ class ConfusionMatrix: if num_ticks < 30: for i in range(array.shape[0]): for j in range(array.shape[1]): - ax.text( - j, - i, - f"{array[i, j]:.2f}" if do_normalize else f"{array[i, j]:.0f}", - ha="center", - va="center", - color="white", - ) + n_preds = array[i, j] + if not np.isnan(n_preds): + ax.text( + j, + i, + f"{n_preds:.2f}" if do_normalize else f"{n_preds:.0f}", + ha="center", + va="center", + color="black" + if n_preds < 0.5 * np.nanmax(array) + else "white", + ) if title: ax.set_title(title, fontsize=20) From 56e0b772119c5f4079a8306865cd64f36d2429a5 Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Thu, 20 Jul 2023 15:35:59 +0200 Subject: [PATCH 40/49] add _validate_input_tensors --- supervision/metrics/detection.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/supervision/metrics/detection.py b/supervision/metrics/detection.py index 3a9ab80a..f1cd8207 100644 --- a/supervision/metrics/detection.py +++ b/supervision/metrics/detection.py @@ -96,9 +96,7 @@ class ConfusionMatrix: ... [1., 1., 0., 0.] ... ]) ``` - Source: https://github.com/SkalskiP/onemetric/blob/master/onemetric/cv/object_detection/confusion_matrix.py """ - # TODO: add validation for inputs prediction_tensors = [] target_tensors = [] @@ -191,7 +189,7 @@ class ConfusionMatrix: ``` Source: https://github.com/SkalskiP/onemetric/blob/master/onemetric/cv/object_detection/confusion_matrix.py """ - # TODO: add validation for inputs + cls._validate_input_tensors(predictions, targets) num_classes = len(classes) matrix = np.zeros((num_classes + 1, num_classes + 1)) @@ -210,6 +208,29 @@ class ConfusionMatrix: iou_threshold=iou_threshold, ) + @classmethod + def _validate_input_tensors(cls, predictions, targets): + """Checks for shape consistency of input tensors.""" + if len(predictions) != len(targets): + raise ValueError( + f"Number of predictions ({len(predictions)}) and targets ({len(targets)}) must be equal." + ) + if len(predictions) > 0: + if not isinstance(predictions[0], np.ndarray) or not isinstance( + targets[0], np.ndarray + ): + raise ValueError( + f"Predictions and targets must be lists of numpy arrays. Got {type(predictions[0])} and {type(targets[0])} instead." + ) + if predictions[0].shape[1] != 6: + raise ValueError( + f"Predictions must have shape (N, 6). Got {predictions[0].shape} instead." + ) + if targets[0].shape[1] != 5: + raise ValueError( + f"Targets must have shape (N, 5). Got {targets[0].shape} instead." + ) + @staticmethod def _evaluate_detection_batch( true_detections: np.ndarray, From 5cb3c603cd7d281f1b875f99d4c19cab30b0ee30 Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Thu, 20 Jul 2023 15:39:55 +0200 Subject: [PATCH 41/49] remove test_benchmark --- test/metrics/test_detection.py | 168 +++++++++++++++++++-------------- 1 file changed, 96 insertions(+), 72 deletions(-) diff --git a/test/metrics/test_detection.py b/test/metrics/test_detection.py index b1c6286b..f646dfc5 100644 --- a/test/metrics/test_detection.py +++ b/test/metrics/test_detection.py @@ -121,13 +121,6 @@ BAD_CONF_MATRIX = worsen_ideal_conf_matrix( IDEAL_CONF_MATRIX.copy(), [62, 72, 72, 39, 39, 39, 39, 56] ) -DUMMY_DET_DATASET = dummy_detection_dataset_with_map_img_to_annotation() -DUMMY_DET_IDEAL_CONF_MATRIX = create_empty_conf_matrix(len(DUMMY_DET_DATASET.classes)) -for det in DUMMY_DET_DATASET.annotations.values(): - DUMMY_DET_IDEAL_CONF_MATRIX = update_ideal_conf_matrix( - DUMMY_DET_IDEAL_CONF_MATRIX, det.class_id - ) - @pytest.mark.parametrize( "predictions, targets, classes, conf_threshold, iou_threshold, expected_result, exception", @@ -169,51 +162,111 @@ for det in DUMMY_DET_DATASET.annotations.values(): DoesNotRaise(), ), ( - [np.array([ - [0.0, 0.0, 3.0, 3.0, 0, 0.9], # correct detection of [0] - [0.1, 0.1, 3.0, 3.0, 0, 0.9], # additional detection of [0] - FP - [6.0, 1.0, 8.0, 3.0, 1, 0.8], # correct detection with incorrect class - [1.0, 6.0, 2.0, 7.0, 1, 0.8], # incorrect detection - FP - [1.0, 2.0, 2.0, 4.0, 1, 0.8], # incorrect detection with low IoU - FP - ])], - [np.array([ - [0.0, 0.0, 3.0, 3.0, 0], # [0] detected - [2.0, 2.0, 5.0, 5.0, 1], # [1] undetected - FN - [6.0, 1.0, 8.0, 3.0, 2], # [2] correct detection with incorrect class - ])], + [ + np.array( + [ + [0.0, 0.0, 3.0, 3.0, 0, 0.9], # correct detection of [0] + [ + 0.1, + 0.1, + 3.0, + 3.0, + 0, + 0.9, + ], # additional detection of [0] - FP + [ + 6.0, + 1.0, + 8.0, + 3.0, + 1, + 0.8, + ], # correct detection with incorrect class + [1.0, 6.0, 2.0, 7.0, 1, 0.8], # incorrect detection - FP + [ + 1.0, + 2.0, + 2.0, + 4.0, + 1, + 0.8, + ], # incorrect detection with low IoU - FP + ] + ) + ], + [ + np.array( + [ + [0.0, 0.0, 3.0, 3.0, 0], # [0] detected + [2.0, 2.0, 5.0, 5.0, 1], # [1] undetected - FN + [ + 6.0, + 1.0, + 8.0, + 3.0, + 2, + ], # [2] correct detection with incorrect class + ] + ) + ], CLASSES[:3], 0.6, 0.5, - np.array([ - [1, 0, 0, 0], - [0, 0, 0, 1], - [0, 1, 0, 0], - [1, 2, 0, 0] - ]), + np.array([[1, 0, 0, 0], [0, 0, 0, 1], [0, 1, 0, 0], [1, 2, 0, 0]]), DoesNotRaise(), ), ( - [np.array([ - [0.0, 0.0, 3.0, 3.0, 0, 0.9], # correct detection of [0] - [0.1, 0.1, 3.0, 3.0, 0, 0.9], # additional detection of [0] - FP - [6.0, 1.0, 8.0, 3.0, 1, 0.8], # correct detection with incorrect class - [1.0, 6.0, 2.0, 7.0, 1, 0.8], # incorrect detection - FP - [1.0, 2.0, 2.0, 4.0, 1, 0.8], # incorrect detection with low IoU - FP - ])], - [np.array([ - [0.0, 0.0, 3.0, 3.0, 0], # [0] detected - [2.0, 2.0, 5.0, 5.0, 1], # [1] undetected - FN - [6.0, 1.0, 8.0, 3.0, 2], # [2] correct detection with incorrect class - ])], + [ + np.array( + [ + [0.0, 0.0, 3.0, 3.0, 0, 0.9], # correct detection of [0] + [ + 0.1, + 0.1, + 3.0, + 3.0, + 0, + 0.9, + ], # additional detection of [0] - FP + [ + 6.0, + 1.0, + 8.0, + 3.0, + 1, + 0.8, + ], # correct detection with incorrect class + [1.0, 6.0, 2.0, 7.0, 1, 0.8], # incorrect detection - FP + [ + 1.0, + 2.0, + 2.0, + 4.0, + 1, + 0.8, + ], # incorrect detection with low IoU - FP + ] + ) + ], + [ + np.array( + [ + [0.0, 0.0, 3.0, 3.0, 0], # [0] detected + [2.0, 2.0, 5.0, 5.0, 1], # [1] undetected - FN + [ + 6.0, + 1.0, + 8.0, + 3.0, + 2, + ], # [2] correct detection with incorrect class + ] + ) + ], CLASSES[:3], 0.6, 1.0, - np.array([ - [0, 0, 0, 1], - [0, 0, 0, 1], - [0, 0, 0, 1], - [2, 3, 0, 0] - ]), + np.array([[0, 0, 0, 1], [0, 0, 0, 1], [0, 0, 0, 1], [2, 3, 0, 0]]), DoesNotRaise(), ), ], @@ -331,32 +384,3 @@ def test_drop_extra_matches( result = ConfusionMatrix._drop_extra_matches(matches) assert np.array_equal(result, expected_result) - - -@pytest.mark.parametrize( - "dataset, conf_threshold, iou_threshold, expected_result, exception", - [ - ( - DUMMY_DET_DATASET, - 0.3, - 0.5, - DUMMY_DET_IDEAL_CONF_MATRIX, - DoesNotRaise(), - ) - ], -) -def test_benchmark(dataset, conf_threshold, iou_threshold, expected_result, exception): - with exception: - - def callback(img): - return dataset.map_img_to_annotation(img) - - result = ConfusionMatrix.benchmark( - dataset=dataset, - callback=callback, - conf_threshold=conf_threshold, - iou_threshold=iou_threshold, - ) - - assert result.matrix.diagonal().sum() == result.matrix.sum() - assert np.array_equal(result.matrix, expected_result) From ae55e9c908e3eeeaf2562c7a5c9f9070dc5411f6 Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Thu, 20 Jul 2023 15:48:08 +0200 Subject: [PATCH 42/49] add test_convert_detections_to_tensor --- test/metrics/test_detection.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/metrics/test_detection.py b/test/metrics/test_detection.py index f646dfc5..8fcd9482 100644 --- a/test/metrics/test_detection.py +++ b/test/metrics/test_detection.py @@ -122,6 +122,29 @@ BAD_CONF_MATRIX = worsen_ideal_conf_matrix( ) +@pytest.mark.parametrize( + "detections, exception", + [ + ( + DETECTIONS, + DoesNotRaise(), + ) + ], +) +def test_convert_detections_to_tensor( + detections, + exception: Exception, +): + with exception: + result = ConfusionMatrix.convert_detections_to_tensor( + detections=detections, + ) + + assert np.array_equal(result[:, :4], detections.xyxy) + assert np.array_equal(result[:, 4], detections.class_id) + assert np.array_equal(result[:, 5], detections.confidence) + + @pytest.mark.parametrize( "predictions, targets, classes, conf_threshold, iou_threshold, expected_result, exception", [ From 8d131ad3e8dc14d5c850ff52d6208372df3bcaae Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Thu, 20 Jul 2023 15:48:23 +0200 Subject: [PATCH 43/49] add convert_detections_to_tensor --- supervision/metrics/detection.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/supervision/metrics/detection.py b/supervision/metrics/detection.py index f1cd8207..a8aa5f14 100644 --- a/supervision/metrics/detection.py +++ b/supervision/metrics/detection.py @@ -101,21 +101,8 @@ class ConfusionMatrix: prediction_tensors = [] target_tensors = [] for prediction, target in zip(predictions, targets): - prediction_tensors.append( - np.concatenate( - [ - prediction.xyxy, - np.expand_dims(prediction.class_id, 1), - np.expand_dims(prediction.confidence, 1), - ], - axis=1, - ) - ) - target_tensors.append( - np.concatenate( - [target.xyxy, np.expand_dims(target.class_id, 1)], axis=1 - ) - ) + prediction_tensors.append(cls.convert_detections_to_tensor(prediction)) + target_tensors.append(cls.convert_detections_to_tensor(target)) return cls.from_tensors( predictions=prediction_tensors, targets=target_tensors, @@ -124,6 +111,17 @@ class ConfusionMatrix: iou_threshold=iou_threshold, ) + @classmethod + def convert_detections_to_tensor(cls, detections: Detections) -> np.ndarray: + arrays_to_concat = [detections.xyxy, np.expand_dims(detections.class_id, 1)] + if detections.confidence is not None: + arrays_to_concat.append(np.expand_dims(detections.confidence, 1)) + + return np.concatenate( + arrays_to_concat, + axis=1, + ) + @classmethod def from_tensors( cls, From 6016338a4fa83631c13cbdabbaa73f05dec06bd1 Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Thu, 20 Jul 2023 15:48:38 +0200 Subject: [PATCH 44/49] remove test_from_detections --- test/metrics/test_detection.py | 36 ---------------------------------- 1 file changed, 36 deletions(-) diff --git a/test/metrics/test_detection.py b/test/metrics/test_detection.py index 8fcd9482..fc530217 100644 --- a/test/metrics/test_detection.py +++ b/test/metrics/test_detection.py @@ -316,42 +316,6 @@ def test_from_tensors( assert np.array_equal(result.matrix, expected_result) -@pytest.mark.parametrize( - "predictions, targets, classes, conf_threshold, iou_threshold, expected_result, exception", - [ - ( - [DETECTIONS], - [CERTAIN_DETECTIONS], - CLASSES, - 0.2, - 0.5, - IDEAL_CONF_MATRIX, - DoesNotRaise(), - ) - ], -) -def test_from_detections( - predictions, - targets, - classes, - conf_threshold, - iou_threshold, - expected_result: Optional[np.ndarray], - exception: Exception, -): - with exception: - result = ConfusionMatrix.from_detections( - predictions=predictions, - targets=targets, - classes=classes, - conf_threshold=conf_threshold, - iou_threshold=iou_threshold, - ) - - assert result.matrix.diagonal().sum() == result.matrix.sum() - assert np.array_equal(result.matrix, expected_result) - - @pytest.mark.parametrize( "predictions, targets, num_classes, conf_threshold, iou_threshold, expected_result, exception", [ From 7a782b1ac3153dc413444143ec9a4e7ea0c22448 Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Thu, 20 Jul 2023 15:59:50 +0200 Subject: [PATCH 45/49] cleanup test_detection --- test/metrics/test_detection.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/test/metrics/test_detection.py b/test/metrics/test_detection.py index fc530217..31ba20de 100644 --- a/test/metrics/test_detection.py +++ b/test/metrics/test_detection.py @@ -25,13 +25,6 @@ PREDICTIONS = np.array( ], dtype=np.float32, ) -CERTAIN_PREDICTIONS = np.concatenate( - [ - PREDICTIONS[:, :4], - np.ones((len(PREDICTIONS), 1)), - ], - axis=1, -) TARGET_TENSORS = [ np.array( @@ -56,7 +49,7 @@ DETECTIONS = Detections( ) CERTAIN_DETECTIONS = Detections( xyxy=PREDICTIONS[:, :4], - confidence=np.ones_like(PREDICTIONS[:, 4]), + confidence=np.ones(len(PREDICTIONS)), class_id=PREDICTIONS[:, 5].astype(int), ) @@ -73,10 +66,14 @@ DETECTION_TENSORS = [ ] CERTAIN_DETECTION_TENSORS = [ np.concatenate( - [det.xyxy, np.expand_dims(det.class_id, 1)], + [ + det.xyxy, + np.expand_dims(det.class_id, 1), + np.ones((len(det), 1)), + ], axis=1, ) - for det in [CERTAIN_DETECTIONS] + for det in [DETECTIONS] ] IDEAL_MATCHES = np.stack( From 3f55e7a79a8c889aab0e9137b21710d3ab1d2c59 Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Fri, 21 Jul 2023 11:42:50 +0200 Subject: [PATCH 46/49] updates patch --- supervision/metrics/detection.py | 46 ++++++++++++++++++-------------- test/metrics/test_detection.py | 7 +++-- 2 files changed, 29 insertions(+), 24 deletions(-) diff --git a/supervision/metrics/detection.py b/supervision/metrics/detection.py index a8aa5f14..bdfcfb75 100644 --- a/supervision/metrics/detection.py +++ b/supervision/metrics/detection.py @@ -49,8 +49,8 @@ class ConfusionMatrix: Example: - ``` - >>> from supervision.metrics.detection import ConfusionMatrix + ```python + >>> import supervision as sv >>> target = [ ... Detections(xyxy=array([ @@ -82,7 +82,7 @@ class ConfusionMatrix: ... ) ... ] - >>> confusion_matrix = ConfusionMatrix.from_detections( + >>> confusion_matrix = sv.ConfusionMatrix.from_detections( ... predictions=predictions, ... targets=target, ... num_classes=3 @@ -136,15 +136,15 @@ class ConfusionMatrix: Args: predictions: detected objects. Each element of the list describes a single image and has shape = (M, 6) where M is the number of detected objects. Each row is expected to be in (x_min, y_min, x_max, y_max, class, conf) format. - target: ground-truth objects. Each element of the list describes a single image and has shape = (N, 5) where N is the number of ground-truth objects. Each row is expected to be in (x_min, y_min, x_max, y_max, class) format. + targets: ground-truth objects. Each element of the list describes a single image and has shape = (N, 5) where N is the number of ground-truth objects. Each row is expected to be in (x_min, y_min, x_max, y_max, class) format. classes: all known classes. conf_threshold: detection confidence threshold between 0 and 1. Detections with lower confidence will be excluded. iou_threshold: detection iou threshold between 0 and 1. Detections with lower iou will be classified as FP. Example: - ``` - >>> from supervision.metrics.detection import ConfusionMatrix + ```python + >>> import supervision as sv >>> target = ( ... [ @@ -171,7 +171,7 @@ class ConfusionMatrix: ... array([[1.0, 1.0, 2.0, 2.0, 2, 0.8]]) ... ] - >>> confusion_matrix = ConfusionMatrix.from_tensors( + >>> confusion_matrix = sv.ConfusionMatrix.from_tensors( ... predictions=predictions, ... targets=targets, ... num_classes=3 @@ -192,9 +192,9 @@ class ConfusionMatrix: num_classes = len(classes) matrix = np.zeros((num_classes + 1, num_classes + 1)) for true_batch, detection_batch in zip(targets, predictions): - matrix += ConfusionMatrix._evaluate_detection_batch( - true_detections=true_batch, - pred_detections=detection_batch, + matrix += cls.evaluate_detection_batch( + predictions=detection_batch, + targets=true_batch, num_classes=num_classes, conf_threshold=conf_threshold, iou_threshold=iou_threshold, @@ -207,7 +207,9 @@ class ConfusionMatrix: ) @classmethod - def _validate_input_tensors(cls, predictions, targets): + def _validate_input_tensors( + cls, predictions: List[np.ndarray], targets: List[np.ndarray] + ): """Checks for shape consistency of input tensors.""" if len(predictions) != len(targets): raise ValueError( @@ -230,9 +232,9 @@ class ConfusionMatrix: ) @staticmethod - def _evaluate_detection_batch( - true_detections: np.ndarray, - pred_detections: np.ndarray, + def evaluate_detection_batch( + predictions: np.ndarray, + targets: np.ndarray, num_classes: int, conf_threshold: float, iou_threshold: float, @@ -249,15 +251,15 @@ class ConfusionMatrix: result_matrix = np.zeros((num_classes + 1, num_classes + 1)) conf_idx = 5 - confidence = pred_detections[:, conf_idx] - detection_batch_filtered = pred_detections[confidence > conf_threshold] + confidence = predictions[:, conf_idx] + detection_batch_filtered = predictions[confidence > conf_threshold] class_id_idx = 4 - true_classes = np.array(true_detections[:, class_id_idx], dtype=np.int16) + true_classes = np.array(targets[:, class_id_idx], dtype=np.int16) detection_classes = np.array( detection_batch_filtered[:, class_id_idx], dtype=np.int16 ) - true_boxes = true_detections[:, :class_id_idx] + true_boxes = targets[:, :class_id_idx] detection_boxes = detection_batch_filtered[:, :class_id_idx] iou_batch = box_iou_batch( @@ -325,8 +327,12 @@ class ConfusionMatrix: predictions = [] targets = [] for img_name, img in dataset.images.items(): - predictions.append(callback(img)) - targets.append(dataset.annotations[img_name]) + pred_det = callback(img) + print(f"{pred_det.xyxy.shape[0]} detections in {img_name}") + predictions.append(pred_det) + true_det = dataset.annotations[img_name] + print(f"{true_det.xyxy.shape[0]} annotations in {img_name}") + targets.append(true_det) return cls.from_detections( predictions=predictions, targets=targets, diff --git a/test/metrics/test_detection.py b/test/metrics/test_detection.py index 31ba20de..4bd72cf4 100644 --- a/test/metrics/test_detection.py +++ b/test/metrics/test_detection.py @@ -1,5 +1,4 @@ from contextlib import ExitStack as DoesNotRaise -from test.utils import dummy_detection_dataset_with_map_img_to_annotation from typing import Optional, Union import numpy as np @@ -337,9 +336,9 @@ def test_evaluate_detection_batch( exception: Exception, ): with exception: - result = ConfusionMatrix._evaluate_detection_batch( - true_detections=targets, - pred_detections=predictions, + result = ConfusionMatrix.evaluate_detection_batch( + predictions=predictions, + targets=targets, num_classes=num_classes, conf_threshold=conf_threshold, iou_threshold=iou_threshold, From 54524a4a1ffaf0489ac09626fb07cad5b78e626a Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Fri, 21 Jul 2023 11:43:57 +0200 Subject: [PATCH 47/49] drop docstring in _drop_extra_matches --- supervision/metrics/detection.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/supervision/metrics/detection.py b/supervision/metrics/detection.py index bdfcfb75..3089e034 100644 --- a/supervision/metrics/detection.py +++ b/supervision/metrics/detection.py @@ -296,10 +296,6 @@ class ConfusionMatrix: @staticmethod def _drop_extra_matches(matches: np.ndarray) -> np.ndarray: - """ - Deduplicate matches. If there are multiple matches for the same true or predicted box, - only the one with the highest IoU is kept. - """ if matches.shape[0] > 0: matches = matches[matches[:, 2].argsort()[::-1]] matches = matches[np.unique(matches[:, 1], return_index=True)[1]] From 04b7badcbed1a6780b542b88e71cb1b9c6ee3f02 Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Fri, 21 Jul 2023 11:44:28 +0200 Subject: [PATCH 48/49] remove mock detection dataset --- test/utils.py | 50 +------------------------------------------------- 1 file changed, 1 insertion(+), 49 deletions(-) diff --git a/test/utils.py b/test/utils.py index 44b173f6..20c294e5 100644 --- a/test/utils.py +++ b/test/utils.py @@ -1,9 +1,8 @@ -from typing import Dict, List +from typing import List import numpy as np from supervision.detection.core import Detections -from supervision.dataset.core import DetectionDataset def mock_detections( @@ -22,50 +21,3 @@ def mock_detections( if tracker_id is None else np.array(tracker_id, dtype=int), ) - - -def mock_detection_dataset( - images: Dict[str, np.ndarray], - annotations: Dict[str, Detections], - classes: List[str], -) -> DetectionDataset: - return DetectionDataset(classes=classes, images=images, annotations=annotations) - - -def dummy_detection_dataset(): - img_paths = ["a.png", "b.png", "c.png"] - classes = ["a", "b", "c"] - imgs = [ - np.random.randint(0, 255, size=(28, 28, 3), dtype=np.uint8), - np.random.randint(0, 255, size=(28, 28, 3), dtype=np.uint8), - np.random.randint(0, 255, size=(28, 28, 3), dtype=np.uint8), - ] - detections = [ - mock_detections( - xyxy=[[10, 10, 20, 20], [20, 20, 25, 25]], - class_id=[0, 1], - confidence=np.ones(2), - ), - mock_detections( - xyxy=[[10, 10, 20, 20], [20, 20, 25, 25]], - class_id=[2, 1], - confidence=np.ones(2), - ), - mock_detections( - xyxy=[[10, 10, 20, 20], [20, 20, 25, 25], [10, 10, 15, 15]], - class_id=[0, 2, 2], - confidence=np.ones(3), - ), - ] - annotations = dict(zip(img_paths, detections)) - images = dict(zip(img_paths, imgs)) - dataset = mock_detection_dataset(images, annotations, classes) - return dataset - - -def dummy_detection_dataset_with_map_img_to_annotation(): - dataset = dummy_detection_dataset() - dataset.map_img_to_annotation = lambda img: dataset.annotations[ - [k for k, v in dataset.images.items() if np.array_equal(v, img)][0] - ] - return dataset From 4b38634c6365e6f7465fd648fbf1c70ec6884a9a Mon Sep 17 00:00:00 2001 From: kirilllzaitsev Date: Fri, 21 Jul 2023 12:34:43 +0200 Subject: [PATCH 49/49] add comments to _drop_extra_matches --- supervision/metrics/detection.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/supervision/metrics/detection.py b/supervision/metrics/detection.py index 3089e034..fba49dfa 100644 --- a/supervision/metrics/detection.py +++ b/supervision/metrics/detection.py @@ -297,7 +297,10 @@ class ConfusionMatrix: @staticmethod def _drop_extra_matches(matches: np.ndarray) -> np.ndarray: if matches.shape[0] > 0: + # sort by IoU matches = matches[matches[:, 2].argsort()[::-1]] + # If there are multiple matches for the same true or predicted box, + # only the one with the highest IoU is kept. matches = matches[np.unique(matches[:, 1], return_index=True)[1]] matches = matches[matches[:, 2].argsort()[::-1]] matches = matches[np.unique(matches[:, 0], return_index=True)[1]]