From e80278a2ddfb77cee30ccef76c2b4a5ba559acba Mon Sep 17 00:00:00 2001 From: PhatPhuckDave Date: Wed, 17 Sep 2025 09:00:14 +0200 Subject: [PATCH] Refactor frame processing in VideoEditor to enhance cropping and rotation handling This commit updates the frame processing logic in the VideoEditor class to apply rotation before cropping, ensuring accurate handling of frames in rotated space. It introduces effective cropping that accommodates out-of-bounds scenarios by padding with black, improving the visual output during editing. Additionally, debug statements have been refined to provide clearer information on output dimensions and effective crop details, enhancing the overall user experience in video editing. --- croppa/main.py | 85 ++++++++++++++++++++++++++------------------------ 1 file changed, 45 insertions(+), 40 deletions(-) diff --git a/croppa/main.py b/croppa/main.py index fe03f66..d8ddc81 100644 --- a/croppa/main.py +++ b/croppa/main.py @@ -1206,8 +1206,8 @@ class VideoEditor: offy_max = max(0, new_h - self.window_height) offx = max(0, min(int(self.display_offset[0]), offx_max)) offy = max(0, min(int(self.display_offset[1]), offy_max)) - visible_w = self.window_width - visible_h = self.window_height + visible_w = min(new_w, self.window_width) + visible_h = min(new_h, self.window_height) else: offx = 0 offy = 0 @@ -1224,7 +1224,9 @@ class VideoEditor: 'eff_x': eff_x, 'eff_y': eff_y, 'eff_w': eff_w, 'eff_h': eff_h, 'offx': offx, 'offy': offy, 'scale': scale, - 'start_x': start_x, 'start_y': start_y + 'start_x': start_x, 'start_y': start_y, + 'visible_w': visible_w, 'visible_h': visible_h, + 'available_h': available_height } def _map_original_to_screen(self, ox, oy): @@ -2350,21 +2352,10 @@ class VideoEditor: # Send progress update self.render_progress_queue.put(("progress", "Calculating output dimensions...", 0.05, 0.0)) - # Calculate output dimensions (accounting for rotation) - if self.crop_rect: - crop_width = int(self.crop_rect[2]) - crop_height = int(self.crop_rect[3]) - else: - crop_width = self.frame_width - crop_height = self.frame_height - - # Swap dimensions if rotation is 90 or 270 degrees - if self.rotation_angle == 90 or self.rotation_angle == 270: - output_width = int(crop_height * self.zoom_factor) - output_height = int(crop_width * self.zoom_factor) - else: - output_width = int(crop_width * self.zoom_factor) - output_height = int(crop_height * self.zoom_factor) + # Calculate output dimensions to MATCH preview visible region + params = self._get_display_params() + output_width = max(2, params['visible_w'] - (params['visible_w'] % 2)) + output_height = max(2, params['visible_h'] - (params['visible_h'] % 2)) # Ensure dimensions are divisible by 2 for H.264 encoding output_width = output_width - (output_width % 2) @@ -2374,9 +2365,10 @@ class VideoEditor: self.render_progress_queue.put(("progress", "Setting up FFmpeg encoder...", 0.1, 0.0)) # Debug output dimensions - print(f"Output dimensions: {output_width}x{output_height}") + print(f"Output dimensions (match preview): {output_width}x{output_height}") print(f"Zoom factor: {self.zoom_factor}") - print(f"Crop dimensions: {crop_width}x{crop_height}") + eff_x, eff_y, eff_w, eff_h = self._get_effective_crop_rect_for_frame(start_frame) + print(f"Effective crop (rotated): {eff_x},{eff_y} {eff_w}x{eff_h}") # Skip all the OpenCV codec bullshit and go straight to FFmpeg print("Using FFmpeg for encoding with OpenCV transformations...") @@ -2526,32 +2518,45 @@ class VideoEditor: def _process_frame_for_render(self, frame, output_width: int, output_height: int, frame_number: int = None): """Process a single frame for rendering (optimized for speed)""" try: - # Apply crop (vectorized operation) - if self.crop_rect: - if frame_number is None: - x, y, w, h = map(int, self.crop_rect) - else: - x, y, w, h = map(int, self._get_effective_crop_rect_for_frame(frame_number)) + # Apply rotation first to work in rotated space + if self.rotation_angle != 0: + frame = self.apply_rotation(frame) - # Clamp coordinates to frame bounds - h_frame, w_frame = frame.shape[:2] - x = max(0, min(x, w_frame - 1)) - y = max(0, min(y, h_frame - 1)) - w = min(w, w_frame - x) - h = min(h, h_frame - y) + # Apply EFFECTIVE crop regardless of whether a base crop exists, to enable follow and out-of-frame pad + x, y, w, h = self._get_effective_crop_rect_for_frame(frame_number or self.current_frame) - if w > 0 and h > 0: - frame = frame[y : y + h, x : x + w] - else: - return None + # Allow out-of-bounds by padding with black so center can remain when near edges + h_frame, w_frame = frame.shape[:2] + pad_left = max(0, -x) + pad_top = max(0, -y) + pad_right = max(0, (x + w) - w_frame) + pad_bottom = max(0, (y + h) - h_frame) + if any(p > 0 for p in (pad_left, pad_top, pad_right, pad_bottom)): + frame = cv2.copyMakeBorder( + frame, + pad_top, + pad_bottom, + pad_left, + pad_right, + borderType=cv2.BORDER_CONSTANT, + value=(0, 0, 0), + ) + x = x + pad_left + y = y + pad_top + w_frame, h_frame = frame.shape[1], frame.shape[0] + + # Clamp crop to padded frame + x = max(0, min(x, w_frame - 1)) + y = max(0, min(y, h_frame - 1)) + w = min(w, w_frame - x) + h = min(h, h_frame - y) + if w <= 0 or h <= 0: + return None + frame = frame[y : y + h, x : x + w] # Apply brightness and contrast frame = self.apply_brightness_contrast(frame) - # Apply rotation - if self.rotation_angle != 0: - frame = self.apply_rotation(frame) - # Apply zoom and resize directly to final output dimensions if self.zoom_factor != 1.0: height, width = frame.shape[:2]