Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Extract frames from an animated GIF, correctly handling palettes and frame update modes
import os
from PIL import Image
'''
I searched high and low for solutions to the "extract animated GIF frames in Python"
problem, and after much trial and error came up with the following solution based
on several partial examples around the web (mostly Stack Overflow).
There are two pitfalls that aren't often mentioned when dealing with animated GIFs -
firstly that some files feature per-frame local palettes while some have one global
palette for all frames, and secondly that some GIFs replace the entire image with
each new frame ('full' mode in the code below), and some only update a specific
region ('partial').
This code deals with both those cases by examining the palette and redraw
instructions of each frame. In the latter case this requires a preliminary (usually
partial) iteration of the frames before processing, since the redraw mode needs to
be consistently applied across all frames. I found a couple of examples of
partial-mode GIFs containing the occasional full-frame redraw, which would result
in bad renders of those frames if the mode assessment was only done on a
single-frame basis.
Nov 2012
'''
def analyseImage(path):
'''
Pre-process pass over the image to determine the mode (full or additive).
Necessary as assessing single frames isn't reliable. Need to know the mode
before processing all frames.
'''
im = Image.open(path)
results = {
'size': im.size,
'mode': 'full',
}
try:
while True:
if im.tile:
tile = im.tile[0]
update_region = tile[1]
update_region_dimensions = update_region[2:]
if update_region_dimensions != im.size:
results['mode'] = 'partial'
break
im.seek(im.tell() + 1)
except EOFError:
pass
return results
def processImage(path):
'''
Iterate the GIF, extracting each frame.
'''
mode = analyseImage(path)['mode']
im = Image.open(path)
i = 0
p = im.getpalette()
last_frame = im.convert('RGBA')
try:
while True:
print "saving %s (%s) frame %d, %s %s" % (path, mode, i, im.size, im.tile)
'''
If the GIF uses local colour tables, each frame will have its own palette.
If not, we need to apply the global palette to the new frame.
'''
if not im.getpalette():
im.putpalette(p)
new_frame = Image.new('RGBA', im.size)
'''
Is this file a "partial"-mode GIF where frames update a region of a different size to the entire image?
If so, we need to construct the new frame by pasting it on top of the preceding frames.
'''
if mode == 'partial':
new_frame.paste(last_frame)
new_frame.paste(im, (0,0), im.convert('RGBA'))
new_frame.save('%s-%d.png' % (''.join(os.path.basename(path).split('.')[:-1]), i), 'PNG')
i += 1
last_frame = new_frame
im.seek(im.tell() + 1)
except EOFError:
pass
def main():
processImage('foo.gif')
processImage('bar.gif')
if __name__ == "__main__":
main()
@fjania

This comment has been minimized.

Copy link

commented Oct 18, 2013

Thank you sir!

@BigglesZX

This comment has been minimized.

Copy link
Owner Author

commented Oct 28, 2013

@fjania You're most welcome!

@tdhsmith

This comment has been minimized.

Copy link

commented Feb 22, 2014

Awesome work! I needed this. Minor nitpick though: there's a weird semantic division here between partial-size frames and frames that are full size, but have transparent regions. In the former we'll output visually-opaque frames but in the latter we'll output partially transparent frames. That is, partial-syle GIFs will return what the GIF looks like during that frame, but full-style GIFs will return only that frame's delta.

Example: http://imgur.com/a/xuK0T

IMO, pasting the previous frames should be an option independent from the partial size detection. If the user doesn't choose to paste, we can handle partials by offsetting the frame the right amount on a transparent base layer.

@tdhsmith

This comment has been minimized.

Copy link

commented Feb 22, 2014

Also, strictly speaking, it looks like tile[1] returns (x1, y1, x2, y2), not (x1, y1, width, height) so analyseImage should check if the tile starts at 0, 0 as well, in case we have a partial that still reaches to the bottom right corner.

update_region = tile[1]
if update_region != (0, 0,) + im.size:
@tasdemirbahadir

This comment has been minimized.

Copy link

commented Jan 29, 2016

Thank you for the grate work! But I have a very little question to ask:
-) I am currently using your code and it's working great, however for some gif images, the frames are fetched a little bit distorted with this code and I couldn't figure it out yet.
The gif file:
giphys
Do you have any idea why this is happening?

@almost

This comment has been minimized.

Copy link

commented Jun 30, 2016

Thanks for this!

I made a fork that supplies frames as returns from a generator: https://gist.github.com/almost/d2832d0998ad9dfec2cacef934e7d247

@Misairu-G

This comment has been minimized.

Copy link

commented Nov 3, 2016

Awesome code, ineffable appreciation

@tkoak

This comment has been minimized.

Copy link

commented Feb 23, 2017

Very useful code! Thanks!

@Drachenfels

This comment has been minimized.

Copy link

commented Nov 23, 2017

I have plenty of gifts that are defeating this script, I had my suspicion after analysing the code, once I am done I will share my fork with you guys.

Example below:

test2

@elff1493

This comment has been minimized.

Copy link

commented Jul 31, 2018

nice, but the second to last frame of a transparent gif bleeds into the last frame

@JankesJanco

This comment has been minimized.

Copy link

commented Jun 17, 2019

hello! this still not resolve problems with some GIFs, see examples bellow. Partly it is caused by bug within opening GIF in Python PIL library, see python-pillow/Pillow#2893. There is workaround in that issue that resolve problems with singer GIF bellow but will not help with the other GIF.

If you want to implement GIF processing in your application and you dont want just extract/edit few GIFs for yourself, maybe moviepy library is better option. Dont get me wrong, I think that Python PIL is great for image processing, but it seems that processing GIFs by Python PIL is currently buggy.

bowie
singer

UPDATE: looks like there are more reported issues with GIF in Python PIL library https://github.com/python-pillow/Pillow/issues?q=is%3Aissue+is%3Aopen+gif+label%3A%22Palette+Issue%22

@BigglesZX

This comment has been minimized.

Copy link
Owner Author

commented Jun 17, 2019

Thanks @JankesJanco – it's been many years since I went anywhere near this, but I'm glad it's been useful for some folks! I agree that using a library like moviepy is probably going to be more reliable these days.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.