Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding FPS metadata to Video formats #105

Closed
wants to merge 14 commits into from
18 changes: 18 additions & 0 deletions sleap_io/io/video.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,13 @@ class VideoBackend:
If False, will close the reader after each call. If True (the default), it
will keep the reader open and cache it for subsequent calls which may
enhance the performance of reading multiple frames.
_frame_rate: Frame rate of the video.
keyaloding marked this conversation as resolved.
Show resolved Hide resolved
"""

filename: str | Path | list[str] | list[Path]
grayscale: Optional[bool] = None
keep_open: bool = True
_frame_rate: Optional[float] = None
_cached_shape: Optional[Tuple[int, int, int, int]] = None
_open_reader: Optional[object] = None

Expand All @@ -77,6 +79,7 @@ def from_filename(
frames. If False, will close the reader after each call. If True (the
default), it will keep the reader open and cache it for subsequent calls
which may enhance the performance of reading multiple frames.
_frame_rate: Frame rate of the video.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update type hint for _frame_rate parameter in from_filename method.

The type hint for _frame_rate should be Optional[float] instead of Optional[bool].

- _frame_rate: Optional[bool] = None,
+ _frame_rate: Optional[float] = None,

Committable suggestion was skipped due to low confidence.


Returns:
VideoBackend subclass instance.
Expand Down Expand Up @@ -189,6 +192,18 @@ def frames(self) -> int:
"""Number of frames in the video."""
return self.shape[0]

@property
def frame_rate(self) -> Optional[float]:
"""Frames per second of the video."""
video_extensions = ["mp4", "avi", "mov", "mj2", "mkv"]
if not any(self.filename.endswith(ext) for ext in video_extensions):
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This conditional is necessary because ImageVideo inherits from VideoBackend. If we try to check the frame rate of ImageVideo objects, which are stored as image files, we will get an error.

return None

if "cv2" in sys.modules:
return cv2.VideoCapture(self.filename).get(cv2.CAP_PROP_FPS)
else:
return iio.immeta(self.filename)["fps"]

def __len__(self) -> int:
"""Return number of frames in the video."""
return self.shape[0]
Expand Down Expand Up @@ -314,6 +329,7 @@ class MediaVideo(VideoBackend):
If False, will close the reader after each call. If True (the default), it
will keep the reader open and cache it for subsequent calls which may
enhance the performance of reading multiple frames.
_frame_rate: Frame rate of the video.
plugin: Video plugin to use. One of "opencv", "FFMPEG", or "pyav". If `None`,
will use the first available plugin in the order listed above.
"""
Expand Down Expand Up @@ -469,6 +485,7 @@ class HDF5Video(VideoBackend):
If False, will close the reader after each call. If True (the default), it
will keep the reader open and cache it for subsequent calls which may
enhance the performance of reading multiple frames.
_frame_rate: Frame rate of the video.
dataset: Name of dataset to read from. If `None`, will try to find a rank-4
dataset by iterating through datasets in the file. If specifying an embedded
dataset, this can be the group containing a "video" dataset or the dataset
Expand Down Expand Up @@ -710,6 +727,7 @@ class ImageVideo(VideoBackend):
"""

EXTS = ("png", "jpg", "jpeg", "tif", "tiff", "bmp")
_frame_rate = None

@staticmethod
def find_images(folder: str) -> list[str]:
Expand Down
12 changes: 12 additions & 0 deletions sleap_io/model/video.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ def from_filename(
dataset: Optional[str] = None,
grayscale: Optional[bool] = None,
keep_open: bool = True,
_frame_rate: Optional[float] = None,
source_video: Optional[Video] = None,
**kwargs,
) -> VideoBackend:
Expand All @@ -79,6 +80,7 @@ def from_filename(
frames. If False, will close the reader after each call. If True (the
default), it will keep the reader open and cache it for subsequent calls
which may enhance the performance of reading multiple frames.
_frame_rate: The frame rate of the video.
source_video: The source video object if this is a proxy video. This is
present when the video contains an embedded subset of frames from
another video.
Expand All @@ -93,6 +95,7 @@ def from_filename(
dataset=dataset,
grayscale=grayscale,
keep_open=keep_open,
_frame_rate=_frame_rate,
**kwargs,
),
source_video=source_video,
Expand All @@ -107,6 +110,15 @@ def shape(self) -> Tuple[int, int, int, int] | None:
"""
return self._get_shape()

@property
def frame_rate(self) -> float | None:
"""Return the frames per second of the video.

If the video backend is not set or it cannot determine the frames per second of
the video, this will return None.
"""
return self.backend.frame_rate

def _get_shape(self) -> Tuple[int, int, int, int] | None:
"""Return the shape of the video as (num_frames, height, width, channels).

Expand Down
1 change: 1 addition & 0 deletions tests/io/test_video_backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ def test_video_backend_from_filename(centered_pair_low_quality_path, slp_minimal
assert type(backend) == MediaVideo
assert backend.filename == centered_pair_low_quality_path
assert backend.shape == (1100, 384, 384, 1)
assert backend.frame_rate == 15.0

backend = VideoBackend.from_filename(slp_minimal_pkg)
assert type(backend) == HDF5Video
Expand Down
Loading