diff --git a/croppa/main.py b/croppa/main.py index 0c40d63..ebb44d5 100644 --- a/croppa/main.py +++ b/croppa/main.py @@ -1545,27 +1545,34 @@ class VideoEditor: cv2.line(canvas, (bl_x, bl_y), (tl_x, tl_y), (255, 0, 255), 1) # Process each tracking point - for i, point in enumerate(tracking_points): - print(f"draw_tracking_points: processing point {i}: {point}") + for i, tracking_point in enumerate(tracking_points): + # Get the original coordinates + orig_x, orig_y = tracking_point.original + print(f"draw_tracking_points: processing point {i}: original={tracking_point.original}") # Check if the point is within the frame bounds - is_in_frame = (0 <= point[0] < frame_width and 0 <= point[1] < frame_height) + is_in_frame = (0 <= orig_x < frame_width and 0 <= orig_y < frame_height) print(f"draw_tracking_points: point {i} is {'inside' if is_in_frame else 'outside'} frame bounds") # Check if the point is within the crop area (if cropping is active) is_in_crop = True if self.crop_rect: crop_x, crop_y, crop_w, crop_h = self.crop_rect - is_in_crop = (crop_x <= point[0] < crop_x + crop_w and - crop_y <= point[1] < crop_y + crop_h) + is_in_crop = (crop_x <= orig_x < crop_x + crop_w and + crop_y <= orig_y < crop_y + crop_h) print(f"draw_tracking_points: point {i} is {'inside' if is_in_crop else 'outside'} crop area") - # Transform point from original frame coordinates to display coordinates - display_point = self.transform_point(point) + # Get the display coordinates - either from stored value or transform now + if tracking_point.display: + # Use stored display coordinates + display_point = tracking_point.display + print(f"draw_tracking_points: using stored display coordinates {display_point}") + else: + # Transform point from original frame coordinates to display coordinates + display_point = self.transform_point(tracking_point.original) + print(f"draw_tracking_points: transformed to display coordinates {display_point}") if display_point is not None: - print(f"draw_tracking_points: point {i} transformed to {display_point}") - # Scale and offset the point to match the canvas x = int(offset_x + display_point[0] * scale) y = int(offset_y + display_point[1] * scale) @@ -2213,33 +2220,17 @@ class VideoEditor: if removed: print(f"mouse_callback: removed tracking point at {original_point}") else: - # If no point was removed, add a new tracking point + # Add a new tracking point with both original and display coordinates + # This is the key change - we store both coordinate systems self.motion_tracker.add_tracking_point( self.current_frame, original_point[0], - original_point[1] + original_point[1], + display_coords=(display_x, display_y) # Store the display coordinates directly ) - print(f"mouse_callback: added tracking point at {original_point}") + print(f"mouse_callback: added tracking point at {original_point} (display: {display_x}, {display_y})") - # Perform a round-trip verification to ensure our coordinate system is consistent - verification_point = self.transform_point(original_point) - if verification_point: - print(f"mouse_callback: verification - point transforms back to {verification_point}") - - # Calculate expected canvas position for verification - expected_x = int(start_x + verification_point[0] * scale) - expected_y = int(start_y + verification_point[1] * scale) - print(f"mouse_callback: verification - expected canvas position: ({expected_x}, {expected_y}), actual: ({x}, {y})") - - # Calculate the error between click and expected position - error_x = abs(expected_x - x) - error_y = abs(expected_y - y) - print(f"mouse_callback: verification - position error: ({error_x}, {error_y}) pixels") - - # If error is significant, print a warning - if error_x > 2 or error_y > 2: - print(f"WARNING: Significant coordinate transformation error detected!") - print(f"This may indicate a problem with the transform/untransform functions.") + # No need for verification - we're storing both coordinate systems directly # Save state when tracking points change self.save_state() diff --git a/croppa/tracking.py b/croppa/tracking.py index 3400b5f..39987b5 100644 --- a/croppa/tracking.py +++ b/croppa/tracking.py @@ -1,31 +1,55 @@ -from typing import List, Dict, Tuple, Optional +from typing import List, Dict, Tuple, Optional, NamedTuple + + +class TrackingPoint(NamedTuple): + """Represents a tracking point with both original and display coordinates""" + original: Tuple[float, float] # Original frame coordinates (x, y) + display: Optional[Tuple[float, float]] = None # Display coordinates after transformation (x, y) + + def __str__(self): + if self.display: + return f"TrackingPoint(orig={self.original}, display={self.display})" + return f"TrackingPoint(orig={self.original})" class MotionTracker: """Handles motion tracking for crop and pan operations""" def __init__(self): - self.tracking_points = {} # {frame_number: [(x, y), ...]} + self.tracking_points = {} # {frame_number: [TrackingPoint, ...]} self.tracking_enabled = False self.base_crop_rect = None # Original crop rect when tracking started self.base_zoom_center = None # Original zoom center when tracking started - def add_tracking_point(self, frame_number: int, x: int, y: int): - """Add a tracking point at the specified frame and coordinates""" + def add_tracking_point(self, frame_number: int, x: float, y: float, display_coords: Optional[Tuple[float, float]] = None): + """Add a tracking point at the specified frame and coordinates + + Args: + frame_number: The frame number to add the point to + x: Original x coordinate + y: Original y coordinate + display_coords: Optional display coordinates after transformation + """ if frame_number not in self.tracking_points: self.tracking_points[frame_number] = [] - self.tracking_points[frame_number].append((x, y)) + + # Store both original and display coordinates + point = TrackingPoint(original=(float(x), float(y)), display=display_coords) + print(f"Adding tracking point: {point}") + self.tracking_points[frame_number].append(point) - def remove_tracking_point(self, frame_number: int, x: int, y: int, radius: int = 50): + def remove_tracking_point(self, frame_number: int, x: float, y: float, radius: int = 50): """Remove a tracking point by frame and proximity to x,y""" if frame_number not in self.tracking_points: return False points = self.tracking_points[frame_number] - for i, (px, py) in enumerate(points): + for i, point in enumerate(points): + px, py = point.original # Calculate distance between points distance = ((px - x) ** 2 + (py - y) ** 2) ** 0.5 if distance <= radius: + print(f"Removing tracking point: {point}") del points[i] if not points: del self.tracking_points[frame_number] @@ -37,7 +61,7 @@ class MotionTracker: """Clear all tracking points""" self.tracking_points.clear() - def get_tracking_points_for_frame(self, frame_number: int) -> List[Tuple[int, int]]: + def get_tracking_points_for_frame(self, frame_number: int) -> List[TrackingPoint]: """Get all tracking points for a specific frame""" return self.tracking_points.get(frame_number, []) @@ -61,24 +85,24 @@ class MotionTracker: points = self.tracking_points[frame_number] if points: # Return average of all points at this frame - avg_x = sum(p[0] for p in points) / len(points) - avg_y = sum(p[1] for p in points) / len(points) + avg_x = sum(p.original[0] for p in points) / len(points) + avg_y = sum(p.original[1] for p in points) / len(points) return (avg_x, avg_y) # If frame is before first tracking point if frame_number < frames[0]: points = self.tracking_points[frames[0]] if points: - avg_x = sum(p[0] for p in points) / len(points) - avg_y = sum(p[1] for p in points) / len(points) + avg_x = sum(p.original[0] for p in points) / len(points) + avg_y = sum(p.original[1] for p in points) / len(points) return (avg_x, avg_y) # If frame is after last tracking point if frame_number > frames[-1]: points = self.tracking_points[frames[-1]] if points: - avg_x = sum(p[0] for p in points) / len(points) - avg_y = sum(p[1] for p in points) / len(points) + avg_x = sum(p.original[0] for p in points) / len(points) + avg_y = sum(p.original[1] for p in points) / len(points) return (avg_x, avg_y) # Find the two frames to interpolate between @@ -92,10 +116,10 @@ class MotionTracker: continue # Get average positions for each frame - avg_x1 = sum(p[0] for p in points1) / len(points1) - avg_y1 = sum(p[1] for p in points1) / len(points1) - avg_x2 = sum(p[0] for p in points2) / len(points2) - avg_y2 = sum(p[1] for p in points2) / len(points2) + avg_x1 = sum(p.original[0] for p in points1) / len(points1) + avg_y1 = sum(p.original[1] for p in points1) / len(points1) + avg_x2 = sum(p.original[0] for p in points2) / len(points2) + avg_y2 = sum(p.original[1] for p in points2) / len(points2) # Linear interpolation t = (frame_number - frame1) / (frame2 - frame1) @@ -154,8 +178,14 @@ class MotionTracker: def to_dict(self) -> Dict: """Convert to dictionary for serialization""" + # Convert TrackingPoint objects to tuples for serialization + serialized_points = {} + for frame_num, points in self.tracking_points.items(): + # Store only the original coordinates for serialization + serialized_points[frame_num] = [p.original for p in points] + return { - 'tracking_points': self.tracking_points, + 'tracking_points': serialized_points, 'tracking_enabled': self.tracking_enabled, 'base_crop_rect': self.base_crop_rect, 'base_zoom_center': self.base_zoom_center @@ -168,7 +198,8 @@ class MotionTracker: self.tracking_points = {} for frame_str, points in tracking_points_data.items(): frame_num = int(frame_str) # Convert string key to integer - self.tracking_points[frame_num] = points + # Convert tuples to TrackingPoint objects + self.tracking_points[frame_num] = [TrackingPoint(original=p) for p in points] self.tracking_enabled = data.get('tracking_enabled', False) self.base_crop_rect = data.get('base_crop_rect', None)