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

Zoom-In Effect for moviepy #1402

Open
mowshon opened this issue Jan 5, 2021 · 16 comments
Open

Zoom-In Effect for moviepy #1402

mowshon opened this issue Jan 5, 2021 · 16 comments
Labels
feature-request Request for a new feature or additional functionality. fx Related to advanced effects applied via clip.fx(), or effects in general.

Comments

@mowshon
Copy link

mowshon commented Jan 5, 2021

I am trying to create a video from slides. Each slide has a zoom-in effect. All the code examples for creating the zoom-in effect gave poor results if the zoom speed is very slow. The side effect was a slight twitching.

Below I will provide examples that the documentation offers and the function that I offer.

The problem I faced too: #183

Example from documentation

https://zulko.github.io/moviepy/ref/videofx/moviepy.video.fx.all.resize.html

import moviepy.editor as mp

size = (1920, 1080)

img = 'https://www.colorado.edu/cumuseum/sites/default/files/styles/widescreen/public/slider/coachwhip2_1.jpg'

slide = mp.ImageClip(img).set_fps(25).set_duration(10).resize(size)

slide = slide.resize(lambda t: 1 + 0.04 * t)  # Zoom-in effect
slide = slide.set_position(('center', 'center'))
slide = mp.CompositeVideoClip([slide], size=size)

slide.write_videofile('zoom-test-1.mp4')

Result: https://youtu.be/qlU_4hVFm6I

My function

Slideshow example: https://gist.github.com/mowshon/2a0664fab0ae799734594a5e91e518d5

import moviepy.editor as mp
import math
from PIL import Image
import numpy


def zoom_in_effect(clip, zoom_ratio=0.04):
    def effect(get_frame, t):
        img = Image.fromarray(get_frame(t))
        base_size = img.size

        new_size = [
            math.ceil(img.size[0] * (1 + (zoom_ratio * t))),
            math.ceil(img.size[1] * (1 + (zoom_ratio * t)))
        ]

        # The new dimensions must be even.
        new_size[0] = new_size[0] + (new_size[0] % 2)
        new_size[1] = new_size[1] + (new_size[1] % 2)

        img = img.resize(new_size, Image.LANCZOS)

        x = math.ceil((new_size[0] - base_size[0]) / 2)
        y = math.ceil((new_size[1] - base_size[1]) / 2)

        img = img.crop([
            x, y, new_size[0] - x, new_size[1] - y
        ]).resize(base_size, Image.LANCZOS)

        result = numpy.array(img)
        img.close()

        return result

    return clip.fl(effect)


size = (1920, 1080)

img = 'https://www.colorado.edu/cumuseum/sites/default/files/styles/widescreen/public/slider/coachwhip2_1.jpg'

slide = mp.ImageClip(img).set_fps(25).set_duration(10).resize(size)
slide = zoom_in_effect(slide, 0.04)

slide.write_videofile('zoom-test-2.mp4')

Result: https://youtu.be/U-A54E00sC8

In my example, there is also a slight wobble, but not as obvious as in the first example. Below is a link to a video where you can compare both options.

Comparation video: https://www.youtube.com/watch?v=UPyYdrwWE14

  • Left side: My function
  • Right side: Example from docs

I would be glad to get advice on improving the code.

@mowshon mowshon added the feature-request Request for a new feature or additional functionality. label Jan 5, 2021
@jcbrockschmidt
Copy link
Contributor

jcbrockschmidt commented Jan 11, 2021

Possible explanation for wobbling (in your solution)

The wobbling effect might be caused by the use of math.ceil and evening for new_size. This may result in one or both of the following:

  1. A dimension's delta / change-per-frame modulates inconsistently. For example, the width might progress like 640 638 638 336 334 334, alternating between 1 and 2 frames for a given quantity.
  2. The width and height are modulating out of phase. This would result in one axis appearing slightly stretched every couple frames or so. For example, the width and height progress like so:
t w h
0 640 480
1 638 480
2 638 478
3 636 478
4 634 476
5 634 476

It's likely a combination of both, depending on the circumstance. Given the out-of-phase nature of the hypothesized causes, I think wobbling occurs whenever the number of frames that the effect lasts (duration * FPS) does not share a common factor with the width and height of the image (or something along those lines).

Possible reason your solution wobbles less

resize might be rounding the width and height under the hood. So compared to the width and height modulation for your solution (shown in the earlier table), resize by itself modulates more erratically.

t w h
0 640 480
1 639 479
2 637 478
3 636 477
4 635 476
5 633 475

By evening the width and height, you're essentially forcing the ratio between the width and height to fluctuate less.

@jcbrockschmidt
Copy link
Contributor

Looking at the various resizers in moviepy/video/fx/resize.py it looks like they are in fact casting the width and height to ints. I'm wondering if the aliasing algorithms can account for fractions of pixels. But that may be beyond the scope of what moviepys contract covers, as it's using OpenCV, Pillow, and SciPy to resize.

Proposed solution

Always maintain the ratio of width-to-height by first calculating the new width, then calculating the height based on the width. So replace

        new_size = [
            math.ceil(img.size[0] * (1 + (zoom_ratio * t))),
            math.ceil(img.size[1] * (1 + (zoom_ratio * t)))
        ]

        # The new dimensions must be even.
        new_size[0] = new_size[0] + (new_size[0] % 2)
        new_size[1] = new_size[1] + (new_size[1] % 2)

with something like

w, h = img.size
new_w = w * (1 + (zoom_ratio * t))
new_h = new_w * (h / w)
# Determine the height based on the new width to maintain the aspect ratio.
new_size = (new_w, new_h)

@keikoro keikoro added the fx Related to advanced effects applied via clip.fx(), or effects in general. label Jan 14, 2021
@mondeja mondeja added bug-fix For PRs and issues solving bugs. and removed bug-fix For PRs and issues solving bugs. labels Jan 20, 2021
@kalloszsolty
Copy link

Problem: resize can't handle floating numbers where a smooth zoom would need it.

Solution: Only increase the width & height with the aspect ratio (or multiple of it).
I.E. if the aspect ratio is 4:3, increase the width by 4 and height by 3.
This might be too fast for a zoom effect but won't be wobbling.

An ideal aspect ratio would be a 1:1 ratio and place your content in it masking the space left.
I.E. If your image is 640x480, you can create an empty image of 640x640 and place your image centered inside of it. This way every time you increase the aspect ratio, it will be an integer, which will be digestible by the resize function

I'm about to try out the theory. If anybody's still interested in an example code let me know

@vloe
Copy link

vloe commented Feb 27, 2022

@kalloszsolty I'm interested!! :)

@kalloszsolty
Copy link

kalloszsolty commented Feb 27, 2022

Use this function to create a perfectly square image with even width and height:

def expand2square(img_path):
    pil_img = Image.open(img_path)
    width, height = pil_img.size
    if width == height and width % 2 == 0:
        return pil_img

    if width % 2 != 0:
        width += 1
    if height % 2 != 0:
        height += 1

    elif width > height:
        result = Image.new('RGBA', (width, width))
        result.paste(pil_img, (0, (width - height) // 2))
        return result
    else:
        result = Image.new('RGBA', (height, height))
        result.paste(pil_img, ((height - width) // 2, 0))
        return result

Then just use a lambda function to increase the width and height with the same integer:

starting_scale = 400
scale_speed = 2
expanded_img_1 = expand2square(img_path)
expanded_img_1 = numpy.array(expanded_img_1)
img_clip = (ImageClip(expanded_img_1)
    .set_fps(25)
    .set_duration(4)
    .set_position(('center', 'center'))
    .resize(width=lambda t: starting_scale + round(t * 25 * scale_speed),
            height=lambda t: starting_scale + round(t * 25 * scale_speed)))

@kalloszsolty I'm interested!! :)

@kalloszsolty
Copy link

At the end, I ended up generating separate videos of zooming with command line ffmpeg
(credits to: https://superuser.com/a/1112680)

import subprocess

def run_ffmpeg_zoom(image_path, output_file, screensize, duration=5, fps=25, zoom_ratio=0.0015, zoom_smooth=5):
    ffmpeg_command = f"""./ffmpeg -framerate {fps} -loop 1 -i {image_path} -filter_complex "[0:v]scale={screensize[0] * zoom_smooth}x{screensize[1] * zoom_smooth},
    zoompan=z='min(zoom+{zoom_ratio},1.5)':x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':d={duration * fps},trim=duration={duration}[v1];[ 
    v1]scale={screensize[0]}:{screensize[1]}[v]" -map "[v]" -y {output_file}"""
    process = subprocess.Popen(ffmpeg_command, shell=True, stdout=subprocess.PIPE)
    process.wait()

increase the zoom_smooth parameter for example to 10 to get a smoother zoom (it will also take you more time/resources)

@gaurav-95
Copy link

gaurav-95 commented Jan 3, 2023

For anyone wanting a periodic zoom-in and out effect. I've used a sin wave to replicate that.

# Zoom In and Out effect
def zoom_in_out(t):
    return 2 + 0.5*np.sin(t/6)

clip.resize(zoom_in_out)

@barbarosalp
Copy link

I am getting AttributeError: 'ImageClip' object has no attribute 'resize'. Did you mean: 'size'? and I couldn't understand why. Could you help?

@gaurav-95
Copy link

gaurav-95 commented Jul 2, 2023

I am getting AttributeError: 'ImageClip' object has no attribute 'resize'. Did you mean: 'size'? and I couldn't understand why. Could you help?

Maybe this will help.
#1004

@githubloader
Copy link

Just curious which is the solution? Because the output video still seems to wobble...

@Blackpidev
Copy link

the input is 1 video, what to do!
Looking forward to your support

@gracopordeus
Copy link

i made this one (pretty fast and almost without wobbling) with opencv warpAffine plus some additional options :

def Zoom(clip,mode='in',position='center',speed=1):
    fps = clip.fps
    duration = clip.duration
    total_frames = int(duration*fps)
    def main(getframe,t):
        frame = getframe(t)
        h,w = frame.shape[:2]
        i = t*fps
        if mode == 'out':
            i = total_frames-i
        zoom = 1+(i*((0.1*speed)/total_frames))
        positions = {'center':[(w-(w*zoom))/2,(h-(h*zoom))/2],
                     'left':[0,(h-(h*zoom))/2],
                     'right':[(w-(w*zoom)),(h-(h*zoom))/2],
                     'top':[(w-(w*zoom))/2,0],
                     'topleft':[0,0],
                     'topright':[(w-(w*zoom)),0],
                     'bottom':[(w-(w*zoom))/2,(h-(h*zoom))],
                     'bottomleft':[0,(h-(h*zoom))],
                     'bottomright':[(w-(w*zoom)),(h-(h*zoom))]}
        tx,ty = positions[position]
        M = np.array([[zoom,0,tx], [0,zoom,ty]])
        frame = cv2.warpAffine(frame,M,(w,h))
        return frame
    return clip.fl(main)

you can use this way:

from moviepy.editor import *
import cv2
import numpy as np

img = 'https://www.colorado.edu/cumuseum/sites/default/files/styles/widescreen/public/slider/coachwhip2_1.jpg' #using  the image link above

clip = ImageClip(img).set_fps(30).set_duration(5)
clip = Zoom(clip,mode='in',position='center',speed=1.2) #zoom function above

clip.write_videofile('test.mp4',preset='superfast')

sorry my english.

Worked!

@emorling
Copy link

At the end, I ended up generating separate videos of zooming with command line ffmpeg (credits to: https://superuser.com/a/1112680)

import subprocess

def run_ffmpeg_zoom(image_path, output_file, screensize, duration=5, fps=25, zoom_ratio=0.0015, zoom_smooth=5):
    ffmpeg_command = f"""./ffmpeg -framerate {fps} -loop 1 -i {image_path} -filter_complex "[0:v]scale={screensize[0] * zoom_smooth}x{screensize[1] * zoom_smooth},
    zoompan=z='min(zoom+{zoom_ratio},1.5)':x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':d={duration * fps},trim=duration={duration}[v1];[ 
    v1]scale={screensize[0]}:{screensize[1]}[v]" -map "[v]" -y {output_file}"""
    process = subprocess.Popen(ffmpeg_command, shell=True, stdout=subprocess.PIPE)
    process.wait()

increase the zoom_smooth parameter for example to 10 to get a smoother zoom (it will also take you more time/resources)

Thanks! This is a command version that also works based on this.

ffmpeg -y -loop 1 -t 17 -i /PATH_TO_IMAGE/image-0.png -vf "scale=iw5:ih5,zoompan=z='min(zoom+0.0015,1.5)':x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':d=510,scale=1344:768,trim=duration=17,fps=30" -c:v prores /PATH_TO_OUTPUT/output.mov

@steinathan
Copy link

'ImageClip' object has no attribute 'fl'

im using the updated version of moviepy

@mowshon
Copy link
Author

mowshon commented Aug 29, 2024

@steinathan thanks for your comment, I'll see what changes have been made and update the code.

@steinathan
Copy link

Thanks @mowshon I was able to update it and it works perfectly

the old “.fl” is now “transform” and it works

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature-request Request for a new feature or additional functionality. fx Related to advanced effects applied via clip.fx(), or effects in general.
Projects
None yet
Development

No branches or pull requests