initial commit and version 1.0

This commit is contained in:
2025-04-21 15:14:03 +02:00
commit ae6b2bbf44
82 changed files with 10782 additions and 0 deletions

View File

@@ -0,0 +1,83 @@
from dataclasses import dataclass
from moviepy.Effect import Effect
@dataclass
class AccelDecel(Effect):
"""Accelerates and decelerates a clip, useful for GIF making.
Parameters
----------
new_duration : float
Duration for the new transformed clip. If None, will be that of the
current clip.
abruptness : float
Slope shape in the acceleration-deceleration function. It will depend
on the value of the parameter:
* ``-1 < abruptness < 0``: speed up, down, up.
* ``abruptness == 0``: no effect.
* ``abruptness > 0``: speed down, up, down.
soonness : float
For positive abruptness, determines how soon the transformation occurs.
Should be a positive number.
Raises
------
ValueError
When ``sooness`` argument is lower than 0.
Examples
--------
The following graphs show functions generated by different combinations
of arguments, where the value of the slopes represents the speed of the
videos generated, being the linear function (in red) a combination that
does not produce any transformation.
.. image:: /_static/medias/accel_decel-fx-params.png
:alt: acced_decel FX parameters combinations
"""
new_duration: float = None
abruptness: float = 1.0
soonness: float = 1.0
def _f_accel_decel(
self, t, old_duration, new_duration, abruptness=1.0, soonness=1.0
):
a = 1.0 + abruptness
def _f(t):
def f1(t):
return (0.5) ** (1 - a) * (t**a)
def f2(t):
return 1 - f1(1 - t)
return (t < 0.5) * f1(t) + (t >= 0.5) * f2(t)
return old_duration * _f((t / new_duration) ** soonness)
def apply(self, clip):
"""Apply the effect to the clip."""
if self.new_duration is None:
self.new_duration = clip.duration
if self.soonness < 0:
raise ValueError("'sooness' should be a positive number")
return clip.time_transform(
lambda t: self._f_accel_decel(
t=t,
old_duration=clip.duration,
new_duration=self.new_duration,
abruptness=self.abruptness,
soonness=self.soonness,
)
).with_duration(self.new_duration)

View File

@@ -0,0 +1,38 @@
from dataclasses import dataclass
import numpy as np
from moviepy.Effect import Effect
@dataclass
class BlackAndWhite(Effect):
"""Desaturates the picture, makes it black and white.
Parameter RGB allows to set weights for the different color
channels.
If RBG is 'CRT_phosphor' a special set of values is used.
preserve_luminosity maintains the sum of RGB to 1.
"""
RGB: str = None
preserve_luminosity: bool = True
def apply(self, clip):
"""Apply the effect to the clip."""
if self.RGB is None:
self.RGB = [1, 1, 1]
if self.RGB == "CRT_phosphor":
self.RGB = [0.2125, 0.7154, 0.0721]
R, G, B = (
1.0
* np.array(self.RGB)
/ (sum(self.RGB) if self.preserve_luminosity else 1)
)
def filter(im):
im = R * im[:, :, 0] + G * im[:, :, 1] + B * im[:, :, 2]
return np.dstack(3 * [im]).astype("uint8")
return clip.image_transform(filter)

27
moviepy/video/fx/Blink.py Normal file
View File

@@ -0,0 +1,27 @@
from dataclasses import dataclass
from moviepy.Effect import Effect
@dataclass
class Blink(Effect):
"""
Makes the clip blink. At each blink it will be displayed ``duration_on``
seconds and disappear ``duration_off`` seconds. Will only work in
composite clips.
"""
duration_on: float
duration_off: float
def apply(self, clip):
"""Apply the effect to the clip."""
if clip.mask is None:
clip = clip.with_mask()
duration = self.duration_on + self.duration_off
clip.mask = clip.mask.transform(
lambda get_frame, t: get_frame(t) * ((t % duration) < self.duration_on)
)
return clip

80
moviepy/video/fx/Crop.py Normal file
View File

@@ -0,0 +1,80 @@
from dataclasses import dataclass
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class Crop(Effect):
"""Effect to crop a clip to get a new clip in which just a rectangular
subregion of the original clip is conserved. `x1,y1` indicates the top left
corner and `x2,y2` is the lower right corner of the cropped region. All
coordinates are in pixels. Float numbers are accepted.
To crop an arbitrary rectangle:
>>> Crop(x1=50, y1=60, x2=460, y2=275)
Only remove the part above y=30:
>>> Crop(y1=30)
Crop a rectangle that starts 10 pixels left and is 200px wide
>>> Crop(x1=10, width=200)
Crop a rectangle centered in x,y=(300,400), width=50, height=150 :
>>> Crop(x_center=300, y_center=400, width=50, height=150)
Any combination of the above should work, like for this rectangle
centered in x=300, with explicit y-boundaries:
>>> Crop(x_center=300, width=400, y1=100, y2=600)
"""
x1: int = None
y1: int = None
x2: int = None
y2: int = None
width: int = None
height: int = None
x_center: int = None
y_center: int = None
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
if self.width and self.x1 is not None:
self.x2 = self.x1 + self.width
elif self.width and self.x2 is not None:
self.x1 = self.x2 - self.width
if self.height and self.y1 is not None:
self.y2 = self.y1 + self.height
elif self.height and self.y2 is not None:
self.y1 = self.y2 - self.height
if self.x_center:
self.x1, self.x2 = (
self.x_center - self.width / 2,
self.x_center + self.width / 2,
)
if self.y_center:
self.y1, self.y2 = (
self.y_center - self.height / 2,
self.y_center + self.height / 2,
)
self.x1 = self.x1 or 0
self.y1 = self.y1 or 0
self.x2 = self.x2 or clip.size[0]
self.y2 = self.y2 or clip.size[1]
return clip.image_transform(
lambda frame: frame[
int(self.y1) : int(self.y2), int(self.x1) : int(self.x2)
],
apply_to=["mask"],
)

View File

@@ -0,0 +1,27 @@
from dataclasses import dataclass
from moviepy.Clip import Clip
from moviepy.Effect import Effect
from moviepy.video.fx.FadeIn import FadeIn
@dataclass
class CrossFadeIn(Effect):
"""Makes the clip appear progressively, over ``duration`` seconds.
Only works when the clip is included in a CompositeVideoClip.
"""
duration: float
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
if clip.duration is None:
raise ValueError("Attribute 'duration' not set")
if clip.mask is None:
clip = clip.with_mask()
clip.mask.duration = clip.duration
clip.mask = clip.mask.with_effects([FadeIn(self.duration)])
return clip

View File

@@ -0,0 +1,27 @@
from dataclasses import dataclass
from moviepy.Clip import Clip
from moviepy.Effect import Effect
from moviepy.video.fx.FadeOut import FadeOut
@dataclass
class CrossFadeOut(Effect):
"""Makes the clip disappear progressively, over ``duration`` seconds.
Only works when the clip is included in a CompositeVideoClip.
"""
duration: float
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
if clip.duration is None:
raise ValueError("Attribute 'duration' not set")
if clip.mask is None:
clip = clip.with_mask()
clip.mask.duration = clip.duration
clip.mask = clip.mask.with_effects([FadeOut(self.duration)])
return clip

View File

@@ -0,0 +1,34 @@
from dataclasses import dataclass
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class EvenSize(Effect):
"""Crops the clip to make dimensions even."""
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
w, h = clip.size
w_even = w % 2 == 0
h_even = h % 2 == 0
if w_even and h_even:
return clip
if not w_even and not h_even:
def image_filter(a):
return a[:-1, :-1, :]
elif h_even:
def image_filter(a):
return a[:, :-1, :]
else:
def image_filter(a):
return a[:-1, :, :]
return clip.image_transform(image_filter, apply_to=["mask"])

View File

@@ -0,0 +1,36 @@
from dataclasses import dataclass
import numpy as np
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class FadeIn(Effect):
"""Makes the clip progressively appear from some color (black by default),
over ``duration`` seconds at the beginning of the clip. Can be used for
masks too, where the initial color must be a number between 0 and 1.
For cross-fading (progressive appearance or disappearance of a clip
over another clip, see ``CrossFadeIn``
"""
duration: float
initial_color: list = None
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
if self.initial_color is None:
self.initial_color = 0 if clip.is_mask else [0, 0, 0]
self.initial_color = np.array(self.initial_color)
def filter(get_frame, t):
if t >= self.duration:
return get_frame(t)
else:
fading = 1.0 * t / self.duration
return fading * get_frame(t) + (1 - fading) * self.initial_color
return clip.transform(filter)

View File

@@ -0,0 +1,39 @@
from dataclasses import dataclass
import numpy as np
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class FadeOut(Effect):
"""Makes the clip progressively fade to some color (black by default),
over ``duration`` seconds at the end of the clip. Can be used for masks too,
where the final color must be a number between 0 and 1.
For cross-fading (progressive appearance or disappearance of a clip over another
clip), see ``CrossFadeOut``
"""
duration: float
final_color: list = None
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
if clip.duration is None:
raise ValueError("Attribute 'duration' not set")
if self.final_color is None:
self.final_color = 0 if clip.is_mask else [0, 0, 0]
self.final_color = np.array(self.final_color)
def filter(get_frame, t):
if (clip.duration - t) >= self.duration:
return get_frame(t)
else:
fading = 1.0 * (clip.duration - t) / self.duration
return fading * get_frame(t) + (1 - fading) * self.final_color
return clip.transform(filter)

View File

@@ -0,0 +1,43 @@
from dataclasses import dataclass
from moviepy.Clip import Clip
from moviepy.Effect import Effect
from moviepy.video.compositing.CompositeVideoClip import concatenate_videoclips
@dataclass
class Freeze(Effect):
"""Momentarily freeze the clip at time t.
Set `t='end'` to freeze the clip at the end (actually it will freeze on the
frame at time clip.duration - padding_end seconds - 1 / clip_fps).
With ``duration`` you can specify the duration of the freeze.
With ``total_duration`` you can specify the total duration of
the clip and the freeze (i.e. the duration of the freeze is
automatically computed). One of them must be provided.
"""
t: float = 0
freeze_duration: float = None
total_duration: float = None
padding_end: float = 0
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
if clip.duration is None:
raise ValueError("Attribute 'duration' not set")
if self.t == "end":
self.t = clip.duration - self.padding_end - 1 / clip.fps
if self.freeze_duration is None:
if self.total_duration is None:
raise ValueError(
"You must provide either 'freeze_duration' or 'total_duration'"
)
self.freeze_duration = self.total_duration - clip.duration
before = [clip[: self.t]] if (self.t != 0) else []
freeze = [clip.to_ImageClip(self.t).with_duration(self.freeze_duration)]
after = [clip[self.t :]] if (self.t != clip.duration) else []
return concatenate_videoclips(before + freeze + after)

View File

@@ -0,0 +1,68 @@
from dataclasses import dataclass
from moviepy.Clip import Clip
from moviepy.Effect import Effect
from moviepy.video.compositing.CompositeVideoClip import CompositeVideoClip
from moviepy.video.fx.Crop import Crop
@dataclass
class FreezeRegion(Effect):
"""Freezes one region of the clip while the rest remains animated.
You can choose one of three methods by providing either `region`,
`outside_region`, or `mask`.
Parameters
----------
t
Time at which to freeze the freezed region.
region
A tuple (x1, y1, x2, y2) defining the region of the screen (in pixels)
which will be freezed. You can provide outside_region or mask instead.
outside_region
A tuple (x1, y1, x2, y2) defining the region of the screen (in pixels)
which will be the only non-freezed region.
mask
If not None, will overlay a freezed version of the clip on the current clip,
with the provided mask. In other words, the "visible" pixels in the mask
indicate the freezed region in the final picture.
"""
t: float = 0
region: tuple = None
outside_region: tuple = None
mask: Clip = None
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
if self.region is not None:
x1, y1, _x2, _y2 = self.region
freeze = (
clip.with_effects([Crop(*self.region)])
.to_ImageClip(t=self.t)
.with_duration(clip.duration)
.with_position((x1, y1))
)
return CompositeVideoClip([clip, freeze])
elif self.outside_region is not None:
x1, y1, x2, y2 = self.outside_region
animated_region = clip.with_effects(
[Crop(*self.outside_region)]
).with_position((x1, y1))
freeze = clip.to_ImageClip(t=self.t).with_duration(clip.duration)
return CompositeVideoClip([freeze, animated_region])
elif self.mask is not None:
freeze = (
clip.to_ImageClip(t=self.t)
.with_duration(clip.duration)
.with_mask(self.mask)
)
return CompositeVideoClip([clip, freeze])

View File

@@ -0,0 +1,20 @@
from dataclasses import dataclass
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class GammaCorrection(Effect):
"""Gamma-correction of a video clip."""
gamma: float
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
def filter(im):
corrected = 255 * (1.0 * im / 255) ** self.gamma
return corrected.astype("uint8")
return clip.image_transform(filter)

View File

@@ -0,0 +1,45 @@
from dataclasses import dataclass
import numpy as np
from PIL import Image, ImageDraw, ImageFilter
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class HeadBlur(Effect):
"""Returns a filter that will blur a moving part (a head ?) of the frames.
The position of the blur at time t is defined by (fx(t), fy(t)), the radius
of the blurring by ``radius`` and the intensity of the blurring by ``intensity``.
"""
fx: callable
fy: callable
radius: float
intensity: float = None
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
if self.intensity is None:
self.intensity = int(2 * self.radius / 3)
def filter(gf, t):
im = gf(t).copy()
h, w, d = im.shape
x, y = int(self.fx(t)), int(self.fy(t))
x1, x2 = max(0, x - self.radius), min(x + self.radius, w)
y1, y2 = max(0, y - self.radius), min(y + self.radius, h)
image = Image.fromarray(im)
mask = Image.new("RGB", image.size)
draw = ImageDraw.Draw(mask)
draw.ellipse([x1, y1, x2, y2], fill=(255, 255, 255))
blurred = image.filter(ImageFilter.GaussianBlur(radius=self.intensity))
res = np.where(np.array(mask) > 0, np.array(blurred), np.array(image))
return res
return clip.transform(filter)

View File

@@ -0,0 +1,18 @@
from dataclasses import dataclass
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class InvertColors(Effect):
"""Returns the color-inversed clip.
The values of all pixels are replaced with (255-v) or (1-v) for masks
Black becomes white, green becomes purple, etc.
"""
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
maxi = 1.0 if clip.is_mask else 255
return clip.image_transform(lambda f: maxi - f)

43
moviepy/video/fx/Loop.py Normal file
View File

@@ -0,0 +1,43 @@
from dataclasses import dataclass
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class Loop(Effect):
"""
Returns a clip that plays the current clip in an infinite loop.
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).
duration
Total duration of the clip. Can be specified instead of n.
"""
n: int = None
duration: float = None
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
if clip.duration is None:
raise ValueError("Attribute 'duration' not set")
previous_duration = clip.duration
clip = clip.time_transform(
lambda t: t % previous_duration, apply_to=["mask", "audio"]
)
if self.n:
self.duration = self.n * previous_duration
if self.duration:
clip = clip.with_duration(self.duration)
return clip

View File

@@ -0,0 +1,27 @@
from dataclasses import dataclass
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class LumContrast(Effect):
"""Luminosity-contrast correction of a clip."""
lum: float = 0
contrast: float = 0
contrast_threshold: float = 127
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
def image_filter(im):
im = 1.0 * im # float conversion
corrected = (
im + self.lum + self.contrast * (im - float(self.contrast_threshold))
)
corrected[corrected < 0] = 0
corrected[corrected > 255] = 255
return corrected.astype("uint8")
return clip.image_transform(image_filter)

View File

@@ -0,0 +1,30 @@
from dataclasses import dataclass
from moviepy.Clip import Clip
from moviepy.Effect import Effect
from moviepy.video.compositing.CompositeVideoClip import CompositeVideoClip
from moviepy.video.fx.CrossFadeIn import CrossFadeIn
@dataclass
class MakeLoopable(Effect):
"""Makes the clip fade in progressively at its own end, this way it can be
looped indefinitely.
Parameters
----------
overlap_duration : float
Duration of the fade-in (in seconds).
"""
overlap_duration: float
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
clip2 = clip.with_effects([CrossFadeIn(self.overlap_duration)]).with_start(
clip.duration - self.overlap_duration
)
return CompositeVideoClip([clip, clip2]).subclipped(
self.overlap_duration, clip.duration
)

View File

@@ -0,0 +1,90 @@
from dataclasses import dataclass
import numpy as np
from moviepy.Clip import Clip
from moviepy.Effect import Effect
from moviepy.video.VideoClip import ImageClip
@dataclass
class Margin(Effect):
"""Draws an external margin all around the frame.
Parameters
----------
margin_size : int, optional
If not ``None``, then the new clip has a margin size of
size ``margin_size`` in pixels on the left, right, top, and bottom.
left : int, optional
If ``margin_size=None``, margin size for the new clip in left direction.
right : int, optional
If ``margin_size=None``, margin size for the new clip in right direction.
top : int, optional
If ``margin_size=None``, margin size for the new clip in top direction.
bottom : int, optional
If ``margin_size=None``, margin size for the new clip in bottom direction.
color : tuple, optional
Color of the margin.
opacity : float, optional
Opacity of the margin. Setting this value to 0 yields transparent margins.
"""
margin_size: int = None
left: int = 0
right: int = 0
top: int = 0
bottom: int = 0
color: tuple = (0, 0, 0)
opacity: float = 1.0
def add_margin(self, clip: Clip):
"""Add margins to the clip."""
if (self.opacity != 1.0) and (clip.mask is None) and not (clip.is_mask):
clip = clip.with_mask()
if self.margin_size is not None:
self.left = self.right = self.top = self.bottom = self.margin_size
def make_bg(w, h):
new_w, new_h = w + self.left + self.right, h + self.top + self.bottom
if clip.is_mask:
shape = (new_h, new_w)
bg = np.tile(self.opacity, (new_h, new_w)).astype(float).reshape(shape)
else:
shape = (new_h, new_w, 3)
bg = np.tile(self.color, (new_h, new_w)).reshape(shape)
return bg
if isinstance(clip, ImageClip):
im = make_bg(clip.w, clip.h)
im[self.top : self.top + clip.h, self.left : self.left + clip.w] = clip.img
return clip.image_transform(lambda pic: im)
else:
def filter(get_frame, t):
pic = get_frame(t)
h, w = pic.shape[:2]
im = make_bg(w, h)
im[self.top : self.top + h, self.left : self.left + w] = pic
return im
return clip.transform(filter)
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
# We apply once on clip and once on mask if we have one
clip = self.add_margin(clip=clip)
if clip.mask:
clip.mask = self.add_margin(clip=clip.mask)
return clip

View File

@@ -0,0 +1,45 @@
from dataclasses import dataclass
import numpy as np
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class MaskColor(Effect):
"""Returns a new clip with a mask for transparency where the original
clip is of the given color.
You can also have a "progressive" mask by specifying a non-null distance
threshold ``threshold``. In this case, if the distance between a pixel and
the given color is d, the transparency will be
d**stiffness / (threshold**stiffness + d**stiffness)
which is 1 when d>>threshold and 0 for d<<threshold, the stiffness of the
effect being parametrized by ``stiffness``
"""
color: tuple = (0, 0, 0)
threshold: float = 0
stiffness: float = 1
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
color = np.array(self.color)
def hill(x):
if self.threshold:
return x**self.stiffness / (
self.threshold**self.stiffness + x**self.stiffness
)
else:
return 1.0 * (x != 0)
def flim(im):
return hill(np.sqrt(((im - color) ** 2).sum(axis=2)))
mask = clip.image_transform(flim)
mask.is_mask = True
return clip.with_mask(mask)

View File

@@ -0,0 +1,52 @@
from dataclasses import dataclass
from typing import Union
import numpy as np
from moviepy.Clip import Clip
from moviepy.Effect import Effect
from moviepy.video.VideoClip import ImageClip
@dataclass
class MasksAnd(Effect):
"""Returns the logical 'and' (minimum pixel color values) between two masks.
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
--------
.. code:: python
clip = ColorClip(color=(255, 0, 0), size=(1, 1)) # red
mask = ColorClip(color=(0, 255, 0), size=(1, 1)) # green
masked_clip = clip.with_effects([vfx.MasksAnd(mask)]) # black
masked_clip.get_frame(0)
[[[0 0 0]]]
"""
other_clip: Union[Clip, np.ndarray]
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
# to ensure that 'and' of two ImageClips will be an ImageClip
if isinstance(self.other_clip, ImageClip):
self.other_clip = self.other_clip.img
if isinstance(self.other_clip, np.ndarray):
return clip.image_transform(
lambda frame: np.minimum(frame, self.other_clip)
)
else:
return clip.transform(
lambda get_frame, t: np.minimum(
get_frame(t), self.other_clip.get_frame(t)
)
)

View File

@@ -0,0 +1,52 @@
from dataclasses import dataclass
from typing import Union
import numpy as np
from moviepy.Clip import Clip
from moviepy.Effect import Effect
from moviepy.video.VideoClip import ImageClip
@dataclass
class MasksOr(Effect):
"""Returns the logical 'or' (maximum pixel color values) between two masks.
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
--------
.. code:: python
clip = ColorClip(color=(255, 0, 0), size=(1, 1)) # red
mask = ColorClip(color=(0, 255, 0), size=(1, 1)) # green
masked_clip = clip.with_effects([vfx.MasksOr(mask)]) # yellow
masked_clip.get_frame(0)
[[[255 255 0]]]
"""
other_clip: Union[Clip, np.ndarray]
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
# to ensure that 'or' of two ImageClips will be an ImageClip
if isinstance(self.other_clip, ImageClip):
self.other_clip = self.other_clip.img
if isinstance(self.other_clip, np.ndarray):
return clip.image_transform(
lambda frame: np.maximum(frame, self.other_clip)
)
else:
return clip.transform(
lambda get_frame, t: np.maximum(
get_frame(t), self.other_clip.get_frame(t)
)
)

View File

@@ -0,0 +1,16 @@
from dataclasses import dataclass
from typing import List, Union
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class MirrorX(Effect):
"""Flips the clip horizontally (and its mask too, by default)."""
apply_to: Union[List, str] = "mask"
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
return clip.image_transform(lambda img: img[:, ::-1], apply_to=self.apply_to)

View File

@@ -0,0 +1,16 @@
from dataclasses import dataclass
from typing import List, Union
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class MirrorY(Effect):
"""Flips the clip vertically (and its mask too, by default)."""
apply_to: Union[List, str] = "mask"
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
return clip.image_transform(lambda img: img[::-1], apply_to=self.apply_to)

View File

@@ -0,0 +1,23 @@
from dataclasses import dataclass
import numpy as np
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class MultiplyColor(Effect):
"""
Multiplies the clip's colors by the given factor, can be used
to decrease or increase the clip's brightness (is that the
right word ?)
"""
factor: float
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
return clip.image_transform(
lambda frame: np.minimum(255, (self.factor * frame)).astype("uint8")
)

View File

@@ -0,0 +1,31 @@
from dataclasses import dataclass
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class MultiplySpeed(Effect):
"""Returns a clip playing the current clip but at a speed multiplied by ``factor``.
Instead of factor one can indicate the desired ``final_duration`` of the clip, and
the factor will be automatically computed. The same effect is applied to the clip's
audio and mask if any.
"""
factor: float = None
final_duration: float = None
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
if self.final_duration:
self.factor = 1.0 * clip.duration / self.final_duration
new_clip = clip.time_transform(
lambda t: self.factor * t, apply_to=["mask", "audio"]
)
if clip.duration is not None:
new_clip = new_clip.with_duration(1.0 * clip.duration / self.factor)
return new_clip

View File

@@ -0,0 +1,63 @@
from dataclasses import dataclass
import numpy as np
from PIL import Image, ImageFilter
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class Painting(Effect):
"""Transforms any photo into some kind of painting.
Transforms any photo into some kind of painting. Saturation
tells at which point the colors of the result should be
flashy. ``black`` gives the amount of black lines wanted.
np_image : a numpy image
"""
saturation: float = 1.4
black: float = 0.006
def to_painting(self, np_image, saturation=1.4, black=0.006):
"""Transforms any photo into some kind of painting.
Transforms any photo into some kind of painting. Saturation
tells at which point the colors of the result should be
flashy. ``black`` gives the amount of black lines wanted.
np_image : a numpy image
"""
image = Image.fromarray(np_image)
image = image.filter(ImageFilter.EDGE_ENHANCE_MORE)
# Convert the image to grayscale
grayscale_image = image.convert("L")
# Find the image edges
edges_image = grayscale_image.filter(ImageFilter.FIND_EDGES)
# Convert the edges image to a numpy array
edges = np.array(edges_image)
# Create the darkening effect
darkening = black * (255 * np.dstack(3 * [edges]))
# Apply the painting effect
painting = saturation * np.array(image) - darkening
# Clip the pixel values to the valid range of 0-255
painting = np.maximum(0, np.minimum(255, painting))
# Convert the pixel values to unsigned 8-bit integers
painting = painting.astype("uint8")
return painting
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
return clip.image_transform(
lambda im: self.to_painting(im, self.saturation, self.black)
)

158
moviepy/video/fx/Resize.py Normal file
View File

@@ -0,0 +1,158 @@
import numbers
from dataclasses import dataclass
from typing import Union
import numpy as np
from PIL import Image
from moviepy.Effect import Effect
@dataclass
class Resize(Effect):
"""Effect returning a video clip that is a resized version of the clip.
Parameters
----------
new_size : tuple or float or function, optional
Can be either
- ``(width, height)`` in pixels or a float representing
- A scaling factor, like ``0.5``.
- A function of time returning one of these.
height : int, optional
Height of the new clip in pixels. The width is then computed so
that the width/height ratio is conserved.
width : int, optional
Width of the new clip in pixels. The height is then computed so
that the width/height ratio is conserved.
Examples
--------
.. code:: python
clip.with_effects([vfx.Resize((460,720))]) # New resolution: (460,720)
clip.with_effects([vfx.Resize(0.6)]) # width and height multiplied by 0.6
clip.with_effects([vfx.Resize(width=800)]) # height computed automatically.
clip.with_effects([vfx.Resize(lambda t : 1+0.02*t)]) # slow clip swelling
"""
new_size: Union[tuple, float, callable] = None
height: int = None
width: int = None
apply_to_mask: bool = True
def resizer(self, pic, new_size):
"""Resize the image using PIL."""
new_size = list(map(int, new_size))
pil_img = Image.fromarray(pic)
resized_pil = pil_img.resize(new_size, Image.Resampling.LANCZOS)
return np.array(resized_pil)
def apply(self, clip):
"""Apply the effect to the clip."""
w, h = clip.size
if self.new_size is not None:
def translate_new_size(new_size_):
"""Returns a [w, h] pair from `new_size_`. If `new_size_` is a
scalar, then work out the correct pair using the clip's size.
Otherwise just return `new_size_`
"""
if isinstance(new_size_, numbers.Number):
return [new_size_ * w, new_size_ * h]
else:
return new_size_
if hasattr(self.new_size, "__call__"):
# The resizing is a function of time
def get_new_size(t):
return translate_new_size(self.new_size(t))
if clip.is_mask:
def filter(get_frame, t):
return (
self.resizer(
(255 * get_frame(t)).astype("uint8"), get_new_size(t)
)
/ 255.0
)
else:
def filter(get_frame, t):
return self.resizer(
get_frame(t).astype("uint8"), get_new_size(t)
)
newclip = clip.transform(
filter,
keep_duration=True,
apply_to=(["mask"] if self.apply_to_mask else []),
)
if self.apply_to_mask and clip.mask is not None:
newclip.mask = clip.mask.with_effects(
[Resize(self.new_size, apply_to_mask=False)]
)
return newclip
else:
self.new_size = translate_new_size(self.new_size)
elif self.height is not None:
if hasattr(self.height, "__call__"):
def func(t):
return 1.0 * int(self.height(t)) / h
return clip.with_effects([Resize(func)])
else:
self.new_size = [w * self.height / h, self.height]
elif self.width is not None:
if hasattr(self.width, "__call__"):
def func(t):
return 1.0 * self.width(t) / w
return clip.with_effects([Resize(func)])
else:
self.new_size = [self.width, h * self.width / w]
else:
raise ValueError(
"You must provide either 'new_size' or 'height' or 'width'"
)
# From here, the resizing is constant (not a function of time), size=newsize
if clip.is_mask:
def image_filter(pic):
return (
1.0
* self.resizer((255 * pic).astype("uint8"), self.new_size)
/ 255.0
)
else:
def image_filter(pic):
return self.resizer(pic.astype("uint8"), self.new_size)
new_clip = clip.image_transform(image_filter)
if self.apply_to_mask and clip.mask is not None:
new_clip.mask = clip.mask.with_effects(
[Resize(self.new_size, apply_to_mask=False)]
)
return new_clip

128
moviepy/video/fx/Rotate.py Normal file
View File

@@ -0,0 +1,128 @@
import math
from dataclasses import dataclass
import numpy as np
from PIL import Image
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class Rotate(Effect):
"""
Rotates the specified clip by ``angle`` degrees (or radians) anticlockwise
If the angle is not a multiple of 90 (degrees) or ``center``, ``translate``,
and ``bg_color`` are not ``None``, there will be black borders.
You can make them transparent with:
>>> new_clip = clip.with_mask().rotate(72)
Parameters
----------
clip : VideoClip
A video clip.
angle : float
Either a value or a function angle(t) representing the angle of rotation.
unit : str, optional
Unit of parameter `angle` (either "deg" for degrees or "rad" for radians).
resample : str, optional
An optional resampling filter. One of "nearest", "bilinear", or "bicubic".
expand : bool, optional
If true, expands the output image to make it large enough to hold the
entire rotated image. If false or omitted, make the output image the same
size as the input image.
translate : tuple, optional
An optional post-rotate translation (a 2-tuple).
center : tuple, optional
Optional center of rotation (a 2-tuple). Origin is the upper left corner.
bg_color : tuple, optional
An optional color for area outside the rotated image. Only has effect if
``expand`` is true.
"""
angle: float
unit: str = "deg"
resample: str = "bicubic"
expand: bool = True
center: tuple = None
translate: tuple = None
bg_color: tuple = None
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
try:
resample = {
"bilinear": Image.BILINEAR,
"nearest": Image.NEAREST,
"bicubic": Image.BICUBIC,
}[self.resample]
except KeyError:
raise ValueError(
"'resample' argument must be either 'bilinear', 'nearest' or 'bicubic'"
)
if hasattr(self.angle, "__call__"):
get_angle = self.angle
else:
get_angle = lambda t: self.angle
def filter(get_frame, t):
angle = get_angle(t)
im = get_frame(t)
if self.unit == "rad":
angle = math.degrees(angle)
angle %= 360
if not self.center and not self.translate and not self.bg_color:
if (angle == 0) and self.expand:
return im
if (angle == 90) and self.expand:
transpose = [1, 0] if len(im.shape) == 2 else [1, 0, 2]
return np.transpose(im, axes=transpose)[::-1]
elif (angle == 270) and self.expand:
transpose = [1, 0] if len(im.shape) == 2 else [1, 0, 2]
return np.transpose(im, axes=transpose)[:, ::-1]
elif (angle == 180) and self.expand:
return im[::-1, ::-1]
pillow_kwargs = {}
if self.bg_color is not None:
pillow_kwargs["fillcolor"] = self.bg_color
if self.center is not None:
pillow_kwargs["center"] = self.center
if self.translate is not None:
pillow_kwargs["translate"] = self.translate
# PIL expects uint8 type data. However a mask image has values in the
# range [0, 1] and is of float type. To handle this we scale it up by
# a factor 'a' for use with PIL and then back again by 'a' afterwards.
if im.dtype == "float64":
# this is a mask image
a = 255.0
else:
a = 1
# call PIL.rotate
return (
np.array(
Image.fromarray(np.array(a * im).astype(np.uint8)).rotate(
angle, expand=self.expand, resample=resample, **pillow_kwargs
)
)
/ a
)
return clip.transform(filter, apply_to=["mask"])

View File

@@ -0,0 +1,57 @@
from moviepy.Effect import Effect
class Scroll(Effect):
"""Effect that scrolls horizontally or vertically a clip, e.g. to make end credits
Parameters
----------
w, h
The width and height of the final clip. Default to clip.w and clip.h
x_speed, y_speed
The speed of the scroll in the x and y directions.
x_start, y_start
The starting position of the scroll in the x and y directions.
apply_to
Whether to apply the effect to the mask too.
"""
def __init__(
self,
w=None,
h=None,
x_speed=0,
y_speed=0,
x_start=0,
y_start=0,
apply_to="mask",
):
self.w = w
self.h = h
self.x_speed = x_speed
self.y_speed = y_speed
self.x_start = x_start
self.y_start = y_start
self.apply_to = apply_to
def apply(self, clip):
"""Apply the effect to the clip."""
if self.h is None:
self.h = clip.h
if self.w is None:
self.w = clip.w
x_max = self.w - 1
y_max = self.h - 1
def filter(get_frame, t):
x = int(max(0, min(x_max, self.x_start + round(self.x_speed * t))))
y = int(max(0, min(y_max, self.y_start + round(self.y_speed * t))))
return get_frame(t)[y : y + self.h, x : x + self.w]
return clip.transform(filter, apply_to=self.apply_to)

View File

@@ -0,0 +1,60 @@
from dataclasses import dataclass
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class SlideIn(Effect):
"""Makes the clip arrive from one side of the screen.
Only works when the clip is included in a CompositeVideoClip,
and if the clip has the same size as the whole composition.
Parameters
----------
clip : moviepy.Clip.Clip
A video clip.
duration : float
Time taken for the clip to be fully visible
side : str
Side of the screen where the clip comes from. One of
'top', 'bottom', 'left' or 'right'.
Examples
--------
.. code:: python
from moviepy import *
clips = [... make a list of clips]
slided_clips = [
CompositeVideoClip([clip.with_effects([vfx.SlideIn(1, "left")])])
for clip in clips
]
final_clip = concatenate_videoclips(slided_clips, padding=-1)
clip = ColorClip(
color=(255, 0, 0), duration=1, size=(300, 300)
).with_fps(60)
final_clip = CompositeVideoClip([clip.with_effects([vfx.SlideIn(1, "right")])])
"""
duration: float
side: str
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
w, h = clip.size
pos_dict = {
"left": lambda t: (min(0, w * (t / self.duration - 1)), "center"),
"right": lambda t: (max(0, w * (1 - t / self.duration)), "center"),
"top": lambda t: ("center", min(0, h * (t / self.duration - 1))),
"bottom": lambda t: ("center", max(0, h * (1 - t / self.duration))),
}
return clip.with_position(pos_dict[self.side])

View File

@@ -0,0 +1,64 @@
from dataclasses import dataclass
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class SlideOut(Effect):
"""Makes the clip goes away by one side of the screen.
Only works when the clip is included in a CompositeVideoClip,
and if the clip has the same size as the whole composition.
Parameters
----------
clip : moviepy.Clip.Clip
A video clip.
duration : float
Time taken for the clip to be fully visible
side : str
Side of the screen where the clip goes. One of
'top', 'bottom', 'left' or 'right'.
Examples
--------
.. code:: python
from moviepy import *
clips = [... make a list of clips]
slided_clips = [
CompositeVideoClip([clip.with_effects([vfx.SlideOut(1, "left")])])
for clip in clips
]
final_clip = concatenate_videoclips(slided_clips, padding=-1)
clip = ColorClip(
color=(255, 0, 0), duration=1, size=(300, 300)
).with_fps(60)
final_clip = CompositeVideoClip([clip.with_effects([vfx.SlideOut(1, "right")])])
"""
duration: float
side: str
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
if clip.duration is None:
raise ValueError("Attribute 'duration' not set")
w, h = clip.size
ts = clip.duration - self.duration # start time of the effect.
pos_dict = {
"left": lambda t: (min(0, w * (-(t - ts) / self.duration)), "center"),
"right": lambda t: (max(0, w * ((t - ts) / self.duration)), "center"),
"top": lambda t: ("center", min(0, h * (-(t - ts) / self.duration))),
"bottom": lambda t: ("center", max(0, h * ((t - ts) / self.duration))),
}
return clip.with_position(pos_dict[self.side])

View File

@@ -0,0 +1,29 @@
from dataclasses import dataclass
import numpy as np
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class SuperSample(Effect):
"""Replaces each frame at time t by the mean of `n_frames` equally spaced frames
taken in the interval [t-d, t+d]. This results in motion blur.
"""
d: float
n_frames: int
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
def filter(get_frame, t):
timings = np.linspace(t - self.d, t + self.d, self.n_frames)
frame_average = np.mean(
1.0 * np.array([get_frame(t_) for t_ in timings], dtype="uint16"),
axis=0,
)
return frame_average.astype("uint8")
return clip.transform(filter)

View File

@@ -0,0 +1,20 @@
from dataclasses import dataclass
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class TimeMirror(Effect):
"""
Returns a clip that plays the current clip backwards.
The clip must have its ``duration`` attribute set.
The same effect is applied to the clip's audio and mask if any.
"""
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
if clip.duration is None:
raise ValueError("Attribute 'duration' not set")
return clip[::-1]

View File

@@ -0,0 +1,22 @@
from dataclasses import dataclass
from moviepy.Clip import Clip
from moviepy.Effect import Effect
@dataclass
class TimeSymmetrize(Effect):
"""
Returns a clip that plays the current clip once forwards and
then once backwards. This is very practival to make video that
loop well, e.g. to create animated GIFs.
This effect is automatically applied to the clip's mask and audio
if they exist.
"""
def apply(self, clip: Clip) -> Clip:
"""Apply the effect to the clip."""
if clip.duration is None:
raise ValueError("Attribute 'duration' not set")
return clip + clip[::-1]

View File

@@ -0,0 +1,76 @@
"""All the visual effects that can be applied to VideoClip."""
# import every video fx function
from moviepy.video.fx.AccelDecel import AccelDecel
from moviepy.video.fx.BlackAndWhite import BlackAndWhite
from moviepy.video.fx.Blink import Blink
from moviepy.video.fx.Crop import Crop
from moviepy.video.fx.CrossFadeIn import CrossFadeIn
from moviepy.video.fx.CrossFadeOut import CrossFadeOut
from moviepy.video.fx.EvenSize import EvenSize
from moviepy.video.fx.FadeIn import FadeIn
from moviepy.video.fx.FadeOut import FadeOut
from moviepy.video.fx.Freeze import Freeze
from moviepy.video.fx.FreezeRegion import FreezeRegion
from moviepy.video.fx.GammaCorrection import GammaCorrection
from moviepy.video.fx.HeadBlur import HeadBlur
from moviepy.video.fx.InvertColors import InvertColors
from moviepy.video.fx.Loop import Loop
from moviepy.video.fx.LumContrast import LumContrast
from moviepy.video.fx.MakeLoopable import MakeLoopable
from moviepy.video.fx.Margin import Margin
from moviepy.video.fx.MaskColor import MaskColor
from moviepy.video.fx.MasksAnd import MasksAnd
from moviepy.video.fx.MasksOr import MasksOr
from moviepy.video.fx.MirrorX import MirrorX
from moviepy.video.fx.MirrorY import MirrorY
from moviepy.video.fx.MultiplyColor import MultiplyColor
from moviepy.video.fx.MultiplySpeed import MultiplySpeed
from moviepy.video.fx.Painting import Painting
from moviepy.video.fx.Resize import Resize
from moviepy.video.fx.Rotate import Rotate
from moviepy.video.fx.Scroll import Scroll
from moviepy.video.fx.SlideIn import SlideIn
from moviepy.video.fx.SlideOut import SlideOut
from moviepy.video.fx.SuperSample import SuperSample
from moviepy.video.fx.TimeMirror import TimeMirror
from moviepy.video.fx.TimeSymmetrize import TimeSymmetrize
__all__ = (
"AccelDecel",
"BlackAndWhite",
"Blink",
"Crop",
"CrossFadeIn",
"CrossFadeOut",
"EvenSize",
"FadeIn",
"FadeOut",
"Freeze",
"FreezeRegion",
"GammaCorrection",
"HeadBlur",
"InvertColors",
"Loop",
"LumContrast",
"MakeLoopable",
"Margin",
"MasksAnd",
"MaskColor",
"MasksOr",
"MirrorX",
"MirrorY",
"MultiplyColor",
"MultiplySpeed",
"Painting",
"Resize",
"Rotate",
"Scroll",
"SlideIn",
"SlideOut",
"SuperSample",
"TimeMirror",
"TimeSymmetrize",
)