前两天一直在跟文本和图片打交道,今天我们更进一步,做一个能够播放本地视频文件的播放器。

主要用到了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的坐标。

怎么样,能暂停、开始,能倍速,能显示时长和进度条的视频播放器就此完成了。如果你喜欢看默剧的话,快点玩起来吧!