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.coarse_seek_frames = self.seek_frames # User-configurable
|
||||||
self.fast_seek_frames = self.seek_frames * self.FAST_SEEK_MULTIPLIER
|
self.fast_seek_frames = self.seek_frames * self.FAST_SEEK_MULTIPLIER
|
||||||
|
|
||||||
|
# Current frame cache for display
|
||||||
|
self.current_display_frame = None
|
||||||
|
|
||||||
# Supported media extensions
|
# Supported media extensions
|
||||||
self.extensions = [
|
self.extensions = [
|
||||||
"*.png",
|
"*.png",
|
||||||
@@ -121,30 +124,50 @@ class MediaGrader:
|
|||||||
self.total_frames = 1
|
self.total_frames = 1
|
||||||
self.current_frame = 0
|
self.current_frame = 0
|
||||||
|
|
||||||
|
# Load initial frame
|
||||||
|
self.load_current_frame()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def display_media(self, file_path: Path) -> Optional[Tuple[bool, any]]:
|
def load_current_frame(self):
|
||||||
"""Display current media file"""
|
"""Load the current frame into display cache"""
|
||||||
if self.is_video(file_path):
|
if self.is_video(self.media_files[self.current_index]):
|
||||||
if not self.current_cap:
|
if not self.current_cap:
|
||||||
return None
|
return False
|
||||||
|
|
||||||
# For high-speed playback, skip frames
|
# Read frame at current position
|
||||||
frames_to_skip = self.calculate_frames_to_skip()
|
ret, frame = self.current_cap.read()
|
||||||
|
if ret:
|
||||||
for _ in range(frames_to_skip + 1): # +1 to read at least one frame
|
self.current_display_frame = frame
|
||||||
ret, frame = self.current_cap.read()
|
self.current_frame = int(self.current_cap.get(cv2.CAP_PROP_POS_FRAMES))
|
||||||
if not ret:
|
return True
|
||||||
return False, None
|
return False
|
||||||
|
|
||||||
self.current_frame = int(self.current_cap.get(cv2.CAP_PROP_POS_FRAMES))
|
|
||||||
return True, frame
|
|
||||||
else:
|
else:
|
||||||
# Display image
|
# Load image
|
||||||
frame = cv2.imread(str(file_path))
|
frame = cv2.imread(str(self.media_files[self.current_index]))
|
||||||
if frame is None:
|
if frame is not None:
|
||||||
return False, None
|
self.current_display_frame = frame
|
||||||
return True, 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):
|
def auto_resize_window(self, frame):
|
||||||
"""Auto-resize window to fit media while respecting screen limits"""
|
"""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))
|
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_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}")
|
print(f"Seeked by {frames_delta} frames to frame {new_frame}")
|
||||||
|
|
||||||
def handle_seeking_key(self, key: int) -> bool:
|
def handle_seeking_key(self, key: int) -> bool:
|
||||||
@@ -288,9 +313,9 @@ class MediaGrader:
|
|||||||
print(f"Found {len(self.media_files)} media files")
|
print(f"Found {len(self.media_files)} media files")
|
||||||
print("Controls:")
|
print("Controls:")
|
||||||
print(" Space: Pause/Play")
|
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(" , / . : 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(" 1-5: Grade and move file")
|
||||||
print(" N: Next file")
|
print(" N: Next file")
|
||||||
print(" P: Previous file")
|
print(" P: Previous file")
|
||||||
@@ -312,70 +337,62 @@ class MediaGrader:
|
|||||||
window_resized = False
|
window_resized = False
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
# Always try to get and display a frame (for seeking while paused)
|
# Always display the current cached frame
|
||||||
result = self.display_media(current_file)
|
if self.current_display_frame is not None:
|
||||||
if result is None or not result[0]:
|
frame = self.current_display_frame.copy()
|
||||||
break
|
|
||||||
|
|
||||||
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
|
# Add info overlay
|
||||||
if not window_resized:
|
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'}"
|
||||||
self.auto_resize_window(frame)
|
help_text = "Seek: A/D (hold=FAST) ,. (fine) | W/S speed | 1-5 grade | Space pause | Q quit"
|
||||||
window_resized = True
|
|
||||||
|
|
||||||
# Add info overlay
|
# White background for text visibility
|
||||||
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)}"
|
cv2.putText(
|
||||||
help_text = "Seek: ←→ (hold=FAST) ,. (fine) | A/D speed | 1-5 grade | Space pause | Q quit"
|
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.imshow("Media Grader", frame)
|
||||||
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)
|
# Calculate appropriate delay
|
||||||
|
|
||||||
# Calculate appropriate delay - shorter for paused videos to enable seeking
|
|
||||||
if self.is_video(current_file):
|
if self.is_video(current_file):
|
||||||
if self.is_playing:
|
delay = self.calculate_frame_delay()
|
||||||
delay = self.calculate_frame_delay()
|
|
||||||
else:
|
|
||||||
delay = (
|
|
||||||
30 # Short delay when paused to enable responsive seeking
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
delay = self.IMAGE_DISPLAY_DELAY_MS
|
delay = self.IMAGE_DISPLAY_DELAY_MS
|
||||||
|
|
||||||
@@ -390,20 +407,20 @@ class MediaGrader:
|
|||||||
elif key == ord(" "): # Space - pause/play
|
elif key == ord(" "): # Space - pause/play
|
||||||
self.is_playing = not self.is_playing
|
self.is_playing = not self.is_playing
|
||||||
print(f"{'Playing' if self.is_playing else 'Paused'}")
|
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.playback_speed = max(
|
||||||
self.MIN_PLAYBACK_SPEED,
|
self.MIN_PLAYBACK_SPEED,
|
||||||
self.playback_speed - self.SPEED_INCREMENT,
|
self.playback_speed - self.SPEED_INCREMENT,
|
||||||
)
|
)
|
||||||
print(f"Speed: {self.playback_speed:.1f}x")
|
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.playback_speed = min(
|
||||||
self.MAX_PLAYBACK_SPEED,
|
self.MAX_PLAYBACK_SPEED,
|
||||||
self.playback_speed + self.SPEED_INCREMENT,
|
self.playback_speed + self.SPEED_INCREMENT,
|
||||||
)
|
)
|
||||||
print(f"Speed: {self.playback_speed:.1f}x")
|
print(f"Speed: {self.playback_speed:.1f}x")
|
||||||
elif self.handle_seeking_key(key):
|
elif self.handle_seeking_key(key):
|
||||||
# Seeking was handled
|
# Seeking was handled and frame was updated
|
||||||
pass
|
pass
|
||||||
elif key == ord("n"): # Next file
|
elif key == ord("n"): # Next file
|
||||||
break
|
break
|
||||||
@@ -421,11 +438,11 @@ class MediaGrader:
|
|||||||
self.last_key = None
|
self.last_key = None
|
||||||
print("Key released")
|
print("Key released")
|
||||||
|
|
||||||
# Only advance to next frame if playing AND it's a video
|
# Advance frame only if playing (and it's a video)
|
||||||
if not self.is_playing and self.is_video(current_file):
|
if self.is_playing and self.is_video(current_file):
|
||||||
# When paused, seek back one frame to stay on current frame
|
if not self.advance_frame():
|
||||||
# (since display_media already advanced us)
|
# End of video
|
||||||
continue
|
break
|
||||||
|
|
||||||
if key not in [ord("p")]: # Don't increment for previous
|
if key not in [ord("p")]: # Don't increment for previous
|
||||||
self.current_index += 1
|
self.current_index += 1
|
||||||
|
Reference in New Issue
Block a user