Created
January 7, 2022 17:24
-
-
Save mjforan/7005699eea8f1014a1ad73d59f75caf3 to your computer and use it in GitHub Desktop.
Create boomerang animation from video or frames
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
# Goal: video frames are duplicated and reversed (creating effect like Instagram's boomerang) and combined in animated webp | |
# Requires: ffmpeg, tested on ffmpeg 2021-11-10-git-44c65c6cc0-full_build-www.gyan.dev with Python 3.9.2 on Windows 10 | |
# Args: mode input_file output_file [start_time] [end_time] | |
# Example: python boomerang.py v test.mp4 out.webp -s 00:00:02 -e 00:00:10 | |
import argparse | |
import os | |
import shutil | |
from signal import signal, SIGINT | |
import subprocess | |
import sys | |
temp_modified = False | |
out_modified = False | |
dir_temp = "boomerang_temp" | |
parser = argparse.ArgumentParser(description='Convert video to boomerang animated webp') | |
parser.add_argument('mode', help='mode: v for video and f for frames') | |
parser.add_argument('input', help='input file or directory') | |
parser.add_argument('output', help='output file') | |
parser.add_argument('-s', dest='start', help='optional start time (only applicable to video mode)') | |
parser.add_argument('-e', dest='end', help='optional end time (only applicable to video mode)') | |
args = parser.parse_args() | |
# clean up if killed prematurely | |
def kill_handler(signal_received, frame): | |
print('killed') | |
# don't delete unless we passed the prompt about overwriting contents, in case the user had important files there | |
if temp_modified: | |
shutil.rmtree(dir_temp) | |
if out_modified: | |
os.remove(args.output) | |
sys.exit(0) | |
signal(SIGINT, kill_handler) | |
# check if output file exists | |
if os.path.isfile(args.output): | |
delete = input("Output file exists. Overwrite? [y/N] ") | |
if delete.lower() == 'y' or delete.lower() == 'yes': | |
os.remove(args.output) | |
else: | |
sys.exit(-1) | |
# set flag so output file is cleaned up if the program is killed after this point | |
out_modified = True | |
# check if temp dir exists | |
if os.path.isdir(dir_temp): | |
if len(os.listdir(dir_temp))>0: | |
delete = input("Temp directory exists and is not empty. Overwrite contents? [y/N] ") | |
if delete.lower() == 'y' or delete.lower() == 'yes': | |
shutil.rmtree(dir_temp) | |
os.makedirs(dir_temp) | |
else: | |
sys.exit(-1) | |
else: | |
os.makedirs(dir_temp) | |
# set flag so temp is cleaned up if the program is killed after this point | |
temp_modified = True | |
if args.mode=="v": | |
# Extract frames from input video | |
start = ['-ss', args.start] if args.start else [] | |
end = ['-to', args.end] if args.end else [] | |
subprocess.run(['ffmpeg', '-i', args.input]+start+end+['{}/%08d.png'.format(dir_temp)]) | |
else: | |
# copy frames to temp dir | |
shutil.rmtree(dir_temp) | |
shutil.copytree(args.input, dir_temp) | |
file_names = os.listdir(dir_temp) | |
indices = [int(name[:8].lstrip("0")) for name in file_names] | |
start = max(indices) # index of last frame, i.e. middle of boomerang | |
# mirror all frames about the last frame | |
for i in range(len(indices)): | |
frm = start-i # work backwards from middle point | |
to = start+1+i | |
shutil.copyfile(dir_temp+"\\"+"{:08d}.png".format(frm), dir_temp+"\\"+"{:08d}.png".format(to)) | |
# you might want to change encoder quality for smaller file size and smoother playback | |
subprocess.run(['ffmpeg', '-start_number', str(min(indices)), '-i', dir_temp+'\%8d.png', '-lossless', '1', '-loop', '0', args.output]) | |
# clean up temp dir | |
shutil.rmtree(dir_temp) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment