Compare commits

...

3 Commits

Author SHA1 Message Date
8c45b30bca Enhance VideoEditor with motion path visualization for tracking points
This commit updates the VideoEditor class to visualize the motion path between previous and next tracking points using yellow arrows. The rendering of tracking points has been modified, with previous points displayed as red circles and next points as magenta circles, both with white borders for better visibility. The specification has been updated to reflect these changes in the display of tracking points and their motion direction.
2025-09-17 14:34:26 +02:00
615a3dce0d Implement methods for retrieving previous and next tracking points in VideoEditor
This commit introduces two new methods, _get_previous_tracking_point and _get_next_tracking_point, to the VideoEditor class. These methods allow for the retrieval of tracking points from the previous and next frames that contain tracking data, enhancing navigation capabilities. The rendering logic has been updated to utilize these methods, ensuring that the previous (red) and next (green) tracking points are displayed correctly during video playback. Additionally, the specification has been updated to reflect these changes in the display of tracking points.
2025-09-17 14:29:11 +02:00
1ce05d33ba Add complete reset functionality to VideoEditor for all transformations
This commit introduces a new method, complete_reset, in the VideoEditor class, allowing users to reset all transformations and settings, including crop, zoom, rotation, brightness, contrast, and motion tracking. The method ensures a clean slate for editing by clearing the transformation cache and saving the state. Additionally, the specification has been updated to include this new functionality, enhancing user control over the editing environment.
2025-09-17 14:12:02 +02:00
2 changed files with 133 additions and 19 deletions

View File

@@ -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:

View File

@@ -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.