Implement rotation and faster seeking
This commit is contained in:
107
croppa/main.py
107
croppa/main.py
@@ -80,6 +80,9 @@ class VideoEditor:
|
||||
self.zoom_factor = 1.0
|
||||
self.zoom_center = None # (x, y) center point for zoom
|
||||
|
||||
# Rotation settings
|
||||
self.rotation_angle = 0 # 0, 90, 180, 270 degrees
|
||||
|
||||
# Cut points
|
||||
self.cut_start_frame = None
|
||||
self.cut_end_frame = None
|
||||
@@ -118,11 +121,12 @@ class VideoEditor:
|
||||
self.playback_speed = 1.0
|
||||
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_history = []
|
||||
self.zoom_factor = 1.0
|
||||
self.zoom_center = None
|
||||
self.rotation_angle = 0
|
||||
self.cut_start_frame = None
|
||||
self.cut_end_frame = None
|
||||
self.display_offset = [0, 0]
|
||||
@@ -166,6 +170,17 @@ class VideoEditor:
|
||||
self.current_frame = target_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):
|
||||
"""Seek to specific frame"""
|
||||
self.current_frame = max(0, min(frame_number, self.total_frames - 1))
|
||||
@@ -182,8 +197,8 @@ class VideoEditor:
|
||||
|
||||
return self.load_current_frame()
|
||||
|
||||
def apply_crop_and_zoom(self, frame):
|
||||
"""Apply current crop and zoom settings to frame"""
|
||||
def apply_crop_zoom_and_rotation(self, frame):
|
||||
"""Apply current crop, zoom, and rotation settings to frame"""
|
||||
if frame is None:
|
||||
return None
|
||||
|
||||
@@ -201,6 +216,10 @@ class VideoEditor:
|
||||
if w > 0 and h > 0:
|
||||
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
|
||||
if self.zoom_factor != 1.0:
|
||||
height, width = processed_frame.shape[:2]
|
||||
@@ -219,6 +238,22 @@ class VideoEditor:
|
||||
|
||||
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):
|
||||
"""Draw timeline at the bottom of the frame"""
|
||||
height, width = frame.shape[:2]
|
||||
@@ -303,8 +338,8 @@ class VideoEditor:
|
||||
if self.current_display_frame is None:
|
||||
return
|
||||
|
||||
# Apply crop and zoom transformations for preview
|
||||
display_frame = self.apply_crop_and_zoom(self.current_display_frame.copy())
|
||||
# Apply crop, zoom, and rotation transformations for preview
|
||||
display_frame = self.apply_crop_zoom_and_rotation(self.current_display_frame.copy())
|
||||
|
||||
if display_frame is None:
|
||||
return
|
||||
@@ -334,7 +369,8 @@ class VideoEditor:
|
||||
self.draw_crop_overlay(canvas, start_x, start_y, frame_width, frame_height)
|
||||
|
||||
# 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, (0, 0, 0), 1)
|
||||
|
||||
@@ -429,8 +465,8 @@ class VideoEditor:
|
||||
original_height, original_width = self.current_display_frame.shape[:2]
|
||||
available_height = self.window_height - self.TIMELINE_HEIGHT
|
||||
|
||||
# Calculate how the original frame is displayed (after crop/zoom)
|
||||
display_frame = self.apply_crop_and_zoom(self.current_display_frame.copy())
|
||||
# Calculate how the original frame is displayed (after crop/zoom/rotation)
|
||||
display_frame = self.apply_crop_zoom_and_rotation(self.current_display_frame.copy())
|
||||
if display_frame is None:
|
||||
return
|
||||
|
||||
@@ -528,13 +564,21 @@ class VideoEditor:
|
||||
print("Invalid cut range!")
|
||||
return False
|
||||
|
||||
# Calculate output dimensions
|
||||
# Calculate output dimensions (accounting for rotation)
|
||||
if self.crop_rect:
|
||||
output_width = int(self.crop_rect[2] * self.zoom_factor)
|
||||
output_height = int(self.crop_rect[3] * self.zoom_factor)
|
||||
crop_width = int(self.crop_rect[2])
|
||||
crop_height = int(self.crop_rect[3])
|
||||
else:
|
||||
output_width = int(self.frame_width * self.zoom_factor)
|
||||
output_height = int(self.frame_height * self.zoom_factor)
|
||||
crop_width = self.frame_width
|
||||
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)
|
||||
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
||||
@@ -604,6 +648,10 @@ class VideoEditor:
|
||||
else:
|
||||
return None
|
||||
|
||||
# Apply rotation
|
||||
if self.rotation_angle != 0:
|
||||
frame = self.apply_rotation(frame)
|
||||
|
||||
# Apply zoom and resize in one step for efficiency
|
||||
if self.zoom_factor != 1.0:
|
||||
height, width = frame.shape[:2]
|
||||
@@ -630,8 +678,11 @@ class VideoEditor:
|
||||
"""Main editor loop"""
|
||||
print("Video Editor Controls:")
|
||||
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(" -: Rotate clockwise 90°")
|
||||
print(" Shift+Click+Drag: Select crop area")
|
||||
print(" U: Undo crop")
|
||||
print(" C: Clear crop")
|
||||
@@ -657,14 +708,34 @@ class VideoEditor:
|
||||
delay = self.calculate_frame_delay() if self.is_playing else 30
|
||||
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
|
||||
break
|
||||
elif key == ord(' '):
|
||||
self.is_playing = not self.is_playing
|
||||
elif key == ord('a'):
|
||||
self.seek_video(-1)
|
||||
elif key == ord('d'):
|
||||
self.seek_video(1)
|
||||
elif key == ord('a') or key == ord('A'):
|
||||
# Check if it's uppercase A (Shift+A)
|
||||
if key == ord('A'):
|
||||
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'):
|
||||
self.playback_speed = min(self.MAX_PLAYBACK_SPEED, self.playback_speed + self.SPEED_INCREMENT)
|
||||
elif key == ord('s'):
|
||||
|
Reference in New Issue
Block a user