-
-
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.