Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
7c2caf0
AL-3578: Confidence for image types added
dubininsergey Nov 14, 2022
263d02a
AL-3578: Classifications confidence
dubininsergey Nov 14, 2022
846d512
AL-3578: Pdf confidence
dubininsergey Nov 15, 2022
56347c7
AL-3578: Video annotation test case
dubininsergey Nov 15, 2022
a646fd3
AL-3578: Added video test case
dubininsergey Nov 15, 2022
41c8583
AL-3578: Adjusted image tests
dubininsergey Nov 15, 2022
647760a
AL-3578: Added nested test cases
dubininsergey Nov 15, 2022
5599dea
AL-3578: Added text test case
dubininsergey Nov 15, 2022
3eebb61
AL-3578: Added range validation
dubininsergey Nov 15, 2022
2c70cd6
AL-3578: Added cases for text classifications
dubininsergey Nov 15, 2022
86c505e
AL-3567: Assignment test case
dubininsergey Nov 15, 2022
33a6c8f
AL-3578: Added MAL and LI warning message
dubininsergey Nov 15, 2022
49f884a
AL-3578: Fixed message
dubininsergey Nov 15, 2022
ec2e47a
AL-3578: Fixed typos
dubininsergey Nov 15, 2022
f093e6f
Merge branch 'develop' into sdubinin/al-3578
dubininsergey Nov 15, 2022
aa0c5fe
AL-3578: Format
dubininsergey Nov 15, 2022
8e62e62
AL-3578: Fixed mypy issues
dubininsergey Nov 16, 2022
d714e52
AL-3578: Fixed import
dubininsergey Nov 16, 2022
fe6407b
AL-3578: Yapf formatting
dubininsergey Nov 17, 2022
29942a2
AL-3578: Use explicit types
dubininsergey Nov 17, 2022
4d8fefe
AL-3578: Format again lol
dubininsergey Nov 17, 2022
026a222
AL-3578: Removed import
dubininsergey Nov 17, 2022
1722660
AL-3578: Renamed test file
dubininsergey Nov 17, 2022
da01e92
AL-3578: Fixed test
dubininsergey Nov 17, 2022
aa99894
AL-3578: Fixed attr access
dubininsergey Nov 17, 2022
b77826e
Fix broken pdf mal notebook links
Nov 18, 2022
8e2fe8d
Revert some unintended metadata changes
Nov 18, 2022
cad8224
AL-3578: Removed invalid fixtures from test
dubininsergey Nov 18, 2022
ef04154
Merge pull request #771 from Labelbox/kkim/update-pdf-notebook
kkim-labelbox Nov 18, 2022
a2c167e
update tests
Nov 18, 2022
bf824b8
Merge pull request #772 from Labelbox/ms/update-tests
msokoloff1 Nov 18, 2022
6a0e9e3
AL-3578: Simplify confidence presence checker
dubininsergey Nov 22, 2022
f35d929
AL-3578: Fixed types
dubininsergey Nov 22, 2022
f05d09e
AL-3578: Format
dubininsergey Nov 22, 2022
1b45ef7
Merge pull request #761 from Labelbox/sdubinin/al-3578
dubininsergey Nov 22, 2022
b0d1f1b
Clear_global_keys
Nov 18, 2022
621e24a
removing accidentally commited files
Nov 18, 2022
b17fb5b
fixed docstring and filtering
Nov 21, 2022
9634b7b
formatting
Nov 22, 2022
ae9c990
Merge pull request #773 from Labelbox/AL-0/otrygub/clear_global_keys
kkim-labelbox Nov 23, 2022
972db8e
Prep 3.30.2
Nov 28, 2022
0ab3743
[X-0] Fix create batch with data row classes
mkozik-lb Nov 28, 2022
d11ee6e
Adjust create_batch signature
mkozik-lb Nov 28, 2022
01dabd4
Update types
mkozik-lb Nov 28, 2022
19d076e
Update changelog to include fix to create_batch
Nov 28, 2022
e153e0f
Update types
mkozik-lb Nov 28, 2022
0063709
Merge pull request #777 from Labelbox/mkozik/create-batch-with-data-rows
mkozik-lb Nov 28, 2022
352d161
Merge pull request #776 from Labelbox/kkim/prep_3.30.2
kkim-labelbox Nov 28, 2022
7380188
Update 3.30.2 to 3.31.0 with more in changelogs
Nov 28, 2022
9682825
Merge pull request #780 from Labelbox/kkim/update-changelog
kkim-labelbox Nov 28, 2022
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
# Changelog

# Version 3.31.0 (2022-11-28)
### Added
* Added `client.clear_global_keys()` to remove global keys from their associated data rows
* Added a new attribute `confidence` to `AnnotationObject` and `ClassificationAnswer` for Model Error Analysis

### Fixed
* Fixed `project.create_batch()` to work with both data_row_ids and data_row objects

# Version 3.30.1 (2022-11-16)
### Added
* Added step to `project.create_batch()` to wait for data rows to finish processing
### Fixed
* Running `project.setup_editor()` multiple times no longer resets the ontology, and instead raises an error if the editor is already set up for the project

Expand Down
2 changes: 1 addition & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
copyright = '2021, Labelbox'
author = 'Labelbox'

release = '3.30.1'
release = '3.31.0'

# -- General configuration ---------------------------------------------------

Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Learn more about annotation types in the [docs](https://docs.labelbox.com/docs/a
| Text Annotation Import | [Github](annotation_import/text.ipynb) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Labelbox/labelbox-python/blob/develop/examples/annotation_import/text.ipynb) |
| Tiled Imagery Annotation Import | [Github](annotation_import/tiled.ipynb) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Labelbox/labelbox-python/blob/develop/examples/annotation_import/tiled.ipynb) |
| Video Model-Assisted Labeling | [Github](annotation_import/video.ipynb) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Labelbox/labelbox-python/blob/develop/examples/annotation_import/video.ipynb) |
| PDF Annotation Import | [Github](annotation_import/pdf.ipynb) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Labelbox/labelbox-python/blob/develop/examples/annotation_import/pdf.ipynb) |
------

## [Project Configuration](project_configuration)
Expand Down
12 changes: 2 additions & 10 deletions examples/annotation_import/pdf.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@
"metadata": {},
"source": [
"<td>\n",
"<a href=\"https://colab.research.google.com/github/Labelbox/labelbox-python/blob/develop/examples/annotation_import/pdf_mal.ipynb\" target=\"_blank\"><img\n",
"<a href=\"https://colab.research.google.com/github/Labelbox/labelbox-python/blob/develop/examples/annotation_import/pdf.ipynb\" target=\"_blank\"><img\n",
"src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"></a>\n",
"</td>\n",
"\n",
"<td>\n",
"<a href=\"https://github.com/Labelbox/labelbox-python/tree/develop/examples/annotation_import/pdf_mal.ipynb\" target=\"_blank\"><img\n",
"<a href=\"https://github.com/Labelbox/labelbox-python/tree/develop/examples/annotation_import/pdf.ipynb\" target=\"_blank\"><img\n",
"src=\"https://img.shields.io/badge/GitHub-100000?logo=github&logoColor=white\" alt=\"GitHub\"></a>\n",
"</td>"
]
Expand Down Expand Up @@ -447,14 +447,6 @@
"# This will provide information only after the upload_job is complete, so we do not need to worry about having to rerun\n",
"print(\"Errors:\", upload_job.errors)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "ba9dc45a",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
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.30.1"
__version__ = "3.31.0"

from labelbox.client import Client
from labelbox.schema.project import Project
Expand Down
96 changes: 96 additions & 0 deletions labelbox/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1229,6 +1229,102 @@ def _format_failed_rows(rows: List[str],
)
time.sleep(sleep_time)

def clear_global_keys(
self,
global_keys: List[str],
timeout_seconds=60) -> Dict[str, Union[str, List[Any]]]:
"""
Clears global keys for the data rows tha correspond to the global keys provided.

Args:
A list of global keys
Returns:
Dictionary containing 'status', 'results' and 'errors'.

'Status' contains the outcome of this job. It can be one of
'Success', 'Partial Success', or 'Failure'.

'Results' contains a list global keys that were successfully cleared.

'Errors' contains a list of global_keys correspond to the data rows that could not be
modified, accessed by the user, or not found.
Examples:
>>> job_result = client.get_data_row_ids_for_global_keys(["key1","key2"])
>>> print(job_result['status'])
Partial Success
>>> print(job_result['results'])
['cl7tv9wry00hlka6gai588ozv', 'cl7tv9wxg00hpka6gf8sh81bj']
>>> print(job_result['errors'])
[{'global_key': 'asdf', 'error': 'Data Row not found'}]
"""

def _format_failed_rows(rows: List[str],
error_msg: str) -> List[Dict[str, str]]:
return [{'global_key': r, 'error': error_msg} for r in rows]

# Start get data rows for global keys job
query_str = """mutation clearGlobalKeysPyApi($globalKeys: [ID!]!) {
clearGlobalKeys(where: {ids: $globalKeys}) { jobId}}
"""
params = {"globalKeys": global_keys}
clear_global_keys_job = self.execute(query_str, params)

# Query string for retrieving job status and result, if job is done
result_query_str = """query clearGlobalKeysResultPyApi($jobId: ID!) {
clearGlobalKeysResult(jobId: {id: $jobId}) { data {
clearedGlobalKeys
failedToClearGlobalKeys
notFoundGlobalKeys
accessDeniedGlobalKeys
} jobStatus}}
"""
result_params = {
"jobId": clear_global_keys_job["clearGlobalKeys"]["jobId"]
}
# Poll job status until finished, then retrieve results
sleep_time = 2
start_time = time.time()
while True:
res = self.execute(result_query_str, result_params)
if res["clearGlobalKeysResult"]['jobStatus'] == "COMPLETE":
data = res["clearGlobalKeysResult"]['data']
results, errors = [], []
results.extend(data['clearedGlobalKeys'])
errors.extend(
_format_failed_rows(data['failedToClearGlobalKeys'],
"Clearing global key failed"))
errors.extend(
_format_failed_rows(
data['notFoundGlobalKeys'],
"Failed to find data row matching provided global key"))
errors.extend(
_format_failed_rows(
data['accessDeniedGlobalKeys'],
"Denied access to modify data row matching provided global key"
))

if not errors:
status = CollectionJobStatus.SUCCESS.value
elif errors and len(results) > 0:
status = CollectionJobStatus.PARTIAL_SUCCESS.value
else:
status = CollectionJobStatus.FAILURE.value

if errors:
logger.warning(
"There are errors present. Please look at 'errors' in the returned dict for more details"
)

return {"status": status, "results": results, "errors": errors}
elif res["clearGlobalKeysResult"]['jobStatus'] == "FAILED":
raise labelbox.exceptions.LabelboxError(
"Job clearGlobalKeys failed.")
current_time = time.time()
if current_time - start_time > timeout_seconds:
raise labelbox.exceptions.TimeoutError(
"Timed out waiting for clear_global_keys job to complete.")
time.sleep(sleep_time)

def get_catalog_slice(self, slice_id) -> CatalogSlice:
"""
Fetches a Catalog Slice by ID.
Expand Down
7 changes: 5 additions & 2 deletions labelbox/data/annotation_types/annotation.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import abc
from typing import Any, Dict, List, Optional, Union

from labelbox.data.mixins import ConfidenceNotSupportedMixin, ConfidenceMixin

from .classification import Checklist, Dropdown, Radio, Text
from .feature import FeatureSchema
from .geometry import Geometry, Rectangle, Point
Expand Down Expand Up @@ -31,7 +33,7 @@ class ClassificationAnnotation(BaseAnnotation):
value: Union[Text, Checklist, Radio, Dropdown]


class ObjectAnnotation(BaseAnnotation):
class ObjectAnnotation(BaseAnnotation, ConfidenceMixin):
"""Generic localized annotation (non classifications)

>>> ObjectAnnotation(
Expand All @@ -53,7 +55,7 @@ class ObjectAnnotation(BaseAnnotation):
classifications: List[ClassificationAnnotation] = []


class VideoObjectAnnotation(ObjectAnnotation):
class VideoObjectAnnotation(ObjectAnnotation, ConfidenceNotSupportedMixin):
"""Video object annotation

>>> VideoObjectAnnotation(
Expand All @@ -76,6 +78,7 @@ class VideoObjectAnnotation(ObjectAnnotation):
classifications (List[ClassificationAnnotation]) = []
extra (Dict[str, Any])
"""

frame: int
keyframe: bool
segment_index: Optional[int] = None
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from typing import Any, Dict, List, Union, Optional
import warnings

from labelbox.data.mixins import ConfidenceMixin

try:
from typing import Literal
except:
Expand All @@ -20,7 +22,7 @@ def dict(self, *args, **kwargs):
return res


class ClassificationAnswer(FeatureSchema):
class ClassificationAnswer(FeatureSchema, ConfidenceMixin):
"""
- Represents a classification option.
- Because it inherits from FeatureSchema
Expand Down
32 changes: 32 additions & 0 deletions labelbox/data/mixins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from typing import Optional

from pydantic import BaseModel, validator

from labelbox.exceptions import ConfidenceNotSupportedException


class ConfidenceMixin(BaseModel):
confidence: Optional[float] = None

@validator('confidence')
def confidence_valid_float(cls, value):
if value is None:
return value
if not isinstance(value, (int, float)) or not 0 <= value <= 1:
raise ValueError('must be a number within [0,1] range')
return value

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


class ConfidenceNotSupportedMixin:

def __new__(cls, *args, **kwargs):
if 'confidence' in kwargs:
raise ConfidenceNotSupportedException(
'Confidence is not supported for this annotaiton type yet')
return super().__new__(cls)
39 changes: 27 additions & 12 deletions labelbox/data/serialization/ndjson/classification.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Any, Dict, List, Union, Optional

from pydantic import BaseModel, Field, root_validator
from labelbox.data.mixins import ConfidenceMixin

from labelbox.utils import camel_case
from ...annotation_types.annotation import ClassificationAnnotation, VideoClassificationAnnotation
Expand All @@ -10,7 +11,7 @@
from .base import NDAnnotation


class NDFeature(BaseModel):
class NDFeature(ConfidenceMixin):
name: Optional[str] = None
schema_id: Optional[Cuid] = None

Expand Down Expand Up @@ -41,7 +42,7 @@ class FrameLocation(BaseModel):


class VideoSupported(BaseModel):
#Note that frames are only allowed as top level inferences for video
# Note that frames are only allowed as top level inferences for video
frames: Optional[List[FrameLocation]] = None

def dict(self, *args, **kwargs):
Expand Down Expand Up @@ -70,15 +71,18 @@ class NDChecklistSubclass(NDFeature):
def to_common(self) -> Checklist:
return Checklist(answer=[
ClassificationAnswer(name=answer.name,
feature_schema_id=answer.schema_id)
feature_schema_id=answer.schema_id,
confidence=answer.confidence)
for answer in self.answer
])

@classmethod
def from_common(cls, checklist: Checklist, name: str,
feature_schema_id: Cuid) -> "NDChecklistSubclass":
return cls(answer=[
NDFeature(name=answer.name, schema_id=answer.feature_schema_id)
NDFeature(name=answer.name,
schema_id=answer.feature_schema_id,
confidence=answer.confidence)
for answer in checklist.answer
],
name=name,
Expand All @@ -95,19 +99,22 @@ class NDRadioSubclass(NDFeature):
answer: NDFeature

def to_common(self) -> Radio:
return Radio(answer=ClassificationAnswer(
name=self.answer.name, feature_schema_id=self.answer.schema_id))
return Radio(
answer=ClassificationAnswer(name=self.answer.name,
feature_schema_id=self.answer.schema_id,
confidence=self.answer.confidence))

@classmethod
def from_common(cls, radio: Radio, name: str,
feature_schema_id: Cuid) -> "NDRadioSubclass":
return cls(answer=NDFeature(name=radio.answer.name,
schema_id=radio.answer.feature_schema_id),
schema_id=radio.answer.feature_schema_id,
confidence=radio.answer.confidence),
name=name,
schema_id=feature_schema_id)


### ====== End of subclasses
# ====== End of subclasses


class NDText(NDAnnotation, NDTextSubclass):
Expand All @@ -133,7 +140,9 @@ def from_common(
extra: Dict[str, Any], data: Union[VideoData, TextData,
ImageData]) -> "NDChecklist":
return cls(answer=[
NDFeature(name=answer.name, schema_id=answer.feature_schema_id)
NDFeature(name=answer.name,
schema_id=answer.feature_schema_id,
confidence=answer.confidence)
for answer in checklist.answer
],
data_row={'id': data.uid},
Expand All @@ -150,7 +159,8 @@ def from_common(cls, radio: Radio, name: str, feature_schema_id: Cuid,
extra: Dict[str, Any], data: Union[VideoData, TextData,
ImageData]) -> "NDRadio":
return cls(answer=NDFeature(name=radio.answer.name,
schema_id=radio.answer.feature_schema_id),
schema_id=radio.answer.feature_schema_id,
confidence=radio.answer.confidence),
data_row={'id': data.uid},
name=name,
schema_id=feature_schema_id,
Expand Down Expand Up @@ -241,6 +251,11 @@ def lookup_classification(
}.get(type(annotation.value))


NDSubclassificationType = Union[NDRadioSubclass, NDChecklistSubclass,
# Make sure to keep NDChecklistSubclass prior to NDRadioSubclass in the list,
# otherwise list of answers gets parsed by NDRadio whereas NDChecklist must be used
NDSubclassificationType = Union[NDChecklistSubclass, NDRadioSubclass,
NDTextSubclass]
NDClassificationType = Union[NDRadio, NDChecklist, NDText]

# Make sure to keep NDChecklist prior to NDRadio in the list,
# otherwise list of answers gets parsed by NDRadio whereas NDChecklist must be used
NDClassificationType = Union[NDChecklist, NDRadio, NDText]
Loading