diff --git a/moviepy/Clip.py b/moviepy/Clip.py
index da22c72..a997734 100644
--- a/moviepy/Clip.py
+++ b/moviepy/Clip.py
@@ -40,6 +40,7 @@ class Clip:
self.memoize = False
self.memoized_t = None
self.memoize_frame = None
+ self.make_frame = None
def copy(self):
""" Shallow copy of the clip.
@@ -51,7 +52,12 @@ class Clip:
there is an outplace transformation of the clip (clip.resize,
clip.subclip, etc.)
"""
- pass
+ newclip = copy(self)
+ if hasattr(self, 'audio'):
+ newclip.audio = copy(self.audio)
+ if hasattr(self, 'mask'):
+ newclip.mask = copy(self.mask)
+ return newclip
@convert_to_seconds(['t'])
def get_frame(self, t):
@@ -59,7 +65,19 @@ class Clip:
Gets a numpy array representing the RGB picture of the clip at time t
or (mono or stereo) value for a sound clip
"""
- pass
+ if self.make_frame is None:
+ raise ValueError("No make_frame attribute in this clip.")
+
+ if self.memoize:
+ if t == self.memoized_t:
+ return self.memoized_frame
+ else:
+ frame = self.make_frame(t)
+ self.memoized_t = t
+ self.memoized_frame = frame
+ return frame
+ else:
+ return self.make_frame(t)
def fl(self, fun, apply_to=None, keep_duration=True):
""" General processing of a clip.
@@ -98,7 +116,31 @@ class Clip:
>>> newclip = clip.fl(fl, apply_to='mask')
"""
- pass
+ newclip = self.copy()
+
+ if not hasattr(newclip, 'make_frame'):
+ newclip.make_frame = None
+
+ def make_frame(t):
+ return fun(self.get_frame, t)
+
+ newclip.make_frame = make_frame
+
+ if not keep_duration:
+ newclip.duration = None
+
+ if apply_to is not None:
+ if isinstance(apply_to, str):
+ apply_to = [apply_to]
+
+ for attr in apply_to:
+ if hasattr(newclip, attr):
+ a = getattr(newclip, attr)
+ if a is not None:
+ new_a = a.fl(fun, keep_duration=keep_duration)
+ setattr(newclip, attr, new_a)
+
+ return newclip
def fl_time(self, t_func, apply_to=None, keep_duration=False):
"""
@@ -131,7 +173,7 @@ class Clip:
>>> newclip = clip.fl_time(lambda: 3-t)
"""
- pass
+ return self.fl(lambda gf, t: gf(t_func(t)), apply_to, keep_duration)
def fx(self, func, *args, **kwargs):
"""
@@ -154,7 +196,7 @@ class Clip:
>>> resize( volumex( mirrorx( clip ), 0.5), 0.3)
"""
- pass
+ return func(self, *args, **kwargs)
@apply_to_mask
@apply_to_audio
@@ -178,7 +220,11 @@ class Clip:
These changes are also applied to the ``audio`` and ``mask``
clips of the current clip, if they exist.
"""
- pass
+ self.start = t
+ if (self.duration is not None) and change_end:
+ self.end = t + self.duration
+ elif (self.end is not None) and not change_end:
+ self.duration = self.end - self.start
@apply_to_mask
@apply_to_audio
@@ -192,7 +238,12 @@ class Clip:
Also sets the duration of the mask and audio, if any,
of the returned clip.
"""
- pass
+ self.end = t
+ if self.start is None:
+ if self.duration is not None:
+ self.start = t - self.duration
+ else:
+ self.duration = self.end - self.start
@apply_to_mask
@apply_to_audio
@@ -209,7 +260,13 @@ class Clip:
be modified in function of the duration and the preset end
of the clip.
"""
- pass
+ self.duration = t
+ if change_end:
+ self.end = None if (t is None) else (self.start + t)
+ else:
+ if self.end is None:
+ raise ValueError("Can't change start of clip with undefined end.")
+ self.start = self.end - t
@outplace
def set_make_frame(self, make_frame):
@@ -217,23 +274,26 @@ class Clip:
Sets a ``make_frame`` attribute for the clip. Useful for setting
arbitrary/complicated videoclips.
"""
- pass
+ self.make_frame = make_frame
@outplace
def set_fps(self, fps):
""" Returns a copy of the clip with a new default fps for functions like
write_videofile, iterframe, etc. """
- pass
+ self.fps = fps
@outplace
def set_ismask(self, ismask):
""" Says wheter the clip is a mask or not (ismask is a boolean)"""
- pass
+ self.ismask = ismask
@outplace
def set_memoize(self, memoize):
""" Sets wheter the clip should keep the last frame read in memory """
- pass
+ self.memoize = memoize
+ if not memoize:
+ self.memoized_t = None
+ self.memoized_frame = None
@convert_to_seconds(['t'])
def is_playing(self, t):
@@ -246,7 +306,24 @@ class Clip:
theclip, else returns a vector [b_1, b_2, b_3...] where b_i
is true iff tti is in the clip.
"""
- pass
+ if isinstance(t, np.ndarray):
+ # Array case
+ tmin, tmax = t.min(), t.max()
+
+ if (self.end is not None) and (tmin >= self.end):
+ return False
+
+ if tmax < self.start:
+ return False
+
+ # All times are in the clip
+ result = (t >= self.start)
+ if self.end is not None:
+ result = result & (t <= self.end)
+ return result
+ else:
+ # Single time case
+ return (t >= self.start) and ((self.end is None) or (t <= self.end))
@convert_to_seconds(['t_start', 't_end'])
@apply_to_mask
@@ -272,7 +349,30 @@ class Clip:
subclips of ``mask`` and ``audio`` the original clip, if
they exist.
"""
- pass
+ if t_start < 0:
+ # Make this more intuitive
+ if self.duration is None:
+ raise ValueError("Subclip with negative times can only be"
+ " extracted from clips with a duration")
+ t_start = self.duration + t_start
+
+ if t_end is None:
+ t_end = self.duration
+ elif t_end < 0:
+ if self.duration is None:
+ raise ValueError("Subclip with negative times can only be"
+ " extracted from clips with a duration")
+ t_end = self.duration + t_end
+
+ newclip = self.copy()
+
+ if t_start is None:
+ t_start = 0
+ newclip.start = t_start
+ newclip.end = t_end
+ newclip.duration = t_end - t_start
+
+ return newclip
@apply_to_mask
@apply_to_audio
@@ -290,7 +390,22 @@ class Clip:
The resulting clip's ``audio`` and ``mask`` will also be cutout
if they exist.
"""
- pass
+ newclip = self.copy()
+ if tb is None:
+ tb = self.duration
+
+ def make_frame(t):
+ if t < ta:
+ return self.get_frame(t)
+ else:
+ return self.get_frame(t + (tb - ta))
+
+ newclip.make_frame = make_frame
+
+ if self.duration is not None:
+ newclip.duration = self.duration - (tb - ta)
+
+ return newclip
@requires_duration
@use_clip_fps_by_default
@@ -319,13 +434,50 @@ class Clip:
>>> print ( [frame[0,:,0].max()
for frame in myclip.iter_frames()])
"""
- pass
+ logger = proglog.default_bar_logger(logger)
+
+ if fps is None:
+ fps = self.fps
+
+ if fps is None:
+ raise ValueError("No fps attribute specified")
+
+ # Compute the total number of frames
+ total_frames = int(self.duration * fps)
+ times = np.linspace(0, self.duration, total_frames + 1)[:-1]
+
+ if dtype is not None:
+ def get_frame(t):
+ frame = self.get_frame(t)
+ if dtype != frame.dtype:
+ return frame.astype(dtype)
+ return frame
+ else:
+ get_frame = self.get_frame
+
+ logger.new_bar_segment(total_frames)
+
+ for i, t in enumerate(times):
+ logger.bar_update(i)
+ frame = get_frame(t)
+ if with_times:
+ yield t, frame
+ else:
+ yield frame
+
+ logger.bar_close()
def close(self):
"""
Release any resources that are in use.
"""
- pass
+ if hasattr(self, 'audio') and self.audio is not None:
+ self.audio.close()
+ if hasattr(self, 'mask') and self.mask is not None:
+ self.mask.close()
+ if hasattr(self, 'make_frame'):
+ self.make_frame = None
+ self.memoized_frame = None
def __enter__(self):
return self
diff --git a/moviepy/config.py b/moviepy/config.py
index 985e1ff..100fcb1 100644
--- a/moviepy/config.py
+++ b/moviepy/config.py
@@ -2,6 +2,16 @@ import os
import subprocess as sp
from .compat import DEVNULL
from .config_defaults import FFMPEG_BINARY, IMAGEMAGICK_BINARY
+
+def try_cmd(cmd):
+ """Try to execute a command and return (success, stderr output)."""
+ try:
+ process = sp.Popen(cmd, stdout=DEVNULL, stderr=sp.PIPE)
+ _, error = process.communicate()
+ success = process.returncode == 0
+ return success, error
+ except Exception as e:
+ return False, str(e)
if os.name == 'nt':
try:
import winreg as wr
@@ -44,11 +54,30 @@ else:
def get_setting(varname):
""" Returns the value of a configuration variable. """
- pass
+ if varname == "FFMPEG_BINARY":
+ return FFMPEG_BINARY
+ elif varname == "IMAGEMAGICK_BINARY":
+ return IMAGEMAGICK_BINARY
+ else:
+ raise ValueError(f"Unknown setting {varname}")
def change_settings(new_settings=None, filename=None):
""" Changes the value of configuration variables."""
- pass
+ global FFMPEG_BINARY, IMAGEMAGICK_BINARY
+
+ if new_settings is None:
+ new_settings = {}
+
+ if filename is not None:
+ # Load settings from file
+ with open(filename) as f:
+ exec(f.read(), globals())
+
+ # Update with new settings
+ if "FFMPEG_BINARY" in new_settings:
+ FFMPEG_BINARY = new_settings["FFMPEG_BINARY"]
+ if "IMAGEMAGICK_BINARY" in new_settings:
+ IMAGEMAGICK_BINARY = new_settings["IMAGEMAGICK_BINARY"]
if __name__ == '__main__':
if try_cmd([FFMPEG_BINARY])[0]:
print('MoviePy : ffmpeg successfully found.')
diff --git a/moviepy/decorators.py b/moviepy/decorators.py
index d7484af..6c47edb 100644
--- a/moviepy/decorators.py
+++ b/moviepy/decorators.py
@@ -7,29 +7,42 @@ from moviepy.tools import cvsecs
@decorator.decorator
def outplace(f, clip, *a, **k):
""" Applies f(clip.copy(), *a, **k) and returns clip.copy()"""
- pass
+ newclip = clip.copy()
+ f(newclip, *a, **k)
+ return newclip
@decorator.decorator
def convert_masks_to_RGB(f, clip, *a, **k):
""" If the clip is a mask, convert it to RGB before running the function """
- pass
+ if clip.ismask:
+ clip = clip.rgb_mode()
+ return f(clip, *a, **k)
@decorator.decorator
def apply_to_mask(f, clip, *a, **k):
""" This decorator will apply the same function f to the mask of
the clip created with f """
- pass
+ newclip = f(clip, *a, **k)
+ if getattr(clip, 'mask', None) is not None:
+ newclip.mask = f(clip.mask, *a, **k)
+ return newclip
@decorator.decorator
def apply_to_audio(f, clip, *a, **k):
""" This decorator will apply the function f to the audio of
the clip created with f """
- pass
+ newclip = f(clip, *a, **k)
+ if getattr(clip, 'audio', None) is not None:
+ newclip.audio = f(clip.audio, *a, **k)
+ return newclip
@decorator.decorator
def requires_duration(f, clip, *a, **k):
""" Raise an error if the clip has no duration."""
- pass
+ if clip.duration is None:
+ raise ValueError("Attribute 'duration' not set")
+ else:
+ return f(clip, *a, **k)
@decorator.decorator
def audio_video_fx(f, clip, *a, **k):
@@ -39,22 +52,44 @@ def audio_video_fx(f, clip, *a, **k):
can be also used on a video clip, at which case it returns a
videoclip with unmodified video and modified audio.
"""
- pass
+ if hasattr(clip, 'audio'):
+ newclip = clip.copy()
+ if clip.audio is not None:
+ newclip.audio = f(clip.audio, *a, **k)
+ return newclip
+ else:
+ return f(clip, *a, **k)
def preprocess_args(fun, varnames):
""" Applies fun to variables in varnames before launching the function """
- pass
+ def wrapper(f, *a, **kw):
+ if hasattr(f, '__code__'):
+ names = f.__code__.co_varnames
+ else:
+ names = getattr(f, 'func', f).__code__.co_varnames
+
+ new_a = [fun(arg) if (name in varnames) and (arg is not None)
+ else arg
+ for (arg, name) in zip(a, names)]
+ new_kw = {k: fun(v) if k in varnames else v
+ for (k, v) in kw.items()}
+ return f(*new_a, **new_kw)
+ return decorator.decorator(wrapper)
def convert_to_seconds(varnames):
"""Converts the specified variables to seconds"""
- pass
+ return preprocess_args(cvsecs, varnames)
@decorator.decorator
def add_mask_if_none(f, clip, *a, **k):
""" Add a mask to the clip if there is none. """
- pass
+ if clip.mask is None:
+ clip = clip.add_mask()
+ return f(clip, *a, **k)
@decorator.decorator
def use_clip_fps_by_default(f, clip, *a, **k):
""" Will use clip.fps if no fps=... is provided in **k """
- pass
\ No newline at end of file
+ if 'fps' not in k and hasattr(clip, 'fps') and clip.fps is not None:
+ k['fps'] = clip.fps
+ return f(clip, *a, **k)
\ No newline at end of file
diff --git a/moviepy/tools.py b/moviepy/tools.py
index 50c37e9..afda8e9 100644
--- a/moviepy/tools.py
+++ b/moviepy/tools.py
@@ -10,23 +10,54 @@ from .compat import DEVNULL
def sys_write_flush(s):
""" Writes and flushes without delay a text in the console """
- pass
+ if isinstance(s, bytes):
+ sys.stdout.buffer.write(s)
+ else:
+ sys.stdout.write(s)
+ sys.stdout.flush()
def verbose_print(verbose, s):
""" Only prints s (with sys_write_flush) if verbose is True."""
- pass
+ if verbose:
+ sys_write_flush(s)
def subprocess_call(cmd, logger='bar', errorprint=True):
""" Executes the given subprocess command.
Set logger to None or a custom Proglog logger to avoid printings.
"""
- pass
+ if logger == 'bar':
+ logger = proglog.default_bar_logger('bar')
+
+ try:
+ popen_params = {
+ "stdout": DEVNULL,
+ "stderr": sp.PIPE,
+ "stdin": DEVNULL
+ }
+
+ proc = sp.Popen(cmd, **popen_params)
+
+ out, err = proc.communicate()
+
+ if proc.returncode:
+ if errorprint and err is not None:
+ sys_write_flush(err.decode('utf8'))
+ raise IOError(err.decode('utf8'))
+
+ return proc
+ except Exception as e:
+ if errorprint:
+ sys_write_flush("Moviepy Error: failed command:\n%s\n" % ' '.join(cmd))
+ raise IOError("Moviepy Error: failed command:\n%s\n" % ' '.join(cmd))
def is_string(obj):
""" Returns true if s is string or string-like object,
compatible with Python 2 and Python 3."""
- pass
+ try:
+ return isinstance(obj, str)
+ except Exception:
+ return False
def cvsecs(time):
""" Will convert any time into seconds.
@@ -51,7 +82,39 @@ def cvsecs(time):
>>> cvsecs('33.5') # only secs
33.5
"""
- pass
+ if time is None:
+ return None
+
+ if isinstance(time, (int, float)):
+ return float(time)
+
+ if isinstance(time, (tuple, list)):
+ if len(time) == 1:
+ return float(time[0])
+ elif len(time) == 2:
+ return float(time[0]) * 60 + float(time[1])
+ elif len(time) == 3:
+ return float(time[0]) * 3600 + float(time[1]) * 60 + float(time[2])
+
+ if isinstance(time, str):
+ # Handle comma as decimal separator
+ time = time.replace(',', '.')
+
+ if ':' not in time:
+ # Just seconds
+ return float(time)
+
+ parts = time.split(':')
+ parts = [float(p) for p in parts]
+
+ if len(parts) == 2:
+ # Minutes and seconds
+ return parts[0] * 60 + parts[1]
+ elif len(parts) == 3:
+ # Hours, minutes and seconds
+ return parts[0] * 3600 + parts[1] * 60 + parts[2]
+
+ return time
def deprecated_version_of(f, oldname, newname=None):
""" Indicates that a function is deprecated and has a new name.
@@ -78,7 +141,31 @@ def deprecated_version_of(f, oldname, newname=None):
>>>
>>> Clip.to_file = deprecated_version_of(Clip.write_file, 'to_file')
"""
- pass
+ if newname is None:
+ newname = f.__name__
+
+ warning = ("The function ``%s`` is deprecated and is kept temporarily "
+ "for backwards compatibility.\nPlease use the new name "
+ "``%s`` instead.") % (oldname, newname)
+
+ def deprecated(*args, **kwargs):
+ warnings.warn("MoviePy: " + warning, PendingDeprecationWarning)
+ return f(*args, **kwargs)
+
+ deprecated.__doc__ = warning
+
+ return deprecated
extensions_dict = {'mp4': {'type': 'video', 'codec': ['libx264', 'libmpeg4', 'aac']}, 'ogv': {'type': 'video', 'codec': ['libtheora']}, 'webm': {'type': 'video', 'codec': ['libvpx']}, 'avi': {'type': 'video'}, 'mov': {'type': 'video'}, 'ogg': {'type': 'audio', 'codec': ['libvorbis']}, 'mp3': {'type': 'audio', 'codec': ['libmp3lame']}, 'wav': {'type': 'audio', 'codec': ['pcm_s16le', 'pcm_s24le', 'pcm_s32le']}, 'm4a': {'type': 'audio', 'codec': ['libfdk_aac']}}
for ext in ['jpg', 'jpeg', 'png', 'bmp', 'tiff']:
- extensions_dict[ext] = {'type': 'image'}
\ No newline at end of file
+ extensions_dict[ext] = {'type': 'image'}
+
+def find_extension(codec):
+ """ Returns the extension associated with a codec."""
+ if codec is None:
+ raise ValueError("Codec is None")
+
+ for ext, props in extensions_dict.items():
+ if 'codec' in props and codec in props['codec']:
+ return ext
+
+ raise ValueError(f"No extension found for codec {codec}")
\ No newline at end of file
diff --git a/moviepy/utils.py b/moviepy/utils.py
index a647626..6c5ff89 100644
--- a/moviepy/utils.py
+++ b/moviepy/utils.py
@@ -1,4 +1,20 @@
from moviepy.audio.io.AudioFileClip import AudioFileClip
from moviepy.video.io.VideoFileClip import VideoFileClip
from moviepy.video.VideoClip import ImageClip
-CLIP_TYPES = {'audio': AudioFileClip, 'video': VideoFileClip, 'image': ImageClip}
\ No newline at end of file
+
+CLIP_TYPES = {'audio': AudioFileClip, 'video': VideoFileClip, 'image': ImageClip}
+
+def close_all_clips(objects=None):
+ """Closes all the clips that are passed as arguments or that exist in the global scope.
+
+ This is useful to make sure that all files are closed when leaving a session.
+ """
+ if objects is None:
+ objects = list(globals().values())
+
+ if not isinstance(objects, (list, tuple)):
+ objects = [objects]
+
+ for obj in objects:
+ if hasattr(obj, 'close'):
+ obj.close()
\ No newline at end of file
diff --git a/moviepy/video/VideoClip.py b/moviepy/video/VideoClip.py
index c6694e2..765df74 100644
--- a/moviepy/video/VideoClip.py
+++ b/moviepy/video/VideoClip.py
@@ -95,7 +95,13 @@ class VideoClip(Clip):
the alpha layer of the picture (only works with PNGs).
"""
- pass
+ im = self.get_frame(t)
+ if withmask and self.mask is not None:
+ mask = 255 * self.mask.get_frame(t)
+ im = np.dstack([im, mask]).astype('uint8')
+ else:
+ im = im.astype("uint8")
+ imsave(filename, im)
@requires_duration
@use_clip_fps_by_default
@@ -212,7 +218,48 @@ class VideoClip(Clip):
>>> clip.close()
"""
- pass
+ if codec is None:
+ name, ext = os.path.splitext(filename)
+ try:
+ codec = extensions_dict[ext[1:]]['codec'][0]
+ except:
+ raise ValueError("MoviePy couldn't find the codec associated "
+ "with the filename. Provide the 'codec' "
+ "parameter in write_videofile.")
+
+ if audio_codec is None:
+ if codec == 'libx264':
+ audio_codec = 'aac'
+ elif codec == 'libvpx':
+ audio_codec = 'libvorbis'
+ else:
+ audio_codec = 'libmp3lame'
+
+ if audio is True:
+ audio = self.audio
+
+ if audio is not None:
+ if temp_audiofile is None:
+ temp_audiofile = os.path.splitext(filename)[0] + '_temp_audio.m4a'
+
+ if not rewrite_audio and os.path.exists(temp_audiofile):
+ logger.info("Reusing existing audio file: %s" % temp_audiofile)
+ else:
+ logger.info("Writing audio in %s" % temp_audiofile)
+ audio.write_audiofile(temp_audiofile, audio_fps, audio_nbytes,
+ audio_bufsize, audio_codec, audio_bitrate,
+ write_logfile=write_logfile, verbose=verbose)
+
+ logger.info("Writing video in %s" % filename)
+
+ ffmpeg_write_video(self, filename, fps, codec, bitrate, preset,
+ write_logfile=write_logfile, audiofile=temp_audiofile,
+ verbose=verbose, threads=threads,
+ ffmpeg_params=ffmpeg_params, logger=logger)
+
+ if remove_temp and audio is not None:
+ if os.path.exists(temp_audiofile):
+ os.remove(temp_audiofile)
@requires_duration
@use_clip_fps_by_default
@@ -257,7 +304,31 @@ class VideoClip(Clip):
``ImageSequenceClip``.
"""
- pass
+ logger = proglog.default_bar_logger(logger)
+
+ # Create output directory if it doesn't exist
+ output_dir = os.path.dirname(nameformat)
+ if output_dir and not os.path.exists(output_dir):
+ os.makedirs(output_dir)
+
+ # Get total number of frames
+ total_frames = int(self.duration * fps)
+ times = np.linspace(0, self.duration, total_frames + 1)[:-1]
+
+ # Initialize list to store filenames
+ filenames = []
+
+ logger.new_bar_segment(total_frames)
+
+ for i, t in enumerate(times):
+ name = nameformat % i
+ filenames.append(name)
+ self.save_frame(name, t, withmask=withmask)
+ logger.bar_update(i)
+
+ logger.bar_close()
+
+ return filenames
@requires_duration
@convert_masks_to_RGB
@@ -312,7 +383,23 @@ class VideoClip(Clip):
>>> myClip.speedx(0.5).to_gif('myClip.gif')
"""
- pass
+ if fps is None:
+ fps = self.fps
+
+ if fps is None:
+ raise ValueError("No fps attribute specified")
+
+ if program == 'imageio':
+ write_gif_with_image_io(self, filename, fps=fps, opt=opt, loop=loop,
+ verbose=verbose, colors=colors, logger=logger)
+ elif tempfiles:
+ write_gif_with_tempfiles(self, filename, fps=fps, program=program,
+ opt=opt, fuzz=fuzz, verbose=verbose,
+ dispose=dispose, colors=colors, logger=logger)
+ else:
+ write_gif(self, filename, fps=fps, program=program, opt=opt,
+ fuzz=fuzz, verbose=verbose, dispose=dispose,
+ colors=colors, logger=logger)
def subfx(self, fx, ta=0, tb=None, **kwargs):
"""Apply a transformation to a part of the clip.
@@ -329,14 +416,26 @@ class VideoClip(Clip):
>>> newclip = clip.subapply(lambda c:c.speedx(0.5) , 3,6)
"""
- pass
+ left = None if ta == 0 else self.subclip(0, ta)
+ center = self.subclip(ta, tb).fx(fx, **kwargs)
+ right = None if tb is None else self.subclip(tb, self.duration)
+
+ clips = [c for c in [left, center, right] if c is not None]
+
+ # If there's only one clip (like when ta=0, tb=None), just return it
+ if len(clips) == 1:
+ return clips[0]
+
+ # Concatenate the clips
+ from moviepy.video.compositing.concatenate import concatenate_videoclips
+ return concatenate_videoclips(clips)
def fl_image(self, image_func, apply_to=None):
"""
Modifies the images of a clip by replacing the frame
`get_frame(t)` by another frame, `image_func(get_frame(t))`
"""
- pass
+ return self.fl(lambda gf, t: image_func(gf(t)), apply_to=apply_to)
def blit_on(self, picture, t):
"""
@@ -344,7 +443,35 @@ class VideoClip(Clip):
on the given `picture`, the position of the clip being given
by the clip's ``pos`` attribute. Meant for compositing.
"""
- pass
+ hf, wf = framesize = picture.shape[:2]
+
+ if self.ismask and picture.max() != 0:
+ return np.minimum(1, picture + self.blit_on(np.zeros(framesize), t))
+
+ picture = picture.copy()
+
+ # Get the current frame
+ img = self.get_frame(t)
+ mask = None
+ if self.mask is not None:
+ mask = self.mask.get_frame(t)
+
+ hi, wi = img.shape[:2]
+
+ # Get position of clip
+ pos = self.pos(t)
+
+ # Convert relative position to absolute position
+ if self.relative_pos:
+ pos = (pos[0] * wf, pos[1] * hf)
+
+ # Compute coordinates of clip in the final picture
+ xt, yt = int(pos[0]), int(pos[1])
+
+ # Blit the clip onto the picture
+ blit(img, picture, xt, yt, mask=mask)
+
+ return picture
def add_mask(self):
"""Add a mask VideoClip to the VideoClip.
@@ -356,7 +483,13 @@ class VideoClip(Clip):
Set ``constant_size`` to `False` for clips with moving
image size.
"""
- pass
+ if self.has_constant_size:
+ mask = ColorClip(self.size, 1.0, ismask=True)
+ return self.set_mask(mask.set_duration(self.duration))
+ else:
+ make_frame = lambda t: np.ones(self.get_frame(t).shape[:2], dtype=float)
+ mask = VideoClip(make_frame, ismask=True)
+ return self.set_mask(mask.set_duration(self.duration))
def on_color(self, size=None, color=(0, 0, 0), pos=None, col_opacity=None):
"""Place the clip on a colored background.
@@ -383,7 +516,29 @@ class VideoClip(Clip):
background.
"""
- pass
+ from moviepy.video.compositing.CompositeVideoClip import CompositeVideoClip
+
+ if size is None:
+ size = self.size
+
+ if pos is None:
+ pos = 'center'
+
+ colorclip = ColorClip(size, color)
+
+ if col_opacity is not None:
+ colorclip = colorclip.set_opacity(col_opacity)
+
+ if self.duration is not None:
+ colorclip = colorclip.set_duration(self.duration)
+
+ result = CompositeVideoClip([colorclip, self.set_position(pos)],
+ transparent=(col_opacity is not None))
+
+ if self.mask is not None:
+ return result.set_mask(self.mask)
+
+ return result
@outplace
def set_make_frame(self, mf):
@@ -392,7 +547,9 @@ class VideoClip(Clip):
Returns a copy of the VideoClip instance, with the make_frame
attribute set to `mf`.
"""
- pass
+ self.make_frame = mf
+ if hasattr(self, 'size'):
+ del self.size
@outplace
def set_audio(self, audioclip):
@@ -401,7 +558,7 @@ class VideoClip(Clip):
Returns a copy of the VideoClip instance, with the `audio`
attribute set to ``audio``, which must be an AudioClip instance.
"""
- pass
+ self.audio = audioclip
@outplace
def set_mask(self, mask):
@@ -409,7 +566,7 @@ class VideoClip(Clip):
Returns a copy of the VideoClip with the mask attribute set to
``mask``, which must be a greyscale (values in 0-1) VideoClip"""
- pass
+ self.mask = mask
@add_mask_if_none
@outplace
@@ -419,7 +576,7 @@ class VideoClip(Clip):
Returns a semi-transparent copy of the clip where the mask is
multiplied by ``op`` (any float, normally between 0 and 1).
"""
- pass
+ self.mask = self.mask.fl_image(lambda pic: op * pic)
@apply_to_mask
@outplace
@@ -447,7 +604,12 @@ class VideoClip(Clip):
>>> clip.set_position(lambda t: ('center', 50+t) )
"""
- pass
+ self.relative_pos = relative
+
+ if hasattr(pos, '__call__'):
+ self.pos = pos
+ else:
+ self.pos = lambda t: pos
@convert_to_seconds(['t'])
def to_ImageClip(self, t=0, with_mask=True, duration=None):
@@ -456,15 +618,30 @@ class VideoClip(Clip):
which can be expressed in seconds (15.35), in (min, sec),
in (hour, min, sec), or as a string: '01:03:05.35'.
"""
- pass
+ newclip = ImageClip(self.get_frame(t), ismask=self.ismask)
+ if with_mask and self.mask is not None:
+ newclip.mask = ImageClip(self.mask.get_frame(t), ismask=True)
+ if duration is not None:
+ newclip.duration = duration
+ return newclip
def to_mask(self, canal=0):
"""Return a mask a video clip made from the clip."""
- pass
+ if self.ismask:
+ return self
+ else:
+ newclip = self.fl_image(lambda pic: 1.0 * pic[:, :, canal] / 255)
+ newclip.ismask = True
+ return newclip
def to_RGB(self):
"""Return a non-mask video clip made from the mask video clip."""
- pass
+ if self.ismask:
+ newclip = self.fl_image(lambda pic: np.dstack(3 * [255 * pic]))
+ newclip.ismask = False
+ return newclip
+ else:
+ return self
@outplace
def without_audio(self):
@@ -473,7 +650,7 @@ class VideoClip(Clip):
Return a copy of the clip with audio set to None.
"""
- pass
+ self.audio = None
@outplace
def afx(self, fun, *a, **k):
@@ -482,7 +659,7 @@ class VideoClip(Clip):
Return a new clip whose audio has been transformed by ``fun``.
"""
- pass
+ self.audio = fun(self.audio, *a, **k)
class DataVideoClip(VideoClip):
"""
@@ -592,6 +769,7 @@ class ImageClip(VideoClip):
VideoClip.__init__(self, ismask=ismask, duration=duration)
if isinstance(img, string_types):
img = imread(img)
+
if len(img.shape) == 3:
if img.shape[2] == 4:
if fromalpha:
@@ -601,7 +779,26 @@ class ImageClip(VideoClip):
elif transparent:
self.mask = ImageClip(1.0 * img[:, :, 3] / 255, ismask=True)
img = img[:, :, :3]
- elif ismask:
+ elif ismask and img.shape[2] == 3:
+ img = img.mean(axis=2)
+
+ # Convert to float
+ if img.dtype != 'float':
+ img = 1.0 * img / 255
+
+ # Save the image and its size
+ self.img = img
+ self.size = self.img.shape[:2][::-1]
+ self.w, self.h = self.size
+
+ # Generate a mask if required
+ if not ismask and transparent:
+ self.mask = ImageClip(np.ones(self.size[::-1]), ismask=True)
+
+ def make_frame(t):
+ return self.img
+
+ self.make_frame = make_frame
img = 1.0 * img[:, :, 0] / 255
self.make_frame = lambda t: img
self.size = img.shape[:2][::-1]
diff --git a/moviepy/video/fx/resize.py b/moviepy/video/fx/resize.py
index 257d73f..54bb409 100644
--- a/moviepy/video/fx/resize.py
+++ b/moviepy/video/fx/resize.py
@@ -1,12 +1,23 @@
resize_possible = True
+resizer = None
+
try:
import cv2
import numpy as np
+ def cv2_resize(pic, newsize):
+ return cv2.resize(pic, tuple(map(int, newsize[::-1])))
+ resizer = cv2_resize
resizer.origin = 'cv2'
except ImportError:
try:
from PIL import Image
import numpy as np
+ def pil_resize(pic, newsize):
+ newsize = tuple(map(int, newsize[::-1]))
+ img = Image.fromarray(pic)
+ img = img.resize(newsize, Image.BILINEAR)
+ return np.array(img)
+ resizer = pil_resize
resizer.origin = 'PIL'
except ImportError:
try:
@@ -47,7 +58,50 @@ def resize(clip, newsize=None, height=None, width=None, apply_to_mask=True):
>>> myClip.resize(lambda t : 1+0.02*t) # slow swelling of the clip
"""
- pass
+ if not resize_possible:
+ raise ImportError("No module can be found for video resizing. Install either OpenCV or Pillow.")
+
+ if newsize is not None:
+ if hasattr(newsize, '__call__'):
+ def get_newsize(t):
+ ns = newsize(t)
+ if isinstance(ns, (int, float)):
+ return [ns * clip.size[0], ns * clip.size[1]]
+ else:
+ return ns
+
+ newsize_aux = get_newsize
+ else:
+ if isinstance(newsize, (int, float)):
+ newsize = [newsize * clip.size[0], newsize * clip.size[1]]
+
+ newsize_aux = lambda t: newsize
+
+ elif height is not None:
+ if hasattr(height, '__call__'):
+ newsize_aux = lambda t: [height(t) * clip.w / clip.h, height(t)]
+ else:
+ newsize_aux = lambda t: [height * clip.w / clip.h, height]
+
+ elif width is not None:
+ if hasattr(width, '__call__'):
+ newsize_aux = lambda t: [width(t), width(t) * clip.h / clip.w]
+ else:
+ newsize_aux = lambda t: [width, width * clip.h / clip.w]
+
+ else:
+ raise ValueError('No new size provided! Use newsize, height, or width.')
+
+ def transform(get_frame, t):
+ img = get_frame(t)
+ return resizer(img, newsize_aux(t))
+
+ newclip = clip.transform(transform, keep_duration=True)
+
+ if apply_to_mask and clip.mask is not None:
+ newclip.mask = resize(clip.mask, newsize, height, width, apply_to_mask=False)
+
+ return newclip
if not resize_possible:
doc = resize.__doc__
resize.__doc__ = doc
\ No newline at end of file
diff --git a/moviepy/video/tools/credits.py b/moviepy/video/tools/credits.py
index 3a97163..2bb9343 100644
--- a/moviepy/video/tools/credits.py
+++ b/moviepy/video/tools/credits.py
@@ -68,4 +68,89 @@ def credits1(creditfile, width, stretch=30, color='white', stroke_color='black',
Music Supervisor JEAN DIDIER
"""
- pass
\ No newline at end of file
+ # Parse the credit file
+ with open(creditfile) as f:
+ lines = f.readlines()
+
+ # Initialize variables
+ texts = []
+ current_job = None
+ current_names = []
+ total_height = 0
+ max_job_width = 0
+ max_name_width = 0
+
+ # Process each line
+ for line in lines:
+ line = line.strip()
+
+ # Skip comments
+ if line.startswith('#'):
+ continue
+
+ # Handle blank lines
+ if line.startswith('.blank'):
+ try:
+ n_blanks = int(line.split()[1])
+ total_height += n_blanks * fontsize
+ except:
+ total_height += fontsize
+ continue
+
+ # Handle job titles
+ if line.startswith('..'):
+ # Save previous job if exists
+ if current_job is not None:
+ job_clip = TextClip(current_job, font=font, fontsize=fontsize, color=color,
+ stroke_color=stroke_color, stroke_width=stroke_width)
+ max_job_width = max(max_job_width, job_clip.w)
+
+ for name in current_names:
+ name_clip = TextClip(name, font=font, fontsize=fontsize, color=color,
+ stroke_color=stroke_color, stroke_width=stroke_width)
+ max_name_width = max(max_name_width, name_clip.w)
+ texts.append((current_job, name))
+ total_height += fontsize
+
+ current_job = line[2:]
+ current_names = []
+ continue
+
+ # Handle names
+ if current_job is not None and line:
+ current_names.append(line)
+
+ # Add last job if exists
+ if current_job is not None:
+ job_clip = TextClip(current_job, font=font, fontsize=fontsize, color=color,
+ stroke_color=stroke_color, stroke_width=stroke_width)
+ max_job_width = max(max_job_width, job_clip.w)
+
+ for name in current_names:
+ name_clip = TextClip(name, font=font, fontsize=fontsize, color=color,
+ stroke_color=stroke_color, stroke_width=stroke_width)
+ max_name_width = max(max_name_width, name_clip.w)
+ texts.append((current_job, name))
+ total_height += fontsize
+
+ # Create clips for each text pair
+ clips = []
+ y = 0
+
+ for job, name in texts:
+ # Create job clip
+ job_clip = TextClip(job, font=font, fontsize=fontsize, color=color,
+ stroke_color=stroke_color, stroke_width=stroke_width)
+ job_clip = job_clip.set_position(('right', y))
+
+ # Create name clip
+ name_clip = TextClip(name, font=font, fontsize=fontsize, color=color,
+ stroke_color=stroke_color, stroke_width=stroke_width)
+ name_clip = name_clip.set_position((job_clip.w + gap, y))
+
+ clips.extend([job_clip, name_clip])
+ y += fontsize
+
+ # Create final composite
+ final_clip = CompositeVideoClip(clips, size=(width, total_height))
+ return final_clip
\ No newline at end of file