From 8c45b30bca60f9a321dff9e77e613788d05c0317 Mon Sep 17 00:00:00 2001 From: PhatPhuckDave Date: Wed, 17 Sep 2025 14:31:35 +0200 Subject: [PATCH] Enhance VideoEditor with motion path visualization for tracking points This commit updates the VideoEditor class to visualize the motion path between previous and next tracking points using yellow arrows. The rendering of tracking points has been modified, with previous points displayed as red circles and next points as magenta circles, both with white borders for better visibility. The specification has been updated to reflect these changes in the display of tracking points and their motion direction. --- croppa/main.py | 54 ++++++++++++++++++++++++++++++++++++++++---------- croppa/spec.md | 2 +- 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/croppa/main.py b/croppa/main.py index 5512a99..1c3d162 100644 --- a/croppa/main.py +++ b/croppa/main.py @@ -2090,29 +2090,63 @@ class VideoEditor: cv2.circle(canvas, (sx, sy), 6, (255, 0, 0), -1) cv2.circle(canvas, (sx, sy), 6, (255, 255, 255), 1) - # Draw previous and next tracking points with 50% alpha + # Draw previous and next tracking points with motion path visualization if not self.is_image_mode and self.tracking_points: - # Previous tracking point (red) - from the most recent frame with tracking points before current prev_result = self._get_previous_tracking_point() + next_result = self._get_next_tracking_point() + + # Draw motion path if we have both previous and next points + if prev_result and next_result: + prev_frame, prev_pts = prev_result + next_frame, next_pts = next_result + + # Draw lines between corresponding tracking points + for i, (prev_rx, prev_ry) in enumerate(prev_pts): + if i < len(next_pts): + next_rx, next_ry = next_pts[i] + prev_sx, prev_sy = self._map_rotated_to_screen(prev_rx, prev_ry) + next_sx, next_sy = self._map_rotated_to_screen(next_rx, next_ry) + + # Draw motion path line with arrow (thin and transparent) + overlay = canvas.copy() + cv2.line(overlay, (prev_sx, prev_sy), (next_sx, next_sy), (255, 255, 0), 1) # Thin yellow line + + # Draw arrow head pointing from previous to next + angle = np.arctan2(next_sy - prev_sy, next_sx - prev_sx) + arrow_length = 12 + arrow_angle = np.pi / 6 # 30 degrees + + # Calculate arrow head points + arrow_x1 = int(next_sx - arrow_length * np.cos(angle - arrow_angle)) + arrow_y1 = int(next_sy - arrow_length * np.sin(angle - arrow_angle)) + arrow_x2 = int(next_sx - arrow_length * np.cos(angle + arrow_angle)) + arrow_y2 = int(next_sy - arrow_length * np.sin(angle + arrow_angle)) + + cv2.line(overlay, (next_sx, next_sy), (arrow_x1, arrow_y1), (255, 255, 0), 1) + cv2.line(overlay, (next_sx, next_sy), (arrow_x2, arrow_y2), (255, 255, 0), 1) + cv2.addWeighted(overlay, 0.3, canvas, 0.7, 0, canvas) # Very transparent + + # Previous tracking point (red) - from the most recent frame with tracking points before current if prev_result: prev_frame, prev_pts = prev_result for (rx, ry) in prev_pts: sx, sy = self._map_rotated_to_screen(rx, ry) - # Create overlay for alpha blending + # Create overlay for alpha blending (more transparent) overlay = canvas.copy() - cv2.circle(overlay, (sx, sy), 4, (0, 0, 255), -1) # Red circle - cv2.addWeighted(overlay, 0.5, canvas, 0.5, 0, canvas) + cv2.circle(overlay, (sx, sy), 5, (0, 0, 255), -1) # Red circle + cv2.circle(overlay, (sx, sy), 5, (255, 255, 255), 1) # White border + cv2.addWeighted(overlay, 0.4, canvas, 0.6, 0, canvas) # More transparent - # Next tracking point (green) - from the next frame with tracking points after current - next_result = self._get_next_tracking_point() + # Next tracking point (magenta/purple) - from the next frame with tracking points after current if next_result: next_frame, next_pts = next_result for (rx, ry) in next_pts: sx, sy = self._map_rotated_to_screen(rx, ry) - # Create overlay for alpha blending + # Create overlay for alpha blending (more transparent) overlay = canvas.copy() - cv2.circle(overlay, (sx, sy), 4, (0, 255, 0), -1) # Green circle - cv2.addWeighted(overlay, 0.5, canvas, 0.5, 0, canvas) + cv2.circle(overlay, (sx, sy), 5, (255, 0, 255), -1) # Magenta circle + cv2.circle(overlay, (sx, sy), 5, (255, 255, 255), 1) # White border + cv2.addWeighted(overlay, 0.4, canvas, 0.6, 0, canvas) # More transparent if self.tracking_enabled and not self.is_image_mode: interp = self._get_interpolated_tracking_position(self.current_frame) if interp: diff --git a/croppa/spec.md b/croppa/spec.md index dc4cd0e..dc6f7cb 100644 --- a/croppa/spec.md +++ b/croppa/spec.md @@ -63,7 +63,7 @@ Be careful to save and load settings when navigating this way - **Blue cross**: Shows computed tracking position - **Automatic interpolation**: Tracks between keyframes - **Crop follows**: Crop area centers on tracked object -- **Display** Points are rendered as blue dots per frame, in addition the previous tracking point (red) and next tracking point (green) are shown regardless of which frame they're on +- **Display** Points are rendered as blue dots per frame, in addition the previous tracking point (red) and next tracking point (magenta) are shown with yellow arrows indicating motion direction #### Motion Tracking Navigation - **,**: Jump to previous tracking marker (previous frame that has one or more tracking points). Wrap-around supported.