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}")
|
print(f"DEBUG: Jump next tracking wrap from {current} -> {tracking_frames[0]}; tracking_frames={tracking_frames}")
|
||||||
self.seek_to_frame(tracking_frames[0])
|
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:
|
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:
|
||||||
@@ -1957,10 +1991,13 @@ class VideoEditor:
|
|||||||
motion_text = (
|
motion_text = (
|
||||||
f" | Motion: {self.tracking_enabled}" if self.tracking_enabled else ""
|
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:
|
if self.is_image_mode:
|
||||||
info_text = f"Image | Zoom: {self.zoom_factor:.1f}x{rotation_text}{brightness_text}{contrast_text}{motion_text}"
|
info_text = f"Image | Zoom: {self.zoom_factor:.1f}x{rotation_text}{brightness_text}{contrast_text}{motion_text}"
|
||||||
else:
|
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(
|
cv2.putText(
|
||||||
canvas,
|
canvas,
|
||||||
info_text,
|
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, 0, 0), -1)
|
||||||
cv2.circle(canvas, (sx, sy), 6, (255, 255, 255), 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:
|
if not self.is_image_mode and self.tracking_points:
|
||||||
# Previous frame tracking points (red)
|
prev_result = self._get_previous_tracking_point()
|
||||||
prev_frame = self.current_frame - 1
|
next_result = self._get_next_tracking_point()
|
||||||
if prev_frame in self.tracking_points:
|
|
||||||
prev_pts = self.tracking_points[prev_frame]
|
# 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:
|
for (rx, ry) in prev_pts:
|
||||||
sx, sy = self._map_rotated_to_screen(rx, ry)
|
sx, sy = self._map_rotated_to_screen(rx, ry)
|
||||||
# Create overlay for alpha blending
|
# Create overlay for alpha blending (more transparent)
|
||||||
overlay = canvas.copy()
|
overlay = canvas.copy()
|
||||||
cv2.circle(overlay, (sx, sy), 4, (0, 0, 255), -1) # Red circle
|
cv2.circle(overlay, (sx, sy), 5, (0, 0, 255), -1) # Red circle
|
||||||
cv2.addWeighted(overlay, 0.5, canvas, 0.5, 0, canvas)
|
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 tracking point (magenta/purple) - from the next frame with tracking points after current
|
||||||
next_frame = self.current_frame + 1
|
if next_result:
|
||||||
if next_frame in self.tracking_points:
|
next_frame, next_pts = next_result
|
||||||
next_pts = self.tracking_points[next_frame]
|
|
||||||
for (rx, ry) in next_pts:
|
for (rx, ry) in next_pts:
|
||||||
sx, sy = self._map_rotated_to_screen(rx, ry)
|
sx, sy = self._map_rotated_to_screen(rx, ry)
|
||||||
# Create overlay for alpha blending
|
# Create overlay for alpha blending (more transparent)
|
||||||
overlay = canvas.copy()
|
overlay = canvas.copy()
|
||||||
cv2.circle(overlay, (sx, sy), 4, (0, 255, 0), -1) # Green circle
|
cv2.circle(overlay, (sx, sy), 5, (255, 0, 255), -1) # Magenta circle
|
||||||
cv2.addWeighted(overlay, 0.5, canvas, 0.5, 0, canvas)
|
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:
|
if self.tracking_enabled and not self.is_image_mode:
|
||||||
interp = self._get_interpolated_tracking_position(self.current_frame)
|
interp = self._get_interpolated_tracking_position(self.current_frame)
|
||||||
if interp:
|
if interp:
|
||||||
@@ -2273,6 +2344,44 @@ class VideoEditor:
|
|||||||
self.clear_transformation_cache()
|
self.clear_transformation_cache()
|
||||||
self.save_state() # Save state when crop is undone
|
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):
|
def toggle_marker_looping(self):
|
||||||
"""Toggle looping between cut markers"""
|
"""Toggle looping between cut markers"""
|
||||||
# Check if both markers are set
|
# 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: Contract crop (left/down/up/right)")
|
||||||
print(" H/J/K/L: Expand crop (left/down/up/right)")
|
print(" H/J/K/L: Expand crop (left/down/up/right)")
|
||||||
print(" U: Undo crop")
|
print(" U: Undo crop")
|
||||||
print(" C: Clear crop")
|
print(" c: Clear crop")
|
||||||
|
print(" C: Complete reset (crop, zoom, rotation, brightness, contrast, tracking)")
|
||||||
print()
|
print()
|
||||||
print("Motion Tracking:")
|
print("Motion Tracking:")
|
||||||
print(" Right-click: Add/remove tracking point (at current frame)")
|
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: Contract crop (left/down/up/right)")
|
||||||
print(" H/J/K/L: Expand crop (left/down/up/right)")
|
print(" H/J/K/L: Expand crop (left/down/up/right)")
|
||||||
print(" U: Undo crop")
|
print(" U: Undo crop")
|
||||||
print(" C: Clear crop")
|
print(" c: Clear crop")
|
||||||
|
print(" C: Complete reset (crop, zoom, rotation, brightness, contrast, tracking)")
|
||||||
print()
|
print()
|
||||||
print("Other Controls:")
|
print("Other Controls:")
|
||||||
print(" Ctrl+Scroll: Zoom in/out")
|
print(" Ctrl+Scroll: Zoom in/out")
|
||||||
@@ -3045,6 +3156,8 @@ class VideoEditor:
|
|||||||
self.zoom_factor = 1.0
|
self.zoom_factor = 1.0
|
||||||
self.clear_transformation_cache()
|
self.clear_transformation_cache()
|
||||||
self.save_state() # Save state when crop is cleared
|
self.save_state() # Save state when crop is cleared
|
||||||
|
elif key == ord("C"):
|
||||||
|
self.complete_reset()
|
||||||
elif key == ord("1"):
|
elif key == ord("1"):
|
||||||
# Cut markers only for videos
|
# Cut markers only for videos
|
||||||
if not self.is_image_mode:
|
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)
|
- **H/J/K/L**: Contract crop to left/down/up/right edges (15 pixels per keypress)
|
||||||
- **u**: Undo last crop
|
- **u**: Undo last crop
|
||||||
- **c**: Clear all cropping
|
- **c**: Clear all cropping
|
||||||
|
- **C**: Complete reset (crop, zoom, rotation, brightness, contrast, tracking points, cut markers)
|
||||||
|
|
||||||
### Motion Tracking
|
### Motion Tracking
|
||||||
- **Right-click**: Add tracking point (green circle with white border)
|
- **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
|
- **Blue cross**: Shows computed tracking position
|
||||||
- **Automatic interpolation**: Tracks between keyframes
|
- **Automatic interpolation**: Tracks between keyframes
|
||||||
- **Crop follows**: Crop area centers on tracked object
|
- **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
|
#### Motion Tracking Navigation
|
||||||
- **,**: Jump to previous tracking marker (previous frame that has one or more tracking points). Wrap-around supported.
|
- **,**: Jump to previous tracking marker (previous frame that has one or more tracking points). Wrap-around supported.
|
||||||
|
Reference in New Issue
Block a user