Last active
December 27, 2023 17:55
-
-
Save tstellanova/30c09da159a7bccd136ea1e627c3e5a3 to your computer and use it in GitHub Desktop.
Grab a single frame from a video using ffmpeg
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Input a video ID (this is used to build an input filename) and frame to dump | |
video_id=$1 | |
guess_frame=$2 | |
# modern video formats prefer timestamps over framerates, so YMMV | |
frame=$(( ${guess_frame} )) | |
printf "video_id: %s frame: %d \r\n" ${video_id} ${frame} | |
# this is a heuristic guess as to how frequently (every 250 frames) we see a keyframe | |
keyframe=$(( ${frame} / 250 )) | |
keyframe=$(( (${keyframe}*250) - 2 )) | |
printf "keyframe: %d \r\n" ${keyframe} | |
delta_frames=$(( (${frame} - ${keyframe}) )) | |
printf "delta_frames: %d \r\n" ${delta_frames} | |
# Can likely use eg ffprobe to get the actual framerate | |
#frames_sec=(30000/1001) | |
inv_frames_sec=1001/30000 | |
#frames_sec=30 | |
#printf "frames_sec: %s \r\n" ${frames_sec} | |
printf "inv_frames_sec: %s \r\n" ${inv_frames_sec} | |
total_sec=$((${keyframe}*${inv_frames_sec})) | |
#printf "total_secs: %d \r\n" ${total_sec} | |
whole_min=$(( ${total_sec}/60 )) | |
#printf "whole_min: %d \r\n" ${whole_min} | |
whole_sec=$(((${total_sec}%60))) | |
#printf "whole_sec: %d \r\n" ${whole_sec} | |
# calculate hundredeths of a second for the fractional second | |
sec_frac=$(( ( (${keyframe}*100)*${inv_frames_sec}) % 100 )) | |
#printf "sec_frac: %d \r\n" ${sec_frac} | |
end_sec_frac=$(( ${sec_frac} )) | |
# what is the final timestamp we're trying to capture? | |
end_total_sec=$(( (${frame}*${inv_frames_sec}) )) | |
end_whole_min=$(( ${end_total_sec}/60 )) | |
end_whole_sec=$(((${end_total_sec}%60))) | |
end_whole_sec_frac=$(( ((${frame}*100)*${inv_frames_sec}) % 100 )) | |
# keyframe start time | |
ts_start_spec=$(printf "00:%02d:%02d.%02d" ${whole_min} ${whole_sec} ${sec_frac}) | |
# desired frame start time | |
ts_end_spec=$(printf "00:%02d:%02d.%02d" ${end_whole_min} ${end_whole_sec} ${end_whole_sec_frac} ) | |
printf "ts_start: '%s' \r\n" ${ts_start_spec} | |
printf "ts_end : '%s' \r\n" ${ts_end_spec} | |
# This is a relatively fast method that skips directly to the desired timestamp (or as close as possible): | |
outfile="./annotated_frames/${video_id}_fast_${guess_frame}.png" | |
if test -f "$outfile" | |
then | |
echo "$outfile exists." | |
else | |
echo "$outfile does not exist." | |
ffmpeg -ss ${ts_end_spec} -i eo_wide_front_left-${video_id}.mp4 -frames 1 ${outfile} | |
fi | |
# This method is fast but maybe no more accurate than the method that doesn't guess keyframes (above) | |
# It first skips to a guessed keyframe, then steps forward delta_frames to capture the desired frame. | |
outfile="./annotated_frames/${video_id}_delta_${guess_frame}.png" | |
if test -f "$outfile" | |
then | |
echo "$outfile exists." | |
else | |
echo "$outfile does not exist." | |
ffmpeg -ss ${ts_start_spec} -i eo_wide_front_left-${video_id}.mp4 -vf "select=eq(n\,${delta_frames})" -frames 1 ${outfile} | |
fi | |
# This is the very slow naive method that reads and renders every frame of the video, | |
# up until we reach the desired frame count: | |
outfile="./annotated_frames/${video_id}_slow_${guess_frame}.png" | |
if test -f "$outfile" | |
then | |
echo "$outfile exists." | |
else | |
echo "$outfile does not exist." | |
ffmpeg -i eo_wide_front_left-${video_id}.mp4 -vf "select=eq(n\,${guess_frame})" -frames 1 -ss ${ts_end_spec} ${outfile} | |
fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment