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

Extract clip as video, slp and pkg.slp #2059

Open
wants to merge 25 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c3a8e5b
add extrack clip and labels menu item
ericleonardis Dec 17, 2024
dcfa897
add export video clip and labels
ericleonardis Dec 17, 2024
39c17b5
test exportvideoclip for creating video, slp, and pkg
ericleonardis Dec 17, 2024
945d737
simplify dialogue menu by making new custom dialogue
ericleonardis Dec 17, 2024
1907daf
new custom export clip and labels dialogue
ericleonardis Dec 17, 2024
c31caa8
test length of lists in labels object
ericleonardis Dec 17, 2024
9f71476
change comments
ericleonardis Dec 17, 2024
117d5a3
reformatted linting with black
ericleonardis Dec 17, 2024
351919e
clean up double
ericleonardis Dec 17, 2024
fd1fc67
split clip pkg and clip pkg
ericleonardis Dec 17, 2024
08b40cf
add two menu items for clip video and clip pkg
ericleonardis Dec 17, 2024
ab58597
add error for no clip selected; remove imports
ericleonardis Dec 17, 2024
5f6fb07
add test for clip pkg
ericleonardis Dec 17, 2024
4c4ea5d
added edge case testing
ericleonardis Dec 17, 2024
c964f2a
throw error for single frame, needs selected range
ericleonardis Dec 17, 2024
ace2c85
add error hanlding for slp and package clip writing
ericleonardis Dec 17, 2024
ddcb213
skip progress bar in tests
ericleonardis Dec 17, 2024
2dccc28
remove unused import
ericleonardis Dec 17, 2024
4bc5422
fix type in ffmpeg error message
ericleonardis Dec 18, 2024
2d00bb8
node
ericleonardis Dec 18, 2024
f5c617f
expanded tests for gui dialogues
ericleonardis Dec 18, 2024
e972d3f
test for multiple videos
ericleonardis Dec 18, 2024
39cc496
map frame_to_indedx and find valid_frame_indices for labels
ericleonardis Dec 18, 2024
b529dd8
enable/disable based on whether clip is selected
ericleonardis Dec 18, 2024
cd866f3
make framerange == (0, video.frames) a valid choice
ericleonardis Dec 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions sleap/gui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,22 @@ def new_instance_menu_action():

labelMenu.addSeparator()

add_menu_item(
labelMenu,
"Extract clip and labels",
"Extract Clip and Labels...",
self.commands.exportClipVideo,
)

add_menu_item(
labelMenu,
"extract clip labels package",
"Extract Clip Labels Package...",
self.commands.exportClipPkg,
)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Lets enable/disable these based on whether a clip is selected in MainWindow._update_gui_state (which is called periodically while the GUI is open). For example:

self._menu_actions["delete clip predictions"].setEnabled(has_frame_range)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great suggestion! I have now changed these menu items to be enabled/disabled based on whether a clip is selected.


labelMenu.addSeparator()

add_menu_item(
labelMenu,
"add instances from all frame predictions",
Expand Down
246 changes: 237 additions & 9 deletions sleap/gui/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class which inherits from `AppCommand` (or a more specialized class such as
from sleap.gui.dialogs.merge import MergeDialog, ReplaceSkeletonTableDialog
from sleap.gui.dialogs.message import MessageDialog
from sleap.gui.dialogs.missingfiles import MissingFilesDialog
from sleap.gui.dialogs.export_clip import ExportClipAndLabelsDialog
ericleonardis marked this conversation as resolved.
Show resolved Hide resolved
from sleap.gui.state import GuiState
from sleap.gui.suggestions import VideoFrameSuggestions
from sleap.instance import Instance, LabeledFrame, Point, PredictedInstance, Track
Expand All @@ -57,10 +58,12 @@ class which inherits from `AppCommand` (or a more specialized class such as
from sleap.io.format.adaptor import Adaptor
from sleap.io.format.csv import CSVAdaptor
from sleap.io.format.ndx_pose import NDXPoseAdaptor
from sleap.io.video import Video
from sleap.io.video import Video, MediaVideo
from sleap.io.videowriter import VideoWriter
from sleap.skeleton import Node, Skeleton
from sleap.util import get_package_file


# Indicates whether we support multiple project windows (i.e., "open" opens new window)
OPEN_IN_NEW = True

Expand Down Expand Up @@ -627,6 +630,14 @@ def openPrereleaseVersion(self):
"""Open the current prerelease version."""
self.execute(OpenPrereleaseVersion)

def exportClipVideo(self):
"""Exports a selected range of video frames and their corresponding labels."""
self.execute(ExportClipVideo)

def exportClipPkg(self):
"""Exports a selected range of video frames and their corresponding labels."""
self.execute(ExportClipPkg)


# File Commands

Expand Down Expand Up @@ -3470,13 +3481,230 @@ def do_action(context: CommandContext, params: dict):
if rls is not None:
context.openWebsite(rls.url)

class ExportClipVideo(AppCommand):
@staticmethod
Comment on lines +3484 to +3485
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider creating a base class for clip export commands.

There's significant code duplication between ExportClipVideo and ExportClipPkg, particularly in frame range validation and labels extraction logic. Consider creating a base class to share common functionality.

class BaseClipExportCommand(AppCommand):
    @staticmethod
    def _validate_frame_range(frame_range: tuple, video) -> None:
        """Validates the frame range for clip export."""
        if frame_range[0] < 0 or frame_range[1] > video.frames:
            raise ValueError(f"Frame range {frame_range} is outside video bounds [0, {video.frames}]")
        if frame_range == (0, 1) or frame_range[0] == frame_range[1]:
            raise ValueError("No valid clip frame range selected! Please select a valid frame range using shift + click in the GUI.")

    @staticmethod
    def _extract_labeled_frames(video, labels, frame_range: tuple) -> Labels:
        """Extracts and remaps labeled frames for the given range."""
        frame_to_index = {lf.frame_idx: idx for idx, lf in enumerate(labels.labeled_frames) if lf.video == video}
        valid_frame_indices = [frame for frame in range(*frame_range) if frame in frame_to_index]
        
        if not valid_frame_indices:
            raise ValueError("No valid labeled frames found in the selected frame range.")
            
        pruned_labels = labels.extract(
            inds=[frame_to_index[frame] for frame in valid_frame_indices],
            copy=True
        )
        
        # Remap frame indices
        frame_offset = frame_range[0]
        for labeled_frame in pruned_labels.labeled_frames:
            labeled_frame.frame_idx -= frame_offset
            
        return pruned_labels

Also applies to: 3645-3646

def do_action(context: CommandContext, params: dict):
"""
Executes the export action for a video clip and its labels.

def copy_to_clipboard(text: str):
"""Copy a string to the system clipboard.
Args:
context (CommandContext): The application context containing the current state.
params (dict): Parameters for the export, including:
- 'filename' (str): The path to save the exported video.
- 'fps' (int): Frames per second for the exported video.
- 'open_when_done' (bool): Whether to open the video file after exporting.

Raises:
ValueError: If the frame range is invalid or no clip is selected.
RuntimeError: If there are issues with video writing or saving labels.
"""
# Extract video and labels from context
video = context.state["video"]
labels = context.state["labels"]

Args:
text: String to copy to clipboard.
"""
clipboard = QtWidgets.QApplication.clipboard()
clipboard.clear(mode=clipboard.Clipboard)
clipboard.setText(text, mode=clipboard.Clipboard)
# Ensure frame range is set; default to all frames if None
frame_range = context.state.get("frame_range", (0, video.frames))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe we say

Suggested change
frame_range = context.state.get("frame_range", (0, video.frames))
frame_range = context.state.get("frame_range", None)

and raise the no valid clip range error below if frame_range is None?


ericleonardis marked this conversation as resolved.
Show resolved Hide resolved
# Validate frame range
if frame_range[0] < 0 or frame_range[1] > video.frames:
raise ValueError(f"Frame range {frame_range} is outside video bounds [0, {video.frames}]")

# Check if clip is selected, raise error if no clip selected
if frame_range == (0, video.frames) or frame_range == (0, 1) or frame_range[0] == frame_range[1]:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why is frame_range == (0, video.frames) not a valid choice?

Copy link
Contributor Author

@ericleonardis ericleonardis Dec 20, 2024

Choose a reason for hiding this comment

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

I guess I thought a "clip" meant it wasn't the whole video, but that's just semantics. I went ahead and made that a valid choice.

Unfortunately this has lead to another discovery which has lead me to open a new issue. When I do select the maximum frame range from (0, video.frames) the GUI the selection and frame count display does not match the video. I have opened a new issue at #2074 to describe the mismatch between the video frames and the Shift + Click frame selection tool on the Timeline viewer.

When I select the full clip range I get the ValueError: Frame range (0, 1505) is outside video bounds [0, 1500]. So this PR depends on Issue #2074 being fixed before we can move forward with this one.

raise ValueError("No valid clip frame range selected! Please select a valid frame range using shift + click in the GUI.")

# Map frame indices to the actual labeled frame objects
frame_to_index = {lf.frame_idx: idx for idx, lf in enumerate(labels.labeled_frames) if lf.video == video}
valid_frame_indices = [frame for frame in range(*frame_range) if frame in frame_to_index]

if not valid_frame_indices:
raise ValueError("No valid labeled frames found in the selected frame range.")

# Extract only the selected frames into a new Labels object
pruned_labels = labels.extract(
inds=[frame_to_index[frame] for frame in valid_frame_indices],
copy=True # Ensures a deep copy of the extracted labels
)
Comment on lines +3516 to +3527
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
# Map frame indices to the actual labeled frame objects
frame_to_index = {lf.frame_idx: idx for idx, lf in enumerate(labels.labeled_frames) if lf.video == video}
valid_frame_indices = [frame for frame in range(*frame_range) if frame in frame_to_index]
if not valid_frame_indices:
raise ValueError("No valid labeled frames found in the selected frame range.")
# Extract only the selected frames into a new Labels object
pruned_labels = labels.extract(
inds=[frame_to_index[frame] for frame in valid_frame_indices],
copy=True # Ensures a deep copy of the extracted labels
)
inds = (video, range(frame_range))
# Extract only the selected frames into a new Labels object
pruned_labels = labels.extract(
inds=inds,
copy=True # Ensures a deep copy of the extracted labels
)

since we want to run into this case in Labels.extract -> Labels.__getitem__ -> Labels.get:

sleap/sleap/io/dataset.py

Lines 763 to 764 in 66d96ce

elif isinstance(key[1], (list, range)):
return self.find(video=key[0], frame_idx=key[1])

which will call this case of Labels.find -> Labels._cache.find_frames:

sleap/sleap/io/dataset.py

Lines 142 to 147 in 66d96ce

if isinstance(frame_idx, Iterable):
return [
self._frame_idx_map[video][idx]
for idx in frame_idx
if idx in self._frame_idx_map[video]
]

Comment on lines +3516 to +3527
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Revise labels extraction implementation.

The current implementation might not handle frame indices correctly. Consider using the suggested approach from the codebase maintainer.

-        # Map frame indices to the actual labeled frame objects
-        frame_to_index = {lf.frame_idx: idx for idx, lf in enumerate(labels.labeled_frames) if lf.video == video}
-        valid_frame_indices = [frame for frame in range(*frame_range) if frame in frame_to_index]
-
-        # Extract only the selected frames into a new Labels object
-        pruned_labels = labels.extract(
-            inds=[frame_to_index[frame] for frame in valid_frame_indices],
-            copy=True  # Ensures a deep copy of the extracted labels
-        )
+        # Extract frames using video and range
+        inds = (video, range(*frame_range))
+        pruned_labels = labels.extract(
+            inds=inds,
+            copy=True  # Ensures a deep copy of the extracted labels
+        )
📝 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
# Map frame indices to the actual labeled frame objects
frame_to_index = {lf.frame_idx: idx for idx, lf in enumerate(labels.labeled_frames) if lf.video == video}
valid_frame_indices = [frame for frame in range(*frame_range) if frame in frame_to_index]
if not valid_frame_indices:
raise ValueError("No valid labeled frames found in the selected frame range.")
# Extract only the selected frames into a new Labels object
pruned_labels = labels.extract(
inds=[frame_to_index[frame] for frame in valid_frame_indices],
copy=True # Ensures a deep copy of the extracted labels
)
# Extract frames using video and range
inds = (video, range(*frame_range))
pruned_labels = labels.extract(
inds=inds,
copy=True # Ensures a deep copy of the extracted labels
)


# Remap frame indices in pruned_labels to start from 0 while maintaining spacing
frame_offset = frame_range[0]
for labeled_frame in pruned_labels.labeled_frames:
labeled_frame.frame_idx -= frame_offset

# Initialize VideoWriter
height, width = video.height, video.width
fps = params["fps"]
writer = VideoWriter.safe_builder(params["filename"], height, width, fps)

# Conditionally show progress bar
show_progress = os.getenv("PYTEST_RUNNING") != "1"
if show_progress:
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([])
progress = QtWidgets.QProgressDialog("Exporting video...", "Cancel", 0, len(valid_frame_indices))
progress.setWindowModality(QtCore.Qt.WindowModal)
progress.setValue(0)
else:
progress = None # Progress bar disabled during tests

# Write frames to the video
try:
for idx, frame_idx in enumerate(valid_frame_indices):
if show_progress and progress.wasCanceled():
writer.close()
os.remove(params["filename"])
return

# Read and process frame
try:
frame = video.get_frame(frame_idx)
if frame.ndim == 2: # Grayscale frames
frame = cv2.cvtColor(frame, cv2.COLOR_GRAY2BGR)
writer.add_frame(frame, bgr=True)
except Exception as e:
writer.close()
os.remove(params["filename"])
raise RuntimeError(f"Failed to write frame {frame_idx}: {str(e)}")
ericleonardis marked this conversation as resolved.
Show resolved Hide resolved
Comment on lines +3563 to +3566
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve error handling with proper exception chaining.

Use exception chaining to preserve the original error context.

                 except Exception as e:
                     writer.close()
                     os.remove(params["filename"])
-                    raise RuntimeError(f"Failed to write frame {frame_idx}: {str(e)}")
+                    raise RuntimeError(f"Failed to write frame {frame_idx}: {str(e)}") from e
📝 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
except Exception as e:
writer.close()
os.remove(params["filename"])
raise RuntimeError(f"Failed to write frame {frame_idx}: {str(e)}")
except Exception as e:
writer.close()
os.remove(params["filename"])
raise RuntimeError(f"Failed to write frame {frame_idx}: {str(e)}") from e
🧰 Tools
🪛 Ruff (0.8.2)

3549-3549: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


# Update progress
if show_progress:
progress.setValue(idx + 1)
QtWidgets.QApplication.processEvents()

finally:
writer.close()
if show_progress:
progress.setValue(len(valid_frame_indices)) # Complete progress

# Create a new Video object for the output video
new_media_video = MediaVideo(
filename=params["filename"], grayscale=video.channels == 1, bgr=True
)
Comment on lines +3577 to +3581
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
# Create a new Video object for the output video
new_media_video = MediaVideo(
filename=params["filename"], grayscale=video.channels == 1, bgr=True
)
# Create a new Video object for the output video
new_media_video = MediaVideo(
filename=params["filename"], grayscale=video.grayscale, bgr=video.bgr
)

new_video = Video(backend=new_media_video)

# Update pruned labels to point to the new video
for labeled_frame in pruned_labels.labeled_frames:
labeled_frame.video = new_video

pruned_labels.videos = [new_video]

# Save the pruned labels
labels_filename = params["filename"].replace(".mp4", ".slp")
Comment on lines +3590 to +3591
Copy link
Collaborator

Choose a reason for hiding this comment

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

Use pathlib for path operations

Suggested change
# Save the pruned labels
labels_filename = params["filename"].replace(".mp4", ".slp")
# Save the pruned labels
labels_filename = str(Path(params["filename"]).with_suffix(".slp"))

try:
pruned_labels.save(labels_filename)
except Exception as e:
raise RuntimeError(f"Failed to save labels to {labels_filename}: {str(e)}")
ericleonardis marked this conversation as resolved.
Show resolved Hide resolved

# Open the video file when done, if specified
if params.get("open_when_done", False):
open_file(params["filename"])

@staticmethod
def ask(context: CommandContext, params: dict) -> bool:
"""
Asks the user for export parameters via a custom dialog.

Args:
context: Command context, providing state and application access.
params: A dictionary to populate with user-defined options.

Returns:
bool: True if the user confirmed the action, False if canceled.
"""
# Extract FPS from video metadata (fallback to 30 if unavailable)
video_fps = getattr(context.state["video"], "fps", 30)

# Initialize and show the export dialog
dialog = ExportClipAndLabelsDialog(video_fps=video_fps)
if dialog.exec_() != QtWidgets.QDialog.Accepted:
return False # User canceled

# Get user input from dialog
export_options = dialog.get_results()

# Prompt user to select output file
filename, _ = QtWidgets.QFileDialog.getSaveFileName(
None, "Save Clip As...", "", "Video (*.mp4 *.avi)"
)
if not filename:
return False # User canceled file selection

# Populate export parameters
params["filename"] = filename
params["fps"] = export_options["fps"]
params["open_when_done"] = export_options["open_when_done"]

# Access frame range
if context.state.get("frame_range"):
params["frames"] = range(*context.state["frame_range"])
else:
params["frames"] = range(context.state["video"].frames)

return True


class ExportClipPkg(AppCommand):
@staticmethod
def do_action(context: CommandContext, params: dict):
"""
Exports a pruned labels package based on selected frame range.

Args:
context (CommandContext): Contains state information like video and labels.
params (dict): Parameters including filename, fps, and open_when_done.
"""
# Extract video and labels from context
video = context.state["video"]
labels = context.state["labels"]

# Ensure frame range is set; default to all frames if None
frame_range = context.state.get("frame_range", (0, video.frames))

# Check if clip is selected, raise error if no clip selected
if frame_range == (0, video.frames) or frame_range == (0, 1) or frame_range[0] == frame_range[1]:
raise ValueError("No valid clip frame range selected! Please select a valid frame range using shift + click in the GUI.")

# Map frame indices to the actual labeled frame objects
frame_to_index = {lf.frame_idx: idx for idx, lf in enumerate(labels.labeled_frames) if lf.video == video}
valid_frame_indices = [frame for frame in range(*frame_range) if frame in frame_to_index]

if not valid_frame_indices:
raise ValueError("No valid labeled frames found in the selected frame range.")

# Extract only the selected frames into a new Labels object
pruned_labels = labels.extract(
inds=[frame_to_index[frame] for frame in valid_frame_indices],
copy=True # Ensures a deep copy of the extracted labels
)

# Remap frame indices in pruned_labels to start from 0
for labeled_frame in pruned_labels.labeled_frames:
labeled_frame.frame_idx -= frame_range[0]
Comment on lines +3666 to +3681
Copy link
Collaborator

Choose a reason for hiding this comment

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

A lot of this do_action is the same as the ExportClipVideo class... do you think there is a way we can maybe break out the subparts of do_action into class methods and either combine the ExportClipVideo and ExportClipPkg classes into one class OR create create a base class and subclass to re-use code?


try:
pkg_filename = params["filename"]
pruned_labels.save(pkg_filename, with_images=True)
except Exception as e:
raise RuntimeError(f"Failed to save labels pkg to {pkg_filename}.")
ericleonardis marked this conversation as resolved.
Show resolved Hide resolved
Comment on lines +3683 to +3687
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve error handling in package save operation.

  1. Use proper exception chaining
  2. Include the original error message
  3. Clean up the file on failure
         try:
             pkg_filename = params["filename"]
             pruned_labels.save(pkg_filename, with_images=True)
         except Exception as e:
-            raise RuntimeError(f"Failed to save labels pkg to {pkg_filename}.")
+            if os.path.exists(pkg_filename):
+                os.remove(pkg_filename)
+            raise RuntimeError(f"Failed to save labels pkg to {pkg_filename}: {str(e)}") from e
📝 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
try:
pkg_filename = params["filename"]
pruned_labels.save(pkg_filename, with_images=True)
except Exception as e:
raise RuntimeError(f"Failed to save labels pkg to {pkg_filename}.")
try:
pkg_filename = params["filename"]
pruned_labels.save(pkg_filename, with_images=True)
except Exception as e:
if os.path.exists(pkg_filename):
os.remove(pkg_filename)
raise RuntimeError(f"Failed to save labels pkg to {pkg_filename}: {str(e)}") from e
🧰 Tools
🪛 Ruff (0.8.2)

3661-3661: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)


3662-3662: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


@staticmethod
def ask(context: CommandContext, params: dict) -> bool:
"""
Asks the user for export parameters via a custom dialog.

Args:
context: Command context, providing state and application access.
params: A dictionary to populate with user-defined options.

Returns:
bool: True if the user confirmed the action, False if canceled.
"""

# Prompt user to select output file
filename, _ = QtWidgets.QFileDialog.getSaveFileName(
None, "Save Clip Package As...", "", "Label Package (*.pkg.slp)"
)
if not filename:
return False # User canceled file selection
# Update the params dictionary with the selected filename
params["filename"] = filename
return True
54 changes: 53 additions & 1 deletion sleap/gui/dialogs/export_clip.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""

from sleap.gui.dialogs.formbuilder import FormBuilderModalDialog
from qtpy import QtWidgets


class ExportClipDialog(FormBuilderModalDialog):
Expand All @@ -20,10 +21,61 @@ def __init__(self):
)
else:
message = (
"<i>Unable to use ffpmeg via imageio. "
"<i>Unable to use ffmpeg via imageio. "
"<b>AVI</b> file will be encoding using OpenCV.</i>"
)

self.add_message(message)

self.setWindowTitle("Export Clip Options")


class ExportClipAndLabelsDialog(FormBuilderModalDialog):
def __init__(self, video_fps=30):
from sleap.io.videowriter import VideoWriter

# Initialize with a blank widget (no YAML needed)
super().__init__(form_widget=QtWidgets.QWidget())

self.setWindowTitle("Export Clip Options")

# FPS Field
self.fps_input = QtWidgets.QSpinBox()
self.fps_input.setRange(1, 240)
self.fps_input.setValue(video_fps) # Set default FPS from video
self.add_message("Frames per second:")
self.layout().insertWidget(2, self.fps_input)

# Open when done Checkbox
self.open_when_done = QtWidgets.QCheckBox("Open file after saving")
self.layout().insertWidget(3, self.open_when_done)

# Video format message
can_use_ffmpeg = VideoWriter.can_use_ffmpeg()
if can_use_ffmpeg:
message = (
"<i><b>MP4</b> file will be encoded using "
"system ffmpeg via imageio (preferred option).</i>"
)
else:
message = (
"<i>Unable to use ffmpeg via imageio. "
"<b>AVI</b> file will be encoded using OpenCV.</i>"
)
self.add_message(message)

def _get_form_results(self):
"""Get form results as a dictionary."""
return {
"fps": self.fps_input.value(),
"open_when_done": self.open_when_done.isChecked(),
}

def on_accept(self):
"""Retrieve the form results and accept the dialog."""
self._results = self._get_form_results()
self.accept()

def get_results(self):
"""Get results as a dictionary."""
return self._get_form_results()
Loading