1313 NUM_SENSORS_KEY ,
1414 POINTCLOUD_LOCATION_KEY ,
1515 REFERENCE_ID_KEY ,
16+ VIDEO_LOCATION_KEY ,
1617 VIDEO_UPLOAD_TYPE_KEY ,
18+ VIDEO_URL_KEY ,
1719)
1820
1921from .annotation import is_local_path
@@ -419,16 +421,17 @@ class _VideoUploadType(Enum):
419421
420422@dataclass
421423class 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.
426429
427430 VideoScenes are uploaded to a :class:`Dataset` with any accompanying
428431 metadata. Each of :class:`DatasetItems <DatasetItem>` representing a frame
429432 also accepts metadata.
430433
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
432435 now differ). Existing video are expected to retain the same frames, and only
433436 metadata can be updated. If a video definition is changed (for example,
434437 additional frames added) the update operation will be ignored. If you would
@@ -437,30 +440,31 @@ class VideoScene(ABC):
437440
438441 Parameters:
439442 reference_id (str): User-specified identifier to reference the scene.
440- frame_rate (int): Frame rate of the video.
441443 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.
448454 metadata (Optional[Dict]): Optional metadata to include with the scene.
449455
450456 Refer to our `guide to uploading video data
451457 <https://nucleus.scale.com/docs/uploading-video-data>`_ for more info!
452458 """
453459
454460 reference_id : str
455- frame_rate : int
456461 attachment_type : _VideoUploadType
462+ frame_rate : Optional [int ] = None
463+ video_location : Optional [str ] = None
457464 items : List [DatasetItem ] = field (default_factory = list )
458465 metadata : Optional [dict ] = field (default_factory = dict )
459466
460467 def __post_init__ (self ):
461- assert (
462- self .attachment_type != _VideoUploadType .IMAGE
463- ), "Videos can currently only be uploaded from frames"
464468 if self .metadata is None :
465469 self .metadata = {}
466470
@@ -469,41 +473,67 @@ def __eq__(self, other):
469473 [
470474 self .reference_id == other .reference_id ,
471475 self .items == other .items ,
476+ self .video_location == other .video_location ,
472477 self .metadata == other .metadata ,
473478 ]
474479 )
475480
476481 @property
477482 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"
479487 return len (self .items )
480488
481489 def validate (self ):
482490 # 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"
489499 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" :
492513 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'"
495522
496523 def add_item (
497524 self , item : DatasetItem , index : int = None , update : bool = False
498525 ) -> None :
499- """Adds DatasetItem to the specified index.
526+ """Adds DatasetItem to the specified index for videos uploaded as an array of images .
500527
501528 Parameters:
502529 item (:class:`DatasetItem`): Video item to add.
503530 index: Serial index at which to add the item.
504531 update: Whether to overwrite the item at the specified index, if it
505532 exists. Default is False.
506533 """
534+ assert (
535+ self .video_location is None
536+ ), "Cannot add item to a video uploaded as an mp4"
507537 if index is None :
508538 index = len (self .items )
509539 assert (
@@ -515,44 +545,57 @@ def add_item(
515545 self .items .append (item )
516546
517547 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 .
519549
520550 Parameters:
521551 index: Serial index for which to retrieve the DatasetItem.
522552
523553 Return:
524554 :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"
525558 if index < 0 or index > len (self .items ):
526559 raise ValueError (
527560 f"This scene does not have an item at index { index } "
528561 )
529562 return self .items [index ]
530563
531564 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 .
533566
534567 Returns:
535568 List[:class:`DatasetItem`]: List of DatasetItems, sorted by index ascending.
536569 """
570+ assert (
571+ self .video_location is None
572+ ), "Cannot get items from a video uploaded as an mp4"
537573 return self .items
538574
539575 def info (self ):
540- """Fetches information about the scene.
576+ """Fetches information about the video scene.
541577
542578 Returns:
543579 Payload containing::
544580
545581 {
546582 "reference_id": str,
547- "length": int,
548- "num_sensors": int
583+ "length": Optional[int],
584+ "frame_rate": int,
585+ "video_url": Optional[str],
549586 }
550587 """
551- return {
588+ payload : Dict [ str , Any ] = {
552589 REFERENCE_ID_KEY : self .reference_id ,
553- FRAME_RATE_KEY : self .frame_rate ,
554- LENGTH_KEY : self .length ,
555590 }
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
556599
557600 @classmethod
558601 def from_json (cls , payload : dict ):
@@ -561,24 +604,31 @@ def from_json(cls, payload: dict):
561604 items = [DatasetItem .from_json (item ) for item in items_payload ]
562605 return cls (
563606 reference_id = payload [REFERENCE_ID_KEY ],
564- frame_rate = payload [ FRAME_RATE_KEY ] ,
607+ frame_rate = payload . get ( FRAME_RATE_KEY , None ) ,
565608 attachment_type = payload [VIDEO_UPLOAD_TYPE_KEY ],
566609 items = items ,
567610 metadata = payload .get (METADATA_KEY , {}),
611+ video_location = payload .get (VIDEO_URL_KEY , None ),
568612 )
569613
570614 def to_payload (self ) -> dict :
571615 """Serializes scene object to schematized JSON dict."""
572616 self .validate ()
573- items_payload = [item .to_payload (is_scene = True ) for item in self .items ]
574617 payload : Dict [str , Any ] = {
575618 REFERENCE_ID_KEY : self .reference_id ,
576619 VIDEO_UPLOAD_TYPE_KEY : self .attachment_type ,
577- FRAME_RATE_KEY : self .frame_rate ,
578- FRAMES_KEY : items_payload ,
579620 }
621+ if self .frame_rate :
622+ payload [FRAME_RATE_KEY ] = self .frame_rate
580623 if self .metadata :
581624 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
582632 return payload
583633
584634 def to_json (self ) -> str :
@@ -590,16 +640,24 @@ def check_all_scene_paths_remote(
590640 scenes : Union [List [LidarScene ], List [VideoScene ]]
591641):
592642 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 ):
602646 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 "
604648 "local, or a remote URL type that is not supported."
605649 )
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