diff --git a/croppa/main.py b/croppa/main.py index 7f7a649..eb725ad 100644 --- a/croppa/main.py +++ b/croppa/main.py @@ -1175,11 +1175,18 @@ class VideoEditor: if point is None: return None - x, y = point + x, y = float(point[0]), float(point[1]) # Step 1: Apply crop (adjust point relative to crop origin) if self.crop_rect: - crop_x, crop_y, _, _ = self.crop_rect + crop_x, crop_y, crop_w, crop_h = self.crop_rect + + # Check if point is inside the crop area + if not (crop_x <= x < crop_x + crop_w and crop_y <= y < crop_y + crop_h): + # Point is outside the crop area + return None + + # Adjust coordinates relative to crop origin x -= crop_x y -= crop_y @@ -1228,7 +1235,7 @@ class VideoEditor: if point is None or self.current_display_frame is None: return None - x, y = point + x, y = float(point[0]), float(point[1]) # Step 1: Reverse zoom if self.zoom_factor != 1.0: @@ -1239,9 +1246,10 @@ class VideoEditor: if self.rotation_angle != 0: # Get dimensions after crop but before rotation if self.crop_rect: - crop_w, crop_h = self.crop_rect[2], self.crop_rect[3] + crop_w, crop_h = float(self.crop_rect[2]), float(self.crop_rect[3]) else: crop_h, crop_w = self.current_display_frame.shape[:2] + crop_h, crop_w = float(crop_h), float(crop_w) # Apply inverse rotation to coordinates if self.rotation_angle == 90: @@ -1261,10 +1269,16 @@ class VideoEditor: # Step 3: Reverse crop (add crop offset) if self.crop_rect: - crop_x, crop_y, _, _ = self.crop_rect + crop_x, crop_y = float(self.crop_rect[0]), float(self.crop_rect[1]) x += crop_x y += crop_y + # Ensure coordinates are within the frame bounds + if self.current_display_frame is not None: + height, width = self.current_display_frame.shape[:2] + x = max(0, min(width - 1, x)) + y = max(0, min(height - 1, y)) + return (x, y) @@ -2003,29 +2017,60 @@ class VideoEditor: # Handle tracking points (Right-click) if event == cv2.EVENT_RBUTTONDOWN: - # Convert display coordinates to original frame coordinates - original_point = self.untransform_point((x, y)) - - if original_point: - # Check if clicking on an existing tracking point to remove it - removed = self.motion_tracker.remove_tracking_point( - self.current_frame, - original_point[0], - original_point[1], - self.tracking_point_distance - ) - - if not removed: - # If no point was removed, add a new tracking point - self.motion_tracker.add_tracking_point( - self.current_frame, - original_point[0], - original_point[1] - ) + # First, calculate the canvas offset and scale for the current frame + if self.current_display_frame is not None: + # Get dimensions of the transformed frame + display_frame = self.apply_crop_zoom_and_rotation(self.current_display_frame) + if display_frame is None: + return - # Save state when tracking points change - self.save_state() - self.display_needs_update = True + display_height, display_width = display_frame.shape[:2] + available_height = self.window_height - (0 if self.is_image_mode else self.TIMELINE_HEIGHT) + + # Calculate scale and offset + scale = min(self.window_width / display_width, available_height / display_height) + if scale < 1.0: + final_display_width = int(display_width * scale) + final_display_height = int(display_height * scale) + else: + final_display_width = display_width + final_display_height = display_height + scale = 1.0 + + start_x = (self.window_width - final_display_width) // 2 + start_y = (available_height - final_display_height) // 2 + + # Check if click is within the frame area + if (start_x <= x < start_x + final_display_width and + start_y <= y < start_y + final_display_height): + + # Convert screen coordinates to display frame coordinates + display_x = (x - start_x) / scale + display_y = (y - start_y) / scale + + # Now convert display coordinates to original frame coordinates + original_point = self.untransform_point((display_x, display_y)) + + if original_point: + # Check if clicking on an existing tracking point to remove it + removed = self.motion_tracker.remove_tracking_point( + self.current_frame, + original_point[0], + original_point[1], + self.tracking_point_distance + ) + + if not removed: + # If no point was removed, add a new tracking point + self.motion_tracker.add_tracking_point( + self.current_frame, + original_point[0], + original_point[1] + ) + + # Save state when tracking points change + self.save_state() + self.display_needs_update = True # Handle zoom center (Ctrl + click) if flags & cv2.EVENT_FLAG_CTRLKEY and event == cv2.EVENT_LBUTTONDOWN: @@ -2999,18 +3044,38 @@ class VideoEditor: self.motion_tracker.stop_tracking() print("Motion tracking disabled") else: - # Start tracking with current crop and zoom center - base_zoom_center = self.zoom_center - if not base_zoom_center and self.current_display_frame is not None: - # Use frame center if no zoom center is set - h, w = self.current_display_frame.shape[:2] - base_zoom_center = (w // 2, h // 2) + # If we have tracking points, start tracking + if self.motion_tracker.has_tracking_points(): + # Get the current interpolated position to use as base + current_pos = self.motion_tracker.get_interpolated_position(self.current_frame) - self.motion_tracker.start_tracking( - self.crop_rect, - base_zoom_center - ) - print("Motion tracking enabled") + # Use crop center if we have a crop rect + if self.crop_rect: + x, y, w, h = self.crop_rect + crop_center = (x + w//2, y + h//2) + + # If we have a current position from tracking points, use that as base + if current_pos: + # The base zoom center is the current position + base_zoom_center = current_pos + else: + # Use crop center as fallback + base_zoom_center = crop_center + else: + # No crop rect, use frame center + if self.current_display_frame is not None: + h, w = self.current_display_frame.shape[:2] + base_zoom_center = (w // 2, h // 2) + else: + base_zoom_center = None + + self.motion_tracker.start_tracking( + self.crop_rect, + base_zoom_center + ) + print("Motion tracking enabled") + else: + print("No tracking points available. Add tracking points with right-click first.") self.save_state() else: # V - Clear all tracking points self.motion_tracker.clear_tracking_points() diff --git a/croppa/tracking.py b/croppa/tracking.py index ff3b6ac..14615cb 100644 --- a/croppa/tracking.py +++ b/croppa/tracking.py @@ -126,7 +126,13 @@ class MotionTracker: """Start motion tracking with base positions""" self.tracking_enabled = True self.base_crop_rect = base_crop_rect - self.base_zoom_center = base_zoom_center + + # If no base_zoom_center is provided, use the center of the crop rect + if base_zoom_center is None and base_crop_rect is not None: + x, y, w, h = base_crop_rect + self.base_zoom_center = (x + w//2, y + h//2) + else: + self.base_zoom_center = base_zoom_center def stop_tracking(self): """Stop motion tracking"""