From 3900d1cb11d894c456699490603f8a33fa5d8602 Mon Sep 17 00:00:00 2001 From: PhatPhuckDave Date: Mon, 18 Aug 2025 16:36:18 +0200 Subject: [PATCH] refactor(main.py): introduce frame caching and separate frame loading/advancing logic --- main.py | 193 ++++++++++++++++++++++++++++++-------------------------- 1 file changed, 105 insertions(+), 88 deletions(-) diff --git a/main.py b/main.py index bd2c093..f4ced0b 100644 --- a/main.py +++ b/main.py @@ -49,6 +49,9 @@ class MediaGrader: self.coarse_seek_frames = self.seek_frames # User-configurable self.fast_seek_frames = self.seek_frames * self.FAST_SEEK_MULTIPLIER + # Current frame cache for display + self.current_display_frame = None + # Supported media extensions self.extensions = [ "*.png", @@ -121,30 +124,50 @@ class MediaGrader: self.total_frames = 1 self.current_frame = 0 + # Load initial frame + self.load_current_frame() return True - def display_media(self, file_path: Path) -> Optional[Tuple[bool, any]]: - """Display current media file""" - if self.is_video(file_path): + def load_current_frame(self): + """Load the current frame into display cache""" + if self.is_video(self.media_files[self.current_index]): if not self.current_cap: - return None + return False - # For high-speed playback, skip frames - frames_to_skip = self.calculate_frames_to_skip() - - for _ in range(frames_to_skip + 1): # +1 to read at least one frame - ret, frame = self.current_cap.read() - if not ret: - return False, None - - self.current_frame = int(self.current_cap.get(cv2.CAP_PROP_POS_FRAMES)) - return True, frame + # Read frame at current position + ret, frame = self.current_cap.read() + if ret: + self.current_display_frame = frame + self.current_frame = int(self.current_cap.get(cv2.CAP_PROP_POS_FRAMES)) + return True + return False else: - # Display image - frame = cv2.imread(str(file_path)) - if frame is None: - return False, None - return True, frame + # Load image + frame = cv2.imread(str(self.media_files[self.current_index])) + if frame is not None: + self.current_display_frame = frame + return True + return False + + def advance_frame(self): + """Advance to next frame(s) based on playback speed""" + if ( + not self.is_video(self.media_files[self.current_index]) + or not self.is_playing + ): + return + + # Skip frames for high-speed playback + frames_to_skip = self.calculate_frames_to_skip() + + for _ in range(frames_to_skip + 1): # +1 to advance at least one frame + ret, frame = self.current_cap.read() + if not ret: + return False + + self.current_display_frame = frame + self.current_frame = int(self.current_cap.get(cv2.CAP_PROP_POS_FRAMES)) + return True def auto_resize_window(self, frame): """Auto-resize window to fit media while respecting screen limits""" @@ -184,7 +207,9 @@ class MediaGrader: new_frame = max(0, new_frame - (new_frame % self.IFRAME_SNAP_INTERVAL)) self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, new_frame) - self.current_frame = new_frame + + # Load the frame we just seeked to and display it immediately + self.load_current_frame() print(f"Seeked by {frames_delta} frames to frame {new_frame}") def handle_seeking_key(self, key: int) -> bool: @@ -288,9 +313,9 @@ class MediaGrader: print(f"Found {len(self.media_files)} media files") print("Controls:") print(" Space: Pause/Play") - print(" Left/Right: Seek backward/forward (hold for FAST seek)") + print(" A/D: Seek backward/forward (hold for FAST seek)") print(" , / . : Frame-by-frame seek (fine control)") - print(" A/D: Decrease/Increase playback speed") + print(" W/S: Decrease/Increase playback speed") print(" 1-5: Grade and move file") print(" N: Next file") print(" P: Previous file") @@ -312,70 +337,62 @@ class MediaGrader: window_resized = False while True: - # Always try to get and display a frame (for seeking while paused) - result = self.display_media(current_file) - if result is None or not result[0]: - break + # Always display the current cached frame + if self.current_display_frame is not None: + frame = self.current_display_frame.copy() - ret, frame = result + # Auto-resize window on first frame + if not window_resized: + self.auto_resize_window(frame) + window_resized = True - # Auto-resize window on first frame - if not window_resized: - self.auto_resize_window(frame) - window_resized = True + # Add info overlay + info_text = f"Speed: {self.playback_speed:.1f}x | Frame: {self.current_frame}/{self.total_frames} | File: {self.current_index + 1}/{len(self.media_files)} | {'Playing' if self.is_playing else 'PAUSED'}" + help_text = "Seek: A/D (hold=FAST) ,. (fine) | W/S speed | 1-5 grade | Space pause | Q quit" - # Add info overlay - info_text = f"Speed: {self.playback_speed:.1f}x | Frame: {self.current_frame}/{self.total_frames} | File: {self.current_index + 1}/{len(self.media_files)}" - help_text = "Seek: ←→ (hold=FAST) ,. (fine) | A/D speed | 1-5 grade | Space pause | Q quit" + # White background for text visibility + cv2.putText( + frame, + info_text, + (10, 30), + cv2.FONT_HERSHEY_SIMPLEX, + 0.7, + (255, 255, 255), + 2, + ) + cv2.putText( + frame, + info_text, + (10, 30), + cv2.FONT_HERSHEY_SIMPLEX, + 0.7, + (0, 0, 0), + 1, + ) + cv2.putText( + frame, + help_text, + (10, 60), + cv2.FONT_HERSHEY_SIMPLEX, + 0.5, + (255, 255, 255), + 2, + ) + cv2.putText( + frame, + help_text, + (10, 60), + cv2.FONT_HERSHEY_SIMPLEX, + 0.5, + (0, 0, 0), + 1, + ) - # White background for text visibility - cv2.putText( - frame, - info_text, - (10, 30), - cv2.FONT_HERSHEY_SIMPLEX, - 0.7, - (255, 255, 255), - 2, - ) - cv2.putText( - frame, - info_text, - (10, 30), - cv2.FONT_HERSHEY_SIMPLEX, - 0.7, - (0, 0, 0), - 1, - ) - cv2.putText( - frame, - help_text, - (10, 60), - cv2.FONT_HERSHEY_SIMPLEX, - 0.5, - (255, 255, 255), - 2, - ) - cv2.putText( - frame, - help_text, - (10, 60), - cv2.FONT_HERSHEY_SIMPLEX, - 0.5, - (0, 0, 0), - 1, - ) + cv2.imshow("Media Grader", frame) - cv2.imshow("Media Grader", frame) - - # Calculate appropriate delay - shorter for paused videos to enable seeking + # Calculate appropriate delay if self.is_video(current_file): - if self.is_playing: - delay = self.calculate_frame_delay() - else: - delay = ( - 30 # Short delay when paused to enable responsive seeking - ) + delay = self.calculate_frame_delay() else: delay = self.IMAGE_DISPLAY_DELAY_MS @@ -390,20 +407,20 @@ class MediaGrader: elif key == ord(" "): # Space - pause/play self.is_playing = not self.is_playing print(f"{'Playing' if self.is_playing else 'Paused'}") - elif key == ord("w"): # A - decrease speed + elif key == ord("s"): # W - decrease speed self.playback_speed = max( self.MIN_PLAYBACK_SPEED, self.playback_speed - self.SPEED_INCREMENT, ) print(f"Speed: {self.playback_speed:.1f}x") - elif key == ord("s"): # D - increase speed + elif key == ord("w"): # S - increase speed self.playback_speed = min( self.MAX_PLAYBACK_SPEED, self.playback_speed + self.SPEED_INCREMENT, ) print(f"Speed: {self.playback_speed:.1f}x") elif self.handle_seeking_key(key): - # Seeking was handled + # Seeking was handled and frame was updated pass elif key == ord("n"): # Next file break @@ -421,11 +438,11 @@ class MediaGrader: self.last_key = None print("Key released") - # Only advance to next frame if playing AND it's a video - if not self.is_playing and self.is_video(current_file): - # When paused, seek back one frame to stay on current frame - # (since display_media already advanced us) - continue + # Advance frame only if playing (and it's a video) + if self.is_playing and self.is_video(current_file): + if not self.advance_frame(): + # End of video + break if key not in [ord("p")]: # Don't increment for previous self.current_index += 1