11# type: ignore
2+ from labelbox .data .annotation_types .classification .classification import Checklist , Text , Radio
3+ from labelbox .data .annotation_types import feature
24from typing import Dict , Any , List , Optional , Tuple , Union
35from shapely .geometry import Polygon
46from itertools import product
57import numpy as np
8+ from collections import defaultdict
69
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
1513
16- VectorTool = Union [NDPoint , NDRectangle , NDPolyline , NDPolygon ]
17- ClassificationTool = Union [NDText , NDRadio , NDChecklist ]
1814
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 :
2116 """
2217 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.
2320
2421 Args:
2522 predictions: List of masks objects
26- labels : List of masks objects
23+ ground_truths : List of masks objects
2724 Returns:
2825 float indicating iou score
2926 """
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 )
3034
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 )
4535
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 :
4938 """
5039 Computes iou for classification features.
5140
@@ -67,13 +56,13 @@ def classification_miou(predictions: List[ClassificationTool],
6756 "Classification features must be the same type to compute agreement. "
6857 f"Found `{ type (prediction )} ` and `{ type (label )} `" )
6958
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 }
7766 return float (
7867 len (schema_ids_label & schema_ids_pred ) /
7968 len (schema_ids_label | schema_ids_pred ))
@@ -82,8 +71,8 @@ def classification_miou(predictions: List[ClassificationTool],
8271
8372
8473def 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 ]:
8776 """
8877
8978 Computes subclass iou score between two vector tools that were matched.
@@ -96,12 +85,10 @@ def subclassification_miou(
9685 miou across all subclasses.
9786 """
9887
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 )
10190 feature_schemas = set (subclass_predictions .keys ()).union (
10291 set (subclass_labels .keys ()))
103- # There should only be one feature schema per subclass.
104-
10592 classification_iou = [
10693 feature_miou (subclass_predictions [feature_schema ],
10794 subclass_labels [feature_schema ])
@@ -111,7 +98,7 @@ def subclassification_miou(
11198 return None if not len (classification_iou ) else np .mean (classification_iou )
11299
113100
114- def vector_miou (predictions : List [VectorTool ], labels : List [VectorTool ],
101+ def vector_miou (predictions : List [Geometry ], labels : List [Geometry ],
115102 include_subclasses ) -> float :
116103 """
117104 Computes an iou score for vector tools.
@@ -148,8 +135,8 @@ def vector_miou(predictions: List[VectorTool], labels: List[VectorTool],
148135 return np .mean (solution_agreements )
149136
150137
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 ] ],
153140 include_subclasses = True ) -> Optional [float ]:
154141 """
155142 Computes iou score for all features with the same feature schema id.
@@ -159,7 +146,6 @@ def feature_miou(predictions: List[NDAnnotation],
159146 labels: List of labels with the same feature schema.
160147 Returns:
161148 float representing the iou score for the feature type if score can be computed otherwise None.
162-
163149 """
164150 if len (predictions ):
165151 keys = predictions [0 ]
@@ -170,32 +156,31 @@ def feature_miou(predictions: List[NDAnnotation],
170156 # Ignore examples that do not have any labels or predictions
171157 return None
172158
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?
183161 return mask_miou (predictions , labels )
184- elif tool_type in NDTool . get_union_types ( ):
162+ elif isinstance ( predictions [ 0 ]. value , Geometry ):
185163 return vector_miou (predictions ,
186164 labels ,
187165 include_subclasses = include_subclasses )
188- elif tool_type in NDClassification . get_union_types ( ):
166+ elif isinstance ( predictions [ 0 ]. value , ClassificationAnnotation ):
189167 return classification_miou (predictions , labels )
190168 else :
191- raise ValueError (f"Unexpected annotation found. Found { tool_type } " )
169+ raise ValueError (f"Unexpected annotation found. Found { type ( predictions [ 0 ]) } " )
192170
193171
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 ,
196180 include_classifications = True ,
197181 include_subclasses = True ) -> float :
198182 """
183+ # At this point all object should have schema ids.
199184
200185 Args:
201186 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]],
204189 include_subclassifications: Whether or not to factor in subclassifications into the iou score
205190 Returns:
206191 float indicating the iou score for this data row.
207-
208192 """
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 ()))
213197 ious = [
214- feature_miou (predictions [feature_schema ],
215- labels [feature_schema ],
198+ feature_miou (prediction_annotations [feature_schema ],
199+ ground_truth_annotations [feature_schema ],
216200 include_subclasses = include_subclasses )
217201 for feature_schema in feature_schemas
218202 ]
@@ -222,60 +206,14 @@ def datarow_miou(label_content: List[Dict[str, Any]],
222206 return np .mean (ious )
223207
224208
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 ]):
272210 """
273211 # Get iou score for all pairs of labels and predictions
274212 """
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 )]
279217
280218
281219def _polygon_iou (poly1 : Polygon , poly2 : Polygon ) -> float :
@@ -300,3 +238,4 @@ def _instance_urls_to_binary_mask(urls: List[str],
300238 masks = _remove_opacity_channel ([url_to_numpy (url ) for url in urls ])
301239 return np .sum ([np .all (mask == color , axis = - 1 ) for mask in masks ],
302240 axis = 0 ) > 0
241+
0 commit comments