Skip to content

Instantly share code, notes, and snippets.

@thediveo
Last active October 10, 2024 09:11
Show Gist options
  • Save thediveo/57fd76e4d15252232aaacc7e422a79a2 to your computer and use it in GitHub Desktop.
Save thediveo/57fd76e4d15252232aaacc7e422a79a2 to your computer and use it in GitHub Desktop.
Scans for extracted frame image PNG files and (re)extracts the frame directly from their corresponding video media files, using source resolution and orientation instead of project resolution and orientation. Frame image PNG files are searched in the current working directory, as well as all its sub directories.
#!/bin/bash
#
# (Re-) Extract frame images directly from its corresponding video media
# file. The frame image filenames are expected to be in the format of
# videofilename-f000000.png, that is: the filename of the video media file,
# but without its extension, then followed by "-f" and a six digit frame
# number (base 10), and finally the ".png" suffix.
#
# Copyright 2018 TheDiveO
#
# https://gist.github.com/TheDiveO/57fd76e4d15252232aaacc7e422a79a2
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
shopt -s extglob
shopt -s globstar # who comes up with such option identifiers?
function version {
echo "version 1.0.0"
exit 1
}
function usage {
echo "Usage:" $(basename "$0") "[options] project.kdenlive"
echo "Scans for extracted frame image PNG files and (re)extracts the frame directly"
echo "from their corresponding video media files, using source resolution and orientation"
echo "instead of project resolution and orientation. Frame image PNG files are searched"
echo "in the current working directory, as well as all its sub directories."
echo ""
echo "Options:"
echo " -h, --help print help and exit"
echo " --version print version and exit"
exit 1
}
projectfile=""
# First some not too complicated command line argument parsing...
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--h)
usage
;;
--version)
version
;;
*)
if [[ ! -z "$projectfile" ]]; then
echo "error: too many arguments"
exit 1
fi
projectfile="$1"
;;
esac
shift
done
# Some basic checks on the required first parameter, the Kdenlive project
# filename: it must be specified, AND must end in ".kdenlive", AND must
# refer to an existing and non-empty file.
if [[ -z "$projectfile" ]]; then
usage
fi
if [[ ! "$projectfile" =~ .*\.kdenlive ]]; then
echo "invalid argument: Kdenlive project filename must end in .kdenlive"
exit 1
fi
if [[ ! -s "$projectfile" ]]; then
echo "invalid argument: '$1' is not a file"
exit 1
fi
# Fetch the project frame rate from the project's .kdenlive file: it is
# stored in Kdenlive project files not as a floating point value but
# instead as numerator and denominator.
fps_num=$(xmllint --xpath "string(/mlt/profile/@frame_rate_num)" "$projectfile")
fps_den=$(xmllint --xpath "string(/mlt/profile/@frame_rate_den)" "$projectfile")
# Just for the reporting eye candy we go through hops and loops to achieve
# visually appealing framerate number formatting :) Thanks to
# https://stackoverflow.com/a/30048933/6632214 for the sed expression to
# remove trailing 0 decimal places.
dispfps=$(echo "scale=4; ($fps_num/$fps_den+0.00005)/1" | bc \
| sed "/\./ s/\.\{0,1\}0\{1,\}$//")
echo "detected project framerate:" $dispfps "fps"
# Work on all extracted frame image files following the filename convention
# of <video filename>-f<6digit frame number>.png
# Since the video filename included in the frame image filename lacks the
# original extension, we currently try a (small) set of well-known extensions,
# both in lowercase and uppcase.
for framepng in **/*-f+([0-9]).png; do
if [[ "$framepng" =~ (.*)-f([[:digit:]]+)\.png ]]; then
fbasename=${BASH_REMATCH[1]}
frameno=${BASH_REMATCH[2]}
for ext in mp4 MP4; do
if [[ -f "$fbasename.$ext" ]]; then
videofile=$fbasename.$ext
break
fi
done
if [[ -f "$videofile" ]]; then
# WARNING: we cannot directly use the frame number derived from the frame
# image filename, because Kdenlive calculates those framenumbers based on
# the project framerate, yet ffmeg uses frame numbers based on the particular
# video file framerate (yeah, this is simplified, I know). In consequence, we have
# to convert the project fps-based frame number into a fps-invariant time duration
# which we can then use with the "-ss" parameter. The time duration can be
# either specified as hh:mm:ss.d..., or simply as s+.d... (that is, seconds
# and decimal places). Using the simple seconds.decimals format relieves
# us from complex drop frame calculations in case of "weird" project
# frame rates.
#
# References:
# https://ffmpeg.org/ffmpeg.html#toc-Main-options
# https://ffmpeg.org/ffmpeg-utils.html#time-duration-syntax
# First remove all leading zeros from the filename-embedded frame
# number and interpret it as base 10.
let "fno=10#$frameno"
secs=$(echo "scale=3; ($fno*$fps_den)/$fps_num" | bc)
echo "frame #$frameno@${dispfps}fps/$secs from '$videofile'"
ffmpeg -loglevel warning -y -ss $secs -i "$videofile" -vframes 1 "$framepng"
if [[ $? -ne 0 ]]; then
tput setaf 1; echo "frame extraction failed, exiting."; tput sgr0
exit 1
fi
tput setaf 2; echo "==> '$framepng'"; tput sgr0
else
tput setaf 5; echo "Skipping non-existing '$videofile'"; tput sgr0
fi
fi
done
@kokup
Copy link

kokup commented Oct 10, 2024

more a question than a comment:

just trying to extract a single frame:

frame #000224@29.97fps/7.474 from '2.mp4'
[image2 @ 0x56369e29f800] The specified filename 'untitled-f000224.png' does not contain an image sequence pattern or a pattern is invalid.
[image2 @ 0x56369e29f800] Use a pattern such as %03d for an image sequence or use the -update option (with -frames:v 1 if needed) to write a single image.
...
$ ./extract-frames 2.kdenlive -update -frames:v 1
error: too many arguments

what is the correct syntax? sorry i couldn't figure it out from reading the code

@thediveo
Copy link
Author

Your file name looks fine to me. It appears to be a ffmpeg-originating error message, indicating that there is some change in the ffmpeg args. Unfortunately, I haven't used the script for a long time so I don't know what ffmpeg now expects.

@kokup
Copy link

kokup commented Oct 10, 2024

it reads to me like an error generated by your script, but that may be immaterial if you have moved on from kdenlive to something better? i like kdenlive, but i've had a constant stream of niggles, and the crazy idea to not extract a frame from the image in the preview window seems to me just daft. i glanced at olive, but can't find a decent "how to" manual.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment