飞道的博客

Python教程:竟然用Python实现了电视里的5毛特效

297人阅读  评论(0)

前段时间接触了一个批量抠图的模型库,而后在一些视频中找到灵感,觉得应该可以通过抠图的方式,给视频换一个不同的场景,于是就有了今天的文章。

我们先看看能实现什么效果,先来个正常版的,先看看原场景:

 

下面是我们切换场景后的样子:

 

 

 

看起来效果还是不错的,有了这个我们就可以随意切换场景,坟头蹦迪不是梦。另外,我们再来看看另外一种效果,相比之下要狂放许多:

 

 

 

实现步骤

我们都知道,视频是由一帧一帧的画面组成的,每一帧都是一张图片,我们要实现对视频的修改就需要对视频中每一帧画面进行修改。所以在最开始,我们需要获取视频每一帧画面。

在我们获取帧之后,需要抠取画面中的人物。

抠取人物之后,就需要读取我们的场景图片了,在上面的例子中背景都是静态的,所以我们只需要读取一次场景。在读取场景之后我们切换每一帧画面的场景,并写入新的视频。

这时候我们只是生成了一个视频,我们还需要添加音频。而音频就是我们的原视频中的音频,我们读取音频,并给新视频设置音频就好了。

具体步骤如下:

  1. 读取视频,获取每一帧画面
  2. 批量抠图
  3. 读取场景图片
  4. 对每一帧画面进行场景切换
  5. 写入视频
  6. 读取原视频的音频
  7. 给新视频设置音频

因为上面的步骤还是比较耗时的,所以在视频完成后通过邮箱发送通知,告诉我视频制作完成。

 

模块安装

我们需要使用到的模块主要有如下几个:


  
  1. pillow
  2. opencv
  3. moviepy
  4. paddlehub

我们都可以直接用pip安装:


  
  1. pip  install pillow
  2. pip  install opencv-python
  3. pip  install moviepy

其中OpenCV有一些适配问题,建议选取3.0以上版本。

在我们使用paddlehub之前,我们需要安装paddlepaddle:具体安装步骤可以参见官网。用paddlehub抠图参考:别再自己抠图了,Python用5行代码实现批量抠图。我们这里直接用pip安装cpu版本的:


  
  1. # 安装paddlepaddle
  2. python -m pip install paddlepaddle -i https: //mirror.baidu.com/pypi/simple
  3. # 安装paddlehub
  4. pip install -i https: //mirror.baidu.com/pypi/simple paddlehub

有了这些准备工作就可以开始我们功能的实现了。

 

具体实现

我们导入如下包:


  
  1. import cv2     # opencv
  2. import mail     # 自定义包,用于发邮件
  3. import math
  4. import numpy  as np
  5. from PIL  import Image     # pillow
  6. import paddlehub  as hub
  7. from moviepy.editor  import *

其中Pillow和opencv导入的名称不太一样,还有就是我自定义的mail模块。另外我们还要先准备一些路径:


  
  1. # 当前项目根目录,系统自动获取当前目录
  2. BASE_DIR = os.path.abspath(os.path.join(os.path.dirname( __file__),  "."))
  3. # 每一帧画面保存的地址
  4. frame_path = BASE_DIR +  '\\frames\\'
  5. # 抠好的图片位置
  6. humanseg_path = BASE_DIR +  '\\humanseg_output\\'
  7. # 最终视频的保存路径
  8. output_video = BASE_DIR +  '\\result.mp4'

接下来我们按照上面说的步骤一个一个实现。

(1)读取视频,获取每一帧画面

OpenCV中提供了读取帧的函数,我们只需要使用VideoCapture类读取视频,然后调用read函数读取帧,read方法返回两个参数,ret为是否有下一帧,frame为当前帧的ndarray对象。完整代码如下:


  
  1. def getFrame(video_name, save_path):
  2.      """
  3.     读取视频将视频逐帧保存为图片,并返回视频的分辨率size和帧率fps
  4.     :param video_name: 视频的名称
  5.     :param save_path: 保存的路径
  6.     :return: fps帧率,size分辨率
  7.     """
  8.      # 读取视频
  9.     video = cv2.VideoCapture(video_name)
  10.      # 获取视频帧率
  11.     fps = video.get(cv2.CAP_PROP_FPS)
  12.      # 获取画面大小
  13.     width = int(video.get(cv2.CAP_PROP_FRAME_WIDTH))
  14.     height = int(video.get(cv2.CAP_PROP_FRAME_HEIGHT))
  15.     size = (width, height)
  16.      # 获取帧数,用于给图片命名
  17.     frame_num = str(video.get( 7))
  18.     name = int(math.pow( 10, len(frame_num)))
  19.      # 读取帧,ret为是否还有下一帧,frame为当前帧的ndarray对象
  20.     ret, frame = video.read()
  21.      while ret:
  22.         cv2.imwrite(save_path + str(name) +  '.jpg', frame)
  23.         ret, frame = video.read()
  24.         name +=  1
  25.     video.release()
  26.      return fps, size

在标处,我获取了帧的总数,然后通过如下公式获取比帧数大的整十整百的数:

frame_name = math.pow(10len(frame_num))

这样做是为了让画面逐帧排序,这样读取的时候就不会乱。另外我们获取了视频的帧率和分辨率,这两个参数在我们创建视频时需要用到。这里需要注意的是opencv3.0以下版本获取帧率和画面大小的写法有些许差别。

(2)批量抠图

批量抠图需要用到paddlehub中的模型库,代码很简单,这里就不多说了:


  
  1. def getHumanseg(frames):
  2.      """
  3.     对帧图片进行批量抠图
  4.     :param frames: 帧的路径
  5.     :return:
  6.     """
  7.      # 加载模型库
  8.     humanseg = hub.Module(name= 'deeplabv3p_xception65_humanseg')
  9.      # 准备文件列表
  10.     files = [frames + i  for i  in os.listdir(frames)]
  11.      # 抠图
  12.     humanseg.segmentation(data={ 'image': files})

我们执行上面函数后会在项目下生成一个humanseg_output目录,抠好的图片就在里面。

(3)读取场景图片

这也是简单的图片读取,我们使用pillow中的Image对象:


  
  1. def readBg(bgname, size):
  2.      """
  3.     读取背景图片,并修改尺寸
  4.     :param bgname: 背景图片名称
  5.     :param size: 视频分辨率
  6.     :return: Image对象
  7.     """
  8.     im = Image.open(bgname)
  9.      return im.resize(size)

这里的返回的对象并非ndarray对象,而是Pillow中定义的类对象。

(4)对每一帧画面进行场景切换

简单来说就是将抠好的图片和背景图片合并,我们知道抠好的图片都在humanseg_output目录,这也就是为什么最开始要准备相应的变量存储该目录的原因:


  
  1. def setImageBg(humanseg, bg_im):
  2.      """
  3.     将抠好的图和背景图片合并
  4.     :param humanseg: 抠好的图
  5.     :param bg_im: 背景图片,这里和readBg()函数返回的类型一样
  6.     :return: 合成图的ndarray对象
  7.     """
  8.      # 读取透明图片
  9.     im = Image.open(humanseg)
  10.      # 分离色道
  11.     r, g, b, a = im.split()
  12.      # 复制背景,以免源背景被修改
  13.     bg_im = bg_im.copy()
  14.      # 合并图片
  15.     bg_im.paste(im, ( 00), mask=a)
  16.      return np.array(bg_im.convert( 'RGB'))[:, :, :: -1]

在标处,我们复制了背景,如果少了这一步的话,生成的就是我们上面的“千手观音效果”了。

其它步骤都很好理解,只有返回值比较长,我们来详细看一下:


  
  1. # 将合成图转换成RGB,这样A通道就没了
  2. bg_im = bg_im.convert( 'RGB')
  3. # 将Image对象转换成ndarray对象,方便opencv读取
  4. im_array = np. array(bg_im)
  5. # 此时im_array为rgb模式,而OpenCV为bgr模式,我们通过下面语句将rgb转换成bgr
  6. bgr_im_array = im_array[:, :, :: -1]

最后bgr_im_array就是我们最终的返回结果。

(5)写入视频

为了节约空间,我并非等将写入图片放在合并场景后面,而是边合并场景边写入视频:


  
  1. def writeVideo(humanseg, bg_im, fps, size):
  2.      """
  3.     :param humanseg: png图片的路径
  4.     :param bgname: 背景图片
  5.     :param fps: 帧率
  6.     :param size: 分辨率
  7.     :return:
  8.     """
  9.      # 写入视频
  10.     fourcc = cv2.VideoWriter_fourcc(* 'mp4v')
  11.     out = cv2.VideoWriter( 'green.mp4', fourcc, fps, size)
  12.      # 将每一帧设置背景
  13.     files = [humanseg + i  for i  in os.listdir(humanseg)]
  14.      for file  in files:
  15.          # 循环合并图片
  16.         im_array = setImageBg(file, bg_im)
  17.          # 逐帧写入视频
  18.         out.write(im_array)
  19.     out.release()

上面的代码也非常简单,执行完成后项目下会生成一个green.mp4,这是一个没有音频的视频,后面就需要我们获取音频然后混流了。

(6)读取原视频的音频

因为在opencv中没找到音频相关的处理,所以选用moviepy,使用起来也非常方便:


  
  1. def getMusic(video_name):
  2.      """
  3.     获取指定视频的音频
  4.     :param video_name: 视频名称
  5.     :return: 音频对象
  6.     """
  7.      # 读取视频文件
  8.     video = VideoFileClip(video_name)
  9.      # 返回音频
  10.      return video.audio

然后就是混流了。

(7)给新视频设置音频

这里同样使用moviepy,传入视频名称和音频对象进行混流:


  
  1. def addMusic(video_name, audio):
  2.      """实现混流,给video_name添加音频"""
  3.      # 读取视频
  4.     video = VideoFileClip(video_name)
  5.      # 设置视频的音频
  6.     video = video.set_audio(audio)
  7.      # 保存新的视频文件
  8.     video.write_videofile(output_video)

其中output_video是我们在最开始定义的变量。

(8)删除过渡文件

在我们生产视频时,会产生许多过渡文件,在视频合成后我们将它们删除:


  
  1. def deleteTransitionalFiles():
  2.      """删除过渡文件"""
  3.     frames = [frame_path + i  for i  in os.listdir(frame_path)]
  4.     humansegs = [humanseg_path + i  for i  in os.listdir(humanseg_path)]
  5.      for frame  in frames:
  6.         os.remove(frame)
  7.      for humanseg  in humansegs:
  8.         os.remove(humanseg)

最后就是将整个流程整合一下。

(8)整合

我们将上面完整的流程合并成一个函数:


  
  1. def changeVideoScene(video_name, bgname):
  2.      """
  3.     :param video_name: 视频的文件
  4.     :param bgname: 背景图片
  5.     :return:
  6.     """
  7.      # 读取视频中每一帧画面
  8.     fps, size = getFrame(video_name, frame_path)
  9.      # 批量抠图
  10.     getHumanseg(frame_path)
  11.      # 读取背景图片
  12.     bg_im = readBg(bgname, size)
  13.      # 将画面一帧帧写入视频
  14.     writeVideo(humanseg_path, bg_im, fps, size)
  15.      # 混流
  16.     addMusic( 'green.mp4', getMusic(video_name))
  17.      # 删除过渡文件
  18.     deleteTransitionalFiles()

(9)在main中调用

我们可以把前面定义的路径也放进了:


  
  1. if __name__ ==  '__main__':
  2.      # 当前项目根目录
  3.     BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__),  "."))
  4.      # 每一帧画面保存的地址
  5.     frame_path = BASE_DIR +  '\\frames\\'
  6.      # 抠好的图片位置
  7.     humanseg_path = BASE_DIR +  '\\humanseg_output\\'
  8.      # 最终视频的保存路径
  9.     output_video = BASE_DIR +  '\\result.mp4'
  10.      if  not os.path.exists(frame_path):
  11.         os.makedirs(frame_path)
  12.      try:
  13.          # 调用函数制作视频
  14.         changeVideoScene( 'jljt.mp4''bg.jpg')
  15.          # 当制作完成发送邮箱
  16.         mail.sendMail( '你的视频已经制作完成')
  17.      except Exception  as e:
  18.          # 当发生错误,发送错误信息
  19.         mail.sendMail( '在制作过程中遇到了问题' + e.__str__())

这样我们就完成了完整的流程。

 

发送邮件

邮件的发送又是属于另外的内容了,我定义了一个mail.py文件,具体代码如下:


  
  1. import smtplib
  2. from email.mime.text  import MIMEText
  3. from email.mime.multipart  import MIMEMultipart       # 一封邮件
  4. def sendMail(msg):    
  5.     
  6.     sender =  '发件人'
  7.     to_list = [
  8.          '收件人'
  9.     ]
  10.     subject =  '视频制作情况'
  11.      # 创建邮箱
  12.     em = MIMEMultipart()
  13.     em[ 'subject'] = subject
  14.     em[ 'From'] = sender
  15.     em[ 'To'] =  ",".join(to_list)
  16.      # 邮件的内容
  17.     content = MIMEText(msg)
  18.     em.attach(content)
  19.      # 发送邮件
  20.      # 1、连接服务器
  21.     smtp = smtplib.SMTP()
  22.     smtp.connect( 'smtp.163.com')
  23.      # 2、登录
  24.     smtp.login(sender,  '你的密码或者授权码')
  25.      # 3、发邮件
  26.     smtp.send_message(em)
  27.      # 4、关闭连接
  28.     smtp.close()

里面的邮箱我是直接写死了,大家可以自由发挥。为了方便,推荐发件人使用163邮箱,收件人使用QQ邮箱。另外在登录的时候直接使用密码比较方便,但是有安全隐患。

 

总结

老实说上述程序的效率非常低,不仅占空间,而且耗时也比较长。在最开始我切换场景选择的是遍历图片每一个像素,而后找到了更加高效的方式取代了。但是帧画面的保存,和png图片的存储都很耗费空间。

另外程序设计还是有许多不合理的地方,像是ndarray对象和Image的区分度不高,另外有些函数选择传入路径,而有些函数选择传入文件对象也很容易让人糊涂。

最后说一下,我们用上面的方式不仅可以做静态的场景切换,还可以做动态的场景切换,这样我们就可以制作更加丰富的视频。当然,效率依旧是个问题!

学习Python的伙伴,可以看过来了,整理好的Python学习视频教程,回复:Python,即可领取!

2020最新_Python_(MySQL_SQL_Redis)数据库详解https://pan.baidu.com/s/1W4ACZG2412PgmrG4a9_log 提取


转载:https://blog.csdn.net/GUDUzhongliang/article/details/106124248
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场