Module datatap.metrics.confusion_matrix
Expand source code
from __future__ import annotations
from collections import defaultdict
from typing import DefaultDict, Iterable, Mapping, Optional, Sequence, cast
import numpy as np
from scipy.optimize import linear_sum_assignment
from datatap.droplet import ImageAnnotation
from ._types import GroundTruthBox, PredictionBox
class ConfusionMatrix:
"""
Represents a confusion matrix for a collection of annotations.
This class will handle the matching of instances in a ground truth annotations
to instances in a set of matching prediction annotations.
"""
# TODO(mdsavage): make this accept matching strategies other than bounding box IOU
classes: Sequence[str]
"""
A list of the classes that this confusion matrix is tracking.
"""
matrix: np.ndarray
"""
The current confusion matrix. Entry `(i, j)` represents the number of times that
an instance of `self.classes[i]` was classified as an instance of `self.classes[j]`
"""
_class_map: Mapping[str, int]
def __init__(self, classes: Sequence[str], matrix: Optional[np.ndarray] = None):
self.classes = ["__background__"] + list(classes)
self._class_map = dict([(class_name, index) for index, class_name in enumerate(self.classes)])
dim = len(self.classes)
self.matrix = matrix if matrix is not None else np.zeros((dim, dim))
def add_annotation(
self: ConfusionMatrix,
ground_truth: ImageAnnotation,
prediction: ImageAnnotation,
iou_threshold: float,
confidence_threshold: float
) -> None:
"""
Updates this confusion matrix for the given ground truth and prediction annotations evaluated with the given IOU
threshold, only considering instances meeting the given confidence threshold.
Note: this handles instances only; multi-instances are ignored.
"""
ground_truth_boxes = [
GroundTruthBox(class_name, instance.bounding_box.rectangle)
for class_name in ground_truth.classes.keys()
for instance in ground_truth.classes[class_name].instances
if instance.bounding_box is not None
]
prediction_boxes = sorted([
PredictionBox(instance.bounding_box.confidence or 1, class_name, instance.bounding_box.rectangle)
for class_name in prediction.classes.keys()
for instance in prediction.classes[class_name].instances
if instance.bounding_box is not None and instance.bounding_box.meets_confidence_threshold(confidence_threshold)
], reverse = True, key = lambda p: p.confidence)
iou_matrix = np.array([
[ground_truth_box.box.iou(prediction_box.box) for ground_truth_box in ground_truth_boxes]
for prediction_box in prediction_boxes
], ndmin = 2)
prediction_indices, ground_truth_indices = linear_sum_assignment(iou_matrix, maximize = True)
unmatched_ground_truth_box_counts: DefaultDict[str, int] = defaultdict(lambda: 0)
unmatched_prediction_box_counts: DefaultDict[str, int] = defaultdict(lambda: 0)
for box in ground_truth_boxes:
unmatched_ground_truth_box_counts[box.class_name] += 1
for box in prediction_boxes:
unmatched_prediction_box_counts[box.class_name] += 1
for prediction_index, ground_truth_index in zip(cast(Iterable[int], prediction_indices), cast(Iterable[int], ground_truth_indices)):
if iou_matrix[prediction_index, ground_truth_index] >= iou_threshold:
ground_truth_box = ground_truth_boxes[ground_truth_index]
prediction_box = prediction_boxes[prediction_index]
self._add_detection(ground_truth_box.class_name, prediction_box.class_name)
unmatched_ground_truth_box_counts[ground_truth_box.class_name] -= 1
unmatched_prediction_box_counts[prediction_box.class_name] -= 1
for class_name, count in unmatched_ground_truth_box_counts.items():
if count > 0:
self._add_false_negative(class_name, count = count)
for class_name, count in unmatched_prediction_box_counts.items():
if count > 0:
self._add_false_positive(class_name, count = count)
def batch_add_annotation(
self: ConfusionMatrix,
ground_truths: Sequence[ImageAnnotation],
predictions: Sequence[ImageAnnotation],
iou_threshold: float,
confidence_threshold: float
) -> None:
"""
Updates this confusion matrix with the values from several annotations simultaneously.
"""
for ground_truth, prediction in zip(ground_truths, predictions):
self.add_annotation(
ground_truth,
prediction,
iou_threshold,
confidence_threshold
)
def _add_detection(self, ground_truth_class: str, prediction_class: str, count: int = 1) -> None:
r = self._class_map[ground_truth_class]
c = self._class_map[prediction_class]
self.matrix[r, c] += count
def _add_false_negative(self, ground_truth_class: str, count: int = 1) -> None:
self._add_detection(ground_truth_class, "__background__", count)
def _add_false_positive(self, ground_truth_class: str, count: int = 1) -> None:
self._add_detection("__background__", ground_truth_class, count)
def __add__(self, other: ConfusionMatrix) -> ConfusionMatrix:
if isinstance(other, ConfusionMatrix): # type: ignore - pyright complains about the isinstance check being redundant
return ConfusionMatrix(self.classes, cast(np.ndarray, self.matrix + other.matrix))
return NotImplemented
Classes
class ConfusionMatrix (classes: Sequence[str], matrix: Optional[np.ndarray] = None)
-
Represents a confusion matrix for a collection of annotations. This class will handle the matching of instances in a ground truth annotations to instances in a set of matching prediction annotations.
Expand source code
class ConfusionMatrix: """ Represents a confusion matrix for a collection of annotations. This class will handle the matching of instances in a ground truth annotations to instances in a set of matching prediction annotations. """ # TODO(mdsavage): make this accept matching strategies other than bounding box IOU classes: Sequence[str] """ A list of the classes that this confusion matrix is tracking. """ matrix: np.ndarray """ The current confusion matrix. Entry `(i, j)` represents the number of times that an instance of `self.classes[i]` was classified as an instance of `self.classes[j]` """ _class_map: Mapping[str, int] def __init__(self, classes: Sequence[str], matrix: Optional[np.ndarray] = None): self.classes = ["__background__"] + list(classes) self._class_map = dict([(class_name, index) for index, class_name in enumerate(self.classes)]) dim = len(self.classes) self.matrix = matrix if matrix is not None else np.zeros((dim, dim)) def add_annotation( self: ConfusionMatrix, ground_truth: ImageAnnotation, prediction: ImageAnnotation, iou_threshold: float, confidence_threshold: float ) -> None: """ Updates this confusion matrix for the given ground truth and prediction annotations evaluated with the given IOU threshold, only considering instances meeting the given confidence threshold. Note: this handles instances only; multi-instances are ignored. """ ground_truth_boxes = [ GroundTruthBox(class_name, instance.bounding_box.rectangle) for class_name in ground_truth.classes.keys() for instance in ground_truth.classes[class_name].instances if instance.bounding_box is not None ] prediction_boxes = sorted([ PredictionBox(instance.bounding_box.confidence or 1, class_name, instance.bounding_box.rectangle) for class_name in prediction.classes.keys() for instance in prediction.classes[class_name].instances if instance.bounding_box is not None and instance.bounding_box.meets_confidence_threshold(confidence_threshold) ], reverse = True, key = lambda p: p.confidence) iou_matrix = np.array([ [ground_truth_box.box.iou(prediction_box.box) for ground_truth_box in ground_truth_boxes] for prediction_box in prediction_boxes ], ndmin = 2) prediction_indices, ground_truth_indices = linear_sum_assignment(iou_matrix, maximize = True) unmatched_ground_truth_box_counts: DefaultDict[str, int] = defaultdict(lambda: 0) unmatched_prediction_box_counts: DefaultDict[str, int] = defaultdict(lambda: 0) for box in ground_truth_boxes: unmatched_ground_truth_box_counts[box.class_name] += 1 for box in prediction_boxes: unmatched_prediction_box_counts[box.class_name] += 1 for prediction_index, ground_truth_index in zip(cast(Iterable[int], prediction_indices), cast(Iterable[int], ground_truth_indices)): if iou_matrix[prediction_index, ground_truth_index] >= iou_threshold: ground_truth_box = ground_truth_boxes[ground_truth_index] prediction_box = prediction_boxes[prediction_index] self._add_detection(ground_truth_box.class_name, prediction_box.class_name) unmatched_ground_truth_box_counts[ground_truth_box.class_name] -= 1 unmatched_prediction_box_counts[prediction_box.class_name] -= 1 for class_name, count in unmatched_ground_truth_box_counts.items(): if count > 0: self._add_false_negative(class_name, count = count) for class_name, count in unmatched_prediction_box_counts.items(): if count > 0: self._add_false_positive(class_name, count = count) def batch_add_annotation( self: ConfusionMatrix, ground_truths: Sequence[ImageAnnotation], predictions: Sequence[ImageAnnotation], iou_threshold: float, confidence_threshold: float ) -> None: """ Updates this confusion matrix with the values from several annotations simultaneously. """ for ground_truth, prediction in zip(ground_truths, predictions): self.add_annotation( ground_truth, prediction, iou_threshold, confidence_threshold ) def _add_detection(self, ground_truth_class: str, prediction_class: str, count: int = 1) -> None: r = self._class_map[ground_truth_class] c = self._class_map[prediction_class] self.matrix[r, c] += count def _add_false_negative(self, ground_truth_class: str, count: int = 1) -> None: self._add_detection(ground_truth_class, "__background__", count) def _add_false_positive(self, ground_truth_class: str, count: int = 1) -> None: self._add_detection("__background__", ground_truth_class, count) def __add__(self, other: ConfusionMatrix) -> ConfusionMatrix: if isinstance(other, ConfusionMatrix): # type: ignore - pyright complains about the isinstance check being redundant return ConfusionMatrix(self.classes, cast(np.ndarray, self.matrix + other.matrix)) return NotImplemented
Class variables
var classes : Sequence[str]
-
A list of the classes that this confusion matrix is tracking.
var matrix : numpy.ndarray
-
The current confusion matrix. Entry
(i, j)
represents the number of times that an instance ofself.classes[i]
was classified as an instance ofself.classes[j]
Methods
def add_annotation(self: ConfusionMatrix, ground_truth: ImageAnnotation, prediction: ImageAnnotation, iou_threshold: float, confidence_threshold: float) ‑> None
-
Updates this confusion matrix for the given ground truth and prediction annotations evaluated with the given IOU threshold, only considering instances meeting the given confidence threshold.
Note: this handles instances only; multi-instances are ignored.
Expand source code
def add_annotation( self: ConfusionMatrix, ground_truth: ImageAnnotation, prediction: ImageAnnotation, iou_threshold: float, confidence_threshold: float ) -> None: """ Updates this confusion matrix for the given ground truth and prediction annotations evaluated with the given IOU threshold, only considering instances meeting the given confidence threshold. Note: this handles instances only; multi-instances are ignored. """ ground_truth_boxes = [ GroundTruthBox(class_name, instance.bounding_box.rectangle) for class_name in ground_truth.classes.keys() for instance in ground_truth.classes[class_name].instances if instance.bounding_box is not None ] prediction_boxes = sorted([ PredictionBox(instance.bounding_box.confidence or 1, class_name, instance.bounding_box.rectangle) for class_name in prediction.classes.keys() for instance in prediction.classes[class_name].instances if instance.bounding_box is not None and instance.bounding_box.meets_confidence_threshold(confidence_threshold) ], reverse = True, key = lambda p: p.confidence) iou_matrix = np.array([ [ground_truth_box.box.iou(prediction_box.box) for ground_truth_box in ground_truth_boxes] for prediction_box in prediction_boxes ], ndmin = 2) prediction_indices, ground_truth_indices = linear_sum_assignment(iou_matrix, maximize = True) unmatched_ground_truth_box_counts: DefaultDict[str, int] = defaultdict(lambda: 0) unmatched_prediction_box_counts: DefaultDict[str, int] = defaultdict(lambda: 0) for box in ground_truth_boxes: unmatched_ground_truth_box_counts[box.class_name] += 1 for box in prediction_boxes: unmatched_prediction_box_counts[box.class_name] += 1 for prediction_index, ground_truth_index in zip(cast(Iterable[int], prediction_indices), cast(Iterable[int], ground_truth_indices)): if iou_matrix[prediction_index, ground_truth_index] >= iou_threshold: ground_truth_box = ground_truth_boxes[ground_truth_index] prediction_box = prediction_boxes[prediction_index] self._add_detection(ground_truth_box.class_name, prediction_box.class_name) unmatched_ground_truth_box_counts[ground_truth_box.class_name] -= 1 unmatched_prediction_box_counts[prediction_box.class_name] -= 1 for class_name, count in unmatched_ground_truth_box_counts.items(): if count > 0: self._add_false_negative(class_name, count = count) for class_name, count in unmatched_prediction_box_counts.items(): if count > 0: self._add_false_positive(class_name, count = count)
def batch_add_annotation(self: ConfusionMatrix, ground_truths: Sequence[ImageAnnotation], predictions: Sequence[ImageAnnotation], iou_threshold: float, confidence_threshold: float) ‑> None
-
Updates this confusion matrix with the values from several annotations simultaneously.
Expand source code
def batch_add_annotation( self: ConfusionMatrix, ground_truths: Sequence[ImageAnnotation], predictions: Sequence[ImageAnnotation], iou_threshold: float, confidence_threshold: float ) -> None: """ Updates this confusion matrix with the values from several annotations simultaneously. """ for ground_truth, prediction in zip(ground_truths, predictions): self.add_annotation( ground_truth, prediction, iou_threshold, confidence_threshold )