Compare commits
2 Commits
498a1911b1
...
0570256f65
Author | SHA1 | Date | |
---|---|---|---|
0570256f65 | |||
68a1cc3e7d |
114
croppa/main.py
114
croppa/main.py
@@ -510,7 +510,7 @@ class VideoEditor:
|
||||
CROP_SIZE_STEP = 15 # pixels to expand/contract crop
|
||||
|
||||
# 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_FRAMES_CTRL = 60 # Ctrl modifier: 60 frames
|
||||
@@ -1132,6 +1132,32 @@ class VideoEditor:
|
||||
next_frame = min(next_frames)
|
||||
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:
|
||||
"""Advance to next frame - handles playback speed and marker looping"""
|
||||
if not self.is_playing:
|
||||
@@ -2155,23 +2181,91 @@ class VideoEditor:
|
||||
removed = True
|
||||
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:
|
||||
snapped = False
|
||||
# Check all tracking points from all frames for snapping
|
||||
for _, points in self.tracking_points.items():
|
||||
best_snap_distance = float('inf')
|
||||
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:
|
||||
sxp, syp = self._map_rotated_to_screen(px, py)
|
||||
if (sxp - x) ** 2 + (syp - y) ** 2 <= threshold ** 2:
|
||||
# Snap to this existing point
|
||||
self.tracking_points.setdefault(self.current_frame, []).append((int(px), int(py)))
|
||||
distance = ((sxp - x) ** 2 + (syp - y) ** 2) ** 0.5
|
||||
if distance <= threshold and distance < best_snap_distance:
|
||||
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
|
||||
break
|
||||
if snapped:
|
||||
break
|
||||
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")
|
||||
|
||||
|
@@ -57,8 +57,9 @@ Be careful to save and load settings when navigating this way
|
||||
|
||||
### Motion Tracking
|
||||
- **Right-click**: Add tracking point (green circle with white border)
|
||||
- **Right-click existing point**: Remove tracking point (within 50px)
|
||||
- **Right-click near existing point**: Snap to existing point from any frame (within 50px radius)
|
||||
- **Right-click existing point**: Remove tracking point (within 10px)
|
||||
- **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**: Clear all tracking points
|
||||
- **Blue cross**: Shows computed tracking position
|
||||
|
Reference in New Issue
Block a user