Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
c42d151
WIP to objects file
jtsodapop Mar 30, 2022
c2e496d
nit
jtsodapop Apr 8, 2022
27bf3a9
update to code
jtsodapop Apr 8, 2022
6761544
Merge branch 'develop' into jt/al-1921
jtsodapop Apr 11, 2022
60ab2a0
from_common to go to importable ndjson format
jtsodapop Apr 11, 2022
b3bf569
ability to run to_common to convert back to common labelbox format
jtsodapop Apr 11, 2022
bb9ac32
cleanup of code
jtsodapop Apr 11, 2022
b8439c5
update to test_project. still leaving as skip as there is an issue wi…
jtsodapop Apr 14, 2022
344aaa6
nit update
jtsodapop Apr 14, 2022
f63cf13
addition of raster segmentation. name of the tool is subject to change
jtsodapop Apr 12, 2022
5e3e57f
v.3.19.0
jtsodapop Apr 12, 2022
4e74128
nit update to the comment
jtsodapop Apr 14, 2022
7649763
small update to remove unnecessary libs
jtsodapop Apr 14, 2022
79a74f9
test can now pass and also delete tags
jtsodapop Apr 14, 2022
ef3cadb
update hint
jtsodapop Apr 18, 2022
26a0bc0
update hint
jtsodapop Apr 18, 2022
d18fc16
merge conflicts
jtsodapop Apr 18, 2022
4216a1b
update to onprem test case input from instance id to hostname
jtsodapop Apr 19, 2022
6358d0c
push
jtsodapop Apr 19, 2022
3af9b59
Merge pull request #545 from Labelbox/jt/al-1777
jtsodapop Apr 19, 2022
919da4d
addition of test case
jtsodapop Apr 20, 2022
9e5e1ab
update to test to test each section from manual case
jtsodapop Apr 20, 2022
32fff0c
deprecation message for 3.6
jtsodapop Apr 20, 2022
3c11599
update to test batch to not double delete dataset
jtsodapop Apr 21, 2022
062afe4
another big dataset
jtsodapop Apr 21, 2022
a237ffe
Merge pull request #550 from Labelbox/jt/al-2196
hydak Apr 21, 2022
4f46a72
Merge branch 'develop' into jt/al-2173
jtsodapop Apr 21, 2022
9c23fb2
nit
jtsodapop Apr 21, 2022
e5407e1
Merge branch 'develop' into jt/al-1921
jtsodapop Apr 21, 2022
edf3380
update to test files
jtsodapop Apr 21, 2022
9807f94
Merge branch 'develop' into jt/al-2193
jtsodapop Apr 21, 2022
c32c704
nit
jtsodapop Apr 21, 2022
eb721ac
Merge pull request #549 from Labelbox/jt/al-2193
hydak Apr 21, 2022
96653ff
Merge pull request #548 from Labelbox/jt/al-2173
hydak Apr 21, 2022
55b96ba
Merge pull request #540 from Labelbox/jt/al-1921
hydak Apr 21, 2022
eb7cff0
[AL-2075] Batch list and export
farkob Apr 22, 2022
7dee8ef
format
farkob Apr 22, 2022
658c618
remove archived_at
farkob Apr 22, 2022
a61e64d
add archive, batch.project, changelog
farkob Apr 27, 2022
83bfc53
types
farkob Apr 27, 2022
2eca4cb
types
farkob Apr 27, 2022
398d16e
add project type
farkob Apr 27, 2022
2f62897
fix order
farkob Apr 27, 2022
2ca4d86
ignore type
farkob Apr 27, 2022
3416746
add project.batches() test
farkob Apr 27, 2022
29f7ca0
update test order
farkob Apr 27, 2022
fcd6af3
add changes
farkob Apr 27, 2022
386e163
updated
farkob Apr 27, 2022
5eff529
Merge pull request #551 from Labelbox/farkob/batch-features
farkob Apr 27, 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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Changelog

# Version 3.20.0 (2022-04-27)
## Added
* Batches in a project can be retrieved with `project.batches()`
* Added `Batch.remove_queued_data_rows()` to cancel remaining data rows in batch
* Added `Batch.export_data_rows()` which returns `DataRow`s for a batch

## Updated
* NDJsonConverter now supports Video bounding box annotations.
* Note: Currently does not support nested classifications.
* Note: Converting an export into Labelbox annotation types, and back to export will result in only keyframe annotations. This is to support correct import format.


## Fix
* `batch.project()` now works

# Version 3.19.1 (2022-04-14)
## Fix
* `create_data_rows` and `create_data_rows_sync` now uploads the file with a mimetype
Expand Down
9 changes: 8 additions & 1 deletion labelbox/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
name = "labelbox"
__version__ = "3.19.1"
__version__ = "3.20.0"

import sys
import warnings

if sys.version_info < (3, 7):
warnings.warn("""Python 3.6 will no longer be actively supported
starting 06/01/2022. Please upgrade to Python 3.7 or higher.""")

from labelbox.client import Client
from labelbox.schema.project import Project
Expand Down
62 changes: 44 additions & 18 deletions labelbox/data/serialization/ndjson/label.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
from operator import itemgetter
from typing import Dict, Generator, List, Tuple, Union
from collections import defaultdict
import warnings

from pydantic import BaseModel

from ...annotation_types.annotation import ClassificationAnnotation, ObjectAnnotation, VideoClassificationAnnotation
from ...annotation_types.annotation import ClassificationAnnotation, ObjectAnnotation, VideoClassificationAnnotation, VideoObjectAnnotation
from ...annotation_types.collection import LabelCollection, LabelGenerator
from ...annotation_types.data import ImageData, TextData, VideoData
from ...annotation_types.label import Label
Expand All @@ -15,12 +16,13 @@

from .metric import NDScalarMetric, NDMetricAnnotation, NDConfusionMatrixMetric
from .classification import NDChecklistSubclass, NDClassification, NDClassificationType, NDRadioSubclass
from .objects import NDObject, NDObjectType
from .objects import NDObject, NDObjectType, NDSegments


class NDLabel(BaseModel):
annotations: List[Union[NDObjectType, NDClassificationType,
NDConfusionMatrixMetric, NDScalarMetric]]
NDConfusionMatrixMetric, NDScalarMetric,
NDSegments]]

def to_common(self) -> LabelGenerator:
grouped_annotations = defaultdict(list)
Expand All @@ -37,15 +39,20 @@ def from_common(cls,
yield from cls._create_video_annotations(label)

def _generate_annotations(
self, grouped_annotations: Dict[str, List[Union[NDObjectType,
NDClassificationType,
NDConfusionMatrixMetric,
NDScalarMetric]]]
self,
grouped_annotations: Dict[str,
List[Union[NDObjectType, NDClassificationType,
NDConfusionMatrixMetric,
NDScalarMetric, NDSegments]]]
) -> Generator[Label, None, None]:
for data_row_id, annotations in grouped_annotations.items():
annots = []
for annotation in annotations:
if isinstance(annotation, NDObjectType.__args__):
if isinstance(annotation, NDSegments):
annots.extend(
NDSegments.to_common(annotation, annotation.schema_id))

elif isinstance(annotation, NDObjectType.__args__):
annots.append(NDObject.to_common(annotation))
elif isinstance(annotation, NDClassificationType.__args__):
annots.extend(NDClassification.to_common(annotation))
Expand All @@ -55,7 +62,6 @@ def _generate_annotations(
else:
raise TypeError(
f"Unsupported annotation. {type(annotation)}")

data = self._infer_media_type(annotations)(uid=data_row_id)
yield Label(annotations=annots, data=data)

Expand All @@ -65,7 +71,7 @@ def _infer_media_type(
types = {type(annotation) for annotation in annotations}
if TextEntity in types:
return TextData
elif VideoClassificationAnnotation in types:
elif VideoClassificationAnnotation in types or VideoObjectAnnotation in types:
return VideoData
else:
return ImageData
Expand All @@ -83,26 +89,46 @@ def _get_consecutive_frames(
def _create_video_annotations(
cls, label: Label
) -> Generator[Union[NDChecklistSubclass, NDRadioSubclass], None, None]:

video_annotations = defaultdict(list)
for annot in label.annotations:
if isinstance(annot, VideoClassificationAnnotation):
if isinstance(
annot,
(VideoClassificationAnnotation, VideoObjectAnnotation)):
video_annotations[annot.feature_schema_id].append(annot)

for annotation_group in video_annotations.values():
consecutive_frames = cls._get_consecutive_frames(
sorted([annotation.frame for annotation in annotation_group]))
annotation = annotation_group[0]
frames_data = []
for frames in consecutive_frames:
frames_data.append({'start': frames[0], 'end': frames[-1]})
annotation.extra.update({'frames': frames_data})
yield NDClassification.from_common(annotation, label.data)

if isinstance(annotation_group[0], VideoClassificationAnnotation):
annotation = annotation_group[0]
frames_data = []
for frames in consecutive_frames:
frames_data.append({'start': frames[0], 'end': frames[-1]})
annotation.extra.update({'frames': frames_data})
yield NDClassification.from_common(annotation, label.data)

elif isinstance(annotation_group[0], VideoObjectAnnotation):
warnings.warn(
"""Nested classifications are not currently supported
for video object annotations
and will not import alongside the object annotations.""")
segments = []
for start_frame, end_frame in consecutive_frames:
segment = []
for annotation in annotation_group:
if annotation.keyframe and start_frame <= annotation.frame <= end_frame:
segment.append(annotation)
segments.append(segment)
yield NDObject.from_common(segments, label.data)

@classmethod
def _create_non_video_annotations(cls, label: Label):
non_video_annotations = [
annot for annot in label.annotations
if not isinstance(annot, VideoClassificationAnnotation)
if not isinstance(annot, (VideoClassificationAnnotation,
VideoObjectAnnotation))
]
for annotation in non_video_annotations:
if isinstance(annotation, ClassificationAnnotation):
Expand Down
115 changes: 104 additions & 11 deletions labelbox/data/serialization/ndjson/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
from pydantic import BaseModel
from PIL import Image

from labelbox.data.annotation_types.data.video import VideoData

from ...annotation_types.data import ImageData, TextData, MaskData
from ...annotation_types.ner import TextEntity
from ...annotation_types.types import Cuid
from ...annotation_types.geometry import Rectangle, Polygon, Line, Point, Mask
from ...annotation_types.annotation import ClassificationAnnotation, ObjectAnnotation
from ...annotation_types.annotation import ClassificationAnnotation, ObjectAnnotation, VideoObjectAnnotation
from .classification import NDSubclassification, NDSubclassificationType
from .base import DataRow, NDAnnotation

Expand All @@ -20,6 +22,11 @@ class NDBaseObject(NDAnnotation):
classifications: List[NDSubclassificationType] = []


class VideoSupported(BaseModel):
#support for video for objects are per-frame basis
frame: int


class _Point(BaseModel):
x: float
y: float
Expand Down Expand Up @@ -118,6 +125,75 @@ def from_common(cls, rectangle: Rectangle,
classifications=classifications)


class NDFrameRectangle(VideoSupported):
bbox: Bbox

def to_common(self, feature_schema_id: Cuid) -> VideoObjectAnnotation:
return VideoObjectAnnotation(
frame=self.frame,
keyframe=True,
feature_schema_id=feature_schema_id,
value=Rectangle(start=Point(x=self.bbox.left, y=self.bbox.top),
end=Point(x=self.bbox.left + self.bbox.width,
y=self.bbox.top + self.bbox.height)))

@classmethod
def from_common(cls, frame: int, rectangle: Rectangle):
return cls(frame=frame,
bbox=Bbox(top=rectangle.start.y,
left=rectangle.start.x,
height=rectangle.end.y - rectangle.start.y,
width=rectangle.end.x - rectangle.start.x))


class NDSegment(BaseModel):
keyframes: List[NDFrameRectangle]

@staticmethod
def lookup_segment_object_type(segment: List) -> "NDFrameObjectType":
"""Used for determining which object type the annotation contains
returns the object type"""
result = {Rectangle: NDFrameRectangle}.get(type(segment[0].value))
return result

def to_common(self, feature_schema_id: Cuid):
return [
keyframe.to_common(feature_schema_id) for keyframe in self.keyframes
]

@classmethod
def from_common(cls, segment):
nd_frame_object_type = cls.lookup_segment_object_type(segment)

return cls(keyframes=[
nd_frame_object_type.from_common(object_annotation.frame,
object_annotation.value)
for object_annotation in segment
])


class NDSegments(NDBaseObject):
segments: List[NDSegment]

def to_common(self, feature_schema_id: Cuid):
result = []
for segment in self.segments:
result.extend(NDSegment.to_common(segment, feature_schema_id))
return result

@classmethod
def from_common(cls, segments: List[VideoObjectAnnotation], data: VideoData,
feature_schema_id: Cuid, extra: Dict[str,
Any]) -> "NDSegments":

segments = [NDSegment.from_common(segment) for segment in segments]

return cls(segments=segments,
dataRow=DataRow(id=data.uid),
schema_id=feature_schema_id,
uuid=extra.get('uuid'))


class _URIMask(BaseModel):
instanceURI: str
colorRGB: Tuple[int, int, int]
Expand Down Expand Up @@ -208,9 +284,20 @@ def to_common(annotation: "NDObjectType") -> ObjectAnnotation:

@classmethod
def from_common(
cls, annotation: ObjectAnnotation, data: Union[ImageData, TextData]
cls, annotation: Union[ObjectAnnotation,
List[List[VideoObjectAnnotation]]],
data: Union[ImageData, TextData]
) -> Union[NDLine, NDPoint, NDPolygon, NDRectangle, NDMask, NDTextEntity]:
obj = cls.lookup_object(annotation)

#if it is video segments
if (obj == NDSegments):
return obj.from_common(
annotation,
data,
feature_schema_id=annotation[0][0].feature_schema_id,
extra=annotation[0][0].extra)

subclasses = [
NDSubclassification.from_common(annot)
for annot in annotation.classifications
Expand All @@ -220,15 +307,19 @@ def from_common(
data)

@staticmethod
def lookup_object(annotation: ObjectAnnotation) -> "NDObjectType":
result = {
Line: NDLine,
Point: NDPoint,
Polygon: NDPolygon,
Rectangle: NDRectangle,
Mask: NDMask,
TextEntity: NDTextEntity
}.get(type(annotation.value))
def lookup_object(
annotation: Union[ObjectAnnotation, List]) -> "NDObjectType":
if isinstance(annotation, list):
result = NDSegments
else:
result = {
Line: NDLine,
Point: NDPoint,
Polygon: NDPolygon,
Rectangle: NDRectangle,
Mask: NDMask,
TextEntity: NDTextEntity
}.get(type(annotation.value))
if result is None:
raise TypeError(
f"Unable to convert object to MAL format. `{type(annotation.value)}`"
Expand All @@ -238,3 +329,5 @@ def lookup_object(annotation: ObjectAnnotation) -> "NDObjectType":

NDObjectType = Union[NDLine, NDPolygon, NDPoint, NDRectangle, NDMask,
NDTextEntity]

NDFrameObjectType = NDFrameRectangle
1 change: 1 addition & 0 deletions labelbox/orm/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@ class Entity(metaclass=EntityMeta):
Invite: Type[labelbox.Invite]
InviteLimit: Type[labelbox.InviteLimit]
ProjectRole: Type[labelbox.ProjectRole]
Project: Type[labelbox.Project]
Batch: Type[labelbox.Batch]

@classmethod
Expand Down
4 changes: 2 additions & 2 deletions labelbox/pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def __init__(self,
params: Dict[str, str],
dereferencing: Union[List[str], Dict[str, Any]],
obj_class: Union[Type["DbObject"], Callable[[Any, Any], Any]],
cursor_path: Optional[Dict[str, Any]] = None,
cursor_path: Optional[List[str]] = None,
experimental: bool = False):
""" Creates a PaginatedCollection.

Expand Down Expand Up @@ -105,7 +105,7 @@ def get_next_page(self) -> Tuple[Dict[str, Any], bool]:

class _CursorPagination(_Pagination):

def __init__(self, cursor_path: Dict[str, Any], *args, **kwargs):
def __init__(self, cursor_path: List[str], *args, **kwargs):
super().__init__(*args, **kwargs)
self.cursor_path = cursor_path
self.next_cursor: Optional[Any] = None
Expand Down
Loading