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.
This commit is contained in:
2025-09-17 15:36:05 +02:00
parent 68a1cc3e7d
commit c3e0088a60

View File

@@ -1132,26 +1132,31 @@ class VideoEditor:
next_frame = min(next_frames)
return next_frame, self.tracking_points[next_frame]
def _point_to_line_distance(self, px, py, x1, y1, x2, y2):
"""Calculate distance from point (px, py) to line segment (x1, y1) to (x2, y2)"""
# Vector from line start to end
line_dx = x2 - x1
line_dy = y2 - y1
line_length_sq = line_dx * line_dx + line_dy * line_dy
def _point_to_line_distance_and_foot(self, px, py, x1, y1, x2, y2):
"""Calculate distance from point (px, py) to infinite line (x1, y1) to (x2, y2) and return foot of perpendicular"""
# Convert line to general form: Ax + By + C = 0
# (y2 - y1)(x - x1) - (x2 - x1)(y - y1) = 0
A = y2 - y1
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
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)
t = max(0, min(1, ((px - x1) * line_dx + (py - y1) * line_dy) / line_length_sq))
distance = abs(A * px + B * py + C) / denominator
# Closest point on line segment
closest_x = x1 + t * line_dx
closest_y = y1 + t * line_dy
# Calculate foot of perpendicular: (xf, yf)
# xf = xu - A(Axu + Byu + C)/(A^2 + B^2)
# 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 ((px - closest_x) ** 2 + (py - closest_y) ** 2) ** 0.5
return distance, (xf, yf)
def advance_frame(self) -> bool:
"""Advance to next frame - handles playback speed and marker looping"""
@@ -2183,7 +2188,7 @@ class VideoEditor:
best_snap_point = None
# 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:
sxp, syp = self._map_rotated_to_screen(px, py)
distance = ((sxp - x) ** 2 + (syp - y) ** 2) ** 0.5
@@ -2191,50 +2196,76 @@ class VideoEditor:
best_snap_distance = distance
best_snap_point = (int(px), int(py))
# Check for line snapping between consecutive tracking points
tracking_frames = sorted(self.tracking_points.keys())
for i in range(len(tracking_frames) - 1):
frame1 = tracking_frames[i]
frame2 = tracking_frames[i + 1]
points1 = self.tracking_points[frame1]
points2 = self.tracking_points[frame2]
# Check for line snapping between previous and next tracking points (the actual cyan arrows)
prev_result = self._get_previous_tracking_point()
next_result = self._get_next_tracking_point()
# Check each corresponding pair of points
for j in range(min(len(points1), len(points2))):
px1, py1 = points1[j]
px2, py2 = points2[j]
print(f"DEBUG: Line snapping - prev_result: {prev_result}, next_result: {next_result}")
if prev_result and next_result:
prev_frame, prev_pts = prev_result
next_frame, next_pts = next_result
print(f"DEBUG: Checking line between prev frame {prev_frame} and next frame {next_frame}")
# Check each corresponding pair of points between previous and next
for j in range(min(len(prev_pts), len(next_pts))):
px1, py1 = prev_pts[j]
px2, py2 = next_pts[j]
# Convert to screen coordinates
sx1, sy1 = self._map_rotated_to_screen(px1, py1)
sx2, sy2 = self._map_rotated_to_screen(px2, py2)
# Calculate distance to line segment
line_distance = self._point_to_line_distance(x, y, sx1, sy1, sx2, sy2)
# 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)
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})")
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}")
# Clamp the foot to the line segment
# Calculate parameter t for the foot point
line_dx = sx2 - sx1
line_dy = sy2 - sy1
line_length_sq = line_dx * line_dx + line_dy * line_dy
if line_length_sq > 0:
t = max(0, min(1, ((x - sx1) * line_dx + (y - sy1) * line_dy) / line_length_sq))
closest_sx = sx1 + t * line_dx
closest_sy = sy1 + t * line_dy
t = ((foot_x - sx1) * line_dx + (foot_y - sy1) * line_dy) / line_length_sq
t = max(0, min(1, t)) # Clamp to [0, 1]
# Convert back to rotated coordinates
closest_rx, closest_ry = self._map_screen_to_rotated(int(closest_sx), int(closest_sy))
# 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:
print(f"DEBUG: No prev/next points found for line snapping")
# Apply the best snap if found
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)
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 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.show_feedback_message("Tracking point added")