有没有办法将龟的绘图保存为动画GIF?

时间:2016-12-25 09:23:27

标签: python gif turtle-graphics

我喜欢Python模块在Python中的作用,我想输出绘制形状的整个动画。有没有办法做到这一点? GIF / MP4 /显示动画的任何内容。请注意,我知道外部屏幕录像机可以完成这项工作,但我正在寻找龟模块自行完成此任务的方法。

2 个答案:

答案 0 :(得分:4)

使用OSX上的预览从Python龟制作动画GIF

1)从工作程序开始

看起来很明显,在尝试生成动画GIF时不要调试代码。它应该是一个合适的海龟程序,没有以mainloop()done()exitonclick()结尾的无限循环。

我将用于此解释的程序是我使用turtle为Programming Puzzles & Golf Code that draws an Icelandic flag编写的程序。这是PP& GC的故意极简主义:

from turtle import *
import tkinter as _
_.ROUND = _.BUTT
S = 8
h = 18 * S
color("navy")
width(h)
fd(25 * S)
color("white")
width(4 * S)
home()
pu()
goto(9 * S, -9 * S)
lt(90)
pd()
fd(h)
color("#d72828")
width(S + S)
bk(h)
pu()
home()
pd()
fd(25 * S)
ht()
done()

2)让您的程序定时保存快照

使用draw()save()stop()时间事件重新打包您的计划大致如下:

from turtle import *
import tkinter as _
_.ROUND=_.BUTT

def draw():
    S = 8
    h = 18 * S
    color("navy")
    width(h)
    fd(25 * S)
    color("white")
    width(4 * S)
    home()
    pu()
    goto(9 * S, -9 * S)
    lt(90)
    pd()
    fd(h)
    color("#d72828")
    width(S + S)
    bk(h)
    pu()
    home()
    pd()
    fd(25 * S)
    ht()

    ontimer(stop, 500)  # stop the recording (1/2 second trailer)

running = True
FRAMES_PER_SECOND = 10

def stop():
    global running

    running = False

def save(counter=[1]):
    getcanvas().postscript(file = "iceland{0:03d}.eps".format(counter[0]))
    counter[0] += 1
    if running:
        ontimer(save, int(1000 / FRAMES_PER_SECOND))

save()  # start the recording

ontimer(draw, 500)  # start the program (1/2 second leader)

done()

我使用的是每秒10帧(FPS),因为它与预览在后续步骤中使用的内容相匹配。

3)运行你的程序;完成后退出。

创建一个新的空目录并从那里运行它。如果一切按计划进行,它应该将一系列* .eps文件转储到目录中。

4)将所有这些* .eps文件加载到预览

假设Preview是我的默认预览器,在Terminal.app中我只会这样做:

open iceland*.eps

5) Select-All 预览侧栏中的PDF(EPS)文件和文件/导出... (不是导出为PDF )作为GIF

选项按钮下设置导出类型,将它们保存到我们的临时目录中。在选择格式以查看GIF选项时,您需要按住 Option 键。选择一个好的屏幕分辨率。我们现在应该在临时目录中有* .gif文件。 退出预览。

6)将所有* .gif文件加载到预览

open iceland*.gif

7)将除第一个GIF文件之外的所有文件合并到第一个GIF文件

在预览的侧边栏中选择所有 GIF文件。取消选择(命令单击)第一个GIF文件,例如iceland001.gif。将选定的GIF文件拖到未选择的GIF文件上。这将修改它和它的名字。使用文件/导出... 将修改后的第一个GIF文件导出到新的GIF文件,例如iceland.gif

8)这是一个动画GIF!

通过将自己加载到Safari中来说服自己,例如:

open -a Safari iceland.gif

9)转换为重复的动画GIF

对于重复的动画GIF,您需要一些外部工具,如 ImageMagick Gifsicle 来设置循环值:

convert -loop 0 iceland.gif iceland-repeating.gif

再次让自己相信它有效:

open -a Safari iceland-repeating.gif

10)动画GIF结果。祝你好运!

enter image description here

答案 1 :(得分:0)

主要概念

这是我的解决方案,步骤如下,

  1. 获取框架(您需要使用turtle.ontimer-> turtle.getcanvas().postscript(file=output_file)

  2. 将每个EPS转换为PNG。 (由于turtle.getcanvas().postscript返回EPS,因此您需要使用PIL将EPS转换为PNG)

    您需要下载ghostscript: https://www.ghostscript.com/download/gsdnld.html

  3. 使用您的PNG列表制作GIF。 (使用PIL.ImageFile.ImageFile.save(output_path, format='gif', save_all=True, append_images=, duration, loop)

脚本

这是我的脚本(如果有时间的话,也许我会发布到PyPI ...)

import turtle
import tkinter

from typing import Callable, List

from pathlib import Path
import re
import os
import sys
import functools

import PIL.Image
from PIL.PngImagePlugin import PngImageFile
from PIL.ImageFile import ImageFile
from PIL import EpsImagePlugin


def init(**options):
    # download ghostscript: https://www.ghostscript.com/download/gsdnld.html
    if options.get('gs_windows_binary'):
        EpsImagePlugin.gs_windows_binary = options['gs_windows_binary']  # install ghostscript, otherwise->{OSError} Unable to locate Ghostscript on paths

    # https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/cap-join-styles.html
    # change the default style of the line that made of two connected line segments
    tkinter.ROUND = tkinter.BUTT  # default is ROUND  # https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/create_line.html


def make_gif(image_list: List[Path], output_path: Path, **options):
    """
    :param image_list:
    :param output_path:
    :param options:
        - fps: Frame Per Second. Duration and FPS, choose one to give.
        - duration milliseconds (= 1000/FPS )  (default is 0.1 sec)
        - loop  # int, if 0, then loop forever. Otherwise, it means the loop number.
    :return:
    """
    if not output_path.parent.exists():
        raise FileNotFoundError(output_path.parent)

    if not output_path.name.lower().endswith('.gif'):
        output_path = output_path / Path('.gif')
    image_list: List[ImageFile] = [PIL.Image.open(str(_)) for _ in image_list]
    im = image_list.pop(0)
    fps = options.get('fps', options.get('FPS', 10))
    im.save(output_path, format='gif', save_all=True, append_images=image_list,
            duration=options.get('duration', int(1000 / fps)),
            loop=options.get('loop', 0))


class GIFCreator:
    __slots__ = ['draw',
                 '__temp_dir', '__duration',
                 '__name', '__is_running', '__counter', ]

    TEMP_DIR = Path('.') / Path('__temp__for_gif')

    # The time gap that you pick image after another on the recording. i.e., If the value is low, then you can get more source image, so your GIF has higher quality.
    DURATION = 100  # millisecond.  # 1000 / FPS

    REBUILD = True

    def __init__(self, name, temp_dir: Path = None, duration: int = None, **options):
        self.__name = name
        self.__is_running = False
        self.__counter = 1

        self.__temp_dir = temp_dir if temp_dir else self.TEMP_DIR
        self.__duration = duration if duration else self.DURATION

        if not self.__temp_dir.exists():
            self.__temp_dir.mkdir(parents=True)  # True, it's ok when parents is not exists

    @property
    def name(self):
        return self.__name

    @property
    def duration(self):
        return self.__duration

    @property
    def temp_dir(self):
        if not self.__temp_dir.exists():
            raise FileNotFoundError(self.__temp_dir)
        return self.__temp_dir

    def configure(self, **options):
        gif_class_members = (_ for _ in dir(GIFCreator) if not _.startswith('_') and not callable(getattr(GIFCreator, _)))

        for name, value in options.items():
            name = name.upper()
            if name not in gif_class_members:
                raise KeyError(f"'{name}' does not belong to {GIFCreator} members.")
            correct_type = type(getattr(self, name))

            # type check
            assert isinstance(value, correct_type), TypeError(f'{name} type need {correct_type.__name__} not {type(value).__name__}')

            setattr(self, '_GIFCreator__' + name.lower(), value)

    def record(self, draw_func: Callable = None, **options):
        """

        :param draw_func:
        :param options:
                - fps
                - start_after: milliseconds. While waiting, white pictures will continuously generate to used as the heading image of GIF.
                - end_after:
        :return:
        """
        if draw_func and callable(draw_func):
            setattr(self, 'draw', draw_func)
        if not (hasattr(self, 'draw') and callable(getattr(self, 'draw'))):
            raise NotImplementedError('subclasses of GIFCreatorMixin must provide a draw() method')

        regex = re.compile(fr"""{self.name}_[0-9]{{4}}""")

        def wrap():
            self.draw()
            turtle.ontimer(self._stop, options.get('end_after', 0))

        wrap_draw = functools.wraps(self.draw)(wrap)

        try:
            # https://blog.csdn.net/lingyu_me/article/details/105400510
            turtle.reset()  # Does a turtle.clear() and then resets this turtle's state (i.e. direction, position etc.)
        except turtle.Terminator:
            turtle.reset()

        if self.REBUILD:
            for f in [_ for _ in self.temp_dir.glob(f'*.*') if _.suffix.upper().endswith(('EPS', 'PNG'))]:
                [os.remove(f) for ls in regex.findall(str(f)) if ls is not None]

        self._start()
        self._save()  # init start the recording
        turtle.ontimer(wrap_draw,
                       t=options.get('start_after', 0))  # start immediately
        turtle.done()
        print('convert_eps2image...')
        self.convert_eps2image()
        print('make_gif...')
        self.make_gif(fps=options.get('fps'))
        print(f'done:{self.name}')
        return

    def convert_eps2image(self):
        """
        image extension (PGM, PPM, GIF, PNG) is all compatible with tk.PhotoImage
        .. important:: you need to use ghostscript, see ``init()``
        """
        for eps_file in [_ for _ in self.temp_dir.glob('*.*') if _.name.startswith(self.__name) and _.suffix.upper() == '.EPS']:
            output_path = self.temp_dir / Path(eps_file.name + '.png')
            if output_path.exists():
                continue
            im: PIL.Image.Image = PIL.Image.open(str(eps_file))
            im.save(output_path, 'png')

    def make_gif(self, output_name=None, **options):
        """
        :param output_name: basename `xxx.png` or `xxx`
        :param options:
            - fps: for GIF
        :return:
        """

        if output_name is None:
            output_name = self.__name

        if not output_name.lower().endswith('.gif'):
            output_name += '.gif'

        image_list = [_ for _ in self.temp_dir.glob(f'{self.__name}*.*') if
                      (_.suffix.upper().endswith(('PGM', 'PPM', 'GIF', 'PNG')) and _.name.startswith(self.__name))
                      ]
        if not image_list:
            sys.stderr.write(f'There is no image on the directory. {self.temp_dir / Path(self.__name + "*.*")}')
            return
        output_path = Path('.') / Path(f'{output_name}')

        fps = options.get('fps', options.get('FPS'))
        if fps is None:
            fps = 1000 / self.duration
        make_gif(image_list, output_path,
                 fps=fps, loop=0)
        os.startfile('.')  # open the output folder

    def _start(self):
        self.__is_running = True

    def _stop(self):
        print(f'finished draw:{self.name}')
        self.__is_running = False
        self.__counter = 1

    def _save(self):
        if self.__is_running:
            # print(self.__counter)
            output_file: Path = self.temp_dir / Path(f'{self.__name}_{self.__counter:04d}.eps')
            if not output_file.exists():
                turtle.getcanvas().postscript(file=output_file)  # 0001.eps, 0002.eps ...
            self.__counter += 1
            turtle.ontimer(self._save, t=self.duration)  # trigger only once, so we need to set it again.

用法

init(gs_windows_binary=r'C:\Program Files\gs\gs9.52\bin\gswin64c')

def your_draw_function():
    turtle.color("red")
    turtle.width(20)
    turtle.fd(40)
    turtle.color("#00ffff")
    turtle.bk(40)
    ...


# method 1: pass the draw function directly.
gif_demo = GIFCreator(name='demo')
# gif_demo.configure(duration=400)  # Optional
gif_demo.record(your_draw_function)

# method 2: use class
# If you want to create a class, just define your draw function, and then record it.
class MyGIF(GIFCreator):
    DURATION = 200  # optional

    def draw(self):
        your_draw_function()


MyGIF(name='rectangle demo').record(
    # fps=, start_after=, end_after=  <-- optional
)

演示

init(gs_windows_binary=r'C:\Program Files\gs\gs9.52\bin\gswin64c')


class TaiwanFlag(GIFCreator):
    DURATION = 200
    # REBUILD = False

    def __init__(self, ratio, **kwargs):
        """
        ratio: 0.5 (40*60)  1 (80*120)  2 (160*240) ...
        """
        self.ratio = ratio
        GIFCreator.__init__(self, **kwargs)

    def show_size(self):
        print(f'width:{self.ratio * 120}\nheight:{self.ratio * 80}')

    @property
    def size(self):  # w, h
        return self.ratio * 120, self.ratio * 80

    def draw(self):
        # from turtle import *
        # turtle.tracer(False)
        s = self.ratio  # scale
        pu()
        s_w, s_h = turtle.window_width(), turtle.window_height()
        margin_x = (s_w - self.size[0]) / 2
        home_xy = -s_w / 2 + margin_x, 0
        goto(home_xy)
        pd()
        color("red")
        width(80 * s)
        fd(120 * s)
        pu()

        goto(home_xy)
        color('blue')
        goto(home_xy[0], 20 * s)
        width(40 * s)
        pd()
        fd(60 * s)

        pu()
        bk((30 + 15) * s)
        pd()
        color('white')
        width(1)
        left(15)
        begin_fill()
        for i in range(12):
            fd(30 * s)
            right(150)
        end_fill()

        rt(15)
        pu()
        fd(15 * s)
        rt(90)
        fd(8.5 * s)
        pd()
        lt(90)
        # turtle.tracer(True)
        begin_fill()
        circle(8.5 * s)
        end_fill()

        color('blue')
        width(2 * s)
        circle(8.5 * s)

        # turtle.tracer(True)
        turtle.hideturtle()


taiwan_flag = TaiwanFlag(2, name='taiwan')
turtle.Screen().setup(taiwan_flag.size[0] + 40, taiwan_flag.size[1] + 40)  # margin = 40
# taiwan_flag.draw()
taiwan_flag.record(end_after=2500, fps=10)

enter image description here