diff --git a/croppa/main.py b/croppa/main.py index 9271fc5..c7f1c2e 100644 --- a/croppa/main.py +++ b/croppa/main.py @@ -193,7 +193,7 @@ class MotionTracker: return None 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: return (0.0, 0.0) @@ -201,7 +201,8 @@ class MotionTracker: if not current_pos: 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_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) processed_frame = self.apply_brightness_contrast(processed_frame) - # Apply crop + # Apply crop with motion tracking offset if self.crop_rect: x, y, w, h = self.crop_rect 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 x = max(0, min(x, processed_frame.shape[1] - 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 ) - # 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: - # 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 - start_x = max(0, self.display_offset[0] + tracking_offset_x) - start_y = max(0, self.display_offset[1] + tracking_offset_y) + start_x = max(0, self.display_offset[0]) + start_y = max(0, self.display_offset[1]) end_x = min(new_width, start_x + self.window_width) end_y = min(new_height, start_y + self.window_height) 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: 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 not self.is_image_mode: # Only for videos # Convert screen coordinates to video coordinates 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}") - self.save_state() # Save state when tracking point is added + + # Check if there's a nearby point to remove + 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) 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): """ Adjust crop size in given direction @@ -3168,31 +3251,39 @@ class VideoEditor: print("No render operation to cancel") # Individual direction controls using shift combinations we can detect - elif key == ord("J"): # Shift+i - expand up - self.adjust_crop_size('up', False) + elif key == ord("J"): # Shift+j - expand up (relative to rotation) + direction = self.get_rotated_direction('j') + self.adjust_crop_size(direction, False) print(f"Expanded crop upward by {self.crop_size_step}px") - elif key == ord("K"): # Shift+k - expand down - self.adjust_crop_size('down', False) + elif key == ord("K"): # Shift+k - expand down (relative to rotation) + direction = self.get_rotated_direction('k') + self.adjust_crop_size(direction, False) print(f"Expanded crop downward by {self.crop_size_step}px") - elif key == ord("L"): # Shift+j - expand left - self.adjust_crop_size('left', False) - print(f"Expanded crop leftward by {self.crop_size_step}px") - elif key == ord("H"): # Shift+l - expand right - self.adjust_crop_size('right', False) + elif key == ord("L"): # Shift+l - expand right (relative to rotation) + direction = self.get_rotated_direction('l') + self.adjust_crop_size(direction, False) 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 - elif key == ord("k"): # i - contract from bottom (reduce height from bottom) - self.adjust_crop_size('up', True) + elif key == ord("k"): # k - contract from bottom (relative to rotation) + direction = self.get_rotated_direction('k') + self.adjust_crop_size(direction, True) print(f"Contracted crop from bottom by {self.crop_size_step}px") - elif key == ord("j"): # k - contract from top (reduce height from top) - self.adjust_crop_size('down', True) + elif key == ord("j"): # j - contract from top (relative to rotation) + direction = self.get_rotated_direction('j') + self.adjust_crop_size(direction, True) print(f"Contracted crop from top by {self.crop_size_step}px") - elif key == ord("h"): # j - contract from right (reduce width from right) - self.adjust_crop_size('left', True) + elif key == ord("h"): # h - contract from right (relative to rotation) + direction = self.get_rotated_direction('h') + self.adjust_crop_size(direction, True) 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', True) + elif key == ord("l"): # l - contract from left (relative to rotation) + direction = self.get_rotated_direction('l') + self.adjust_crop_size(direction, True) print(f"Contracted crop from left by {self.crop_size_step}px")