13
13
NUM_SENSORS_KEY ,
14
14
POINTCLOUD_LOCATION_KEY ,
15
15
REFERENCE_ID_KEY ,
16
+ VIDEO_LOCATION_KEY ,
16
17
VIDEO_UPLOAD_TYPE_KEY ,
18
+ VIDEO_URL_KEY ,
17
19
)
18
20
19
21
from .annotation import is_local_path
@@ -419,16 +421,17 @@ class _VideoUploadType(Enum):
419
421
420
422
@dataclass
421
423
class VideoScene (ABC ):
422
- """
423
- Nucleus video datasets are comprised of VideoScenes, which are in turn
424
- comprised of a sequence of :class:`DatasetItems <DatasetItem>` which are
425
- equivalent to frames.
424
+ """Video or sequence of images over time.
425
+
426
+ Nucleus video datasets are comprised of VideoScenes. These can be
427
+ comprised of a single video, or a sequence of :class:`DatasetItems <DatasetItem>`
428
+ which are equivalent to frames.
426
429
427
430
VideoScenes are uploaded to a :class:`Dataset` with any accompanying
428
431
metadata. Each of :class:`DatasetItems <DatasetItem>` representing a frame
429
432
also accepts metadata.
430
433
431
- Note: Uploads with different items will error out (only on scenes that
434
+ Note: Updates with different items will error out (only on scenes that
432
435
now differ). Existing video are expected to retain the same frames, and only
433
436
metadata can be updated. If a video definition is changed (for example,
434
437
additional frames added) the update operation will be ignored. If you would
@@ -437,30 +440,31 @@ class VideoScene(ABC):
437
440
438
441
Parameters:
439
442
reference_id (str): User-specified identifier to reference the scene.
440
- frame_rate (int): Frame rate of the video.
441
443
attachment_type (str): The type of attachments being uploaded as a string literal.
442
- Currently, videos can only be uploaded as an array of frames, so the only
443
- accepted attachment_type is "image".
444
- items (Optional[List[:class:`DatasetItem`]]): List of items representing frames,
445
- to be a part of the scene. A scene can be created before items have been added
446
- to it, but must be non-empty when uploading to a :class:`Dataset`. A video scene
447
- can contain a maximum of 3000 items.
444
+ If the video is uploaded as an array of frames, the attachment_type is "image".
445
+ If the video is uploaded as an MP4, the attachment_type is "video".
446
+ frame_rate (Optional[int]): Required if attachment_type is "image". Frame rate of the video.
447
+ video_location (Optional[str]): Required if attachment_type is "video". The remote URL
448
+ containing the video MP4. Remote formats supported include any URL (``http://``
449
+ or ``https://``) or URIs for AWS S3, Azure, or GCS (i.e. ``s3://``, ``gcs://``).
450
+ items (Optional[List[:class:`DatasetItem`]]): Required if attachment_type is "image".
451
+ List of items representing frames, to be a part of the scene. A scene can be created
452
+ before items have been added to it, but must be non-empty when uploading to
453
+ a :class:`Dataset`. A video scene can contain a maximum of 3000 items.
448
454
metadata (Optional[Dict]): Optional metadata to include with the scene.
449
455
450
456
Refer to our `guide to uploading video data
451
457
<https://nucleus.scale.com/docs/uploading-video-data>`_ for more info!
452
458
"""
453
459
454
460
reference_id : str
455
- frame_rate : int
456
461
attachment_type : _VideoUploadType
462
+ frame_rate : Optional [int ] = None
463
+ video_location : Optional [str ] = None
457
464
items : List [DatasetItem ] = field (default_factory = list )
458
465
metadata : Optional [dict ] = field (default_factory = dict )
459
466
460
467
def __post_init__ (self ):
461
- assert (
462
- self .attachment_type != _VideoUploadType .IMAGE
463
- ), "Videos can currently only be uploaded from frames"
464
468
if self .metadata is None :
465
469
self .metadata = {}
466
470
@@ -469,41 +473,67 @@ def __eq__(self, other):
469
473
[
470
474
self .reference_id == other .reference_id ,
471
475
self .items == other .items ,
476
+ self .video_location == other .video_location ,
472
477
self .metadata == other .metadata ,
473
478
]
474
479
)
475
480
476
481
@property
477
482
def length (self ) -> int :
478
- """Number of items in the scene."""
483
+ """Gets number of items in the scene for videos uploaded as an array of images."""
484
+ assert (
485
+ self .video_location is None
486
+ ), "Videos uploaded as an mp4 have no length"
479
487
return len (self .items )
480
488
481
489
def validate (self ):
482
490
# TODO: make private
483
- assert self .frame_rate > 0 , "Frame rate must be at least 1"
484
- assert self .length > 0 , "Must have at least 1 item in a scene"
485
- for item in self .items :
486
- assert isinstance (
487
- item , DatasetItem
488
- ), "Each item in a scene must be a DatasetItem object"
491
+ assert self .attachment_type in ("image" , "video" )
492
+ if self .attachment_type == "image" :
493
+ assert (
494
+ self .frame_rate > 0
495
+ ), "When attachment_type='image' frame rate must be at least 1"
496
+ assert (
497
+ self .items and self .length > 0
498
+ ), "When attachment_type='image' scene must have a list of items of length at least 1"
489
499
assert (
490
- item .image_location is not None
491
- ), "Each item in a video scene must have an image_location"
500
+ not self .video_location
501
+ ), "No video location is accepted when attachment_type='image'"
502
+ for item in self .items :
503
+ assert isinstance (
504
+ item , DatasetItem
505
+ ), "Each item in a scene must be a DatasetItem object"
506
+ assert (
507
+ item .image_location is not None
508
+ ), "Each item in a video scene must have an image_location"
509
+ assert (
510
+ item .upload_to_scale is not False
511
+ ), "Skipping upload to Scale is not currently implemented for videos"
512
+ if self .attachment_type == "video" :
492
513
assert (
493
- item .upload_to_scale is not False
494
- ), "Skipping upload to Scale is not currently implemented for videos"
514
+ self .video_location
515
+ ), "When attachment_type='video' a video_location is required"
516
+ assert (
517
+ not self .frame_rate
518
+ ), "No frame rate is accepted when attachment_type='video'"
519
+ assert (
520
+ not self .items
521
+ ), "No list of items is accepted when attachment_type='video'"
495
522
496
523
def add_item (
497
524
self , item : DatasetItem , index : int = None , update : bool = False
498
525
) -> None :
499
- """Adds DatasetItem to the specified index.
526
+ """Adds DatasetItem to the specified index for videos uploaded as an array of images .
500
527
501
528
Parameters:
502
529
item (:class:`DatasetItem`): Video item to add.
503
530
index: Serial index at which to add the item.
504
531
update: Whether to overwrite the item at the specified index, if it
505
532
exists. Default is False.
506
533
"""
534
+ assert (
535
+ self .video_location is None
536
+ ), "Cannot add item to a video uploaded as an mp4"
507
537
if index is None :
508
538
index = len (self .items )
509
539
assert (
@@ -515,44 +545,57 @@ def add_item(
515
545
self .items .append (item )
516
546
517
547
def get_item (self , index : int ) -> DatasetItem :
518
- """Fetches the DatasetItem at the specified index.
548
+ """Fetches the DatasetItem at the specified index for videos uploaded as an array of images .
519
549
520
550
Parameters:
521
551
index: Serial index for which to retrieve the DatasetItem.
522
552
523
553
Return:
524
554
:class:`DatasetItem`: DatasetItem at the specified index."""
555
+ assert (
556
+ self .video_location is None
557
+ ), "Cannot get item from a video uploaded as an mp4"
525
558
if index < 0 or index > len (self .items ):
526
559
raise ValueError (
527
560
f"This scene does not have an item at index { index } "
528
561
)
529
562
return self .items [index ]
530
563
531
564
def get_items (self ) -> List [DatasetItem ]:
532
- """Fetches a sorted list of DatasetItems of the scene.
565
+ """Fetches a sorted list of DatasetItems of the scene for videos uploaded as an array of images .
533
566
534
567
Returns:
535
568
List[:class:`DatasetItem`]: List of DatasetItems, sorted by index ascending.
536
569
"""
570
+ assert (
571
+ self .video_location is None
572
+ ), "Cannot get items from a video uploaded as an mp4"
537
573
return self .items
538
574
539
575
def info (self ):
540
- """Fetches information about the scene.
576
+ """Fetches information about the video scene.
541
577
542
578
Returns:
543
579
Payload containing::
544
580
545
581
{
546
582
"reference_id": str,
547
- "length": int,
548
- "num_sensors": int
583
+ "length": Optional[int],
584
+ "frame_rate": int,
585
+ "video_url": Optional[str],
549
586
}
550
587
"""
551
- return {
588
+ payload : Dict [ str , Any ] = {
552
589
REFERENCE_ID_KEY : self .reference_id ,
553
- FRAME_RATE_KEY : self .frame_rate ,
554
- LENGTH_KEY : self .length ,
555
590
}
591
+ if self .frame_rate :
592
+ payload [FRAME_RATE_KEY ] = self .frame_rate
593
+ if self .video_location :
594
+ payload [VIDEO_URL_KEY ] = self .video_location
595
+ if self .items :
596
+ payload [LENGTH_KEY ] = self .length
597
+
598
+ return payload
556
599
557
600
@classmethod
558
601
def from_json (cls , payload : dict ):
@@ -561,24 +604,31 @@ def from_json(cls, payload: dict):
561
604
items = [DatasetItem .from_json (item ) for item in items_payload ]
562
605
return cls (
563
606
reference_id = payload [REFERENCE_ID_KEY ],
564
- frame_rate = payload [ FRAME_RATE_KEY ] ,
607
+ frame_rate = payload . get ( FRAME_RATE_KEY , None ) ,
565
608
attachment_type = payload [VIDEO_UPLOAD_TYPE_KEY ],
566
609
items = items ,
567
610
metadata = payload .get (METADATA_KEY , {}),
611
+ video_location = payload .get (VIDEO_URL_KEY , None ),
568
612
)
569
613
570
614
def to_payload (self ) -> dict :
571
615
"""Serializes scene object to schematized JSON dict."""
572
616
self .validate ()
573
- items_payload = [item .to_payload (is_scene = True ) for item in self .items ]
574
617
payload : Dict [str , Any ] = {
575
618
REFERENCE_ID_KEY : self .reference_id ,
576
619
VIDEO_UPLOAD_TYPE_KEY : self .attachment_type ,
577
- FRAME_RATE_KEY : self .frame_rate ,
578
- FRAMES_KEY : items_payload ,
579
620
}
621
+ if self .frame_rate :
622
+ payload [FRAME_RATE_KEY ] = self .frame_rate
580
623
if self .metadata :
581
624
payload [METADATA_KEY ] = self .metadata
625
+ if self .video_location :
626
+ payload [VIDEO_URL_KEY ] = self .video_location
627
+ if self .items :
628
+ items_payload = [
629
+ item .to_payload (is_scene = True ) for item in self .items
630
+ ]
631
+ payload [FRAMES_KEY ] = items_payload
582
632
return payload
583
633
584
634
def to_json (self ) -> str :
@@ -590,16 +640,24 @@ def check_all_scene_paths_remote(
590
640
scenes : Union [List [LidarScene ], List [VideoScene ]]
591
641
):
592
642
for scene in scenes :
593
- for item in scene .get_items ():
594
- pointcloud_location = getattr (item , POINTCLOUD_LOCATION_KEY )
595
- if pointcloud_location and is_local_path (pointcloud_location ):
596
- raise ValueError (
597
- f"All paths for DatasetItems in a Scene must be remote, but { item .pointcloud_location } is either "
598
- "local, or a remote URL type that is not supported."
599
- )
600
- image_location = getattr (item , IMAGE_LOCATION_KEY )
601
- if image_location and is_local_path (image_location ):
643
+ if isinstance (scene , VideoScene ) and scene .video_location :
644
+ video_location = getattr (scene , VIDEO_LOCATION_KEY )
645
+ if video_location and is_local_path (video_location ):
602
646
raise ValueError (
603
- f"All paths for DatasetItems in a Scene must be remote, but { item . image_location } is either "
647
+ f"All paths for videos must be remote, but { scene . video_location } is either "
604
648
"local, or a remote URL type that is not supported."
605
649
)
650
+ else :
651
+ for item in scene .get_items ():
652
+ pointcloud_location = getattr (item , POINTCLOUD_LOCATION_KEY )
653
+ if pointcloud_location and is_local_path (pointcloud_location ):
654
+ raise ValueError (
655
+ f"All paths for DatasetItems in a Scene must be remote, but { item .pointcloud_location } is either "
656
+ "local, or a remote URL type that is not supported."
657
+ )
658
+ image_location = getattr (item , IMAGE_LOCATION_KEY )
659
+ if image_location and is_local_path (image_location ):
660
+ raise ValueError (
661
+ f"All paths for DatasetItems in a Scene must be remote, but { item .image_location } is either "
662
+ "local, or a remote URL type that is not supported."
663
+ )
0 commit comments