Skip to content

Commit

Permalink
Merge branch 'master' into audio_delay-fx
Browse files Browse the repository at this point in the history
  • Loading branch information
mondeja committed Jan 23, 2021
2 parents eee4581 + b519687 commit dacb02a
Show file tree
Hide file tree
Showing 6 changed files with 274 additions and 37 deletions.
4 changes: 3 additions & 1 deletion moviepy/audio/io/readers.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ def __init__(
self.codec = "pcm_s%dle" % (8 * nbytes)
self.nchannels = nchannels
infos = ffmpeg_parse_infos(filename, decode_file=decode_file)
self.duration = infos["duration"]
self.duration = infos.get("video_duration")
if self.duration is None:
self.duration = infos["duration"]
self.bitrate = infos["audio_bitrate"]
self.infos = infos
self.proc = None
Expand Down
22 changes: 18 additions & 4 deletions moviepy/video/fx/mask_and.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,26 @@


def mask_and(clip, other_clip):
"""Returns the logical 'and' (min) between two masks.
"""Returns the logical 'and' (minimum pixel color values) between two masks.
``other_clip`` can be a mask clip or a picture (np.array).
The result has the duration of 'clip' (if it has any)
The result has the duration of the clip to which has been applied, if it has any.
Parameters
----------
other_clip ImageClip or np.ndarray
Clip used to mask the original clip.
Examples
--------
>>> clip = ColorClip(color=(255, 0, 0), size=(1, 1)) # red
>>> mask = ColorClip(color=(0, 255, 0), size=(1, 1)) # green
>>> masked_clip = clip.fx(mask_and, mask) # black
>>> masked_clip.get_frame(0)
[[[0 0 0]]]
"""
# To ensure that 'or' of two ImageClips will be an ImageClip.
# to ensure that 'and' of two ImageClips will be an ImageClip
if isinstance(other_clip, ImageClip):
other_clip = other_clip.img

Expand Down
22 changes: 18 additions & 4 deletions moviepy/video/fx/mask_or.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,26 @@


def mask_or(clip, other_clip):
"""Returns the logical 'or' (max) between two masks.
"""Returns the logical 'or' (maximum pixel color values) between two masks.
``other_clip`` can be a mask clip or a picture (np.array).
The result has the duration of 'clip' (if it has any).
The result has the duration of the clip to which has been applied, if it has any.
Parameters
----------
other_clip ImageClip or np.ndarray
Clip used to mask the original clip.
Examples
--------
>>> clip = ColorClip(color=(255, 0, 0), size=(1, 1)) # red
>>> mask = ColorClip(color=(0, 255, 0), size=(1, 1)) # green
>>> masked_clip = clip.fx(mask_or, mask) # yellow
>>> masked_clip.get_frame(0)
[[[255 255 0]]]
"""
# To ensure that 'or' of two ImageClips will be an ImageClip.
# to ensure that 'or' of two ImageClips will be an ImageClip
if isinstance(other_clip, ImageClip):
other_clip = other_clip.img

Expand Down
3 changes: 2 additions & 1 deletion moviepy/video/fx/rotate.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import math
import warnings

import numpy as np
Expand Down Expand Up @@ -99,7 +100,7 @@ def filter(get_frame, t):
im = get_frame(t)

if unit == "rad":
angle = 360.0 * angle / (2 * np.pi)
angle = math.degrees(angle)

angle %= 360
if not center and not translate and not bg_color:
Expand Down
2 changes: 1 addition & 1 deletion moviepy/video/io/ffmpeg_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -766,4 +766,4 @@ def ffmpeg_parse_infos(
raise IsADirectoryError(f"'{filename}' is a directory")
elif not os.path.exists(filename):
raise FileNotFoundError(f"'{filename}' not found")
raise exc
raise IOError(f"Error pasing `ffmpeg -i` command output:\n\n{infos}") from exc
258 changes: 232 additions & 26 deletions tests/test_fx.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import math
import os
import random

import numpy as np
import pytest

from moviepy import AudioClip, AudioFileClip, BitmapClip, ColorClip, VideoFileClip
from moviepy import (
AudioClip,
AudioFileClip,
BitmapClip,
ColorClip,
VideoClip,
VideoFileClip,
)
from moviepy.audio.fx import (
audio_delay,
audio_normalize,
Expand All @@ -25,6 +33,8 @@
lum_contrast,
make_loopable,
margin,
mask_and,
mask_or,
mirror_x,
mirror_y,
multiply_color,
Expand Down Expand Up @@ -447,16 +457,114 @@ def test_margin():
assert target == clip3


def test_mask_and():
pass
@pytest.mark.parametrize("image_from", ("np.ndarray", "ImageClip"))
@pytest.mark.parametrize("duration", (None, "random"))
@pytest.mark.parametrize(
("color", "mask_color", "expected_color"),
(
(
(0, 0, 0),
(255, 255, 255),
(0, 0, 0),
),
(
(255, 0, 0),
(0, 0, 255),
(0, 0, 0),
),
(
(255, 255, 255),
(0, 10, 20),
(0, 10, 20),
),
(
(10, 10, 10),
(20, 0, 20),
(10, 0, 10),
),
),
)
def test_mask_and(image_from, duration, color, mask_color, expected_color):
"""Checks ``mask_and`` FX behaviour."""
clip_size = tuple(random.randint(3, 10) for i in range(2))

if duration == "random":
duration = round(random.uniform(0, 0.5), 2)

# test ImageClip and np.ndarray types as mask argument
clip = ColorClip(color=color, size=clip_size).with_duration(duration)
mask_clip = ColorClip(color=mask_color, size=clip.size)
masked_clip = mask_and(
clip, mask_clip if image_from == "ImageClip" else mask_clip.get_frame(0)
)

assert masked_clip.duration == clip.duration
assert np.array_equal(masked_clip.get_frame(0)[0][0], np.array(expected_color))

# test VideoClip as mask argument
color_frame, mask_color_frame = (np.array([[color]]), np.array([[mask_color]]))
clip = VideoClip(lambda t: color_frame).with_duration(duration)
mask_clip = VideoClip(lambda t: mask_color_frame).with_duration(duration)
masked_clip = mask_and(clip, mask_clip)

assert np.array_equal(masked_clip.get_frame(0)[0][0], np.array(expected_color))


def test_mask_color():
pass


def test_mask_or():
pass
@pytest.mark.parametrize("image_from", ("np.ndarray", "ImageClip"))
@pytest.mark.parametrize("duration", (None, "random"))
@pytest.mark.parametrize(
("color", "mask_color", "expected_color"),
(
(
(0, 0, 0),
(255, 255, 255),
(255, 255, 255),
),
(
(255, 0, 0),
(0, 0, 255),
(255, 0, 255),
),
(
(255, 255, 255),
(0, 10, 20),
(255, 255, 255),
),
(
(10, 10, 10),
(20, 0, 20),
(20, 10, 20),
),
),
)
def test_mask_or(image_from, duration, color, mask_color, expected_color):
"""Checks ``mask_or`` FX behaviour."""
clip_size = tuple(random.randint(3, 10) for i in range(2))

if duration == "random":
duration = round(random.uniform(0, 0.5), 2)

# test ImageClip and np.ndarray types as mask argument
clip = ColorClip(color=color, size=clip_size).with_duration(duration)
mask_clip = ColorClip(color=mask_color, size=clip.size)
masked_clip = mask_or(
clip, mask_clip if image_from == "ImageClip" else mask_clip.get_frame(0)
)

assert masked_clip.duration == clip.duration
assert np.array_equal(masked_clip.get_frame(0)[0][0], np.array(expected_color))

# test VideoClip as mask argument
color_frame, mask_color_frame = (np.array([[color]]), np.array([[mask_color]]))
clip = VideoClip(lambda t: color_frame).with_duration(duration)
mask_clip = VideoClip(lambda t: mask_color_frame).with_duration(duration)
masked_clip = mask_or(clip, mask_clip)

assert np.array_equal(masked_clip.get_frame(0)[0][0], np.array(expected_color))


def test_mirror_x():
Expand Down Expand Up @@ -500,30 +608,128 @@ def test_resize():
# clip4.write_videofile(os.path.join(TMP_DIR, "resize4.webm"))


# Run several times to ensure that adding 360 to rotation angles has no effect
@pytest.mark.parametrize("angle_offset", [-360, 0, 360, 720])
def test_rotate(angle_offset):
# Run several times to ensure that adding 360 to rotation angles has no effect
clip = BitmapClip([["AAAA", "BBBB", "CCCC"], ["ABCD", "BCDE", "CDEA"]], fps=1)

clip1 = rotate(clip, 0 + angle_offset)
target1 = BitmapClip([["AAAA", "BBBB", "CCCC"], ["ABCD", "BCDE", "CDEA"]], fps=1)
assert clip1 == target1

clip2 = rotate(clip, 90 + angle_offset)
target2 = BitmapClip(
[["ABC", "ABC", "ABC", "ABC"], ["DEA", "CDE", "BCD", "ABC"]], fps=1
)
assert clip2 == target2, clip2.to_bitmap()
@pytest.mark.parametrize("unit", ["deg", "rad"])
@pytest.mark.parametrize("resample", ["bilinear", "nearest", "bicubic", "unknown"])
@pytest.mark.parametrize(
(
"angle",
"translate",
"center",
"bg_color",
"expected_frames",
),
(
(
0,
None,
None,
None,
[["AAAA", "BBBB", "CCCC"], ["ABCD", "BCDE", "CDEA"]],
),
(
90,
None,
None,
None,
[["ABC", "ABC", "ABC", "ABC"], ["DEA", "CDE", "BCD", "ABC"]],
),
(
lambda t: 90,
None,
None,
None,
[["ABC", "ABC", "ABC", "ABC"], ["DEA", "CDE", "BCD", "ABC"]],
),
(
180,
None,
None,
None,
[["CCCC", "BBBB", "AAAA"], ["AEDC", "EDCB", "DCBA"]],
),
(
270,
None,
None,
None,
[["CBA", "CBA", "CBA", "CBA"], ["CBA", "DCB", "EDC", "AED"]],
),
(
45,
(50, 50),
None,
(0, 255, 0),
[
["GGGGGG", "GGGGGG", "GGGGGG", "GGGGGG", "GGGGGG", "GGGGGG"],
["GGGGGG", "GGGGGG", "GGGGGG", "GGGGGG", "GGGGGG", "GGGGGG"],
],
),
(
45,
(50, 50),
(20, 20),
(255, 0, 0),
[
["RRRRRR", "RRRRRR", "RRRRRR", "RRRRRR", "RRRRRR"],
["RRRRRR", "RRRRRR", "RRRRRR", "RRRRRR", "RRRRRR"],
],
),
(
135,
(-100, -100),
None,
(0, 0, 255),
[
["BBBBBB", "BBBBBB", "BBBBBB", "BBBBBB", "BBBBBB"],
["BBBBBB", "BBBBBB", "BBBBBB", "BBBBBB", "BBBBBB"],
],
),
),
)
def test_rotate(
angle_offset,
angle,
unit,
resample,
translate,
center,
bg_color,
expected_frames,
):
"""Check ``rotate`` FX behaviour against possible combinations of arguments."""
original_frames = [["AAAA", "BBBB", "CCCC"], ["ABCD", "BCDE", "CDEA"]]

# angles are defined in degrees, so convert to radians testing ``unit="rad"``
if unit == "rad":
if hasattr(angle, "__call__"):
_angle = lambda t: math.radians(angle(0))
else:
_angle = math.radians(angle)
else:
_angle = angle
clip = BitmapClip(original_frames, fps=1)

clip3 = rotate(clip, 180 + angle_offset)
target3 = BitmapClip([["CCCC", "BBBB", "AAAA"], ["AEDC", "EDCB", "DCBA"]], fps=1)
assert clip3 == target3
kwargs = {
"unit": unit,
"resample": resample,
"translate": translate,
"center": center,
"bg_color": bg_color,
}
if resample not in ["bilinear", "nearest", "bicubic"]:
with pytest.raises(ValueError) as exc:
clip.rotate(_angle, **kwargs)
assert (
"'resample' argument must be either 'bilinear', 'nearest' or 'bicubic'"
) == str(exc.value)
return
else:
rotated_clip = clip.rotate(_angle, **kwargs)

clip4 = rotate(clip, 270 + angle_offset)
target4 = BitmapClip(
[["CBA", "CBA", "CBA", "CBA"], ["CBA", "DCB", "EDC", "AED"]], fps=1
)
assert clip4 == target4
expected_clip = BitmapClip(expected_frames, fps=1)
assert rotated_clip.to_bitmap() == expected_clip.to_bitmap()


def test_rotate_nonstandard_angles():
Expand Down

0 comments on commit dacb02a

Please sign in to comment.