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:
101
croppa/main.py
101
croppa/main.py
@@ -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"""
|
||||||
@@ -2183,7 +2188,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,50 +2196,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 between previous and next tracking points (the actual cyan arrows)
|
||||||
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]
|
if prev_result and next_result:
|
||||||
|
prev_frame, prev_pts = prev_result
|
||||||
|
next_frame, next_pts = next_result
|
||||||
|
|
||||||
# Check each corresponding pair of points
|
print(f"DEBUG: Checking line between prev frame {prev_frame} and next frame {next_frame}")
|
||||||
for j in range(min(len(points1), len(points2))):
|
|
||||||
px1, py1 = points1[j]
|
# Check each corresponding pair of points between previous and next
|
||||||
px2, py2 = points2[j]
|
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
|
# 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 {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}")
|
||||||
|
|
||||||
|
# Clamp the foot to the line segment
|
||||||
|
# Calculate parameter t for the foot point
|
||||||
line_dx = sx2 - sx1
|
line_dx = sx2 - sx1
|
||||||
line_dy = sy2 - sy1
|
line_dy = sy2 - sy1
|
||||||
line_length_sq = line_dx * line_dx + line_dy * line_dy
|
line_length_sq = line_dx * line_dx + line_dy * line_dy
|
||||||
|
|
||||||
if line_length_sq > 0:
|
if line_length_sq > 0:
|
||||||
t = max(0, min(1, ((x - sx1) * line_dx + (y - sy1) * line_dy) / line_length_sq))
|
t = ((foot_x - sx1) * line_dx + (foot_y - sy1) * line_dy) / line_length_sq
|
||||||
closest_sx = sx1 + t * line_dx
|
t = max(0, min(1, t)) # Clamp to [0, 1]
|
||||||
closest_sy = sy1 + t * line_dy
|
|
||||||
|
|
||||||
# Convert back to rotated coordinates
|
# Calculate clamped foot point
|
||||||
closest_rx, closest_ry = self._map_screen_to_rotated(int(closest_sx), int(closest_sy))
|
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_distance = line_distance
|
||||||
best_snap_point = (int(closest_rx), int(closest_ry))
|
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
|
# 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")
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user