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
|
# 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)
|
||||||
|
|
||||||
@@ -113,6 +116,9 @@ class VideoEditor:
|
|||||||
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"""
|
||||||
directory = video_path.parent
|
directory = video_path.parent
|
||||||
@@ -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")
|
||||||
@@ -1280,6 +1361,59 @@ 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))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 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:
|
||||||
self.advance_frame()
|
self.advance_frame()
|
||||||
|
Reference in New Issue
Block a user