Implement crop adjustments

This commit is contained in:
2025-09-04 16:35:40 +02:00
parent f8780a2d43
commit b59e3bd570

View File

@@ -46,6 +46,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"}
# Crop adjustment settings
CROP_SIZE_STEP = 15 # pixels to expand/contract crop
def __init__(self, path: str): def __init__(self, path: str):
self.path = Path(path) self.path = Path(path)
@@ -112,6 +115,9 @@ class VideoEditor:
self.progress_bar_complete_time = None self.progress_bar_complete_time = None
self.progress_bar_text = "" self.progress_bar_text = ""
self.progress_bar_fps = 0.0 # Current rendering FPS self.progress_bar_fps = 0.0 # Current rendering FPS
# Crop adjustment settings
self.crop_size_step = self.CROP_SIZE_STEP
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"""
@@ -641,12 +647,8 @@ class VideoEditor:
def draw_crop_overlay(self, canvas, start_x, start_y, frame_width, frame_height): def draw_crop_overlay(self, canvas, start_x, start_y, frame_width, frame_height):
"""Draw crop overlay on canvas using screen coordinates""" """Draw crop overlay on canvas using screen coordinates"""
# Draw preview rectangle (green) - already in screen coordinates # Note: crop_preview_rect disabled as it was showing in wrong position
if self.crop_preview_rect: # The final crop rectangle (red) is sufficient for visual feedback
x, y, w, h = self.crop_preview_rect
cv2.rectangle(
canvas, (int(x), int(y)), (int(x + w), int(y + h)), (0, 255, 0), 2
)
# Draw final crop rectangle (red) - convert from video to screen coordinates # Draw final crop rectangle (red) - convert from video to screen coordinates
if self.crop_rect: if self.crop_rect:
@@ -978,6 +980,78 @@ class VideoEditor:
else: else:
self.crop_rect = None self.crop_rect = None
def adjust_crop_size(self, direction: str, expand: bool, amount: int = None):
"""
Adjust crop size in given direction
direction: 'up', 'down', 'left', 'right'
expand: True to expand, False to contract
amount: pixels to adjust by (uses self.crop_size_step if None)
"""
if amount is None:
amount = self.crop_size_step
if not self.crop_rect:
# If no crop exists, create a default one in the center
center_x = self.frame_width // 2
center_y = self.frame_height // 2
default_size = min(self.frame_width, self.frame_height) // 4
self.crop_rect = (
center_x - default_size // 2,
center_y - default_size // 2,
default_size,
default_size
)
return
x, y, w, h = self.crop_rect
if direction == 'up':
if expand:
# Expand upward - decrease y, increase height
new_y = max(0, y - amount)
new_h = h + (y - new_y)
self.crop_rect = (x, new_y, w, new_h)
else:
# Contract from bottom - decrease height
new_h = max(10, h - amount) # Minimum size of 10 pixels
self.crop_rect = (x, y, w, new_h)
elif direction == 'down':
if expand:
# Expand downward - increase height
new_h = min(self.frame_height - y, h + amount)
self.crop_rect = (x, y, w, new_h)
else:
# Contract from top - increase y, decrease height
amount = min(amount, h - 10) # Don't make it smaller than 10 pixels
new_y = y + amount
new_h = h - amount
self.crop_rect = (x, new_y, w, new_h)
elif direction == 'left':
if expand:
# Expand leftward - decrease x, increase width
new_x = max(0, x - amount)
new_w = w + (x - new_x)
self.crop_rect = (new_x, y, new_w, h)
else:
# Contract from right - decrease width
new_w = max(10, w - amount) # Minimum size of 10 pixels
self.crop_rect = (x, y, new_w, h)
elif direction == 'right':
if expand:
# Expand rightward - increase width
new_w = min(self.frame_width - x, w + amount)
self.crop_rect = (x, y, new_w, h)
else:
# Contract from left - increase x, decrease width
amount = min(amount, w - 10) # Don't make it smaller than 10 pixels
new_x = x + amount
new_w = w - amount
self.crop_rect = (new_x, y, new_w, h)
def render_video(self, output_path: str): 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"):
@@ -1179,9 +1253,16 @@ class VideoEditor:
print(" E/Shift+E: Increase/Decrease brightness") print(" E/Shift+E: Increase/Decrease brightness")
print(" R/Shift+R: Increase/Decrease contrast") print(" R/Shift+R: Increase/Decrease contrast")
print(" -: Rotate clockwise 90°") print(" -: Rotate clockwise 90°")
print()
print("Crop Controls:")
print(" Shift+Click+Drag: Select crop area") print(" Shift+Click+Drag: Select crop area")
print(" I/J/K/L: Contract crop (up/left/down/right)")
print(" Shift+I/J/K/L: Expand crop (up/left/down/right)")
print(" [/]: Contract/Expand crop (cycles directions)")
print(" U: Undo crop") print(" U: Undo crop")
print(" C: Clear crop") print(" C: Clear crop")
print()
print("Other Controls:")
print(" Ctrl+Scroll: Zoom in/out") print(" Ctrl+Scroll: Zoom in/out")
print(" 1: Set cut start point") print(" 1: Set cut start point")
print(" 2: Set cut end point") print(" 2: Set cut end point")
@@ -1279,6 +1360,59 @@ class VideoEditor:
elif key == 13: # Enter elif key == 13: # Enter
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))
# Use keyboard shortcuts for crop size adjustment since modifier detection is unreliable
# Using bracket keys for crop size adjustment
elif key == ord("["): # Contract crop
# Cycle through directions: up, right, down, left
if not hasattr(self, '_contract_direction'):
self._contract_direction = 0
directions = ['up', 'right', 'down', 'left']
direction = directions[self._contract_direction % 4]
self.adjust_crop_size(direction, False, 20)
self._contract_direction += 1
print(f"Contracted crop {direction}")
elif key == ord("]"): # Expand crop
# Cycle through directions: up, right, down, left
if not hasattr(self, '_expand_direction'):
self._expand_direction = 0
directions = ['up', 'right', 'down', 'left']
direction = directions[self._expand_direction % 4]
self.adjust_crop_size(direction, True, 20)
self._expand_direction += 1
print(f"Expanded crop {direction}")
# Individual direction controls using shift combinations we can detect
elif key == ord("I"): # Shift+i - expand up
self.adjust_crop_size('up', True)
print(f"Expanded crop upward by {self.crop_size_step}px")
elif key == ord("K"): # Shift+k - expand down
self.adjust_crop_size('down', True)
print(f"Expanded crop downward by {self.crop_size_step}px")
elif key == ord("J"): # Shift+j - expand left
self.adjust_crop_size('left', True)
print(f"Expanded crop leftward by {self.crop_size_step}px")
elif key == ord("L"): # Shift+l - expand right
self.adjust_crop_size('right', True)
print(f"Expanded crop rightward by {self.crop_size_step}px")
# Contract in specific directions
elif key == ord("i"): # i - contract from bottom (reduce height from bottom)
self.adjust_crop_size('up', False)
print(f"Contracted crop from bottom by {self.crop_size_step}px")
elif key == ord("k"): # k - contract from top (reduce height from top)
self.adjust_crop_size('down', False)
print(f"Contracted crop from top by {self.crop_size_step}px")
elif key == ord("j"): # j - contract from right (reduce width from right)
self.adjust_crop_size('left', False)
print(f"Contracted crop from right by {self.crop_size_step}px")
elif key == ord("l"): # l - contract from left (reduce width from left)
self.adjust_crop_size('right', False)
print(f"Contracted crop from left by {self.crop_size_step}px")
# Auto advance frame when playing # Auto advance frame when playing
if self.is_playing: if self.is_playing: