Zulko / moviepy

Video editing with Python
https://zulko.github.io/moviepy/
MIT License
12.51k stars 1.57k forks source link

Slide In Effect with Images #1795

Open stefanodvx opened 2 years ago

stefanodvx commented 2 years ago
        clips = []
        for _, name in urls:
            img = name.replace("webp", "jpg")
            Image.open(name).convert("RGB").save(img)
            clips.append(ImageClip(img).set_duration(3))

        video = CompositeVideoClip([
            clip.fx(transfx.slide_out, duration=0.4, side="left")
            for clip in clips
        ])
        video.write_videofile(file_name,
            codec="libx264", audio_codec="aac",
            preset="ultrafast", fps=24, logger=None,
            ffmpeg_params=[
                "-vf", "pad=ceil(iw/2)*2:ceil(ih/2)*2",
                "-pix_fmt", "yuv420p"
            ]
        )

this is my code. im trying to make a slideshow from these images, but the issue is that the result is not what it should be. every clip is on top of each other and trasnition is not working, it just do the effect to a black screen: here's the result i get:

https://user-images.githubusercontent.com/69367859/175075544-e9b56cb8-5af6-40ed-b2a4-97714941ef65.mp4

stefanodvx commented 2 years ago

I fixed images position using set_start() but still slide out effect shows a black screen for the duration of the transition (it should display the next Image instead). also, images after transition are totally shifted to left. they should be centered like normally and be full screen

stefanodvx commented 2 years ago

this is what i got so far. as you can see images are shifted to left and not centered, also they are not resized well (see 3rd slide( https://user-images.githubusercontent.com/69367859/175087198-674bc797-036a-4d53-8742-00da8cd1a347.mp4

MohamedAbdultawab commented 2 years ago

You probably need to do this:

slided_clips = [CompositeVideoClip([clip.fx(transfx.slide_out, duration=0.4, side="left")])
                    for clip in clips]
final_clip = concatenate( slided_clips, padding=-1)
stefanodvx commented 2 years ago

@MohamedAbdultawab mh, you sure? i thought CompositeVideoClip arleady make a Video, you sure i can pass it in concatenate?? Ill try soon tho. Thanks man

MohamedAbdultawab commented 2 years ago

yes pretty sure, because slide_out won't work unless clip is in a composition, so you create a composition from it, and then concatenate those.

stefanodvx commented 2 years ago

this is the result. a bit funny XD. it doesnt work tho :( https://user-images.githubusercontent.com/69367859/175133209-5818ac73-6837-4ed4-a2ae-f0c9588570c9.mp4

Code:

video = concatenate([
    CompositeVideoClip([
        clip.fx(transfx.slide_out, duration=0.3, side="left")
    ]) for clip in clips
], padding=-1)
MohamedAbdultawab commented 2 years ago

Can you please check this Thread

stefanodvx commented 2 years ago

@MohamedAbdultawab ok so, i checked it. but what about HX and WX? i never know the images i use so size couldnt be a constant.

MohamedAbdultawab commented 2 years ago

You can always assign them dynamically by getting clip.h and clip.w You can focus on getting the effect done right manually first, won't hurt to add those values manually just to get the effect working then aftdr that, you can look into automating it.

stefanodvx commented 2 years ago

@MohamedAbdultawab i cant make it working even manually, i tried a lot :/

MohamedAbdultawab commented 2 years ago
clips = [ImageClip(img).set_duration(1) for img in ["1.jpeg","2.jpeg","3.jpeg"]]

videos= [CompositeVideoClip([
    clip.fx(transfx.slide_out, duration=0.4, side="left")])
    for clip in clips
]
video = concatenate_videoclips(videos, method="compose")
video.write_videofile("output.mp4",
    codec="libx264", audio_codec="aac",
    preset="ultrafast", fps=24,
    ffmpeg_params=[
        "-vf", "pad=ceil(iw/2)*2:ceil(ih/2)*2",
        "-pix_fmt", "yuv420p"
    ]
)

https://user-images.githubusercontent.com/19710525/175368708-ada6662e-d34e-46ae-a667-8228b7431039.mp4

can you confirm if the behavior in the attached video is what you want?

stefanodvx commented 2 years ago
```python
clips = [ImageClip(img).set_duration(1) for img in ["1.jpeg","2.jpeg","3.jpeg"]]

videos= [CompositeVideoClip([
    clip.fx(transfx.slide_out, duration=0.4, side="left")])
    for clip in clips
]
video = concatenate_videoclips(videos, method="compose")
video.write_videofile("output.mp4",
    codec="libx264", audio_codec="aac",
    preset="ultrafast", fps=24,
    ffmpeg_params=[
        "-vf", "pad=ceil(iw/2)*2:ceil(ih/2)*2",
        "-pix_fmt", "yuv420p"
    ]
)
moviepy_concatenate.mp4

can you confirm if the behavior in the attached video is what you want?

sorry for late reply. btw, not really. if you see, there's a black screen moving in the fx. instead, the next image should move to left, not a black screen

MohamedAbdultawab commented 2 years ago

https://user-images.githubusercontent.com/19710525/175792652-5aad2f7b-454b-4458-a538-c1a4992c06c2.mp4

I believe this is what you want, and you can achieve by this:

# Create image clips and resize them to same size so no black borders gets added

EFFECT_DURATION = 0.4
CLIP_DURATION = 1

clip1 = ImageClip("../experiments/1.jpeg").set_duration(CLIP_DURATION)
clip2 = (
    ImageClip("../experiments/2.jpeg").resize(clip1.size).set_duration(CLIP_DURATION)
)
clip3 = (
    ImageClip("../experiments/3.jpeg").resize(clip1.size).set_duration(CLIP_DURATION)
)
clips = [clip1, clip2, clip3]

# For the first clip we will need it to start from the beginning and only add
# slide out effect to the end of it
first_clip = CompositeVideoClip(
    [clips[0].fx(transfx.slide_out, duration=EFFECT_DURATION, side="left")]
).set_start((CLIP_DURATION - EFFECT_DURATION) * 0)

# For the last video we only need it to start entring the screen from the left going right
# but not slide out at the end so the end clip exits on a full image not a partial image or black screen
last_clip = CompositeVideoClip(
    [clips[-1].fx(transfx.slide_in, duration=EFFECT_DURATION, side="right")]
    # -1 because we start with index 0 so we go all the way up to array length - 1
).set_start((CLIP_DURATION - EFFECT_DURATION) * (len(clips) - 1))

videos = (
    [first_clip]
    # For all other clips in the middle, we need them to slide in to the previous clip and out for the next one
    + [
        (
            CompositeVideoClip(
                [clip.fx(transfx.slide_in, duration=EFFECT_DURATION, side="right")]
            )
            .set_start((CLIP_DURATION - EFFECT_DURATION) * idx)
            .fx(transfx.slide_out, duration=EFFECT_DURATION, side="left")
        )
            # set start to 1 since we start from second clip in the original array
        for idx, clip in enumerate(clips[1:-1], start=1)
    ]
    + [last_clip]
)

video = CompositeVideoClip(videos)
video.write_videofile(
    "final_clip.mp4",
    codec="libx264",
    audio_codec="aac",
    preset="ultrafast",
    fps=24,
    threads=24,
    ffmpeg_params=["-vf", "pad=ceil(iw/2)*2:ceil(ih/2)*2", "-pix_fmt", "yuv420p"],
)

Note: you will need to tweak the effect duration and clip duration for the first and last clips and maybe the middle clips so that all clips runs for the same time.

stefanodvx commented 2 years ago

that's exactly what i need. ill try in some hourd and let you know. thanks!

stefanodvx commented 2 years ago
    def create_clip(self, urls, audio_name, file_name):
        EFFECT_DURATION = 0.4
        CLIP_DURATION = 3

        clips = []
        for _, name in urls:
            img = name.replace("webp", "jpg")
            Image.open(name).convert("RGB").save(img)
            clip = ImageClip(img).set_duration(CLIP_DURATION)
            if len(clips) > 0:
                clip = clip.resize(clips[0].size)
            clips.append(clip)

        # For the first clip we will need it to start from the beginning and only add
        # slide out effect to the end of it
        first_clip = CompositeVideoClip(
            [clips[0].fx(transfx.slide_out, duration=EFFECT_DURATION, side="left")]
        ).set_start((CLIP_DURATION - EFFECT_DURATION) * 0)

        # For the last video we only need it to start entring the screen from the left going right
        # but not slide out at the end so the end clip exits on a full image not a partial image or black screen
        last_clip = CompositeVideoClip(
            [clips[-1].fx(transfx.slide_in, duration=EFFECT_DURATION, side="right")]
            # -1 because we start with index 0 so we go all the way up to array length - 1
        ).set_start((CLIP_DURATION - EFFECT_DURATION) * (len(clips) - 1))

        videos = (
            [first_clip]
            # For all other clips in the middle, we need them to slide in to the previous clip and out for the next one
            + [
                (
                    CompositeVideoClip(
                        [clip.fx(transfx.slide_in, duration=EFFECT_DURATION, side="right")]
                    )
                    .set_start((CLIP_DURATION - EFFECT_DURATION) * idx)
                    .fx(transfx.slide_out, duration=EFFECT_DURATION, side="left")
                )
                    # set start to 1 since we start from second clip in the original array
                for idx, clip in enumerate(clips[1:-1], start=1)
            ]
            + [last_clip]
        )
        video = CompositeVideoClip(videos)
        audio = AudioFileClip(audio_name)
        if audio.duration > video.duration:
            video = vfx.loop(video, duration=audio.duration)
        elif audio.duration < video.duration:
            audio = afx.audio_loop(audio, duration=video.duration)
        video.audio = audio
        video.audio.duration = video.duration
        video.write_videofile(file_name,
            codec="libx264", audio_codec="aac", threads=24,
            preset="ultrafast", fps=24, logger=None,
            ffmpeg_params=[
                "-vf", "pad=ceil(iw/2)*2:ceil(ih/2)*2",
                "-pix_fmt", "yuv420p"
            ]
        )
        video.close()
        audio.close()

ok currently this is the code im using, the one you made, and then this is the result i get:

https://user-images.githubusercontent.com/69367859/175934871-ae14e09b-0d48-49b1-a07d-088646968361.mp4

as you can see, when the loops end, there's no animation, and it should have it (since the slideshow loops for the duration of the audio). also the images are stretched. they should fit properly in the screen (lets suppose its a phone screen resolution) but not get stretched. like the slideshow of TikTok @MohamedAbdultawab

MohamedAbdultawab commented 2 years ago

You can add a slide out effect to the last clip if you want it, I guessed you didn't need it so didn't add it. images are stretched because they are not of the same size, ypu need to pre procese those images or set their size explicitly so moviepy doesn't guess it for you

stefanodvx commented 2 years ago

what if i want images fit good in a 1792×828 video? (screen phone)

MohamedAbdultawab commented 2 years ago

You can make a small function that takes an image6clip and video width and height, and check the size of the image, if it matches the video width and height then ok return the clip as is, if bigger then resize it down and fix the apsect ratio by, adding a margin to the image, if smaller then you can resize up or add margin to match the size

stefanodvx commented 2 years ago

aight okok

stefanodvx commented 2 years ago

@MohamedAbdultawab tried something to resize keeping good aspect ratio (using most high image as referer). works fine but some images are not centered to the screen:

Result: https://user-images.githubusercontent.com/69367859/176437681-712dc817-3a5e-41c3-8afe-a18931480a95.mp4

Code:

def resize_image(clip: ImageClip, ideal: ImageClip):
    width, height = clip.size
    ratio = width / height

    new_h = ideal.h
    new_w = int(ratio * new_h)

    return clip.resize((new_w, new_h))

EFFECT_DURATION = 0.3
CLIP_DURATION = 3.3

clip1 = ImageClip("imgs/1.jpg").set_duration(CLIP_DURATION)
clip2 = ImageClip("imgs/2.png").set_duration(CLIP_DURATION)
clip3 = ImageClip("imgs/3.jpg").set_duration(CLIP_DURATION)
clips = [clip1, clip2, clip3]
most_high_clip = max(clips, key=lambda x: x.h)
clips = [resize_image(clip, most_high_clip) for clip in clips]

first_clip = CompositeVideoClip(
    [clips[0].fx(transfx.slide_out, duration=EFFECT_DURATION, side="left")]
).set_start((CLIP_DURATION - EFFECT_DURATION) * 0)

# For the last video we only need it to start entring the screen from the left going right
# but not slide out at the end so the end clip exits on a full image not a partial image or black screen
last_clip = CompositeVideoClip(
    [clips[-1].fx(transfx.slide_in, duration=EFFECT_DURATION, side="right")]
    # -1 because we start with index 0 so we go all the way up to array length - 1
).set_start((CLIP_DURATION - EFFECT_DURATION) * (len(clips) - 1))

videos = (
    [first_clip]
    # For all other clips in the middle, we need them to slide in to the previous clip and out for the next one
    + [
        (
            CompositeVideoClip(
                [clip.fx(transfx.slide_in, duration=EFFECT_DURATION, side="right")]
            )
            .set_start((CLIP_DURATION - EFFECT_DURATION) * idx)
            .fx(transfx.slide_out, duration=EFFECT_DURATION, side="left")
        )
            # set start to 1 since we start from second clip in the original array
        for idx, clip in enumerate(clips[1:-1], start=1)
    ]
    + [last_clip]
)
video = CompositeVideoClip(videos)

video.write_videofile(
    "test.mp4",
    codec="libx264", audio_codec="aac", threads=48,
    preset="ultrafast", fps=24,
    ffmpeg_params=[
        "-vf", "pad=ceil(iw/2)*2:ceil(ih/2)*2",
        "-pix_fmt", "yuv420p"
    ]
)
video.close()
MohamedAbdultawab commented 2 years ago

Call set_position("center") on all resized clips (or all clips maybe)

stefanodvx commented 2 years ago

same result with

return clip.set_position("center").resize((new_w, new_h))

and even with

return clip.resize((new_w, new_h)).set_position("center")
MohamedAbdultawab commented 2 years ago

Ok then put it in here

CompositeVideoClip(
                [clip.fx(transfx.slide_in, duration=EFFECT_DURATION, side="right")]
            )
            .set_start((CLIP_DURATION - EFFECT_DURATION) * idx)
            .fx(transfx.slide_out, duration=EFFECT_DURATION, side="left").set_position("center")
        )
stefanodvx commented 2 years ago

it works like this, thanks!

MohamedAbdultawab commented 2 years ago

You're welcome

stefanodvx commented 2 years ago

last thing i need to do is loop the video for a duration i want, lets suppose its 15 seconds. how i can do it? the slide effect should be looped too, so vfx.loop its not a good choice ig

MohamedAbdultawab commented 2 years ago

The whole video? You can use moviepy.video.fx.all.loop function on the final clip and set only the duration you want

stefanodvx commented 2 years ago

yea, loop the whole video. until it reaches the time i set if i use moviepy.video.fx.all.loop on last clip, only last clip would be looped right?

MohamedAbdultawab commented 2 years ago

Yes, if you used it on the last clip only it will be looped, but you need to take into consideration how this affect the audio clip attached, it won't be applied the same way you expect, you will need to preprocess it differently. Also you will need to subtract the clips duration from the desired duration you added on the last clip If you want to loop the whole video then it's easy.

stefanodvx commented 2 years ago

yea i want to loop the whole video :D

stefanodvx commented 2 years ago

i used moviepy.video.fx.all.loop function but last clip has no animation to the looped first clip, so i guess i have to do something like this

videos = [
    first_clip,
    *[(
        CompositeVideoClip([clip.fx(transfx.slide_in, duration=EFFECT_DURATION, side="right")])
            .set_position(("center", "center"))
            .set_start((CLIP_DURATION - EFFECT_DURATION) * idx)
            .fx(transfx.slide_out, duration=EFFECT_DURATION, side="left")
        ) for idx, clip in enumerate(clips[1:-1], start=1)
    ], CompositeVideoClip([last_clip]).fx(transfx.slide_in, duration=EFFECT_DURATION, side="left")
]
MohamedAbdultawab commented 2 years ago

Yes and then loop the whole vidoe after

stefanodvx commented 2 years ago

doesnt work. last clip has still no slide to the first one :(

MohamedAbdultawab commented 2 years ago

Can you please send ghe code used and the output file

stefanodvx commented 2 years ago

Code:

first_clip = CompositeVideoClip(
    [clips[0].fx(transfx.slide_out, duration=EFFECT_DURATION, side="left")]
).set_start((CLIP_DURATION - EFFECT_DURATION) * 0).set_position(("center", "center"))

last_clip = CompositeVideoClip(
    [clips[-1].fx(transfx.slide_in, duration=EFFECT_DURATION, side="right")]
).set_start((CLIP_DURATION - EFFECT_DURATION) * (len(clips) - 1)).set_position(("center", "center"))

videos = [
    first_clip,
    *[(
        CompositeVideoClip([clip.fx(transfx.slide_in, duration=EFFECT_DURATION, side="right")])
            .set_position(("center", "center"))
            .set_start((CLIP_DURATION - EFFECT_DURATION) * idx)
            .fx(transfx.slide_out, duration=EFFECT_DURATION, side="left")
        ) for idx, clip in enumerate(clips[1:-1], start=1)
    ], CompositeVideoClip([last_clip]).fx(transfx.slide_in, duration=EFFECT_DURATION, side="left")
]

video = CompositeVideoClip(videos)
video = loop(video, duration=20)

video.write_videofile(
    "test.mp4",
    codec="libx264", audio_codec="aac", threads=48,
    preset="ultrafast", fps=24,
    ffmpeg_params=[
        "-vf", "pad=ceil(iw/2)*2:ceil(ih/2)*2",
        "-pix_fmt", "yuv420p"
    ]
)

Output:

https://user-images.githubusercontent.com/69367859/176458887-6a011988-df90-4a78-ad71-0001a02daf9c.mp4

MohamedAbdultawab commented 2 years ago

You're missing the set_start call on last clip

stefanodvx commented 2 years ago

.set_start((CLIP_DURATION - EFFECT_DURATION) * len(clips)) ?

MohamedAbdultawab commented 2 years ago

set_start((CLIP_DURATION - EFFECT_DURATION) * (len(clips) - 1))

stefanodvx commented 2 years ago
videos = [
    first_clip,
    *[(
        CompositeVideoClip([clip.fx(transfx.slide_in, duration=EFFECT_DURATION, side="right")])
            .set_position(("center", "center"))
            .set_start((CLIP_DURATION - EFFECT_DURATION) * idx)
            .fx(transfx.slide_out, duration=EFFECT_DURATION, side="left")
        ) for idx, clip in enumerate(clips[1:-1], start=1)
    ], CompositeVideoClip([last_clip]).set_start((CLIP_DURATION - EFFECT_DURATION) * (len(clips)-1)).fx(transfx.slide_in, duration=EFFECT_DURATION, side="left")
]

Output:

https://user-images.githubusercontent.com/69367859/176462981-41a80199-8e59-4a2f-87f1-6fd3afad87d8.mp4

stefanodvx commented 2 years ago

@MohamedAbdultawab any idea on how to fix it?

MohamedAbdultawab commented 2 years ago

You need to make the fx inside the composite video, and also you ne3d to make it slide in from the right and slide out from left

videos = [
    first_clip,
    *[(
        CompositeVideoClip([clip.fx(transfx.slide_in, duration=EFFECT_DURATION, side="right")])
            .set_position(("center", "center"))
            .set_start((CLIP_DURATION - EFFECT_DURATION) * idx)
            .fx(transfx.slide_out, duration=EFFECT_DURATION, side="left")
        ) for idx, clip in enumerate(clips[1:-1], start=1)
    ], CompositeVideoClip([last_clip.fx(transfx.slide_in, duration=EFFECT_DURATION, side="right")]).set_start((CLIP_DURATION - EFFECT_DURATION) * (len(clips)-1)).fx(transfx.slide_out, duration=EFFECT_DURATION, side="left")
]
stefanodvx commented 2 years ago

Tried this code, this is the output: last clip its not showing well, it appears on last second

https://user-images.githubusercontent.com/69367859/176747562-a9e8b321-aae4-456a-997e-451b010866e7.mp4

MohamedAbdultawab commented 2 years ago

I believe the problem is with your ordering of the operations you are performing on each clip Check the following snippet and the following output

PS. I've also added a set_position call to fix the positioning of the unfit images

# Create image clips and resize them to same size so no black borders gets added

EFFECT_DURATION = 0.3
CLIP_DURATION = 3.3

clip1 = ImageClip("../experiments/1.jpeg").set_duration(CLIP_DURATION)
clip2 = ImageClip("../experiments/2.jpeg").set_duration(CLIP_DURATION)
clip3 = ImageClip("../experiments/3.jpeg").set_duration(CLIP_DURATION)
clips = [clip1, clip2, clip3]

# For the first clip we will need it to start from the beginning and only add
# slide out effect to the end of it
first_clip = CompositeVideoClip(
    [
        clips[0]
        .set_pos("center")
        .fx(transfx.slide_out, duration=EFFECT_DURATION, side="left")
    ]
).set_start((CLIP_DURATION - EFFECT_DURATION) * 0)

# For the last video we only need it to start entring the screen from the left going right
# but not slide out at the end so the end clip exits on a full image not a partial image or black screen
last_clip = (
    CompositeVideoClip(
        [
            clips[-1]
            .set_pos("center")
            .fx(transfx.slide_in, duration=EFFECT_DURATION, side="right")
        ]
        # -1 because we start with index 0 so we go all the way up to array length - 1
    )
    .set_start((CLIP_DURATION - EFFECT_DURATION) * (len(clips) - 1))
    .fx(transfx.slide_out, duration=EFFECT_DURATION, side="left")
)

videos = (
    [first_clip]
    # For all other clips in the middle, we need them to slide in to the previous clip and out for the next one
    + [
        (
            CompositeVideoClip(
                [
                    clip.set_pos("center").fx(
                        transfx.slide_in, duration=EFFECT_DURATION, side="right"
                    )
                ]
            )
            .set_start((CLIP_DURATION - EFFECT_DURATION) * idx)
            .fx(transfx.slide_out, duration=EFFECT_DURATION, side="left")
        )
        # set start to 1 since we start from second clip in the original array
        for idx, clip in enumerate(clips[1:-1], start=1)
    ]
    + [last_clip]
)

video = CompositeVideoClip(videos)
video.write_videofile(
    "2_final_clip.mp4",
    codec="libx264",
    audio_codec="aac",
    preset="ultrafast",
    fps=24,
    threads=24,
    ffmpeg_params=["-vf", "pad=ceil(iw/2)*2:ceil(ih/2)*2", "-pix_fmt", "yuv420p"],
)

https://user-images.githubusercontent.com/19710525/176975569-40f0d587-e8e0-4928-bb35-82e65e517d95.mp4

stefanodvx commented 2 years ago

used your code, works pretty well but middle image is not centered to the screen even with set_pos("center"). Result:

https://user-images.githubusercontent.com/69367859/176976555-9e24b684-4404-4946-97ec-e661ce111c79.mp4

MohamedAbdultawab commented 2 years ago

Please send me the image and the code you used

stefanodvx commented 2 years ago

1 2 3

IMAGES ^^^

CODE: https://nekobin.com/hasezehugu.py (i uploaded it on nekobin since its too long)

MohamedAbdultawab commented 2 years ago

The problem is with your resizing logic, you can't just take the clip with largest height as reference and make others follow it. The problem with this is if you have horizontal images(width is way bigger than height) and vertical images (height is way bigger than width) then resizing the horizontal image to the height of the vertical ones will lead to even bigger horizontal because aspect ratio is preserved, so when you do so the vertical image still can't be positioned in the middle of the screen on its own because it's not actually overlayed on something else so it can be centered, instead it's just displayed at the left of the screen and then the res of the canvas is empty so it appears black.

What you need to do instead is make sure all images be of the same size by either cropping them all or fit them all in one specific size and add margins accordingly.

I prefer adding margins, but you will need to adjust for all the possible cases that might happen which is tedious, so cropping is easier since you can crop horizontal rectangle from horizontal or vertical images.

stefanodvx commented 2 years ago

i guess i'll crop to a specific size, or cant i just crop to the highest clip? not resizing according to it, i just mean cropping it so it has black bands oj the top / sides

MohamedAbdultawab commented 2 years ago

I think you can't make an image bigger by cropping it, you will need combination of cropping resizing and adding margins.