前兩天一直在跟文本和圖片打交道,今天我們更進一步,做一個能夠播放本地視頻文件的播放器。
主要用到了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的坐標。
怎么樣,能暫停、開始,能倍速,能顯示時長和進度條的視頻播放器就此完成了。如果你喜歡看默劇的話,快點玩起來吧!