Python+OpenCV+Moviepy实现武汉灯光秀短视频制作详解
本文详细介绍利用Python结合OpenCV与Moviepy制作庆祝武汉重启一周年灯光秀短视频的完整流程。内容涵盖视频结构设计、图像预处理、动态激光特效绘制、帧生成函数逻辑及视频导出方法,通过具体代码示例解析关键技术点,帮助开发者掌握基于Python的视频合成与特效处理技巧。
一、引言
2021年4月8日是武汉重启一周年纪念日,这是一个值得庆祝的时刻。作为一名武汉本地程序员,作者希望以此为契机创作一份独特的纪念作品。受武汉长江两岸灯光秀的启发,作者决定编写程序生成一段庆祝武汉重启的灯光秀短视频。该项目于4月7日晚开始构思与开发,并于4月8日晚顺利完成。实现过程中,作者分别采用了OpenCV以及OpenCV结合Moviepy两种技术方案进行验证。
本文重点阐述基于Python、OpenCV和Moviepy组合实现该视频的技术思路与具体过程。至于纯Python结合OpenCV的实现方案,将在后续独立文章中详细展开。
二、实现思路
2.1、视频内容设计
由于作者自认缺乏艺术细胞,本视频的内容设计主要侧重于技术实现,最终视觉效果仅供参考,无需对艺术性做过高评价。
在创作前,对视频结构进行了初步规划,将其划分为片头、主体内容和片尾三个部分:
- 片头部分:时长5秒,展示一张黄鹤楼照片,并叠加标题文字“武汉重启一周年灯光秀”。
- 主体内容:时长35秒,每隔2秒随机切换一张武汉灯光秀景观图。视频中需包含向上滚动的文字“热烈庆祝武汉重启一周年!”和“武汉万岁!中国万岁!”。此外,在画面左下角和右下角绘制红、绿、蓝三色向上晃动的线条,模拟彩色激光照射效果。
- 片尾部分:时长25秒,每隔2秒随机展示一张武汉风景照片,并显示“制作:老猿Python”等制作信息。
2.2、开发设计
2.2.1、视频图片处理
视频所需的图片均来源于互联网,为确保输出效果一致,所有图片在程序中统一调整至相同尺寸。片头使用的图像通过全局变量保存;主体内容和片尾使用的图像则分别通过两个全局生成器进行访问。这两个生成器能够从图片列表中顺序取出图像,并在到达列表末尾时自动回到第一个元素。当然,也可以使用多个全局变量替代生成器来实现相同功能。
2.2.2、灯光效果处理
在主体内容部分,左下角和右下角的彩色激光效果是通过在背景图片上根据时间动态绘制彩色线条实现的。为了限制激光晃动的范围,设定了激光终点x坐标的最小值和最大值。激光终点的位置随时间动态计算,当x坐标达到最小值或最大值时,激光会自动回扫,从而形成晃动照射的视觉效果。
2.2.3、帧图像的生成
为了满足Moviepy剪辑中get_frame函数仅接受一个时间参数t的要求,上述所有图像处理逻辑被集中封装在一个帧图像生成函数中,该函数仅接收时间t作为参数。
帧图像生成函数根据时间参数判断当前应生成的内容是片头、主体内容还是片尾,并据此生成对应的帧图像。由于需要判断图像是否切换,函数内部记录了上一次切换的时间和切换后的图像。在未达到切换时间前,使用上一张图像作为帧图像的背景;达到切换时间后,则切换新图像作为后续帧生成的背景。为此,函数内部使用了两个全局变量来记录当前帧图像的背景图片和上次切换时间。
2.2.4、输出到视频
为了将视频输出到文件,通过Moviepy构建剪辑对象,指定帧图像生成函数,并附加音频。最后使用剪辑对象的输出方法将视频保存至指定文件,从而生成完整的视频文件。
三、具体实现
3.1、总流程
- 加载片头图像,并构造用于访问视频内容和片尾图像的生成器;
- 构建帧图像生成函数,依据帧图像生成逻辑生成每一帧图像;
- 加载并绑定音频,指定帧率、时长及帧图像生成函数,构造视频剪辑对象;
- 将视频剪辑对象输出到指定文件。
3.2、定义两个图片获取生成器函数
这两个图片获取生成器分别为构造视频主体内容和片尾提供背景图像;
def getLightShowImgFun():#定义灯光秀图片访问生成器函数
lightShowImgList = [cv2.resize(readImgFile(r'f:\pic\武汉\灯光秀-一桥俯瞰.jpg'), (800, 600)),
cv2.resize(readImgFile(r'f:\pic\武汉\灯光秀-一桥激光.jpg'), (800, 600)),
cv2.resize(readImgFile(r'f:\pic\武汉\灯光秀-二七桥.jpg'), (800, 600)),
cv2.resize(readImgFile(r'f:\pic\武汉\灯光秀-二桥.jpg'), (800, 600)),
cv2.resize(readImgFile(r'f:\pic\武汉\灯光秀-晴川桥.jpg'), (800, 600)),
cv2.resize(readImgFile(r'f:\pic\武汉\灯光秀-月湖.jpg'), (800, 600)),
cv2.resize(readImgFile(r'f:\pic\武汉\灯光秀-汉口.jpg'), (800, 600)),
cv2.resize(readImgFile(r'f:\pic\武汉\灯光秀-汉口3.jpg'), (800, 600)),
cv2.resize(readImgFile(r'f:\pic\武汉\灯光秀-汉口江景.jpg'), (800, 600)),
cv2.resize(readImgFile(r'f:\pic\武汉\灯光秀-远光青山.jpg'), (800, 600)),
cv2.resize(readImgFile(r'f:\pic\武汉\灯光秀-隔江看汉口.jpg'), (800, 600)),
cv2.resize(readImgFile(r'f:\pic\武汉\灯光秀-青山.jpg'), (800, 600)),
cv2.resize(readImgFile(r'f:\pic\武汉\灯光秀-黄鹤楼.jpg'), (800, 600)),
cv2.resize(readImgFile(r'f:\pic\武汉\灯光秀-龟山电视塔.jpg'), (800, 600)),
cv2.resize(readImgFile(r'f:\pic\武汉\灯光秀_一桥.jpg'), (800, 600)),
cv2.resize(readImgFile(r'f:\pic\武汉\灯光秀_一桥底部.jpg'), (800, 600)),
cv2.resize(readImgFile(r'f:\pic\武汉\灯光秀_一桥远景.jpg'), (800, 600)),
cv2.resize(readImgFile(r'f:\pic\武汉\灯光秀_桥.jpg'), (800, 600)),
cv2.resize(readImgFile(r'f:\pic\武汉\灯光秀_桥上灯.jpg'), (800, 600)),
cv2.resize(readImgFile(r'f:\pic\武汉\灯光秀_武汉江边.jpg'), (800, 600)),
cv2.resize(readImgFile(r'f:\pic\武汉\灯光秀_远桥.jpg'), (800, 600)),
cv2.resize(readImgFile(r'f:\pic\武汉\灯光秀江景.jpg'), (800, 600)),
cv2.resize(readImgFile(r'f:\pic\武汉\灯光秀江滩.jpg'), (800, 600)),
cv2.resize(readImgFile(r'f:\pic\武汉\灯光秀激光.jpg'), (800, 600)),
cv2.resize(readImgFile(r'f:\pic\武汉\灯光秀远观汉口.jpg'), (800, 600))]
index = 0
count = len(lightShowImgList)
while True:
index += 1
if index>=count:index = 1
yield lightShowImgList[index-1]
def getWHImgFunc():#定义片尾图片访问生成器函数
# 片尾背景图片
whImgList = [cv2.resize(readImgFile(r'f:\pic\武汉\东湖1.jpg'), (800, 600)),
cv2.resize(readImgFile(r'f:\pic\武汉\东湖2.jpg'), (800, 600)),
cv2.resize(readImgFile(r'f:\pic\武汉\东湖樱园樱花.jpg'), (800, 600)),
cv2.resize(readImgFile(r'f:\pic\武汉\武大牌楼.jpg'), (800, 600)),
cv2.resize(readImgFile(r'f:\pic\武汉\武大牌楼远观.jpg'), (800, 600)),
cv2.resize(readImgFile(r'f:\pic\武汉\武大樱花2.jpg'), (800, 600)),
cv2.resize(readImgFile(r'f:\pic\武汉\武大樱园顶高拍照.jpg'), (800, 600)),
cv2.resize(readImgFile(r'f:\pic\武汉\武大樱园门洞.jpg'), (800, 600)),
cv2.resize(readImgFile(r'f:\pic\武汉\武大樱园入口.jpg'), (800, 600)),
cv2.resize(readImgFile(r'f:\pic\武汉\武大老图书馆.jpg'), (800, 600))]
index = 0
count = len(whImgList)
while True:
index += 1
if index>=count:index = 1
yield whImgList[index-1]
3.3、全局变量初始化
全局变量主要包括视频片头使用的图片hhlImg、两个图像访问生成器,以及preImg和preTime,用于记录上次切换视频图片的图像和切换时间。
#片头背景图片
hhlImg = cv2.resize(readImgFile(r'f:\pic\武汉\黄鹤楼.jpg'),(800,600))
#灯光秀背景图片和片尾图片全局生成器初始化
getLightShowImg = getLightShowImgFun()
getWHImg = getWHImgFunc()
#上次切换背景图片和切换时间全局变量初始化
preImg,preTime = None,0
3.4、实现给背景图像添加彩色激光照射效果
lightShowImg函数实现了在背景图像指定点发射彩色激光的特效。激光发射点固定,由参数lightStartPos指定;终点随参数t在一定范围内变化,终点x坐标受参数minX和maxX控制;同一发射源的三条激光之间的间距由参数distance控制。
def lightShowImg(bg,minX, maxX,distance,lightStartPos,t):
"""
实现在背景图像上添加当前散发彩色激光的处理
:param bg: 背景图像
:param minX:灯光终点的最大x坐标
:param maxX:灯光终点的最小x坐标
:param distance: 不同灯光之间的间距
:param lightStartPos: 灯光发射点
:param t: 时间t
:return: 添加了发射灯光的图像
"""
x = (minX+int(t*200))%(maxX*2) #按时间t计算灯光终点的x坐标,该坐标可以超出背景图像范围
img = np.array(bg)
if x>maxX: #到达最大范围,需要回扫
x = 2*maxX-x
color1,color2,color3 = (255,0,0),(0,255,0),(0,0 ,255 ) #蓝、绿、红三色
cv2.line(img, lightStartPos, (x, 0), color1, 4)
cv2.line(img, lightStartPos, (x + distance, 0), color2, 4)
cv2.line(img, lightStartPos, (x - distance, 0), color3, 4)
return img
3.4、帧图片生成
makeframe函数实现帧图片生成,带有一个参数t表示当前帧对应的剪辑时间。在函数内部,根据剪辑时间判断是生成片头内容、灯光秀内容还是片尾内容,并生成t时刻对应的帧图像返回。
def makeframe(t):
"""
makeframe函数实现帧图片生成,带一个参数t表示当前帧对应的剪辑时间,在函数内根据剪辑时间来判断是生成片头内容、灯光秀内容还是片尾内容生成t时刻对应帧图像返回:
1. 对于片头,采用黄鹤楼照片作为背景,并在图片中央显示“武汉重启一周年灯光秀”;
2. 对于视频内容,则每2秒从灯光秀图片队列中随机取一张图片作为当前背景图像,并调用lightShowImg增加左下角和右下角的彩色激光效果,同时动态向上滚动显示“热烈庆祝武汉重启一周年”、“武汉万岁!中国万岁!”等标语;
3. 对于片尾,则每2秒从武汉风景图片队列中随机取一张图片作为当前背景图像,同时动态向上滚动显示制作信息。
:param t: 生成帧对应剪辑的时间位置
:return: 对应帧图像
"""
#生成t时刻的视频帧图像
global preImg,preTime
print(f"\rt={t:4.2f}", end='')
if t<5:#5秒片头
img = imgAddText(hhlImg,'武汉重启一周年灯光秀',64,(255,0,0),vRefPos='C')
elif t<40:#5-40秒灯光秀
minX, maxX = 200, 1200 # 灯光横向扫射范围
lightStartPos1 = (0, 600) # 灯光1的发射点坐标设置为左下角
lightStartPos2 = (800, 600) # 灯光2的发射点坐标设置为右下角
if (t-preTime)>2 or preImg is None:
img = next(getLightShowImg)
preImg,preTime = img,t
else:
img = preImg
img = lightShowImg(img, minX, maxX, 80,lightStartPos1, t)
img = lightShowImg(img, minX-100, maxX+100,48, lightStartPos2, t)
t = int((t-4)*40)%img.shape[0]
img = imgAddText(img,'热烈庆祝武汉重启一周年!',48,(0,0,255),vRefPos=-80-t,)
img = imgAddText(img, '武汉万岁!中国万岁!',48,(255,0,255),vRefPos=-t)
else:#片尾
if (t-preTime)>2 or preImg is None:
img = next(getWHImg)
preImg,preTime = img,t
else:
img = preImg
t = int((t - 39) * 20) % img.shape[0]
img = imgAddText(img,"制作:老猿Python",36,(255,255,255),vRefPos=-120-t)
img = imgAddText(img, "https://blog.csdn.net/LaoYuanPython",24,(255,255,255),vRefPos= -60-t)
img = imgAddText(img, "2021年4月8日", 24, (255,255,255), vRefPos=-t)
img = cv2.cvtColor(img,cv2.COLOR_BGR2RGB)
return img
3.5、制作视频文件
函数buildVideo用于制作视频文件:
def buildVideo():
#构建视频
audio = AudioFileClip(r"F:\video\友谊之光.mp3").set_duration(65)
clip = VideoClip(makeframe, False, 65).set_fps(24).set_audio(audio)
clip.write_videofile(r"F:\video\lightShow.avi", codec='png', threads=8)
调用buildVideo函数即可完成视频制作。
3.6、视频效果

四、小结
本文完整介绍了使用Python、OpenCV和Moviepy制作庆祝武汉重启一周年灯光秀短视频的实现思路、过程及关键函数,有助于理解OpenCV的图像操作和Moviepy生成视频的实现原理。
到此这篇关于用Python制作灯光秀短视频的文章就介绍到这了,更多相关python制作短视频内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













