Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
558e8a4
adding vscode to the gitignore so they are not uploaded
jtsodapop Nov 9, 2021
c41a144
update to make the ability to separate between label and value and up…
jtsodapop Nov 10, 2021
016898c
yapf update
jtsodapop Nov 10, 2021
5f10b66
nit
jtsodapop Nov 10, 2021
5141dd1
Introduce progress
tomislav-peharda Nov 11, 2021
e144220
Add progress bar
tomislav-peharda Nov 11, 2021
f49f4e4
Upd progress
tomislav-peharda Nov 11, 2021
28f9d28
Formatting
tomislav-peharda Nov 11, 2021
82f87da
Additional formatting
tomislav-peharda Nov 11, 2021
bcfd9d6
Formatting
tomislav-peharda Nov 11, 2021
eab0226
Formatgit add labelbox/schema/annotation_import.py --no-verify
tomislav-peharda Nov 11, 2021
f3836b9
Upd
tomislav-peharda Nov 12, 2021
b3a266d
Upd workflow
tomislav-peharda Nov 12, 2021
53c5acf
Revert
tomislav-peharda Nov 12, 2021
ec56f2d
Add type
tomislav-peharda Nov 12, 2021
8ecadd8
Add aditional space
tomislav-peharda Nov 12, 2021
64b5263
fix applied
jtsodapop Nov 12, 2021
bc969e7
update
jtsodapop Nov 12, 2021
4a89ead
update
jtsodapop Nov 12, 2021
88b59f8
Merge branch 'tpeharda/DIAG-904-import-prediction-progress' of https:…
Nov 12, 2021
d24246a
Update CHANGELOG.md
msokoloff1 Nov 15, 2021
b6a6bc3
Merge pull request #344 from Labelbox/changelog-update
msokoloff1 Nov 16, 2021
543245d
support keyframes in videos
Nov 16, 2021
4cac910
update to test to notify us in the future we need to look into correc…
jtsodapop Nov 16, 2021
a9d5711
Merge pull request #341 from Labelbox/DIAG-983-fix
jtsodapop Nov 16, 2021
580b9d6
Merge branch 'develop' of https://github.com/Labelbox/labelbox-python…
Nov 16, 2021
5f0d421
fix bug
Nov 16, 2021
d7e96ae
Merge pull request #336 from Labelbox/update_gitignore
msokoloff1 Nov 17, 2021
a3fa5af
add support for media types in label exports
Nov 17, 2021
e8649d9
add media type
Nov 17, 2021
d47e638
Merge pull request #337 from Labelbox/DIAG-982
msokoloff1 Nov 17, 2021
484d7b7
Merge branch 'develop' of https://github.com/Labelbox/labelbox-python…
Nov 17, 2021
251fbd4
Merge branch 'develop' into tpeharda/DIAG-904-import-prediction-progress
tomislav-peharda Nov 18, 2021
fbbd365
update progress bar
Nov 18, 2021
ca25681
Merge branch 'tpeharda/DIAG-904-import-prediction-progress' of https:…
Nov 18, 2021
b814162
Merge pull request #345 from Labelbox/ms/video-classification-keyframe
msokoloff1 Nov 18, 2021
2ea6e29
Merge branch 'develop' of https://github.com/Labelbox/labelbox-python…
Nov 18, 2021
aea83ef
format
Nov 18, 2021
8d0487d
mypy
Nov 18, 2021
1a3826c
Merge branch 'develop' of https://github.com/Labelbox/labelbox-python…
Nov 18, 2021
1336a9b
prep for release
Nov 18, 2021
e7e7ea7
format
Nov 18, 2021
f630f37
Merge pull request #339 from Labelbox/tpeharda/DIAG-904-import-predic…
msokoloff1 Nov 18, 2021
7f8a3be
Merge branch 'develop' of https://github.com/Labelbox/labelbox-python…
Nov 18, 2021
4071dc2
Merge pull request #346 from Labelbox/ms/3.10.0-dev
msokoloff1 Nov 18, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ ipython_config.py
# pyenv
.python-version

# vscode
.vscode

# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
Expand Down
25 changes: 20 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,25 @@
## Deprecation Notice
| Name | Replacement | Removed After |
| ------------------------------------- | ------------------------------------- | ------------- |
| `ModelRun.delete_annotation_groups()` | `ModelRun.delete_model_run_data_rows()`| 3.9 |
| `ModelRun.annotation_groups()` | `ModelRun.model_run_data_rows()` | 3.9 |
| `DataRowMetadataSchema.id` | `DataRowMetadataSchema.uid` | 3.9 |
| `ModelRun.delete_annotation_groups()` | `ModelRun.delete_model_run_data_rows()`| 2021-12-06 |
| `ModelRun.annotation_groups()` | `ModelRun.model_run_data_rows()` | 2021-12-06 |
| `DataRowMetadataSchema.id` | `DataRowMetadataSchema.uid` | 2021-12-06 |
-----

# Version 3.10.0 (2021-11-18)
## Added
* `AnnotationImport.wait_until_done()` accepts a `show_progress` param. This is set to `False` by default.
* If enabled, a tqdm progress bar will indicate the import progress.
* This works for all classes that inherit from AnnotationImport: `LabelImport`, `MALPredictionImport`, `MEAPredictionImport`
* This is not support for `BulkImportRequest` (which will eventually be replaced by `MALPredictionImport`)
* `Option.label` and `Option.value` can now be set independently
* `ClassificationAnswer`s now support a new `keyframe` field for videos
* New `LBV1Label.media_type field. This is a placeholder for future backend changes.

## Fix
* Nested checklists can have extra brackets. This would cause the annotation type converter to break.


# Version 3.9.0 (2021-11-12)
## Added
* New ontology management features
Expand All @@ -18,8 +33,8 @@
* Set up a project from an existing ontology with `project.setup_edior()`
* Added new `FeatureSchema` entity
* Add support for new queue modes
* Send batches of data direction to a project with `project.queue()`
* Remove items from the queue with `project.dequeue()`
* Send batches of data directly to a project queue with `project.queue()`
* Remove items from a project queue with `project.dequeue()`
* Query for and toggle the queue mode

# Version 3.8.0 (2021-10-22)
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ The package `rasterio` installed by `labelbox[data]` relies on GDAL which could
You may see the following error message:

```
INFO:root:Building on Windows requires extra options to setup.py to locate needed GDAL files. More information is available in the README.
INFO:root:Building on Windows requires extra options to setup.py to locate needed GDAL files. More information is available in the README.

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.
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.
```

As a workaround:
Expand All @@ -72,7 +72,7 @@ As a workaround:

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

2. After downloading the files, please run the following commands, in this particular order.
2. After downloading the files, please run the following commands, in this particular order.

```
pip install GDAL‑3.3.2‑cp38‑cp38‑win_amd64.wh
Expand Down
2 changes: 1 addition & 1 deletion labelbox/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name = "labelbox"
__version__ = "3.9.0"
__version__ = "3.10.0"

from labelbox.schema.project import Project
from labelbox.client import Client
Expand Down
18 changes: 15 additions & 3 deletions labelbox/data/annotation_types/classification/classification.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Dict, List
from typing import Any, Dict, List, Union, Optional

try:
from typing import Literal
Expand All @@ -24,13 +24,25 @@ class ClassificationAnswer(FeatureSchema):
- Represents a classification option.
- Because it inherits from FeatureSchema
the option can be represented with either the name or feature_schema_id

- The keyframe arg only applies to video classifications.
Each answer can have a keyframe independent of the others.
So unlike object annotations, classification annotations
track keyframes at a classification answer level.
"""
extra: Dict[str, Any] = {}
keyframe: Optional[bool] = None

def dict(self, *args, **kwargs):
res = super().dict(*args, **kwargs)
if res['keyframe'] is None:
res.pop('keyframe')
return res


class Radio(BaseModel):
""" A classification with only one selected option allowed

>>> Radio(answer = ClassificationAnswer(name = "dog"))

"""
Expand All @@ -50,7 +62,7 @@ class Checklist(_TempName):
class Text(BaseModel):
""" Free form text

>>> Text(answer = "some text answer")
>>> Text(answer = "some text answer")

"""
answer: str
Expand Down
64 changes: 25 additions & 39 deletions labelbox/data/serialization/labelbox_v1/classification.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,56 +9,53 @@


class LBV1ClassificationAnswer(LBV1Feature):
...

def to_common(self) -> ClassificationAnswer:
return ClassificationAnswer(feature_schema_id=self.schema_id,
name=self.title,
keyframe=self.keyframe,
extra={
'feature_id': self.feature_id,
'value': self.value
})

@classmethod
def from_common(
cls,
answer: ClassificationAnnotation) -> "LBV1ClassificationAnswer":
return cls(schema_id=answer.feature_schema_id,
title=answer.name,
value=answer.extra.get('value'),
feature_id=answer.extra.get('feature_id'),
keyframe=answer.keyframe)


class LBV1Radio(LBV1Feature):
answer: LBV1ClassificationAnswer

def to_common(self) -> Radio:
return Radio(answer=ClassificationAnswer(
feature_schema_id=self.answer.schema_id,
name=self.answer.title,
extra={
'feature_id': self.answer.feature_id,
'value': self.answer.value
}))
return Radio(answer=self.answer.to_common())

@classmethod
def from_common(cls, radio: Radio, feature_schema_id: Cuid,
**extra) -> "LBV1Radio":
return cls(schema_id=feature_schema_id,
answer=LBV1ClassificationAnswer(
schema_id=radio.answer.feature_schema_id,
title=radio.answer.name,
value=radio.answer.extra.get('value'),
feature_id=radio.answer.extra.get('feature_id')),
answer=LBV1ClassificationAnswer.from_common(radio.answer),
**extra)


class LBV1Checklist(LBV1Feature):
answers: List[LBV1ClassificationAnswer]

def to_common(self) -> Checklist:
return Checklist(answer=[
ClassificationAnswer(feature_schema_id=answer.schema_id,
name=answer.title,
extra={
'feature_id': answer.feature_id,
'value': answer.value
}) for answer in self.answers
])
return Checklist(answer=[answer.to_common() for answer in self.answers])

@classmethod
def from_common(cls, checklist: Checklist, feature_schema_id: Cuid,
**extra) -> "LBV1Checklist":
return cls(schema_id=feature_schema_id,
answers=[
LBV1ClassificationAnswer(
schema_id=answer.feature_schema_id,
title=answer.name,
value=answer.extra.get('value'),
feature_id=answer.extra.get('feature_id'))
LBV1ClassificationAnswer.from_common(answer)
for answer in checklist.answer
],
**extra)
Expand All @@ -68,25 +65,14 @@ class LBV1Dropdown(LBV1Feature):
answer: List[LBV1ClassificationAnswer]

def to_common(self) -> Dropdown:
return Dropdown(answer=[
ClassificationAnswer(feature_schema_id=answer.schema_id,
name=answer.title,
extra={
'feature_id': answer.feature_id,
'value': answer.value
}) for answer in self.answer
])
return Dropdown(answer=[answer.to_common() for answer in self.answer])

@classmethod
def from_common(cls, dropdown: Dropdown, feature_schema_id: Cuid,
**extra) -> "LBV1Dropdown":
return cls(schema_id=feature_schema_id,
answer=[
LBV1ClassificationAnswer(
schema_id=answer.feature_schema_id,
title=answer.name,
value=answer.extra.get('value'),
feature_id=answer.extra.get('feature_id'))
LBV1ClassificationAnswer.from_common(answer)
for answer in dropdown.answer
],
**extra)
Expand Down
66 changes: 34 additions & 32 deletions labelbox/data/serialization/labelbox_v1/label.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,20 +137,17 @@ class LBV1Label(BaseModel):
label_url: Optional[str] = Extra('View Label')
has_open_issues: Optional[float] = Extra('Has Open Issues')
skipped: Optional[bool] = Extra('Skipped')
media_type: Optional[str] = Extra('media_type')

def to_common(self) -> Label:
if isinstance(self.label, list):
annotations = []
for lbl in self.label:
annotations.extend(lbl.to_common())
data = VideoData(url=self.row_data,
external_id=self.external_id,
uid=self.data_row_id)
else:
annotations = self.label.to_common()
data = self._infer_media_type()

return Label(data=data,
return Label(data=self._data_row_to_common(),
uid=self.id,
annotations=annotations,
extra={
Expand All @@ -174,44 +171,49 @@ def from_common(cls, label: Label):
external_id=label.data.external_id,
**label.extra)

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

self.media_type = self.media_type or self._infer_media_type()
media_mapping = {
'text': TextData,
'image': ImageData,
'video': VideoData
}
if self.media_type not in media_mapping:
raise ValueError(
f"Annotation types are only supported for {list(media_mapping)} media types."
f" Found {self.media_type}.")
return media_mapping[self.media_type](**data_row_info)

def _infer_media_type(self) -> str:
# Determines the data row type based on the label content
if isinstance(self.label, list):
return 'video'
if self._has_text_annotations():
# If it has text annotations then it must be text
if self._is_url():
return TextData(url=self.row_data, **data_row_info)
else:
return TextData(text=self.row_data, **data_row_info)
return 'text'
elif self._has_object_annotations():
# If it has object annotations and none are text annotations then it must be an image
if self._is_url():
return ImageData(url=self.row_data, **data_row_info)
else:
return ImageData(text=self.row_data, **data_row_info)
return 'image'
else:
# no annotations to infer data type from.
# Use information from the row_data format if possible.
if self._row_contains((".jpg", ".png", ".jpeg")) and self._is_url():
return ImageData(url=self.row_data, **data_row_info)
elif self._row_contains(
(".txt", ".text", ".html")) and self._is_url():
return TextData(url=self.row_data, **data_row_info)
elif not self._is_url():
return TextData(text=self.row_data, **data_row_info)
return 'image'
elif (self._row_contains((".txt", ".text", ".html")) and
self._is_url()) or not self._is_url():
return 'text'
else:
# This is going to be urls that do not contain any file extensions
# This will only occur on skipped images.
# To use this converter on data with this url format
# filter out empty examples from the payload before deserializing.
# This condition will occur when a data row url does not contain a file extension
# and the label does not contain object annotations that indicate the media type.
# As a temporary workaround you can explicitly set the media_type
# in each label json payload before converting.
# We will eventually provide the media type in the export.
raise TypeError(
"Can't infer data type from row data. Remove empty examples before trying again. "
f"row_data: {self.row_data[:200]}")
f"Can't infer data type from row data. row_data: {self.row_data[:200]}"
)

def _has_object_annotations(self):
return len(self.label.objects) > 0
Expand Down
14 changes: 11 additions & 3 deletions labelbox/data/serialization/labelbox_v1/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,18 @@ def dict(self, *args, **kwargs):

@validator('classifications', pre=True)
def validate_subclasses(cls, value, field):
# Dropdown subclasses create extra unessesary nesting. So we just remove it.
# checklist subclasses create extra unessesary nesting. So we just remove it.
if isinstance(value, list) and len(value):
if isinstance(value[0], list):
return value[0]
subclasses = []
for v in value:
# this is due to Checklists providing extra brackets []. We grab every item
# in the brackets if this is the case
if isinstance(v, list):
for inner_v in v:
subclasses.append(inner_v)
else:
subclasses.append(v)
return subclasses
return value


Expand Down
Loading