前两天一直在跟文本和图片打交道,今天我们更进一步,做一个能够播放本地视频文件的播放器。
主要用到了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的坐标。
怎么样,能暂停、开始,能倍速,能显示时长和进度条的视频播放器就此完成了。如果你喜欢看默剧的话,快点玩起来吧!