GIF frame extraction made easy (sci. vis. util)

TL;DR;Summary

Extract the single frames of a GIF to repurpose them (for example to create a video) with the below code. As always, the most up-to-date version can be found in the GitHub gist

def gif2imgs(file,
             save=False,
             outpath='same',
             outfilename='same',
             ):
    """
    Extract image array from a GIF file.

    Based on PIL, frames of a GIF file are extracted to a numpy array. If
    more than one frame is present in the GIF, a list of numpy arrays is 
    returned. It is possible to save the images directy, by default in the 
    same path as the GIF and the same basename.
    Parameters
    ----------
    file : str
        The GIF file containing one or more image frames.
    save : bool, optional
        Whether to save the image frames directy. The default is False, i.e.
        the function returns a list of the image frames as seperate numpy arrays
    outpath : str, optional
        Where to save the extracted image frames.
        The default is 'same', i.e. same folder
    outfilename : str, optional
        How to name the extracted image as files.
        The default is 'same', i.e. taking the basename and adding _frameX
        to it. If a string is passed, the same suffix is added but the basename
        is taken as the passed in string.

    Returns
    -------
    list of frames or two lists (of filenames of extracted image and images)
        The extracted frames from the GIF and if saving was turned on also 
        the filenames of written out frames.

    """

    import PIL
    import numpy as np
    import os
    import matplotlib.image as img

    pilIm = PIL.Image.open(file)
    pilIm.seek(0)

    # Read all images inside
    images = []
    try:
        while True:
            # Get image as numpy array
            tmp = pilIm.convert()  # Make without palette
            a = np.asarray(tmp)
            if len(a.shape) == 0:
                continue
            # Store, and next
            images.append(a)
            pilIm.seek(pilIm.tell()+1)
    except EOFError:
        pass

    if save:
        if outpath == 'same':
            outpath = os.path.dirname(file) + os.sep
        if outfilename == 'same':
            outfilename = os.path.basename(file)

            outfiles = []
        for frameno, frame in enumerate(images):
            _ = outpath+outfilename.rstrip('.gif') + f'_frame{frameno}.png'
            img.imsave(_, frame, vmin=0, vmax=255)
            outfiles.append(_)

        return outfiles, images
    else:
        return images
      

if __name__ == '__main__':
    import requests
    import matplotlib.pyplot as plt
    fileurl = 'https://gifdb.com/images/high/jumping-cat-typing-on-keyboard-2b15r60jnh8hn5sv.gif'
    response = requests.get(fileurl)
    filename = './example.gif'
    with open(filename, 'wb') as fo:
      fo.write(response.content)
    frames = gif2imgs(filename)
    plt.imshow(frames[0])

Recently I wanted to make a video for a talk. And that is where the trouble started.

Initially, I thought to place a video and GIF side by side. However, getting a synchronous playback was tricky because there are few to no control options for GIFs in most presentation software. As a result, I had to extract the frames from the GIF instead and had to convert them into a video. While I could do this with any of many online services I specifically needed a synchronised playback as the timing of the content needed to match. The code above does exactly these frame extractions and pairs up nicely with another utility, the img2vid (which converts image files to a video with a choosable frame rate.

Simply set the option to save the frames and then run the img2vid on the single images to make the video and you can use it with controls in your presentation software, be it PowerPoint, or anything else.

The above main part gets you an example GIF (it had to be a cat, right?) and shows you the first frame. Enjoy!

Images to video (sci. vis. util)

TL;DR; Summary & Code

Use the below Python snippet to create an mp4/webm video based on images in a path that all have a prefix (which can be an empty string too of course). It requires you to have setup ffmpeg to be found on the command line (there would be ways to use the Python version too but this way you can also simply create a link to the standalone version if you are on *nix). The gist version on my github should always be up to date.

Python
# -*- coding: utf-8 -*-
"""
At some point in time

@author: elbarto
"""

def img2vid(path,
            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)
    os.chdir(path)

    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)

    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+os.sep + moviename}')
        except subprocess.CalledProcessError:
            print('Calling ffmpeg failed!',
                  'Make sure it is installed on your system via conda/pip/...')
        finally:
            pass
            os.chdir(curdir)
            os.remove(tmpfile)

        return path + os.sep + moviename

Background & motivation

Who doesn’t know it? You have to give a talk, illustrate your findings or simply want to show something extra on your poster at a conference with a tablet or linked via QR code. Now you can upload your image sequence to many online pages that will convert it into a format of your choice. After you made the video, you notice a mistake in the images and you have to redo it – maybe more than once even. If you want several videos, repeat the process even more often.

Instead, you could use video software suites that render the images into videos, but this is essentially the same tedious process and often requires you to learn the software (which has its own merit but maybe you are lacking the time). Why not program it instead?

The requirements are actually quite easy to meet, especially if we are using ffmpeg and Python. This requires you to have setup ffmpeg so it can be found on the command line.

Development process

To simplify the process, let’s look at the requirements for the function that were important for me at the time:

  • Call ffmpeg on the command line
  • Name the movie and do not overwrite existing movies
  • Pass in a path of images or a list of files (handy if you store the output from another script)
  • Which kind of move to make (mp4 usually is compatible the most, but webm is also useful when making videos for the web/browsers – I tend to go with mp4 for powerpoint presentations, but webm is also supported by MS Office 365 nowadays)
  • How fast the movie should play (in/outrate)
  • Which image type the images are (read jpeg, jpg, png gif are fine)
  • The dimensions the video should have (width, height), per default the first image dimension is taken
  • How the rendering by ffmpeg should be done (fast is usually good enough quality, there is a tradeoff, see documentation of ffmpeg)
  • Whether to report some progress during the making – aka the quiet option

Some things to consider are:

  • You could use natsorted, to get a natural sort of the files as that is usually how we humans would sort them. Usually, this makes little difference but natural sorting works better with mixed naming conventions (0001, 0010, 0100 … vs 1, 10, 100 …). Instead of another dependency, a cheap natsort is implemented as well. Replace the function if you actually have natsort installed and want to use it instead.
  • Instead you can also directly run the video command via ffmpeg on the command line – this is just a thin wrapper to keep some default options in place that made sense to me. You could also write files out to a file and load them via ffmpeg instead …

Other than that, the process is straightforward. Pass in your directory and the prefix that the images might have (tune some things if you want to). Otherwise, enjoy your video making and as a teaser, the following timelapse is made via the above script on a regular basis and linked here

Further reading and resources