前段时间接触了一个批量抠图的模型库,而后在一些视频中找到灵感,觉得应该可以通过抠图的方式,给视频换一个不同的场景,于是就有了今天的文章。
我们先看看能实现什么效果,先来个正常版的,先看看原场景:
下面是我们切换场景后的样子:
看起来效果还是不错的,有了这个我们就可以随意切换场景,坟头蹦迪不是梦。另外,我们再来看看另外一种效果,相比之下要狂放许多:
实现步骤
我们都知道,视频是由一帧一帧的画面组成的,每一帧都是一张图片,我们要实现对视频的修改就需要对视频中每一帧画面进行修改。所以在最开始,我们需要获取视频每一帧画面。
在我们获取帧之后,需要抠取画面中的人物。
抠取人物之后,就需要读取我们的场景图片了,在上面的例子中背景都是静态的,所以我们只需要读取一次场景。在读取场景之后我们切换每一帧画面的场景,并写入新的视频。
这时候我们只是生成了一个视频,我们还需要添加音频。而音频就是我们的原视频中的音频,我们读取音频,并给新视频设置音频就好了。
具体步骤如下:
- 读取视频,获取每一帧画面
- 批量抠图
- 读取场景图片
- 对每一帧画面进行场景切换
- 写入视频
- 读取原视频的音频
- 给新视频设置音频
因为上面的步骤还是比较耗时的,所以在视频完成后通过邮箱发送通知,告诉我视频制作完成。
模块安装
我们需要使用到的模块主要有如下几个:
pillow opencv moviepy paddlehub
我们都可以直接用pip安装:
pip install pillow pip install opencv-python pip install moviepy
其中OpenCV有一些适配问题,建议选取3.0以上版本。
在我们使用paddlehub之前,我们需要安装paddlepaddle:具体安装步骤可以参见官网。用paddlehub抠图参考:别再自己抠图了,Python用5行代码实现批量抠图。我们这里直接用pip安装cpu版本的:
# 安装paddlepaddle python -m pip install paddlepaddle -i https://mirror.baidu.com/pypi/simple # 安装paddlehub pip install -i https://mirror.baidu.com/pypi/simple paddlehub
有了这些准备工作就可以开始我们功能的实现了。
具体实现
我们导入如下包:
import cv2 # opencv import mail # 自定义包,用于发邮件 import math import numpy as np from PIL import Image # pillow import paddlehub as hub from moviepy.editor import *
其中Pillow和opencv导入的名称不太一样,还有就是我自定义的mail模块。另外我们还要先准备一些路径:
# 当前项目根目录,系统自动获取当前目录 BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".")) # 每一帧画面保存的地址 frame_path = BASE_DIR + '\\frames\\' # 抠好的图片位置 humanseg_path = BASE_DIR + '\\humanseg_output\\' # 最终视频的保存路径 output_video = BASE_DIR + '\\result.mp4'
接下来我们按照上面说的步骤一个一个实现。
(1)读取视频,获取每一帧画面
在OpenCV中提供了读取帧的函数,我们只需要使用VideoCapture类读取视频,然后调用read函数读取帧,read方法返回两个参数,ret为是否有下一帧,frame为当前帧的ndarray对象。完整代码如下:
def getFrame(video_name, save_path): """ 读取视频将视频逐帧保存为图片,并返回视频的分辨率size和帧率fps :param video_name: 视频的名称 :param save_path: 保存的路径 :return: fps帧率,size分辨率 """ # 读取视频 video = cv2.VideoCapture(video_name) # 获取视频帧率 fps = video.get(cv2.CAP_PROP_FPS) # 获取画面大小 width = int(video.get(cv2.CAP_PROP_FRAME_WIDTH)) height = int(video.get(cv2.CAP_PROP_FRAME_HEIGHT)) size = (width, height) # 获取帧数,用于给图片命名 frame_num = str(video.get(7)) name = int(math.pow(10, len(frame_num))) # 读取帧,ret为是否还有下一帧,frame为当前帧的ndarray对象 ret, frame = video.read() while ret: cv2.imwrite(save_path + str(name) + '.jpg', frame) ret, frame = video.read() name += 1 video.release() return fps, size
在标处,我获取了帧的总数,然后通过如下公式获取比帧数大的整十整百的数:
frame_name = math.pow(10, len(frame_num))
这样做是为了让画面逐帧排序,这样读取的时候就不会乱。另外我们获取了视频的帧率和分辨率,这两个参数在我们创建视频时需要用到。这里需要注意的是opencv3.0以下版本获取帧率和画面大小的写法有些许差别。
(2)批量抠图
批量抠图需要用到paddlehub中的模型库,代码很简单,这里就不多说了:
def getHumanseg(frames): """ 对帧图片进行批量抠图 :param frames: 帧的路径 :return: """ # 加载模型库 humanseg = hub.Module(name='deeplabv3p_xception65_humanseg') # 准备文件列表 files = [frames + i for i in os.listdir(frames)] # 抠图 humanseg.segmentation(data={'image': files})
我们执行上面函数后会在项目下生成一个humanseg_output目录,抠好的图片就在里面。
(3)读取场景图片
这也是简单的图片读取,我们使用pillow中的Image对象:
def readBg(bgname, size): """ 读取背景图片,并修改尺寸 :param bgname: 背景图片名称 :param size: 视频分辨率 :return: Image对象 """ im = Image.open(bgname) return im.resize(size)
这里的返回的对象并非ndarray对象,而是Pillow中定义的类对象。
(4)对每一帧画面进行场景切换
简单来说就是将抠好的图片和背景图片合并,我们知道抠好的图片都在humanseg_output目录,这也就是为什么最开始要准备相应的变量存储该目录的原因:
def setImageBg(humanseg, bg_im): """ 将抠好的图和背景图片合并 :param humanseg: 抠好的图 :param bg_im: 背景图片,这里和readBg()函数返回的类型一样 :return: 合成图的ndarray对象 """ # 读取透明图片 im = Image.open(humanseg) # 分离色道 r, g, b, a = im.split() # 复制背景,以免源背景被修改 bg_im = bg_im.copy() # 合并图片 bg_im.paste(im, (0, 0), mask=a) return np.array(bg_im.convert('RGB'))[:, :, ::-1]
在标处,我们复制了背景,如果少了这一步的话,生成的就是我们上面的“千手观音效果”了。
其它步骤都很好理解,只有返回值比较长,我们来详细看一下:
# 将合成图转换成RGB,这样A通道就没了 bg_im = bg_im.convert('RGB') # 将Image对象转换成ndarray对象,方便opencv读取 im_array = np.array(bg_im) # 此时im_array为rgb模式,而OpenCV为bgr模式,我们通过下面语句将rgb转换成bgr bgr_im_array = im_array[:, :, ::-1]
最后bgr_im_array就是我们最终的返回结果。
(5)写入视频
为了节约空间,我并非等将写入图片放在合并场景后面,而是边合并场景边写入视频:
def writeVideo(humanseg, bg_im, fps, size): """ :param humanseg: jpg图片的路径 :param bgname: 背景图片 :param fps: 帧率 :param size: 分辨率 :return: """ # 写入视频 fourcc = cv2.VideoWriter_fourcc(*'mp4v') out = cv2.VideoWriter('green.mp4', fourcc, fps, size) # 将每一帧设置背景 files = [humanseg + i for i in os.listdir(humanseg)] for file in files: # 循环合并图片 im_array = setImageBg(file, bg_im) # 逐帧写入视频 out.write(im_array) out.release()
上面的代码也非常简单,执行完成后项目下会生成一个green.mp4,这是一个没有音频的视频,后面就需要我们获取音频然后混流了。
(6)读取原视频的音频
因为在opencv中没找到音频相关的处理,所以选用moviepy,使用起来也非常方便:
def getMusic(video_name): """ 获取指定视频的音频 :param video_name: 视频名称 :return: 音频对象 """ # 读取视频文件 video = VideoFileClip(video_name) # 返回音频 return video.audio
然后就是混流了。
(7)给新视频设置音频
这里同样使用moviepy,传入视频名称和音频对象进行混流:
def addMusic(video_name, audio): """实现混流,给video_name添加音频""" # 读取视频 video = VideoFileClip(video_name) # 设置视频的音频 video = video.set_audio(audio) # 保存新的视频文件 video.write_videofile(output_video)
其中output_video是我们在最开始定义的变量。
(8)删除过渡文件
在我们生产视频时,会产生许多过渡文件,在视频合成后我们将它们删除:
def deleteTransitionalFiles(): """删除过渡文件""" frames = [frame_path + i for i in os.listdir(frame_path)] humansegs = [humanseg_path + i for i in os.listdir(humanseg_path)] for frame in frames: os.remove(frame) for humanseg in humansegs: os.remove(humanseg)
最后就是将整个流程整合一下。
(8)整合
我们将上面完整的流程合并成一个函数:
def changeVideoScene(video_name, bgname): """ :param video_name: 视频的文件 :param bgname: 背景图片 :return: """ # 读取视频中每一帧画面 fps, size = getFrame(video_name, frame_path) # 批量抠图 getHumanseg(frame_path) # 读取背景图片 bg_im = readBg(bgname, size) # 将画面一帧帧写入视频 writeVideo(humanseg_path, bg_im, fps, size) # 混流 addMusic('green.mp4', getMusic(video_name)) # 删除过渡文件 deleteTransitionalFiles()
(9)在main中调用
我们可以把前面定义的路径也放进了:
if __name__ == '__main__': # 当前项目根目录 BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".")) # 每一帧画面保存的地址 frame_path = BASE_DIR + '\\frames\\' # 抠好的图片位置 humanseg_path = BASE_DIR + '\\humanseg_output\\' # 最终视频的保存路径 output_video = BASE_DIR + '\\result.mp4' if not os.path.exists(frame_path): os.makedirs(frame_path) try: # 调用函数制作视频 changeVideoScene('jljt.mp4', 'bg.jpg') # 当制作完成发送邮箱 mail.sendMail('你的视频已经制作完成') except Exception as e: # 当发生错误,发送错误信息 mail.sendMail('在制作过程中遇到了问题' + e.__str__())
这样我们就完成了完整的流程。
发送邮件
邮件的发送又是属于另外的内容了,我定义了一个mail.py文件,具体代码如下:
import smtplib from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart # 一封邮件 def sendMail(msg): # sender = '发件人' to_list = [ '收件人' ] subject = '视频制作情况' # 创建邮箱 em = MIMEMultipart() em['subject'] = subject em['From'] = sender em['To'] = ",".join(to_list) # 邮件的内容 content = MIMEText(msg) em.attach(content) # 发送邮件 # 1、连接服务器 smtp = smtplib.SMTP() smtp.connect('smtp.163.com') # 2、登录 smtp.login(sender, '你的密码或者授权码') # 3、发邮件 smtp.send_message(em) # 4、关闭连接 smtp.close()
里面的邮箱我是直接写死了,大家可以自由发挥。为了方便,推荐发件人使用163邮箱,收件人使用QQ邮箱。另外在登录的时候直接使用密码比较方便,但是有安全隐患。
总结
老实说上述程序的效率非常低,不仅占空间,而且耗时也比较长。在最开始我切换场景选择的是遍历图片每一个像素,而后找到了更加高效的方式取代了。但是帧画面的保存,和jpg图片的存储都很耗费空间。
另外程序设计还是有许多不合理的地方,像是ndarray对象和Image的区分度不高,另外有些函数选择传入路径,而有些函数选择传入文件对象也很容易让人糊涂。
最后说一下,我们用上面的方式不仅可以做静态的场景切换,还可以做动态的场景切换,这样我们就可以制作更加丰富的视频。当然,效率依旧是个问题!
免责声明:本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除!
更新日志
- 凤飞飞《我们的主题曲》飞跃制作[正版原抓WAV+CUE]
- 刘嘉亮《亮情歌2》[WAV+CUE][1G]
- 红馆40·谭咏麟《歌者恋歌浓情30年演唱会》3CD[低速原抓WAV+CUE][1.8G]
- 刘纬武《睡眠宝宝竖琴童谣 吉卜力工作室 白噪音安抚》[320K/MP3][193.25MB]
- 【轻音乐】曼托凡尼乐团《精选辑》2CD.1998[FLAC+CUE整轨]
- 邝美云《心中有爱》1989年香港DMIJP版1MTO东芝首版[WAV+CUE]
- 群星《情叹-发烧女声DSD》天籁女声发烧碟[WAV+CUE]
- 刘纬武《睡眠宝宝竖琴童谣 吉卜力工作室 白噪音安抚》[FLAC/分轨][748.03MB]
- 理想混蛋《Origin Sessions》[320K/MP3][37.47MB]
- 公馆青少年《我其实一点都不酷》[320K/MP3][78.78MB]
- 群星《情叹-发烧男声DSD》最值得珍藏的完美男声[WAV+CUE]
- 群星《国韵飘香·贵妃醉酒HQCD黑胶王》2CD[WAV]
- 卫兰《DAUGHTER》【低速原抓WAV+CUE】
- 公馆青少年《我其实一点都不酷》[FLAC/分轨][398.22MB]
- ZWEI《迟暮的花 (Explicit)》[320K/MP3][57.16MB]