Skip to content

Commit ed993c8

Browse files
authored
Merge pull request #347 from Labelbox/develop
3.10.0
2 parents ce7a51a + 4071dc2 commit ed993c8

File tree

21 files changed

+328
-110
lines changed

21 files changed

+328
-110
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ ipython_config.py
8383
# pyenv
8484
.python-version
8585

86+
# vscode
87+
.vscode
88+
8689
# pipenv
8790
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
8891
# However, in case of collaboration, if having platform-specific dependencies or dependencies

CHANGELOG.md

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,25 @@
33
## Deprecation Notice
44
| Name | Replacement | Removed After |
55
| ------------------------------------- | ------------------------------------- | ------------- |
6-
| `ModelRun.delete_annotation_groups()` | `ModelRun.delete_model_run_data_rows()`| 3.9 |
7-
| `ModelRun.annotation_groups()` | `ModelRun.model_run_data_rows()` | 3.9 |
8-
| `DataRowMetadataSchema.id` | `DataRowMetadataSchema.uid` | 3.9 |
6+
| `ModelRun.delete_annotation_groups()` | `ModelRun.delete_model_run_data_rows()`| 2021-12-06 |
7+
| `ModelRun.annotation_groups()` | `ModelRun.model_run_data_rows()` | 2021-12-06 |
8+
| `DataRowMetadataSchema.id` | `DataRowMetadataSchema.uid` | 2021-12-06 |
99
-----
10+
11+
# Version 3.10.0 (2021-11-18)
12+
## Added
13+
* `AnnotationImport.wait_until_done()` accepts a `show_progress` param. This is set to `False` by default.
14+
* If enabled, a tqdm progress bar will indicate the import progress.
15+
* This works for all classes that inherit from AnnotationImport: `LabelImport`, `MALPredictionImport`, `MEAPredictionImport`
16+
* This is not support for `BulkImportRequest` (which will eventually be replaced by `MALPredictionImport`)
17+
* `Option.label` and `Option.value` can now be set independently
18+
* `ClassificationAnswer`s now support a new `keyframe` field for videos
19+
* New `LBV1Label.media_type field. This is a placeholder for future backend changes.
20+
21+
## Fix
22+
* Nested checklists can have extra brackets. This would cause the annotation type converter to break.
23+
24+
1025
# Version 3.9.0 (2021-11-12)
1126
## Added
1227
* New ontology management features
@@ -18,8 +33,8 @@
1833
* Set up a project from an existing ontology with `project.setup_edior()`
1934
* Added new `FeatureSchema` entity
2035
* Add support for new queue modes
21-
* Send batches of data direction to a project with `project.queue()`
22-
* Remove items from the queue with `project.dequeue()`
36+
* Send batches of data directly to a project queue with `project.queue()`
37+
* Remove items from a project queue with `project.dequeue()`
2338
* Query for and toggle the queue mode
2439

2540
# Version 3.8.0 (2021-10-22)

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,9 @@ The package `rasterio` installed by `labelbox[data]` relies on GDAL which could
5757
You may see the following error message:
5858

5959
```
60-
INFO:root:Building on Windows requires extra options to setup.py to locate needed GDAL files. More information is available in the README.
60+
INFO:root:Building on Windows requires extra options to setup.py to locate needed GDAL files. More information is available in the README.
6161
62-
ERROR: A GDAL API version must be specified. Provide a path to gdal-config using a GDAL_CONFIG environment variable or use a GDAL_VERSION environment variable.
62+
ERROR: A GDAL API version must be specified. Provide a path to gdal-config using a GDAL_CONFIG environment variable or use a GDAL_VERSION environment variable.
6363
```
6464

6565
As a workaround:
@@ -72,7 +72,7 @@ As a workaround:
7272

7373
Note: You need to download the right files for your Python version. In the files above `cp38` means CPython 3.8.
7474

75-
2. After downloading the files, please run the following commands, in this particular order.
75+
2. After downloading the files, please run the following commands, in this particular order.
7676

7777
```
7878
pip install GDAL‑3.3.2‑cp38‑cp38‑win_amd64.wh

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.9.0"
2+
__version__ = "3.10.0"
33

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

labelbox/data/annotation_types/classification/classification.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, Dict, List
1+
from typing import Any, Dict, List, Union, Optional
22

33
try:
44
from typing import Literal
@@ -24,13 +24,25 @@ class ClassificationAnswer(FeatureSchema):
2424
- Represents a classification option.
2525
- Because it inherits from FeatureSchema
2626
the option can be represented with either the name or feature_schema_id
27+
28+
- The keyframe arg only applies to video classifications.
29+
Each answer can have a keyframe independent of the others.
30+
So unlike object annotations, classification annotations
31+
track keyframes at a classification answer level.
2732
"""
2833
extra: Dict[str, Any] = {}
34+
keyframe: Optional[bool] = None
35+
36+
def dict(self, *args, **kwargs):
37+
res = super().dict(*args, **kwargs)
38+
if res['keyframe'] is None:
39+
res.pop('keyframe')
40+
return res
2941

3042

3143
class Radio(BaseModel):
3244
""" A classification with only one selected option allowed
33-
45+
3446
>>> Radio(answer = ClassificationAnswer(name = "dog"))
3547
3648
"""
@@ -50,7 +62,7 @@ class Checklist(_TempName):
5062
class Text(BaseModel):
5163
""" Free form text
5264
53-
>>> Text(answer = "some text answer")
65+
>>> Text(answer = "some text answer")
5466
5567
"""
5668
answer: str

labelbox/data/serialization/labelbox_v1/classification.py

Lines changed: 25 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -9,56 +9,53 @@
99

1010

1111
class LBV1ClassificationAnswer(LBV1Feature):
12-
...
12+
13+
def to_common(self) -> ClassificationAnswer:
14+
return ClassificationAnswer(feature_schema_id=self.schema_id,
15+
name=self.title,
16+
keyframe=self.keyframe,
17+
extra={
18+
'feature_id': self.feature_id,
19+
'value': self.value
20+
})
21+
22+
@classmethod
23+
def from_common(
24+
cls,
25+
answer: ClassificationAnnotation) -> "LBV1ClassificationAnswer":
26+
return cls(schema_id=answer.feature_schema_id,
27+
title=answer.name,
28+
value=answer.extra.get('value'),
29+
feature_id=answer.extra.get('feature_id'),
30+
keyframe=answer.keyframe)
1331

1432

1533
class LBV1Radio(LBV1Feature):
1634
answer: LBV1ClassificationAnswer
1735

1836
def to_common(self) -> Radio:
19-
return Radio(answer=ClassificationAnswer(
20-
feature_schema_id=self.answer.schema_id,
21-
name=self.answer.title,
22-
extra={
23-
'feature_id': self.answer.feature_id,
24-
'value': self.answer.value
25-
}))
37+
return Radio(answer=self.answer.to_common())
2638

2739
@classmethod
2840
def from_common(cls, radio: Radio, feature_schema_id: Cuid,
2941
**extra) -> "LBV1Radio":
3042
return cls(schema_id=feature_schema_id,
31-
answer=LBV1ClassificationAnswer(
32-
schema_id=radio.answer.feature_schema_id,
33-
title=radio.answer.name,
34-
value=radio.answer.extra.get('value'),
35-
feature_id=radio.answer.extra.get('feature_id')),
43+
answer=LBV1ClassificationAnswer.from_common(radio.answer),
3644
**extra)
3745

3846

3947
class LBV1Checklist(LBV1Feature):
4048
answers: List[LBV1ClassificationAnswer]
4149

4250
def to_common(self) -> Checklist:
43-
return Checklist(answer=[
44-
ClassificationAnswer(feature_schema_id=answer.schema_id,
45-
name=answer.title,
46-
extra={
47-
'feature_id': answer.feature_id,
48-
'value': answer.value
49-
}) for answer in self.answers
50-
])
51+
return Checklist(answer=[answer.to_common() for answer in self.answers])
5152

5253
@classmethod
5354
def from_common(cls, checklist: Checklist, feature_schema_id: Cuid,
5455
**extra) -> "LBV1Checklist":
5556
return cls(schema_id=feature_schema_id,
5657
answers=[
57-
LBV1ClassificationAnswer(
58-
schema_id=answer.feature_schema_id,
59-
title=answer.name,
60-
value=answer.extra.get('value'),
61-
feature_id=answer.extra.get('feature_id'))
58+
LBV1ClassificationAnswer.from_common(answer)
6259
for answer in checklist.answer
6360
],
6461
**extra)
@@ -68,25 +65,14 @@ class LBV1Dropdown(LBV1Feature):
6865
answer: List[LBV1ClassificationAnswer]
6966

7067
def to_common(self) -> Dropdown:
71-
return Dropdown(answer=[
72-
ClassificationAnswer(feature_schema_id=answer.schema_id,
73-
name=answer.title,
74-
extra={
75-
'feature_id': answer.feature_id,
76-
'value': answer.value
77-
}) for answer in self.answer
78-
])
68+
return Dropdown(answer=[answer.to_common() for answer in self.answer])
7969

8070
@classmethod
8171
def from_common(cls, dropdown: Dropdown, feature_schema_id: Cuid,
8272
**extra) -> "LBV1Dropdown":
8373
return cls(schema_id=feature_schema_id,
8474
answer=[
85-
LBV1ClassificationAnswer(
86-
schema_id=answer.feature_schema_id,
87-
title=answer.name,
88-
value=answer.extra.get('value'),
89-
feature_id=answer.extra.get('feature_id'))
75+
LBV1ClassificationAnswer.from_common(answer)
9076
for answer in dropdown.answer
9177
],
9278
**extra)

labelbox/data/serialization/labelbox_v1/label.py

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -137,20 +137,17 @@ class LBV1Label(BaseModel):
137137
label_url: Optional[str] = Extra('View Label')
138138
has_open_issues: Optional[float] = Extra('Has Open Issues')
139139
skipped: Optional[bool] = Extra('Skipped')
140+
media_type: Optional[str] = Extra('media_type')
140141

141142
def to_common(self) -> Label:
142143
if isinstance(self.label, list):
143144
annotations = []
144145
for lbl in self.label:
145146
annotations.extend(lbl.to_common())
146-
data = VideoData(url=self.row_data,
147-
external_id=self.external_id,
148-
uid=self.data_row_id)
149147
else:
150148
annotations = self.label.to_common()
151-
data = self._infer_media_type()
152149

153-
return Label(data=data,
150+
return Label(data=self._data_row_to_common(),
154151
uid=self.id,
155152
annotations=annotations,
156153
extra={
@@ -174,44 +171,49 @@ def from_common(cls, label: Label):
174171
external_id=label.data.external_id,
175172
**label.extra)
176173

177-
def _infer_media_type(self):
178-
# Video annotations are formatted differently from text and images
179-
# So we only need to differentiate those two
174+
def _data_row_to_common(self) -> Union[ImageData, TextData, VideoData]:
175+
# Use data row information to construct the appropriate annotatin type
180176
data_row_info = {
177+
'url' if self._is_url() else 'text': self.row_data,
181178
'external_id': self.external_id,
182179
'uid': self.data_row_id
183180
}
184181

182+
self.media_type = self.media_type or self._infer_media_type()
183+
media_mapping = {
184+
'text': TextData,
185+
'image': ImageData,
186+
'video': VideoData
187+
}
188+
if self.media_type not in media_mapping:
189+
raise ValueError(
190+
f"Annotation types are only supported for {list(media_mapping)} media types."
191+
f" Found {self.media_type}.")
192+
return media_mapping[self.media_type](**data_row_info)
193+
194+
def _infer_media_type(self) -> str:
195+
# Determines the data row type based on the label content
196+
if isinstance(self.label, list):
197+
return 'video'
185198
if self._has_text_annotations():
186-
# If it has text annotations then it must be text
187-
if self._is_url():
188-
return TextData(url=self.row_data, **data_row_info)
189-
else:
190-
return TextData(text=self.row_data, **data_row_info)
199+
return 'text'
191200
elif self._has_object_annotations():
192-
# If it has object annotations and none are text annotations then it must be an image
193-
if self._is_url():
194-
return ImageData(url=self.row_data, **data_row_info)
195-
else:
196-
return ImageData(text=self.row_data, **data_row_info)
201+
return 'image'
197202
else:
198-
# no annotations to infer data type from.
199-
# Use information from the row_data format if possible.
200203
if self._row_contains((".jpg", ".png", ".jpeg")) and self._is_url():
201-
return ImageData(url=self.row_data, **data_row_info)
202-
elif self._row_contains(
203-
(".txt", ".text", ".html")) and self._is_url():
204-
return TextData(url=self.row_data, **data_row_info)
205-
elif not self._is_url():
206-
return TextData(text=self.row_data, **data_row_info)
204+
return 'image'
205+
elif (self._row_contains((".txt", ".text", ".html")) and
206+
self._is_url()) or not self._is_url():
207+
return 'text'
207208
else:
208-
# This is going to be urls that do not contain any file extensions
209-
# This will only occur on skipped images.
210-
# To use this converter on data with this url format
211-
# filter out empty examples from the payload before deserializing.
209+
# This condition will occur when a data row url does not contain a file extension
210+
# and the label does not contain object annotations that indicate the media type.
211+
# As a temporary workaround you can explicitly set the media_type
212+
# in each label json payload before converting.
213+
# We will eventually provide the media type in the export.
212214
raise TypeError(
213-
"Can't infer data type from row data. Remove empty examples before trying again. "
214-
f"row_data: {self.row_data[:200]}")
215+
f"Can't infer data type from row data. row_data: {self.row_data[:200]}"
216+
)
215217

216218
def _has_object_annotations(self):
217219
return len(self.label.objects) > 0

labelbox/data/serialization/labelbox_v1/objects.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,18 @@ def dict(self, *args, **kwargs):
2727

2828
@validator('classifications', pre=True)
2929
def validate_subclasses(cls, value, field):
30-
# Dropdown subclasses create extra unessesary nesting. So we just remove it.
30+
# checklist subclasses create extra unessesary nesting. So we just remove it.
3131
if isinstance(value, list) and len(value):
32-
if isinstance(value[0], list):
33-
return value[0]
32+
subclasses = []
33+
for v in value:
34+
# this is due to Checklists providing extra brackets []. We grab every item
35+
# in the brackets if this is the case
36+
if isinstance(v, list):
37+
for inner_v in v:
38+
subclasses.append(inner_v)
39+
else:
40+
subclasses.append(v)
41+
return subclasses
3442
return value
3543

3644

0 commit comments

Comments
 (0)