diff --git a/croppa/main.py b/croppa/main.py index 245d16f..10e147e 100644 --- a/croppa/main.py +++ b/croppa/main.py @@ -142,6 +142,10 @@ class VideoEditor: self.render_thread = None self.render_cancelled = False self.render_progress_queue = queue.Queue() + + # Display optimization - track when redraw is needed + self.display_needs_update = True + self.last_display_state = None def _get_state_file_path(self) -> Path: """Get the state file path for the current media file""" @@ -507,6 +511,7 @@ class VideoEditor: ) self.current_frame = target_frame self.load_current_frame() + self.display_needs_update = True def seek_video_with_modifier( @@ -561,9 +566,6 @@ class VideoEditor: ) self.last_display_update = current_time - def should_update_display(self) -> bool: - """Check if display should be updated""" - return True def seek_to_frame(self, frame_number: int): """Seek to specific frame""" @@ -640,7 +642,8 @@ class VideoEditor: if frame is None: return None - processed_frame = 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) @@ -715,10 +718,12 @@ 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.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.display_needs_update = True def show_progress_bar(self, text: str = "Processing..."): """Show progress bar with given text""" @@ -727,6 +732,7 @@ class VideoEditor: self.progress_bar_complete = False self.progress_bar_complete_time = None self.progress_bar_text = text + self.display_needs_update = True def update_progress_bar(self, progress: float, text: str = None, fps: float = None): """Update progress bar progress (0.0 to 1.0) and optionally text and FPS""" @@ -753,6 +759,7 @@ class VideoEditor: """Show a feedback message on screen for a few seconds""" self.feedback_message = message self.feedback_message_time = time.time() + self.display_needs_update = True def draw_feedback_message(self, frame): """Draw feedback message on frame if visible""" @@ -1055,9 +1062,28 @@ class VideoEditor: if self.current_display_frame is None: return + # Check if display needs update (optimization) + current_state = ( + self.current_frame, + self.crop_rect, + self.zoom_factor, + self.rotation_angle, + self.brightness, + self.contrast, + self.display_offset, + self.progress_bar_visible, + self.feedback_message + ) + + if not self.display_needs_update and current_state == self.last_display_state: + return # Skip redraw if nothing changed + + self.last_display_state = current_state + self.display_needs_update = False + # Apply crop, zoom, and rotation transformations for preview display_frame = self.apply_crop_zoom_and_rotation( - self.current_display_frame.copy() + self.current_display_frame ) if display_frame is None: @@ -1577,11 +1603,37 @@ class VideoEditor: # Send progress update self.render_progress_queue.put(("progress", "Setting up video writer...", 0.1, 0.0)) - # Use mp4v codec (most compatible with MP4) - fourcc = cv2.VideoWriter_fourcc(*"mp4v") - out = cv2.VideoWriter( - output_path, fourcc, self.fps, (output_width, output_height) - ) + # Try hardware-accelerated codecs first, fallback to mp4v + codecs_to_try = [ + ("h264_nvenc", "H264"), # NVIDIA hardware acceleration + ("h264_qsv", "H264"), # Intel Quick Sync + ("h264", "H264"), # Software H.264 + ("mp4v", "MP4V") # Fallback + ] + + out = None + for codec_name, fourcc_code in codecs_to_try: + try: + fourcc = cv2.VideoWriter_fourcc(*fourcc_code) + out = cv2.VideoWriter( + output_path, fourcc, self.fps, (output_width, output_height) + ) + if out.isOpened(): + print(f"Using {codec_name} codec for rendering") + break + else: + out.release() + out = None + except Exception: + continue + + if out is None: + # Final fallback to mp4v + fourcc = cv2.VideoWriter_fourcc(*"mp4v") + out = cv2.VideoWriter( + output_path, fourcc, self.fps, (output_width, output_height) + ) + print("Using mp4v codec (fallback)") if not out.isOpened(): self.render_progress_queue.put(("error", "Could not open video writer!", 1.0, 0.0)) @@ -1805,34 +1857,29 @@ class VideoEditor: if self.rotation_angle != 0: frame = self.apply_rotation(frame) - # Apply zoom and resize in one step for efficiency + # Apply zoom and resize directly to final output dimensions if self.zoom_factor != 1.0: height, width = frame.shape[:2] - intermediate_width = int(width * self.zoom_factor) - intermediate_height = int(height * self.zoom_factor) - - # If zoom results in different dimensions than output, resize directly to output - if ( - intermediate_width != output_width - or intermediate_height != output_height - ): + # Calculate what the zoomed dimensions would be + zoomed_width = int(width * self.zoom_factor) + zoomed_height = int(height * self.zoom_factor) + + # If zoomed dimensions match output, use them; otherwise resize directly to output + if zoomed_width == output_width and zoomed_height == output_height: frame = cv2.resize( - frame, - (output_width, output_height), - interpolation=cv2.INTER_LINEAR, + frame, (zoomed_width, zoomed_height), interpolation=cv2.INTER_LINEAR ) else: + # Resize directly to final output dimensions frame = cv2.resize( - frame, - (intermediate_width, intermediate_height), - interpolation=cv2.INTER_LINEAR, + frame, (output_width, output_height), interpolation=cv2.INTER_LINEAR + ) + else: + # No zoom, just resize to output dimensions if needed + if frame.shape[1] != output_width or frame.shape[0] != output_height: + frame = cv2.resize( + frame, (output_width, output_height), interpolation=cv2.INTER_LINEAR ) - - # Final size check and resize if needed - if frame.shape[1] != output_width or frame.shape[0] != output_height: - frame = cv2.resize( - frame, (output_width, output_height), interpolation=cv2.INTER_LINEAR - ) return frame @@ -1913,9 +1960,8 @@ class VideoEditor: # Update render progress from background thread self.update_render_progress() - # Only update display if needed and throttled - if self.should_update_display(): - self.display_current_frame() + # Update display + self.display_current_frame() delay = self.calculate_frame_delay() if self.is_playing else 1 # Very short delay for responsive key detection key = cv2.waitKey(delay) & 0xFF