1
0
mirror of https://github.com/Zulko/moviepy.git synced 2021-07-27 01:17:47 +03:00

Add 'loop' argument support writing GIFs with ffmpeg (#1605)

* Add support for 'loop' argument writing GIFs with ffmpeg

* Add CHANGELOG entry
This commit is contained in:
Álvaro Mondéjar
2021-06-01 15:15:38 +02:00
committed by GitHub
parent 2aff1cfc15
commit ad1177e33b
9 changed files with 209 additions and 63 deletions

View File

@@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `center`, `translate` and `bg_color` arguments to `video.fx.rotate` [\#1474](https://github.com/Zulko/moviepy/pull/1474)
- `audio.fx.audio_delay` FX [\#1481](https://github.com/Zulko/moviepy/pull/1481)
- `start_time` and `end_time` optional arguments to `multiply_volume` FX which allow to specify a range applying the transformation [\#1572](https://github.com/Zulko/moviepy/pull/1572)
- `loop` argument support writing GIFs with ffmpeg for `write_gif` and `write_gif_with_tempfiles` [\#1605](https://github.com/Zulko/moviepy/pull/1605)
### Changed <!-- for changes in existing functionality -->
- Lots of method and parameter names have been changed. This will be explained better in the documentation soon. See https://github.com/Zulko/moviepy/pull/1170 for more information. [\#1170](https://github.com/Zulko/moviepy/pull/1170)

View File

@@ -220,7 +220,7 @@ class AudioClip(Clip):
when writing the file
logger
Either 'bar' or None or any Proglog logger
Either ``"bar"`` for progress bar or ``None`` or any Proglog logger.
"""
if not fps:

View File

@@ -307,7 +307,7 @@ class VideoClip(Clip):
output file in them.
logger
Either "bar" for progress bar or None or any Proglog logger.
Either ``"bar"`` for progress bar or ``None`` or any Proglog logger.
pixel_format
Pixel format for the output video file.
@@ -422,7 +422,7 @@ class VideoClip(Clip):
will save the clip's mask (if any) as an alpha canal (PNGs only).
logger
Either 'bar' (progress bar) or None or any Proglog logger.
Either ``"bar"`` for progress bar or ``None`` or any Proglog logger.
Returns

View File

@@ -5,10 +5,11 @@ from moviepy.decorators import requires_duration
def loop(clip, n=None, duration=None):
"""
Returns a clip that plays the current clip in an infinite loop.
Ideal for clips coming from gifs.
Ideal for clips coming from GIFs.
Parameters
----------
n
Number of times the clip should be played. If `None` the
the clip will loop indefinitely (i.e. with no set duration).

View File

@@ -7,6 +7,7 @@ import proglog
from moviepy.config import FFMPEG_BINARY, IMAGEMAGICK_BINARY
from moviepy.decorators import requires_duration, use_clip_fps_by_default
from moviepy.tools import cross_platform_popen_params, subprocess_call
from moviepy.video.fx.loop import loop as loop_fx
try:
@@ -29,8 +30,8 @@ def write_gif_with_tempfiles(
loop=0,
dispose=True,
colors=None,
logger="bar",
pixel_format=None,
logger="bar",
):
"""Write the VideoClip to a GIF file.
@@ -40,6 +41,53 @@ def write_gif_with_tempfiles(
docstring), but writes every frame to a file instead of passing
them in the RAM. Useful on computers with little RAM.
Parameters
----------
clip : moviepy.video.VideoClip.VideoClip
The clip from which the frames will be extracted to create the GIF image.
filename : str
Name of the resulting gif file.
fps : int, optional
Number of frames per second. If it isn't provided, then the function will
look for the clip's ``fps`` attribute.
program : str, optional
Software to use for the conversion, either ``"ImageMagick"`` or
``"ffmpeg"``.
opt : str, optional
ImageMagick only optimalization to apply, either ``"optimizeplus"`` or
``"OptimizeTransparency"``. Doesn't takes effect if ``program="ffmpeg"``.
fuzz : float, optional
ImageMagick only compression option which compresses the GIF by
considering that the colors that are less than ``fuzz`` different are in
fact the same.
loop : int, optional
Repeat the clip using ``loop`` iterations in the resulting GIF.
dispose : bool, optional
ImageMagick only option which, when enabled, the ImageMagick binary will
take the argument `-dispose 2`, clearing the frame area with the
background color, otherwise it will be defined as ``-dispose 1`` which
will not dispose, just overlays next frame image.
colors : int, optional
ImageMagick only option for color reduction. Defines the maximum number
of colors that the output image will have.
pixel_format : str, optional
FFmpeg pixel format for the output gif file. If is not specified
``"rgb24"`` will be used as the default format unless ``clip.mask``
exist, then ``"rgba"`` will be used. Doesn't takes effect if
``program="ImageMagick"``.
logger : str, optional
Either ``"bar"`` for progress bar or ``None`` or any Proglog logger.
"""
logger = proglog.default_bar_logger(logger)
file_root, ext = os.path.splitext(filename)
@@ -90,6 +138,9 @@ def write_gif_with_tempfiles(
)
elif program == "ffmpeg":
if loop:
clip = loop_fx(clip, n=loop)
if not pixel_format:
pixel_format = "rgba" if with_mask else "rgb24"
@@ -140,15 +191,15 @@ def write_gif(
clip,
filename,
fps=None,
with_mask=True,
program="ImageMagick",
opt="OptimizeTransparency",
fuzz=1,
with_mask=True,
loop=0,
dispose=True,
colors=None,
logger="bar",
pixel_format=None,
logger="bar",
):
"""Write the VideoClip to a GIF file, without temporary files.
@@ -159,44 +210,65 @@ def write_gif(
Parameters
----------
filename
clip : moviepy.video.VideoClip.VideoClip
The clip from which the frames will be extracted to create the GIF image.
filename : str
Name of the resulting gif file.
fps
Number of frames per second (see note below). If it
isn't provided, then the function will look for the clip's
``fps`` attribute (VideoFileClip, for instance, have one).
fps : int, optional
Number of frames per second. If it isn't provided, then the function will
look for the clip's ``fps`` attribute.
program
Software to use for the conversion, either 'ImageMagick' or
'ffmpeg'.
with_mask : bool, optional
Includes tha mask of the clip in the output (the clip must have a mask
if this argument is ``True``).
opt
(ImageMagick only) optimalization to apply, either
'optimizeplus' or 'OptimizeTransparency'.
program : str, optional
Software to use for the conversion, either ``"ImageMagick"`` or
``"ffmpeg"``.
fuzz
(ImageMagick only) Compresses the GIF by considering that
the colors that are less than fuzz% different are in fact
the same.
opt : str, optional
ImageMagick only optimalization to apply, either ``"optimizeplus"`` or
``"OptimizeTransparency"``. Doesn't takes effect if ``program="ffmpeg"``.
pixel_format
Pixel format for the output gif file. If is not specified
'rgb24' will be used as the default format unless ``clip.mask``
exist, then 'rgba' will be used. This option is going to
be ignored if ``program=ImageMagick``.
fuzz : float, optional
ImageMagick only compression option which compresses the GIF by
considering that the colors that are less than ``fuzz`` different are in
fact the same.
loop : int, optional
Repeat the clip using ``loop`` iterations in the resulting GIF.
dispose : bool, optional
ImageMagick only option which, when enabled, the ImageMagick binary will
take the argument `-dispose 2`, clearing the frame area with the
background color, otherwise it will be defined as ``-dispose 1`` which
will not dispose, just overlays next frame image.
colors : int, optional
ImageMagick only option for color reduction. Defines the maximum number
of colors that the output image will have.
pixel_format : str, optional
FFmpeg pixel format for the output gif file. If is not specified
``"rgb24"`` will be used as the default format unless ``clip.mask``
exist, then ``"rgba"`` will be used. Doesn't takes effect if
``program="ImageMagick"``.
logger : str, optional
Either ``"bar"`` for progress bar or ``None`` or any Proglog logger.
Notes
-----
Examples
--------
The gif will be playing the clip in real time (you can
only change the frame rate). If you want the gif to be played
slower than the clip you will use ::
>>> # slow down clip 50% and make it a gif
>>> myClip.multiply_speed(0.5).write_gif('myClip.gif')
The gif will be playing the clip in real time, you can only change the
frame rate. If you want the gif to be played slower than the clip you will
use:
>>> # slow down clip 50% and make it a GIF
>>> myClip.multiply_speed(0.5).write_gif('myClip.gif')
"""
#
# We use processes chained with pipes.
@@ -243,6 +315,9 @@ def write_gif(
)
if program == "ffmpeg":
if loop:
clip = loop_fx(clip, n=loop)
popen_params["stdin"] = sp.PIPE
popen_params["stdout"] = sp.DEVNULL

View File

@@ -10,9 +10,10 @@ from moviepy.audio.io.AudioFileClip import AudioFileClip
from moviepy.tools import convert_to_seconds
from moviepy.utils import close_all_clips
from moviepy.video.compositing.CompositeVideoClip import CompositeVideoClip
from moviepy.video.fx.mask_color import mask_color
from moviepy.video.fx.multiply_speed import multiply_speed
from moviepy.video.io.VideoFileClip import VideoFileClip
from moviepy.video.VideoClip import BitmapClip, ColorClip, VideoClip
from moviepy.video.VideoClip import BitmapClip, ColorClip, ImageClip, VideoClip
from tests.test_helper import TMP_DIR, get_stereo_wave, get_test_video
@@ -448,5 +449,17 @@ def test_videoclip_copy(copy_func):
assert other_clip.audio is None
def test_afterimage():
ai = ImageClip("media/afterimage.png")
masked_clip = mask_color(ai, color=[0, 255, 1]) # for green
some_background_clip = ColorClip((800, 600), color=(255, 255, 255))
final_clip = CompositeVideoClip(
[some_background_clip, masked_clip], use_bgclip=True
).with_duration(0.2)
filename = os.path.join(TMP_DIR, "afterimage.mp4")
final_clip.write_videofile(filename, fps=30, logger=None)
if __name__ == "__main__":
pytest.main()

View File

@@ -1,25 +0,0 @@
"""Video tests meant to be run with pytest."""
import os
import pytest
from moviepy.video.compositing.CompositeVideoClip import CompositeVideoClip
from moviepy.video.fx.mask_color import mask_color
from moviepy.video.VideoClip import ColorClip, ImageClip
from tests.test_helper import TMP_DIR
def test_afterimage():
ai = ImageClip("media/afterimage.png")
masked_clip = mask_color(ai, color=[0, 255, 1]) # for green
some_background_clip = ColorClip((800, 600), color=(255, 255, 255))
final_clip = CompositeVideoClip(
[some_background_clip, masked_clip], use_bgclip=True
).with_duration(0.2)
final_clip.write_videofile(os.path.join(TMP_DIR, "afterimage.mp4"), fps=30)
if __name__ == "__main__":
pytest.main()

View File

@@ -35,4 +35,8 @@ def test_matplotlib_simple_example():
return mplfig_to_npimage(fig)
animation = VideoClip(make_frame, duration=duration)
animation.write_gif(os.path.join(TMP_DIR, "matplotlib.gif"), fps=20)
filename = os.path.join(TMP_DIR, "matplotlib.gif")
animation.write_gif(filename, fps=20)
assert os.path.isfile(filename)

View File

@@ -6,10 +6,12 @@ import os
import pytest
from PIL import Image
from moviepy.video.compositing.concatenate import concatenate_videoclips
from moviepy.video.io.ffmpeg_writer import ffmpeg_write_image, ffmpeg_write_video
from moviepy.video.io.gif_writers import write_gif
from moviepy.video.io.VideoFileClip import VideoFileClip
from moviepy.video.tools.drawing import color_gradient
from moviepy.video.VideoClip import BitmapClip
from moviepy.video.VideoClip import BitmapClip, ColorClip
from tests.test_helper import TMP_DIR
@@ -171,3 +173,78 @@ def test_ffmpeg_write_image(size, logfile, pixel_format, expected_result):
for i in range(im.width):
for j in range(im.height):
assert im.getpixel((i, j)) == expected_result[j][i]
@pytest.mark.parametrize("loop", (None, 2), ids=("loop=None", "loop=2"))
@pytest.mark.parametrize(
"opt",
(False, "OptimizeTransparency"),
ids=("opt=False", "opt=OptimizeTransparency"),
)
@pytest.mark.parametrize("clip_class", ("BitmapClip", "ColorClip"))
@pytest.mark.parametrize(
"with_mask", (False, True), ids=("with_mask=False", "with_mask=True")
)
@pytest.mark.parametrize("pixel_format", ("invalid", None))
def test_write_gif(clip_class, opt, loop, with_mask, pixel_format):
filename = os.path.join(TMP_DIR, "moviepy_write_gif.gif")
if os.path.isfile(filename):
os.remove(filename)
fps = 10
if clip_class == "BitmapClip":
original_clip = BitmapClip([["R"], ["G"], ["B"]], fps=fps).with_duration(0.3)
else:
original_clip = concatenate_videoclips(
[
ColorClip(
(1, 1),
color=color,
)
.with_duration(0.1)
.with_fps(fps)
for color in [(255, 0, 0), (0, 255, 0), (0, 0, 255)]
]
)
if with_mask:
original_clip = original_clip.with_mask(
ColorClip((1, 1), color=1, is_mask=True).with_fps(fps).with_duration(0.3)
)
kwargs = {}
if pixel_format is not None:
kwargs["pixel_format"] = pixel_format
write_gif(
original_clip,
filename,
fps=fps,
with_mask=with_mask,
program="ffmpeg",
logger=None,
opt=opt,
loop=loop,
**kwargs,
)
if pixel_format != "invalid":
final_clip = VideoFileClip(filename)
r, g, b = final_clip.get_frame(0)[0][0]
assert r == 252
assert g == 0
assert b == 0
r, g, b = final_clip.get_frame(0.1)[0][0]
assert r == 0
assert g == 252
assert b == 0
r, g, b = final_clip.get_frame(0.2)[0][0]
assert r == 0
assert g == 0
assert b == 255
assert final_clip.duration == (loop or 1) * round(original_clip.duration, 6)