Skip to content

Commit c186ca9

Browse files
authored
Merge pull request #271 from Labelbox/develop
3.3.0
2 parents c33fc04 + e89b9aa commit c186ca9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+1146
-247
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Changelog
22

3+
# Version 3.3.0 (2021-09-02)
4+
## Added
5+
* `Dataset.create_data_rows_sync()` for synchronous bulk uploads of data rows
6+
* `Model.delete()`, `ModelRun.delete()`, and `ModelRun.delete_annotation_groups()` to
7+
Clean up models, model runs, and annotation groups.
8+
9+
## Fix
10+
* Increased timeout for label exports since projects with many segmentation masks weren't finishing quickly enough.
11+
312
# Version 3.2.1 (2021-08-31)
413
## Fix
514
* Resolved issue with `create_data_rows()` was not working on amazon linux

labelbox/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name = "labelbox"
2-
__version__ = "3.2.1"
2+
__version__ = "3.3.0"
33

44
from labelbox.schema.project import Project
55
from labelbox.client import Client

labelbox/data/annotation_types/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,4 @@
2929
from .collection import LabelGenerator
3030

3131
from .metrics import ScalarMetric
32+
from .metrics import MetricAggregation

labelbox/data/annotation_types/annotation.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
from typing import Any, Dict, List, Union
22

3-
from pydantic.main import BaseModel
4-
53
from .classification import Checklist, Dropdown, Radio, Text
64
from .feature import FeatureSchema
75
from .geometry import Geometry
@@ -16,6 +14,7 @@ class BaseAnnotation(FeatureSchema):
1614

1715
class ClassificationAnnotation(BaseAnnotation):
1816
"""Class representing classification annotations (annotations that don't have a location) """
17+
1918
value: Union[Text, Checklist, Radio, Dropdown]
2019

2120

labelbox/data/annotation_types/classification/classification.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,24 @@
11
from typing import Any, Dict, List
22

3-
from pydantic.main import BaseModel
3+
try:
4+
from typing import Literal
5+
except:
6+
from typing_extensions import Literal
47

8+
from pydantic import BaseModel, validator
59
from ..feature import FeatureSchema
610

711

12+
# TODO: Replace when pydantic adds support for unions that don't coerce types
13+
class _TempName(BaseModel):
14+
name: str
15+
16+
def dict(self, *args, **kwargs):
17+
res = super().dict(*args, **kwargs)
18+
res.pop('name')
19+
return res
20+
21+
822
class ClassificationAnswer(FeatureSchema):
923
"""
1024
- Represents a classification option.
@@ -19,8 +33,9 @@ class Radio(BaseModel):
1933
answer: ClassificationAnswer
2034

2135

22-
class Checklist(BaseModel):
36+
class Checklist(_TempName):
2337
""" A classification with many selected options allowed """
38+
name: Literal["checklist"] = "checklist"
2439
answer: List[ClassificationAnswer]
2540

2641

@@ -29,9 +44,10 @@ class Text(BaseModel):
2944
answer: str
3045

3146

32-
class Dropdown(BaseModel):
47+
class Dropdown(_TempName):
3348
"""
3449
- A classification with many selected options allowed .
3550
- This is not currently compatible with MAL.
3651
"""
52+
name: Literal["dropdown"] = "dropdown"
3753
answer: List[ClassificationAnswer]

labelbox/data/annotation_types/data/raster.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ def value(self) -> np.ndarray:
8181
with open(self.file_path, "rb") as img:
8282
im_bytes = img.read()
8383
self.im_bytes = im_bytes
84-
return self.bytes_to_np(im_bytes)
84+
arr = self.bytes_to_np(im_bytes)
85+
return arr
8586
elif self.url is not None:
8687
im_bytes = self.fetch_remote()
8788
self.im_bytes = im_bytes
@@ -92,7 +93,7 @@ def value(self) -> np.ndarray:
9293
def set_fetch_fn(self, fn):
9394
object.__setattr__(self, 'fetch_remote', lambda: fn(self))
9495

95-
@retry.Retry(deadline=15.)
96+
@retry.Retry(deadline=60.)
9697
def fetch_remote(self) -> bytes:
9798
"""
9899
Method for accessing url.
@@ -104,7 +105,7 @@ def fetch_remote(self) -> bytes:
104105
response.raise_for_status()
105106
return response.content
106107

107-
@retry.Retry(deadline=15.)
108+
@retry.Retry(deadline=30.)
108109
def create_url(self, signer: Callable[[bytes], str]) -> str:
109110
"""
110111
Utility for creating a url from any of the other image representations.

labelbox/data/annotation_types/label.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from collections import defaultdict
2+
from labelbox.data.annotation_types.metrics.scalar import ScalarMetric
23

34
from typing import Any, Callable, Dict, List, Union, Optional
45

@@ -21,7 +22,8 @@ class Label(BaseModel):
2122
data: Union[VideoData, ImageData, TextData]
2223
annotations: List[Union[ClassificationAnnotation, ObjectAnnotation,
2324
VideoObjectAnnotation,
24-
VideoClassificationAnnotation, ScalarMetric]] = []
25+
VideoClassificationAnnotation, ScalarMetric,
26+
ScalarMetric]] = []
2527
extra: Dict[str, Any] = {}
2628

2729
def object_annotations(self) -> List[ObjectAnnotation]:

labelbox/data/annotation_types/metrics.py

Lines changed: 0 additions & 9 deletions
This file was deleted.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from .scalar import ScalarMetric
2+
from .aggregations import MetricAggregation
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from enum import Enum
2+
3+
4+
class MetricAggregation(Enum):
5+
ARITHMETIC_MEAN = "ARITHMETIC_MEAN"
6+
GEOMETRIC_MEAN = "GEOMETRIC_MEAN"
7+
HARMONIC_MEAN = "HARMONIC_MEAN"
8+
SUM = "SUM"
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from labelbox.data.annotation_types.metrics.aggregations import MetricAggregation
2+
from typing import Any, Dict, Optional
3+
from pydantic import BaseModel
4+
5+
6+
class ScalarMetric(BaseModel):
7+
""" Class representing metrics
8+
9+
# For backwards compatibility, metric_name is optional. This will eventually be deprecated
10+
# The metric_name will be set to a default name in the editor if it is not set.
11+
12+
# aggregation will be ignored wihtout providing a metric name.
13+
# Not providing a metric name is deprecated.
14+
"""
15+
value: float
16+
metric_name: Optional[str] = None
17+
feature_name: Optional[str] = None
18+
subclass_name: Optional[str] = None
19+
aggregation: MetricAggregation = MetricAggregation.ARITHMETIC_MEAN
20+
extra: Dict[str, Any] = {}
21+
22+
def dict(self, *args, **kwargs):
23+
res = super().dict(*args, **kwargs)
24+
if res['metric_name'] is None:
25+
res.pop('aggregation')
26+
return {k: v for k, v in res.items() if v is not None}

labelbox/data/metrics/group.py

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
"""
2+
Tools for grouping features and labels so that we can compute metrics on the individual groups
3+
"""
4+
from collections import defaultdict
5+
from typing import Dict, List, Tuple, Union
6+
try:
7+
from typing import Literal
8+
except ImportError:
9+
from typing_extensions import Literal
10+
11+
from labelbox.data.annotation_types import Label
12+
from labelbox.data.annotation_types.collection import LabelList
13+
from labelbox.data.annotation_types.feature import FeatureSchema
14+
15+
16+
def get_identifying_key(
17+
features_a: List[FeatureSchema], features_b: List[FeatureSchema]
18+
) -> Union[Literal['name'], Literal['feature_schema_id']]:
19+
"""
20+
Checks to make sure that features in both sets contain the same type of identifying keys.
21+
This can either be the feature name or feature schema id.
22+
23+
Args:
24+
features_a : List of FeatureSchemas (usually ObjectAnnotations or ClassificationAnnotations)
25+
features_b : List of FeatureSchemas (usually ObjectAnnotations or ClassificationAnnotations)
26+
Returns:
27+
The field name that is present in both feature lists.
28+
"""
29+
30+
all_schema_ids_defined_pred, all_names_defined_pred = all_have_key(
31+
features_a)
32+
if (not all_schema_ids_defined_pred and not all_names_defined_pred):
33+
raise ValueError("All data must have feature_schema_ids or names set")
34+
35+
all_schema_ids_defined_gt, all_names_defined_gt = all_have_key(features_b)
36+
37+
# Prefer name becuse the user will be able to know what it means
38+
# Schema id incase that doesn't exist.
39+
if (all_names_defined_pred and all_names_defined_gt):
40+
return 'name'
41+
elif all_schema_ids_defined_pred and all_schema_ids_defined_gt:
42+
return 'feature_schema_id'
43+
else:
44+
raise ValueError(
45+
"Ground truth and prediction annotations must have set all name or feature ids. "
46+
"Otherwise there is no key to match on. Please update.")
47+
48+
49+
def all_have_key(features: List[FeatureSchema]) -> Tuple[bool, bool]:
50+
"""
51+
Checks to make sure that all FeatureSchemas have names set or feature_schema_ids set.
52+
53+
Args:
54+
features (List[FeatureSchema]) :
55+
56+
"""
57+
all_names = True
58+
all_schemas = True
59+
for feature in features:
60+
if feature.name is None:
61+
all_names = False
62+
if feature.feature_schema_id is None:
63+
all_schemas = False
64+
return all_schemas, all_names
65+
66+
67+
def get_label_pairs(labels_a: LabelList,
68+
labels_b: LabelList,
69+
match_on="uid",
70+
filter=False) -> Dict[str, Tuple[Label, Label]]:
71+
"""
72+
This is a function to pairing a list of prediction labels and a list of ground truth labels easier.
73+
There are a few potentiall problems with this function.
74+
We are assuming that the data row `uid` or `external id` have been provided by the user.
75+
However, these particular fields are not required and can be empty.
76+
If this assumption fails, then the user has to determine their own matching strategy.
77+
78+
Args:
79+
labels_a (LabelList): A collection of labels to match with labels_b
80+
labels_b (LabelList): A collection of labels to match with labels_a
81+
match_on ('uid' or 'external_id'): The data row key to match labels by. Can either be uid or external id.
82+
filter (bool): Whether or not to ignore mismatches
83+
84+
Returns:
85+
A dict containing the union of all either uids or external ids and values as a tuple of the matched labels
86+
87+
"""
88+
89+
if match_on not in ['uid', 'external_id']:
90+
raise ValueError("Can only match on `uid` or `exteranl_id`.")
91+
92+
label_lookup_a = {
93+
getattr(label.data, match_on, None): label for label in labels_a
94+
}
95+
label_lookup_b = {
96+
getattr(label.data, match_on, None): label for label in labels_b
97+
}
98+
all_keys = set(label_lookup_a.keys()).union(label_lookup_b.keys())
99+
if None in label_lookup_a or None in label_lookup_b:
100+
raise ValueError(
101+
f"One or more of the labels has a data row without the required key {match_on}."
102+
" It cannot be determined which labels match without this information."
103+
f" Either assign {match_on} to each Label or create your own pairing function."
104+
)
105+
pairs = defaultdict(list)
106+
for key in all_keys:
107+
a, b = label_lookup_a.pop(key, None), label_lookup_b.pop(key, None)
108+
if a is None or b is None:
109+
if not filter:
110+
raise ValueError(
111+
f"{match_on} {key} is not available in both LabelLists. "
112+
"Set `filter = True` to filter out these examples, assign the ids manually, or create your own matching function."
113+
)
114+
else:
115+
continue
116+
pairs[key].append([a, b])
117+
return pairs
118+
119+
120+
def get_feature_pairs(
121+
features_a: List[FeatureSchema], features_b: List[FeatureSchema]
122+
) -> Dict[str, Tuple[List[FeatureSchema], List[FeatureSchema]]]:
123+
"""
124+
Matches features by schema_ids
125+
126+
Args:
127+
labels_a (List[FeatureSchema]): A list of features to match with features_b
128+
labels_b (List[FeatureSchema]): A list of features to match with features_a
129+
Returns:
130+
The matched features as dict. The key will be the feature name and the value will be
131+
two lists each containing the matched features from each set.
132+
133+
"""
134+
identifying_key = get_identifying_key(features_a, features_b)
135+
lookup_a, lookup_b = _create_feature_lookup(
136+
features_a,
137+
identifying_key), _create_feature_lookup(features_b, identifying_key)
138+
139+
keys = set(lookup_a.keys()).union(set(lookup_b.keys()))
140+
result = defaultdict(list)
141+
for key in keys:
142+
result[key].extend([lookup_a[key], lookup_b[key]])
143+
return result
144+
145+
146+
def _create_feature_lookup(features: List[FeatureSchema],
147+
key: str) -> Dict[str, List[FeatureSchema]]:
148+
"""
149+
Groups annotation by name (if available otherwise feature schema id).
150+
151+
Args:
152+
annotations: List of annotations to group
153+
Returns:
154+
a dict where each key is the feature_schema_id (or name)
155+
and the value is a list of annotations that have that feature_schema_id (or name)
156+
"""
157+
grouped_features = defaultdict(list)
158+
for feature in features:
159+
grouped_features[getattr(feature, key)].append(feature)
160+
return grouped_features

labelbox/data/metrics/iou/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from .calculation import *
2+
from .iou import *

0 commit comments

Comments
 (0)