Skip to content

Commit e5770fd

Browse files
Merge pull request #635 from Labelbox/kkim/AL-2896
[AL-2896] Video annotation serialization/deserialization using segment_index
2 parents 1f35904 + d771bda commit e5770fd

File tree

5 files changed

+85
-18
lines changed

5 files changed

+85
-18
lines changed

labelbox/data/annotation_types/annotation.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import abc
2-
from typing import Any, Dict, List, Union
2+
from typing import Any, Dict, List, Optional, Union
33

44
from .classification import Checklist, Dropdown, Radio, Text
55
from .feature import FeatureSchema
@@ -72,11 +72,13 @@ class VideoObjectAnnotation(ObjectAnnotation):
7272
value (Geometry)
7373
frame (Int): The frame index that this annotation corresponds to
7474
keyframe (bool): Whether or not this annotation was a human generated or interpolated annotation
75+
segment_id (Optional[Int]): Index of video segment this annotation belongs to
7576
classifications (List[ClassificationAnnotation]) = []
7677
extra (Dict[str, Any])
7778
"""
7879
frame: int
7980
keyframe: bool
81+
segment_index: Optional[int] = None
8082

8183

8284
class VideoClassificationAnnotation(ClassificationAnnotation):
@@ -87,6 +89,8 @@ class VideoClassificationAnnotation(ClassificationAnnotation):
8789
feature_schema_id (Optional[Cuid])
8890
value (Union[Text, Checklist, Radio, Dropdown])
8991
frame (int): The frame index that this annotation corresponds to
92+
segment_id (Optional[Int]): Index of video segment this annotation belongs to
9093
extra (Dict[str, Any])
9194
"""
9295
frame: int
96+
segment_index: Optional[int] = None

labelbox/data/serialization/ndjson/label.py

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,39 @@ def _get_consecutive_frames(
8888
consecutive.append((group[0], group[-1]))
8989
return consecutive
9090

91+
@classmethod
92+
def _get_segment_frame_ranges(
93+
cls, annotation_group: List[Union[VideoClassificationAnnotation,
94+
VideoObjectAnnotation]]
95+
) -> List[Tuple[int, int]]:
96+
sorted_frame_segment_indices = sorted([
97+
(annotation.frame, annotation.segment_index)
98+
for annotation in annotation_group
99+
if annotation.segment_index is not None
100+
])
101+
if len(sorted_frame_segment_indices) == 0:
102+
# Group segment by consecutive frames, since `segment_index` is not present
103+
return cls._get_consecutive_frames(
104+
sorted([annotation.frame for annotation in annotation_group]))
105+
elif len(sorted_frame_segment_indices) == len(annotation_group):
106+
# Group segment by segment_index
107+
last_segment_id = 0
108+
segment_groups = defaultdict(list)
109+
for frame, segment_index in sorted_frame_segment_indices:
110+
if segment_index < last_segment_id:
111+
raise ValueError(
112+
f"`segment_index` must be in ascending order. Please investigate video annotation at frame, '{frame}'"
113+
)
114+
segment_groups[segment_index].append(frame)
115+
last_segment_id = segment_index
116+
frame_ranges = []
117+
for group in segment_groups.values():
118+
frame_ranges.append((group[0], group[-1]))
119+
return frame_ranges
120+
else:
121+
raise ValueError(
122+
f"Video annotations cannot partially have `segment_index` set")
123+
91124
@classmethod
92125
def _create_video_annotations(
93126
cls, label: Label
@@ -102,12 +135,12 @@ def _create_video_annotations(
102135
annot.name].append(annot)
103136

104137
for annotation_group in video_annotations.values():
105-
consecutive_frames = cls._get_consecutive_frames(
106-
sorted([annotation.frame for annotation in annotation_group]))
138+
segment_frame_ranges = cls._get_segment_frame_ranges(
139+
annotation_group)
107140
if isinstance(annotation_group[0], VideoClassificationAnnotation):
108141
annotation = annotation_group[0]
109142
frames_data = []
110-
for frames in consecutive_frames:
143+
for frames in segment_frame_ranges:
111144
frames_data.append({'start': frames[0], 'end': frames[-1]})
112145
annotation.extra.update({'frames': frames_data})
113146
yield NDClassification.from_common(annotation, label.data)
@@ -118,7 +151,7 @@ def _create_video_annotations(
118151
for video object annotations
119152
and will not import alongside the object annotations.""")
120153
segments = []
121-
for start_frame, end_frame in consecutive_frames:
154+
for start_frame, end_frame in segment_frame_ranges:
122155
segment = []
123156
for annotation in annotation_group:
124157
if annotation.keyframe and start_frame <= annotation.frame <= end_frame:

labelbox/data/serialization/ndjson/objects.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,10 @@ def from_common(cls, point: Point,
6565
class NDFramePoint(VideoSupported):
6666
point: _Point
6767

68-
def to_common(self, name: str,
69-
feature_schema_id: Cuid) -> VideoObjectAnnotation:
68+
def to_common(self, name: str, feature_schema_id: Cuid,
69+
segment_index: int) -> VideoObjectAnnotation:
7070
return VideoObjectAnnotation(frame=self.frame,
71+
segment_index=segment_index,
7172
keyframe=True,
7273
name=name,
7374
feature_schema_id=feature_schema_id,
@@ -104,10 +105,11 @@ def from_common(cls, line: Line,
104105
class NDFrameLine(VideoSupported):
105106
line: List[_Point]
106107

107-
def to_common(self, name: str,
108-
feature_schema_id: Cuid) -> VideoObjectAnnotation:
108+
def to_common(self, name: str, feature_schema_id: Cuid,
109+
segment_index: int) -> VideoObjectAnnotation:
109110
return VideoObjectAnnotation(
110111
frame=self.frame,
112+
segment_index=segment_index,
111113
keyframe=True,
112114
name=name,
113115
feature_schema_id=feature_schema_id,
@@ -171,10 +173,11 @@ def from_common(cls, rectangle: Rectangle,
171173
class NDFrameRectangle(VideoSupported):
172174
bbox: Bbox
173175

174-
def to_common(self, name: str,
175-
feature_schema_id: Cuid) -> VideoObjectAnnotation:
176+
def to_common(self, name: str, feature_schema_id: Cuid,
177+
segment_index: int) -> VideoObjectAnnotation:
176178
return VideoObjectAnnotation(
177179
frame=self.frame,
180+
segment_index=segment_index,
178181
keyframe=True,
179182
name=name,
180183
feature_schema_id=feature_schema_id,
@@ -211,11 +214,13 @@ def segment_with_uuid(keyframe: Union[NDFrameRectangle, NDFramePoint,
211214
keyframe.extra = {'uuid': uuid}
212215
return keyframe
213216

214-
def to_common(self, name: str, feature_schema_id: Cuid, uuid: str):
217+
def to_common(self, name: str, feature_schema_id: Cuid, uuid: str,
218+
segment_index: int):
215219
return [
216220
self.segment_with_uuid(
217221
keyframe.to_common(name=name,
218-
feature_schema_id=feature_schema_id), uuid)
222+
feature_schema_id=feature_schema_id,
223+
segment_index=segment_index), uuid)
219224
for keyframe in self.keyframes
220225
]
221226

@@ -235,11 +240,12 @@ class NDSegments(NDBaseObject):
235240

236241
def to_common(self, name: str, feature_schema_id: Cuid):
237242
result = []
238-
for segment in self.segments:
243+
for idx, segment in enumerate(self.segments):
239244
result.extend(
240245
NDSegment.to_common(segment,
241246
name=name,
242247
feature_schema_id=feature_schema_id,
248+
segment_index=idx,
243249
uuid=self.uuid))
244250
return result
245251

tests/data/assets/ndjson/video_import.json

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,17 @@
3030
{
3131
"frame": 1,
3232
"line": [{"x": 10.0, "y": 10.0}, {"x": 100.0, "y": 100.0}, {"x": 50.0, "y": 30.0}]
33+
},
34+
{
35+
"frame": 5,
36+
"line": [{"x": 15.0, "y": 10.0}, {"x": 50.0, "y": 100.0}, {"x": 50.0, "y": 30.0}]
3337
}
3438
]
3539
},
3640
{
3741
"keyframes": [
3842
{
39-
"frame": 5,
43+
"frame": 8,
4044
"line": [{"x": 100.0, "y": 10.0}, {"x": 50.0, "y": 100.0}, {"x": 50.0, "y": 30.0}]
4145
}
4246
]
@@ -62,6 +66,10 @@
6266
{
6367
"frame": 5,
6468
"point": {"x": 50.0, "y": 50.0}
69+
},
70+
{
71+
"frame": 10,
72+
"point": {"x": 10.0, "y": 50.0}
6573
}
6674
]
6775
}
@@ -78,13 +86,17 @@
7886
{
7987
"frame": 1,
8088
"bbox": {"top": 10.0, "left": 5.0, "height": 100.0, "width": 150.0}
89+
},
90+
{
91+
"frame": 5,
92+
"bbox": {"top": 30.0, "left": 5.0, "height": 50.0, "width": 150.0}
8193
}
8294
]
8395
},
8496
{
8597
"keyframes": [
8698
{
87-
"frame": 5,
99+
"frame": 10,
88100
"bbox": {"top": 300.0, "left": 200.0, "height": 400.0, "width": 150.0}
89101
}
90102
]

tests/data/assets/ndjson/video_import_name_only.json

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,17 @@
3030
{
3131
"frame": 1,
3232
"line": [{"x": 10.0, "y": 10.0}, {"x": 100.0, "y": 100.0}, {"x": 50.0, "y": 30.0}]
33+
},
34+
{
35+
"frame": 5,
36+
"line": [{"x": 15.0, "y": 10.0}, {"x": 50.0, "y": 100.0}, {"x": 50.0, "y": 30.0}]
3337
}
3438
]
3539
},
3640
{
3741
"keyframes": [
3842
{
39-
"frame": 5,
43+
"frame": 8,
4044
"line": [{"x": 100.0, "y": 10.0}, {"x": 50.0, "y": 100.0}, {"x": 50.0, "y": 30.0}]
4145
}
4246
]
@@ -62,6 +66,10 @@
6266
{
6367
"frame": 5,
6468
"point": {"x": 50.0, "y": 50.0}
69+
},
70+
{
71+
"frame": 10,
72+
"point": {"x": 10.0, "y": 50.0}
6573
}
6674
]
6775
}
@@ -78,13 +86,17 @@
7886
{
7987
"frame": 1,
8088
"bbox": {"top": 10.0, "left": 5.0, "height": 100.0, "width": 150.0}
89+
},
90+
{
91+
"frame": 5,
92+
"bbox": {"top": 30.0, "left": 5.0, "height": 50.0, "width": 150.0}
8193
}
8294
]
8395
},
8496
{
8597
"keyframes": [
8698
{
87-
"frame": 5,
99+
"frame": 10,
88100
"bbox": {"top": 300.0, "left": 200.0, "height": 400.0, "width": 150.0}
89101
}
90102
]

0 commit comments

Comments
 (0)