generated from thinkode/modelRepository
initial commit and version 1.0
This commit is contained in:
378
moviepy/video/compositing/CompositeVideoClip.py
Normal file
378
moviepy/video/compositing/CompositeVideoClip.py
Normal file
@@ -0,0 +1,378 @@
|
||||
"""Main video composition interface of MoviePy."""
|
||||
|
||||
from functools import reduce
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
from moviepy.audio.AudioClip import CompositeAudioClip
|
||||
from moviepy.video.VideoClip import ColorClip, VideoClip
|
||||
|
||||
|
||||
class CompositeVideoClip(VideoClip):
|
||||
"""
|
||||
A VideoClip made of other videoclips displayed together. This is the
|
||||
base class for most compositions.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
size
|
||||
The size (width, height) of the final clip.
|
||||
|
||||
clips
|
||||
A list of videoclips.
|
||||
|
||||
Clips with a higher ``layer`` attribute will be displayed
|
||||
on top of other clips in a lower layer.
|
||||
If two or more clips share the same ``layer``,
|
||||
then the one appearing latest in ``clips`` will be displayed
|
||||
on top (i.e. it has the higher layer).
|
||||
|
||||
For each clip:
|
||||
|
||||
- The attribute ``pos`` determines where the clip is placed.
|
||||
See ``VideoClip.set_pos``
|
||||
- The mask of the clip determines which parts are visible.
|
||||
|
||||
Finally, if all the clips in the list have their ``duration``
|
||||
attribute set, then the duration of the composite video clip
|
||||
is computed automatically
|
||||
|
||||
bg_color
|
||||
Color for the unmasked and unfilled regions. Set to None for these
|
||||
regions to be transparent (will be slower).
|
||||
|
||||
use_bgclip
|
||||
Set to True if the first clip in the list should be used as the
|
||||
'background' on which all other clips are blitted. That first clip must
|
||||
have the same size as the final clip. If it has no transparency, the final
|
||||
clip will have no mask.
|
||||
|
||||
The clip with the highest FPS will be the FPS of the composite clip.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, clips, size=None, bg_color=None, use_bgclip=False, is_mask=False
|
||||
):
|
||||
if size is None:
|
||||
size = clips[0].size
|
||||
|
||||
if use_bgclip and (clips[0].mask is None):
|
||||
transparent = False
|
||||
else:
|
||||
transparent = True if bg_color is None else False
|
||||
|
||||
# If we must not use first clip as background and we dont have a color
|
||||
# we generate a black background if clip should not be transparent and
|
||||
# a transparent background if transparent
|
||||
if (not use_bgclip) and bg_color is None:
|
||||
if transparent:
|
||||
bg_color = 0.0 if is_mask else (0, 0, 0, 0)
|
||||
else:
|
||||
bg_color = 0.0 if is_mask else (0, 0, 0)
|
||||
|
||||
fpss = [clip.fps for clip in clips if getattr(clip, "fps", None)]
|
||||
self.fps = max(fpss) if fpss else None
|
||||
|
||||
VideoClip.__init__(self)
|
||||
|
||||
self.size = size
|
||||
self.is_mask = is_mask
|
||||
self.clips = clips
|
||||
self.bg_color = bg_color
|
||||
|
||||
# Use first clip as background if necessary, else use color
|
||||
# either set by user or previously generated
|
||||
if use_bgclip:
|
||||
self.bg = clips[0]
|
||||
self.clips = clips[1:]
|
||||
self.created_bg = False
|
||||
else:
|
||||
self.clips = clips
|
||||
self.bg = ColorClip(size, color=self.bg_color, is_mask=is_mask)
|
||||
self.created_bg = True
|
||||
|
||||
# order self.clips by layer
|
||||
self.clips = sorted(self.clips, key=lambda clip: clip.layer_index)
|
||||
|
||||
# compute duration
|
||||
ends = [clip.end for clip in self.clips]
|
||||
if None not in ends:
|
||||
duration = max(ends)
|
||||
self.duration = duration
|
||||
self.end = duration
|
||||
|
||||
# compute audio
|
||||
audioclips = [v.audio for v in self.clips if v.audio is not None]
|
||||
if audioclips:
|
||||
self.audio = CompositeAudioClip(audioclips)
|
||||
|
||||
# compute mask if necessary
|
||||
if transparent:
|
||||
maskclips = [
|
||||
(clip.mask if (clip.mask is not None) else clip.with_mask().mask)
|
||||
.with_position(clip.pos)
|
||||
.with_end(clip.end)
|
||||
.with_start(clip.start, change_end=False)
|
||||
.with_layer_index(clip.layer_index)
|
||||
for clip in self.clips
|
||||
]
|
||||
|
||||
if use_bgclip and self.bg.mask:
|
||||
maskclips = [self.bg.mask] + maskclips
|
||||
|
||||
self.mask = CompositeVideoClip(
|
||||
maskclips, self.size, is_mask=True, bg_color=0.0
|
||||
)
|
||||
|
||||
def frame_function(self, t):
|
||||
"""The clips playing at time `t` are blitted over one another."""
|
||||
# For the mask we recalculate the final transparency we'll need
|
||||
# to apply on the result image
|
||||
if self.is_mask:
|
||||
mask = np.zeros((self.size[1], self.size[0]), dtype=float)
|
||||
for clip in self.playing_clips(t):
|
||||
mask = clip.compose_mask(mask, t)
|
||||
|
||||
return mask
|
||||
|
||||
# Try doing clip merging with pillow
|
||||
bg_t = t - self.bg.start
|
||||
bg_frame = self.bg.get_frame(bg_t).astype("uint8")
|
||||
bg_img = Image.fromarray(bg_frame)
|
||||
|
||||
if self.bg.mask:
|
||||
bgm_t = t - self.bg.mask.start
|
||||
bg_mask = (self.bg.mask.get_frame(bgm_t) * 255).astype("uint8")
|
||||
bg_mask_img = Image.fromarray(bg_mask).convert("L")
|
||||
|
||||
# Resize bg_mask_img to match bg_img, always use top left corner
|
||||
if bg_mask_img.size != bg_img.size:
|
||||
mask_width, mask_height = bg_mask_img.size
|
||||
img_width, img_height = bg_img.size
|
||||
|
||||
if mask_width > img_width or mask_height > img_height:
|
||||
bg_mask_img = bg_mask_img.crop((0, 0, img_width, img_height))
|
||||
else:
|
||||
new_mask = Image.new("L", (img_width, img_height), 0)
|
||||
new_mask.paste(bg_mask_img, (0, 0))
|
||||
bg_mask_img = new_mask
|
||||
|
||||
bg_img = bg_img.convert("RGBA")
|
||||
bg_img.putalpha(bg_mask_img)
|
||||
|
||||
# For each clip apply on top of current img
|
||||
current_img = bg_img
|
||||
for clip in self.playing_clips(t):
|
||||
current_img = clip.compose_on(current_img, t)
|
||||
|
||||
# Turn Pillow image into a numpy array
|
||||
frame = np.array(current_img)
|
||||
|
||||
# If frame have transparency, remove it
|
||||
# our mask will take care of it during rendering
|
||||
if frame.shape[2] == 4:
|
||||
return frame[:, :, :3]
|
||||
|
||||
return frame
|
||||
|
||||
def playing_clips(self, t=0):
|
||||
"""Returns a list of the clips in the composite clips that are
|
||||
actually playing at the given time `t`.
|
||||
"""
|
||||
return [clip for clip in self.clips if clip.is_playing(t)]
|
||||
|
||||
def close(self):
|
||||
"""Closes the instance, releasing all the resources."""
|
||||
if self.created_bg and self.bg:
|
||||
# Only close the background clip if it was locally created.
|
||||
# Otherwise, it remains the job of whoever created it.
|
||||
self.bg.close()
|
||||
self.bg = None
|
||||
if hasattr(self, "audio") and self.audio:
|
||||
self.audio.close()
|
||||
self.audio = None
|
||||
|
||||
|
||||
def clips_array(array, rows_widths=None, cols_heights=None, bg_color=None):
|
||||
"""Given a matrix whose rows are clips, creates a CompositeVideoClip where
|
||||
all clips are placed side by side horizontally for each clip in each row
|
||||
and one row on top of the other for each row. So given next matrix of clips
|
||||
with same size:
|
||||
|
||||
```python
|
||||
clips_array([[clip1, clip2, clip3], [clip4, clip5, clip6]])
|
||||
```
|
||||
|
||||
the result will be a CompositeVideoClip with a layout displayed like:
|
||||
|
||||
```
|
||||
┏━━━━━━━┳━━━━━━━┳━━━━━━━┓
|
||||
┃ ┃ ┃ ┃
|
||||
┃ clip1 ┃ clip2 ┃ clip3 ┃
|
||||
┃ ┃ ┃ ┃
|
||||
┣━━━━━━━╋━━━━━━━╋━━━━━━━┫
|
||||
┃ ┃ ┃ ┃
|
||||
┃ clip4 ┃ clip5 ┃ clip6 ┃
|
||||
┃ ┃ ┃ ┃
|
||||
┗━━━━━━━┻━━━━━━━┻━━━━━━━┛
|
||||
```
|
||||
|
||||
If some clips doesn't fulfill the space required by the rows or columns
|
||||
in which are placed, that space will be filled by the color defined in
|
||||
``bg_color``.
|
||||
|
||||
array
|
||||
Matrix of clips included in the returned composited video clip.
|
||||
|
||||
rows_widths
|
||||
Widths of the different rows in pixels. If ``None``, is set automatically.
|
||||
|
||||
cols_heights
|
||||
Heights of the different columns in pixels. If ``None``, is set automatically.
|
||||
|
||||
bg_color
|
||||
Fill color for the masked and unfilled regions. Set to ``None`` for these
|
||||
regions to be transparent (processing will be slower).
|
||||
"""
|
||||
array = np.array(array)
|
||||
sizes_array = np.array([[clip.size for clip in line] for line in array])
|
||||
|
||||
# find row width and col_widths automatically if not provided
|
||||
if rows_widths is None:
|
||||
rows_widths = sizes_array[:, :, 1].max(axis=1)
|
||||
if cols_heights is None:
|
||||
cols_heights = sizes_array[:, :, 0].max(axis=0)
|
||||
|
||||
# compute start positions of X for rows and Y for columns
|
||||
xs = np.cumsum([0] + list(cols_heights))
|
||||
ys = np.cumsum([0] + list(rows_widths))
|
||||
|
||||
for j, (x, ch) in enumerate(zip(xs[:-1], cols_heights)):
|
||||
for i, (y, rw) in enumerate(zip(ys[:-1], rows_widths)):
|
||||
clip = array[i, j]
|
||||
w, h = clip.size
|
||||
# if clip not fulfill row width or column height
|
||||
if (w < ch) or (h < rw):
|
||||
clip = CompositeVideoClip(
|
||||
[clip.with_position("center")], size=(ch, rw), bg_color=bg_color
|
||||
).with_duration(clip.duration)
|
||||
|
||||
array[i, j] = clip.with_position((x, y))
|
||||
|
||||
return CompositeVideoClip(array.flatten(), size=(xs[-1], ys[-1]), bg_color=bg_color)
|
||||
|
||||
|
||||
def concatenate_videoclips(
|
||||
clips, method="chain", transition=None, bg_color=None, is_mask=False, padding=0
|
||||
):
|
||||
"""Concatenates several video clips.
|
||||
|
||||
Returns a video clip made by clip by concatenating several video clips.
|
||||
(Concatenated means that they will be played one after another).
|
||||
|
||||
There are two methods:
|
||||
|
||||
- method="chain": will produce a clip that simply outputs
|
||||
the frames of the successive clips, without any correction if they are
|
||||
not of the same size of anything. If none of the clips have masks the
|
||||
resulting clip has no mask, else the mask is a concatenation of masks
|
||||
(using completely opaque for clips that don't have masks, obviously).
|
||||
If you have clips of different size and you want to write directly the
|
||||
result of the concatenation to a file, use the method "compose" instead.
|
||||
|
||||
- method="compose", if the clips do not have the same resolution, the final
|
||||
resolution will be such that no clip has to be resized.
|
||||
As a consequence the final clip has the height of the highest clip and the
|
||||
width of the widest clip of the list. All the clips with smaller dimensions
|
||||
will appear centered. The border will be transparent if mask=True, else it
|
||||
will be of the color specified by ``bg_color``.
|
||||
|
||||
The clip with the highest FPS will be the FPS of the result clip.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
clips
|
||||
A list of video clips which must all have their ``duration``
|
||||
attributes set.
|
||||
method
|
||||
"chain" or "compose": see above.
|
||||
transition
|
||||
A clip that will be played between each two clips of the list.
|
||||
|
||||
bg_color
|
||||
Only for method='compose'. Color of the background.
|
||||
Set to None for a transparent clip
|
||||
|
||||
padding
|
||||
Only for method='compose'. Duration during two consecutive clips.
|
||||
Note that for negative padding, a clip will partly play at the same
|
||||
time as the clip it follows (negative padding is cool for clips who fade
|
||||
in on one another). A non-null padding automatically sets the method to
|
||||
`compose`.
|
||||
|
||||
"""
|
||||
if transition is not None:
|
||||
clip_transition_pairs = [[v, transition] for v in clips[:-1]]
|
||||
clips = reduce(lambda x, y: x + y, clip_transition_pairs) + [clips[-1]]
|
||||
transition = None
|
||||
|
||||
timings = np.cumsum([0] + [clip.duration for clip in clips])
|
||||
|
||||
sizes = [clip.size for clip in clips]
|
||||
|
||||
w = max(size[0] for size in sizes)
|
||||
h = max(size[1] for size in sizes)
|
||||
|
||||
timings = np.maximum(0, timings + padding * np.arange(len(timings)))
|
||||
timings[-1] -= padding # Last element is the duration of the whole
|
||||
|
||||
if method == "chain":
|
||||
|
||||
def frame_function(t):
|
||||
i = max([i for i, e in enumerate(timings) if e <= t])
|
||||
return clips[i].get_frame(t - timings[i])
|
||||
|
||||
def get_mask(clip):
|
||||
mask = clip.mask or ColorClip(clip.size, color=1, is_mask=True)
|
||||
if mask.duration is None:
|
||||
mask.duration = clip.duration
|
||||
return mask
|
||||
|
||||
result = VideoClip(is_mask=is_mask, frame_function=frame_function)
|
||||
if any([clip.mask is not None for clip in clips]):
|
||||
masks = [get_mask(clip) for clip in clips]
|
||||
result.mask = concatenate_videoclips(masks, method="chain", is_mask=True)
|
||||
result.clips = clips
|
||||
elif method == "compose":
|
||||
result = CompositeVideoClip(
|
||||
[
|
||||
clip.with_start(t).with_position("center")
|
||||
for (clip, t) in zip(clips, timings)
|
||||
],
|
||||
size=(w, h),
|
||||
bg_color=bg_color,
|
||||
is_mask=is_mask,
|
||||
)
|
||||
else:
|
||||
raise Exception(
|
||||
"MoviePy Error: The 'method' argument of "
|
||||
"concatenate_videoclips must be 'chain' or 'compose'"
|
||||
)
|
||||
|
||||
result.timings = timings
|
||||
|
||||
result.start_times = timings[:-1]
|
||||
result.start, result.duration, result.end = 0, timings[-1], timings[-1]
|
||||
|
||||
audio_t = [
|
||||
(clip.audio, t) for clip, t in zip(clips, timings) if clip.audio is not None
|
||||
]
|
||||
if audio_t:
|
||||
result.audio = CompositeAudioClip([a.with_start(t) for a, t in audio_t])
|
||||
|
||||
fpss = [clip.fps for clip in clips if getattr(clip, "fps", None) is not None]
|
||||
result.fps = max(fpss) if fpss else None
|
||||
return result
|
||||
1
moviepy/video/compositing/__init__.py
Normal file
1
moviepy/video/compositing/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""All for compositing video clips."""
|
||||
Reference in New Issue
Block a user