起因
最近在学 scrapy 爬虫,顺手从微博上爬了张子枫和谭松韵的微博相片。然后就想,要不就顺手用爬的相片做个视频出来?
需求&思路
我的思路是这样子的:
- 这个视频是 1080p 30fps;
- 我要给这个视频选一首好听的 BGM,然后每一幅相片都按 BGM 的节拍来显示;
- 视频长度和 BGM 长度匹配;
- 获取相片并将相片分辨率处理成和视频一样;
- 按节拍创建剪辑片段;
- 合并剪辑,生成视频
准备工作
- 由于我机器上使用的是 python 3 的环境,所以需要有 python 3。
- 建议在 python 的虚拟环境中运行这个项目。
- 在虚拟环境中安装
moviepy
librosa
click
三个包。
- 如果要加字幕,还需要安装
imagemagick
。
- 如果需要对字幕做分词处理,再安装一个
jieba
。
我用的是 macbook,所有的命令都是在 macos 环境中执行。linux 的过程大同小异,windows 下需要自行搞定 python。
先分析一下最终的目录结构和文件
1 2 3 4 5 6 7 8 9 10 11
| . ├── ./createVideoWithMoviepy.py ├── ./dist │ └── ./dist/1353112775.mp4 ├── ./src │ ├── ./src/font │ ├── ./src/images -> ~/.../idols/images_origin │ ├── ./src/imgs │ ├── ./src/music │ └── ./src/subtitles └── ./utils.py
|
假设 python3 环境已经存在。
1 2
| mkdir -p ./{moviepy/dist,moviepy/src/imgs,moviepy/src/music}
|
可有注意到,这里我没有创建moviepy/src/images
这个目录?由于我爬取的相片在另外一个项目目录里idols/images_origin
,所以这里将创建一个软链接来指向源目录。
1 2 3 4
| cd moviepy/src && ll ln -s ~/idols/images_origin images ll
|
目录有了,接下来准备开发环境。
1 2 3 4 5 6 7 8 9 10 11
| cd moviepy
python3 -m venv venv
. venv/bin/activate
|
接下来,安装我们的依赖moviepy
librosa
click
,另外,可能会需要处理图片,需要额外安装PIL
。
1 2 3
| pip install moviepy librosa click
pip install pillow
|
最后,创建我们的两个脚本文件
1
| touch createVideoWithMoviepy.py utils.py
|
正式开始
获取创建视频的一些参数
:param width: 生成视频的宽度 default: 1920
:param height: 生成视频的高度 default: 1080
:param images_origin: 源图片的路径
:param origin_target_dir: 将源路径替换为目标路径。
这个项目的源路径是爬虫下载图片的路径,最后还要把下载的图片处理成和视频一样的大小存放在目标目录
default: (‘./src/images’, ‘./src/imgs’)
:param music: 背景音乐路径
:param fps: 视频的帧率 default: 30
:param output: 生成视频的文件名
如此,我们先来定义要获取的参数。这里用到了Click,它以简单的交互方式,帮助我在命令行模式下获取需要的参数值。
# createVideoWithMoviepy.py @python31 2 3 4 5 6 7 8 9 10 11
| import click
@click.command() @click.option('--width', prompt='Width', default=1920, help='The width of video clips') @click.option('--height', prompt='Height', default=1080, help='The height of video clips') @click.option('--images_origin', prompt='images file', default='./src/images', help='The source images path') @click.option('--origin_target_dir', prompt='replace origin dir to target dir', default=('./src/images', './src/imgs'), help='how replace origin dir to target dir') @click.option('--music', prompt='Music file', default='./src/music/1302.mp3', help='The music file') @click.option('--fps', prompt='video fps ', default=30, help='The output video fps') @click.option('--output', prompt='Output file', default='./dist/1353112775.mp4', help='The output file name')
|
获取相片并调整相片大小
# createVideoWithMoviepy.py @python31 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| print('>>>>>>>>>>>>>>开始加载库>>>>>>>>>>>>>>') import os import math import click
from utils import resizeImage, readDir
def main(width, height, images_origin, origin_target_dir, music, fps, output): target_images_dir = images_origin.replace(origin_target_dir[0], origin_target_dir[-1])
if not os.path.exists(target_images_dir): filesPath = readDir(images_origin) print('>>已载入文件 %s 个' % len(filesPath)) print('>>>>>>>>>>>>>>开始调整图片大小>>>>>>>>>>>>>>') for img in filesPath: resizeImage(img, origin_target_dir, (width, height))
print('>>>>>>>>>>>>>>开始读取调整过的图片>>>>>>>>>>>>>>') filesPath = readDir(images_origin.replace(origin_target_dir[0], origin_target_dir[-1])) print('>>已载入文件 %s 个' % len(filesPath))
if len(filesPath) == 0: print('>>请检查源目录 %s 下是否有图片。并且删除 %s 目录' % (images_origin, origin_target_dir[-1])) os._exit(0)
|
使用 librosa 分析 BGM 的节拍
# createVideoWithMoviepy.py @python31 2 3 4 5 6 7
| import librosa
print('>>>>>>>>>>>>>>开始分析节拍>>>>>>>>>>>>>>') y, sr = librosa.load(music, sr=None) tempo, beats = librosa.beat.beat_track(y=y, sr=sr) beat_times = list(librosa.frames_to_time(beats, sr=sr)) beat_times.append(beat_times[-1] + 1)
|
创建视频剪辑(Clip)前的一些小准备
先简单说一下视频/动画的小原理,我们看到的每一秒画面,一般都是由很多帧组成的。在这个项目里,一秒画面有 30 帧(30fps)。而一个动作的连续画面,至少要有头尾两个动作关键帧。但是现在只有单幅的相片,表现不出一个完整的动作,所以我们最终的成品会是一个幻灯片。😅
到这里就大概清楚了,如果要让一幅照片播放一秒,那就需要在这一秒里插入 30 帧。因为是单幅,所以这 30 帧就是这幅照片重复 30 次。
项目要求我们按照节拍来显示相片,但是两个节拍之间的间隔可能不到一秒,所以相片的帧数可能有多有少,不一定是 30 帧。 而且一秒的画面停留时间太短,也就是一闪而过的效果,根本看不清,那怎么办呢?
#createVideoWithMoviepy.py @python31 2 3 4 5 6 7 8 9 10 11
| clips = [] audio_time = librosa.get_duration(filename=music) print('>>音频时长(s):%f >>节拍数量:%s' % (audio_time, len(beat_times))) ''' 计算节拍数量和相片数量的差值比例,以计算需要在每幅相片后补足多少帧, 这里计算的差值,是为了让相片能补足节拍的数量,多跨几个节拍,让每一幅相片停留更长时间。 另外,这个算法有 bug,下面再说。 ''' interval = math.ceil(abs( len(beat_times) / len(filesPath) )) filesPath = [f for f in filesPath for i in range( interval )] print('>>新的文件列表长度:%s' % len(filesPath))
|
按节拍插入帧
# createVideoWithMoviepy.py @python31 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| print('>>>>>>>>>>>>>>开始按节拍生成视频帧>>>>>>>>>>>>>>') for index, beat_time in enumerate(beat_times[:-1]): if index >= len( filesPath): print('>>图片数量不足以匹配节拍,中止匹配。输出的视频后段可能会出现黑屏。') print('>>图片数量:{0} >节拍数量:{1}'.format(len( filesPath), len(beat_times))) break print(f'{index + 1}/{len(beat_times)}>>{ filesPath[index]}') ''' 说下 time_deff 的作用: 因为每个节拍间的间隔时间非常短,所以计算节拍间的间隔再乘以帧率, 得出这幅画面在这个节拍应该停留的时间 '''` time_diff = math.modf(beat_time - beat_times[index -1]) time_diff = math.ceil(time_diff[0]*10) if (time_diff[0] * 10) > time_diff[-1] else math.ceil(time_diff[-1]) image_clip = (ImageClip(filesPath[index], duration=abs(time_diff)*fps) .set_fps(abs(time_diff)*fps) .set_start(beat_time) .set_end(beat_times[index + 1])) image_clip = image_clip.set_pos('center') if index % interval == 0: image_clip = image_clip.fx(vfx.fadein, duration=0.5) clips.append(image_clip)
|
合并剪辑,生成视频
终于到最后一步了!
# createVideoWithMoviepy.py1 2 3 4 5 6 7 8 9 10 11 12 13 14
| print('>>>>>>>>>>>>>>开始合并剪辑,生成视频>>>>>>>>>>>>>>') final_clip = CompositeVideoClip(clips) audio_clip = AudioFileClip(music) final_video = final_clip.set_audio(audio_clip)
final_video.write_videofile( output, fps=fps, codec='mpeg4', preset='medium', audio_codec="libmp3lame", threads=4, bitrate ='6000k')
|
一个问题
还记得上面提到的那个 bug 吗?
这里面有一些问题,如果图片数量多于节拍数?如果补齐节拍数量后的图片 list length 多于节拍数?
如何确定匹配 bgm 节拍的最佳图片数量和帧率?
总结
这个脚本问题还是挺多的,但是完成了我最初的要求。唯一不满意的就是这是一个加了 BGM 的幻灯片,我下次还是爬视频吧。
moviepy 做逐帧动画不是强项,效率低于 opencv,但是如果想要批量修改一些比较相似的视频剪辑,还是可以用的。这货有一个大缺点,就是太费内存了,文档也不够友好。但是,好用就够了。
librosa 这个库也很强大,能够各种分析音频,对声音领域有要求的小伙伴可以试试。
大家关心的源码,Create video with Moviepy
评论