Merge pull request #270 from roboflow/feature/map_reformat

🧹 small cleanup
This commit is contained in:
Piotr Skalski 2023-08-07 11:53:34 +02:00 committed by GitHub
commit a6c73f70ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 120 additions and 118 deletions

View File

@ -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.

View File

@ -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