diff --git a/croppa/main.py b/croppa/main.py index 2b77972..066700d 100644 --- a/croppa/main.py +++ b/croppa/main.py @@ -1154,9 +1154,7 @@ class VideoEditor: return (x, y, w, h) def _get_interpolated_tracking_position(self, frame_number): - """Linear interpolation between keyed tracking points. - Returns (x, y) in original frame coords or None. - """ + """Linear interpolation in ROTATED frame coords. Returns (rx, ry) or None.""" if not self.tracking_points: return None frames = sorted(self.tracking_points.keys()) @@ -1299,6 +1297,38 @@ class VideoEditor: oy = max(0, min(int(round(oy)), self.frame_height - 1)) return ox, oy + def _map_rotated_to_screen(self, rx, ry): + """Map a point in ROTATED frame coords to canvas screen coords (post-crop).""" + # Subtract crop offset in rotated space + cx, cy, cw, ch = self._get_effective_crop_rect_for_frame(getattr(self, 'current_frame', 0)) + rx2 = rx - cx + ry2 = ry - cy + # Zoomed dimensions of cropped-rotated frame + new_w = int(cw * self.zoom_factor) + new_h = int(ch * self.zoom_factor) + cropped_due_to_zoom = (self.zoom_factor != 1.0) and (new_w > self.window_width or new_h > self.window_height) + if cropped_due_to_zoom: + offx_max = max(0, new_w - self.window_width) + offy_max = max(0, new_h - self.window_height) + offx = max(0, min(int(self.display_offset[0]), offx_max)) + offy = max(0, min(int(self.display_offset[1]), offy_max)) + else: + offx = 0 + offy = 0 + zx = rx2 * self.zoom_factor - offx + zy = ry2 * self.zoom_factor - offy + visible_w = new_w if not cropped_due_to_zoom else min(new_w, self.window_width) + visible_h = new_h if not cropped_due_to_zoom else min(new_h, self.window_height) + available_height = self.window_height - (0 if self.is_image_mode else self.TIMELINE_HEIGHT) + scale_canvas = min(self.window_width / max(1, visible_w), available_height / max(1, visible_h)) + final_w = int(visible_w * scale_canvas) + final_h = int(visible_h * scale_canvas) + start_x_canvas = (self.window_width - final_w) // 2 + start_y_canvas = (available_height - final_h) // 2 + sx = int(round(start_x_canvas + zx * scale_canvas)) + sy = int(round(start_y_canvas + zy * scale_canvas)) + return sx, sy + def _map_screen_to_rotated(self, sx, sy): """Map a point on canvas screen coords back to ROTATED frame coords (pre-crop).""" frame_number = getattr(self, 'current_frame', 0) @@ -1969,16 +1999,16 @@ class VideoEditor: 1, ) - # Draw tracking overlays (points and interpolated cross) + # Draw tracking overlays (points and interpolated cross), points stored in ROTATED space pts = self.tracking_points.get(self.current_frame, []) if not self.is_image_mode else [] - for (ox, oy) in pts: - sx, sy = self._map_original_to_screen(ox, oy) + for (rx, ry) in pts: + sx, sy = self._map_rotated_to_screen(rx, ry) cv2.circle(canvas, (sx, sy), 6, (0, 255, 0), -1) cv2.circle(canvas, (sx, sy), 6, (255, 255, 255), 1) if self.tracking_enabled and not self.is_image_mode: interp = self._get_interpolated_tracking_position(self.current_frame) if interp: - sx, sy = self._map_original_to_screen(interp[0], interp[1]) + sx, sy = self._map_rotated_to_screen(interp[0], interp[1]) cv2.line(canvas, (sx - 10, sy), (sx + 10, sy), (255, 0, 0), 2) cv2.line(canvas, (sx, sy - 10), (sx, sy + 10), (255, 0, 0), 2) @@ -2045,13 +2075,14 @@ class VideoEditor: # Handle right-click for tracking points (no modifiers) if event == cv2.EVENT_RBUTTONDOWN and not (flags & (cv2.EVENT_FLAG_CTRLKEY | cv2.EVENT_FLAG_SHIFTKEY)): if not self.is_image_mode: - ox, oy = self._map_screen_to_original(x, y) + # Store tracking points in ROTATED frame coordinates (pre-crop) + rx, ry = self._map_screen_to_rotated(x, y) threshold = 50 removed = False if self.current_frame in self.tracking_points: pts_screen = [] for idx, (px, py) in enumerate(self.tracking_points[self.current_frame]): - sxp, syp = self._map_original_to_screen(px, py) + sxp, syp = self._map_rotated_to_screen(px, py) pts_screen.append((idx, sxp, syp)) for idx, sxp, syp in pts_screen: if (sxp - x) ** 2 + (syp - y) ** 2 <= threshold ** 2: @@ -2062,7 +2093,7 @@ class VideoEditor: removed = True break if not removed: - self.tracking_points.setdefault(self.current_frame, []).append((int(ox), int(oy))) + self.tracking_points.setdefault(self.current_frame, []).append((int(rx), int(ry))) self.show_feedback_message("Tracking point added") self.clear_transformation_cache() self.save_state()