refactor(main.py): introduce frame caching and separate frame loading/advancing logic
This commit is contained in:
193
main.py
193
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
|
||||
|
Reference in New Issue
Block a user