Skip to content

Commit

Permalink
Merge branch 'master' into doc-accel_decel
Browse files Browse the repository at this point in the history
  • Loading branch information
mondeja authored May 24, 2021
2 parents 6685f89 + 8523616 commit c0c8241
Show file tree
Hide file tree
Showing 16 changed files with 360 additions and 51 deletions.
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://gitlab.com/pycqa/flake8
rev: master
- repo: https://github.com/PyCQA/flake8
rev: 3.9.2
hooks:
- id: flake8
additional_dependencies:
Expand All @@ -9,7 +9,7 @@ repos:
name: flake8-test
files: \.py$
- repo: https://github.com/PyCQA/isort
rev: master
rev: 5.8.0
hooks:
- id: isort
args:
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Renamed `colorx` FX by `multiply_color` [\#1475](https://github.com/Zulko/moviepy/pull/1475)
- Renamed `speedx` FX by `multiply_speed` [\#1478](https://github.com/Zulko/moviepy/pull/1478)
- `make_loopable` transition must be used as FX [\#1477](https://github.com/Zulko/moviepy/pull/1477)
- `requests` package is no longer a dependency [\#1566](https://github.com/Zulko/moviepy/pull/1566)
- `accel_decel` FX raises `ValueError` if `sooness` parameter value is lower than zero [\#1546](https://github.com/Zulko/moviepy/pull/1546)

### Deprecated <!-- for soon-to-be removed features -->
Expand All @@ -51,6 +52,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed `fps` not defined in `CompositeAudioClip` at initialization [\#1462](https://github.com/Zulko/moviepy/pull/1462)
- Fixed `clip.preview()` crashing at exit when running inside Jupyter Notebook in Windows [\#1537](https://github.com/Zulko/moviepy/pull/1537)
- Fixed rotate FX not being applied to mask images [\#1399](https://github.com/Zulko/moviepy/pull/1399)
- Fixed opacity error blitting VideoClips [\#1552](https://github.com/Zulko/moviepy/pull/1552)
- Fixed rotation metadata of input not being taken into account rendering VideoClips [\#577](https://github.com/Zulko/moviepy/pull/577)
- Fixed mono clips crashing when `audio_fadein` FX applied [\#1574](https://github.com/Zulko/moviepy/pull/1574)
- Fixed mono clips crashing when `audio_fadeout` FX applied [\#1578](https://github.com/Zulko/moviepy/pull/1578)


## [v2.0.0.dev2](https://github.com/zulko/moviepy/tree/v2.0.0.dev2) (2020-10-05)
Expand Down
Binary file added media/rotated-90-degrees.mp4
Binary file not shown.
46 changes: 34 additions & 12 deletions moviepy/audio/fx/audio_fadein.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,45 @@
import numpy as np

from moviepy.decorators import audio_video_fx
from moviepy.decorators import audio_video_fx, convert_parameter_to_seconds


def _mono_factor_getter():
return lambda t, duration: np.minimum(t / duration, 1)


def _stereo_factor_getter(nchannels):
def getter(t, duration):
factor = np.minimum(t / duration, 1)
return np.array([factor for _ in range(nchannels)]).T

return getter


@audio_video_fx
@convert_parameter_to_seconds(["duration"])
def audio_fadein(clip, duration):
"""Return an audio (or video) clip that is first mute, then the
sound arrives progressively over ``duration`` seconds.
"""
def fading(get_frame, t):
frame = get_frame(t)
Parameters
----------
duration : float
How long does it take for the sound to return to its normal level.
if np.isscalar(t):
factor = min(1.0 * t / duration, 1)
factor = np.array([factor, factor])
else:
factor = np.minimum(1.0 * t / duration, 1)
factor = np.vstack([factor, factor]).T
return factor * frame
Examples
--------
>>> clip = VideoFileClip("media/chaplin.mp4")
>>> clip.fx(audio_fadein, "00:00:06")
"""
get_factor = (
_mono_factor_getter()
if clip.nchannels == 1
else _stereo_factor_getter(clip.nchannels)
)

return clip.transform(fading, keep_duration=True)
return clip.transform(
lambda get_frame, t: get_factor(t, duration) * get_frame(t),
keep_duration=True,
)
51 changes: 39 additions & 12 deletions moviepy/audio/fx/audio_fadeout.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,51 @@
import numpy as np

from moviepy.decorators import audio_video_fx, requires_duration
from moviepy.decorators import (
audio_video_fx,
convert_parameter_to_seconds,
requires_duration,
)


def _mono_factor_getter(clip_duration):
return lambda t, duration: np.minimum(1.0 * (clip_duration - t) / duration, 1)


def _stereo_factor_getter(clip_duration, nchannels):
def getter(t, duration):
factor = np.minimum(1.0 * (clip_duration - t) / duration, 1)
return np.array([factor for _ in range(nchannels)]).T

return getter


@audio_video_fx
@requires_duration
@convert_parameter_to_seconds(["duration"])
def audio_fadeout(clip, duration):
"""Return a sound clip where the sound fades out progressively
over ``duration`` seconds at the end of the clip.
"""
def fading(get_frame, t):
frame = get_frame(t)
Parameters
----------
duration : float
How long does it take for the sound to reach the zero level at the end
of the clip.
if np.isscalar(t):
factor = min(1.0 * (clip.duration - t) / duration, 1)
factor = np.array([factor, factor])
else:
factor = np.minimum(1.0 * (clip.duration - t) / duration, 1)
factor = np.vstack([factor, factor]).T
return factor * frame
Examples
--------
>>> clip = VideoFileClip("media/chaplin.mp4")
>>> clip.fx(audio_fadeout, "00:00:06")
"""
get_factor = (
_mono_factor_getter(clip.duration)
if clip.nchannels == 1
else _stereo_factor_getter(clip.duration, clip.nchannels)
)

return clip.transform(fading, keep_duration=True)
return clip.transform(
lambda get_frame, t: get_factor(t, duration) * get_frame(t),
keep_duration=True,
)
9 changes: 3 additions & 6 deletions moviepy/decorators.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Decorators used by moviepy."""
import inspect
import os

import decorator
Expand Down Expand Up @@ -81,9 +82,7 @@ def preprocess_args(fun, varnames):
"""Applies fun to variables in varnames before launching the function."""

def wrapper(func, *args, **kwargs):
func_code = func.__code__

names = func_code.co_varnames
names = inspect.getfullargspec(func).args
new_args = [
fun(arg) if (name in varnames) and (arg is not None) else arg
for (arg, name) in zip(args, names)
Expand Down Expand Up @@ -131,9 +130,7 @@ def find_fps(fps):
" the clip's fps with `clip.fps=24`" % func.__name__
)

func_code = func.__code__

names = func_code.co_varnames[1:]
names = inspect.getfullargspec(func).args[1:]

new_args = [
find_fps(arg) if (name == "fps") else arg for (arg, name) in zip(args, names)
Expand Down
4 changes: 2 additions & 2 deletions moviepy/video/VideoClip.py
Original file line number Diff line number Diff line change
Expand Up @@ -636,8 +636,8 @@ def blit_on(self, picture, t):
im_img = Image.fromarray(img)

if self.mask is not None:
mask = self.mask.get_frame(ct).astype("uint8")
im_mask = Image.fromarray(255 * mask).convert("L")
mask = (self.mask.get_frame(ct) * 255).astype("uint8")
im_mask = Image.fromarray(mask).convert("L")

if im_img.size != im_mask.size:
bg_size = (
Expand Down
7 changes: 4 additions & 3 deletions moviepy/video/fx/rotate.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,10 @@ def filter(get_frame, t):
else:
if kw_value is not None: # if not default value
warnings.warn(
f"rotate '{kw_name}' argument not supported by your"
" Pillow version and is being ignored. Minimum Pillow version"
f" required: v{'.'.join(str(n) for n in min_version)}",
f"rotate '{kw_name}' argument is not supported"
" by your Pillow version and is being ignored. Minimum"
" Pillow version required:"
f" v{'.'.join(str(n) for n in min_version)}",
UserWarning,
)

Expand Down
10 changes: 4 additions & 6 deletions moviepy/video/io/downloader.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""Utilities to get a file from the internet."""

import os

import requests
import shutil
import urllib.request

from moviepy.tools import subprocess_call

Expand All @@ -18,10 +18,8 @@ def download_webfile(url, filename, overwrite=False):
return

if "." in url:
r = requests.get(url, stream=True)
with open(filename, "wb") as fd:
for chunk in r.iter_content(chunk_size=128):
fd.write(chunk)
with urllib.request.urlopen(url) as req, open(filename, "wb") as f:
shutil.copyfileobj(req, f, 128)

else:
try:
Expand Down
7 changes: 6 additions & 1 deletion moviepy/video/io/ffmpeg_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,12 @@ def __init__(
)
self.fps = infos["video_fps"]
self.size = infos["video_size"]
self.rotation = infos.get("video_rotation", 0)

# ffmpeg automatically rotates videos if rotation information is
# available, so exchange width and height
self.rotation = abs(infos.get("video_rotation", 0))
if self.rotation in [90, 270]:
self.size = [self.size[1], self.size[0]]

if target_resolution:
if None in target_resolution:
Expand Down
2 changes: 1 addition & 1 deletion moviepy/video/tools/segmenting.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def find_objects(clip, size_threshold=500, preview=False):

letters = []
for i, (sy, sx) in indexed_slices:
""" crop each letter separately """
# crop each letter separately
sy = slice(sy.start - 1, sy.stop + 1)
sx = slice(sx.start - 1, sx.stop + 1)
letter = image[sy, sx]
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ per-file-ignores =
# the version file doesn't need module level docstring
moviepy/version.py: D100
# tests doesn't require docstring (although is recommended)
tests/*.py: D103
tests/*.py: D101,D102,D103
# allow 'from moviepy import *' in examples
examples/*.py: F403, F405
docstring-convention = numpy
Expand Down
3 changes: 1 addition & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,10 @@ def run_tests(self):

# Define the requirements for specific execution needs.
requires = [
"decorator>=4.0.2,<5.0",
"decorator>=4.0.2,<6.0",
"imageio>=2.5,<3.0",
"imageio_ffmpeg>=0.2.0",
"numpy>=1.17.3,<=1.20",
"requests>=2.8.1,<3.0",
"proglog<=1.0.0",
]

Expand Down
48 changes: 47 additions & 1 deletion tests/test_compositing.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,37 @@
import pytest

from moviepy.utils import close_all_clips
from moviepy.video.compositing.CompositeVideoClip import clips_array
from moviepy.video.compositing.CompositeVideoClip import CompositeVideoClip, clips_array
from moviepy.video.compositing.concatenate import concatenate_videoclips
from moviepy.video.fx.resize import resize
from moviepy.video.io.VideoFileClip import VideoFileClip
from moviepy.video.VideoClip import BitmapClip, ColorClip

from tests.test_helper import TMP_DIR


class ClipPixelTest:
ALLOWABLE_COLOR_VARIATION = 3 # from 0-767: how much mismatch do we allow

def __init__(self, clip):
self.clip = clip

def expect_color_at(self, ts, expected, xy=[0, 0]):
frame = self.clip.make_frame(ts)
r, g, b = expected
actual = frame[xy[1]][xy[0]]
diff = abs(actual[0] - r) + abs(actual[1] - g) + abs(actual[2] - b)

mismatch = diff > ClipPixelTest.ALLOWABLE_COLOR_VARIATION
assert (
not mismatch
), "Expected (%02x,%02x,%02x) but got (%02x,%02x,%02x) at timestamp %s" % (
*expected,
*actual,
ts,
)


def test_clips_array():
red = ColorClip((1024, 800), color=(255, 0, 0))
green = ColorClip((1024, 800), color=(0, 255, 0))
Expand Down Expand Up @@ -67,3 +90,26 @@ def test_concatenate_floating_point():
concat = concatenate_videoclips([clip])
concat.write_videofile(os.path.join(TMP_DIR, "concat.mp4"), preset="ultrafast")
close_all_clips(locals())


def test_blit_with_opacity():
# bitmap.mp4 has one second R, one second G, one second B
clip1 = VideoFileClip("media/bitmap.mp4")
# overlay same clip, shifted by 1 second, at half opacity
clip2 = (
VideoFileClip("media/bitmap.mp4")
.subclip(1, 2)
.with_start(0)
.with_end(2)
.with_opacity(0.5)
)
composite = CompositeVideoClip([clip1, clip2])
bt = ClipPixelTest(composite)

bt.expect_color_at(0.5, (0x7F, 0x7F, 0x00))
bt.expect_color_at(1.5, (0x00, 0x7F, 0x7F))
bt.expect_color_at(2.5, (0x00, 0x00, 0xFF))


if __name__ == "__main__":
pytest.main()
22 changes: 22 additions & 0 deletions tests/test_ffmpeg_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
FFmpegInfosParser,
ffmpeg_parse_infos,
)
from moviepy.video.io.VideoFileClip import VideoFileClip
from moviepy.video.VideoClip import BitmapClip

from tests.test_helper import TMP_DIR
Expand Down Expand Up @@ -299,6 +300,27 @@ def test_ffmpeg_parse_infos_metadata_with_attached_pic():
assert len(d["metadata"].keys()) == 7


def test_ffmpeg_parse_video_rotation():
d = ffmpeg_parse_infos("media/rotated-90-degrees.mp4")
assert d["video_rotation"] == 90
assert d["video_size"] == [1920, 1080]


def test_correct_video_rotation():
"""See https://github.com/Zulko/moviepy/pull/577"""
clip = VideoFileClip("media/rotated-90-degrees.mp4").subclip(0.2, 0.4)

corrected_rotation_filename = os.path.join(
TMP_DIR,
"correct_video_rotation.mp4",
)
clip.write_videofile(corrected_rotation_filename)

d = ffmpeg_parse_infos(corrected_rotation_filename)
assert "video_rotation" not in d
assert d["video_size"] == [1080, 1920]


def test_ffmpeg_parse_infos_multiline_metadata():
"""Check that the parser can parse multiline metadata values."""
infos = """Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '/home/110_PREV_FINAL.mov':
Expand Down
Loading

0 comments on commit c0c8241

Please sign in to comment.