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:
2025-09-16 14:35:42 +02:00
parent 0b007b572e
commit 4960812cba

View File

@@ -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")