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

Video handle persistence #64

Merged
merged 6 commits into from
Sep 12, 2023
Merged

Video handle persistence #64

merged 6 commits into from
Sep 12, 2023

Conversation

talmo
Copy link
Contributor

@talmo talmo commented Sep 12, 2023

This PR implements video handle persistence which should improve performance when reading repeatedly.

Addresses #62.

Summary by CodeRabbit

  • New Feature: Added a keep_open parameter to the VideoBackend class and its subclasses in sleap_io/io/video.py. This feature allows users to control whether the video reader should be kept open between calls to read frames or closed after each call, potentially improving performance when reading multiple frames.
  • Test: Updated test functions in tests/io/test_video_backends.py to include the new keep_open parameter. This ensures that the new functionality is adequately tested under different scenarios.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 12, 2023

Walkthrough

The changes introduce a keep_open parameter to the VideoBackend class and its subclasses, allowing the video reader to be kept open between frame reads. This potentially improves performance when reading multiple frames. The changes also include updates to test functions to accommodate this new feature.

Changes

File(s) Summary
sleap_io/io/video.py Introduced a keep_open parameter to the VideoBackend class and its subclasses. If keep_open is set to True, the reader is cached for subsequent calls. Added an _open_reader attribute to cache the video reader object.
sleap_io/model/video.py Added a keep_open parameter to the from_filename method of the Video class. The keep_open parameter is passed down to the VideoBackend.from_filename method.
tests/io/test_video_backends.py Updated test functions to include a keep_open parameter. The tests assert the expected behavior based on the value of keep_open.

🐇💻

In the land where code rabbits hop,
A change was made, it's quite the top!
With keep_open true, the reader stays,
For faster frame reads, it paves the ways.
Tests updated too, with care and hope,
Ensuring our code can smoothly cope.
So let's celebrate this clever tweak,
Making our software strong, not weak! 🎉🥕


Tips

Chat with CodeRabbit Bot (@coderabbitai)

  • Mention @coderabbitai in any review comment for bot assistance.
  • Note: Review comments are made on code diffs or files, not on the PR overview.

Pause Incremental Reviews

  • Insert @coderabbitai: ignore in the PR description to halt the bot's ongoing reviews. Remove the line to resume.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Review Status

Actionable comments generated: 4

Commits Files that changed from the base of the PR and between e0ead82 and a7d728d commits.
Files selected for processing (2)
  • sleap_io/io/video.py (6 hunks)
  • tests/io/test_video_backends.py (2 hunks)
Additional comments (Suppressed): 4
tests/io/test_video_backends.py (1)
  • 4-7: The import of the pytest module is necessary for the new parameterized test. This change aligns with the goals of the PR.
sleap_io/io/video.py (3)
  • 43-56: The new attribute keep_open and _open_reader have been added to the class. Ensure that these attributes are properly initialized in the constructor of the class and that they are used correctly throughout the codebase.

  • 58-66: Ensure that all calls to this function throughout the codebase have been updated to match the new signature.

  • 84-100: The from_filename method has been updated to include the keep_open parameter when creating instances of MediaVideo and HDF5Video. Make sure that the MediaVideo and HDF5Video classes have been updated to accept this new parameter.

tests/io/test_video_backends.py Show resolved Hide resolved
Comment on lines 334 to 357
`get_frame` method of the `VideoBackend` class instead.
"""
if self.plugin == "opencv":
reader = cv2.VideoCapture(self.filename)
if self.keep_open:
if self._open_reader is None:
self._open_reader = cv2.VideoCapture(self.filename)
reader = self._open_reader
else:
reader = cv2.VideoCapture(self.filename)
reader.set(cv2.CAP_PROP_POS_FRAMES, frame_idx)
_, img = reader.read()
else:
with iio.imopen(self.filename, "r", plugin=self.plugin) as vid:
elif self.plugin == "pyav" or self.plugin == "FFMPEG":
if self.keep_open:
if self._open_reader is None:
self._open_reader = iio.imopen(
self.filename, "r", plugin=self.plugin
)
reader = self._open_reader
img = vid.read(index=frame_idx)
else:
with iio.imopen(self.filename, "r", plugin=self.plugin) as vid:
img = vid.read(index=frame_idx)
return img

Copy link
Contributor

Choose a reason for hiding this comment

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

The _read_frame method has been updated to use the cached reader if keep_open is True. This could potentially improve performance when reading multiple frames. However, it's important to ensure that the reader is properly closed when it's no longer needed to avoid resource leaks.

+            if self.keep_open:
+                if self._open_reader is None:
+                    self._open_reader = cv2.VideoCapture(self.filename)
+                reader = self._open_reader
+            else:
+                reader = cv2.VideoCapture(self.filename)

Comment on lines 369 to 381
`get_frames` method of the `VideoBackend` class instead.
"""
if self.plugin == "opencv":
reader = cv2.VideoCapture(self.filename)
if self.keep_open:
if self._open_reader is None:
self._open_reader = cv2.VideoCapture(self.filename)
reader = self._open_reader
else:
reader = cv2.VideoCapture(self.filename)

reader.set(cv2.CAP_PROP_POS_FRAMES, frame_inds[0])
imgs = []
for idx in frame_inds:
Copy link
Contributor

Choose a reason for hiding this comment

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

Similar to the _read_frame method, the _read_frames method has also been updated to use the cached reader if keep_open is True. Again, make sure that the reader is properly closed when it's no longer needed.

+            if self.keep_open:
+                if self._open_reader is None:
+                    self._open_reader = cv2.VideoCapture(self.filename)
+                reader = self._open_reader
+            else:
+                reader = cv2.VideoCapture(self.filename)

Comment on lines 386 to 400
imgs.append(img)
imgs = np.stack(imgs, axis=0)

else:
with iio.imopen(self.filename, "r", plugin=self.plugin) as vid:
elif self.plugin == "pyav" or self.plugin == "FFMPEG":
if self.keep_open:
if self._open_reader is None:
self._open_reader = iio.imopen(
self.filename, "r", plugin=self.plugin
)
reader = self._open_reader
imgs = np.stack([vid.read(index=idx) for idx in frame_inds], axis=0)
else:
with iio.imopen(self.filename, "r", plugin=self.plugin) as vid:
imgs = np.stack([vid.read(index=idx) for idx in frame_inds], axis=0)
return imgs
Copy link
Contributor

Choose a reason for hiding this comment

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

The changes in this hunk are similar to the ones in the _read_frame method. The same comments apply here.

+            if self.keep_open:
+                if self._open_reader is None:
+                    self._open_reader = iio.imopen(
+                        self.filename, "r", plugin=self.plugin
+                    )
+                reader = self._open_reader
+                imgs = np.stack([vid.read(index=idx) for idx in frame_inds], axis=0)
+            else:
+                with iio.imopen(self.filename, "r", plugin=self.plugin) as vid:
+                    imgs = np.stack([vid.read(index=idx) for idx in frame_inds], axis=0)

@codecov
Copy link

codecov bot commented Sep 12, 2023

Codecov Report

Merging #64 (27740b1) into main (7ede786) will increase coverage by 0.03%.
The diff coverage is 90.14%.

@@            Coverage Diff             @@
##             main      #64      +/-   ##
==========================================
+ Coverage   94.64%   94.68%   +0.03%     
==========================================
  Files          12       12              
  Lines        1195     1241      +46     
==========================================
+ Hits         1131     1175      +44     
- Misses         64       66       +2     
Files Changed Coverage Δ
sleap_io/model/video.py 91.89% <ø> (ø)
sleap_io/io/video.py 85.60% <90.14%> (+2.05%) ⬆️

📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Review Status

Actionable comments generated: 1

Commits Files that changed from the base of the PR and between a7d728d and d56e11d commits.
Files selected for processing (3)
  • sleap_io/io/video.py (12 hunks)
  • sleap_io/model/video.py (2 hunks)
  • tests/io/test_video_backends.py (3 hunks)
Additional comments (Suppressed): 15
sleap_io/model/video.py (3)
  • 46-51: The new keep_open parameter has been added to the from_filename method of the Video class. This parameter controls whether the video reader should be kept open between calls to read frames or closed after each call. Ensure that all calls to this function throughout the codebase have been updated to match the new signature.

  • 56-62: The docstring has been updated to include a description for the new keep_open parameter. The explanation is clear and concise, providing enough information about what the parameter does and its default value.

  • 67-76: The keep_open parameter is passed to the VideoBackend.from_filename method when initializing the backend. This change aligns with the goal of the PR to allow control over whether the video reader is cached for subsequent calls.

tests/io/test_video_backends.py (1)
  • 7-7: The import of the pytest module is necessary for the new parameterized tests. This change is appropriate.
sleap_io/io/video.py (11)
  • 33-34: The change from using cls.__attrs_attrs__ to attrs.fields(cls) is a good one. It's more readable and maintainable.

  • 44-57: The addition of the keep_open attribute and _open_reader attribute is well implemented. The default value for keep_open is set to True, which aligns with the PR summary. This will potentially improve performance when reading multiple frames.

  • 72-78: The docstring for the from_filename method has been updated to include the new keep_open parameter. This is a good practice as it keeps the documentation up-to-date with the code changes.

  • 86-101: The from_filename method now passes the keep_open argument to the MediaVideo and HDF5Video constructors. This is consistent with the PR summary and the changes made to these classes.

  • 290-299: The MediaVideo class now includes the keep_open attribute in its docstring, keeping the documentation up-to-date with the code changes.

  • 321-337: The reader property has been updated to cache the reader object if keep_open is True. This is consistent with the PR summary and should improve performance when reading multiple frames.

  • 359-371: The _read_frame method has been updated to use the cached reader object if keep_open is True. This is consistent with the PR summary and should improve performance when reading multiple frames.

  • 384-396: The _read_frames method has been updated to use the cached reader object if keep_open is True. This is consistent with the PR summary and should improve performance when reading multiple frames.

  • 440-449: The HDF5Video class now includes the keep_open attribute in its docstring, keeping the documentation up-to-date with the code changes.

  • 600-625: The _read_frame method has been updated to use the cached reader object if keep_open is True. This is consistent with the PR summary and should improve performance when reading multiple frames.

  • 637-665: The _read_frames method has been updated to use the cached reader object if keep_open is True. This is consistent with the PR summary and should improve performance when reading multiple frames.

Comment on lines 56 to 108
assert_equal(backend[-3:-1], backend.get_frames(range(1097, 1099)))


def test_mediavideo(centered_pair_low_quality_path):
@pytest.mark.parametrize("keep_open", [False, True])
def test_mediavideo(centered_pair_low_quality_path, keep_open):
backend = VideoBackend.from_filename(
centered_pair_low_quality_path, plugin="FFMPEG"
centered_pair_low_quality_path, plugin="FFMPEG", keep_open=keep_open
)
assert type(backend) == MediaVideo
assert backend.filename == centered_pair_low_quality_path
assert backend.shape == (1100, 384, 384, 1)
assert backend[0].shape == (384, 384, 1)
assert backend[:3].shape == (3, 384, 384, 1)
if keep_open:
assert backend._open_reader is not None
assert backend[0].shape == (384, 384, 1)
assert type(backend._open_reader).__name__ == "LegacyPlugin"

backend = VideoBackend.from_filename(centered_pair_low_quality_path, plugin="pyav")
backend = VideoBackend.from_filename(
centered_pair_low_quality_path, plugin="pyav", keep_open=keep_open
)
assert type(backend) == MediaVideo
assert backend.filename == centered_pair_low_quality_path
assert backend.shape == (1100, 384, 384, 1)
assert backend[0].shape == (384, 384, 1)
assert backend[:3].shape == (3, 384, 384, 1)
if keep_open:
assert backend._open_reader is not None
assert backend[0].shape == (384, 384, 1)
assert type(backend._open_reader).__name__ == "PyAVPlugin"

backend = VideoBackend.from_filename(
centered_pair_low_quality_path, plugin="opencv"
centered_pair_low_quality_path, plugin="opencv", keep_open=keep_open
)
assert type(backend) == MediaVideo
assert backend.filename == centered_pair_low_quality_path
assert backend.shape == (1100, 384, 384, 1)
assert backend[0].shape == (384, 384, 1)
assert backend[:3].shape == (3, 384, 384, 1)
if keep_open:
assert backend._open_reader is not None
assert backend[0].shape == (384, 384, 1)
assert type(backend._open_reader).__name__ == "VideoCapture"


def test_hdf5video_rank4(centered_pair_low_quality_path, tmp_path):
backend = VideoBackend.from_filename(centered_pair_low_quality_path)
@pytest.mark.parametrize("keep_open", [False, True])
def test_hdf5video_rank4(centered_pair_low_quality_path, tmp_path, keep_open):
backend = VideoBackend.from_filename(
centered_pair_low_quality_path, keep_open=keep_open
)
imgs = backend[:3]
assert imgs.shape == (3, 384, 384, 1)

Copy link
Contributor

Choose a reason for hiding this comment

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

Note: This review was outside of the patch, so it was mapped to the patch with the greatest overlap. Original lines [59-122]

The addition of the keep_open parameter to the test_mediavideo and test_hdf5video_rank4 functions is a good improvement. It allows testing the behavior of the VideoBackend class when the video reader is kept open between calls to read frames. The assertions added to check the _open_reader attribute when keep_open is True are also correct. However, there should be an else clause to assert that _open_reader is None when keep_open is False. This will ensure that the video reader is indeed closed after each call when keep_open is False.

  if keep_open:
      assert backend._open_reader is not None
      assert backend[0].shape == (384, 384, 1)
+ else:
+     assert backend._open_reader is None

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Review Status

Actionable comments generated: 0

Commits Files that changed from the base of the PR and between d56e11d and 1efa15c commits.
Files selected for processing (1)
  • tests/io/test_video_backends.py (3 hunks)
Additional comments (Suppressed): 3
tests/io/test_video_backends.py (3)
  • 7-7: The import of pytest is necessary for the new parameterized tests. This change is appropriate.

  • 59-114: The changes to the test_mediavideo and test_hdf5video_rank4 functions are well done. The use of pytest.mark.parametrize allows testing both scenarios where keep_open is True and False. The assertions checking the _open_reader attribute based on the keep_open value are correct and ensure that the video reader caching behavior works as expected.

  • 122-128: The addition of the keep_open check in the test_hdf5video_rank4 function is consistent with the changes made in other test functions. It ensures that the video reader caching behavior is tested in this scenario as well.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Review Status

Actionable comments generated: 1

Commits Files that changed from the base of the PR and between 1efa15c and 97fb1f8 commits.
Files selected for processing (2)
  • sleap_io/io/video.py (12 hunks)
  • tests/io/test_video_backends.py (3 hunks)
Additional comments (Suppressed): 9
tests/io/test_video_backends.py (3)
  • 4-10: The import of pytest has been added. Ensure that pytest is included in the project dependencies.

  • 59-104: The test function test_mediavideo has been updated to include a parameter for keep_open. The assertions have been updated to check the _open_reader attribute based on the value of keep_open. This is a good improvement as it tests both scenarios where keep_open is True and False.

  • 56-114: > Note: This review was outside of the patch, so it was mapped to the patch with the greatest overlap. Original lines [107-128]

The test function test_hdf5video_rank4 has been updated similarly to test_mediavideo, with the addition of the keep_open parameter and corresponding assertions. This ensures that the keep_open functionality is tested for different types of video backends.

sleap_io/io/video.py (6)
  • 31-34: The change from using cls.__attrs_attrs__ to attrs.fields(cls) is a good one. The latter is the recommended way to get attribute names in attrs classes, as it returns a list of Attribute objects which can be used to extract the name.

  • 44-57: The addition of the keep_open attribute and its default value being set to True is a significant change. This attribute controls whether the video reader should be kept open between calls to read frames, potentially improving performance for repeated reads. The reader is cached in the _open_reader attribute for subsequent use. Ensure that this change doesn't introduce any resource leaks where file handles are left open indefinitely.

  • 85-101: The from_filename method now accepts the keep_open parameter, which is passed down to the MediaVideo and HDF5Video constructors. This is consistent with the changes made to these classes.

  • 320-337: The new reader property has been added to handle the creation and caching of the video reader object based on the keep_open attribute. This is a good example of encapsulation, as it hides the complexity of managing the reader object behind a simple property.

  • 364-376: The _read_frame method has been updated to use the new reader property instead of creating a new reader each time. This should improve performance when reading multiple frames from the same video.

  • 406-424: > Note: This review was outside of the patch, so it was mapped to the patch with the greatest overlap. Original lines [389-421]

The _read_frames method has been updated in a similar way to the _read_frame method, using the reader property to manage the reader object. This is consistent with the changes made elsewhere in the code.

Comment on lines 338 to +351
@property
def num_frames(self) -> int:
"""Number of frames in the video."""
if self.plugin == "opencv":
return int(cv2.VideoCapture(self.filename).get(cv2.CAP_PROP_FRAME_COUNT))
return int(self.reader.get(cv2.CAP_PROP_FRAME_COUNT))
else:
return iio.improps(self.filename, plugin="pyav").shape[0]
props = iio.improps(self.filename, plugin=self.plugin)
n_frames = props.n_images
if np.isinf(n_frames):
legacy_reader = self.reader.legacy_get_reader()
# Note: This might be super slow for some videos, so maybe we should
# defer evaluation of this or give the user control over it.
n_frames = legacy_reader.count_frames()
return n_frames
Copy link
Contributor

Choose a reason for hiding this comment

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

The num_frames method has been updated to handle cases where the number of frames is infinite. This is a good improvement as it makes the code more robust to different types of videos. However, there's a comment indicating that counting frames might be slow for some videos. It would be beneficial to provide an option to control this behavior, perhaps by adding a timeout or a maximum number of frames to count.

@talmo talmo merged commit d4aa283 into main Sep 12, 2023
9 checks passed
@talmo talmo deleted the talmo/keep_video_handles_open branch September 12, 2023 21:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant