Add support for images

This commit is contained in:
2025-09-05 09:21:18 +02:00
parent 4ffd4cd321
commit 0adcc8f32a

View File

@@ -47,6 +47,9 @@ class VideoEditor:
# Supported video extensions # Supported video extensions
VIDEO_EXTENSIONS = {".mp4", ".avi", ".mov", ".mkv", ".wmv", ".flv", ".webm", ".m4v"} VIDEO_EXTENSIONS = {".mp4", ".avi", ".mov", ".mkv", ".wmv", ".flv", ".webm", ".m4v"}
# Supported image extensions
IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".bmp", ".tiff", ".tif", ".webp", ".jp2", ".pbm", ".pgm", ".ppm", ".sr", ".ras"}
# Crop adjustment settings # Crop adjustment settings
CROP_SIZE_STEP = 15 # pixels to expand/contract crop CROP_SIZE_STEP = 15 # pixels to expand/contract crop
@@ -56,15 +59,18 @@ class VideoEditor:
# Video file management # Video file management
self.video_files = [] self.video_files = []
self.current_video_index = 0 self.current_video_index = 0
# Media type tracking
self.is_image_mode = False # True if current file is an image
# Determine if path is file or directory # Determine if path is file or directory
if self.path.is_file(): if self.path.is_file():
self.video_files = [self.path] self.video_files = [self.path]
elif self.path.is_dir(): elif self.path.is_dir():
# Load all video files from directory # Load all media files from directory
self.video_files = self._get_video_files_from_directory(self.path) self.video_files = self._get_media_files_from_directory(self.path)
if not self.video_files: if not self.video_files:
raise ValueError(f"No video files found in directory: {path}") raise ValueError(f"No media files found in directory: {path}")
else: else:
raise ValueError(f"Path does not exist: {path}") raise ValueError(f"Path does not exist: {path}")
@@ -122,6 +128,18 @@ class VideoEditor:
# Crop adjustment settings # Crop adjustment settings
self.crop_size_step = self.CROP_SIZE_STEP self.crop_size_step = self.CROP_SIZE_STEP
def _is_video_file(self, file_path: Path) -> bool:
"""Check if file is a supported video format"""
return file_path.suffix.lower() in self.VIDEO_EXTENSIONS
def _is_image_file(self, file_path: Path) -> bool:
"""Check if file is a supported image format"""
return file_path.suffix.lower() in self.IMAGE_EXTENSIONS
def _is_media_file(self, file_path: Path) -> bool:
"""Check if file is a supported media format (video or image)"""
return self._is_video_file(file_path) or self._is_image_file(file_path)
def _get_next_edited_filename(self, video_path: Path) -> str: def _get_next_edited_filename(self, video_path: Path) -> str:
"""Generate the next available _edited_%03d filename""" """Generate the next available _edited_%03d filename"""
directory = video_path.parent directory = video_path.parent
@@ -145,92 +163,120 @@ class VideoEditor:
return f"{base_name}_edited_{next_number:03d}{extension}" return f"{base_name}_edited_{next_number:03d}{extension}"
def _get_video_files_from_directory(self, directory: Path) -> List[Path]: def _get_media_files_from_directory(self, directory: Path) -> List[Path]:
"""Get all video files from a directory, sorted by name""" """Get all media files (video and image) from a directory, sorted by name"""
video_files = set() media_files = set()
for file_path in directory.iterdir(): for file_path in directory.iterdir():
if ( if (
file_path.is_file() file_path.is_file()
and file_path.suffix.lower() in self.VIDEO_EXTENSIONS and self._is_media_file(file_path)
): ):
video_files.add(file_path) media_files.add(file_path)
# Pattern to match edited files: basename_edited_001.ext, basename_edited_002.ext, etc. # Pattern to match edited files: basename_edited_001.ext, basename_edited_002.ext, etc.
edited_pattern = re.compile(r"^(.+)_edited_\d{3}$") edited_pattern = re.compile(r"^(.+)_edited_\d{3}$")
edited_base_names = set() edited_base_names = set()
for file_path in video_files: for file_path in media_files:
match = edited_pattern.match(file_path.stem) match = edited_pattern.match(file_path.stem)
if match: if match:
edited_base_names.add(match.group(1)) edited_base_names.add(match.group(1))
non_edited_videos = set() non_edited_media = set()
for file_path in video_files: for file_path in media_files:
# Skip if this is an edited video # Skip if this is an edited file
if edited_pattern.match(file_path.stem): if edited_pattern.match(file_path.stem):
continue continue
# Skip if there's already an edited version of this video # Skip if there's already an edited version of this file
if file_path.stem in edited_base_names: if file_path.stem in edited_base_names:
continue continue
non_edited_videos.add(file_path) non_edited_media.add(file_path)
return sorted(non_edited_videos) return sorted(non_edited_media)
def _load_video(self, video_path: Path): def _load_video(self, media_path: Path):
"""Load a video file and initialize video properties""" """Load a media file (video or image) and initialize properties"""
if hasattr(self, "cap") and self.cap: if hasattr(self, "cap") and self.cap:
self.cap.release() self.cap.release()
self.video_path = video_path self.video_path = media_path
self.is_image_mode = self._is_image_file(media_path)
# Try different backends for better performance if self.is_image_mode:
# Order of preference: FFmpeg (best for video files), DirectShow (cameras), any available # Load static image
backends_to_try = [] self.static_image = cv2.imread(str(media_path))
if hasattr(cv2, 'CAP_FFMPEG'): # FFmpeg - best for video files if self.static_image is None:
backends_to_try.append(cv2.CAP_FFMPEG) raise ValueError(f"Could not load image file: {media_path}")
if hasattr(cv2, 'CAP_DSHOW'): # DirectShow - usually for cameras
backends_to_try.append(cv2.CAP_DSHOW) # Set up image properties to mimic video interface
backends_to_try.append(cv2.CAP_ANY) # Fallback self.frame_height, self.frame_width = self.static_image.shape[:2]
self.total_frames = 1
self.cap = None self.fps = 30 # Dummy FPS for image mode
for backend in backends_to_try: self.cap = None
try:
self.cap = cv2.VideoCapture(str(self.video_path), backend) print(f"Loaded image: {self.video_path.name}")
if self.cap.isOpened(): print(f" Resolution: {self.frame_width}x{self.frame_height}")
# Optimize buffer settings for better performance else:
self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # Minimize buffer to reduce latency # Try different backends for better performance
# Try to set hardware acceleration if available # Order of preference: FFmpeg (best for video files), DirectShow (cameras), any available
if hasattr(cv2, 'CAP_PROP_HW_ACCELERATION'): backends_to_try = []
self.cap.set(cv2.CAP_PROP_HW_ACCELERATION, cv2.VIDEO_ACCELERATION_ANY) if hasattr(cv2, 'CAP_FFMPEG'): # FFmpeg - best for video files
break backends_to_try.append(cv2.CAP_FFMPEG)
self.cap.release() if hasattr(cv2, 'CAP_DSHOW'): # DirectShow - usually for cameras
except: backends_to_try.append(cv2.CAP_DSHOW)
continue backends_to_try.append(cv2.CAP_ANY) # Fallback
if not self.cap or not self.cap.isOpened(): self.cap = None
raise ValueError(f"Could not open video file: {video_path}") for backend in backends_to_try:
try:
self.cap = cv2.VideoCapture(str(self.video_path), backend)
if self.cap.isOpened():
# Optimize buffer settings for better performance
self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # Minimize buffer to reduce latency
# Try to set hardware acceleration if available
if hasattr(cv2, 'CAP_PROP_HW_ACCELERATION'):
self.cap.set(cv2.CAP_PROP_HW_ACCELERATION, cv2.VIDEO_ACCELERATION_ANY)
break
self.cap.release()
except:
continue
if not self.cap or not self.cap.isOpened():
raise ValueError(f"Could not open video file: {media_path}")
# Video properties
self.total_frames = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT))
self.fps = self.cap.get(cv2.CAP_PROP_FPS)
self.frame_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
self.frame_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
# Video properties # Get codec information for debugging
self.total_frames = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT)) fourcc = int(self.cap.get(cv2.CAP_PROP_FOURCC))
self.fps = self.cap.get(cv2.CAP_PROP_FPS) codec = "".join([chr((fourcc >> 8 * i) & 0xFF) for i in range(4)])
self.frame_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
self.frame_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) # Get backend information
backend = self.cap.getBackendName()
print(f"Loaded video: {self.video_path.name} ({self.current_video_index + 1}/{len(self.video_files)})")
print(f" Codec: {codec} | Backend: {backend} | Resolution: {self.frame_width}x{self.frame_height}")
print(f" FPS: {self.fps:.2f} | Frames: {self.total_frames} | Duration: {self.total_frames/self.fps:.1f}s")
# Performance warning for known problematic cases
if codec in ['H264', 'H.264', 'AVC1', 'avc1'] and self.total_frames > 10000:
print(f" Warning: Large H.264 video detected - seeking may be slow")
if self.frame_width * self.frame_height > 1920 * 1080:
print(f" Warning: High resolution video - decoding may be slow")
if self.fps > 60:
print(f" Warning: High framerate video - may impact playback smoothness")
# Get codec information for debugging # Reset playback state for new media
fourcc = int(self.cap.get(cv2.CAP_PROP_FOURCC))
codec = "".join([chr((fourcc >> 8 * i) & 0xFF) for i in range(4)])
# Get backend information
backend = self.cap.getBackendName()
# Reset playback state for new video
self.current_frame = 0 self.current_frame = 0
self.is_playing = False self.is_playing = False if self.is_image_mode else False # Images start paused
self.playback_speed = 1.0 self.playback_speed = 1.0
self.current_display_frame = None self.current_display_frame = None
# Reset crop, zoom, rotation, brightness/contrast, and cut settings for new video # Reset crop, zoom, rotation, brightness/contrast, and cut settings for new media
self.crop_rect = None self.crop_rect = None
self.crop_history = [] self.crop_history = []
self.zoom_factor = 1.0 self.zoom_factor = 1.0
@@ -242,20 +288,6 @@ class VideoEditor:
self.cut_end_frame = None self.cut_end_frame = None
self.display_offset = [0, 0] self.display_offset = [0, 0]
print(
f"Loaded video: {self.video_path.name} ({self.current_video_index + 1}/{len(self.video_files)})"
)
print(f" Codec: {codec} | Backend: {backend} | Resolution: {self.frame_width}x{self.frame_height}")
print(f" FPS: {self.fps:.2f} | Frames: {self.total_frames} | Duration: {self.total_frames/self.fps:.1f}s")
# Performance warning for known problematic cases
if codec in ['H264', 'H.264', 'AVC1', 'avc1'] and self.total_frames > 10000:
print(f" Warning: Large H.264 video detected - seeking may be slow")
if self.frame_width * self.frame_height > 1920 * 1080:
print(f" Warning: High resolution video - decoding may be slow")
if self.fps > 60:
print(f" Warning: High framerate video - may impact playback smoothness")
def switch_to_video(self, index: int): def switch_to_video(self, index: int):
"""Switch to a specific video by index""" """Switch to a specific video by index"""
if 0 <= index < len(self.video_files): if 0 <= index < len(self.video_files):
@@ -275,12 +307,18 @@ class VideoEditor:
def load_current_frame(self) -> bool: def load_current_frame(self) -> bool:
"""Load the current frame into display cache""" """Load the current frame into display cache"""
self.cap.set(cv2.CAP_PROP_POS_FRAMES, self.current_frame) if self.is_image_mode:
ret, frame = self.cap.read() # For images, just copy the static image
if ret: self.current_display_frame = self.static_image.copy()
self.current_display_frame = frame
return True return True
return False else:
# For videos, seek and read frame
self.cap.set(cv2.CAP_PROP_POS_FRAMES, self.current_frame)
ret, frame = self.cap.read()
if ret:
self.current_display_frame = frame
return True
return False
def calculate_frame_delay(self) -> int: def calculate_frame_delay(self) -> int:
"""Calculate frame delay in milliseconds based on playback speed""" """Calculate frame delay in milliseconds based on playback speed"""
@@ -634,6 +672,10 @@ class VideoEditor:
def draw_timeline(self, frame): def draw_timeline(self, frame):
"""Draw timeline at the bottom of the frame""" """Draw timeline at the bottom of the frame"""
# Don't draw timeline for images
if self.is_image_mode:
return
height, width = frame.shape[:2] height, width = frame.shape[:2]
# Timeline background area # Timeline background area
@@ -753,7 +795,7 @@ class VideoEditor:
# Resize to fit window while maintaining aspect ratio # Resize to fit window while maintaining aspect ratio
height, width = display_frame.shape[:2] height, width = display_frame.shape[:2]
available_height = self.window_height - self.TIMELINE_HEIGHT available_height = self.window_height - (0 if self.is_image_mode else self.TIMELINE_HEIGHT)
scale = min(self.window_width / width, available_height / height) scale = min(self.window_width / width, available_height / height)
if scale < 1.0: if scale < 1.0:
@@ -790,7 +832,10 @@ class VideoEditor:
contrast_text = ( contrast_text = (
f" | Contrast: {self.contrast:.1f}" if self.contrast != 1.0 else "" f" | Contrast: {self.contrast:.1f}" if self.contrast != 1.0 else ""
) )
info_text = f"Frame: {self.current_frame}/{self.total_frames} | Speed: {self.playback_speed:.1f}x | Zoom: {self.zoom_factor:.1f}x{rotation_text}{brightness_text}{contrast_text} | {'Playing' if self.is_playing else 'Paused'}" if self.is_image_mode:
info_text = f"Image | Zoom: {self.zoom_factor:.1f}x{rotation_text}{brightness_text}{contrast_text}"
else:
info_text = f"Frame: {self.current_frame}/{self.total_frames} | Speed: {self.playback_speed:.1f}x | Zoom: {self.zoom_factor:.1f}x{rotation_text}{brightness_text}{contrast_text} | {'Playing' if self.is_playing else 'Paused'}"
cv2.putText( cv2.putText(
canvas, canvas,
info_text, info_text,
@@ -882,12 +927,13 @@ class VideoEditor:
# Draw progress bar (if visible) # Draw progress bar (if visible)
self.draw_progress_bar(canvas) self.draw_progress_bar(canvas)
cv2.imshow("Video Editor", canvas) window_title = "Image Editor" if self.is_image_mode else "Video Editor"
cv2.imshow(window_title, canvas)
def mouse_callback(self, event, x, y, flags, param): def mouse_callback(self, event, x, y, flags, param):
"""Handle mouse events""" """Handle mouse events"""
# Handle timeline interaction # Handle timeline interaction (not for images)
if self.timeline_rect: if self.timeline_rect and not self.is_image_mode:
bar_x_start, bar_y, bar_width, bar_height = self.timeline_rect bar_x_start, bar_y, bar_width, bar_height = self.timeline_rect
bar_x_end = bar_x_start + bar_width bar_x_end = bar_x_start + bar_width
@@ -905,7 +951,7 @@ class VideoEditor:
# Handle crop selection (Shift + click and drag) # Handle crop selection (Shift + click and drag)
if flags & cv2.EVENT_FLAG_SHIFTKEY: if flags & cv2.EVENT_FLAG_SHIFTKEY:
available_height = self.window_height - self.TIMELINE_HEIGHT available_height = self.window_height - (0 if self.is_image_mode else self.TIMELINE_HEIGHT)
if event == cv2.EVENT_LBUTTONDOWN: if event == cv2.EVENT_LBUTTONDOWN:
self.crop_selecting = True self.crop_selecting = True
@@ -1133,6 +1179,38 @@ class VideoEditor:
self.crop_rect = (new_x, y, new_w, h) self.crop_rect = (new_x, y, new_w, h)
def render_video(self, output_path: str): def render_video(self, output_path: str):
"""Render video or save image with current edits applied"""
if self.is_image_mode:
return self._render_image(output_path)
else:
return self._render_video(output_path)
def _render_image(self, output_path: str):
"""Save image with current edits applied"""
# Get the appropriate file extension
original_ext = self.video_path.suffix.lower()
if not output_path.endswith(original_ext):
output_path += original_ext
print(f"Saving image to {output_path}...")
# Apply all transformations to the image
processed_image = self.apply_crop_zoom_and_rotation(self.static_image.copy())
if processed_image is not None:
# Save the image
success = cv2.imwrite(output_path, processed_image)
if success:
print(f"Image saved successfully to {output_path}")
return True
else:
print(f"Error: Could not save image to {output_path}")
return False
else:
print("Error: Could not process image")
return False
def _render_video(self, output_path: str):
"""Optimized video rendering with multithreading and batch processing""" """Optimized video rendering with multithreading and batch processing"""
if not output_path.endswith(".mp4"): if not output_path.endswith(".mp4"):
output_path += ".mp4" output_path += ".mp4"
@@ -1324,39 +1402,61 @@ class VideoEditor:
def run(self): def run(self):
"""Main editor loop""" """Main editor loop"""
print("Video Editor Controls:") if self.is_image_mode:
print(" Space: Play/Pause") print("Image Editor Controls:")
print(" A/D: Seek backward/forward (1 frame)") print(" E/Shift+E: Increase/Decrease brightness")
print(" Shift+A/D: Seek backward/forward (10 frames)") print(" R/Shift+R: Increase/Decrease contrast")
print(" Ctrl+A/D: Seek backward/forward (60 frames)") print(" -: Rotate clockwise 90°")
print(" W/S: Increase/Decrease speed") print()
print(" E/Shift+E: Increase/Decrease brightness") print("Crop Controls:")
print(" R/Shift+R: Increase/Decrease contrast") print(" Shift+Click+Drag: Select crop area")
print(" -: Rotate clockwise 90°") print(" h/j/k/l: Contract crop (left/down/up/right)")
print() print(" H/J/K/L: Expand crop (left/down/up/right)")
print("Crop Controls:") print(" U: Undo crop")
print(" Shift+Click+Drag: Select crop area") print(" C: Clear crop")
print(" I/J/K/L: Contract crop (up/left/down/right)") print()
print(" Shift+I/J/K/L: Expand crop (up/left/down/right)") print("Other Controls:")
print(" [/]: Contract/Expand crop (cycles directions)") print(" Ctrl+Scroll: Zoom in/out")
print(" U: Undo crop") if len(self.video_files) > 1:
print(" C: Clear crop") print(" N: Next file")
print() print(" n: Previous file")
print("Other Controls:") print(" Enter: Save image")
print(" Ctrl+Scroll: Zoom in/out") print(" Q/ESC: Quit")
print(" 1: Set cut start point") print()
print(" 2: Set cut end point") else:
print(" T: Toggle loop between markers") print("Video Editor Controls:")
if len(self.video_files) > 1: print(" Space: Play/Pause")
print(" N: Next video") print(" A/D: Seek backward/forward (1 frame)")
print(" n: Previous video") print(" Shift+A/D: Seek backward/forward (10 frames)")
print(" Enter: Render video") print(" Ctrl+A/D: Seek backward/forward (60 frames)")
print(" Q/ESC: Quit") print(" W/S: Increase/Decrease speed")
print() print(" E/Shift+E: Increase/Decrease brightness")
print(" R/Shift+R: Increase/Decrease contrast")
print(" -: Rotate clockwise 90°")
print()
print("Crop Controls:")
print(" Shift+Click+Drag: Select crop area")
print(" h/j/k/l: Contract crop (left/down/up/right)")
print(" H/J/K/L: Expand crop (left/down/up/right)")
print(" U: Undo crop")
print(" C: Clear crop")
print()
print("Other Controls:")
print(" Ctrl+Scroll: Zoom in/out")
print(" 1: Set cut start point")
print(" 2: Set cut end point")
print(" T: Toggle loop between markers")
if len(self.video_files) > 1:
print(" N: Next video")
print(" n: Previous video")
print(" Enter: Render video")
print(" Q/ESC: Quit")
print()
cv2.namedWindow("Video Editor", cv2.WINDOW_NORMAL) window_title = "Image Editor" if self.is_image_mode else "Video Editor"
cv2.resizeWindow("Video Editor", self.window_width, self.window_height) cv2.namedWindow(window_title, cv2.WINDOW_NORMAL)
cv2.setMouseCallback("Video Editor", self.mouse_callback) cv2.resizeWindow(window_title, self.window_width, self.window_height)
cv2.setMouseCallback(window_title, self.mouse_callback)
self.load_current_frame() self.load_current_frame()
@@ -1368,43 +1468,58 @@ class VideoEditor:
key = cv2.waitKey(delay) & 0xFF key = cv2.waitKey(delay) & 0xFF
# Get modifier key states # Get modifier key states
modifiers = cv2.getWindowProperty("Video Editor", cv2.WND_PROP_AUTOSIZE) window_title = "Image Editor" if self.is_image_mode else "Video Editor"
modifiers = cv2.getWindowProperty(window_title, cv2.WND_PROP_AUTOSIZE)
# Note: OpenCV doesn't provide direct access to modifier keys in waitKey # Note: OpenCV doesn't provide direct access to modifier keys in waitKey
# We'll handle this through special key combinations # We'll handle this through special key combinations
if key == ord("q") or key == 27: # ESC if key == ord("q") or key == 27: # ESC
break break
elif key == ord(" "): elif key == ord(" "):
self.is_playing = not self.is_playing # Don't allow play/pause for images
if not self.is_image_mode:
self.is_playing = not self.is_playing
elif key == ord("a") or key == ord("A"): elif key == ord("a") or key == ord("A"):
# Check if it's uppercase A (Shift+A) # Seeking only for videos
if key == ord("A"): if not self.is_image_mode:
self.seek_video_with_modifier( # Check if it's uppercase A (Shift+A)
-1, True, False if key == ord("A"):
) # Shift+A: -10 frames self.seek_video_with_modifier(
else: -1, True, False
self.seek_video_with_modifier(-1, False, False) # A: -1 frame ) # Shift+A: -10 frames
else:
self.seek_video_with_modifier(-1, False, False) # A: -1 frame
elif key == ord("d") or key == ord("D"): elif key == ord("d") or key == ord("D"):
# Check if it's uppercase D (Shift+D) # Seeking only for videos
if key == ord("D"): if not self.is_image_mode:
self.seek_video_with_modifier(1, True, False) # Shift+D: +10 frames # Check if it's uppercase D (Shift+D)
else: if key == ord("D"):
self.seek_video_with_modifier(1, False, False) # D: +1 frame self.seek_video_with_modifier(1, True, False) # Shift+D: +10 frames
else:
self.seek_video_with_modifier(1, False, False) # D: +1 frame
elif key == 1: # Ctrl+A elif key == 1: # Ctrl+A
self.seek_video_with_modifier(-1, False, True) # Ctrl+A: -60 frames # Seeking only for videos
if not self.is_image_mode:
self.seek_video_with_modifier(-1, False, True) # Ctrl+A: -60 frames
elif key == 4: # Ctrl+D elif key == 4: # Ctrl+D
self.seek_video_with_modifier(1, False, True) # Ctrl+D: +60 frames # Seeking only for videos
if not self.is_image_mode:
self.seek_video_with_modifier(1, False, True) # Ctrl+D: +60 frames
elif key == ord("-") or key == ord("_"): elif key == ord("-") or key == ord("_"):
self.rotate_clockwise() self.rotate_clockwise()
print(f"Rotated to {self.rotation_angle}°") print(f"Rotated to {self.rotation_angle}°")
elif key == ord("w"): elif key == ord("w"):
self.playback_speed = min( # Speed control only for videos
self.MAX_PLAYBACK_SPEED, self.playback_speed + self.SPEED_INCREMENT if not self.is_image_mode:
) self.playback_speed = min(
self.MAX_PLAYBACK_SPEED, self.playback_speed + self.SPEED_INCREMENT
)
elif key == ord("s"): elif key == ord("s"):
self.playback_speed = max( # Speed control only for videos
self.MIN_PLAYBACK_SPEED, self.playback_speed - self.SPEED_INCREMENT if not self.is_image_mode:
) self.playback_speed = max(
self.MIN_PLAYBACK_SPEED, self.playback_speed - self.SPEED_INCREMENT
)
elif key == ord("e") or key == ord("E"): elif key == ord("e") or key == ord("E"):
# Brightness adjustment: E (increase), Shift+E (decrease) # Brightness adjustment: E (increase), Shift+E (decrease)
if key == ord("E"): if key == ord("E"):
@@ -1428,11 +1543,15 @@ class VideoEditor:
self.crop_history.append(self.crop_rect) self.crop_history.append(self.crop_rect)
self.crop_rect = None self.crop_rect = None
elif key == ord("1"): elif key == ord("1"):
self.cut_start_frame = self.current_frame # Cut markers only for videos
print(f"Set cut start at frame {self.current_frame}") if not self.is_image_mode:
self.cut_start_frame = self.current_frame
print(f"Set cut start at frame {self.current_frame}")
elif key == ord("2"): elif key == ord("2"):
self.cut_end_frame = self.current_frame # Cut markers only for videos
print(f"Set cut end at frame {self.current_frame}") if not self.is_image_mode:
self.cut_end_frame = self.current_frame
print(f"Set cut end at frame {self.current_frame}")
elif key == ord("N"): elif key == ord("N"):
if len(self.video_files) > 1: if len(self.video_files) > 1:
self.previous_video() self.previous_video()
@@ -1443,7 +1562,9 @@ class VideoEditor:
output_name = self._get_next_edited_filename(self.video_path) output_name = self._get_next_edited_filename(self.video_path)
self.render_video(str(self.video_path.parent / output_name)) self.render_video(str(self.video_path.parent / output_name))
elif key == ord("t"): elif key == ord("t"):
self.toggle_marker_looping() # Marker looping only for videos
if not self.is_image_mode:
self.toggle_marker_looping()
# Individual direction controls using shift combinations we can detect # Individual direction controls using shift combinations we can detect
elif key == ord("J"): # Shift+i - expand up elif key == ord("J"): # Shift+i - expand up
@@ -1474,20 +1595,21 @@ class VideoEditor:
print(f"Contracted crop from left by {self.crop_size_step}px") print(f"Contracted crop from left by {self.crop_size_step}px")
# Auto advance frame when playing # Auto advance frame when playing (videos only)
if self.is_playing: if self.is_playing and not self.is_image_mode:
self.advance_frame() self.advance_frame()
self.cap.release() if hasattr(self, 'cap') and self.cap:
self.cap.release()
cv2.destroyAllWindows() cv2.destroyAllWindows()
def main(): def main():
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Fast Video Editor - Crop, Zoom, and Cut videos" description="Fast Media Editor - Crop, Zoom, and Edit videos and images"
) )
parser.add_argument( parser.add_argument(
"video", help="Path to video file or directory containing videos" "media", help="Path to media file or directory containing videos/images"
) )
try: try:
@@ -1497,17 +1619,17 @@ def main():
input(f"Argument parsing failed. Press Enter to exit...") input(f"Argument parsing failed. Press Enter to exit...")
return return
if not os.path.exists(args.video): if not os.path.exists(args.media):
error_msg = f"Error: {args.video} does not exist" error_msg = f"Error: {args.media} does not exist"
print(error_msg) print(error_msg)
input("Press Enter to exit...") # Keep window open in context menu input("Press Enter to exit...") # Keep window open in context menu
sys.exit(1) sys.exit(1)
try: try:
editor = VideoEditor(args.video) editor = VideoEditor(args.media)
editor.run() editor.run()
except Exception as e: except Exception as e:
error_msg = f"Error initializing video editor: {e}" error_msg = f"Error initializing media editor: {e}"
print(error_msg) print(error_msg)
import traceback import traceback
traceback.print_exc() # Full error trace for debugging traceback.print_exc() # Full error trace for debugging