Last active
May 17, 2024 02:41
-
-
Save shimpe/f74a3a19960553390808d31445e59f56 to your computer and use it in GitHub Desktop.
fancy captions with moviepy
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import PySide6.QtSvg | |
import PySide6.QtGui | |
import PySide6.QtCore | |
import moviepy.video.VideoClip | |
import moviepy.editor | |
import numpy as np | |
from moviepy.editor import * | |
from vectortween.PointAnimation import PointAnimation | |
from vectortween.SequentialAnimation import SequentialAnimation | |
# set up some properties of the clip we are going to generate | |
fps = 25 | |
duration = 10 | |
W = 1000 | |
H = 500 | |
# some helper code to convert SVG to png | |
def toNumpy(incomingImage): | |
''' Converts a QImage into numpy format ''' | |
incomingImage = incomingImage.convertToFormat(PySide6.QtGui.QImage.Format.Format_RGB32) | |
width = incomingImage.width() | |
height = incomingImage.height() | |
ptr = incomingImage.bits() | |
arr = np.array(ptr).reshape(height, width, 4) # Copies the data | |
return arr[:, :, :3] # remove alpha channel | |
# need to create dummy application to access font database | |
dummyapp = PySide6.QtGui.QGuiApplication() | |
# dynamically build a function that generates frames based on animation parameters and an svg text template | |
def make_frame_with_animation_and_text(xyanimation, anim_fraction, total_duration, text): | |
""" | |
xyanimation: a PointAnimation to vary the x, y position of the text. (0, 0) is centered in the middle of the screen. | |
anim_fraction: how long should the animation take to complete relative to the total_duration of the clip (e.g. 0.9 is 90%) | |
total_duration: total duration of the clip in seconds | |
text: svg text template (see example code below) | |
""" | |
def make_frame(t): | |
df = total_duration * fps | |
tf = t * fps | |
x, y = xyanimation.make_frame(tf, 0, 0, anim_fraction * df, df, None) | |
svg = f"""<?xml version="1.0" encoding="UTF-8"?> | |
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" | |
width="{W}" height="{H}" viewBox="{-W / 2} {-H / 2} {W} {H}"> | |
{text.format(x=x, y=y)} | |
</svg> | |
""" | |
r = PySide6.QtSvg.QSvgRenderer(bytes(svg, "utf-8")) | |
i = PySide6.QtGui.QImage(PySide6.QtCore.QSize(W, H), PySide6.QtGui.QImage.Format_RGB32) | |
i.fill(PySide6.QtGui.QColor(0, 0, 0)) | |
p = PySide6.QtGui.QPainter(i) | |
r.render(p) | |
p.end() | |
return toNumpy(i.rgbSwapped()) | |
return make_frame | |
# define some fancy styles we can use while defining texts | |
my_styles = { | |
"normal": """fill="black" stroke="white" stroke-width="2px" font-size="55" font-family="sans-serif" text-anchor="middle" """, | |
"h1": """fill="blue" stroke="gold" stroke-width="4px" font-size="80" font-family="serif" text-anchor="middle" """, | |
"h2": """fill="gold" stroke="maroon" stroke-width="2px" font-size="90" font-family="serif" font-style="italic" text-anchor="middle" """, | |
"h3": """fill="green" stroke="white" stroke-width="6px" font-size="200" font-family="serif" text-anchor="middle" """, | |
"small": """fill="red" stroke="white" stroke-width="5px" font-size="30" font-weight="bold" font-family="serif" text-anchor="middle" """ | |
} | |
# define some fancy animations we can use to move our text around the screen | |
anim = { | |
"top_to_bottom": PointAnimation((-W, -H / 2), (0, H / 3), tween=['easeOutBounce'], ytween=['easeOutBounce']), | |
"topright_to_left": PointAnimation((W + 10, -H), (0, 0), tween=['easeOutQuad'], ytween=['easeOutCirc']), | |
"multi": SequentialAnimation([ | |
PointAnimation((-W / 5, -H / 3), (W / 3, H / 2), tween=['easeOutBounce'], ytween=['easeOutQuad']), | |
PointAnimation((W / 3, H / 2), (-W / 5, H / 5), tween=['easeOutQuad'], ytween=['easeOutBounce']), | |
PointAnimation((-W / 5, H / 5), (W / 5, -H / 3), tween=['easeOutQuad'], ytween=['easeOutQuad']), | |
PointAnimation((W / 5, -H / 3), (-W / 5, -H / 3), tween=['easeOutQuad'], ytween=['easeOutBounce']), | |
], timeweight=[1, 0.5, 0.5, 1], repeats=3) | |
} | |
# define some texts | |
# each line of text must be defined separately, and can be styled and animated separately | |
# text svg template must take include x="{{x}}" y="{{y}}" properties for an animation to have effect | |
my_text = """<text {normal} x="{{x}}" y="{{y}}">Outline text <tspan {h1}>with</tspan> <tspan {h2}>highlights</tspan>!</text>""".format( | |
**my_styles) | |
# render clip with text line | |
txt_clip = moviepy.video.VideoClip.VideoClip( | |
make_frame=make_frame_with_animation_and_text(anim['top_to_bottom'], 0.25, duration, my_text), \ | |
duration=duration / 2) | |
# make black (= background) transparent so we can overlay text lines later on | |
txt_clip = moviepy.editor.vfx.mask_color(txt_clip, color=[0, 0, 0]) | |
my_2nd_text = """<text {normal} x="{{x}}" y="{{y}}">Follow-up <tspan {h3}>to</tspan> <tspan {h1}>first part?!</tspan></text>""".format( | |
**my_styles) | |
_2nd_txt_clip = moviepy.video.VideoClip.VideoClip( | |
make_frame=make_frame_with_animation_and_text(anim['topright_to_left'], 0.1, duration, my_2nd_text), \ | |
duration=duration / 2) | |
_2nd_txt_clip = moviepy.editor.vfx.mask_color(_2nd_txt_clip, | |
color=[0, 0, 0]) # make black transparent so we can overlay clips | |
my_3rd_text = """<text {small} x="{{x}}" y="{{y}}">MOVIEPY ROCKS!</text>""".format(**my_styles) | |
_3rd_txt_clip = moviepy.video.VideoClip.VideoClip( | |
make_frame=make_frame_with_animation_and_text(anim['multi'], 1.0, duration, my_3rd_text), \ | |
duration=duration | |
) | |
_3rd_txt_clip = moviepy.editor.vfx.mask_color(_3rd_txt_clip, | |
color=[0, 0, 0]) # make black transparent so we can overlay clips | |
# Overlay all text clips for some fancy animation | |
video = CompositeVideoClip([txt_clip, _2nd_txt_clip, _3rd_txt_clip]) | |
# Write video file with the result | |
video.write_gif("test_with_svg.gif", fps=fps) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
see: Example of what this produces.