Skip to content

Instantly share code, notes, and snippets.

@tstellanova
Last active December 27, 2023 17:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tstellanova/30c09da159a7bccd136ea1e627c3e5a3 to your computer and use it in GitHub Desktop.
Save tstellanova/30c09da159a7bccd136ea1e627c3e5a3 to your computer and use it in GitHub Desktop.
Grab a single frame from a video using ffmpeg
# 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