Compare commits

..

2 Commits

Author SHA1 Message Date
0570256f65 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:37:12 +02:00
68a1cc3e7d Enhance snapping functionality in VideoEditor to include line snapping
This commit improves the snapping feature in the VideoEditor class by adding the ability to snap new tracking points to the closest point on the line segment between consecutive tracking points, in addition to existing point snapping. The distance calculation logic has been refined, and the specification has been updated to reflect the new snapping behavior, which now operates within a 10px radius for both point and line snapping, enhancing the precision of motion tracking.
2025-09-17 15:12:35 +02:00
2 changed files with 108 additions and 13 deletions

View File

@@ -510,7 +510,7 @@ class VideoEditor:
CROP_SIZE_STEP = 15 # pixels to expand/contract crop CROP_SIZE_STEP = 15 # pixels to expand/contract crop
# Motion tracking settings # Motion tracking settings
TRACKING_POINT_THRESHOLD = 10 # pixels for delete/snap radius TRACKING_POINT_THRESHOLD = 20 # pixels for delete/snap radius
# Seek frame counts # Seek frame counts
SEEK_FRAMES_CTRL = 60 # Ctrl modifier: 60 frames SEEK_FRAMES_CTRL = 60 # Ctrl modifier: 60 frames
@@ -1132,6 +1132,32 @@ 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_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)
# 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
distance = ((px - x1) ** 2 + (py - y1) ** 2) ** 0.5
return distance, (x1, y1)
distance = abs(A * px + B * py + C) / denominator
# 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)
return distance, (xf, yf)
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"""
if not self.is_playing: if not self.is_playing:
@@ -2155,23 +2181,91 @@ class VideoEditor:
removed = True removed = True
break break
# If not removed, check for snapping to nearby points from other frames # If not removed, check for snapping to nearby points or lines from other frames
if not removed: if not removed:
snapped = False snapped = False
# Check all tracking points from all frames for snapping best_snap_distance = float('inf')
for _, points in self.tracking_points.items(): best_snap_point = None
# Check all tracking points from all frames for point snapping
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)
if (sxp - x) ** 2 + (syp - y) ** 2 <= threshold ** 2: distance = ((sxp - x) ** 2 + (syp - y) ** 2) ** 0.5
# Snap to this existing point if distance <= threshold and distance < best_snap_distance:
self.tracking_points.setdefault(self.current_frame, []).append((int(px), int(py))) best_snap_distance = distance
best_snap_point = (int(px), int(py))
# 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()
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 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:
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 = ((foot_x - sx1) * line_dx + (foot_y - sy1) * line_dy) / line_length_sq
t = max(0, min(1, t)) # Clamp to [0, 1]
# 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 snapped = True
break else:
if snapped: print(f"DEBUG: No snap found, adding new point at: ({rx}, {ry})")
break
# 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")

View File

@@ -57,8 +57,9 @@ Be careful to save and load settings when navigating this way
### Motion Tracking ### Motion Tracking
- **Right-click**: Add tracking point (green circle with white border) - **Right-click**: Add tracking point (green circle with white border)
- **Right-click existing point**: Remove tracking point (within 50px) - **Right-click existing point**: Remove tracking point (within 10px)
- **Right-click near existing point**: Snap to existing point from any frame (within 50px radius) - **Right-click near existing point**: Snap to existing point from any frame (within 10px radius)
- **Right-click near motion path**: Snap to closest point on yellow arrow line between tracking points (within 10px radius)
- **v**: Toggle motion tracking on/off - **v**: Toggle motion tracking on/off
- **V**: Clear all tracking points - **V**: Clear all tracking points
- **Blue cross**: Shows computed tracking position - **Blue cross**: Shows computed tracking position