Compare commits
3 Commits
f8780a2d43
...
85bef2b3bd
Author | SHA1 | Date | |
---|---|---|---|
85bef2b3bd | |||
525ecd2cf5 | |||
b59e3bd570 |
189
croppa/main.py
189
croppa/main.py
@@ -46,6 +46,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)
|
||||
@@ -112,6 +115,9 @@ class VideoEditor:
|
||||
self.progress_bar_complete_time = None
|
||||
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"""
|
||||
@@ -639,48 +645,6 @@ class VideoEditor:
|
||||
1,
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
# Draw final crop rectangle (red) - convert from video to screen coordinates
|
||||
if self.crop_rect:
|
||||
# Convert crop coordinates from original video to screen coordinates
|
||||
x, y, w, h = self.crop_rect
|
||||
|
||||
# Apply the same scaling logic as in display_current_frame
|
||||
original_height, original_width = self.current_display_frame.shape[:2]
|
||||
available_height = self.window_height - self.TIMELINE_HEIGHT
|
||||
|
||||
scale = min(
|
||||
self.window_width / original_width, available_height / original_height
|
||||
)
|
||||
if scale < 1.0:
|
||||
new_width = int(original_width * scale)
|
||||
new_height = int(original_height * scale)
|
||||
else:
|
||||
new_width = original_width
|
||||
new_height = original_height
|
||||
|
||||
# Convert video coordinates to screen coordinates
|
||||
screen_x = start_x + (x * new_width / original_width)
|
||||
screen_y = start_y + (y * new_height / original_height)
|
||||
screen_w = w * new_width / original_width
|
||||
screen_h = h * new_height / original_height
|
||||
|
||||
cv2.rectangle(
|
||||
canvas,
|
||||
(int(screen_x), int(screen_y)),
|
||||
(int(screen_x + screen_w), int(screen_y + screen_h)),
|
||||
(255, 0, 0),
|
||||
2,
|
||||
)
|
||||
|
||||
def display_current_frame(self):
|
||||
"""Display the current frame with all overlays"""
|
||||
if self.current_display_frame is None:
|
||||
@@ -716,9 +680,12 @@ class VideoEditor:
|
||||
display_frame
|
||||
)
|
||||
|
||||
# Draw crop overlay
|
||||
if self.crop_rect or self.crop_preview_rect:
|
||||
self.draw_crop_overlay(canvas, start_x, start_y, frame_width, frame_height)
|
||||
# Draw crop selection preview during Shift+Click+Drag
|
||||
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
|
||||
)
|
||||
|
||||
# Add info overlay
|
||||
rotation_text = (
|
||||
@@ -978,6 +945,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 +1218,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")
|
||||
@@ -1279,6 +1325,59 @@ class VideoEditor:
|
||||
elif key == 13: # Enter
|
||||
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:
|
||||
|
Reference in New Issue
Block a user