diff --git a/croppa/main.py b/croppa/main.py index 760c4e6..d87a4bd 100644 --- a/croppa/main.py +++ b/croppa/main.py @@ -151,6 +151,11 @@ class VideoEditor: # Display optimization - track when redraw is needed self.display_needs_update = True self.last_display_state = None + + # Cached transformations for performance + self.cached_transformed_frame = None + self.cached_frame_number = None + self.cached_transform_hash = None def _get_state_file_path(self) -> Path: """Get the state file path for the current media file""" @@ -660,13 +665,29 @@ class VideoEditor: if frame is 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, + 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 processed_frame = frame # Apply brightness/contrast first (to original frame for best quality) processed_frame = self.apply_brightness_contrast(processed_frame) - # Apply crop if self.crop_rect: x, y, w, h = self.crop_rect @@ -701,8 +722,20 @@ class VideoEditor: end_y = min(new_height, start_y + self.window_height) 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 + 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): """Apply rotation to frame""" if self.rotation_angle == 0: @@ -718,6 +751,7 @@ class VideoEditor: def rotate_clockwise(self): """Rotate video 90 degrees clockwise""" self.rotation_angle = (self.rotation_angle + 90) % 360 + self.clear_transformation_cache() def apply_brightness_contrast(self, frame): """Apply brightness and contrast adjustments to frame""" @@ -736,11 +770,13 @@ class VideoEditor: def adjust_brightness(self, delta: int): """Adjust brightness by delta (-100 to 100)""" self.brightness = max(-100, min(100, self.brightness + delta)) + self.clear_transformation_cache() self.display_needs_update = True def adjust_contrast(self, delta: float): """Adjust contrast by delta (0.1 to 3.0)""" self.contrast = max(0.1, min(3.0, self.contrast + delta)) + self.clear_transformation_cache() self.display_needs_update = True def show_progress_bar(self, text: str = "Processing..."): @@ -1326,6 +1362,7 @@ class VideoEditor: self.zoom_factor = max( self.MIN_ZOOM, self.zoom_factor - self.ZOOM_INCREMENT ) + self.clear_transformation_cache() def set_crop_from_screen_coords(self, screen_rect): """Convert screen coordinates to video frame coordinates and set crop""" @@ -1445,6 +1482,7 @@ class VideoEditor: if self.crop_rect: self.crop_history.append(self.crop_rect) self.crop_rect = (original_x, original_y, original_w, original_h) + self.clear_transformation_cache() self.save_state() # Save state when crop is set def seek_to_timeline_position(self, mouse_x, bar_x_start, bar_width): @@ -1460,6 +1498,7 @@ class VideoEditor: self.crop_rect = self.crop_history.pop() else: self.crop_rect = None + self.clear_transformation_cache() self.save_state() # Save state when crop is undone def toggle_marker_looping(self): @@ -1557,6 +1596,7 @@ class VideoEditor: new_w = w - amount self.crop_rect = (new_x, y, new_w, h) + self.clear_transformation_cache() self.save_state() # Save state when crop is adjusted def render_video(self, output_path: str): @@ -2170,6 +2210,7 @@ class VideoEditor: self.crop_history.append(self.crop_rect) self.crop_rect = None self.zoom_factor = 1.0 + self.clear_transformation_cache() self.save_state() # Save state when crop is cleared elif key == ord("1"): # Cut markers only for videos