1
1
# type: ignore
2
+ from labelbox .data .annotation_types .classification .classification import Checklist , Text , Radio
3
+ from labelbox .data .annotation_types import feature
2
4
from typing import Dict , Any , List , Optional , Tuple , Union
3
5
from shapely .geometry import Polygon
4
6
from itertools import product
5
7
import numpy as np
8
+ from collections import defaultdict
6
9
7
- from labelbox .data .metrics .preprocess import label_to_ndannotation
8
- from labelbox .schema .bulk_import_request import (NDAnnotation , NDChecklist ,
9
- NDClassification , NDTool ,
10
- NDMask , NDPoint , NDPolygon ,
11
- NDPolyline , NDRadio , NDText ,
12
- NDRectangle )
13
- from labelbox .data .metrics .preprocess import (create_schema_lookup ,
14
- url_to_numpy )
10
+ from ..annotation_types import Label , ObjectAnnotation , ClassificationAnnotation , Mask , Geometry
11
+ from ..annotation_types .annotation import BaseAnnotation
12
+ from labelbox .data import annotation_types
15
13
16
- VectorTool = Union [NDPoint , NDRectangle , NDPolyline , NDPolygon ]
17
- ClassificationTool = Union [NDText , NDRadio , NDChecklist ]
18
14
19
-
20
- def mask_miou (predictions : List [NDMask ], labels : List [NDMask ]) -> float :
15
+ def mask_miou (predictions : List [Mask ], ground_truths : List [Mask ], resize_height = None , resize_width = None ) -> float :
21
16
"""
22
17
Creates prediction and label binary mask for all features with the same feature schema id.
18
+ Masks are flattened and treated as one class.
19
+ If you want to treat each object as an instance then convert each mask to a polygon annotation.
23
20
24
21
Args:
25
22
predictions: List of masks objects
26
- labels : List of masks objects
23
+ ground_truths : List of masks objects
27
24
Returns:
28
25
float indicating iou score
29
26
"""
27
+ prediction_np = np .max ([pred .raster (binary = True , height = resize_height , width = resize_width ) for pred in predictions ], axis = 0 )
28
+ ground_truth_np = np .max ([ground_truth .raster (binary = True , height = resize_height , width = resize_width ) for ground_truth in ground_truths ], axis = 0 )
29
+ if prediction_np .shape != ground_truth_np .shape :
30
+ raise ValueError ("Prediction and mask must have the same shape."
31
+ f" Found { prediction_np .shape } /{ ground_truth_np .shape } ."
32
+ " Add resize params to fix this." )
33
+ return _mask_iou (ground_truth_np , prediction_np )
30
34
31
- colors_pred = {tuple (pred .mask ['colorRGB' ]) for pred in predictions }
32
- colors_label = {tuple (label .mask ['colorRGB' ]) for label in labels }
33
- error_msg = "segmentation {} should all have the same color. Found {}"
34
- if len (colors_pred ) > 1 :
35
- raise ValueError (error_msg .format ("predictions" , colors_pred ))
36
- elif len (colors_label ) > 1 :
37
- raise ValueError (error_msg .format ("labels" , colors_label ))
38
-
39
- pred_mask = _instance_urls_to_binary_mask (
40
- [pred .mask ['instanceURI' ] for pred in predictions ], colors_pred .pop ())
41
- label_mask = _instance_urls_to_binary_mask (
42
- [label .mask ['instanceURI' ] for label in labels ], colors_label .pop ())
43
- assert label_mask .shape == pred_mask .shape
44
- return _mask_iou (label_mask , pred_mask )
45
35
46
-
47
- def classification_miou (predictions : List [ClassificationTool ],
48
- labels : List [ClassificationTool ]) -> float :
36
+ def classification_miou (predictions : List [ClassificationAnnotation ],
37
+ labels : List [ClassificationAnnotation ]) -> float :
49
38
"""
50
39
Computes iou for classification features.
51
40
@@ -67,13 +56,13 @@ def classification_miou(predictions: List[ClassificationTool],
67
56
"Classification features must be the same type to compute agreement. "
68
57
f"Found `{ type (prediction )} ` and `{ type (label )} `" )
69
58
70
- if isinstance (prediction , NDText ):
71
- return float (prediction .answer == label .answer )
72
- elif isinstance (prediction , NDRadio ):
73
- return float (prediction .answer .schemaId == label .answer .schemaId )
74
- elif isinstance (prediction , NDChecklist ):
75
- schema_ids_pred = {answer .schemaId for answer in prediction .answers }
76
- schema_ids_label = {answer .schemaId for answer in label .answers }
59
+ if isinstance (prediction . value , Text ):
60
+ return float (prediction .value . answer == label . value .answer )
61
+ elif isinstance (prediction . value , Radio ):
62
+ return float (prediction .value . answer .schema_id == label .value . answer .schema_id )
63
+ elif isinstance (prediction . value , Checklist ):
64
+ schema_ids_pred = {answer .schema_id for answer in prediction .value . answer }
65
+ schema_ids_label = {answer .schema_id for answer in label .value . answer }
77
66
return float (
78
67
len (schema_ids_label & schema_ids_pred ) /
79
68
len (schema_ids_label | schema_ids_pred ))
@@ -82,8 +71,8 @@ def classification_miou(predictions: List[ClassificationTool],
82
71
83
72
84
73
def subclassification_miou (
85
- subclass_predictions : List [ClassificationTool ],
86
- subclass_labels : List [ClassificationTool ]) -> Optional [float ]:
74
+ subclass_predictions : List [ClassificationAnnotation ],
75
+ subclass_labels : List [ClassificationAnnotation ]) -> Optional [float ]:
87
76
"""
88
77
89
78
Computes subclass iou score between two vector tools that were matched.
@@ -96,12 +85,10 @@ def subclassification_miou(
96
85
miou across all subclasses.
97
86
"""
98
87
99
- subclass_predictions = create_schema_lookup (subclass_predictions )
100
- subclass_labels = create_schema_lookup (subclass_labels )
88
+ subclass_predictions = _create_schema_lookup (subclass_predictions )
89
+ subclass_labels = _create_schema_lookup (subclass_labels )
101
90
feature_schemas = set (subclass_predictions .keys ()).union (
102
91
set (subclass_labels .keys ()))
103
- # There should only be one feature schema per subclass.
104
-
105
92
classification_iou = [
106
93
feature_miou (subclass_predictions [feature_schema ],
107
94
subclass_labels [feature_schema ])
@@ -111,7 +98,7 @@ def subclassification_miou(
111
98
return None if not len (classification_iou ) else np .mean (classification_iou )
112
99
113
100
114
- def vector_miou (predictions : List [VectorTool ], labels : List [VectorTool ],
101
+ def vector_miou (predictions : List [Geometry ], labels : List [Geometry ],
115
102
include_subclasses ) -> float :
116
103
"""
117
104
Computes an iou score for vector tools.
@@ -148,8 +135,8 @@ def vector_miou(predictions: List[VectorTool], labels: List[VectorTool],
148
135
return np .mean (solution_agreements )
149
136
150
137
151
- def feature_miou (predictions : List [NDAnnotation ],
152
- labels : List [NDAnnotation ],
138
+ def feature_miou (predictions : List [Union [ ObjectAnnotation , ClassificationAnnotation ] ],
139
+ labels : List [Union [ ObjectAnnotation , ClassificationAnnotation ] ],
153
140
include_subclasses = True ) -> Optional [float ]:
154
141
"""
155
142
Computes iou score for all features with the same feature schema id.
@@ -159,7 +146,6 @@ def feature_miou(predictions: List[NDAnnotation],
159
146
labels: List of labels with the same feature schema.
160
147
Returns:
161
148
float representing the iou score for the feature type if score can be computed otherwise None.
162
-
163
149
"""
164
150
if len (predictions ):
165
151
keys = predictions [0 ]
@@ -170,32 +156,31 @@ def feature_miou(predictions: List[NDAnnotation],
170
156
# Ignore examples that do not have any labels or predictions
171
157
return None
172
158
173
- tool_types = {type (annot ) for annot in predictions
174
- }.union ({type (annot ) for annot in labels })
175
-
176
- if len (tool_types ) > 1 :
177
- raise ValueError (
178
- "feature_miou predictions and annotations should all be of the same type"
179
- )
180
-
181
- tool_type = tool_types .pop ()
182
- if tool_type == NDMask :
159
+ if isinstance (predictions [0 ].value , Mask ):
160
+ # TODO: A mask can have subclasses too.. Why are we treating this differently?
183
161
return mask_miou (predictions , labels )
184
- elif tool_type in NDTool . get_union_types ( ):
162
+ elif isinstance ( predictions [ 0 ]. value , Geometry ):
185
163
return vector_miou (predictions ,
186
164
labels ,
187
165
include_subclasses = include_subclasses )
188
- elif tool_type in NDClassification . get_union_types ( ):
166
+ elif isinstance ( predictions [ 0 ]. value , ClassificationAnnotation ):
189
167
return classification_miou (predictions , labels )
190
168
else :
191
- raise ValueError (f"Unexpected annotation found. Found { tool_type } " )
169
+ raise ValueError (f"Unexpected annotation found. Found { type ( predictions [ 0 ]) } " )
192
170
193
171
194
- def datarow_miou (label_content : List [Dict [str , Any ]],
195
- ndjsons : List [Dict [str , Any ]],
172
+ def _create_schema_lookup (annotations : List [BaseAnnotation ]):
173
+ grouped_annotations = defaultdict (list )
174
+ for annotation in annotations :
175
+ grouped_annotations [annotation .schema_id ] = annotation
176
+ return grouped_annotations
177
+
178
+ def data_row_miou (ground_truth : Label ,
179
+ predictions : Label ,
196
180
include_classifications = True ,
197
181
include_subclasses = True ) -> float :
198
182
"""
183
+ # At this point all object should have schema ids.
199
184
200
185
Args:
201
186
label_content : one row from the bulk label export - `project.export_labels()`
@@ -204,15 +189,14 @@ def datarow_miou(label_content: List[Dict[str, Any]],
204
189
include_subclassifications: Whether or not to factor in subclassifications into the iou score
205
190
Returns:
206
191
float indicating the iou score for this data row.
207
-
208
192
"""
209
-
210
- predictions , labels , feature_schemas = _preprocess_args (
211
- label_content , ndjsons , include_classifications )
212
-
193
+ annotation_types = None if include_classifications else Geometry
194
+ prediction_annotations = predictions . get_annotations_by_attr ( attr = "name" , annotation_types = annotation_types )
195
+ ground_truth_annotations = ground_truth . get_annotations_by_attr ( attr = "name" , annotation_types = annotation_types )
196
+ feature_schemas = set ( prediction_annotations . keys ()). union ( set ( ground_truth_annotations . keys ()))
213
197
ious = [
214
- feature_miou (predictions [feature_schema ],
215
- labels [feature_schema ],
198
+ feature_miou (prediction_annotations [feature_schema ],
199
+ ground_truth_annotations [feature_schema ],
216
200
include_subclasses = include_subclasses )
217
201
for feature_schema in feature_schemas
218
202
]
@@ -222,60 +206,14 @@ def datarow_miou(label_content: List[Dict[str, Any]],
222
206
return np .mean (ious )
223
207
224
208
225
- def _preprocess_args (
226
- label_content : List [Dict [str , Any ]],
227
- ndjsons : List [Dict [str , Any ]],
228
- include_classifications = True
229
- ) -> Tuple [Dict [str , List [NDAnnotation ]], Dict [str , List [NDAnnotation ]],
230
- List [str ]]:
231
- """
232
-
233
- This function takes in the raw json payloads, validates, and converts to python objects.
234
- In the future datarow_miou will directly take the objects as args.
235
-
236
- Args:
237
- label_content : one row from the bulk label export - `project.export_labels()`
238
- ndjsons: Model predictions in the ndjson format specified here (https://docs.labelbox.com/data-model/en/index-en#annotations)
239
- Returns a tuple containing:
240
- - a dict for looking up a list of predictions by feature schema id
241
- - a dict for looking up a list of labels by feature schema id
242
- - a list of a all feature schema ids
243
-
244
- """
245
- labels = label_content ['Label' ].get ('objects' )
246
- if include_classifications :
247
- labels += label_content ['Label' ].get ('classifications' )
248
-
249
- predictions = [NDAnnotation (** pred .copy ()) for pred in ndjsons ]
250
-
251
- unique_datarows = {pred .dataRow .id for pred in predictions }
252
- if len (unique_datarows ):
253
- # Empty set of annotations is valid (if labels exist but no inferences then iou will be 0.)
254
- if unique_datarows != {label_content ['DataRow ID' ]}:
255
- raise ValueError (
256
- f"There should only be one datarow passed to the datarow_miou function. Found { unique_datarows } "
257
- )
258
-
259
- labels = [
260
- label_to_ndannotation (label , label_content ['DataRow ID' ])
261
- for label in labels
262
- ]
263
-
264
- labels = create_schema_lookup (labels )
265
- predictions = create_schema_lookup (predictions )
266
-
267
- feature_schemas = set (predictions .keys ()).union (set (labels .keys ()))
268
- return predictions , labels , feature_schemas
269
-
270
-
271
- def _get_vector_pairs (predictions : List [Dict [str , Any ]], labels ):
209
+ def _get_vector_pairs (predictions : List [Geometry ], ground_truths : List [Geometry ]):
272
210
"""
273
211
# Get iou score for all pairs of labels and predictions
274
212
"""
275
- return [(prediction , label ,
276
- _polygon_iou (prediction .to_shapely_poly () ,
277
- label . to_shapely_poly () ))
278
- for prediction , label in product (predictions , labels )]
213
+ return [(prediction , ground_truth ,
214
+ _polygon_iou (prediction .shapely ,
215
+ ground_truth . shapely ))
216
+ for prediction , ground_truth in product (predictions , ground_truths )]
279
217
280
218
281
219
def _polygon_iou (poly1 : Polygon , poly2 : Polygon ) -> float :
@@ -300,3 +238,4 @@ def _instance_urls_to_binary_mask(urls: List[str],
300
238
masks = _remove_opacity_channel ([url_to_numpy (url ) for url in urls ])
301
239
return np .sum ([np .all (mask == color , axis = - 1 ) for mask in masks ],
302
240
axis = 0 ) > 0
241
+
0 commit comments