-
-
Save BigglesZX/4016539 to your computer and use it in GitHub Desktop.
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() |
Hi Biggles,
you did a great job! There are many, many ... many coders searching for this solution.
Best regards
Axel Arnold Bangert - Herzogenrath 05.03.2020
How would you save the changed frames inside the gif as a new animated gif? With this we could operate on frames inside the gif and save it.
Axel
Hi Axel, that's a little outside of the scope of this code I'm afraid – I also haven't touched it in many years so there are probably better solutions out there by now. moviepy is mentioned above, that might help. In any case I hope it was helpful.
Hi Axel – that's great, always nice to hear from other people. Buying extra pasta in London today :). Good luck with your project and stay healthy! James
Due to PIL updates, to continue using this file as is this would be a possible solution
if not im.getpalette() and im.mode in ("L", "LA", "P", "PA"):
im.putpalette(p)
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.