Skip to content

Commit 48d1c56

Browse files
author
Matt Sokoloff
committed
wip
1 parent 39f0809 commit 48d1c56

File tree

10 files changed

+78
-210
lines changed

10 files changed

+78
-210
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ test-staging: build
66
docker run -it -v ${PWD}:/usr/src -w /usr/src \
77
-e LABELBOX_TEST_ENVIRON="staging" \
88
-e LABELBOX_TEST_API_KEY_STAGING=${LABELBOX_TEST_API_KEY_STAGING} \
9-
local/labelbox-python:test pytest $(PATH_TO_TEST) -svvx
9+
local/labelbox-python:test pytest $(PATH_TO_TEST) -svv
1010

1111
test-prod: build
1212
docker run -it -v ${PWD}:/usr/src -w /usr/src \

labelbox/data/annotation_types/data/raster.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import numpy as np
55
import requests
6+
from google.api_core import retry
67
from typing_extensions import Literal
78
from pydantic import root_validator
89
from PIL import Image
@@ -84,6 +85,7 @@ def data(self) -> np.ndarray:
8485
def set_fetch_fn(self, fn):
8586
object.__setattr__(self, 'fetch_remote', lambda: fn(self))
8687

88+
@retry.Retry(deadline=15.)
8789
def fetch_remote(self) -> bytes:
8890
"""
8991
Method for accessing url.
@@ -95,6 +97,7 @@ def fetch_remote(self) -> bytes:
9597
response.raise_for_status()
9698
return response.content
9799

100+
@retry.Retry(deadline=15.)
98101
def create_url(self, signer: Callable[[bytes], str]) -> str:
99102
"""
100103
Utility for creating a url from any of the other image representations.

labelbox/data/annotation_types/data/text.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import Callable, Optional
22

33
import requests
4+
from google.api_core import retry
45
from pydantic import root_validator
56

67
from .base_data import BaseData
@@ -39,6 +40,7 @@ def data(self) -> str:
3940
def set_fetch_fn(self, fn):
4041
object.__setattr__(self, 'fetch_remote', lambda: fn(self))
4142

43+
@retry.Retry(deadline=15.)
4244
def fetch_remote(self) -> str:
4345
"""
4446
Method for accessing url.
@@ -50,6 +52,7 @@ def fetch_remote(self) -> str:
5052
response.raise_for_status()
5153
return response.text
5254

55+
@retry.Retry(deadline=15.)
5356
def create_url(self, signer: Callable[[bytes], str]) -> None:
5457
"""
5558
Utility for creating a url from any of the other text references.

labelbox/data/annotation_types/data/video.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import cv2
99
import numpy as np
10+
from google.api_core import retry
1011
from pydantic import root_validator
1112

1213
from .base_data import BaseData
@@ -89,6 +90,7 @@ def __getitem__(self, idx: int) -> np.ndarray:
8990
def set_fetch_fn(self, fn):
9091
object.__setattr__(self, 'fetch_remote', lambda: fn(self))
9192

93+
@retry.Retry(deadline=15.)
9294
def fetch_remote(self, local_path) -> None:
9395
"""
9496
Method for downloading data from self.url
@@ -101,6 +103,7 @@ def fetch_remote(self, local_path) -> None:
101103
"""
102104
urllib.request.urlretrieve(self.url, local_path)
103105

106+
@retry.Retry(deadline=15.)
104107
def create_url(self, signer: Callable[[bytes], str]) -> None:
105108
"""
106109
Utility for creating a url from any of the other video references.

labelbox/data/annotation_types/label.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from .metrics import Metric
1313
from .annotation import (ClassificationAnnotation, ObjectAnnotation,
1414
VideoClassificationAnnotation, VideoObjectAnnotation)
15+
from labelbox.data.annotation_types import annotation
1516

1617

1718
class Label(BaseModel):
@@ -22,16 +23,13 @@ class Label(BaseModel):
2223
extra: Dict[str, Any] = {}
2324

2425
def object_annotations(self) -> List[ObjectAnnotation]:
25-
return [
26-
annot for annot in self.annotations
27-
if isinstance(annot, ObjectAnnotation)
28-
]
26+
return self.get_annotations_by_type(ObjectAnnotation)
2927

3028
def classification_annotations(self) -> List[ClassificationAnnotation]:
31-
return [
32-
annot for annot in self.annotations
33-
if isinstance(annot, ClassificationAnnotation)
34-
]
29+
return self.get_annotations_by_type(ClassificationAnnotation)
30+
31+
def get_annotations_by_type(self, annotation_type):
32+
return [annot for annot in self.annotations if isinstance(annotation_type)]
3533

3634
def frame_annotations(
3735
self

labelbox/data/metrics/iou.py

Lines changed: 59 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,40 @@
11
# type: ignore
2+
from labelbox.data.annotation_types.classification.classification import Checklist, Text, Radio
3+
from labelbox.data.annotation_types import feature
24
from typing import Dict, Any, List, Optional, Tuple, Union
35
from shapely.geometry import Polygon
46
from itertools import product
57
import 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

8473
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]:
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

281219
def _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

Comments
 (0)