1. 程式人生 > 程式設計 >Python實現電視裡的5毛特效例項程式碼詳解

Python實現電視裡的5毛特效例項程式碼詳解

前段時間接觸了一個批量摳圖的模型庫,而後在一些視訊中找到靈感,覺得應該可以通過摳圖的方式,給視訊換一個不同的場景,於是就有了今天的文章。

我們先看看能實現什麼效果,先來個正常版的,先看看原場景:

Python教程:竟然用Python實現了電視裡的5毛特效

下面是我們切換場景後的樣子:

Python教程:竟然用Python實現了電視裡的5毛特效

看起來效果還是不錯的,有了這個我們就可以隨意切換場景,墳頭蹦迪不是夢。另外,我們再來看看另外一種效果,相比之下要狂放許多:

Python教程:竟然用Python實現了電視裡的5毛特效

實現步驟

我們都知道,視訊是由一幀一幀的畫面組成的,每一幀都是一張圖片,我們要實現對視訊的修改就需要對視訊中每一幀畫面進行修改。所以在最開始,我們需要獲取視訊每一幀畫面。

在我們獲取幀之後,需要摳取畫面中的人物。

摳取人物之後,就需要讀取我們的場景圖片了,在上面的例子中背景都是靜態的,所以我們只需要讀取一次場景。在讀取場景之後我們切換每一幀畫面的場景,並寫入新的視訊。

這時候我們只是生成了一個視訊,我們還需要新增音訊。而音訊就是我們的原視訊中的音訊,我們讀取音訊,並給新視訊設定音訊就好了。

具體步驟如下:

  • 讀取視訊,獲取每一幀畫面
  • 批量摳圖
  • 讀取場景圖片
  • 對每一幀畫面進行場景切換
  • 寫入視訊
  • 讀取原視訊的音訊
  • 給新視訊設定音訊

因為上面的步驟還是比較耗時的,所以在視訊完成後通過郵箱傳送通知,告訴我視訊製作完成。

模組安裝

我們需要使用到的模組主要有如下幾個:

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,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,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的區分度不高,另外有些函式選擇傳入路徑,而有些函式選擇傳入檔案物件也很容易讓人糊塗。

最後說一下,我們用上面的方式不僅可以做靜態的場景切換,還可以做動態的場景切換,這樣我們就可以製作更加豐富的視訊。當然,效率依舊是個問題!

到此這篇關於Python實現了電視裡的5毛特效的文章就介紹到這了,更多相關Python 5毛特效內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!