Skip to content

Commit e05322c

Browse files
author
Matt Sokoloff
committed
wip
1 parent e4fd74e commit e05322c

File tree

5 files changed

+348
-17
lines changed

5 files changed

+348
-17
lines changed

labelbox/data/annotation_types/metrics/confusion_matrix.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22
from typing import Tuple, Dict, Union
33

44
from pydantic import conint, Field
5+
from pydantic.main import BaseModel
56

67
from .base import ConfidenceValue, BaseMetric
78

8-
Count = conint(ge=0, le=10_000)
9+
Count = conint(ge=0, le=1e10)
10+
11+
912
ConfusionMatrixMetricValue = Tuple[Count, Count, Count, Count]
1013
ConfusionMatrixMetricConfidenceValue = Dict[ConfidenceValue,
1114
ConfusionMatrixMetricValue]

labelbox/data/metrics/confusion_matrix/__init__.py

Whitespace-only changes.
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
2+
3+
4+
from pydantic.utils import truncate
5+
6+
from labelbox.data.annotation_types.metrics.confusion_matrix import \
7+
ConfusionMatrixMetricValue
8+
9+
10+
from labelbox.data.annotation_types.metrics.scalar import ScalarMetricValue
11+
from typing import List, Optional, Tuple, Union
12+
from shapely.geometry import Polygon
13+
from itertools import product
14+
import numpy as np
15+
from ...annotation_types import (ObjectAnnotation, ClassificationAnnotation,
16+
Mask, Geometry, Point, Line, Checklist, Text,
17+
Radio)
18+
from ..group import get_feature_pairs, get_identifying_key
19+
20+
21+
def confusion_matrix(ground_truths: List[Union[ObjectAnnotation,
22+
ClassificationAnnotation]],
23+
predictions: List[Union[ObjectAnnotation,
24+
ClassificationAnnotation]],
25+
iou: float,
26+
include_subclasses: bool) -> ConfusionMatrixMetricValue:
27+
28+
annotation_pairs = get_feature_pairs(predictions, ground_truths)
29+
ious = [
30+
feature_confusion_matrix(annotation_pair[0], annotation_pair[1], include_subclasses)
31+
for annotation_pair in annotation_pairs.values()
32+
]
33+
ious = [iou for iou in ious if iou is not None]
34+
return None if not len(ious) else np.sum(ious, axis = 0 )
35+
36+
37+
38+
def feature_confusion_matrix(ground_truths: List[Union[ObjectAnnotation,
39+
ClassificationAnnotation]],
40+
predictions: List[Union[ObjectAnnotation,
41+
ClassificationAnnotation]],
42+
iou: float,
43+
include_subclasses: bool) -> Optional[ConfusionMatrixMetricValue]:
44+
if _no_matching_annotations(ground_truths, predictions):
45+
return 0.
46+
elif _no_annotations(ground_truths, predictions):
47+
return None
48+
elif isinstance(predictions[0].value, Mask):
49+
return mask_confusion_matrix(ground_truths, predictions, include_subclasses)
50+
elif isinstance(predictions[0].value, Geometry):
51+
return vector_confusion_matrix(ground_truths, predictions, include_subclasses)
52+
elif isinstance(predictions[0], ClassificationAnnotation):
53+
return classification_confusion_matrix(ground_truths, predictions)
54+
else:
55+
raise ValueError(
56+
f"Unexpected annotation found. Found {type(predictions[0].value)}")
57+
58+
59+
def classification_confusion_matrix(ground_truths: List[ClassificationAnnotation],
60+
predictions: List[ClassificationAnnotation]) -> ScalarMetricValue:
61+
"""
62+
Computes iou score for all features with the same feature schema id.
63+
64+
Args:
65+
ground_truths: List of ground truth classification annotations
66+
predictions: List of prediction classification annotations
67+
Returns:
68+
float representing the iou score for the classification
69+
"""
70+
71+
if len(predictions) != len(ground_truths) != 1:
72+
return 0.
73+
74+
prediction, ground_truth = predictions[0], ground_truths[0]
75+
76+
if type(prediction) != type(ground_truth):
77+
raise TypeError(
78+
"Classification features must be the same type to compute agreement. "
79+
f"Found `{type(prediction)}` and `{type(ground_truth)}`")
80+
81+
if isinstance(prediction.value, Text):
82+
return text_confusion_matrix(ground_truth.value, prediction.value)
83+
elif isinstance(prediction.value, Radio):
84+
return radio_confusion_matrix(ground_truth.value, prediction.value)
85+
elif isinstance(prediction.value, Checklist):
86+
return checklist_confusion_matrix(ground_truth.value, prediction.value)
87+
else:
88+
raise ValueError(f"Unsupported subclass. {prediction}.")
89+
90+
def vector_confusion_matrix(ground_truths: List[ObjectAnnotation],
91+
predictions: List[ObjectAnnotation],
92+
include_subclasses: bool,
93+
buffer=70.) -> Optional[ConfusionMatrixMetricValue]:
94+
if _no_matching_annotations(ground_truths, predictions):
95+
return 0.
96+
elif _no_annotations(ground_truths, predictions):
97+
return None
98+
99+
pairs = _get_vector_pairs(ground_truths, predictions, buffer=buffer)
100+
pairs.sort(key=lambda triplet: triplet[2], reverse=True)
101+
102+
prediction_ids = {id(pred) for pred in predictions}
103+
ground_truth_ids = {id(gt) for gt in ground_truths}
104+
matched_predictions = set()
105+
matched_ground_truths = set()
106+
107+
for prediction, ground_truth, agreement in pairs:
108+
if id(prediction) not in matched_predictions and id(
109+
ground_truth) not in matched_ground_truths:
110+
matched_predictions.add(id(prediction))
111+
matched_ground_truths.add(id(ground_truth))
112+
113+
tps = len(matched_ground_truths)
114+
fps = len(prediction_ids.difference(matched_predictions))
115+
fns = len(ground_truth_ids.difference(matched_predictions))
116+
# Not defined for object detection.
117+
tns = 0
118+
return [tps, fps, tns, fns]
119+
120+
121+
122+
def _get_vector_pairs(
123+
ground_truths: List[ObjectAnnotation],
124+
predictions: List[ObjectAnnotation], buffer: float
125+
) -> List[Tuple[ObjectAnnotation, ObjectAnnotation, ScalarMetricValue]]:
126+
"""
127+
# Get iou score for all pairs of ground truths and predictions
128+
"""
129+
pairs = []
130+
for prediction, ground_truth in product(predictions, ground_truths):
131+
if isinstance(prediction.value, Geometry) and isinstance(
132+
ground_truth.value, Geometry):
133+
if isinstance(prediction.value, (Line, Point)):
134+
score = _polygon_iou(prediction.value.shapely.buffer(buffer),
135+
ground_truth.value.shapely.buffer(buffer))
136+
else:
137+
score = _polygon_iou(prediction.value.shapely,
138+
ground_truth.value.shapely)
139+
pairs.append((prediction, ground_truth, score))
140+
return pairs
141+
142+
143+
def _polygon_iou(poly1: Polygon, poly2: Polygon) -> ScalarMetricValue:
144+
"""Computes iou between two shapely polygons."""
145+
if poly1.intersects(poly2):
146+
return poly1.intersection(poly2).area / poly1.union(poly2).area
147+
return 0.
148+
149+
150+
151+
def radio_confusion_matrix(ground_truth: Radio, prediction: Radio) -> ScalarMetricValue:
152+
"""
153+
Calculates confusion between ground truth and predicted radio values
154+
"""
155+
key = get_identifying_key([prediction.answer], [ground_truth.answer])
156+
157+
return float(getattr(prediction.answer, key) ==
158+
getattr(ground_truth.answer, key))
159+
160+
161+
def text_confusion_matrix(ground_truth: Text, prediction: Text) -> ScalarMetricValue:
162+
"""
163+
Calculates agreement between ground truth and predicted text
164+
"""
165+
return float(prediction.answer == ground_truth.answer)
166+
167+
168+
def checklist_confusion_matrix(ground_truth: Checklist, prediction: Checklist) -> ScalarMetricValue:
169+
"""
170+
Calculates agreement between ground truth and predicted checklist items
171+
"""
172+
key = get_identifying_key(prediction.answer, ground_truth.answer)
173+
schema_ids_pred = {getattr(answer, key) for answer in prediction.answer}
174+
schema_ids_label = {
175+
getattr(answer, key) for answer in ground_truth.answer
176+
}
177+
return float(
178+
len(schema_ids_label & schema_ids_pred) /
179+
len(schema_ids_label | schema_ids_pred))
180+
181+
182+
183+
184+
def mask_confusion_matrix(ground_truths: List[ObjectAnnotation],
185+
predictions: List[ObjectAnnotation]) -> Optional[ScalarMetricValue]:
186+
"""
187+
Computes iou score for all features with the same feature schema id.
188+
Calculation includes subclassifications.
189+
190+
Args:
191+
ground_truths: List of ground truth mask annotations
192+
predictions: List of prediction mask annotations
193+
Returns:
194+
float representing the iou score for the masks
195+
"""
196+
if _no_matching_annotations(ground_truths, predictions):
197+
return 0.
198+
elif _no_annotations(ground_truths, predictions):
199+
return None
200+
201+
prediction_np = np.max([pred.value.draw(color=1) for pred in predictions],
202+
axis=0)
203+
ground_truth_np = np.max(
204+
[ground_truth.value.draw(color=1) for ground_truth in ground_truths],
205+
axis=0)
206+
if prediction_np.shape != ground_truth_np.shape:
207+
raise ValueError(
208+
"Prediction and mask must have the same shape."
209+
f" Found {prediction_np.shape}/{ground_truth_np.shape}.")
210+
211+
tp_mask = prediction_np == ground_truth_np == 1
212+
fp_mask = (prediction_np == 1) & (ground_truth_np==0)
213+
fn_mask = (prediction_np == 0) & (ground_truth_np==1)
214+
tn_mask = prediction_np == ground_truth_np == 0
215+
return [np.sum(tp_mask), np.sum(fp_mask), np.sum(fn_mask), np.sum(tn_mask)]
216+
217+
218+
219+
def _no_matching_annotations(ground_truths: List[ObjectAnnotation],
220+
predictions: List[ObjectAnnotation]):
221+
if len(ground_truths) and not len(predictions):
222+
# No existing predictions but existing ground truths means no matches.
223+
return True
224+
elif not len(ground_truths) and len(predictions):
225+
# No ground truth annotations but there are predictions means no matches
226+
return True
227+
return False
228+
229+
230+
def _no_annotations(ground_truths: List[ObjectAnnotation],
231+
predictions: List[ObjectAnnotation]):
232+
return not len(ground_truths) and not len(predictions)
233+
234+
235+
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# type: ignore
2+
from labelbox.data.annotation_types.metrics import ConfusionMatrixMetric
3+
from typing import List, Optional, Union
4+
from ...annotation_types import (Label, ObjectAnnotation,
5+
ClassificationAnnotation)
6+
7+
from ..group import get_feature_pairs
8+
from .calculation import feature_miou
9+
from .calculation import miou
10+
import numpy as np
11+
12+
13+
# You can include subclasses for each of these.
14+
# However, subclasses are only considered matching if there is 100% agreement
15+
# This is most applicable for Radio.
16+
17+
# TODO: Do the top level grouping by all subclasses and support a feature level option..
18+
19+
20+
def confusion_matrix_metric(ground_truths: List[Union[ObjectAnnotation,
21+
ClassificationAnnotation]],
22+
predictions: List[Union[ObjectAnnotation,
23+
ClassificationAnnotation]],
24+
include_subclasses=True, iou = 0.5) -> List[ConfusionMatrixMetric]:
25+
"""
26+
Computes miou between two sets of annotations.
27+
This will most commonly be used for data row level metrics.
28+
Each class in the annotation list is weighted equally in the iou score.
29+
30+
Args:
31+
ground_truth : Label containing human annotations or annotations known to be correct
32+
prediction: Label representing model predictions
33+
include_subclasses (bool): Whether or not to include subclasses in the iou calculation.
34+
If set to True, the iou between two overlapping objects of the same type is 0 if the subclasses are not the same.
35+
Returns:
36+
Returns a list of ScalarMetrics. Will be empty if there were no predictions and labels. Otherwise a single metric will be returned.
37+
"""
38+
if not (0. < iou < 1.):
39+
raise ValueError("iou must be between 0 and 1")
40+
41+
iou = miou(ground_truths, predictions, include_subclasses)
42+
# If both gt and preds are empty there is no metric
43+
if iou is None:
44+
return []
45+
46+
return [ConfusionMatrixMetric(metric_name="confusion_matrix_{iou}pct_iou", value=iou)]
47+
48+
49+
def feature_confusion_matrix_metric(ground_truths: List[Union[ObjectAnnotation,
50+
ClassificationAnnotation]],
51+
predictions: List[Union[ObjectAnnotation,
52+
ClassificationAnnotation]],
53+
include_subclasses=True) -> List[ConfusionMatrixMetric]:
54+
"""
55+
Computes the miou for each type of class in the list of annotations.
56+
57+
Args:
58+
ground_truth : Label containing human annotations or annotations known to be correct
59+
prediction: Label representing model predictions
60+
include_subclasses (bool): Whether or not to include subclasses in the iou calculation.
61+
If set to True, the iou between two overlapping objects of the same type is 0 if the subclasses are not the same.
62+
Returns:
63+
Returns a list of ScalarMetrics.
64+
There will be one metric for each class in the union of ground truth and prediction classes.
65+
"""
66+
# Classifications are supported because we just take a naive approach to them..
67+
annotation_pairs = get_feature_pairs(predictions, ground_truths)
68+
metrics = []
69+
for key in annotation_pairs:
70+
71+
value = feature_miou(annotation_pairs[key][0], annotation_pairs[key][1],
72+
include_subclasses)
73+
if value is None:
74+
continue
75+
metrics.append(
76+
ConfusionMatrixMetric(metric_name="iou", feature_name=key, value=value))
77+
return metrics
78+
79+
80+
81+
def iou_by_tool():
82+
#... We want to group by tool type.
83+
#... Otherwise the weighted aggregates could be overpowered.
84+
#... Since images might be huge, instances will have a few, and classifications will have the fewest.
85+
86+
87+
88+
89+
90+

0 commit comments

Comments
 (0)