Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

a good way to solve the black border of alpha channel (text/image/video), caused by using compositeVideoClip #1395

Closed
clotyxf opened this issue Dec 30, 2020 · 2 comments
Labels
bug Issues that report (apparent) bugs. text Issues dealing with TextClip, SubtitlesClip, or handling of text in general.

Comments

@clotyxf
Copy link

clotyxf commented Dec 30, 2020

import numpy as np
import moviepy.editor as mpy
from PIL import Image

def clip_frame_to_img(
    clip, frame=2.0, withmask=True
):
    try:
        img = clip.get_frame(frame)

        if withmask and clip.mask is not None:
            mask = 255 * clip.mask.get_frame(frame)
            img = np.dstack([img, mask])
    except:
        composite_clip = mpy.CompositeVideoClip([clip])
        img = composite_clip.get_frame(frame)

        if withmask and composite_clip.mask is not None:
            mask = 255 * composite_clip.mask.get_frame(frame)
            img = np.dstack([img, mask])
      
    im = Image.fromarray(img.astype("uint8"))
    return im

def cal_relative_position(bg_position, position, bg_size=(1,1), size=(1,1)):
    bg_w, bg_h = bg_size
    w, h = size
    bg_pos_x, bg_pos_y = bg_position
    pos_x, pos_y = position

    if isinstance(pos_x, str):
        D = {'left': 0, 'center': round((bg_w - w) / 2), 'right': round(bg_w - w)}
        pos_x = D.get(pos_x, 0)

    if isinstance(position[1], str):
        D = {'top': 0, 'center': round((bg_h - h) / 2), 'bottom': round(bg_h - h)}
        pos_y = D.get(pos_y, 0)

    return bg_pos_x + pos_x, bg_pos_y + pos_y

def transform_to_video_clip(func: callable, duration: float, position=None):
    im_dict = {"memoized_t": 0, "memoized_frame": None}
    
    def make_frame(t):
        if t == im_dict["memoized_t"] and im_dict["memoized_frame"] is not None:
            im = im_dict["memoized_frame"]
        else:
            im = func(t)
            im = np.array(im).astype(np.uint8)
            im_dict["memoized_t"] = t
            im_dict["memoized_frame"] = im
    
        return im[:,:,:3]

    def mask_make_frame(t):
        if t == im_dict["memoized_t"] and im_dict["memoized_frame"] is not None:
            im = im_dict["memoized_frame"]
        else:
            im = func(t)
            im = np.array(im).astype(np.uint8)
            im_dict["memoized_t"] = t
            im_dict["memoized_frame"] = im

        im = 1.0 * im[:, :, 3] / 255
        return im
    
    newclip = mpy.VideoClip(make_frame, duration=duration)
    newclip.mask = mpy.VideoClip(mask_make_frame, duration=duration, ismask=True)

    if position is not None:
        newclip = newclip.set_position(position)

    return newclip

def refactor_to_video_clip(clips, duration=None, is_mask=False, position=None, bg_size=None, scale_size=None):
    """ 重构 mpy.Clip (多次调用mpy.CompositeVideoClip时解决黑边问题)
    """
    clips = clips if isinstance(clips, list) else [clips]
    fpss = [c.fps for c in clips if getattr(c, 'fps', None)]
    fps = max(fpss) if fpss else None
    duration = duration if duration is not None else max([_.duration for _ in clips])
    bg_size = bg_size if bg_size is not None else clips[0].size
    # compute audio
    audio_clips = [v.audio for v in clips if v.audio is not None]

    def func(t):
        bg_im = Image.new('RGBA', size=list(map(int, bg_size)), color=(0,0,0,0))
        
        for clip in clips:
            if clip.is_playing(t) == False:
                continue

            im = clip_frame_to_img(clip=clip, frame=t - clip.start)

            if im is None:
                continue

            if position is None and hasattr(clip, 'pos') and hasattr(clip.pos, '__call__'):
                clip_pos = clip.pos(t - clip.start)
            else:
                clip_pos = position

            if clip_pos is not None and bg_size is not None:
                dest = cal_relative_position((0, 0), clip_pos, bg_size, im.size)
                mask_im = Image.new('RGBA', size=list(map(int, bg_size)), color=(0,0,0,0))
                dest = list(map(int, dest))

                if dest[0] < 0:
                    if abs(dest[0]) >= im.width:
                        im = Image.new('RGBA', size=(im.size), color=(0,0,0,0))
                    else:
                        im = im.crop(box=(abs(dest[0]), 0, im.width, im.height))
                    dest[0] = 0

                if dest[1] < 0:
                    if abs(dest[1]) >= im.height:
                        im = Image.new('RGBA', size=(im.size), color=(0,0,0,0))
                    else:
                        im = im.crop(box=(0, abs(dest[1]), im.width, im.height))
                    dest[1] = 0

                mask_im.alpha_composite(im, dest=tuple(dest))
                im = mask_im

            bg_im.alpha_composite(im, dest=(0, 0))
        
        if scale_size is not None:
            newsize = list(map(int, scale_size))[::-1]
            bg_im = bg_im.resize(newsize[::-1], Image.ANTIALIAS)

        return bg_im

    newclip = transform_to_video_clip(func=func, duration=duration)
    newclip.fps = fps
    newclip.end = duration

    if audio_clips:
        newclip.audio = concatenate_audioclips(clips=audio_clips)
    
    return newclip

using compositeVideoClip:

text_clip = mpy.TextClip(txt="PRIMARY TEXT", fontsize=55, color='red')
text_clip = text_clip.set_duration(3).set_fps(30)
bg_clip = mpy.ColorClip(size=[text_clip.w, text_clip.h * 2], color=(255, 255, 255, 255), duration=3)

# change size
text_clip = mpy.CompositeVideoClip([text_clip.set_position(('center', 'center'))], size=(int(text_clip.w * 0.7), text_clip.h))
text_clip = mpy.CompositeVideoClip([text_clip])
text_clip = mpy.CompositeVideoClip([text_clip])
newclip = mpy.CompositeVideoClip([bg_clip, text_clip.set_position('center')])

#newclip.ipython_display()
Image.fromarray(newclip.get_frame(1))

im1

using refactor_to_video_clip:

text_clip = mpy.TextClip(txt="PRIMARY TEXT", fontsize=55, color='red')
text_clip = text_clip.set_duration(3).set_fps(30)
bg_clip = mpy.ColorClip(size=[text_clip.w, text_clip.h * 2], color=(255, 255, 255, 255), duration=3)

# change size
text_clip = refactor_to_video_clip(text_clip, position=('center', 'center'), bg_size=(int(text_clip.w * 0.7), text_clip.h))
text_clip = refactor_to_video_clip(text_clip)
text_clip = refactor_to_video_clip(text_clip)
# ...#
text_clip = refactor_to_video_clip(text_clip)
newclip = refactor_to_video_clip([bg_clip, text_clip])
newclip = newclip.set_fps(30)
newclip.ipython_display()

#newclip.ipython_display()
Image.fromarray(newclip.get_frame(1))

im2

@clotyxf clotyxf added the bug Issues that report (apparent) bugs. label Dec 30, 2020
@clotyxf
Copy link
Author

clotyxf commented Dec 31, 2020

when more than one clip is applied, refactor_to_video_clip performs better than CompositeVideoClip, for example: text moving with letters.

@keikoro keikoro added the text Issues dealing with TextClip, SubtitlesClip, or handling of text in general. label Jan 14, 2022
@keikoro
Copy link
Collaborator

keikoro commented Jan 14, 2022

@clotyxf Hi, it's unclear to me what this is – a suggestion for a refactor? Does this go together with what's suggested for the upcoming version of MoviePy? See #1089

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Issues that report (apparent) bugs. text Issues dealing with TextClip, SubtitlesClip, or handling of text in general.
Projects
None yet
Development

No branches or pull requests

2 participants