Implement crop adjustments
This commit is contained in:
146
croppa/main.py
146
croppa/main.py
@@ -47,6 +47,9 @@ class VideoEditor:
|
||||
# Supported video extensions
|
||||
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):
|
||||
self.path = Path(path)
|
||||
|
||||
@@ -113,6 +116,9 @@ class VideoEditor:
|
||||
self.progress_bar_text = ""
|
||||
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:
|
||||
"""Generate the next available _edited_%03d filename"""
|
||||
directory = video_path.parent
|
||||
@@ -641,12 +647,8 @@ class VideoEditor:
|
||||
|
||||
def draw_crop_overlay(self, canvas, start_x, start_y, frame_width, frame_height):
|
||||
"""Draw crop overlay on canvas using screen coordinates"""
|
||||
# Draw preview rectangle (green) - already in screen coordinates
|
||||
if self.crop_preview_rect:
|
||||
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
|
||||
)
|
||||
# Note: crop_preview_rect disabled as it was showing in wrong position
|
||||
# The final crop rectangle (red) is sufficient for visual feedback
|
||||
|
||||
# Draw final crop rectangle (red) - convert from video to screen coordinates
|
||||
if self.crop_rect:
|
||||
@@ -978,6 +980,78 @@ class VideoEditor:
|
||||
else:
|
||||
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):
|
||||
"""Optimized video rendering with multithreading and batch processing"""
|
||||
if not output_path.endswith(".mp4"):
|
||||
@@ -1179,9 +1253,16 @@ class VideoEditor:
|
||||
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(" 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(" 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")
|
||||
@@ -1280,6 +1361,59 @@ class VideoEditor:
|
||||
output_name = self._get_next_edited_filename(self.video_path)
|
||||
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
|
||||
if self.is_playing:
|
||||
self.advance_frame()
|
||||
|
Reference in New Issue
Block a user