Implement rotation and faster seeking

This commit is contained in:
2025-09-04 15:05:30 +02:00
parent 692c413f13
commit cd89bbf4e4

View File

@@ -80,6 +80,9 @@ class VideoEditor:
self.zoom_factor = 1.0 self.zoom_factor = 1.0
self.zoom_center = None # (x, y) center point for zoom self.zoom_center = None # (x, y) center point for zoom
# Rotation settings
self.rotation_angle = 0 # 0, 90, 180, 270 degrees
# Cut points # Cut points
self.cut_start_frame = None self.cut_start_frame = None
self.cut_end_frame = None self.cut_end_frame = None
@@ -118,11 +121,12 @@ class VideoEditor:
self.playback_speed = 1.0 self.playback_speed = 1.0
self.current_display_frame = None self.current_display_frame = None
# Reset crop, zoom, and cut settings for new video # Reset crop, zoom, rotation, and cut settings for new video
self.crop_rect = None self.crop_rect = None
self.crop_history = [] self.crop_history = []
self.zoom_factor = 1.0 self.zoom_factor = 1.0
self.zoom_center = None self.zoom_center = None
self.rotation_angle = 0
self.cut_start_frame = None self.cut_start_frame = None
self.cut_end_frame = None self.cut_end_frame = None
self.display_offset = [0, 0] self.display_offset = [0, 0]
@@ -166,6 +170,17 @@ class VideoEditor:
self.current_frame = target_frame self.current_frame = target_frame
self.load_current_frame() self.load_current_frame()
def seek_video_with_modifier(self, direction: int, shift_pressed: bool, ctrl_pressed: bool):
"""Seek video with different frame counts based on modifiers"""
if ctrl_pressed:
frames = direction * 60 # Ctrl: 60 frames
elif shift_pressed:
frames = direction * 10 # Shift: 10 frames
else:
frames = direction * 1 # Default: 1 frame
self.seek_video(frames)
def seek_to_frame(self, frame_number: int): def seek_to_frame(self, frame_number: int):
"""Seek to specific frame""" """Seek to specific frame"""
self.current_frame = max(0, min(frame_number, self.total_frames - 1)) self.current_frame = max(0, min(frame_number, self.total_frames - 1))
@@ -182,8 +197,8 @@ class VideoEditor:
return self.load_current_frame() return self.load_current_frame()
def apply_crop_and_zoom(self, frame): def apply_crop_zoom_and_rotation(self, frame):
"""Apply current crop and zoom settings to frame""" """Apply current crop, zoom, and rotation settings to frame"""
if frame is None: if frame is None:
return None return None
@@ -201,6 +216,10 @@ class VideoEditor:
if w > 0 and h > 0: if w > 0 and h > 0:
processed_frame = processed_frame[y:y+h, x:x+w] processed_frame = processed_frame[y:y+h, x:x+w]
# Apply rotation
if self.rotation_angle != 0:
processed_frame = self.apply_rotation(processed_frame)
# Apply zoom # Apply zoom
if self.zoom_factor != 1.0: if self.zoom_factor != 1.0:
height, width = processed_frame.shape[:2] height, width = processed_frame.shape[:2]
@@ -219,6 +238,22 @@ class VideoEditor:
return processed_frame return processed_frame
def apply_rotation(self, frame):
"""Apply rotation to frame"""
if self.rotation_angle == 0:
return frame
elif self.rotation_angle == 90:
return cv2.rotate(frame, cv2.ROTATE_90_CLOCKWISE)
elif self.rotation_angle == 180:
return cv2.rotate(frame, cv2.ROTATE_180)
elif self.rotation_angle == 270:
return cv2.rotate(frame, cv2.ROTATE_90_COUNTERCLOCKWISE)
return frame
def rotate_clockwise(self):
"""Rotate video 90 degrees clockwise"""
self.rotation_angle = (self.rotation_angle + 90) % 360
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"""
height, width = frame.shape[:2] height, width = frame.shape[:2]
@@ -303,8 +338,8 @@ class VideoEditor:
if self.current_display_frame is None: if self.current_display_frame is None:
return return
# Apply crop and zoom transformations for preview # Apply crop, zoom, and rotation transformations for preview
display_frame = self.apply_crop_and_zoom(self.current_display_frame.copy()) display_frame = self.apply_crop_zoom_and_rotation(self.current_display_frame.copy())
if display_frame is None: if display_frame is None:
return return
@@ -334,7 +369,8 @@ class VideoEditor:
self.draw_crop_overlay(canvas, start_x, start_y, frame_width, frame_height) self.draw_crop_overlay(canvas, start_x, start_y, frame_width, frame_height)
# Add info overlay # Add info overlay
info_text = f"Frame: {self.current_frame}/{self.total_frames} | Speed: {self.playback_speed:.1f}x | Zoom: {self.zoom_factor:.1f}x | {'Playing' if self.is_playing else 'Paused'}" rotation_text = f" | Rotation: {self.rotation_angle}°" if self.rotation_angle != 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} | {'Playing' if self.is_playing else 'Paused'}"
cv2.putText(canvas, info_text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2) cv2.putText(canvas, info_text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
cv2.putText(canvas, info_text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 0), 1) cv2.putText(canvas, info_text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 0), 1)
@@ -429,8 +465,8 @@ class VideoEditor:
original_height, original_width = self.current_display_frame.shape[:2] original_height, original_width = self.current_display_frame.shape[:2]
available_height = self.window_height - self.TIMELINE_HEIGHT available_height = self.window_height - self.TIMELINE_HEIGHT
# Calculate how the original frame is displayed (after crop/zoom) # Calculate how the original frame is displayed (after crop/zoom/rotation)
display_frame = self.apply_crop_and_zoom(self.current_display_frame.copy()) display_frame = self.apply_crop_zoom_and_rotation(self.current_display_frame.copy())
if display_frame is None: if display_frame is None:
return return
@@ -528,13 +564,21 @@ class VideoEditor:
print("Invalid cut range!") print("Invalid cut range!")
return False return False
# Calculate output dimensions # Calculate output dimensions (accounting for rotation)
if self.crop_rect: if self.crop_rect:
output_width = int(self.crop_rect[2] * self.zoom_factor) crop_width = int(self.crop_rect[2])
output_height = int(self.crop_rect[3] * self.zoom_factor) crop_height = int(self.crop_rect[3])
else: else:
output_width = int(self.frame_width * self.zoom_factor) crop_width = self.frame_width
output_height = int(self.frame_height * self.zoom_factor) crop_height = self.frame_height
# Swap dimensions if rotation is 90 or 270 degrees
if self.rotation_angle == 90 or self.rotation_angle == 270:
output_width = int(crop_height * self.zoom_factor)
output_height = int(crop_width * self.zoom_factor)
else:
output_width = int(crop_width * self.zoom_factor)
output_height = int(crop_height * self.zoom_factor)
# Use mp4v codec (most compatible with MP4) # Use mp4v codec (most compatible with MP4)
fourcc = cv2.VideoWriter_fourcc(*'mp4v') fourcc = cv2.VideoWriter_fourcc(*'mp4v')
@@ -604,6 +648,10 @@ class VideoEditor:
else: else:
return None return None
# Apply rotation
if self.rotation_angle != 0:
frame = self.apply_rotation(frame)
# Apply zoom and resize in one step for efficiency # Apply zoom and resize in one step for efficiency
if self.zoom_factor != 1.0: if self.zoom_factor != 1.0:
height, width = frame.shape[:2] height, width = frame.shape[:2]
@@ -630,8 +678,11 @@ class VideoEditor:
"""Main editor loop""" """Main editor loop"""
print("Video Editor Controls:") print("Video Editor Controls:")
print(" Space: Play/Pause") print(" Space: Play/Pause")
print(" A/D: Seek backward/forward") print(" A/D: Seek backward/forward (1 frame)")
print(" Shift+A/D: Seek backward/forward (10 frames)")
print(" Ctrl+A/D: Seek backward/forward (60 frames)")
print(" W/S: Increase/Decrease speed") print(" W/S: Increase/Decrease speed")
print(" -: Rotate clockwise 90°")
print(" Shift+Click+Drag: Select crop area") print(" Shift+Click+Drag: Select crop area")
print(" U: Undo crop") print(" U: Undo crop")
print(" C: Clear crop") print(" C: Clear crop")
@@ -657,14 +708,34 @@ class VideoEditor:
delay = self.calculate_frame_delay() if self.is_playing else 30 delay = self.calculate_frame_delay() if self.is_playing else 30
key = cv2.waitKey(delay) & 0xFF key = cv2.waitKey(delay) & 0xFF
# Get modifier key states
modifiers = cv2.getWindowProperty("Video Editor", cv2.WND_PROP_AUTOSIZE)
# Note: OpenCV doesn't provide direct access to modifier keys in waitKey
# 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 self.is_playing = not self.is_playing
elif key == ord('a'): elif key == ord('a') or key == ord('A'):
self.seek_video(-1) # Check if it's uppercase A (Shift+A)
elif key == ord('d'): if key == ord('A'):
self.seek_video(1) self.seek_video_with_modifier(-1, True, False) # Shift+A: -10 frames
else:
self.seek_video_with_modifier(-1, False, False) # A: -1 frame
elif key == ord('d') or key == ord('D'):
# Check if it's uppercase D (Shift+D)
if key == ord('D'):
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
self.seek_video_with_modifier(-1, False, True) # Ctrl+A: -60 frames
elif key == 4: # Ctrl+D
self.seek_video_with_modifier(1, False, True) # Ctrl+D: +60 frames
elif key == ord('-') or key == ord('_'):
self.rotate_clockwise()
print(f"Rotated to {self.rotation_angle}°")
elif key == ord('w'): elif key == ord('w'):
self.playback_speed = min(self.MAX_PLAYBACK_SPEED, self.playback_speed + self.SPEED_INCREMENT) self.playback_speed = min(self.MAX_PLAYBACK_SPEED, self.playback_speed + self.SPEED_INCREMENT)
elif key == ord('s'): elif key == ord('s'):