diff --git a/supervision/detection/core.py b/supervision/detection/core.py index 825d00c7..5d8cec32 100644 --- a/supervision/detection/core.py +++ b/supervision/detection/core.py @@ -209,7 +209,7 @@ class Detections: Creates a Detections instance from a [YOLOv8](https://github.com/ultralytics/ultralytics) inference result. Args: - yolov8_results (ultralytics.yolo.engine.results.Results): The output Results instance from YOLOv8 + ultralytics_results (ultralytics.yolo.engine.results.Results): The output Results instance from YOLOv8 Returns: Detections: A new Detections object. @@ -274,10 +274,10 @@ class Detections: Also supported for [mmyolo](https://github.com/open-mmlab/mmyolo) Args: - mmdet_results (mmdet.structures.DetDataSample): The output Results instance from MMDetection + mmdet_results (mmdet.structures.DetDataSample): The output Results instance from MMDetection Returns: - Detections: A new Detections object. + Detections: A new Detections object. Example: ```python @@ -423,7 +423,7 @@ class Detections: return Detections(xyxy=xywh_to_xyxy(boxes_xywh=xywh), mask=mask) @classmethod - def from_paddledet(cls, paddledet_result): + def from_paddledet(cls, paddledet_result) -> Detections: """ Creates a Detections instance from [PaddleDetection](https://github.com/PaddlePaddle/PaddleDetection) inference result. diff --git a/supervision/metrics/detection.py b/supervision/metrics/detection.py index d126f82e..177cac24 100644 --- a/supervision/metrics/detection.py +++ b/supervision/metrics/detection.py @@ -246,8 +246,8 @@ class ConfusionMatrix: Calculate confusion matrix for a batch of detections for a single image. Args: - predictions (List[np.ndarray]): 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. - targets (List[np.ndarray]): 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. + predictions (np.ndarray): Batch prediction. 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. + targets (np.ndarray): Batch target labels. 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. num_classes (int): Number of classes. conf_threshold (float): Detection confidence threshold between `0` and `1`. Detections with lower confidence will be excluded. iou_threshold (float): Detection iou threshold between `0` and `1`. Detections with lower iou will be classified as `FP`. @@ -468,16 +468,16 @@ class MeanAveragePrecision: Mean Average Precision for object detection tasks. Attributes: - map (float): mAP value. - map50 (float): mAP value at IoU `threshold = 0.5`. - map75 (float): mAP value at IoU `threshold = 0.75`. - per_class_ap (np.ndarray): values for every classes. + map50_95 (float): Mean Average Precision (mAP) calculated over IoU thresholds ranging from `0.50` to `0.95` with a step size of `0.05`. + map50 (float): Mean Average Precision (mAP) calculated specifically at an IoU threshold of `0.50`. + map75 (float): Mean Average Precision (mAP) calculated specifically at an IoU threshold of `0.75`. + per_class_ap50_95 (np.ndarray): Average Precision (AP) values calculated over IoU thresholds ranging from `0.50` to `0.95` with a step size of `0.05`, provided for each individual class. """ - map: float + map50_95: float map50: float map75: float - per_class_ap: np.ndarray + per_class_ap50_95: np.ndarray @classmethod def from_detections( @@ -513,7 +513,7 @@ class MeanAveragePrecision: ... targets=target, ... ) - >>> mean_average_precison.map + >>> mean_average_precison.map50_95 0.2899 ``` """ @@ -561,7 +561,7 @@ class MeanAveragePrecision: ... callback = callback ... ) - >>> mean_average_precision.map + >>> mean_average_precision.map50_95 0.433 ``` """ @@ -625,93 +625,57 @@ class MeanAveragePrecision: ... targets=targets, ... ) - >>> mean_average_precison.map + >>> mean_average_precison.map50_95 0.2899 ``` """ validate_input_tensors(predictions, targets) - map, map50, map75 = 0, 0, 0 + iou_thresholds = np.linspace(0.5, 0.95, 10) + stats = [] - class_index = 4 - conf_index = 5 - - stats, average_precisions = [], [] - iou_levels = np.linspace(0.5, 0.95, 10) - num_ious = iou_levels.size - - for true_batch, detection_batch in zip(targets, predictions): - nl, npr = ( - true_batch.shape[0], - detection_batch.shape[0], - ) - correct = np.zeros((npr, num_ious), dtype=bool) - - if npr == 0: - if nl: - stats.append((correct, *np.zeros((2, 0)), true_batch[:, 4])) + # Gather matching stats for predictions and targets + for true_objs, predicted_objs in zip(targets, predictions): + if predicted_objs.shape[0] == 0: + if true_objs.shape[0]: + stats.append( + ( + np.zeros((0, iou_thresholds.size), dtype=bool), + *np.zeros((2, 0)), + true_objs[:, 4], + ) + ) continue - if nl: - correct = MeanAveragePrecision._match_detection_batch( - predictions=detection_batch, - targets=true_batch, - iou_levels=iou_levels, + + if true_objs.shape[0]: + matches = cls._match_detection_batch( + predicted_objs, true_objs, iou_thresholds ) stats.append( ( - correct, - detection_batch[:, conf_index], - detection_batch[:, class_index], - true_batch[:, class_index], + matches, + predicted_objs[:, 5], + predicted_objs[:, 4], + true_objs[:, 4], ) ) - stats = [np.concatenate(x, 0) for x in zip(*stats)] + # Compute average precisions if any matches exist + if stats: + concatenated_stats = [np.concatenate(items, 0) for items in zip(*stats)] + average_precisions = cls._average_precisions_per_class(*concatenated_stats) + map50 = average_precisions[:, 0].mean() + map75 = average_precisions[:, 5].mean() + map50_95 = average_precisions.mean() + else: + map50, map75, map50_95 = 0, 0, 0 + average_precisions = [] - if len(stats) and stats[0].any(): - average_precisions = cls._average_precisions_per_class(*stats) - ap50, ap75, average_precisions = ( - average_precisions[:, 0], - average_precisions[:, 5], - average_precisions.mean(1), - ) - map50, map75, map = ap50.mean(), ap75.mean(), average_precisions.mean() - - return cls(map=map, map50=map50, map75=map75, per_class_ap=average_precisions) - - @staticmethod - def _match_detection_batch( - predictions: np.ndarray, targets: np.ndarray, iou_levels: np.ndarray - ) -> np.ndarray: - """ - Args: - predictions (np.ndarray): batch prediction - targets (np.ndarray): batch target labels - iou_levels (np.ndarray): iou levels array contains different iou levels - - Returns: - (np.ndarray): matched prediction with target lebels result - """ - correct = np.zeros((predictions.shape[0], iou_levels.shape[0])).astype(bool) - iou = box_iou_batch(targets[:, :4], predictions[:, :4]) - - correct_class = targets[:, 4:5] == predictions[:, 4] - - for i in range(len(iou_levels)): - x = np.where((iou >= iou_levels[i]) & correct_class) - - if x[0].shape[0]: - _X1 = np.concatenate( - [np.expand_dims(x[0], 1), np.expand_dims(x[1], 1)], axis=1 - ) - _x2 = iou[x[0], x[1]][:, None] - matches = np.concatenate([_X1, _x2], axis=1) - if x[0].shape[0] > 1: - matches = matches[matches[:, 2].argsort()[::-1]] - matches = matches[np.unique(matches[:, 1], return_index=True)[1]] - matches = matches[np.unique(matches[:, 0], return_index=True)[1]] - correct[matches[:, 1].astype(int), i] = True - correct[matches[:, 1].astype(int), i] = True - return correct + return cls( + map50_95=map50_95, + map50=map50, + map75=map75, + per_class_ap50_95=average_precisions, + ) @staticmethod def compute_average_precision(recall: np.ndarray, precision: np.ndarray) -> float: @@ -737,54 +701,92 @@ class MeanAveragePrecision: average_precision = np.trapz(interpolated_precision, interpolated_recall_levels) return average_precision + @staticmethod + def _match_detection_batch( + predictions: np.ndarray, targets: np.ndarray, iou_thresholds: np.ndarray + ) -> np.ndarray: + """ + Match predictions with target labels based on IoU levels. + + Args: + predictions (np.ndarray): Batch prediction. 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. + targets (np.ndarray): Batch target labels. 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. + iou_thresholds (np.ndarray): Array contains different IoU thresholds. + + Returns: + np.ndarray: Matched prediction with target labels result. + """ + num_predictions, num_iou_levels = predictions.shape[0], iou_thresholds.shape[0] + correct = np.zeros((num_predictions, num_iou_levels), dtype=bool) + iou = box_iou_batch(targets[:, :4], predictions[:, :4]) + correct_class = targets[:, 4:5] == predictions[:, 4] + + for i, iou_level in enumerate(iou_thresholds): + matched_indices = np.where((iou >= iou_level) & correct_class) + + if matched_indices[0].shape[0]: + combined_indices = np.stack(matched_indices, axis=1) + iou_values = iou[matched_indices][:, None] + matches = np.hstack([combined_indices, iou_values]) + + if matched_indices[0].shape[0] > 1: + matches = matches[matches[:, 2].argsort()[::-1]] + matches = matches[np.unique(matches[:, 1], return_index=True)[1]] + matches = matches[np.unique(matches[:, 0], return_index=True)[1]] + + correct[matches[:, 1].astype(int), i] = True + + return correct + @staticmethod def _average_precisions_per_class( matches: np.ndarray, prediction_confidence: np.ndarray, prediction_class_ids: np.ndarray, - true_batch_class_ids: np.ndarray, + true_class_ids: np.ndarray, eps: float = 1e-16, ) -> np.ndarray: """ Compute the average precision, given the recall and precision curves. Source: https://github.com/rafaelpadilla/Object-Detection-Metrics. - Args: - matches (np.ndarray): True positives (nparray, nx1 or nx10). - prediction_confidence (np.ndarray): Objectness value from 0-1 (nparray). - prediction_class_ids (np.ndarray): Predicted object classes (nparray). - true_batch_class_ids (np.ndarray): True object classes (nparray). - Returns: - (np.ndarray): Average precision for different iou level array - """ - sorted_confidences = np.argsort(-prediction_confidence) - matches = matches[sorted_confidences] - prediction_class_ids = prediction_class_ids[sorted_confidences] - # Find unique classes - unique_classes, class_counts = np.unique( - true_batch_class_ids, return_counts=True - ) - num_classes = unique_classes.shape[0] # number of classes, number of detections + Args: + matches (np.ndarray): True positives. + prediction_confidence (np.ndarray): Objectness value from 0-1. + prediction_class_ids (np.ndarray): Predicted object classes. + true_class_ids (np.ndarray): True object classes. + eps (float, optional): Small value to prevent division by zero. + + Returns: + np.ndarray: Average precision for different IoU levels. + """ + sorted_indices = np.argsort(-prediction_confidence) + matches = matches[sorted_indices] + prediction_class_ids = prediction_class_ids[sorted_indices] + + unique_classes, class_counts = np.unique(true_class_ids, return_counts=True) + num_classes = unique_classes.shape[0] average_precisions = np.zeros((num_classes, matches.shape[1])) - for ci, c in enumerate(unique_classes): - valid = prediction_class_ids == c - num_targets = class_counts[ci] # number of labels - num_predictions = valid.sum() # number of predictions - if num_predictions == 0 or num_targets == 0: + + for class_idx, class_id in enumerate(unique_classes): + is_class = prediction_class_ids == class_id + total_true = class_counts[class_idx] + total_prediction = is_class.sum() + + if total_prediction == 0 or total_true == 0: continue - fp_pool = (1 - matches[valid]).cumsum(0) - tp_pool = matches[valid].cumsum(0) + false_positives = (1 - matches[is_class]).cumsum(0) + true_positives = matches[is_class].cumsum(0) + recall = true_positives / (total_true + eps) + precision = true_positives / (true_positives + false_positives) - recall = tp_pool / (num_targets + eps) - precision = tp_pool / (tp_pool + fp_pool) - - for j in range(matches.shape[1]): + for iou_level_idx in range(matches.shape[1]): average_precisions[ - ci, j + class_idx, iou_level_idx ] = MeanAveragePrecision.compute_average_precision( - recall[:, j], precision[:, j] + recall[:, iou_level_idx], precision[:, iou_level_idx] ) return average_precisions