New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: allow animations to be saved as animated GIFs #1337

Merged
merged 2 commits into from Oct 8, 2012

Conversation

Projects
None yet
5 participants
@jakevdp
Contributor

jakevdp commented Oct 5, 2012

This is a basic enhancement that allows animations to be saved as animated GIFs, using the ImageMagick convert utility. It works with either temporary files or piping.

A deficiency currently is that extra arguments and options are not passed through: the only option used currently is setting the correct frame rate. Extra options are tough, because they are position dependent. I'm not sure where they should be inserted in the command by default.

A test script can be found here: https://gist.github.com/3843162

@ghost ghost assigned dopplershift Oct 8, 2012

@dopplershift

This comment has been minimized.

Show comment
Hide comment
@dopplershift

dopplershift Oct 8, 2012

Contributor

Great work, this should be useful to a lot of people. I'm content to punt on the extra arguments until someone comes up with a use case, because I'm not familiar enough with convert. The extra arguments are a lot more important with ffmpeg and mencoder since you've got a lot of fine grained options with the codecs for quality. As far as I know, you don't have nearly the abilities (or need) with gifs.

Contributor

dopplershift commented Oct 8, 2012

Great work, this should be useful to a lot of people. I'm content to punt on the extra arguments until someone comes up with a use case, because I'm not familiar enough with convert. The extra arguments are a lot more important with ffmpeg and mencoder since you've got a lot of fine grained options with the codecs for quality. As far as I know, you don't have nearly the abilities (or need) with gifs.

@dopplershift dopplershift reopened this Oct 8, 2012

dopplershift added a commit that referenced this pull request Oct 8, 2012

Merge pull request #1337 from jakevdp/gif-animation
ENH: allow animations to be saved as animated GIFs

@dopplershift dopplershift merged commit 94dec18 into matplotlib:master Oct 8, 2012

1 check passed

default The Travis build passed
Details
@jakevdp

This comment has been minimized.

Show comment
Hide comment
@jakevdp

jakevdp Oct 8, 2012

Contributor

Great, thanks!

Contributor

jakevdp commented Oct 8, 2012

Great, thanks!

@WeatherGod

This comment has been minimized.

Show comment
Hide comment
@WeatherGod

WeatherGod Oct 10, 2012

Member

Note to self: Needs at least an entry in whats_new.rst.

As for extra arguments, from my personal archive of code, I have found the following arguments to be very valuable:

  • "-set delay 40"
    For obvious reasons
  • "-set dispose none"
    For some reason, some browsers and/or viewers wouldn't display the animated gif properly until I did this. Note that I think this can cause the viewer's memory usage to go up. So this might not be advisable anymore.
  • "-loop 0"
    This indicates how many times to loop the animation. If you want infinite loop, you have to specify a loop count of zero. While many viewers will still infinitely loop if this option is not given, some will not (I forget which).
Member

WeatherGod commented Oct 10, 2012

Note to self: Needs at least an entry in whats_new.rst.

As for extra arguments, from my personal archive of code, I have found the following arguments to be very valuable:

  • "-set delay 40"
    For obvious reasons
  • "-set dispose none"
    For some reason, some browsers and/or viewers wouldn't display the animated gif properly until I did this. Note that I think this can cause the viewer's memory usage to go up. So this might not be advisable anymore.
  • "-loop 0"
    This indicates how many times to loop the animation. If you want infinite loop, you have to specify a loop count of zero. While many viewers will still infinitely loop if this option is not given, some will not (I forget which).
@jakevdp

This comment has been minimized.

Show comment
Hide comment
@jakevdp

jakevdp Oct 10, 2012

Contributor

In the code, -loop 0 is set by default, and -delay is set appropriately based on the user-specified frame rate.

Regarding the dispose argument: where does this appear in the argument list? We could allow setting this through extra_args without overriding the base class __init__, but the various possible extra arguments need to be at different places in the command, so it's not clear what the default behavior should be.

One solution would be to add multiple "extra_args" options, but this might be confusing.

Contributor

jakevdp commented Oct 10, 2012

In the code, -loop 0 is set by default, and -delay is set appropriately based on the user-specified frame rate.

Regarding the dispose argument: where does this appear in the argument list? We could allow setting this through extra_args without overriding the base class __init__, but the various possible extra arguments need to be at different places in the command, so it's not clear what the default behavior should be.

One solution would be to add multiple "extra_args" options, but this might be confusing.

@WeatherGod

This comment has been minimized.

Show comment
Hide comment
@WeatherGod

WeatherGod Oct 10, 2012

Member

Right, I was merely doing a knowledge dump of what I have learned over the
years with image magick. I didn't look closely enough at the code to
notice that the delay and -loop 0 was already set (although I would wonder
if it is worth making it possible for the -loop option to be
parameterized?).

Here are some references that may prove useful for anybody who cares enough:
http://www.imagemagick.org/Usage/anim_basics/
http://www.imagemagick.org/Usage/anim_opt/

Member

WeatherGod commented Oct 10, 2012

Right, I was merely doing a knowledge dump of what I have learned over the
years with image magick. I didn't look closely enough at the code to
notice that the delay and -loop 0 was already set (although I would wonder
if it is worth making it possible for the -loop option to be
parameterized?).

Here are some references that may prove useful for anybody who cares enough:
http://www.imagemagick.org/Usage/anim_basics/
http://www.imagemagick.org/Usage/anim_opt/

@dopplershift

This comment has been minimized.

Show comment
Hide comment
@dopplershift

dopplershift Oct 10, 2012

Contributor

The convert backend could add logic to handle extra_args on its own and put pieces in the appropriate place, I guess. But then again it's been awhile since I wrote the backend code, so I'm not even sure what landmines I may have left that make this non-sensical.

Contributor

dopplershift commented Oct 10, 2012

The convert backend could add logic to handle extra_args on its own and put pieces in the appropriate place, I guess. But then again it's been awhile since I wrote the backend code, so I'm not even sure what landmines I may have left that make this non-sensical.

@JamesPHoughton

This comment has been minimized.

Show comment
Hide comment
@JamesPHoughton

JamesPHoughton Jul 31, 2013

I've managed to create a .gif animation using matplotlib and imagemagick, but haven't managed to then import the created file into the notebook:

anim = animation.FuncAnimation(fig, animate, frames=100, init_func=init)
anim.save('animation.gif', writer='imagemagick', fps=10);

from IPython.display import Image
Image(filename='animation.gif')

gives an error:

---------------------------------------------------------------------------
UnicodeDecodeError                        Traceback (most recent call last)
<ipython-input-81-8c01b6fb0d34> in <module>()
      1 from IPython.display import Image
----> 2 Image(filename='animation.gif')

/Library/Python/2.7/site-packages/IPython/core/displayhook.pyc in __call__(self, result)
    240             self.update_user_ns(result)
    241             self.log_output(format_dict)
--> 242             self.finish_displayhook()
    243 
    244     def flush(self):

/Library/Python/2.7/site-packages/IPython/zmq/displayhook.pyc in finish_displayhook(self)
     59         sys.stdout.flush()
     60         sys.stderr.flush()
---> 61         self.session.send(self.pub_socket, self.msg, ident=self.topic)
     62         self.msg = None
     63 

/Library/Python/2.7/site-packages/IPython/zmq/session.pyc in send(self, stream, msg_or_type, content, parent, ident, buffers, subheader, track, header)
    557 
    558         buffers = [] if buffers is None else buffers
--> 559         to_send = self.serialize(msg, ident)
    560         flag = 0
    561         if buffers:

/Library/Python/2.7/site-packages/IPython/zmq/session.pyc in serialize(self, msg, ident)
    461             content = self.none
    462         elif isinstance(content, dict):
--> 463             content = self.pack(content)
    464         elif isinstance(content, bytes):
    465             # content is already packed, as in a relayed message

/Library/Python/2.7/site-packages/IPython/zmq/session.pyc in <lambda>(obj)
     76 
     77 # ISO8601-ify datetime objects
---> 78 json_packer = lambda obj: jsonapi.dumps(obj, default=date_default)
     79 json_unpacker = lambda s: extract_dates(jsonapi.loads(s))
     80 

/Library/Python/2.7/site-packages/zmq/utils/jsonapi.pyc in dumps(o, **kwargs)
     70         kwargs['separators'] = (',', ':')
     71 
---> 72     return _squash_unicode(jsonmod.dumps(o, **kwargs))
     73 
     74 def loads(s, **kwargs):

/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/__init__.pyc in dumps(obj, skipkeys, ensure_ascii, check_circular, allow_nan, cls, indent, separators, encoding, default, **kw)
    236         check_circular=check_circular, allow_nan=allow_nan, indent=indent,
    237         separators=separators, encoding=encoding, default=default,
--> 238         **kw).encode(obj)
    239 
    240 

/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/encoder.pyc in encode(self, o)
    199         # exceptions aren't as detailed.  The list call should be roughly
    200         # equivalent to the PySequence_Fast that ''.join() would do.
--> 201         chunks = self.iterencode(o, _one_shot=True)
    202         if not isinstance(chunks, (list, tuple)):
    203             chunks = list(chunks)

/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/encoder.pyc in iterencode(self, o, _one_shot)
    262                 self.key_separator, self.item_separator, self.sort_keys,
    263                 self.skipkeys, _one_shot)
--> 264         return _iterencode(o, 0)
    265 
    266 def _make_iterencode(markers, _default, _encoder, _indent, _floatstr,

UnicodeDecodeError: 'utf8' codec can't decode byte 0xf7 in position 10: invalid start byte

Am I missing something?

JamesPHoughton commented Jul 31, 2013

I've managed to create a .gif animation using matplotlib and imagemagick, but haven't managed to then import the created file into the notebook:

anim = animation.FuncAnimation(fig, animate, frames=100, init_func=init)
anim.save('animation.gif', writer='imagemagick', fps=10);

from IPython.display import Image
Image(filename='animation.gif')

gives an error:

---------------------------------------------------------------------------
UnicodeDecodeError                        Traceback (most recent call last)
<ipython-input-81-8c01b6fb0d34> in <module>()
      1 from IPython.display import Image
----> 2 Image(filename='animation.gif')

/Library/Python/2.7/site-packages/IPython/core/displayhook.pyc in __call__(self, result)
    240             self.update_user_ns(result)
    241             self.log_output(format_dict)
--> 242             self.finish_displayhook()
    243 
    244     def flush(self):

/Library/Python/2.7/site-packages/IPython/zmq/displayhook.pyc in finish_displayhook(self)
     59         sys.stdout.flush()
     60         sys.stderr.flush()
---> 61         self.session.send(self.pub_socket, self.msg, ident=self.topic)
     62         self.msg = None
     63 

/Library/Python/2.7/site-packages/IPython/zmq/session.pyc in send(self, stream, msg_or_type, content, parent, ident, buffers, subheader, track, header)
    557 
    558         buffers = [] if buffers is None else buffers
--> 559         to_send = self.serialize(msg, ident)
    560         flag = 0
    561         if buffers:

/Library/Python/2.7/site-packages/IPython/zmq/session.pyc in serialize(self, msg, ident)
    461             content = self.none
    462         elif isinstance(content, dict):
--> 463             content = self.pack(content)
    464         elif isinstance(content, bytes):
    465             # content is already packed, as in a relayed message

/Library/Python/2.7/site-packages/IPython/zmq/session.pyc in <lambda>(obj)
     76 
     77 # ISO8601-ify datetime objects
---> 78 json_packer = lambda obj: jsonapi.dumps(obj, default=date_default)
     79 json_unpacker = lambda s: extract_dates(jsonapi.loads(s))
     80 

/Library/Python/2.7/site-packages/zmq/utils/jsonapi.pyc in dumps(o, **kwargs)
     70         kwargs['separators'] = (',', ':')
     71 
---> 72     return _squash_unicode(jsonmod.dumps(o, **kwargs))
     73 
     74 def loads(s, **kwargs):

/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/__init__.pyc in dumps(obj, skipkeys, ensure_ascii, check_circular, allow_nan, cls, indent, separators, encoding, default, **kw)
    236         check_circular=check_circular, allow_nan=allow_nan, indent=indent,
    237         separators=separators, encoding=encoding, default=default,
--> 238         **kw).encode(obj)
    239 
    240 

/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/encoder.pyc in encode(self, o)
    199         # exceptions aren't as detailed.  The list call should be roughly
    200         # equivalent to the PySequence_Fast that ''.join() would do.
--> 201         chunks = self.iterencode(o, _one_shot=True)
    202         if not isinstance(chunks, (list, tuple)):
    203             chunks = list(chunks)

/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/encoder.pyc in iterencode(self, o, _one_shot)
    262                 self.key_separator, self.item_separator, self.sort_keys,
    263                 self.skipkeys, _one_shot)
--> 264         return _iterencode(o, 0)
    265 
    266 def _make_iterencode(markers, _default, _encoder, _indent, _floatstr,

UnicodeDecodeError: 'utf8' codec can't decode byte 0xf7 in position 10: invalid start byte

Am I missing something?

@jakevdp

This comment has been minimized.

Show comment
Hide comment
@jakevdp

jakevdp Aug 1, 2013

Contributor

The IPython Image tag doesn't support gifs.

Contributor

jakevdp commented Aug 1, 2013

The IPython Image tag doesn't support gifs.

@gepcel

This comment has been minimized.

Show comment
Hide comment
@gepcel

gepcel Oct 14, 2013

Contributor

I couldn't make it work in Windows 7 (x64 with matplotlib 1.3.1 & ImageMagick installed)

After running the sample script from https://gist.github.com/3843162, I got the following error message. Do anybody know why?


RuntimeError                              Traceback (most recent call last)
<ipython-input-2-7b2f7b9edcb4> in <module>()
     26 # this is how you save your animation to file:
     27 #anim.save('animation.gif', writer='imagemagick_file', fps=30)
---> 28 anim.save('animation.gif', writer='imagemagick', fps=30)
     29 
     30 plt.show()

D:\Python27\lib\site-packages\matplotlib\animation.pyc in save(self, filename, writer, fps, dpi, codec, bitrate, extra_args, metadata, extra_anim, savefig_kwargs)
    716                     #TODO: Need to see if turning off blit is really necessary
    717                     anim._draw_next_frame(d, blit=False)
--> 718                 writer.grab_frame(**savefig_kwargs)
    719 
    720         # Reconnect signal for first draw if necessary

D:\Python27\lib\site-packages\matplotlib\animation.pyc in grab_frame(self, **savefig_kwargs)
    202             # frame format and dpi.
    203             self.fig.savefig(self._frame_sink(), format=self.frame_format,
--> 204                              dpi=self.dpi, **savefig_kwargs)
    205         except RuntimeError:
    206             out, err = self._proc.communicate()

D:\Python27\lib\site-packages\matplotlib\figure.pyc in savefig(self, *args, **kwargs)
   1419             self.set_frameon(frameon)
   1420 
-> 1421         self.canvas.print_figure(*args, **kwargs)
   1422 
   1423         if frameon:

D:\Python27\lib\site-packages\matplotlib\backend_bases.pyc in print_figure(self, filename, dpi, facecolor, edgecolor, orientation, format, **kwargs)
   2218                 orientation=orientation,
   2219                 bbox_inches_restore=_bbox_inches_restore,
-> 2220                 **kwargs)
   2221         finally:
   2222             if bbox_inches and restore_bbox:

D:\Python27\lib\site-packages\matplotlib\backends\backend_agg.pyc in print_raw(self, filename_or_obj, *args, **kwargs)
    495             close = False
    496         try:
--> 497             renderer._renderer.write_rgba(filename_or_obj)
    498         finally:
    499             if close:

RuntimeError: Error writing to file
Contributor

gepcel commented Oct 14, 2013

I couldn't make it work in Windows 7 (x64 with matplotlib 1.3.1 & ImageMagick installed)

After running the sample script from https://gist.github.com/3843162, I got the following error message. Do anybody know why?


RuntimeError                              Traceback (most recent call last)
<ipython-input-2-7b2f7b9edcb4> in <module>()
     26 # this is how you save your animation to file:
     27 #anim.save('animation.gif', writer='imagemagick_file', fps=30)
---> 28 anim.save('animation.gif', writer='imagemagick', fps=30)
     29 
     30 plt.show()

D:\Python27\lib\site-packages\matplotlib\animation.pyc in save(self, filename, writer, fps, dpi, codec, bitrate, extra_args, metadata, extra_anim, savefig_kwargs)
    716                     #TODO: Need to see if turning off blit is really necessary
    717                     anim._draw_next_frame(d, blit=False)
--> 718                 writer.grab_frame(**savefig_kwargs)
    719 
    720         # Reconnect signal for first draw if necessary

D:\Python27\lib\site-packages\matplotlib\animation.pyc in grab_frame(self, **savefig_kwargs)
    202             # frame format and dpi.
    203             self.fig.savefig(self._frame_sink(), format=self.frame_format,
--> 204                              dpi=self.dpi, **savefig_kwargs)
    205         except RuntimeError:
    206             out, err = self._proc.communicate()

D:\Python27\lib\site-packages\matplotlib\figure.pyc in savefig(self, *args, **kwargs)
   1419             self.set_frameon(frameon)
   1420 
-> 1421         self.canvas.print_figure(*args, **kwargs)
   1422 
   1423         if frameon:

D:\Python27\lib\site-packages\matplotlib\backend_bases.pyc in print_figure(self, filename, dpi, facecolor, edgecolor, orientation, format, **kwargs)
   2218                 orientation=orientation,
   2219                 bbox_inches_restore=_bbox_inches_restore,
-> 2220                 **kwargs)
   2221         finally:
   2222             if bbox_inches and restore_bbox:

D:\Python27\lib\site-packages\matplotlib\backends\backend_agg.pyc in print_raw(self, filename_or_obj, *args, **kwargs)
    495             close = False
    496         try:
--> 497             renderer._renderer.write_rgba(filename_or_obj)
    498         finally:
    499             if close:

RuntimeError: Error writing to file
@WeatherGod

This comment has been minimized.

Show comment
Hide comment
@WeatherGod

WeatherGod Oct 14, 2013

Member

To update this issue from discussion on the mailing list, it looks like
"convert.exe" on Windows is a system binary.

Member

WeatherGod commented Oct 14, 2013

To update this issue from discussion on the mailing list, it looks like
"convert.exe" on Windows is a system binary.

@jakevdp

This comment has been minimized.

Show comment
Hide comment
@jakevdp

jakevdp Oct 14, 2013

Contributor

Might be an issue with piping. Can you try using writer="imagemagick_file" and see if that works?

Contributor

jakevdp commented Oct 14, 2013

Might be an issue with piping. Can you try using writer="imagemagick_file" and see if that works?

@gepcel

This comment has been minimized.

Show comment
Hide comment
@gepcel

gepcel Oct 15, 2013

Contributor

The writer='imagemagick_file' didn't work.
I'm pretty sure it's because of the convert.exe file in the Windows\System32' directory.
Following a suggest of Christoph Gohlke from the matplotlib-user mailing list, adding
animation.convert_path: D:\Program Files\ImageMagick-6.8.7-Q16\convert.exe
to the matplotlibrc worked.

Contributor

gepcel commented Oct 15, 2013

The writer='imagemagick_file' didn't work.
I'm pretty sure it's because of the convert.exe file in the Windows\System32' directory.
Following a suggest of Christoph Gohlke from the matplotlib-user mailing list, adding
animation.convert_path: D:\Program Files\ImageMagick-6.8.7-Q16\convert.exe
to the matplotlibrc worked.

@jakevdp

This comment has been minimized.

Show comment
Hide comment
@jakevdp

jakevdp Oct 15, 2013

Contributor

OK, great

Contributor

jakevdp commented Oct 15, 2013

OK, great

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment