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:
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user