Compare commits
3 Commits
1aea3b8a6e
...
8c45b30bca
Author | SHA1 | Date | |
---|---|---|---|
8c45b30bca | |||
615a3dce0d | |||
1ce05d33ba |
149
croppa/main.py
149
croppa/main.py
@@ -1102,6 +1102,40 @@ class VideoEditor:
|
||||
print(f"DEBUG: Jump next tracking wrap from {current} -> {tracking_frames[0]}; tracking_frames={tracking_frames}")
|
||||
self.seek_to_frame(tracking_frames[0])
|
||||
|
||||
def _get_previous_tracking_point(self):
|
||||
"""Get the tracking point from the previous frame that has tracking points."""
|
||||
if self.is_image_mode or not self.tracking_points:
|
||||
return None
|
||||
|
||||
tracking_frames = sorted(k for k, v in self.tracking_points.items() if v and 0 <= k < self.total_frames)
|
||||
if not tracking_frames:
|
||||
return None
|
||||
|
||||
# Find the last frame with tracking points that's before current frame
|
||||
prev_frames = [f for f in tracking_frames if f < self.current_frame]
|
||||
if not prev_frames:
|
||||
return None
|
||||
|
||||
prev_frame = max(prev_frames)
|
||||
return prev_frame, self.tracking_points[prev_frame]
|
||||
|
||||
def _get_next_tracking_point(self):
|
||||
"""Get the tracking point from the next frame that has tracking points."""
|
||||
if self.is_image_mode or not self.tracking_points:
|
||||
return None
|
||||
|
||||
tracking_frames = sorted(k for k, v in self.tracking_points.items() if v and 0 <= k < self.total_frames)
|
||||
if not tracking_frames:
|
||||
return None
|
||||
|
||||
# Find the first frame with tracking points that's after current frame
|
||||
next_frames = [f for f in tracking_frames if f > self.current_frame]
|
||||
if not next_frames:
|
||||
return None
|
||||
|
||||
next_frame = min(next_frames)
|
||||
return next_frame, self.tracking_points[next_frame]
|
||||
|
||||
def advance_frame(self) -> bool:
|
||||
"""Advance to next frame - handles playback speed and marker looping"""
|
||||
if not self.is_playing:
|
||||
@@ -1957,10 +1991,13 @@ class VideoEditor:
|
||||
motion_text = (
|
||||
f" | Motion: {self.tracking_enabled}" if self.tracking_enabled else ""
|
||||
)
|
||||
autorepeat_text = (
|
||||
f" | Loop: ON" if self.looping_between_markers else ""
|
||||
)
|
||||
if self.is_image_mode:
|
||||
info_text = f"Image | Zoom: {self.zoom_factor:.1f}x{rotation_text}{brightness_text}{contrast_text}{motion_text}"
|
||||
else:
|
||||
info_text = f"Frame: {self.current_frame}/{self.total_frames} | Speed: {self.playback_speed:.1f}x | Zoom: {self.zoom_factor:.1f}x{seek_multiplier_text}{rotation_text}{brightness_text}{contrast_text}{motion_text} | {'Playing' if self.is_playing else 'Paused'}"
|
||||
info_text = f"Frame: {self.current_frame}/{self.total_frames} | Speed: {self.playback_speed:.1f}x | Zoom: {self.zoom_factor:.1f}x{seek_multiplier_text}{rotation_text}{brightness_text}{contrast_text}{motion_text}{autorepeat_text} | {'Playing' if self.is_playing else 'Paused'}"
|
||||
cv2.putText(
|
||||
canvas,
|
||||
info_text,
|
||||
@@ -2053,29 +2090,63 @@ class VideoEditor:
|
||||
cv2.circle(canvas, (sx, sy), 6, (255, 0, 0), -1)
|
||||
cv2.circle(canvas, (sx, sy), 6, (255, 255, 255), 1)
|
||||
|
||||
# Draw previous and next frame tracking points with 50% alpha
|
||||
# Draw previous and next tracking points with motion path visualization
|
||||
if not self.is_image_mode and self.tracking_points:
|
||||
# Previous frame tracking points (red)
|
||||
prev_frame = self.current_frame - 1
|
||||
if prev_frame in self.tracking_points:
|
||||
prev_pts = self.tracking_points[prev_frame]
|
||||
prev_result = self._get_previous_tracking_point()
|
||||
next_result = self._get_next_tracking_point()
|
||||
|
||||
# Draw motion path if we have both previous and next points
|
||||
if prev_result and next_result:
|
||||
prev_frame, prev_pts = prev_result
|
||||
next_frame, next_pts = next_result
|
||||
|
||||
# Draw lines between corresponding tracking points
|
||||
for i, (prev_rx, prev_ry) in enumerate(prev_pts):
|
||||
if i < len(next_pts):
|
||||
next_rx, next_ry = next_pts[i]
|
||||
prev_sx, prev_sy = self._map_rotated_to_screen(prev_rx, prev_ry)
|
||||
next_sx, next_sy = self._map_rotated_to_screen(next_rx, next_ry)
|
||||
|
||||
# Draw motion path line with arrow (thin and transparent)
|
||||
overlay = canvas.copy()
|
||||
cv2.line(overlay, (prev_sx, prev_sy), (next_sx, next_sy), (255, 255, 0), 1) # Thin yellow line
|
||||
|
||||
# Draw arrow head pointing from previous to next
|
||||
angle = np.arctan2(next_sy - prev_sy, next_sx - prev_sx)
|
||||
arrow_length = 12
|
||||
arrow_angle = np.pi / 6 # 30 degrees
|
||||
|
||||
# Calculate arrow head points
|
||||
arrow_x1 = int(next_sx - arrow_length * np.cos(angle - arrow_angle))
|
||||
arrow_y1 = int(next_sy - arrow_length * np.sin(angle - arrow_angle))
|
||||
arrow_x2 = int(next_sx - arrow_length * np.cos(angle + arrow_angle))
|
||||
arrow_y2 = int(next_sy - 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, (next_sx, next_sy), (arrow_x2, arrow_y2), (255, 255, 0), 1)
|
||||
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
|
||||
if prev_result:
|
||||
prev_frame, prev_pts = prev_result
|
||||
for (rx, ry) in prev_pts:
|
||||
sx, sy = self._map_rotated_to_screen(rx, ry)
|
||||
# Create overlay for alpha blending
|
||||
# Create overlay for alpha blending (more transparent)
|
||||
overlay = canvas.copy()
|
||||
cv2.circle(overlay, (sx, sy), 4, (0, 0, 255), -1) # Red circle
|
||||
cv2.addWeighted(overlay, 0.5, canvas, 0.5, 0, canvas)
|
||||
cv2.circle(overlay, (sx, sy), 5, (0, 0, 255), -1) # Red circle
|
||||
cv2.circle(overlay, (sx, sy), 5, (255, 255, 255), 1) # White border
|
||||
cv2.addWeighted(overlay, 0.4, canvas, 0.6, 0, canvas) # More transparent
|
||||
|
||||
# Next frame tracking points (green)
|
||||
next_frame = self.current_frame + 1
|
||||
if next_frame in self.tracking_points:
|
||||
next_pts = self.tracking_points[next_frame]
|
||||
# Next tracking point (magenta/purple) - from the next frame with tracking points after current
|
||||
if next_result:
|
||||
next_frame, next_pts = next_result
|
||||
for (rx, ry) in next_pts:
|
||||
sx, sy = self._map_rotated_to_screen(rx, ry)
|
||||
# Create overlay for alpha blending
|
||||
# Create overlay for alpha blending (more transparent)
|
||||
overlay = canvas.copy()
|
||||
cv2.circle(overlay, (sx, sy), 4, (0, 255, 0), -1) # Green circle
|
||||
cv2.addWeighted(overlay, 0.5, canvas, 0.5, 0, canvas)
|
||||
cv2.circle(overlay, (sx, sy), 5, (255, 0, 255), -1) # Magenta circle
|
||||
cv2.circle(overlay, (sx, sy), 5, (255, 255, 255), 1) # White border
|
||||
cv2.addWeighted(overlay, 0.4, canvas, 0.6, 0, canvas) # More transparent
|
||||
if self.tracking_enabled and not self.is_image_mode:
|
||||
interp = self._get_interpolated_tracking_position(self.current_frame)
|
||||
if interp:
|
||||
@@ -2273,6 +2344,44 @@ class VideoEditor:
|
||||
self.clear_transformation_cache()
|
||||
self.save_state() # Save state when crop is undone
|
||||
|
||||
def complete_reset(self):
|
||||
"""Complete reset of all transformations and settings"""
|
||||
# Reset crop
|
||||
if self.crop_rect:
|
||||
self.crop_history.append(self.crop_rect)
|
||||
self.crop_rect = None
|
||||
|
||||
# Reset zoom
|
||||
self.zoom_factor = 1.0
|
||||
self.zoom_center = None
|
||||
|
||||
# Reset rotation
|
||||
self.rotation_angle = 0
|
||||
|
||||
# Reset brightness and contrast
|
||||
self.brightness = 0
|
||||
self.contrast = 1.0
|
||||
|
||||
# Reset motion tracking
|
||||
self.tracking_enabled = False
|
||||
self.tracking_points = {}
|
||||
|
||||
# Reset cut markers
|
||||
self.cut_start_frame = None
|
||||
self.cut_end_frame = None
|
||||
self.looping_between_markers = False
|
||||
|
||||
# Reset display offset
|
||||
self.display_offset = [0, 0]
|
||||
|
||||
# Clear transformation cache
|
||||
self.clear_transformation_cache()
|
||||
|
||||
# Save state
|
||||
self.save_state()
|
||||
|
||||
print("Complete reset applied - all transformations and markers cleared")
|
||||
|
||||
def toggle_marker_looping(self):
|
||||
"""Toggle looping between cut markers"""
|
||||
# Check if both markers are set
|
||||
@@ -2818,7 +2927,8 @@ class VideoEditor:
|
||||
print(" h/j/k/l: Contract crop (left/down/up/right)")
|
||||
print(" H/J/K/L: Expand crop (left/down/up/right)")
|
||||
print(" U: Undo crop")
|
||||
print(" C: Clear crop")
|
||||
print(" c: Clear crop")
|
||||
print(" C: Complete reset (crop, zoom, rotation, brightness, contrast, tracking)")
|
||||
print()
|
||||
print("Motion Tracking:")
|
||||
print(" Right-click: Add/remove tracking point (at current frame)")
|
||||
@@ -2854,7 +2964,8 @@ class VideoEditor:
|
||||
print(" h/j/k/l: Contract crop (left/down/up/right)")
|
||||
print(" H/J/K/L: Expand crop (left/down/up/right)")
|
||||
print(" U: Undo crop")
|
||||
print(" C: Clear crop")
|
||||
print(" c: Clear crop")
|
||||
print(" C: Complete reset (crop, zoom, rotation, brightness, contrast, tracking)")
|
||||
print()
|
||||
print("Other Controls:")
|
||||
print(" Ctrl+Scroll: Zoom in/out")
|
||||
@@ -3045,6 +3156,8 @@ class VideoEditor:
|
||||
self.zoom_factor = 1.0
|
||||
self.clear_transformation_cache()
|
||||
self.save_state() # Save state when crop is cleared
|
||||
elif key == ord("C"):
|
||||
self.complete_reset()
|
||||
elif key == ord("1"):
|
||||
# Cut markers only for videos
|
||||
if not self.is_image_mode:
|
||||
|
@@ -53,6 +53,7 @@ Be careful to save and load settings when navigating this way
|
||||
- **H/J/K/L**: Contract crop to left/down/up/right edges (15 pixels per keypress)
|
||||
- **u**: Undo last crop
|
||||
- **c**: Clear all cropping
|
||||
- **C**: Complete reset (crop, zoom, rotation, brightness, contrast, tracking points, cut markers)
|
||||
|
||||
### Motion Tracking
|
||||
- **Right-click**: Add tracking point (green circle with white border)
|
||||
@@ -62,7 +63,7 @@ Be careful to save and load settings when navigating this way
|
||||
- **Blue cross**: Shows computed tracking position
|
||||
- **Automatic interpolation**: Tracks between keyframes
|
||||
- **Crop follows**: Crop area centers on tracked object
|
||||
- **Display** Points are rendered as blue dots per frame, in addition dots are rendered on each frame for each dot on the previous (in red) and next (in green) frame
|
||||
- **Display** Points are rendered as blue dots per frame, in addition the previous tracking point (red) and next tracking point (magenta) are shown with yellow arrows indicating motion direction
|
||||
|
||||
#### Motion Tracking Navigation
|
||||
- **,**: Jump to previous tracking marker (previous frame that has one or more tracking points). Wrap-around supported.
|
||||
|
Reference in New Issue
Block a user