Created
July 3, 2023 17:39
-
-
Save spirrobe/48f8c8fa033957f0f9798f085835bf22 to your computer and use it in GitHub Desktop.
A python wrapper for a subprocess call of ffmpeg (needs to be callable on command line) to make a video (controllable out/in rate) of a directory of images (any supported type, i.e. png, jpg, gif (untested)). Handy to create animation for teaching, websites and presentations
This file contains hidden or 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
| # -*- coding: utf-8 -*- | |
| """ | |
| Created on Tue May 24 17:10:42 2022 | |
| @author: rspirig | |
| """ | |
| def img2vid(path_or_list_of_files, | |
| prefix='', | |
| moviename='auto', | |
| movietype='mp4', | |
| outrate=15, | |
| inrate=15, | |
| imgtype='auto', | |
| width=1280, | |
| height=960, | |
| preset='fast', | |
| quiet=True, | |
| ): | |
| """ | |
| Create a movie (mp4 or webm) from a series of images from a folder. | |
| Parameters | |
| ---------- | |
| path : str | |
| Where the images are located. | |
| prefix : str | |
| The prefix the images have, e.g., img_XX.png | |
| moviename : str | |
| The name of the movie that should be written out e.g., img_XX.png | |
| The default is movie_XX.mp4 where XX is checked avoid overwriting. | |
| movietype : str | |
| The format of the movie to be created e.g., mp4 or webm | |
| The default is mp4, see also parameter moviename. | |
| outrate : int, optional | |
| The framerate of the input. The default is 15. | |
| inrate : int, optional | |
| The framerate of the output video. The default is 15. | |
| imgtype : str, optional | |
| The imagetype to use as input. The default is 'auto', | |
| which means that jpg, jpeg, png, gif are looked at and collected | |
| width : int, optional | |
| The width of the output video. The default is 1280. | |
| height : int, optional | |
| The height of the output video. The default is 960. | |
| preset : str, optional | |
| The preset for video creation, determining the creation speed. | |
| The default is 'fast', other options are very_fast, medium, slow... | |
| quiet : bool, optional | |
| Whether to print progress to stdout or not. The default is True. | |
| Returns | |
| ------- | |
| None. | |
| """ | |
| import os | |
| import subprocess | |
| # cheap implementation of natsort to avoid dependency | |
| def natsorted(listlike): | |
| import re | |
| convert = lambda x: int(x) if x.isdigit() else x.lower() | |
| alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key)] | |
| return sorted(listlike, key=alphanum_key) | |
| # for convenience move to the path where the images are located | |
| # will change at the end to the original path again | |
| curdir = os.path.abspath(os.curdir) | |
| if isinstance(path_or_list_of_files, str): | |
| path = path_or_list_of_files | |
| else: | |
| path = os.path.dirname(path_or_list_of_files[0]) + os.sep | |
| if path.endswith(os.sep): | |
| pass | |
| else: | |
| path += os.sep | |
| os.chdir(path) | |
| if isinstance(path_or_list_of_files, str): | |
| filelist = [] | |
| for entry in os.scandir(path): | |
| if (not entry.name.startswith('.') | |
| and entry.is_file() | |
| and entry.name.startswith(prefix)): | |
| pass | |
| else: | |
| continue | |
| if imgtype == 'auto': | |
| imgtypes = ['png', 'jpeg', 'jpg', 'gif'] | |
| chk = [entry.name.lower().endswith(_) for _ in imgtypes] | |
| if max(chk): | |
| filelist.append(entry.name) | |
| _imgtype = [_ | |
| for _ in imgtypes | |
| if entry.name.lower().endswith(_)] | |
| else: | |
| if entry.name.lower().endswith(imgtype): | |
| filelist.append(entry.name) | |
| filelist = natsorted(filelist) | |
| else: | |
| filelist = [] | |
| for entry in path_or_list_of_files: | |
| entry = entry.split(os.sep)[-1] | |
| if imgtype == 'auto': | |
| imgtypes = ['png', 'jpeg', 'jpg', 'gif'] | |
| chk = [entry.lower().endswith(_) for _ in imgtypes] | |
| if max(chk): | |
| filelist.append(entry) | |
| _imgtype = [_ | |
| for _ in imgtypes | |
| if entry.lower().endswith(_)] | |
| else: | |
| if entry.name.lower().endswith(imgtype): | |
| filelist.append(entry) | |
| if imgtype == 'auto': | |
| if len(_imgtype) != 1: | |
| print('Issues with autodetection of image format.', | |
| 'We found the formats', _imgtype, | |
| 'Please pass in type directly via imgtype=...') | |
| return False | |
| imgtype = _imgtype[0] | |
| if filelist == []: | |
| print('No files found with these parameters') | |
| else: | |
| if not imgtype.startswith('.'): | |
| imgtype = '.' + imgtype | |
| cmd = "ffmpeg -r " | |
| cmd += f'{inrate} ' | |
| cmd += " -f concat " | |
| tmpfile = 'temp_filelist.txt' | |
| with open(path + tmpfile, 'w') as fo: | |
| for file in filelist: | |
| fo.writelines('file ' + (file).replace('/', "\\") + '\n') | |
| cmd += f' -i {tmpfile}' | |
| cmd += ' -vcodec libx264' | |
| cmd += f' -preset {preset} ' | |
| cmd += '-pix_fmt yuv420p -r ' | |
| cmd += str(outrate) | |
| cmd += ' -y -s ' + f'{width}x{height} ' | |
| # may be an issue if you have 1382195208752376502350 movie files in | |
| # the same folder which we hope is unlikely! | |
| startnumber = 0 | |
| while os.path.exists(path+f'movie_{startnumber}.mp4'): | |
| startnumber += 1 | |
| if moviename == 'auto': | |
| moviename = (f'movie_{startnumber}.{movietype}').replace('/', os.sep) | |
| cmd += moviename | |
| try: | |
| if not quiet: | |
| print('Calling', cmd) | |
| subprocess.check_call(cmd.split()) | |
| print(f'Successfully made movie {path + moviename}') | |
| except subprocess.CalledProcessError: | |
| print('Calling ffmpeg failed!', | |
| 'Make sure it is installed on your system via conda/pip/...') | |
| finally: | |
| pass | |
| os.remove(tmpfile) | |
| os.chdir(curdir) | |
| return path + os.sep + moviename |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment