Spaces:
Sleeping
Sleeping
File size: 13,503 Bytes
9b79705 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 |
import gradio as gr
import cv2
import numpy as np
import os
import io
import base64
import tempfile # Make sure tempfile is imported
import math # Import math for ceiling function
# --- Helper Function ---
# (Keep the get_frame_from_video function exactly as it was)
def get_frame_from_video(video_path, frame_number):
"""Reads a specific frame from a video file."""
if not video_path or not os.path.exists(video_path):
return None, "Error: Video file not found or path is invalid."
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
try: cap.release()
except Exception: pass
return None, f"Error: Could not open video file: {video_path}"
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
frame_number = int(frame_number)
if total_frames <= 0:
cap.release()
return None, f"Error: Video file seems to have no frames or is corrupted: {os.path.basename(video_path)}"
max_frame = total_frames - 1
if frame_number < 0 or frame_number > max_frame:
cap.release()
return None, f"Error: Frame number {frame_number} is out of bounds (0-{max_frame})."
# Ensure frame number doesn't exceed max_frame even after calculation
frame_number = min(frame_number, max_frame)
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
ret, frame = cap.read()
cap.release()
if not ret:
# Attempt to read the last valid frame if the target failed (might happen near the end)
if frame_number > 0:
cap = cv2.VideoCapture(video_path)
cap.set(cv2.CAP_PROP_POS_FRAMES, max_frame)
ret_last, frame_last = cap.read()
cap.release()
if ret_last:
return cv2.cvtColor(frame_last, cv2.COLOR_BGR2RGB), f"Could not read frame {frame_number}. Showing last valid frame {max_frame}/{max_frame}."
return None, f"Error: Could not read frame {frame_number}."
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
status_message = f"Showing frame {frame_number}/{max_frame}." # Basic status
return frame_rgb, status_message
# --- Gradio Callback Functions ---
# 1. Function triggered when a video file is uploaded
def load_video(video_path):
"""Loads the video, gets its properties, updates the slider, displays the first frame, and hides download links."""
if video_path is None:
# Reset UI elements, including FPS state
return {
video_path_state: None,
fps_state: 0.0, # Reset FPS
frame_slider: gr.Slider(minimum=0, maximum=0, value=0, step=1, interactive=False),
frame_display: None,
info_text: "Please upload a video file (e.g., MP4).",
jpg_download_output: gr.File(value=None, visible=False),
png_download_output: gr.File(value=None, visible=False)
}
cap = cv2.VideoCapture(video_path)
fps = 0.0 # Initialize fps
if not cap.isOpened():
try: cap.release();
except: pass
return {
video_path_state: None,
fps_state: 0.0, # Reset FPS
frame_slider: gr.Slider(minimum=0, maximum=0, value=0, step=1, interactive=False),
frame_display: None,
info_text: f"Error: Could not open video file: {os.path.basename(video_path)}",
jpg_download_output: gr.File(value=None, visible=False),
png_download_output: gr.File(value=None, visible=False)
}
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
fps = cap.get(cv2.CAP_PROP_FPS) # Get FPS here
# Validate total_frames and fps
if total_frames <= 0 or not fps or fps <= 0:
cap.release()
error_msg = f"Error: Could not read valid metadata (frames={total_frames}, fps={fps}) from {os.path.basename(video_path)}."
return {
video_path_state: None,
fps_state: 0.0, # Reset FPS
frame_slider: gr.Slider(minimum=0, maximum=0, value=0, step=1, interactive=False),
frame_display: None,
info_text: error_msg,
jpg_download_output: gr.File(value=None, visible=False),
png_download_output: gr.File(value=None, visible=False)
}
duration = total_frames / fps
cap.release()
first_frame, status = get_frame_from_video(video_path, 0)
if first_frame is None:
# FPS is valid here, but first frame failed
return {
video_path_state: None,
fps_state: 0.0, # Reset FPS as we can't proceed
frame_slider: gr.Slider(minimum=0, maximum=0, value=0, step=1, interactive=False),
frame_display: None,
info_text: f"Error reading first frame: {status}",
jpg_download_output: gr.File(value=None, visible=False),
png_download_output: gr.File(value=None, visible=False)
}
info = (
f"Video: {os.path.basename(video_path)}\n"
f"Total Frames: {total_frames}\n"
f"FPS: {fps:.2f}\n"
f"Duration: {duration:.2f} seconds\n"
f"Use slider or video controls." # Updated instruction
)
slider_max = max(0, total_frames - 1)
return {
video_path_state: video_path,
fps_state: fps, # Store the valid FPS
frame_slider: gr.Slider(minimum=0, maximum=slider_max, value=0, step=1, interactive=True),
frame_display: first_frame,
info_text: info + "\n" + status,
jpg_download_output: gr.File(value=None, visible=False),
png_download_output: gr.File(value=None, visible=False)
}
# 2. Function triggered when the slider value changes (on release)
def update_frame_on_slider(video_path, frame_number):
"""Gets and displays the frame, updates status, and hides download links."""
if video_path is None:
return None, "No video loaded.", gr.File(value=None, visible=False), gr.File(value=None, visible=False)
frame_number = int(frame_number)
frame, status = get_frame_from_video(video_path, frame_number)
return frame, status, gr.File(value=None, visible=False), gr.File(value=None, visible=False)
# 3. Function to prepare and save the frame temporarily
# (Keep the save_frame_temporarily function exactly as it was)
def save_frame_temporarily(video_path, frame_number, image_format):
"""Saves the frame temporarily and returns the file path."""
if video_path is None:
return None, "No video loaded to download from."
frame_number = int(frame_number)
frame, status = get_frame_from_video(video_path, frame_number)
if frame is None:
return None, status # Return error status from get_frame
# Ensure frame is uint8
if frame.dtype != np.uint8:
if frame.max() <= 1.0:
frame = (frame * 255).astype(np.uint8)
else:
frame = frame.astype(np.uint8) # Potential clipping
# Choose suffix and encode
suffix = f".{image_format}"
frame_bgr = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) # Convert for OpenCV saving
if image_format == "jpeg":
ret, buffer = cv2.imencode('.jpg', frame_bgr)
elif image_format == "png":
ret, buffer = cv2.imencode('.png', frame_bgr)
else:
return None, f"Unsupported format: {image_format}"
if not ret:
return None, f"Error encoding frame {frame_number} to {image_format}."
try:
temp_file = tempfile.NamedTemporaryFile(suffix=suffix, delete=False)
temp_file.write(buffer)
temp_file.close()
file_path = temp_file.name
status = f"Frame {frame_number} ready. Click link below to download {image_format.upper()}."
return file_path, status
except Exception as e:
if 'temp_file' in locals() and temp_file:
try: os.unlink(temp_file.name)
except OSError: pass
return None, f"Error creating temporary file: {e}"
# 4. NEW Function: Triggered when video is paused
def sync_slider_on_pause(video_path, fps, pause_time: gr.Number):
"""Updates the slider value based on the video pause time."""
print(f"Pause event: video_path={video_path}, fps={fps}, pause_time={pause_time}") # Debug print
if video_path is None or not fps or fps <= 0 or pause_time is None:
print("Skipping slider sync due to invalid inputs.")
# Return an update that does nothing if inputs are invalid
return gr.Slider() # No change update
# Calculate frame number - use math.ceil to lean towards the frame *after* the pause time
# Or use int() for the frame *at* or *before* the pause time. Let's use int() for simplicity.
calculated_frame = int(pause_time * fps)
# Get the current max of the slider to prevent going out of bounds
# This requires the slider component itself to be passed, which is complex.
# Alternative: Re-read total frames here (less efficient but simpler state management)
cap = cv2.VideoCapture(video_path)
if cap.isOpened():
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
cap.release()
if total_frames > 0:
max_frame = total_frames - 1
# Clamp the calculated frame to the valid range
target_frame = max(0, min(calculated_frame, max_frame))
print(f"Calculated frame: {calculated_frame}, Target frame: {target_frame}")
return gr.Slider(value=target_frame)
else:
print("Skipping slider sync because total_frames <= 0.")
return gr.Slider() # No change
else:
print("Skipping slider sync because video couldn't be opened.")
return gr.Slider() # No change
# --- Gradio Interface ---
with gr.Blocks() as demo:
gr.Markdown("# Video Frame Explorer and Downloader")
gr.Markdown("Upload a video file (like MP4), use the slider OR pause the video to select a frame. Click a 'Prepare' button, then click the link that appears to download.")
video_path_state = gr.State(None)
fps_state = gr.State(0.0) # NEW state for FPS
with gr.Row():
with gr.Column(scale=1):
# Assign elem_id for potential JS interaction if needed later
video_input = gr.Video(label="Upload Video", sources=["upload"], format="mp4", elem_id="video_player")
info_text = gr.Textbox(label="Video Info / Status", interactive=False, lines=5)
jpg_download_output = gr.File(label="JPG Download Link", visible=False, interactive=False)
png_download_output = gr.File(label="PNG Download Link", visible=False, interactive=False)
with gr.Column(scale=2):
frame_display = gr.Image(label="Selected Frame", type="numpy", interactive=False)
# Assign elem_id for potential JS interaction
frame_slider = gr.Slider(
minimum=0, maximum=0, value=0, step=1,
label="Frame Number", interactive=False, elem_id="frame_slider"
)
with gr.Row():
prepare_jpg_button = gr.Button("Prepare JPG Download")
prepare_png_button = gr.Button("Prepare PNG Download")
# --- Component Interactions ---
# 1. Video Upload/Clear
video_input.upload(
fn=load_video,
inputs=video_input,
# Include fps_state in outputs
outputs=[video_path_state, fps_state, frame_slider, frame_display, info_text, jpg_download_output, png_download_output]
)
video_input.clear(
fn=load_video,
inputs=video_input,
# Include fps_state in outputs
outputs=[video_path_state, fps_state, frame_slider, frame_display, info_text, jpg_download_output, png_download_output]
)
# 2. Slider Release (hides download links)
frame_slider.release(
fn=update_frame_on_slider,
inputs=[video_path_state, frame_slider],
outputs=[frame_display, info_text, jpg_download_output, png_download_output]
)
# 3. Prepare JPG Download Button Click
prepare_jpg_button.click(
fn=save_frame_temporarily,
inputs=[video_path_state, frame_slider, gr.Textbox("jpeg", visible=False)],
outputs=[jpg_download_output, info_text]
).then(
lambda: {jpg_download_output: gr.File(visible=True), png_download_output: gr.File(visible=False)},
outputs=[jpg_download_output, png_download_output]
)
# 4. Prepare PNG Download Button Click
prepare_png_button.click(
fn=save_frame_temporarily,
inputs=[video_path_state, frame_slider, gr.Textbox("png", visible=False)],
outputs=[png_download_output, info_text]
).then(
lambda: {png_download_output: gr.File(visible=True), jpg_download_output: gr.File(visible=False)},
outputs=[png_download_output, jpg_download_output]
)
# 5. NEW: Video Pause Event
# The 'pause_time' argument to sync_slider_on_pause comes from the event data
video_input.pause(
fn=sync_slider_on_pause,
inputs=[video_path_state, fps_state], # Pass needed state
outputs=[frame_slider] # Update the slider value
)
# Optional: Trigger frame update immediately after slider syncs from pause
# video_input.pause(...).then(
# fn=update_frame_on_slider,
# inputs=[video_path_state, frame_slider], # Use the NEW slider value
# outputs=[frame_display, info_text, jpg_download_output, png_download_output]
# )
# --- Launch the App ---
if __name__ == "__main__":
demo.launch(debug=True) # Add debug=True for easier troubleshooting |