Skip to content

Commit 6d3bdc5

Browse files
authored
Merge pull request #211 from Labelbox/ms/mea-fix
annotation metric type and demo
2 parents 3e98d9d + 4cbc322 commit 6d3bdc5

File tree

17 files changed

+478
-152
lines changed

17 files changed

+478
-152
lines changed

examples/model_assisted_labeling/image_mea.ipynb

Lines changed: 354 additions & 115 deletions
Large diffs are not rendered by default.

labelbox/data/annotation_types/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,5 @@
2626

2727
from .collection import LabelList
2828
from .collection import LabelGenerator
29+
30+
from .metrics import ScalarMetric

labelbox/data/annotation_types/data/raster.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class RasterData(BaseData):
2323

2424
@classmethod
2525
def from_2D_arr(cls, arr: TypedArray[Literal['uint8']], **kwargs):
26-
if len(arr.shape):
26+
if len(arr.shape) != 2:
2727
raise ValueError(
2828
f"Found array with shape {arr.shape}. Expected two dimensions ([W,H])"
2929
)
@@ -38,7 +38,7 @@ def bytes_to_np(self, image_bytes: bytes) -> np.ndarray:
3838
Returns:
3939
numpy array representing the image
4040
"""
41-
return np.array(Image.open(BytesIO(image_bytes)))
41+
return np.array(Image.open(BytesIO(image_bytes)))[:, :, :3]
4242

4343
def np_to_bytes(self, arr: np.ndarray) -> bytes:
4444
"""

labelbox/data/annotation_types/label.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from collections import defaultdict
2-
from typing import Any, Callable, Dict, List, Union
2+
3+
from typing import Any, Callable, Dict, List, Union, Optional
34

45
from pydantic import BaseModel, validator
56

@@ -9,28 +10,30 @@
910
from .classification import ClassificationAnswer
1011
from .data import VideoData, TextData, RasterData
1112
from .geometry import Mask
12-
from .metrics import Metric
13+
from .metrics import ScalarMetric
14+
from .types import Cuid
1315
from .annotation import (ClassificationAnnotation, ObjectAnnotation,
1416
VideoClassificationAnnotation, VideoObjectAnnotation)
15-
from labelbox.data.annotation_types import annotation
1617

1718

1819
class Label(BaseModel):
20+
uid: Optional[Cuid] = None
1921
data: Union[VideoData, RasterData, TextData]
2022
annotations: List[Union[ClassificationAnnotation, ObjectAnnotation,
2123
VideoObjectAnnotation,
22-
VideoClassificationAnnotation, Metric]] = []
24+
VideoClassificationAnnotation, ScalarMetric]] = []
2325
extra: Dict[str, Any] = {}
2426

2527
def object_annotations(self) -> List[ObjectAnnotation]:
26-
return self.get_annotations_by_type(ObjectAnnotation)
28+
return self._get_annotations_by_type(ObjectAnnotation)
2729

2830
def classification_annotations(self) -> List[ClassificationAnnotation]:
29-
return self.get_annotations_by_type(ClassificationAnnotation)
31+
return self._get_annotations_by_type(ClassificationAnnotation)
3032

31-
def get_annotations_by_type(self, annotation_type):
33+
def _get_annotations_by_type(self, annotation_type):
3234
return [
33-
annot for annot in self.annotations if isinstance(annotation_type)
35+
annot for annot in self.annotations
36+
if isinstance(annot, annotation_type)
3437
]
3538

3639
def frame_annotations(
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
from typing import Any, Dict
2+
from labelbox.data.annotation_types.feature import FeatureSchema
13
from pydantic import BaseModel
24

35

4-
class Metric(BaseModel):
6+
class ScalarMetric(BaseModel):
57
""" Class representing metrics """
6-
metric_value: float
8+
value: float
9+
extra: Dict[str, Any] = {}

labelbox/data/serialization/labelbox_v1/converter.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ def serialize(
5757
"""
5858
Converts a labelbox common object to the labelbox json export format
5959
60+
Note that any metric annotations will not be written since they are not defined in the LBV1 format.
61+
6062
Args:
6163
labels: Either a LabelList or a LabelGenerator (LabelCollection)
6264
Returns:

labelbox/data/serialization/labelbox_v1/label.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,12 @@ class LBV1Label(BaseModel):
120120
label: Union[LBV1LabelAnnotations,
121121
List[LBV1LabelAnnotationsVideo]] = Field(..., alias='Label')
122122
data_row_id: str = Field(..., alias="DataRow ID")
123-
row_data: str = Field(..., alias="Labeled Data")
123+
row_data: str = Field(None, alias="Labeled Data")
124+
id: Optional[str] = Field(None, alias='ID')
124125
external_id: Optional[str] = Field(None, alias="External ID")
125126

126127
created_by: Optional[str] = Extra('Created By')
127128
project_name: Optional[str] = Extra('Project Name')
128-
id: Optional[str] = Extra('ID')
129129
created_at: Optional[str] = Extra('Created At')
130130
updated_at: Optional[str] = Extra('Updated At')
131131
seconds_to_label: Optional[float] = Extra('Seconds to Label')
@@ -149,9 +149,9 @@ def to_common(self) -> Label:
149149
else:
150150
annotations = self.label.to_common()
151151
data = self._infer_media_type()
152-
print(data)
153152

154153
return Label(data=data,
154+
uid=self.id,
155155
annotations=annotations,
156156
extra={
157157
field.alias: getattr(self, field_name)
@@ -167,13 +167,8 @@ def from_common(cls, label: Label):
167167
else:
168168
label_ = LBV1LabelAnnotations.from_common(label.annotations)
169169

170-
if label.data.url is None:
171-
raise ValueError(
172-
"Url attribute required for serializing data objects. "
173-
"Use <LabelList,LabelGenerator>.add_url_to_data "
174-
"or <LabelList,LabelGenerator>.add_to_dataset")
175-
176170
return LBV1Label(label=label_,
171+
id=label.uid,
177172
data_row_id=label.data.uid,
178173
row_data=label.data.url,
179174
external_id=label.data.external_id,

labelbox/data/serialization/ndjson/classification.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def from_common(cls, text: Text, schema_id: Cuid, extra: Dict[str, Any],
101101
data: Union[TextData, RasterData]) -> "NDText":
102102
return cls(
103103
answer=text.answer,
104-
dataRow={'id': data.uid},
104+
data_row={'id': data.uid},
105105
schema_id=schema_id,
106106
uuid=extra.get('uuid'),
107107
)
@@ -116,7 +116,7 @@ def from_common(
116116
return cls(answer=[
117117
NDFeature(schema_id=answer.schema_id) for answer in checklist.answer
118118
],
119-
dataRow={'id': data.uid},
119+
data_row={'id': data.uid},
120120
schema_id=schema_id,
121121
uuid=extra.get('uuid'),
122122
frames=extra.get('frames'))
@@ -128,7 +128,7 @@ class NDRadio(NDAnnotation, NDRadioSubclass, VideoSupported):
128128
def from_common(cls, radio: Radio, schema_id: Cuid, extra: Dict[str, Any],
129129
data: Union[VideoData, TextData, RasterData]) -> "NDRadio":
130130
return cls(answer=NDFeature(schema_id=radio.answer.schema_id),
131-
dataRow={'id': data.uid},
131+
data_row={'id': data.uid},
132132
schema_id=schema_id,
133133
uuid=extra.get('uuid'),
134134
frames=extra.get('frames'))

labelbox/data/serialization/ndjson/label.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from itertools import groupby
2+
from labelbox.data.annotation_types.metrics import ScalarMetric
3+
24
from operator import itemgetter
35
from typing import Dict, Generator, List, Tuple, Union
46
from collections import defaultdict
@@ -10,12 +12,13 @@
1012
from ...annotation_types.data import RasterData, TextData, VideoData
1113
from ...annotation_types.label import Label
1214
from ...annotation_types.ner import TextEntity
13-
from .classification import NDChecklistSubclass, NDClassification, NDRadioSubclass, NDClassificationType
15+
from .metric import NDMetricAnnotation, NDMetricType
16+
from .classification import NDChecklistSubclass, NDClassification, NDClassificationType, NDRadioSubclass
1417
from .objects import NDObject, NDObjectType
1518

1619

1720
class NDLabel(BaseModel):
18-
annotations: List[Union[NDObjectType, NDClassificationType]]
21+
annotations: List[Union[NDObjectType, NDClassificationType, NDMetricType]]
1922

2023
def to_common(self) -> LabelGenerator:
2124
grouped_annotations = defaultdict(list)
@@ -33,7 +36,8 @@ def from_common(cls,
3336

3437
def _generate_annotations(
3538
self, grouped_annotations: Dict[str, List[Union[NDObjectType,
36-
NDClassificationType]]]
39+
NDClassificationType,
40+
NDMetricType]]]
3741
) -> Generator[Label, None, None]:
3842
for data_row_id, annotations in grouped_annotations.items():
3943
annots = []
@@ -42,6 +46,8 @@ def _generate_annotations(
4246
annots.append(NDObject.to_common(annotation))
4347
elif isinstance(annotation, NDClassificationType.__args__):
4448
annots.extend(NDClassification.to_common(annotation))
49+
elif isinstance(annotation, NDMetricType):
50+
annots.append(NDMetricAnnotation.to_common(annotation))
4551
else:
4652
raise TypeError(
4753
f"Unsupported annotation. {type(annotation)}")
@@ -99,6 +105,8 @@ def _create_non_video_annotations(cls, label: Label):
99105
yield NDClassification.from_common(annotation, label.data)
100106
elif isinstance(annotation, ObjectAnnotation):
101107
yield NDObject.from_common(annotation, label.data)
108+
elif isinstance(annotation, ScalarMetric):
109+
yield NDMetricAnnotation.from_common(annotation, label.data)
102110
else:
103111
raise TypeError(
104112
f"Unable to convert object to MAL format. `{type(annotation.value)}`"
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from labelbox.data.annotation_types.data import TextData, RasterData
2+
from labelbox.data.annotation_types.metrics import ScalarMetric
3+
from labelbox.data.serialization.ndjson.base import NDJsonBase
4+
from typing import Union
5+
6+
7+
class NDDataRowScalarMetric(NDJsonBase):
8+
metric_value: float
9+
10+
def to_common(self) -> ScalarMetric:
11+
return ScalarMetric(value=self.metric_value, extra={'uuid': self.uuid})
12+
13+
@classmethod
14+
def from_common(
15+
cls, metric: ScalarMetric,
16+
data: Union[TextData, RasterData]) -> "NDDataRowScalarMetric":
17+
return NDDataRowScalarMetric(uuid=metric.extra.get('uuid'),
18+
metric_value=metric.value,
19+
data_row={'id': data.uid})
20+
21+
22+
class NDMetricAnnotation:
23+
24+
@classmethod
25+
def to_common(cls, annotation: NDDataRowScalarMetric) -> ScalarMetric:
26+
return annotation.to_common()
27+
28+
@classmethod
29+
def from_common(cls, annotation: ScalarMetric,
30+
data: Union[TextData, RasterData]) -> NDDataRowScalarMetric:
31+
return NDDataRowScalarMetric.from_common(annotation, data)
32+
33+
34+
NDMetricType = NDDataRowScalarMetric

0 commit comments

Comments
 (0)