Compare commits

...

3 Commits

Author SHA1 Message Date
b123b12d0d Refactor motion path logic in VideoEditor for clarity
This commit updates the VideoEditor class to improve the logic for drawing and checking motion paths between tracking points. The comments have been adjusted to clarify the conditions for drawing lines between previous→current and previous→next frames. Additionally, debug statements have been enhanced to provide clearer insights during the snapping process, ensuring better tracking point interactions.
2025-09-17 15:46:39 +02:00
1bd935646e 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.
2025-09-17 15:44:55 +02:00
c3e0088a60 Refactor point-to-line distance calculation in VideoEditor
This commit refines the distance calculation method in the VideoEditor class by introducing a new function, _point_to_line_distance_and_foot, which computes the distance from a point to an infinite line and returns the foot of the perpendicular. The snapping logic has been updated to utilize this new method, enhancing the accuracy of line snapping between tracking points. Additionally, debug statements have been added to assist in tracking the snapping process and verifying point conversions.
2025-09-17 15:38:57 +02:00

View File

@@ -1132,26 +1132,31 @@ class VideoEditor:
next_frame = min(next_frames) next_frame = min(next_frames)
return next_frame, self.tracking_points[next_frame] return next_frame, self.tracking_points[next_frame]
def _point_to_line_distance(self, px, py, x1, y1, x2, y2): def _point_to_line_distance_and_foot(self, px, py, x1, y1, x2, y2):
"""Calculate distance from point (px, py) to line segment (x1, y1) to (x2, y2)""" """Calculate distance from point (px, py) to infinite line (x1, y1) to (x2, y2) and return foot of perpendicular"""
# Vector from line start to end # Convert line to general form: Ax + By + C = 0
line_dx = x2 - x1 # (y2 - y1)(x - x1) - (x2 - x1)(y - y1) = 0
line_dy = y2 - y1 A = y2 - y1
line_length_sq = line_dx * line_dx + line_dy * line_dy B = -(x2 - x1) # Note the negative sign
C = -(A * x1 + B * y1)
if line_length_sq == 0: # Calculate distance: d = |Ax + By + C| / sqrt(A^2 + B^2)
denominator = (A * A + B * B) ** 0.5
if denominator == 0:
# Line is actually a point # Line is actually a point
return ((px - x1) ** 2 + (py - y1) ** 2) ** 0.5 distance = ((px - x1) ** 2 + (py - y1) ** 2) ** 0.5
return distance, (x1, y1)
# Parameter t for closest point on line (0 = start, 1 = end) distance = abs(A * px + B * py + C) / denominator
t = max(0, min(1, ((px - x1) * line_dx + (py - y1) * line_dy) / line_length_sq))
# Closest point on line segment # Calculate foot of perpendicular: (xf, yf)
closest_x = x1 + t * line_dx # xf = xu - A(Axu + Byu + C)/(A^2 + B^2)
closest_y = y1 + t * line_dy # yf = yu - B(Axu + Byu + C)/(A^2 + B^2)
numerator = A * px + B * py + C
xf = px - A * numerator / (A * A + B * B)
yf = py - B * numerator / (A * A + B * B)
# Distance to closest point return distance, (xf, yf)
return ((px - closest_x) ** 2 + (py - closest_y) ** 2) ** 0.5
def advance_frame(self) -> bool: def advance_frame(self) -> bool:
"""Advance to next frame - handles playback speed and marker looping""" """Advance to next frame - handles playback speed and marker looping"""
@@ -2028,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→current OR previous→next
if prev_result and next_result: line_to_draw = None
prev_frame, prev_pts = prev_result if prev_result and self.current_frame in self.tracking_points:
next_frame, next_pts = next_result # Draw previous→current line (we're on a frame with tracking points)
line_to_draw = ("prev_current", prev_result, (self.current_frame, self.tracking_points[self.current_frame]))
elif prev_result and next_result:
# Draw previous→next line (we're between frames)
line_to_draw = ("prev_next", prev_result, next_result)
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
@@ -2183,7 +2195,7 @@ class VideoEditor:
best_snap_point = None best_snap_point = None
# Check all tracking points from all frames for point snapping # Check all tracking points from all frames for point snapping
for _, points in self.tracking_points.items(): for frame_num, points in self.tracking_points.items():
for (px, py) in points: for (px, py) in points:
sxp, syp = self._map_rotated_to_screen(px, py) sxp, syp = self._map_rotated_to_screen(px, py)
distance = ((sxp - x) ** 2 + (syp - y) ** 2) ** 0.5 distance = ((sxp - x) ** 2 + (syp - y) ** 2) ** 0.5
@@ -2191,55 +2203,76 @@ 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 consecutive tracking points # Check for line snapping - either previous→next OR previous→current
tracking_frames = sorted(self.tracking_points.keys()) prev_result = self._get_previous_tracking_point()
for i in range(len(tracking_frames) - 1): next_result = self._get_next_tracking_point()
frame1 = tracking_frames[i]
frame2 = tracking_frames[i + 1] print(f"DEBUG: Line snapping - prev_result: {prev_result}, next_result: {next_result}")
points1 = self.tracking_points[frame1]
points2 = self.tracking_points[frame2] # Determine which line to check: previous→current OR previous→next
line_to_check = None
if prev_result and self.current_frame in self.tracking_points:
# Check previous→current line (we're on a frame with tracking points)
line_to_check = ("prev_current", prev_result, (self.current_frame, self.tracking_points[self.current_frame]))
print(f"DEBUG: Checking prev->current line")
elif prev_result and next_result:
# Check previous→next line (we're between frames)
line_to_check = ("prev_next", prev_result, next_result)
print(f"DEBUG: Checking prev->next line")
if line_to_check:
line_type, (frame1, pts1), (frame2, pts2) = line_to_check
# Check each corresponding pair of points # Check each corresponding pair of points
for j in range(min(len(points1), len(points2))): for j in range(min(len(pts1), len(pts2))):
px1, py1 = points1[j] px1, py1 = pts1[j]
px2, py2 = points2[j] px2, py2 = pts2[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)
sx2, sy2 = self._map_rotated_to_screen(px2, py2) sx2, sy2 = self._map_rotated_to_screen(px2, py2)
# Calculate distance to line segment # Calculate distance to infinite line and foot of perpendicular
line_distance = self._point_to_line_distance(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_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:
# Find the closest point on the line segment print(f"DEBUG: Line snap found! Distance {line_distance:.2f} <= threshold {threshold}")
line_dx = sx2 - sx1
line_dy = sy2 - sy1
line_length_sq = line_dx * line_dx + line_dy * line_dy
if line_length_sq > 0: # Convert foot of perpendicular back to rotated coordinates (no clamping - infinite line)
t = max(0, min(1, ((x - sx1) * line_dx + (y - sy1) * line_dy) / line_length_sq)) closest_rx, closest_ry = self._map_screen_to_rotated(int(foot_x), int(foot_y))
closest_sx = sx1 + t * line_dx
closest_sy = sy1 + t * line_dy best_snap_distance = line_distance
best_snap_point = (int(closest_rx), int(closest_ry))
# Convert back to rotated coordinates print(f"DEBUG: Best line snap point: ({closest_rx}, {closest_ry})")
closest_rx, closest_ry = self._map_screen_to_rotated(int(closest_sx), int(closest_sy)) else:
print(f"DEBUG: No line found for snapping")
best_snap_distance = line_distance
best_snap_point = (int(closest_rx), int(closest_ry))
# Apply the best snap if found # Apply the best snap if found
if best_snap_point: if best_snap_point:
print(f"DEBUG: Final best_snap_point: {best_snap_point} (distance: {best_snap_distance:.2f})")
self.tracking_points.setdefault(self.current_frame, []).append(best_snap_point) self.tracking_points.setdefault(self.current_frame, []).append(best_snap_point)
snapped = True snapped = True
else:
print(f"DEBUG: No snap found, adding new point at: ({rx}, {ry})")
# If no snapping, add new point at clicked location # If no snapping, add new point at clicked location
if not snapped: if not snapped:
print(f"DEBUG: No snap found, adding new point at: ({rx}, {ry})")
print(f"DEBUG: Click was at screen coords: ({x}, {y})")
print(f"DEBUG: Converted to rotated coords: ({rx}, {ry})")
# Verify the conversion
verify_sx, verify_sy = self._map_rotated_to_screen(rx, ry)
print(f"DEBUG: Verification - rotated ({rx}, {ry}) -> screen ({verify_sx}, {verify_sy})")
self.tracking_points.setdefault(self.current_frame, []).append((int(rx), int(ry))) self.tracking_points.setdefault(self.current_frame, []).append((int(rx), int(ry)))
# self.show_feedback_message("Tracking point added") # self.show_feedback_message("Tracking point added")
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: