前兩天一直在跟文本和圖片打交道,今天我們更進一步,做一個能夠播放本地視頻文件的播放器。

主要用到了opencv庫,原理和實時的攝像頭顯示是一樣,只是把每一幀圖像經過轉換后封裝到tkinter上。但是這個圖像的顯示,要想沒有延遲、且不占用過多內存,只能使用canvas畫布來實現。只想把視頻播放出來的話,也可以用label顯示圖片,然后調用.after()方法更新,但是這種方法至少要把更新間隔設為10ms(i7處理器),否則會無法正常顯示,而且內存也會逐漸增長。

我們直接來看完整代碼:

import pygame as py
import _thread
import time
import tkinter as tk
from tkinter import *
import cv2
from PIL import Image, ImageTk
import multiprocessing

window_width=960
window_height=720
image_width=int(window_width*0.5)
image_height=int(window_height*0.5)
imagepos_x=0
imagepos_y=0
butpos_x=450
butpos_y=450
vc1 = cv2.VideoCapture('25.mp4')  #讀取視頻

#圖像轉換,用于在畫布中顯示
def tkImage(vc):
    ref,frame = vc.read()
    cvimage = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
    pilImage = Image.fromarray(cvimage)
    pilImage = pilImage.resize((image_width, image_height),Image.ANTIALIAS)
    tkImage =  ImageTk.PhotoImage(image=pilImage)
    return tkImage
#圖像的顯示與更新
def video():
    def video_loop():
       try:
            while True:
                picture1=tkImage(vc1)
                canvas1.create_image(0,0,anchor='nw',image=picture1)  
                canvas2.create_image(0,0,anchor='nw',image=picture1)
                canvas3.create_image(0,0,anchor='nw',image=picture1)
                canvas4.create_image(0,0,anchor='nw',image=picture1)
                win.update_idletasks()  #最重要的更新是靠這兩句來實現
                win.update()
       except:
            pass
          
    video_loop()
    win.mainloop()
    vc1.release()
    cv2.destroyAllWindows()

'''布局'''
win = tk.Tk()
win.geometry(str(window_width)+'x'+str(window_height))
canvas1 =Canvas(win,bg='white',width=image_width,height=image_height)
canvas1.place(x=imagepos_x,y=imagepos_y)
canvas2 =Canvas(win,bg='white',width=image_width,height=image_height)
canvas2.place(x=480,y=0)
canvas3 =Canvas(win,bg='white',width=image_width,height=image_height)
canvas3.place(x=imagepos_x,y=360)   
canvas4 =Canvas(win,bg='white',width=image_width,height=image_height)
canvas4.place(x=480,y=360) 

if __name__ == '__main__': 
    p1 = multiprocessing.Process(target=video)
    p1.start()

你可能會很奇怪,為什么要用到多進程?這里我先賣一個關子,現在這個程序里其實并不需要多進程,但是一會我們就用到了。

如果我們實現了以上內容,我們發現了一個很嚴重的問題——沒有聲音!這是因為cv2.VideoCapture是無法獲取聲音的,可是看視頻沒聲怎么行,總不能只看卓別林和葉逢春吧?

我琢磨了許久,看來要想播放聲音,只能單獨提取出音頻文件,和視頻一起播放了。提取mp4中的音頻,并寫入mp3文件,需要moviepy這個庫,代碼很簡單:

from moviepy.editor import*  
video = VideoFileClip('25.mp4') 
audio = video.audio 
audio.write_audiofile('25.mp3')

800M的視頻,提取出的音頻文件只有50M左右,還算能接受吧。

接下來只要在播放視頻的同時播放音頻就可以了,最開始我嘗試了用多線程,發現音頻會影響tkinter的刷新,導致視頻十分卡頓,所以我就改用了多進程,視頻終于不卡了(如果畫布太大還是會略有延遲,我現在設的大小基本沒有延遲了)。

用pygame來播放mp3文件:

def voice():
    py.mixer.init()
    # 文件加載
    track=py.mixer.music.load('25.mp3')
    # 播放,第一個是播放值 -1代表循環播放, 第二個參數代表開始播放的時間
    py.mixer.music.play(-1, 0)
    while 1:  #一定要有whlie讓程序暫停在這,否則會自動停止
        pass

最后的主函數改為:(多進程的實現一定要放在主函數里)

if __name__ == '__main__': 
    p1 = multiprocessing.Process(target=voice)
    p2 = multiprocessing.Process(target=video)
    p1.start() 
    p2.start()

(好吧,其實不用多進程也行,在播放視頻前先執行播放音頻的語句就行,音頻會在后臺自動運行,但是會讓視頻變卡)

這樣一個簡單的本地視頻播放器就實現了,但是每看一個視頻都要提取出音頻,未免太智障了吧?所以今天這個程序玩玩就行,用處不大……(除非你愛看相聲,提取出的音頻還能放到手機里隨時聽)

但是,你以為到這就結束了嗎?

剛才我們同時創建了四個畫布,一起播放視頻。同樣的方法,是不是可以用來做視頻監控呢?就像電影里演的那樣,屏幕上顯示好幾個攝像頭的監控影像,其實用tkinter就能實現了!當然,如果同時顯示太多圖像,延遲肯定會增加。

那么作業來了——

小作業:制作一個多攝像頭的實時監控軟件,同時檢測圖像中是否有人物出現,一旦有人則立刻報警。(提示:攝像頭圖像的人臉識別上網一搜就能找到,需要調用opencv官方提供的人臉分類器文件;報警的方式則有很多,如果不嫌麻煩的話,可以用twilio給自己發短信)

你以為這又結束了?呵呵,你還是不了解我啊……

既然是視頻軟件,怎么少得了暫停與倍速的功能呢?

先說暫停,我們用單機左鍵暫停,再點一下繼續。我們需要加一個lock變量作為視頻是否播放的判斷條件,初始值設為0,每次點擊左鍵就加一;

至于倍速功能,則綁定右鍵事件,倍速值也是每點擊一次則加一,并且設置倍速上限為4倍;倍速的實現在tkImage函數里。

增加和修改的代碼如下:

lock=0  #暫停標志
n=1  #初始倍速

def tkImage(n):
    #倍速在這里實現
    for i in range(n):
        ref,frame = vc1.read()
    cvimage = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)  #注意這句,后面再說明
    pilImage = Image.fromarray(cvimage)
    pilImage = pilImage.resize((image_width, image_height),Image.ANTIALIAS)
    tkImage1 =  ImageTk.PhotoImage(image=pilImage)
    return tkImage1

def video():    
    def video_loop():
       try:
            while True:
                if lock % 2 == 0:
                    picture1=tkImage(n)
                    canvas1.create_image(0,0,anchor='nw',image=picture1)  
                    canvas2.create_image(0,0,anchor='nw',image=picture1)
                    canvas3.create_image(0,0,anchor='nw',image=picture1)
                    canvas4.create_image(0,0,anchor='nw',image=picture1)
                    win.update_idletasks()  #最重要的更新是靠這兩句來實現
                    win.update()
                else:
                    win.update_idletasks()  #最重要的更新是靠這兩句來實現
                    win.update()
       except:
            pass
          
    video_loop()
    win.mainloop()
    vc1.release()
    cv2.destroyAllWindows()
def right(self):
    global n
    n+=1
    if n>4:
        n=1
def left(self):
    global lock
    lock+=1

#放在創建canvas的后面
canvas1.bind('<Button-1>', left)
canvas1.bind('<Button-3>', right)

當然,這兩個功能僅限于視頻,另一個進程的音頻文件是無法暫停和倍速的。所以啊,還是得看默片。

注意事項:

tkImage函數中的cvimage = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY),和前文同樣位置的cvimage = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)不同,前者是灰度圖,后者是彩色圖。如果我們用灰度圖的話,會有丟幀現象,播放速度變成正常速度的1.5倍左右。

拋開音頻不談,這個播放器還是差點意思——沒有時間和進度條??!時間好說,cv2.VideoCapture讀取視頻后,可以用.get()獲取總幀數和幀率,做除法就是總時間(比如1000和40,那么時長就是40秒),然后在每次讀幀的時候計數,每過一個幀率就是一秒,最后用label顯示出來就行了。

至于進度條咋辦呢?一樣不難!用canvas.create_rectangle繪制整個進度條的矩形框,然后用canvas.coords來填充。你可以每過一個幀率就填充一次,也可以自定義填充頻率,只要根據矩形框的寬度,計算好每次填充的大小就行。注意:這兩個函數的參數都包括了矩形框的對角線坐標,但是這個坐標不是絕對坐標,而是相對于矩形框所在的canvas的坐標。

怎么樣,能暫停、開始,能倍速,能顯示時長和進度條的視頻播放器就此完成了。如果你喜歡看默劇的話,快點玩起來吧!