当前位置: 首页 > 工具软件 > JsHtml > 使用案例 >

matplotlib:将动画输出为动画文件、HTML(标签文本、文件、嵌入jupyter notebook)

吴建中
2023-12-01

当前有效matplotlib版本为:3.4.1

概述

FuncAnimationmatplotlib生成逐帧动画的常用类,它的工作原理就是按照一定时间间隔不断调用参数func对应的函数生成动画。

FuncAnimation的继承关系如下:
matplotlib.animation.Animation→matplotlib.animation.TimedAnimation→matplotlib.animation.FuncAnimation

基类matplotlib.animation.Animation定义了三种动画的输出方法。

  • save():通过绘制每帧数据将动画保存为文件。
  • to_html5_video() :将动画转换为HTML5 标签。
  • to_jshtml():以HTML形式生成动画,HTML页面中自带动画控制组件。

save()方法

功能为通过绘制每帧数据将动画保存为文件。

save()方法签名为save(self, filename, writer=None, fps=None, dpi=None, codec=None, bitrate=None, extra_args=None, metadata=None, extra_anim=None, savefig_kwargs=None, *, progress_callback=None)
save()方法的参数为:

  • embed_limit:嵌入动画的大小限制,单位为MB。默认值为rcParams['animation.embed_limit']20

  • filename : 输出文件的名称。字符串。

  • writer : 用于输出文件的写入器类,MovieWriter实例或可以标识类的字符串。MovieWriter或字符串,默认值为rcParams['animation.writer'],即ffmpeg

  • fps : 动画的帧率(每秒)。整数。可选参数。默认值为None,如果没有设置该参数,动画帧率根据动画的帧间隔时间推断。

  • dpi : 控制动画每帧的分辨率(每英寸的点数)。配合图形的尺寸控制动画的大小。浮点数。默认值为rcParams['savefig.dpi']'figure'

  • codec : 动画使用的视频解码器字符串。默认值为rcParams['animation.codec']'h264'

  • bitrate : 动画的比特率,单位为KB/s。整数,值越大说明动画图像质量越高。-1代表让编码器选择比特率。默认值为rcParams['animation.bitrate']-1
    fpscodecbitrate用于构造MovieWriter实例,仅当writer参数值为字符串时有效。

源码为:

def save(self, filename, writer=None, fps=None, dpi=None, codec=None,
         bitrate=None, extra_args=None, metadata=None, extra_anim=None,
         savefig_kwargs=None, *, progress_callback=None):
    

    if writer is None:
        writer = mpl.rcParams['animation.writer']
    elif (not isinstance(writer, str) and
          any(arg is not None
              for arg in (fps, codec, bitrate, extra_args, metadata))):
        raise RuntimeError('Passing in values for arguments '
                           'fps, codec, bitrate, extra_args, or metadata '
                           'is not supported when writer is an existing '
                           'MovieWriter instance. These should instead be '
                           'passed as arguments when creating the '
                           'MovieWriter instance.')

    if savefig_kwargs is None:
        savefig_kwargs = {}

    if fps is None and hasattr(self, '_interval'):
        # Convert interval in ms to frames per second
        fps = 1000. / self._interval

    # Re-use the savefig DPI for ours if none is given
    if dpi is None:
        dpi = mpl.rcParams['savefig.dpi']
    if dpi == 'figure':
        dpi = self._fig.dpi

    writer_kwargs = {}
    if codec is not None:
        writer_kwargs['codec'] = codec
    if bitrate is not None:
        writer_kwargs['bitrate'] = bitrate
    if extra_args is not None:
        writer_kwargs['extra_args'] = extra_args
    if metadata is not None:
        writer_kwargs['metadata'] = metadata

    all_anim = [self]
    if extra_anim is not None:
        all_anim.extend(anim
                        for anim
                        in extra_anim if anim._fig is self._fig)

    # If we have the name of a writer, instantiate an instance of the
    # registered class.
    if isinstance(writer, str):
        try:
            writer_cls = writers[writer]
        except RuntimeError:  # Raised if not available.
            writer_cls = PillowWriter  # Always available.
            _log.warning("MovieWriter %s unavailable; using Pillow "
                         "instead.", writer)
        writer = writer_cls(fps, **writer_kwargs)
    _log.info('Animation.save using %s', type(writer))

    if 'bbox_inches' in savefig_kwargs:
        _log.warning("Warning: discarding the 'bbox_inches' argument in "
                     "'savefig_kwargs' as it may cause frame size "
                     "to vary, which is inappropriate for animation.")
        savefig_kwargs.pop('bbox_inches')

    # Create a new sequence of frames for saved data. This is different
    # from new_frame_seq() to give the ability to save 'live' generated
    # frame information to be saved later.
    # TODO: Right now, after closing the figure, saving a movie won't work
    # since GUI widgets are gone. Either need to remove extra code to
    # allow for this non-existent use case or find a way to make it work.
    if mpl.rcParams['savefig.bbox'] == 'tight':
        _log.info("Disabling savefig.bbox = 'tight', as it may cause "
                  "frame size to vary, which is inappropriate for "
                  "animation.")
    # canvas._is_saving = True makes the draw_event animation-starting
    # callback a no-op; canvas.manager = None prevents resizing the GUI
    # widget (both are likewise done in savefig()).
    with mpl.rc_context({'savefig.bbox': None}), \
         writer.saving(self._fig, filename, dpi), \
         cbook._setattr_cm(self._fig.canvas,
                           _is_saving=True, manager=None):
        for anim in all_anim:
            anim._init_draw()  # Clear the initial frame
        frame_number = 0
        # TODO: Currently only FuncAnimation has a save_count
        #       attribute. Can we generalize this to all Animations?
        save_count_list = [getattr(a, 'save_count', None)
                           for a in all_anim]
        if None in save_count_list:
            total_frames = None
        else:
            total_frames = sum(save_count_list)
        for data in zip(*[a.new_saved_frame_seq() for a in all_anim]):
            for anim, d in zip(all_anim, data):
                # TODO: See if turning off blit is really necessary
                anim._draw_next_frame(d, blit=False)
                if progress_callback is not None:
                    progress_callback(frame_number, total_frames)
                    frame_number += 1
            writer.grab_frame(**savefig_kwargs)

to_html5_video()方法

功能为将动画转换为HTML5 标签,默认生成base64编码动画。

to_html5_video()方法签名为to_html5_video(self, embed_limit=None)
to_html5_video()方法的参数为:

  • embed_limit:嵌入动画的大小限制,单位为MB。默认值为rcParams['animation.embed_limit']20

返回值为嵌入base64编码动画的元素字符串。

源码为:

    def to_html5_video(self, embed_limit=None):
        """
        Convert the animation to an HTML5 ``<video>`` tag.

        This saves the animation as an h264 video, encoded in base64
        directly into the HTML5 video tag. This respects :rc:`animation.writer`
        and :rc:`animation.bitrate`. This also makes use of the
        ``interval`` to control the speed, and uses the ``repeat``
        parameter to decide whether to loop.

        Parameters
        ----------
        embed_limit : float, optional
            Limit, in MB, of the returned animation. No animation is created
            if the limit is exceeded.
            Defaults to :rc:`animation.embed_limit` = 20.0.

        Returns
        -------
        str
            An HTML5 video tag with the animation embedded as base64 encoded
            h264 video.
            If the *embed_limit* is exceeded, this returns the string
            "Video too large to embed."
        """
        VIDEO_TAG = r'''<video {size} {options}>
  <source type="video/mp4" src="data:video/mp4;base64,{video}">
  Your browser does not support the video tag.
</video>'''
        # Cache the rendering of the video as HTML
        if not hasattr(self, '_base64_video'):
            # Save embed limit, which is given in MB
            if embed_limit is None:
                embed_limit = mpl.rcParams['animation.embed_limit']

            # Convert from MB to bytes
            embed_limit *= 1024 * 1024

            # Can't open a NamedTemporaryFile twice on Windows, so use a
            # TemporaryDirectory instead.
            with TemporaryDirectory() as tmpdir:
                path = Path(tmpdir, "temp.m4v")
                # We create a writer manually so that we can get the
                # appropriate size for the tag
                Writer = writers[mpl.rcParams['animation.writer']]
                writer = Writer(codec='h264',
                                bitrate=mpl.rcParams['animation.bitrate'],
                                fps=1000. / self._interval)
                self.save(str(path), writer=writer)
                # Now open and base64 encode.
                vid64 = base64.encodebytes(path.read_bytes())

            vid_len = len(vid64)
            if vid_len >= embed_limit:
                _log.warning(
                    "Animation movie is %s bytes, exceeding the limit of %s. "
                    "If you're sure you want a large animation embedded, set "
                    "the animation.embed_limit rc parameter to a larger value "
                    "(in MB).", vid_len, embed_limit)
            else:
                self._base64_video = vid64.decode('ascii')
                self._video_size = 'width="{}" height="{}"'.format(
                        *writer.frame_size)

        # If we exceeded the size, this attribute won't exist
        if hasattr(self, '_base64_video'):
            # Default HTML5 options are to autoplay and display video controls
            options = ['controls', 'autoplay']

            # If we're set to repeat, make it loop
            if hasattr(self, 'repeat') and self.repeat:
                options.append('loop')

            return VIDEO_TAG.format(video=self._base64_video,
                                    size=self._video_size,
                                    options=' '.join(options))
        else:
            return 'Video too large to embed.'

to_jshtml()方法

功能为以HTML形式生成动画,HTML页面中自带动画控制组件。
底层调用HTMLWriter类实现。
to_jshtml()方法签名为to_jshtml(self, fps=None, embed_frames=True, default_mode=None)
to_jshtml()方法的参数为:

  • fps:每秒钟传输的帧数。默认值为None,即根据interval参数确定值。
  • default_mode:动画是否为循环模式。取值为{'loop','once'},默认值为None,即根据repeat参数确定值。

返回值为嵌入动画的HTML字符串。
源码如下:

def to_jshtml(self, fps=None, embed_frames=True, default_mode=None):
    """Generate HTML representation of the animation"""
    if fps is None and hasattr(self, '_interval'):
        # Convert interval in ms to frames per second
        fps = 1000 / self._interval

    # If we're not given a default mode, choose one base on the value of
    # the repeat attribute
    if default_mode is None:
        default_mode = 'loop' if self.repeat else 'once'

    if not hasattr(self, "_html_representation"):
        # Can't open a NamedTemporaryFile twice on Windows, so use a
        # TemporaryDirectory instead.
        with TemporaryDirectory() as tmpdir:
            path = Path(tmpdir, "temp.html")
            writer = HTMLWriter(fps=fps,
                                embed_frames=embed_frames,
                                default_mode=default_mode)
            self.save(str(path), writer=writer)
            self._html_representation = path.read_text()

    return self._html_representation

案例:将动画输出为各种形式

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation

fig, ax = plt.subplots()
x = np.arange(0, 2*np.pi, 0.01)
line, = ax.plot(x, np.sin(x))
def animate(i):
    line.set_ydata(np.sin(x + i / 50))
    return line,
ani = animation.FuncAnimation(
    fig, animate, interval=20, blit=True, save_count=50)
# 形式1:利用pillow生成gif文件
ani.save("test.gif",writer='pillow')
# 形式2:输出HTML5 video元素
print(ani.to_html5_video())
# 形式3:将HTML5 video元素保存为html文件
with open("myvideo.html", "w") as f:
    print(ani.to_html5_video(), file=f)
# 形式4:输出html
print(ani.to_jshtml())
# 形式5:以HTML形式在jupyter notebook中显示动画
from IPython.display import HTML
HTML(ani.to_jshtml())
# 形式6:将html保存为文件
with open("myhtml.html", "w") as f:
    print(ani.to_jshtml(), file=f)
 类似资料: