Skip to content

Commit 838fe1c

Browse files
authored
Merge pull request #553 from Labelbox/develop
3.20.0
2 parents 90c22ee + 5eff529 commit 838fe1c

File tree

15 files changed

+997
-54
lines changed

15 files changed

+997
-54
lines changed

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
# Changelog
22

3+
# Version 3.20.0 (2022-04-27)
4+
## Added
5+
* Batches in a project can be retrieved with `project.batches()`
6+
* Added `Batch.remove_queued_data_rows()` to cancel remaining data rows in batch
7+
* Added `Batch.export_data_rows()` which returns `DataRow`s for a batch
8+
9+
## Updated
10+
* NDJsonConverter now supports Video bounding box annotations.
11+
* Note: Currently does not support nested classifications.
12+
* 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.
13+
14+
15+
## Fix
16+
* `batch.project()` now works
17+
318
# Version 3.19.1 (2022-04-14)
419
## Fix
520
* `create_data_rows` and `create_data_rows_sync` now uploads the file with a mimetype

labelbox/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
name = "labelbox"
2-
__version__ = "3.19.1"
2+
__version__ = "3.20.0"
3+
4+
import sys
5+
import warnings
6+
7+
if sys.version_info < (3, 7):
8+
warnings.warn("""Python 3.6 will no longer be actively supported
9+
starting 06/01/2022. Please upgrade to Python 3.7 or higher.""")
310

411
from labelbox.client import Client
512
from labelbox.schema.project import Project

labelbox/data/serialization/ndjson/label.py

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
from operator import itemgetter
33
from typing import Dict, Generator, List, Tuple, Union
44
from collections import defaultdict
5+
import warnings
56

67
from pydantic import BaseModel
78

8-
from ...annotation_types.annotation import ClassificationAnnotation, ObjectAnnotation, VideoClassificationAnnotation
9+
from ...annotation_types.annotation import ClassificationAnnotation, ObjectAnnotation, VideoClassificationAnnotation, VideoObjectAnnotation
910
from ...annotation_types.collection import LabelCollection, LabelGenerator
1011
from ...annotation_types.data import ImageData, TextData, VideoData
1112
from ...annotation_types.label import Label
@@ -15,12 +16,13 @@
1516

1617
from .metric import NDScalarMetric, NDMetricAnnotation, NDConfusionMatrixMetric
1718
from .classification import NDChecklistSubclass, NDClassification, NDClassificationType, NDRadioSubclass
18-
from .objects import NDObject, NDObjectType
19+
from .objects import NDObject, NDObjectType, NDSegments
1920

2021

2122
class NDLabel(BaseModel):
2223
annotations: List[Union[NDObjectType, NDClassificationType,
23-
NDConfusionMatrixMetric, NDScalarMetric]]
24+
NDConfusionMatrixMetric, NDScalarMetric,
25+
NDSegments]]
2426

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

3941
def _generate_annotations(
40-
self, grouped_annotations: Dict[str, List[Union[NDObjectType,
41-
NDClassificationType,
42-
NDConfusionMatrixMetric,
43-
NDScalarMetric]]]
42+
self,
43+
grouped_annotations: Dict[str,
44+
List[Union[NDObjectType, NDClassificationType,
45+
NDConfusionMatrixMetric,
46+
NDScalarMetric, NDSegments]]]
4447
) -> Generator[Label, None, None]:
4548
for data_row_id, annotations in grouped_annotations.items():
4649
annots = []
4750
for annotation in annotations:
48-
if isinstance(annotation, NDObjectType.__args__):
51+
if isinstance(annotation, NDSegments):
52+
annots.extend(
53+
NDSegments.to_common(annotation, annotation.schema_id))
54+
55+
elif isinstance(annotation, NDObjectType.__args__):
4956
annots.append(NDObject.to_common(annotation))
5057
elif isinstance(annotation, NDClassificationType.__args__):
5158
annots.extend(NDClassification.to_common(annotation))
@@ -55,7 +62,6 @@ def _generate_annotations(
5562
else:
5663
raise TypeError(
5764
f"Unsupported annotation. {type(annotation)}")
58-
5965
data = self._infer_media_type(annotations)(uid=data_row_id)
6066
yield Label(annotations=annots, data=data)
6167

@@ -65,7 +71,7 @@ def _infer_media_type(
6571
types = {type(annotation) for annotation in annotations}
6672
if TextEntity in types:
6773
return TextData
68-
elif VideoClassificationAnnotation in types:
74+
elif VideoClassificationAnnotation in types or VideoObjectAnnotation in types:
6975
return VideoData
7076
else:
7177
return ImageData
@@ -83,26 +89,46 @@ def _get_consecutive_frames(
8389
def _create_video_annotations(
8490
cls, label: Label
8591
) -> Generator[Union[NDChecklistSubclass, NDRadioSubclass], None, None]:
92+
8693
video_annotations = defaultdict(list)
8794
for annot in label.annotations:
88-
if isinstance(annot, VideoClassificationAnnotation):
95+
if isinstance(
96+
annot,
97+
(VideoClassificationAnnotation, VideoObjectAnnotation)):
8998
video_annotations[annot.feature_schema_id].append(annot)
9099

91100
for annotation_group in video_annotations.values():
92101
consecutive_frames = cls._get_consecutive_frames(
93102
sorted([annotation.frame for annotation in annotation_group]))
94-
annotation = annotation_group[0]
95-
frames_data = []
96-
for frames in consecutive_frames:
97-
frames_data.append({'start': frames[0], 'end': frames[-1]})
98-
annotation.extra.update({'frames': frames_data})
99-
yield NDClassification.from_common(annotation, label.data)
103+
104+
if isinstance(annotation_group[0], VideoClassificationAnnotation):
105+
annotation = annotation_group[0]
106+
frames_data = []
107+
for frames in consecutive_frames:
108+
frames_data.append({'start': frames[0], 'end': frames[-1]})
109+
annotation.extra.update({'frames': frames_data})
110+
yield NDClassification.from_common(annotation, label.data)
111+
112+
elif isinstance(annotation_group[0], VideoObjectAnnotation):
113+
warnings.warn(
114+
"""Nested classifications are not currently supported
115+
for video object annotations
116+
and will not import alongside the object annotations.""")
117+
segments = []
118+
for start_frame, end_frame in consecutive_frames:
119+
segment = []
120+
for annotation in annotation_group:
121+
if annotation.keyframe and start_frame <= annotation.frame <= end_frame:
122+
segment.append(annotation)
123+
segments.append(segment)
124+
yield NDObject.from_common(segments, label.data)
100125

101126
@classmethod
102127
def _create_non_video_annotations(cls, label: Label):
103128
non_video_annotations = [
104129
annot for annot in label.annotations
105-
if not isinstance(annot, VideoClassificationAnnotation)
130+
if not isinstance(annot, (VideoClassificationAnnotation,
131+
VideoObjectAnnotation))
106132
]
107133
for annotation in non_video_annotations:
108134
if isinstance(annotation, ClassificationAnnotation):

labelbox/data/serialization/ndjson/objects.py

Lines changed: 104 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77
from pydantic import BaseModel
88
from PIL import Image
99

10+
from labelbox.data.annotation_types.data.video import VideoData
11+
1012
from ...annotation_types.data import ImageData, TextData, MaskData
1113
from ...annotation_types.ner import TextEntity
1214
from ...annotation_types.types import Cuid
1315
from ...annotation_types.geometry import Rectangle, Polygon, Line, Point, Mask
14-
from ...annotation_types.annotation import ClassificationAnnotation, ObjectAnnotation
16+
from ...annotation_types.annotation import ClassificationAnnotation, ObjectAnnotation, VideoObjectAnnotation
1517
from .classification import NDSubclassification, NDSubclassificationType
1618
from .base import DataRow, NDAnnotation
1719

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

2224

25+
class VideoSupported(BaseModel):
26+
#support for video for objects are per-frame basis
27+
frame: int
28+
29+
2330
class _Point(BaseModel):
2431
x: float
2532
y: float
@@ -118,6 +125,75 @@ def from_common(cls, rectangle: Rectangle,
118125
classifications=classifications)
119126

120127

128+
class NDFrameRectangle(VideoSupported):
129+
bbox: Bbox
130+
131+
def to_common(self, feature_schema_id: Cuid) -> VideoObjectAnnotation:
132+
return VideoObjectAnnotation(
133+
frame=self.frame,
134+
keyframe=True,
135+
feature_schema_id=feature_schema_id,
136+
value=Rectangle(start=Point(x=self.bbox.left, y=self.bbox.top),
137+
end=Point(x=self.bbox.left + self.bbox.width,
138+
y=self.bbox.top + self.bbox.height)))
139+
140+
@classmethod
141+
def from_common(cls, frame: int, rectangle: Rectangle):
142+
return cls(frame=frame,
143+
bbox=Bbox(top=rectangle.start.y,
144+
left=rectangle.start.x,
145+
height=rectangle.end.y - rectangle.start.y,
146+
width=rectangle.end.x - rectangle.start.x))
147+
148+
149+
class NDSegment(BaseModel):
150+
keyframes: List[NDFrameRectangle]
151+
152+
@staticmethod
153+
def lookup_segment_object_type(segment: List) -> "NDFrameObjectType":
154+
"""Used for determining which object type the annotation contains
155+
returns the object type"""
156+
result = {Rectangle: NDFrameRectangle}.get(type(segment[0].value))
157+
return result
158+
159+
def to_common(self, feature_schema_id: Cuid):
160+
return [
161+
keyframe.to_common(feature_schema_id) for keyframe in self.keyframes
162+
]
163+
164+
@classmethod
165+
def from_common(cls, segment):
166+
nd_frame_object_type = cls.lookup_segment_object_type(segment)
167+
168+
return cls(keyframes=[
169+
nd_frame_object_type.from_common(object_annotation.frame,
170+
object_annotation.value)
171+
for object_annotation in segment
172+
])
173+
174+
175+
class NDSegments(NDBaseObject):
176+
segments: List[NDSegment]
177+
178+
def to_common(self, feature_schema_id: Cuid):
179+
result = []
180+
for segment in self.segments:
181+
result.extend(NDSegment.to_common(segment, feature_schema_id))
182+
return result
183+
184+
@classmethod
185+
def from_common(cls, segments: List[VideoObjectAnnotation], data: VideoData,
186+
feature_schema_id: Cuid, extra: Dict[str,
187+
Any]) -> "NDSegments":
188+
189+
segments = [NDSegment.from_common(segment) for segment in segments]
190+
191+
return cls(segments=segments,
192+
dataRow=DataRow(id=data.uid),
193+
schema_id=feature_schema_id,
194+
uuid=extra.get('uuid'))
195+
196+
121197
class _URIMask(BaseModel):
122198
instanceURI: str
123199
colorRGB: Tuple[int, int, int]
@@ -208,9 +284,20 @@ def to_common(annotation: "NDObjectType") -> ObjectAnnotation:
208284

209285
@classmethod
210286
def from_common(
211-
cls, annotation: ObjectAnnotation, data: Union[ImageData, TextData]
287+
cls, annotation: Union[ObjectAnnotation,
288+
List[List[VideoObjectAnnotation]]],
289+
data: Union[ImageData, TextData]
212290
) -> Union[NDLine, NDPoint, NDPolygon, NDRectangle, NDMask, NDTextEntity]:
213291
obj = cls.lookup_object(annotation)
292+
293+
#if it is video segments
294+
if (obj == NDSegments):
295+
return obj.from_common(
296+
annotation,
297+
data,
298+
feature_schema_id=annotation[0][0].feature_schema_id,
299+
extra=annotation[0][0].extra)
300+
214301
subclasses = [
215302
NDSubclassification.from_common(annot)
216303
for annot in annotation.classifications
@@ -220,15 +307,19 @@ def from_common(
220307
data)
221308

222309
@staticmethod
223-
def lookup_object(annotation: ObjectAnnotation) -> "NDObjectType":
224-
result = {
225-
Line: NDLine,
226-
Point: NDPoint,
227-
Polygon: NDPolygon,
228-
Rectangle: NDRectangle,
229-
Mask: NDMask,
230-
TextEntity: NDTextEntity
231-
}.get(type(annotation.value))
310+
def lookup_object(
311+
annotation: Union[ObjectAnnotation, List]) -> "NDObjectType":
312+
if isinstance(annotation, list):
313+
result = NDSegments
314+
else:
315+
result = {
316+
Line: NDLine,
317+
Point: NDPoint,
318+
Polygon: NDPolygon,
319+
Rectangle: NDRectangle,
320+
Mask: NDMask,
321+
TextEntity: NDTextEntity
322+
}.get(type(annotation.value))
232323
if result is None:
233324
raise TypeError(
234325
f"Unable to convert object to MAL format. `{type(annotation.value)}`"
@@ -238,3 +329,5 @@ def lookup_object(annotation: ObjectAnnotation) -> "NDObjectType":
238329

239330
NDObjectType = Union[NDLine, NDPolygon, NDPoint, NDRectangle, NDMask,
240331
NDTextEntity]
332+
333+
NDFrameObjectType = NDFrameRectangle

labelbox/orm/model.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ class Entity(metaclass=EntityMeta):
347347
Invite: Type[labelbox.Invite]
348348
InviteLimit: Type[labelbox.InviteLimit]
349349
ProjectRole: Type[labelbox.ProjectRole]
350+
Project: Type[labelbox.Project]
350351
Batch: Type[labelbox.Batch]
351352

352353
@classmethod

labelbox/pagination.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def __init__(self,
2525
params: Dict[str, str],
2626
dereferencing: Union[List[str], Dict[str, Any]],
2727
obj_class: Union[Type["DbObject"], Callable[[Any, Any], Any]],
28-
cursor_path: Optional[Dict[str, Any]] = None,
28+
cursor_path: Optional[List[str]] = None,
2929
experimental: bool = False):
3030
""" Creates a PaginatedCollection.
3131
@@ -105,7 +105,7 @@ def get_next_page(self) -> Tuple[Dict[str, Any], bool]:
105105

106106
class _CursorPagination(_Pagination):
107107

108-
def __init__(self, cursor_path: Dict[str, Any], *args, **kwargs):
108+
def __init__(self, cursor_path: List[str], *args, **kwargs):
109109
super().__init__(*args, **kwargs)
110110
self.cursor_path = cursor_path
111111
self.next_cursor: Optional[Any] = None

0 commit comments

Comments
 (0)