Compare commits
3 Commits
0570256f65
...
b123b12d0d
Author | SHA1 | Date | |
---|---|---|---|
b123b12d0d | |||
1bd935646e | |||
c3e0088a60 |
153
croppa/main.py
153
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"""
|
||||||
@@ -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:
|
||||||
|
Reference in New Issue
Block a user