Compare commits
2 Commits
83b40af001
...
ae2b156b87
Author | SHA1 | Date | |
---|---|---|---|
ae2b156b87 | |||
01aaa36eb0 |
116
croppa/main.py
116
croppa/main.py
@@ -83,9 +83,6 @@ class VideoEditor:
|
|||||||
else:
|
else:
|
||||||
raise ValueError(f"Path does not exist: {path}")
|
raise ValueError(f"Path does not exist: {path}")
|
||||||
|
|
||||||
# Initialize with first video
|
|
||||||
self._load_video(self.video_files[0])
|
|
||||||
|
|
||||||
# Mouse and keyboard interaction
|
# Mouse and keyboard interaction
|
||||||
self.mouse_dragging = False
|
self.mouse_dragging = False
|
||||||
self.timeline_rect = None
|
self.timeline_rect = None
|
||||||
@@ -151,6 +148,17 @@ class VideoEditor:
|
|||||||
# Display optimization - track when redraw is needed
|
# Display optimization - track when redraw is needed
|
||||||
self.display_needs_update = True
|
self.display_needs_update = True
|
||||||
self.last_display_state = None
|
self.last_display_state = None
|
||||||
|
|
||||||
|
# Cached transformations for performance
|
||||||
|
self.cached_transformed_frame = None
|
||||||
|
self.cached_frame_number = None
|
||||||
|
self.cached_transform_hash = None
|
||||||
|
|
||||||
|
# Initialize with first video
|
||||||
|
self._load_video(self.video_files[0])
|
||||||
|
|
||||||
|
# Load saved state after all attributes are initialized
|
||||||
|
self.load_state()
|
||||||
|
|
||||||
def _get_state_file_path(self) -> Path:
|
def _get_state_file_path(self) -> Path:
|
||||||
"""Get the state file path for the current media file"""
|
"""Get the state file path for the current media file"""
|
||||||
@@ -240,20 +248,6 @@ class VideoEditor:
|
|||||||
if 'cut_end_frame' in state:
|
if 'cut_end_frame' in state:
|
||||||
self.cut_end_frame = state['cut_end_frame']
|
self.cut_end_frame = state['cut_end_frame']
|
||||||
print(f"Loaded cut_end_frame: {self.cut_end_frame}")
|
print(f"Loaded cut_end_frame: {self.cut_end_frame}")
|
||||||
|
|
||||||
# Validate cut markers against current video length
|
|
||||||
if self.cut_start_frame is not None and self.cut_start_frame >= self.total_frames:
|
|
||||||
print(f"DEBUG: cut_start_frame {self.cut_start_frame} is beyond video length {self.total_frames}, clearing")
|
|
||||||
self.cut_start_frame = None
|
|
||||||
if self.cut_end_frame is not None and self.cut_end_frame >= self.total_frames:
|
|
||||||
print(f"DEBUG: cut_end_frame {self.cut_end_frame} is beyond video length {self.total_frames}, clearing")
|
|
||||||
self.cut_end_frame = None
|
|
||||||
|
|
||||||
# Calculate and show marker positions on timeline
|
|
||||||
if self.cut_start_frame is not None and self.cut_end_frame is not None:
|
|
||||||
start_progress = self.cut_start_frame / max(1, self.total_frames - 1)
|
|
||||||
end_progress = self.cut_end_frame / max(1, self.total_frames - 1)
|
|
||||||
print(f"Markers will be drawn at: Start {start_progress:.4f} ({self.cut_start_frame}/{self.total_frames}), End {end_progress:.4f} ({self.cut_end_frame}/{self.total_frames})")
|
|
||||||
if 'looping_between_markers' in state:
|
if 'looping_between_markers' in state:
|
||||||
self.looping_between_markers = state['looping_between_markers']
|
self.looping_between_markers = state['looping_between_markers']
|
||||||
print(f"Loaded looping_between_markers: {self.looping_between_markers}")
|
print(f"Loaded looping_between_markers: {self.looping_between_markers}")
|
||||||
@@ -270,6 +264,20 @@ class VideoEditor:
|
|||||||
self.is_playing = state['is_playing']
|
self.is_playing = state['is_playing']
|
||||||
print(f"Loaded is_playing: {self.is_playing}")
|
print(f"Loaded is_playing: {self.is_playing}")
|
||||||
|
|
||||||
|
# Validate cut markers against current video length
|
||||||
|
if self.cut_start_frame is not None and self.cut_start_frame >= self.total_frames:
|
||||||
|
print(f"DEBUG: cut_start_frame {self.cut_start_frame} is beyond video length {self.total_frames}, clearing")
|
||||||
|
self.cut_start_frame = None
|
||||||
|
if self.cut_end_frame is not None and self.cut_end_frame >= self.total_frames:
|
||||||
|
print(f"DEBUG: cut_end_frame {self.cut_end_frame} is beyond video length {self.total_frames}, clearing")
|
||||||
|
self.cut_end_frame = None
|
||||||
|
|
||||||
|
# Calculate and show marker positions on timeline
|
||||||
|
if self.cut_start_frame is not None and self.cut_end_frame is not None:
|
||||||
|
start_progress = self.cut_start_frame / max(1, self.total_frames - 1)
|
||||||
|
end_progress = self.cut_end_frame / max(1, self.total_frames - 1)
|
||||||
|
print(f"Markers will be drawn at: Start {start_progress:.4f} ({self.cut_start_frame}/{self.total_frames}), End {end_progress:.4f} ({self.cut_end_frame}/{self.total_frames})")
|
||||||
|
|
||||||
# Validate and clamp values
|
# Validate and clamp values
|
||||||
self.current_frame = max(0, min(self.current_frame, getattr(self, 'total_frames', 1) - 1))
|
self.current_frame = max(0, min(self.current_frame, getattr(self, 'total_frames', 1) - 1))
|
||||||
self.zoom_factor = max(self.MIN_ZOOM, min(self.MAX_ZOOM, self.zoom_factor))
|
self.zoom_factor = max(self.MIN_ZOOM, min(self.MAX_ZOOM, self.zoom_factor))
|
||||||
@@ -278,6 +286,11 @@ class VideoEditor:
|
|||||||
self.playback_speed = max(self.MIN_PLAYBACK_SPEED, min(self.MAX_PLAYBACK_SPEED, self.playback_speed))
|
self.playback_speed = max(self.MIN_PLAYBACK_SPEED, min(self.MAX_PLAYBACK_SPEED, self.playback_speed))
|
||||||
self.seek_multiplier = max(self.MIN_SEEK_MULTIPLIER, min(self.MAX_SEEK_MULTIPLIER, self.seek_multiplier))
|
self.seek_multiplier = max(self.MIN_SEEK_MULTIPLIER, min(self.MAX_SEEK_MULTIPLIER, self.seek_multiplier))
|
||||||
|
|
||||||
|
# Apply loaded settings
|
||||||
|
self.clear_transformation_cache()
|
||||||
|
self.load_current_frame()
|
||||||
|
|
||||||
|
print("Successfully loaded and applied all settings from state file")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error loading state: {e}")
|
print(f"Error loading state: {e}")
|
||||||
@@ -454,30 +467,13 @@ class VideoEditor:
|
|||||||
if self.fps > 60:
|
if self.fps > 60:
|
||||||
print(" Warning: High framerate video - may impact playback smoothness")
|
print(" Warning: High framerate video - may impact playback smoothness")
|
||||||
|
|
||||||
# Try to load saved state for this media file first
|
# Set default values for video-specific properties
|
||||||
if self.load_state():
|
self.current_frame = 0
|
||||||
print("Loaded saved state for this media file")
|
self.is_playing = False if self.is_image_mode else False # Images start paused
|
||||||
if self.cut_start_frame is not None:
|
self.playback_speed = 1.0
|
||||||
print(f" Cut start frame: {self.cut_start_frame}")
|
self.seek_multiplier = 1.0
|
||||||
if self.cut_end_frame is not None:
|
self.cut_start_frame = None
|
||||||
print(f" Cut end frame: {self.cut_end_frame}")
|
self.cut_end_frame = None
|
||||||
else:
|
|
||||||
print("No saved state found for this media file")
|
|
||||||
# Only reset to defaults if no state was loaded
|
|
||||||
self.current_frame = 0
|
|
||||||
self.is_playing = False if self.is_image_mode else False # Images start paused
|
|
||||||
self.playback_speed = 1.0
|
|
||||||
self.seek_multiplier = 1.0
|
|
||||||
self.crop_rect = None
|
|
||||||
self.crop_history = []
|
|
||||||
self.zoom_factor = 1.0
|
|
||||||
self.zoom_center = None
|
|
||||||
self.rotation_angle = 0
|
|
||||||
self.brightness = 0
|
|
||||||
self.contrast = 1.0
|
|
||||||
self.cut_start_frame = None
|
|
||||||
self.cut_end_frame = None
|
|
||||||
self.display_offset = [0, 0]
|
|
||||||
|
|
||||||
# Always reset these regardless of state
|
# Always reset these regardless of state
|
||||||
self.current_display_frame = None
|
self.current_display_frame = None
|
||||||
@@ -660,13 +656,29 @@ class VideoEditor:
|
|||||||
if frame is None:
|
if frame is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Create a hash of the transformation parameters for caching
|
||||||
|
transform_hash = hash((
|
||||||
|
self.crop_rect,
|
||||||
|
self.zoom_factor,
|
||||||
|
self.rotation_angle,
|
||||||
|
self.brightness,
|
||||||
|
self.contrast,
|
||||||
|
tuple(self.display_offset)
|
||||||
|
))
|
||||||
|
|
||||||
|
# Check if we can use cached transformation during auto-repeat seeking
|
||||||
|
if (self.auto_repeat_active and
|
||||||
|
self.cached_transformed_frame is not None and
|
||||||
|
self.cached_frame_number == self.current_frame and
|
||||||
|
self.cached_transform_hash == transform_hash):
|
||||||
|
return self.cached_transformed_frame.copy()
|
||||||
|
|
||||||
# Work in-place when possible to avoid unnecessary copying
|
# Work in-place when possible to avoid unnecessary copying
|
||||||
processed_frame = frame
|
processed_frame = frame
|
||||||
|
|
||||||
# Apply brightness/contrast first (to original frame for best quality)
|
# Apply brightness/contrast first (to original frame for best quality)
|
||||||
processed_frame = self.apply_brightness_contrast(processed_frame)
|
processed_frame = self.apply_brightness_contrast(processed_frame)
|
||||||
|
|
||||||
|
|
||||||
# Apply crop
|
# Apply crop
|
||||||
if self.crop_rect:
|
if self.crop_rect:
|
||||||
x, y, w, h = self.crop_rect
|
x, y, w, h = self.crop_rect
|
||||||
@@ -701,8 +713,20 @@ class VideoEditor:
|
|||||||
end_y = min(new_height, start_y + self.window_height)
|
end_y = min(new_height, start_y + self.window_height)
|
||||||
processed_frame = processed_frame[start_y:end_y, start_x:end_x]
|
processed_frame = processed_frame[start_y:end_y, start_x:end_x]
|
||||||
|
|
||||||
|
# Cache the result for auto-repeat seeking performance
|
||||||
|
if self.auto_repeat_active:
|
||||||
|
self.cached_transformed_frame = processed_frame.copy()
|
||||||
|
self.cached_frame_number = self.current_frame
|
||||||
|
self.cached_transform_hash = transform_hash
|
||||||
|
|
||||||
return processed_frame
|
return processed_frame
|
||||||
|
|
||||||
|
def clear_transformation_cache(self):
|
||||||
|
"""Clear the cached transformation to force recalculation"""
|
||||||
|
self.cached_transformed_frame = None
|
||||||
|
self.cached_frame_number = None
|
||||||
|
self.cached_transform_hash = None
|
||||||
|
|
||||||
def apply_rotation(self, frame):
|
def apply_rotation(self, frame):
|
||||||
"""Apply rotation to frame"""
|
"""Apply rotation to frame"""
|
||||||
if self.rotation_angle == 0:
|
if self.rotation_angle == 0:
|
||||||
@@ -718,6 +742,7 @@ class VideoEditor:
|
|||||||
def rotate_clockwise(self):
|
def rotate_clockwise(self):
|
||||||
"""Rotate video 90 degrees clockwise"""
|
"""Rotate video 90 degrees clockwise"""
|
||||||
self.rotation_angle = (self.rotation_angle + 90) % 360
|
self.rotation_angle = (self.rotation_angle + 90) % 360
|
||||||
|
self.clear_transformation_cache()
|
||||||
|
|
||||||
def apply_brightness_contrast(self, frame):
|
def apply_brightness_contrast(self, frame):
|
||||||
"""Apply brightness and contrast adjustments to frame"""
|
"""Apply brightness and contrast adjustments to frame"""
|
||||||
@@ -736,11 +761,13 @@ class VideoEditor:
|
|||||||
def adjust_brightness(self, delta: int):
|
def adjust_brightness(self, delta: int):
|
||||||
"""Adjust brightness by delta (-100 to 100)"""
|
"""Adjust brightness by delta (-100 to 100)"""
|
||||||
self.brightness = max(-100, min(100, self.brightness + delta))
|
self.brightness = max(-100, min(100, self.brightness + delta))
|
||||||
|
self.clear_transformation_cache()
|
||||||
self.display_needs_update = True
|
self.display_needs_update = True
|
||||||
|
|
||||||
def adjust_contrast(self, delta: float):
|
def adjust_contrast(self, delta: float):
|
||||||
"""Adjust contrast by delta (0.1 to 3.0)"""
|
"""Adjust contrast by delta (0.1 to 3.0)"""
|
||||||
self.contrast = max(0.1, min(3.0, self.contrast + delta))
|
self.contrast = max(0.1, min(3.0, self.contrast + delta))
|
||||||
|
self.clear_transformation_cache()
|
||||||
self.display_needs_update = True
|
self.display_needs_update = True
|
||||||
|
|
||||||
def show_progress_bar(self, text: str = "Processing..."):
|
def show_progress_bar(self, text: str = "Processing..."):
|
||||||
@@ -1326,6 +1353,7 @@ class VideoEditor:
|
|||||||
self.zoom_factor = max(
|
self.zoom_factor = max(
|
||||||
self.MIN_ZOOM, self.zoom_factor - self.ZOOM_INCREMENT
|
self.MIN_ZOOM, self.zoom_factor - self.ZOOM_INCREMENT
|
||||||
)
|
)
|
||||||
|
self.clear_transformation_cache()
|
||||||
|
|
||||||
def set_crop_from_screen_coords(self, screen_rect):
|
def set_crop_from_screen_coords(self, screen_rect):
|
||||||
"""Convert screen coordinates to video frame coordinates and set crop"""
|
"""Convert screen coordinates to video frame coordinates and set crop"""
|
||||||
@@ -1445,6 +1473,7 @@ class VideoEditor:
|
|||||||
if self.crop_rect:
|
if self.crop_rect:
|
||||||
self.crop_history.append(self.crop_rect)
|
self.crop_history.append(self.crop_rect)
|
||||||
self.crop_rect = (original_x, original_y, original_w, original_h)
|
self.crop_rect = (original_x, original_y, original_w, original_h)
|
||||||
|
self.clear_transformation_cache()
|
||||||
self.save_state() # Save state when crop is set
|
self.save_state() # Save state when crop is set
|
||||||
|
|
||||||
def seek_to_timeline_position(self, mouse_x, bar_x_start, bar_width):
|
def seek_to_timeline_position(self, mouse_x, bar_x_start, bar_width):
|
||||||
@@ -1460,6 +1489,7 @@ class VideoEditor:
|
|||||||
self.crop_rect = self.crop_history.pop()
|
self.crop_rect = self.crop_history.pop()
|
||||||
else:
|
else:
|
||||||
self.crop_rect = None
|
self.crop_rect = None
|
||||||
|
self.clear_transformation_cache()
|
||||||
self.save_state() # Save state when crop is undone
|
self.save_state() # Save state when crop is undone
|
||||||
|
|
||||||
def toggle_marker_looping(self):
|
def toggle_marker_looping(self):
|
||||||
@@ -1557,6 +1587,7 @@ class VideoEditor:
|
|||||||
new_w = w - amount
|
new_w = w - amount
|
||||||
self.crop_rect = (new_x, y, new_w, h)
|
self.crop_rect = (new_x, y, new_w, h)
|
||||||
|
|
||||||
|
self.clear_transformation_cache()
|
||||||
self.save_state() # Save state when crop is adjusted
|
self.save_state() # Save state when crop is adjusted
|
||||||
|
|
||||||
def render_video(self, output_path: str):
|
def render_video(self, output_path: str):
|
||||||
@@ -2170,6 +2201,7 @@ class VideoEditor:
|
|||||||
self.crop_history.append(self.crop_rect)
|
self.crop_history.append(self.crop_rect)
|
||||||
self.crop_rect = None
|
self.crop_rect = None
|
||||||
self.zoom_factor = 1.0
|
self.zoom_factor = 1.0
|
||||||
|
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("1"):
|
elif key == ord("1"):
|
||||||
# Cut markers only for videos
|
# Cut markers only for videos
|
||||||
|
Reference in New Issue
Block a user