前两天一直在跟文本和图片打交道,今天我们更进一步,做一个能够播放本地视频文件的播放器。
主要用到了opencv库,原理和实时的摄像头显示是一样,只是把每一帧图像经过转换后封装到tkinter上。但是这个图像的显示,要想没有延迟、且不占用过多内存,只能使用canvas画布来实现。只想把视频播放出来的话,也可以用label显示图片,然后调用.after()方法更新,但是这种方法至少要把更新间隔设为10ms(i7处理器),否则会无法正常显示,而且内存也会逐渐增长。
我们直接来看完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | 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这个库,代码很简单:
1 2 3 4 | from moviepy.editor import * video = VideoFileClip( '25.mp4' ) audio = video.audio audio.write_audiofile( '25.mp3' ) |
800M的视频,提取出的音频文件只有50M左右,还算能接受吧。
接下来只要在播放视频的同时播放音频就可以了,最开始我尝试了用多线程,发现音频会影响tkinter的刷新,导致视频十分卡顿,所以我就改用了多进程,视频终于不卡了(如果画布太大还是会略有延迟,我现在设的大小基本没有延迟了)。
用pygame来播放mp3文件:
1 2 3 4 5 6 7 8 | def voice(): py.mixer.init() # 文件加载 track = py.mixer.music.load( '25.mp3' ) # 播放,第一个是播放值 -1代表循环播放, 第二个参数代表开始播放的时间 py.mixer.music.play( - 1 , 0 ) while 1 : #一定要有whlie让程序暂停在这,否则会自动停止 pass |
最后的主函数改为:(多进程的实现一定要放在主函数里)
1 2 3 4 5 | if __name__ = = '__main__' : p1 = multiprocessing.Process(target = voice) p2 = multiprocessing.Process(target = video) p1.start() p2.start() |
(好吧,其实不用多进程也行,在播放视频前先执行播放音频的语句就行,音频会在后台自动运行,但是会让视频变卡)
这样一个简单的本地视频播放器就实现了,但是每看一个视频都要提取出音频,未免太智障了吧?所以今天这个程序玩玩就行,用处不大……(除非你爱看相声,提取出的音频还能放到手机里随时听)
但是,你以为到这就结束了吗?

刚才我们同时创建了四个画布,一起播放视频。同样的方法,是不是可以用来做视频监控呢?就像电影里演的那样,屏幕上显示好几个摄像头的监控影像,其实用tkinter就能实现了!当然,如果同时显示太多图像,延迟肯定会增加。
那么作业来了——
小作业:制作一个多摄像头的实时监控软件,同时检测图像中是否有人物出现,一旦有人则立刻报警。(提示:摄像头图像的人脸识别上网一搜就能找到,需要调用opencv官方提供的人脸分类器文件;报警的方式则有很多,如果不嫌麻烦的话,可以用twilio给自己发短信)
你以为这又结束了?呵呵,你还是不了解我啊……
既然是视频软件,怎么少得了暂停与倍速的功能呢?
先说暂停,我们用单机左键暂停,再点一下继续。我们需要加一个lock变量作为视频是否播放的判断条件,初始值设为0,每次点击左键就加一;
至于倍速功能,则绑定右键事件,倍速值也是每点击一次则加一,并且设置倍速上限为4倍;倍速的实现在tkImage函数里。
增加和修改的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | 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的坐标。
怎么样,能暂停、开始,能倍速,能显示时长和进度条的视频播放器就此完成了。如果你喜欢看默剧的话,快点玩起来吧!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· .NET周刊【3月第1期 2025-03-02】
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· [AI/GPT/综述] AI Agent的设计模式综述