Refactor motion path and snapping logic in VideoEditor

This commit enhances the VideoEditor class by refining the logic for drawing motion paths and checking line snapping between tracking points. It introduces a unified approach to handle both previous→next and previous→current lines, improving the clarity of the visual representation. Additionally, debug statements have been updated to provide better insights during the snapping process, ensuring accurate tracking point interactions. The display update method is also called to refresh the visual state after changes.
This commit is contained in:
2025-09-17 15:44:55 +02:00
parent c3e0088a60
commit 1bd935646e

View File

@@ -2033,35 +2033,42 @@ class VideoEditor:
prev_result = self._get_previous_tracking_point() prev_result = self._get_previous_tracking_point()
next_result = self._get_next_tracking_point() next_result = self._get_next_tracking_point()
# Draw motion path if we have both previous and next points # Draw motion path - either previous→next OR previous→current
line_to_draw = None
if prev_result and next_result: if prev_result and next_result:
prev_frame, prev_pts = prev_result # Draw previous→next line
next_frame, next_pts = next_result line_to_draw = ("prev_next", prev_result, next_result)
elif prev_result and self.current_frame in self.tracking_points:
# Draw previous→current line
line_to_draw = ("prev_current", prev_result, (self.current_frame, self.tracking_points[self.current_frame]))
if line_to_draw:
line_type, (frame1, pts1), (frame2, pts2) = line_to_draw
# Draw lines between corresponding tracking points # Draw lines between corresponding tracking points
for i, (prev_rx, prev_ry) in enumerate(prev_pts): for i, (px1, py1) in enumerate(pts1):
if i < len(next_pts): if i < len(pts2):
next_rx, next_ry = next_pts[i] px2, py2 = pts2[i]
prev_sx, prev_sy = self._map_rotated_to_screen(prev_rx, prev_ry) sx1, sy1 = self._map_rotated_to_screen(px1, py1)
next_sx, next_sy = self._map_rotated_to_screen(next_rx, next_ry) sx2, sy2 = self._map_rotated_to_screen(px2, py2)
# Draw motion path line with arrow (thin and transparent) # Draw motion path line with arrow (thin and transparent)
overlay = canvas.copy() overlay = canvas.copy()
cv2.line(overlay, (prev_sx, prev_sy), (next_sx, next_sy), (255, 255, 0), 1) # Thin yellow line cv2.line(overlay, (sx1, sy1), (sx2, sy2), (255, 255, 0), 1) # Thin yellow line
# Draw arrow head pointing from previous to next # Draw arrow head pointing from first to second point
angle = np.arctan2(next_sy - prev_sy, next_sx - prev_sx) angle = np.arctan2(sy2 - sy1, sx2 - sx1)
arrow_length = 12 arrow_length = 12
arrow_angle = np.pi / 6 # 30 degrees arrow_angle = np.pi / 6 # 30 degrees
# Calculate arrow head points # Calculate arrow head points
arrow_x1 = int(next_sx - arrow_length * np.cos(angle - arrow_angle)) arrow_x1 = int(sx2 - arrow_length * np.cos(angle - arrow_angle))
arrow_y1 = int(next_sy - arrow_length * np.sin(angle - arrow_angle)) arrow_y1 = int(sy2 - arrow_length * np.sin(angle - arrow_angle))
arrow_x2 = int(next_sx - arrow_length * np.cos(angle + arrow_angle)) arrow_x2 = int(sx2 - arrow_length * np.cos(angle + arrow_angle))
arrow_y2 = int(next_sy - arrow_length * np.sin(angle + arrow_angle)) arrow_y2 = int(sy2 - 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, (sx2, sy2), (arrow_x1, arrow_y1), (255, 255, 0), 1)
cv2.line(overlay, (next_sx, next_sy), (arrow_x2, arrow_y2), (255, 255, 0), 1) cv2.line(overlay, (sx2, sy2), (arrow_x2, arrow_y2), (255, 255, 0), 1)
cv2.addWeighted(overlay, 0.3, canvas, 0.7, 0, canvas) # Very transparent 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 # Previous tracking point (red) - from the most recent frame with tracking points before current
@@ -2196,22 +2203,30 @@ class VideoEditor:
best_snap_distance = distance best_snap_distance = distance
best_snap_point = (int(px), int(py)) best_snap_point = (int(px), int(py))
# Check for line snapping between previous and next tracking points (the actual cyan arrows) # Check for line snapping - either previous→next OR previous→current
prev_result = self._get_previous_tracking_point() prev_result = self._get_previous_tracking_point()
next_result = self._get_next_tracking_point() next_result = self._get_next_tracking_point()
print(f"DEBUG: Line snapping - prev_result: {prev_result}, next_result: {next_result}") print(f"DEBUG: Line snapping - prev_result: {prev_result}, next_result: {next_result}")
# Determine which line to check: previous→next OR previous→current
line_to_check = None
if prev_result and next_result: if prev_result and next_result:
prev_frame, prev_pts = prev_result # Check previous→next line
next_frame, next_pts = next_result line_to_check = ("prev_next", prev_result, next_result)
print(f"DEBUG: Checking prev→next line")
elif prev_result and self.current_frame in self.tracking_points:
# Check previous→current line
line_to_check = ("prev_current", prev_result, (self.current_frame, self.tracking_points[self.current_frame]))
print(f"DEBUG: Checking prev→current line")
if line_to_check:
line_type, (frame1, pts1), (frame2, pts2) = line_to_check
print(f"DEBUG: Checking line between prev frame {prev_frame} and next frame {next_frame}") # Check each corresponding pair of points
for j in range(min(len(pts1), len(pts2))):
# Check each corresponding pair of points between previous and next px1, py1 = pts1[j]
for j in range(min(len(prev_pts), len(next_pts))): px2, py2 = pts2[j]
px1, py1 = prev_pts[j]
px2, py2 = next_pts[j]
# Convert to screen coordinates # Convert to screen coordinates
sx1, sy1 = self._map_rotated_to_screen(px1, py1) sx1, sy1 = self._map_rotated_to_screen(px1, py1)
@@ -2220,35 +2235,19 @@ class VideoEditor:
# Calculate distance to infinite line and foot of perpendicular # Calculate distance to infinite line and foot of perpendicular
line_distance, (foot_x, foot_y) = self._point_to_line_distance_and_foot(x, y, sx1, sy1, sx2, sy2) line_distance, (foot_x, foot_y) = self._point_to_line_distance_and_foot(x, y, sx1, sy1, sx2, sy2)
print(f"DEBUG: Line {j}: ({sx1},{sy1}) to ({sx2},{sy2}), distance to click ({x},{y}) = {line_distance:.2f}, foot = ({foot_x:.1f}, {foot_y:.1f})") print(f"DEBUG: {line_type} Line {j}: ({sx1},{sy1}) to ({sx2},{sy2}), distance to click ({x},{y}) = {line_distance:.2f}, foot = ({foot_x:.1f}, {foot_y:.1f})")
if line_distance <= threshold and line_distance < best_snap_distance: if line_distance <= threshold and line_distance < best_snap_distance:
print(f"DEBUG: Line snap found! Distance {line_distance:.2f} <= threshold {threshold}") print(f"DEBUG: Line snap found! Distance {line_distance:.2f} <= threshold {threshold}")
# Clamp the foot to the line segment # Convert foot of perpendicular back to rotated coordinates (no clamping - infinite line)
# Calculate parameter t for the foot point closest_rx, closest_ry = self._map_screen_to_rotated(int(foot_x), int(foot_y))
line_dx = sx2 - sx1
line_dy = sy2 - sy1
line_length_sq = line_dx * line_dx + line_dy * line_dy
if line_length_sq > 0: best_snap_distance = line_distance
t = ((foot_x - sx1) * line_dx + (foot_y - sy1) * line_dy) / line_length_sq best_snap_point = (int(closest_rx), int(closest_ry))
t = max(0, min(1, t)) # Clamp to [0, 1] print(f"DEBUG: Best line snap point: ({closest_rx}, {closest_ry})")
# Calculate clamped foot point
clamped_foot_x = sx1 + t * line_dx
clamped_foot_y = sy1 + t * line_dy
print(f"DEBUG: Clamped foot from ({foot_x:.1f}, {foot_y:.1f}) to ({clamped_foot_x:.1f}, {clamped_foot_y:.1f})")
# Convert clamped foot back to rotated coordinates
closest_rx, closest_ry = self._map_screen_to_rotated(int(clamped_foot_x), int(clamped_foot_y))
best_snap_distance = line_distance
best_snap_point = (int(closest_rx), int(closest_ry))
print(f"DEBUG: Best line snap point: ({closest_rx}, {closest_ry})")
else: else:
print(f"DEBUG: No prev/next points found for line snapping") print(f"DEBUG: No line found for snapping")
# Apply the best snap if found # Apply the best snap if found
if best_snap_point: if best_snap_point:
@@ -2271,6 +2270,9 @@ class VideoEditor:
self.clear_transformation_cache() self.clear_transformation_cache()
self.save_state() self.save_state()
# Force immediate display update to recalculate previous/next points and arrows
self.display_current_frame()
# Handle scroll wheel: Ctrl+scroll -> zoom; plain scroll -> seek ±1 frame (independent of multiplier) # Handle scroll wheel: Ctrl+scroll -> zoom; plain scroll -> seek ±1 frame (independent of multiplier)
if event == cv2.EVENT_MOUSEWHEEL: if event == cv2.EVENT_MOUSEWHEEL: