Enhance motion tracking functionality in VideoEditor
This commit updates the MotionTracker class to improve the offset calculation for centering crops on tracked points. It modifies the user interaction for adding and removing tracking points, allowing for nearby points to be removed with a right-click. Additionally, it introduces a method to map HJKL keys to directions based on the current rotation, enhancing the crop adjustment experience. The VideoEditor class has been updated to apply these changes, ensuring a more intuitive and responsive editing workflow.
This commit is contained in:
151
croppa/main.py
151
croppa/main.py
@@ -193,7 +193,7 @@ class MotionTracker:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def get_tracking_offset(self, frame_number: int) -> Tuple[float, float]:
|
def get_tracking_offset(self, frame_number: int) -> Tuple[float, float]:
|
||||||
"""Get the offset from the base position for motion tracking"""
|
"""Get the offset to center the crop on the tracked point"""
|
||||||
if not self.tracking_enabled or not self.base_zoom_center:
|
if not self.tracking_enabled or not self.base_zoom_center:
|
||||||
return (0.0, 0.0)
|
return (0.0, 0.0)
|
||||||
|
|
||||||
@@ -201,7 +201,8 @@ class MotionTracker:
|
|||||||
if not current_pos:
|
if not current_pos:
|
||||||
return (0.0, 0.0)
|
return (0.0, 0.0)
|
||||||
|
|
||||||
# Calculate offset from base position
|
# Calculate offset to center the crop on the tracked point
|
||||||
|
# The offset should move the crop so the tracked point stays centered
|
||||||
offset_x = current_pos[0] - self.base_zoom_center[0]
|
offset_x = current_pos[0] - self.base_zoom_center[0]
|
||||||
offset_y = current_pos[1] - self.base_zoom_center[1]
|
offset_y = current_pos[1] - self.base_zoom_center[1]
|
||||||
|
|
||||||
@@ -1240,10 +1241,17 @@ class VideoEditor:
|
|||||||
# Apply brightness/contrast first (to original frame for best quality)
|
# Apply brightness/contrast first (to original frame for best quality)
|
||||||
processed_frame = self.apply_brightness_contrast(processed_frame)
|
processed_frame = self.apply_brightness_contrast(processed_frame)
|
||||||
|
|
||||||
# Apply crop
|
# Apply crop with motion tracking offset
|
||||||
if self.crop_rect:
|
if self.crop_rect:
|
||||||
x, y, w, h = self.crop_rect
|
x, y, w, h = self.crop_rect
|
||||||
x, y, w, h = int(x), int(y), int(w), int(h)
|
x, y, w, h = int(x), int(y), int(w), int(h)
|
||||||
|
|
||||||
|
# Apply motion tracking offset to center crop on tracked point
|
||||||
|
if self.motion_tracker.tracking_enabled:
|
||||||
|
tracking_offset_x, tracking_offset_y = self.motion_tracker.get_tracking_offset(self.current_frame)
|
||||||
|
x += int(tracking_offset_x)
|
||||||
|
y += int(tracking_offset_y)
|
||||||
|
|
||||||
# Ensure crop is within frame bounds
|
# Ensure crop is within frame bounds
|
||||||
x = max(0, min(x, processed_frame.shape[1] - 1))
|
x = max(0, min(x, processed_frame.shape[1] - 1))
|
||||||
y = max(0, min(y, processed_frame.shape[0] - 1))
|
y = max(0, min(y, processed_frame.shape[0] - 1))
|
||||||
@@ -1265,14 +1273,11 @@ class VideoEditor:
|
|||||||
processed_frame, (new_width, new_height), interpolation=cv2.INTER_LINEAR
|
processed_frame, (new_width, new_height), interpolation=cv2.INTER_LINEAR
|
||||||
)
|
)
|
||||||
|
|
||||||
# Handle zoom center and display offset with motion tracking
|
# Handle zoom center and display offset
|
||||||
if new_width > self.window_width or new_height > self.window_height:
|
if new_width > self.window_width or new_height > self.window_height:
|
||||||
# Apply motion tracking offset to display offset
|
|
||||||
tracking_offset_x, tracking_offset_y = self.motion_tracker.get_tracking_offset(self.current_frame)
|
|
||||||
|
|
||||||
# Calculate crop from zoomed image to fit window
|
# Calculate crop from zoomed image to fit window
|
||||||
start_x = max(0, self.display_offset[0] + tracking_offset_x)
|
start_x = max(0, self.display_offset[0])
|
||||||
start_y = max(0, self.display_offset[1] + tracking_offset_y)
|
start_y = max(0, self.display_offset[1])
|
||||||
end_x = min(new_width, start_x + self.window_width)
|
end_x = min(new_width, start_x + self.window_width)
|
||||||
end_y = min(new_height, start_y + self.window_height)
|
end_y = min(new_height, start_y + self.window_height)
|
||||||
processed_frame = processed_frame[start_y:end_y, start_x:end_x]
|
processed_frame = processed_frame[start_y:end_y, start_x:end_x]
|
||||||
@@ -1977,14 +1982,48 @@ class VideoEditor:
|
|||||||
if flags & cv2.EVENT_FLAG_CTRLKEY and event == cv2.EVENT_LBUTTONDOWN:
|
if flags & cv2.EVENT_FLAG_CTRLKEY and event == cv2.EVENT_LBUTTONDOWN:
|
||||||
self.zoom_center = (x, y)
|
self.zoom_center = (x, y)
|
||||||
|
|
||||||
# Handle motion tracking point addition (Right click)
|
# Handle motion tracking point addition/removal (Right click)
|
||||||
if event == cv2.EVENT_RBUTTONDOWN:
|
if event == cv2.EVENT_RBUTTONDOWN:
|
||||||
if not self.is_image_mode: # Only for videos
|
if not self.is_image_mode: # Only for videos
|
||||||
# Convert screen coordinates to video coordinates
|
# Convert screen coordinates to video coordinates
|
||||||
video_x, video_y = self.screen_to_video_coords(x, y)
|
video_x, video_y = self.screen_to_video_coords(x, y)
|
||||||
self.motion_tracker.add_tracking_point(self.current_frame, video_x, video_y)
|
|
||||||
self.set_feedback_message(f"Tracking point added at frame {self.current_frame}")
|
# Check if there's a nearby point to remove
|
||||||
self.save_state() # Save state when tracking point is added
|
current_points = self.motion_tracker.get_tracking_points_for_frame(self.current_frame)
|
||||||
|
point_removed = False
|
||||||
|
|
||||||
|
for i, (px, py) in enumerate(current_points):
|
||||||
|
# Convert point to screen coordinates to check distance
|
||||||
|
# We need to calculate the same parameters as in display_current_frame
|
||||||
|
if self.current_display_frame is not None:
|
||||||
|
# Apply transformations to get display frame
|
||||||
|
display_frame = self.apply_crop_zoom_and_rotation(self.current_display_frame)
|
||||||
|
if display_frame is not None:
|
||||||
|
height, width = display_frame.shape[:2]
|
||||||
|
available_height = self.window_height - (0 if self.is_image_mode else self.TIMELINE_HEIGHT)
|
||||||
|
scale = min(self.window_width / width, available_height / height)
|
||||||
|
|
||||||
|
# Calculate display position
|
||||||
|
frame_height, frame_width = display_frame.shape[:2]
|
||||||
|
start_y = (available_height - frame_height) // 2
|
||||||
|
start_x = (self.window_width - frame_width) // 2
|
||||||
|
|
||||||
|
screen_px, screen_py = self.video_to_screen_coords(px, py, start_x, start_y, scale)
|
||||||
|
if screen_px is not None and screen_py is not None:
|
||||||
|
# Calculate distance in screen coordinates
|
||||||
|
distance = ((x - screen_px) ** 2 + (y - screen_py) ** 2) ** 0.5
|
||||||
|
if distance < 20: # Within 20 pixels
|
||||||
|
self.motion_tracker.remove_tracking_point(self.current_frame, i)
|
||||||
|
self.set_feedback_message(f"Tracking point removed at frame {self.current_frame}")
|
||||||
|
point_removed = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not point_removed:
|
||||||
|
# No nearby point found, add a new one
|
||||||
|
self.motion_tracker.add_tracking_point(self.current_frame, video_x, video_y)
|
||||||
|
self.set_feedback_message(f"Tracking point added at frame {self.current_frame}")
|
||||||
|
|
||||||
|
self.save_state() # Save state when tracking point is modified
|
||||||
|
|
||||||
# Handle scroll wheel for zoom (Ctrl + scroll)
|
# Handle scroll wheel for zoom (Ctrl + scroll)
|
||||||
if flags & cv2.EVENT_FLAG_CTRLKEY:
|
if flags & cv2.EVENT_FLAG_CTRLKEY:
|
||||||
@@ -2344,6 +2383,50 @@ class VideoEditor:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def get_rotated_direction(self, hjkl_key: str) -> str:
|
||||||
|
"""Map HJKL keys to actual directions based on current rotation"""
|
||||||
|
# Normalize rotation to 0-270 degrees
|
||||||
|
rotation = self.rotation_angle % 360
|
||||||
|
|
||||||
|
if hjkl_key == 'h': # Left
|
||||||
|
if rotation == 0:
|
||||||
|
return 'left'
|
||||||
|
elif rotation == 90:
|
||||||
|
return 'down'
|
||||||
|
elif rotation == 180:
|
||||||
|
return 'right'
|
||||||
|
elif rotation == 270:
|
||||||
|
return 'up'
|
||||||
|
elif hjkl_key == 'j': # Down
|
||||||
|
if rotation == 0:
|
||||||
|
return 'down'
|
||||||
|
elif rotation == 90:
|
||||||
|
return 'left'
|
||||||
|
elif rotation == 180:
|
||||||
|
return 'up'
|
||||||
|
elif rotation == 270:
|
||||||
|
return 'right'
|
||||||
|
elif hjkl_key == 'k': # Up
|
||||||
|
if rotation == 0:
|
||||||
|
return 'up'
|
||||||
|
elif rotation == 90:
|
||||||
|
return 'right'
|
||||||
|
elif rotation == 180:
|
||||||
|
return 'down'
|
||||||
|
elif rotation == 270:
|
||||||
|
return 'left'
|
||||||
|
elif hjkl_key == 'l': # Right
|
||||||
|
if rotation == 0:
|
||||||
|
return 'right'
|
||||||
|
elif rotation == 90:
|
||||||
|
return 'up'
|
||||||
|
elif rotation == 180:
|
||||||
|
return 'left'
|
||||||
|
elif rotation == 270:
|
||||||
|
return 'down'
|
||||||
|
|
||||||
|
return hjkl_key # Fallback to original if not recognized
|
||||||
|
|
||||||
def adjust_crop_size(self, direction: str, expand: bool, amount: int = None):
|
def adjust_crop_size(self, direction: str, expand: bool, amount: int = None):
|
||||||
"""
|
"""
|
||||||
Adjust crop size in given direction
|
Adjust crop size in given direction
|
||||||
@@ -3168,31 +3251,39 @@ class VideoEditor:
|
|||||||
print("No render operation to cancel")
|
print("No render operation to cancel")
|
||||||
|
|
||||||
# Individual direction controls using shift combinations we can detect
|
# Individual direction controls using shift combinations we can detect
|
||||||
elif key == ord("J"): # Shift+i - expand up
|
elif key == ord("J"): # Shift+j - expand up (relative to rotation)
|
||||||
self.adjust_crop_size('up', False)
|
direction = self.get_rotated_direction('j')
|
||||||
|
self.adjust_crop_size(direction, False)
|
||||||
print(f"Expanded crop upward by {self.crop_size_step}px")
|
print(f"Expanded crop upward by {self.crop_size_step}px")
|
||||||
elif key == ord("K"): # Shift+k - expand down
|
elif key == ord("K"): # Shift+k - expand down (relative to rotation)
|
||||||
self.adjust_crop_size('down', False)
|
direction = self.get_rotated_direction('k')
|
||||||
|
self.adjust_crop_size(direction, False)
|
||||||
print(f"Expanded crop downward by {self.crop_size_step}px")
|
print(f"Expanded crop downward by {self.crop_size_step}px")
|
||||||
elif key == ord("L"): # Shift+j - expand left
|
elif key == ord("L"): # Shift+l - expand right (relative to rotation)
|
||||||
self.adjust_crop_size('left', False)
|
direction = self.get_rotated_direction('l')
|
||||||
print(f"Expanded crop leftward by {self.crop_size_step}px")
|
self.adjust_crop_size(direction, False)
|
||||||
elif key == ord("H"): # Shift+l - expand right
|
|
||||||
self.adjust_crop_size('right', False)
|
|
||||||
print(f"Expanded crop rightward by {self.crop_size_step}px")
|
print(f"Expanded crop rightward by {self.crop_size_step}px")
|
||||||
|
elif key == ord("H"): # Shift+h - expand left (relative to rotation)
|
||||||
|
direction = self.get_rotated_direction('h')
|
||||||
|
self.adjust_crop_size(direction, False)
|
||||||
|
print(f"Expanded crop leftward by {self.crop_size_step}px")
|
||||||
|
|
||||||
# Contract in specific directions
|
# Contract in specific directions
|
||||||
elif key == ord("k"): # i - contract from bottom (reduce height from bottom)
|
elif key == ord("k"): # k - contract from bottom (relative to rotation)
|
||||||
self.adjust_crop_size('up', True)
|
direction = self.get_rotated_direction('k')
|
||||||
|
self.adjust_crop_size(direction, True)
|
||||||
print(f"Contracted crop from bottom by {self.crop_size_step}px")
|
print(f"Contracted crop from bottom by {self.crop_size_step}px")
|
||||||
elif key == ord("j"): # k - contract from top (reduce height from top)
|
elif key == ord("j"): # j - contract from top (relative to rotation)
|
||||||
self.adjust_crop_size('down', True)
|
direction = self.get_rotated_direction('j')
|
||||||
|
self.adjust_crop_size(direction, True)
|
||||||
print(f"Contracted crop from top by {self.crop_size_step}px")
|
print(f"Contracted crop from top by {self.crop_size_step}px")
|
||||||
elif key == ord("h"): # j - contract from right (reduce width from right)
|
elif key == ord("h"): # h - contract from right (relative to rotation)
|
||||||
self.adjust_crop_size('left', True)
|
direction = self.get_rotated_direction('h')
|
||||||
|
self.adjust_crop_size(direction, True)
|
||||||
print(f"Contracted crop from right by {self.crop_size_step}px")
|
print(f"Contracted crop from right by {self.crop_size_step}px")
|
||||||
elif key == ord("l"): # l - contract from left (reduce width from left)
|
elif key == ord("l"): # l - contract from left (relative to rotation)
|
||||||
self.adjust_crop_size('right', True)
|
direction = self.get_rotated_direction('l')
|
||||||
|
self.adjust_crop_size(direction, True)
|
||||||
print(f"Contracted crop from left by {self.crop_size_step}px")
|
print(f"Contracted crop from left by {self.crop_size_step}px")
|
||||||
|
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user