Skip to content

Commit b27e132

Browse files
authored
Merge pull request #273 from Labelbox/ms/custom-scalar-metrics
scalar metrics
2 parents 9ca7187 + 8dd53bd commit b27e132

File tree

24 files changed

+682
-142
lines changed

24 files changed

+682
-142
lines changed

labelbox/data/annotation_types/__init__.py

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

3131
from .metrics import ScalarMetric
32-
from .metrics import CustomScalarMetric
3332
from .metrics import MetricAggregation

labelbox/data/annotation_types/annotation.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class BaseAnnotation(FeatureSchema):
1414

1515
class ClassificationAnnotation(BaseAnnotation):
1616
"""Class representing classification annotations (annotations that don't have a location) """
17+
1718
value: Union[Text, Checklist, Radio, Dropdown]
1819

1920

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: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from collections import defaultdict
2-
from labelbox.data.annotation_types.metrics.scalar import CustomScalarMetric
2+
from labelbox.data.annotation_types.metrics.scalar import ScalarMetric
33

44
from typing import Any, Callable, Dict, List, Union, Optional
55

@@ -22,7 +22,7 @@ class Label(BaseModel):
2222
data: Union[VideoData, ImageData, TextData]
2323
annotations: List[Union[ClassificationAnnotation, ObjectAnnotation,
2424
VideoObjectAnnotation,
25-
VideoClassificationAnnotation, CustomScalarMetric,
25+
VideoClassificationAnnotation, ScalarMetric,
2626
ScalarMetric]] = []
2727
extra: Dict[str, Any] = {}
2828

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
from .scalar import ScalarMetric, CustomScalarMetric
1+
from .scalar import ScalarMetric
22
from .aggregations import MetricAggregation

labelbox/data/annotation_types/metrics/scalar.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,23 @@
44

55

66
class ScalarMetric(BaseModel):
7-
""" Class representing metrics """
8-
value: float
9-
extra: Dict[str, Any] = {}
7+
""" Class representing metrics
108
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.
1111
12-
class CustomScalarMetric(BaseModel):
13-
metric_name: str
12+
# aggregation will be ignored wihtout providing a metric name.
13+
# Not providing a metric name is deprecated.
14+
"""
1415
value: float
16+
metric_name: Optional[str] = None
1517
feature_name: Optional[str] = None
1618
subclass_name: Optional[str] = None
1719
aggregation: MetricAggregation = MetricAggregation.ARITHMETIC_MEAN
1820
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)