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

Add video writing #129

Merged
merged 4 commits into from
Oct 4, 2024
Merged

Add video writing #129

merged 4 commits into from
Oct 4, 2024

Conversation

talmo
Copy link
Contributor

@talmo talmo commented Oct 4, 2024

  • Add sio.VideoWriter: basic imageio-ffmpeg video writer with sensible H264 presets. This can be used as a context manager:
    with sio.VideoWriter("video.mp4") as vw:
        for frame in video:
            vw(frame)
  • Add sio.save_video: high-level video writing. This can be used to quickly write a set of frames or even a whole Video for easy (if inefficient) re-encoding:
    bad_video = sio.load_video("unseekable.avi")
    sio.save_video(bad_video, "seekable.mp4")
  • Added IndexError in VideoBackend to enable sequence protocol for iteration over Videos:
    for frame in video:
        pass
  • Refactored sio.io.video to sio.io.video_reading.

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced save_video functionality for saving video files.
    • Added VideoWriter class for structured video writing using customizable settings.
  • Bug Fixes

    • Enhanced error handling for frame index access in video reading.
  • Documentation

    • Updated documentation to include new video handling capabilities and formats.
  • Tests

    • Added tests for the new video writing functionality and improved error handling in video reading tests.

Copy link
Contributor

coderabbitai bot commented Oct 4, 2024

Walkthrough

The changes in this pull request introduce the save_video function to the sleap_io module, enhancing its capabilities for handling video files. Documentation updates include a new entry for this function in formats.md. Additionally, new classes and functions related to video reading and writing have been implemented, along with modifications to existing import paths and error handling in several modules. Tests have been added or updated to ensure that the new video functionalities are properly validated.

Changes

File Path Change Summary
docs/formats.md Added documentation for save_video function.
sleap_io/init.py Added save_video, VideoBackend, and VideoWriter imports.
sleap_io/io/init.py Imported video_reading module as video.
sleap_io/io/main.py Introduced save_video function; updated imports for video writing.
sleap_io/io/slp.py Updated import paths for video-related classes; refined make_video and embed_video functions.
sleap_io/io/video_reading.py Updated VideoBackend class methods for error handling; clarified module docstring.
sleap_io/io/video_writing.py Introduced VideoWriter class with methods for writing videos.
tests/io/test_main.py Added test_save_video to validate save_video functionality.
tests/io/test_slp.py Updated import paths for video classes.
tests/io/test_video_reading.py Updated import paths; added test for IndexError in get_frame.
tests/io/test_video_writing.py Introduced tests for VideoWriter class functionality.
tests/model/test_video.py Updated import paths for video classes.

Possibly related PRs

🐇 In the meadow, where videos play,
A new function hops in, hip-hip-hooray!
With frames and formats, it saves with glee,
A writer so clever, as clever can be!
Through tests and imports, it finds its way,
In the world of SLEAP, it's here to stay!
🌼


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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.

Actionable comments posted: 3

🧹 Outside diff range and nitpick comments (17)
docs/formats.md (2)

9-10: LGTM! Documentation updated correctly.

The addition of the sleap_io.save_video entry is consistent with the existing documentation structure and is placed logically after load_video. This update aligns well with the PR objective of introducing new video writing capabilities.

Consider adding a brief description or example usage of the save_video function to provide more context for users, similar to how other functions might be documented in the project.


Line range hint 1-24: Documentation updated to reflect new video writing capability.

The addition of the sleap_io.save_video entry in the documentation is consistent with the PR objectives and existing documentation structure. This update effectively communicates the new video writing capability to users.

To further improve the documentation:

  1. Consider grouping related functions (e.g., load_video and save_video) under a "Video Operations" section for better organization.
  2. If not already present elsewhere in the documentation, consider adding a brief overview of the supported data formats at the beginning of this file to provide context for new users.
tests/io/test_video_writing.py (1)

7-15: LGTM: Test function covers basic functionality. Consider adding more comprehensive tests.

The test_video_writer function effectively tests the basic functionality of the VideoWriter class. It writes frames to a video file and verifies the output's existence and shape. This is a good starting point for ensuring the video writing process works as expected.

To enhance the test coverage, consider the following suggestions:

  1. Verify the content of the written frames, not just the shape.
  2. Add error handling tests (e.g., writing invalid frames).
  3. Test edge cases (e.g., writing an empty video, very large frames).
  4. Test different output formats or encoding options if supported.

Would you like assistance in implementing these additional test cases?

sleap_io/__init__.py (2)

30-31: Consider documenting the purpose of new imports.

The new imports of VideoBackend and VideoWriter classes align with the PR objectives of adding video writing capabilities. However, they are currently flagged as unused by the static analysis tool.

If these imports are intended to be part of the public API, consider one of the following options:

  1. Add them to the __all__ list if you have one (not shown in this file).
  2. Use a redundant alias to silence the linter warning, e.g., VideoBackend as VideoBackend.
  3. Add a comment explaining why they are imported here if they are meant to be accessible but not directly used.

Example:

# Expose VideoBackend and VideoWriter as part of the public API
from sleap_io.io.video_reading import VideoBackend as VideoBackend
from sleap_io.io.video_writing import VideoWriter as VideoWriter
🧰 Tools
🪛 Ruff

30-30: sleap_io.io.video_reading.VideoBackend imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


31-31: sleap_io.io.video_writing.VideoWriter imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


Line range hint 1-31: Summary: Changes align with PR objectives, but some imports need clarification.

The modifications to sleap_io/__init__.py successfully introduce new video writing capabilities, aligning with the PR objectives. The addition of save_video, VideoBackend, and VideoWriter enhances the package's functionality for handling videos.

However, there are a few points that need attention:

  1. Some imported functions and classes are flagged as unused by the static analysis tool.
  2. The purpose of exposing VideoBackend and VideoWriter in the package's public API is not immediately clear.

Next steps:

  1. Review the usage of save_video, load_file, and save_file in the project and decide whether to keep them in the imports.
  2. Clarify the intention behind importing VideoBackend and VideoWriter and consider documenting their purpose if they are meant to be part of the public API.
  3. If these imports are intentional, consider using techniques to silence the linter warnings, such as adding them to __all__ or using redundant aliases.

These adjustments will improve the clarity and maintainability of the package's public API.

🧰 Tools
🪛 Ruff

23-23: sleap_io.io.main.load_jabs imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


24-24: sleap_io.io.main.save_jabs imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


25-25: sleap_io.io.main.load_video imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


26-26: sleap_io.io.main.save_video imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


27-27: sleap_io.io.main.load_file imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


28-28: sleap_io.io.main.save_file imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


30-30: sleap_io.io.video_reading.VideoBackend imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


31-31: sleap_io.io.video_writing.VideoWriter imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)

tests/io/test_main.py (1)

110-117: LGTM: Test function is well-structured and covers key functionality.

The test_save_video function effectively verifies the save_video functionality by:

  1. Saving a slice of the original video
  2. Loading and verifying the saved video's shape
  3. Re-saving the loaded video
  4. Loading the re-saved video and verifying its shape

This approach ensures that the save_video function works correctly for both original and loaded video data.

Consider enhancing the test by verifying the content of the saved video, not just its shape. This could involve comparing pixel values or checksums of the original and saved videos.

tests/io/test_video_reading.py (1)

59-61: LGTM: New test case for IndexError added.

The new test case correctly checks for an IndexError when attempting to access an out-of-range frame index, which is consistent with the PR objectives.

Consider adding a comment explaining why 1100 is out of range (e.g., "Video has 1100 frames, indexed 0-1099") to improve readability:

 with pytest.raises(IndexError):
+    # Video has 1100 frames, indexed 0-1099
     backend.get_frame(1100)
sleap_io/io/main.py (2)

153-178: LGTM: Well-implemented save_video function.

The new save_video function is well-designed and implemented. It aligns with the PR objectives and provides a high-level interface for video writing. The docstring is comprehensive, and the implementation is concise and follows good practices.

A minor suggestion for improvement:

Consider adding type hinting for the **kwargs to improve code readability and IDE support. You can use TypedDict to define the expected keyword arguments:

from typing import TypedDict

class VideoWriterKwargs(TypedDict, total=False):
    fps: int
    pixelformat: str
    codec: str
    crf: int
    preset: str
    output_params: list[str]

def save_video(frames: np.ndarray | Video, filename: str | Path, **kwargs: VideoWriterKwargs):
    ...

This change would provide better type information for users of the function.


Line range hint 181-270: Existing functions unaffected, potential for enhancement.

The addition of the save_video function doesn't impact existing I/O operations. The load_file and save_file functions remain unchanged and can implicitly support the new video format through their existing logic.

Consider enhancing the save_file function to explicitly support video saving:

def save_file(
    data: Labels | np.ndarray | Video,
    filename: str | Path,
    format: Optional[str] = None,
    **kwargs
):
    ...
    elif format == "video":
        save_video(data, filename, **kwargs)
    ...

This would provide a consistent interface for saving different types of data, including videos.

sleap_io/io/video_reading.py (2)

215-220: Improved frame index handling and error checking.

The changes to the get_frame method enhance its robustness and flexibility:

  1. An IndexError is now raised for out-of-range frame indices, providing clear error messages.
  2. Support for negative indices allows for more intuitive frame access, similar to Python's list indexing.

These improvements make the method more user-friendly and less prone to silent errors.

Consider adding a comment explaining the behavior of negative indices for clarity:

 if frame_idx < 0:
+    # Support negative indexing (e.g., -1 for the last frame)
     frame_idx = frame_idx % len(self)

Line range hint 1-1000: Consider standardizing error handling across backend classes.

While reviewing the file, I noticed that the error handling improvements made to the VideoBackend class could be beneficial if applied consistently across all backend classes (MediaVideo, HDF5Video, and ImageVideo).

Consider implementing similar index validation and negative index support in the _read_frame methods of each backend class. This would ensure consistent behavior across all video backends and improve the overall robustness of the library.

For example, in the MediaVideo class:

def _read_frame(self, frame_idx: int) -> np.ndarray:
    if frame_idx >= self.num_frames:
        raise IndexError(f"Frame index {frame_idx} out of range.")
    if frame_idx < 0:
        frame_idx = frame_idx % self.num_frames
    
    # Existing implementation...

Apply similar changes to HDF5Video and ImageVideo classes for consistency.

sleap_io/io/video_writing.py (2)

66-69: Consider adding exception handling when opening the video writer.

If an error occurs during the initialization of the video writer (e.g., invalid codec or file path issues), it will raise an uncaught exception. Adding exception handling can provide more informative error messages and allow for graceful degradation or cleanup.

You might wrap the writer initialization in a try-except block:

 def open(self):
     """Open the video writer."""
     self.close()

     self.filename.parent.mkdir(parents=True, exist_ok=True)
+    try:
         self._writer = iio_v2.get_writer(
             self.filename.as_posix(),
             format="FFMPEG",
             fps=self.fps,
             codec=self.codec,
             output_params=self.build_output_params(),
         )
+    except Exception as e:
+        raise RuntimeError(f"Failed to open video writer: {e}")

43-51: Ensure all class attributes are documented in the docstring.

Attributes like output_params and _writer are present in the class but not described in the class docstring. Including all attributes in the documentation improves code readability and maintainability.

Consider updating the docstring:

     Attributes:
         filename: Path to output video file.
         fps: Frames per second. Defaults to 30.
         pixelformat: Pixel format for video. Defaults to "yuv420p".
         codec: Codec to use for encoding. Defaults to "libx264".
         crf: Constant rate factor to control lossiness of video. Values go from 2 to 32,
             with numbers in the 18 to 30 range being most common. Lower values mean less
             compressed/higher quality. Defaults to 25. No effect if codec is not
             "libx264".
         preset: H264 encoding preset. Defaults to "superfast". No effect if codec is not
             "libx264".
+        output_params: Additional output parameters for FFMPEG. Defaults to empty list.
+        _writer: Internal imageio writer object. Initialized in `open` method.
sleap_io/io/slp.py (4)

Line range hint 188-213: Refactor Image Encoding Logic in embed_video Function

The current implementation uses different methods to encode images based on the availability of cv2, leading to code duplication. Consider refactoring to improve maintainability:

if image_format == "hdf5":
    img_data = frame
else:
    if frame.shape[-1] == 1:
        frame = frame.squeeze(axis=-1)
    encode_extension = "." + image_format
    if "cv2" in sys.modules:
        _, buffer = cv2.imencode(encode_extension, frame)
        img_data = buffer.astype("int8")
    else:
        img_data_bytes = iio.imwrite("<bytes>", frame, extension=encode_extension)
        img_data = np.frombuffer(img_data_bytes, dtype="int8")

Applying this refactor reduces code duplication and unifies the image encoding logic.


Line range hint 223-225: Handle Potential ValueError When Replacing Videos

In the embed_frames function:

video_ind = labels.videos.index(video)

If video is not present in labels.videos, this line can raise a ValueError. To ensure robustness, add a check or exception handling:

Apply this diff to handle the potential error:

+    try:
        video_ind = labels.videos.index(video)
+    except ValueError:
+        raise ValueError(f"Video {video} not found in labels.videos.")

Line range hint 265-268: Clarify the Use of the GROUP Variable in write_suggestions

The variable GROUP is hardcoded to 0, and a TODO comment indicates handling extraneous metadata:

GROUP = 0  # TODO: Handle storing extraneous metadata.

Consider implementing the logic for storing metadata or removing the unused variable if it's not needed.


Line range hint 742-744: Correct Docstring in write_lfs Function

In the docstring for write_lfs, there's a minor typo:

-    labels: A `Labels` object to store the metadata for.
+    labels: A `Labels` object containing the labeled frames to write.
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 2aa891d and dddf2dc.

📒 Files selected for processing (13)
  • docs/formats.md (1 hunks)
  • sleap_io/init.py (1 hunks)
  • sleap_io/io/init.py (1 hunks)
  • sleap_io/io/main.py (2 hunks)
  • sleap_io/io/slp.py (1 hunks)
  • sleap_io/io/video_reading.py (2 hunks)
  • sleap_io/io/video_writing.py (1 hunks)
  • sleap_io/model/video.py (1 hunks)
  • tests/io/test_main.py (2 hunks)
  • tests/io/test_slp.py (1 hunks)
  • tests/io/test_video_reading.py (2 hunks)
  • tests/io/test_video_writing.py (1 hunks)
  • tests/model/test_video.py (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • sleap_io/model/video.py
  • tests/io/test_slp.py
🧰 Additional context used
🪛 Ruff
sleap_io/__init__.py

26-26: sleap_io.io.main.save_video imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


27-27: sleap_io.io.main.load_file imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


28-28: sleap_io.io.main.save_file imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


30-30: sleap_io.io.video_reading.VideoBackend imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


31-31: sleap_io.io.video_writing.VideoWriter imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)

sleap_io/io/__init__.py

3-3: .video_reading imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)

🔇 Additional comments (17)
tests/io/test_video_writing.py (2)

1-5: LGTM: File structure and imports are well-organized.

The file structure is clean and follows good practices:

  • Clear docstring describing the file's purpose
  • Appropriate imports for the test requirements
  • Good separation between imports and the test function

1-15: Overall, the test file is well-structured and provides a good foundation for testing video writing functionality.

The test_video_writer function effectively tests the basic operation of the VideoWriter class. It demonstrates the ability to write frames to a video file and verify the output's existence and basic properties.

To further strengthen the test suite, consider expanding it with additional test cases as suggested in the previous comment. This will help ensure the robustness and reliability of the video writing functionality across various scenarios.

sleap_io/__init__.py (2)

26-26: LGTM: New save_video function added to I/O operations.

The addition of the save_video function to the list of I/O operations enhances the video handling capabilities of the package. This aligns with the PR objectives of introducing new video writing functionality.

🧰 Tools
🪛 Ruff

26-26: sleap_io.io.main.save_video imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


26-28: Verify the necessity of imported functions.

The static analysis tool flags save_video, load_file, and save_file as unused imports.

Let's verify if these functions are used elsewhere in the project:

If these functions are not used elsewhere, consider removing them from the imports or adding them to __all__ if they are intended to be part of the public API.

🧰 Tools
🪛 Ruff

26-26: sleap_io.io.main.save_video imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


27-27: sleap_io.io.main.load_file imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


28-28: sleap_io.io.main.save_file imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)

tests/io/test_main.py (1)

14-14: LGTM: Import statement addition is correct.

The addition of save_video to the import list is consistent with the PR objectives and is correctly placed within the existing import structure.

tests/model/test_video.py (2)

Line range hint 1-224: Verify consistency of video_reading module with previous video module.

While the import statement has been updated, the rest of the test file remains unchanged. This suggests that the refactoring maintains backwards compatibility, which is good. However, it's important to ensure that the behavior of MediaVideo and ImageVideo in the new video_reading module is consistent with their previous implementation in the video module.

To verify this consistency, please review the video_reading module implementation:

#!/bin/bash
# Description: Display the content of the video_reading module

cat sleap_io/io/video_reading.py

Compare this with the previous video module implementation:

#!/bin/bash
# Description: Display the content of the previous video module

git show HEAD^:sleap_io/io/video.py

Ensure that the MediaVideo and ImageVideo classes maintain the same interface and behavior in the new module.


4-4: LGTM! Verify test suite execution.

The import statement has been updated to reflect the new module structure mentioned in the PR objectives. This change from sleap_io.io.video to sleap_io.io.video_reading aligns with the refactoring of the existing module structure.

Please ensure that all tests in this file still pass after this change. Run the following command to verify:

tests/io/test_video_reading.py (3)

1-1: LGTM: Module documentation updated correctly.

The module documentation has been updated to reflect the new module name sleap_io.io.video_reading, which is consistent with the PR objectives of refactoring the module structure.


3-3: LGTM: Import statement updated correctly.

The import statement has been updated to import from sleap_io.io.video_reading, which is consistent with the PR objectives of refactoring the module structure.


Line range hint 1-61: Summary: File updates align with PR objectives and improve test coverage.

The changes in this file correctly implement the refactoring of the module structure from sleap_io.io.video to sleap_io.io.video_reading. The new test case for IndexError enhances the error handling coverage. These updates contribute to better organization and robustness of the codebase.

sleap_io/io/main.py (2)

5-5: LGTM: Import statements updated correctly.

The new imports for video_writing and numpy are appropriately added and necessary for the new save_video function.

Also applies to: 8-8


Line range hint 1-270: Summary: Excellent addition of video writing capabilities.

The changes in this file successfully implement the PR objectives by adding video writing capabilities. The new save_video function is well-designed and doesn't disrupt existing functionality. The code is clean, well-documented, and follows good practices.

Key points:

  1. New imports are correctly added.
  2. The save_video function provides a high-level interface for video writing.
  3. Existing functions remain intact and can implicitly support the new video format.

Consider the suggested enhancements to further improve type hinting and consistency across I/O operations.

Overall, this is a valuable addition to the codebase that enhances its capabilities while maintaining its structure and readability.

sleap_io/io/video_reading.py (1)

Line range hint 1-1000: Overall improvement in video reading functionality.

The changes made to the sleap_io/io/video_reading.py file, particularly in the VideoBackend class, enhance the robustness and usability of the video reading functionality. The improved frame index handling and error checking in the get_frame method provide a more user-friendly experience and reduce the likelihood of silent errors.

The file maintains its well-structured and comprehensive implementations for various video backends. The suggested improvements for consistency across backend classes and the minor nitpick for code clarity would further enhance the overall quality of the implementation.

These changes align well with the PR objectives of improving the video handling capabilities of the project.

sleap_io/io/video_writing.py (1)

98-110: Verify the handling of exceptions in context manager methods.

Ensure that any exceptions raised within the context manager are appropriately handled or propagated. The current implementation returns False in __exit__, which will not suppress exceptions, keeping standard behavior.

If you'd like, I can help review the exception handling strategy for your context manager or provide examples of best practices.

sleap_io/io/slp.py (3)

23-23: Import Path Updated to Reflect Module Restructuring

The import statement has been updated to reflect the new module structure:

from sleap_io.io.video_reading import VideoBackend, ImageVideo, MediaVideo, HDF5Video

This change aligns with the refactoring of sleap_io.io.video to sleap_io.io.video_reading.


Line range hint 506-526: Potential Misalignment in Node and Edge Serialization

In the serialize_skeletons function, ensure that node IDs and indices are correctly mapped when serializing skeletons. Misalignment may cause incorrect node references in the serialized data.

Run the following script to check for correct node and edge mappings:

#!/bin/bash
# Description: Verify node IDs and indices alignment in serialized skeletons.

# Test: Extract node IDs and ensure they match node indices in skeletons.
ast-grep --lang python --pattern '
def serialize_skeletons(skeletons: list[Skeleton]) -> $_:
    $$$
' sleap_io/io/slp.py -A 20 | grep -E 'node_to_id|nodes_dicts.append'

Line range hint 152-164: Verify Correct Handling of Embedded Videos in write_videos Function

In the write_videos function, the logic for handling already embedded videos has been enhanced. Ensure that the check for existing embedded videos functions correctly, especially when re-embedding is needed:

if Path(labels_path).exists():
    with h5py.File(labels_path, "r") as f:
        already_embedded = f"video{video_ind}/video" in f

Run the following script to confirm that embedded videos are correctly identified:

Comment on lines +52 to +65
def build_output_params(self) -> list[str]:
"""Build the output parameters for FFMPEG."""
output_params = []
if self.codec == "libx264":
output_params.extend(
[
"-crf",
str(self.crf),
"-preset",
self.preset,
]
)
return output_params + self.output_params

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Refactor build_output_params for extensibility.

Currently, build_output_params only adjusts parameters for the libx264 codec. To support additional codecs in the future, consider generalizing this method or adding conditions for other codecs.

For example:

 def build_output_params(self) -> list[str]:
     """Build the output parameters for FFMPEG."""
     output_params = []
-    if self.codec == "libx264":
+    if self.codec in ("libx264", "h264"):
         output_params.extend(
             [
                 "-crf",
                 str(self.crf),
                 "-preset",
                 self.preset,
             ]
         )
+    # Add additional codec-specific parameters here
     return output_params + self.output_params
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def build_output_params(self) -> list[str]:
"""Build the output parameters for FFMPEG."""
output_params = []
if self.codec == "libx264":
output_params.extend(
[
"-crf",
str(self.crf),
"-preset",
self.preset,
]
)
return output_params + self.output_params
def build_output_params(self) -> list[str]:
"""Build the output parameters for FFMPEG."""
output_params = []
if self.codec in ("libx264", "h264"):
output_params.extend(
[
"-crf",
str(self.crf),
"-preset",
self.preset,
]
)
# Add additional codec-specific parameters here
return output_params + self.output_params

Comment on lines +85 to +95
def write_frame(self, frame: np.ndarray):
"""Write a frame to the video.

Args:
frame: Frame to write to video. Should be a 2D or 3D numpy array with
dimensions (height, width) or (height, width, channels).
"""
if self._writer is None:
self.open()

self._writer.append_data(frame)
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add input validation for frame in write_frame method.

To enhance robustness, consider validating that frame is a NumPy array with appropriate dimensions and data type before appending it to the writer. This can help catch unexpected inputs early.

You might include checks like:

 def write_frame(self, frame: np.ndarray):
     """Write a frame to the video.

     Args:
         frame: Frame to write to video. Should be a 2D or 3D numpy array with
             dimensions (height, width) or (height, width, channels).
     """
     if self._writer is None:
         self.open()

+    if not isinstance(frame, np.ndarray):
+        raise TypeError("Frame must be a NumPy ndarray.")
+    if frame.ndim not in (2, 3):
+        raise ValueError("Frame must be a 2D or 3D array.")

     self._writer.append_data(frame)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def write_frame(self, frame: np.ndarray):
"""Write a frame to the video.
Args:
frame: Frame to write to video. Should be a 2D or 3D numpy array with
dimensions (height, width) or (height, width, channels).
"""
if self._writer is None:
self.open()
self._writer.append_data(frame)
def write_frame(self, frame: np.ndarray):
"""Write a frame to the video.
Args:
frame: Frame to write to video. Should be a 2D or 3D numpy array with
dimensions (height, width) or (height, width, channels).
"""
if self._writer is None:
self.open()
if not isinstance(frame, np.ndarray):
raise TypeError("Frame must be a NumPy ndarray.")
if frame.ndim not in (2, 3):
raise ValueError("Frame must be a 2D or 3D array.")
self._writer.append_data(frame)


filename: Path = attrs.field(converter=Path)
fps: float = 30
pixelformat: str = "yuv420p"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

The pixelformat attribute is unused in the open method.

The pixelformat attribute is defined but not passed to get_writer in the open method. If specifying the pixel format is important for your video output, consider passing it to ensure the correct format is used.

Apply this diff to include pixelformat in get_writer:

 self._writer = iio_v2.get_writer(
     self.filename.as_posix(),
     format="FFMPEG",
     fps=self.fps,
     codec=self.codec,
+    pixelformat=self.pixelformat,
     output_params=self.build_output_params(),
 )

Committable suggestion was skipped due to low confidence.

Copy link

codecov bot commented Oct 4, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 96.48%. Comparing base (2aa891d) to head (9226fcc).
Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #129      +/-   ##
==========================================
+ Coverage   96.38%   96.48%   +0.09%     
==========================================
  Files          15       17       +2     
  Lines        2049     2107      +58     
==========================================
+ Hits         1975     2033      +58     
  Misses         74       74              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

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.

Actionable comments posted: 2

🧹 Outside diff range and nitpick comments (1)
sleap_io/io/video_writing.py (1)

80-110: Consider adding input validation to write_frame method.

The close, write_frame, __enter__, and __exit__ methods are implemented correctly and provide the necessary functionality for the VideoWriter class. However, as suggested in a previous review comment, consider adding input validation for the frame parameter in the write_frame method to enhance robustness and catch unexpected inputs early.

Example of input validation:

def write_frame(self, frame: np.ndarray):
    if not isinstance(frame, np.ndarray):
        raise TypeError("Frame must be a NumPy ndarray.")
    if frame.ndim not in (2, 3):
        raise ValueError("Frame must be a 2D or 3D array.")
    
    if self._writer is None:
        self.open()
    
    self._writer.append_data(frame)

This validation will help prevent potential issues with incorrect input types or shapes.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between dddf2dc and 9226fcc.

📒 Files selected for processing (2)
  • sleap_io/io/video_reading.py (4 hunks)
  • sleap_io/io/video_writing.py (1 hunks)
🔇 Additional comments (7)
sleap_io/io/video_writing.py (6)

13-50: LGTM: Well-structured class definition with appropriate attributes.

The VideoWriter class is well-defined using attrs.define, which simplifies the class implementation. The attributes cover essential parameters for video writing, and their default values seem reasonable. The use of Path for the filename and the lazy initialization of the _writer attribute are good practices.


52-65: Skipping comment generation.

An existing review comment already addresses the extensibility concern for the build_output_params method.


66-78: LGTM: Improved open method with addressed concerns.

The open method has been implemented well. It correctly handles directory creation and properly closes any existing writer before opening a new one. The pixelformat attribute is now being passed to get_writer, addressing a previous review comment about its usage.


112-119: LGTM: Well-implemented __call__ method.

The __call__ method is a good addition to the class. It provides a convenient way to write frames by allowing the class instance to be called directly with a frame. This implementation enhances the usability of the VideoWriter class and is consistent with its purpose.


1-119: Overall: Well-implemented VideoWriter class with minor improvement opportunities.

The VideoWriter class provides a comprehensive and user-friendly interface for video writing operations using imageio and FFMPEG. The implementation is generally well-structured, follows good practices, and includes useful features such as context manager support.

Key strengths:

  1. Clear and descriptive class and method docstrings
  2. Use of attrs for simplified class definition
  3. Proper resource management with open and close methods
  4. Context manager support for ease of use

Areas for potential improvement:

  1. Input validation in the write_frame method
  2. Extensibility of the build_output_params method for different codecs

Overall, this is a solid implementation that provides a valuable addition to the project's video writing capabilities.


7-8: Verify the need for both imageio imports.

The code imports both imageio and imageio.v2 as iio_v2. While this might be intentional, it's worth confirming if both imports are necessary. If possible, consider using only one version of imageio to maintain consistency and avoid potential version conflicts.

sleap_io/io/video_reading.py (1)

226-228: Good Addition of Frame Index Validation in get_frame

The addition of the frame index validation in get_frame (lines 226-228) enhances the robustness of the method by ensuring that an IndexError is raised when an invalid frame index is requested. This provides clearer error handling and prevents potential downstream errors.

Comment on lines +637 to +650
def has_frame(self, frame_idx: int) -> bool:
"""Check if a frame index is contained in the video.

Args:
frame_idx: Index of frame to check.

Returns:
`True` if the index is contained in the video, otherwise `False`.
"""
if self.frame_map:
return frame_idx in self.frame_map
else:
return frame_idx < len(self)

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Handle Negative Frame Indices in has_frame Method for HDF5Video

In the HDF5Video subclass, the has_frame method (lines 637-650) does not handle negative frame indices. Negative indices may result in unintended behavior, especially when accessing frames via frame_map.

Consider modifying the method to ensure that frame_idx is non-negative before performing the check:

def has_frame(self, frame_idx: int) -> bool:
    """Check if a frame index is contained in the video.

    Args:
        frame_idx: Index of frame to check.

    Returns:
        `True` if the index is contained in the video, otherwise `False`.
    """
+   if frame_idx < 0:
+       return False
    if self.frame_map:
        return frame_idx in self.frame_map
    else:
        return frame_idx < len(self)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def has_frame(self, frame_idx: int) -> bool:
"""Check if a frame index is contained in the video.
Args:
frame_idx: Index of frame to check.
Returns:
`True` if the index is contained in the video, otherwise `False`.
"""
if self.frame_map:
return frame_idx in self.frame_map
else:
return frame_idx < len(self)
def has_frame(self, frame_idx: int) -> bool:
"""Check if a frame index is contained in the video.
Args:
frame_idx: Index of frame to check.
Returns:
`True` if the index is contained in the video, otherwise `False`.
"""
if frame_idx < 0:
return False
if self.frame_map:
return frame_idx in self.frame_map
else:
return frame_idx < len(self)

Comment on lines +196 to +206
def has_frame(self, frame_idx: int) -> bool:
"""Check if a frame index is contained in the video.

Args:
frame_idx: Index of frame to check.

Returns:
`True` if the index is contained in the video, otherwise `False`.
"""
return frame_idx < len(self)

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Handle Negative Frame Indices in has_frame Method

The has_frame method (lines 196-206) in VideoBackend does not account for negative frame indices. Since negative indices can lead to unexpected behavior or errors when accessing frames, it's important to ensure that only valid, non-negative indices are considered.

Consider updating the condition to include a check for non-negative indices:

def has_frame(self, frame_idx: int) -> bool:
    """Check if a frame index is contained in the video.

    Args:
        frame_idx: Index of frame to check.

    Returns:
        `True` if the index is contained in the video, otherwise `False`.
    """
-   return frame_idx < len(self)
+   return 0 <= frame_idx < len(self)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def has_frame(self, frame_idx: int) -> bool:
"""Check if a frame index is contained in the video.
Args:
frame_idx: Index of frame to check.
Returns:
`True` if the index is contained in the video, otherwise `False`.
"""
return frame_idx < len(self)
def has_frame(self, frame_idx: int) -> bool:
"""Check if a frame index is contained in the video.
Args:
frame_idx: Index of frame to check.
Returns:
`True` if the index is contained in the video, otherwise `False`.
"""
return 0 <= frame_idx < len(self)

@talmo talmo merged commit 0990f1a into main Oct 4, 2024
9 checks passed
@talmo talmo deleted the talmo/video-writer branch October 4, 2024 05:51
@coderabbitai coderabbitai bot mentioned this pull request Oct 4, 2024
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