用Python编写录屏程序将播放的视频用截屏方法转换为多帧图像编辑后保存为GIF格式动图文件

Posted geng_zhaoying

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了用Python编写录屏程序将播放的视频用截屏方法转换为多帧图像编辑后保存为GIF格式动图文件相关的知识,希望对你有一定的参考价值。

有时编写游戏后发博文,为使读者有一个直观的游戏效果,会把游戏运行动画转换GIF格式动图发到博文中。本文介绍如何用python PIL库ImageGrab.grab()函数截屏,编写录屏程序,将视频转换为GIF格式动图文件。
所谓录屏后保存为GIF格式动图文件,就是以固定周期对播放视频连续截屏,保存为多帧图像,再把多帧图像保存为GIF格式动图文件。可使用PIL库ImageGrab.grab()函数截屏,函数的参数是播放视频窗体在显示器屏幕坐标系的左上角和右下角坐标。得到其它软件播放视频的这两个坐标对于一个python程序是很困难的。解决的方法是将播放视频窗体移到录屏窗体中,录屏程序计算自己窗体在显示器屏幕坐标系的左上角和右下角坐标,间接地得到播放软件中的视频窗体在显示器屏幕坐标系的左上角和右下角坐标。为避免被录视频被录屏窗体遮住,这就要求录屏窗体是透明的。实现方法是在窗体画一个和窗体等宽和高的矩形,其填充色和外轮廓色都是透明的。源程序第12行openDialog方法实现这些功能。第15-16行建立一个模式对话框,第28-33行定义了一个透明颜色;然后在canvas中用透明颜色画一个和窗体等宽高的透明的矩形,使窗体变透明。可用移动录屏窗体位置和改变录屏窗体尺寸方法,将播放软件中的视频窗体移到录屏窗体中。由于录屏窗体移动以及窗体尺寸改变,导致系统调用系统函数根据录屏窗体新位置和窗体新尺寸,重画新窗体,同时重画那个矩形,但不会使用透明颜色画矩形。为重画透明矩形,定义自己的on_resize函数(第8句),重画透明矩形,在第19句使自定义on_resize函数替换系统对应函数。在第21-27行,在录屏窗体增加一个’开始录屏’按钮、一个tix组件Control用来设定截屏频率和一个Label组件用显示录屏的时间的秒表。单击主窗体(图3)的’检测fps值’按钮,可检测最大fps,即每秒截屏的最大频率,因此在录屏窗体中设定的fps必须小于所测值。动图本质上是一个图片,但图像会动。一般网站发图片不能超过5M。选择截屏频率要根据将来动图播放频率和动图最终尺寸综合考虑来决定。连续截屏和秒表都是无限循环程序,所有它们代码都不能在主线程运行,必须在子线程运行,否则就无法用单击主线程’停止’按钮结束截屏或秒表。如先关闭主线程,后关闭子线程,有可能产生错误抛出异常。虽然官方文档介绍第48行语句可保证在关闭主线程前关闭相关子线程,但试验后还是抛出了异常,因此将关闭窗体事件绑定自定义函数closef1(第20行),保证先关闭子线程,再关闭主线程。‘开始录屏’和’停止录屏’共用同一按钮,根据标题执行不同代码(第42-57行)。如标题是’开始录屏’将建立两个子线程,一个用来录屏(用第59行的RecordScreen方法),一个用来做秒表计时(用第77行的aTimer方法)。秒表线程while循环条件k==0,如k=1,退出循环,因此退出aTimer()方法,也就退出秒表子线程。另一个录屏子线程同样的道理。要注意第68、72和73行语句,它们保证指定的截屏频率。
单击主窗体(图3)"录屏"按钮,打开录屏窗体(图1),该窗体是模式对话框,其不关闭无法操作主窗口,可以看到其窗体下部是透明的,有个应用程序在其中,还可以看到win10桌面上的图案和图标。点击标题栏左侧图标在下拉菜单中选择移动或大小,可将窗体移动或改变大小。用此法可使视频窗体(图4)移到录屏窗体透明矩形内(图5),请注意,视频窗体的标题栏也被遮挡。

                  图4 将此视频录屏

             图5 已将视频窗体移到录屏窗体透明矩形内
单击开始录屏按钮开始录屏,单击停止录屏按钮停止录屏,关闭录屏窗体。返回主窗体,这时可直接把录屏所得到的多帧图像保存为GIF格式动图文件。但实际上所得图像还是要编辑一下的。例如fps不匹配,截屏频率大于视频播放频率,有重复帧,应删除多余的仅保留一个。录屏前后由于操作鼠标,前后产生很多无用帧,必须删除等等。当然可以保存为动图后,用其它动图软件进行编辑。本程序采用直接编辑所得图像方法,因此关闭录屏窗体后,所录图像出现在主窗体如图2。

其中12个小图是所录图像的缩小图像,键盘左右键可使12个小图显示不同帧号图像。单击小图下边的数字帧号使其变红色,表示该帧被选中,称为’选中帧’,选中帧图像同时在左大图显示。可删除选中帧图像,删除选中帧前边所有帧图像(不包括选中帧),删除选中帧后边所有帧图像(不包括选中帧)。右击数字帧号,所选图像在右大图显示,用来和左图比较。所有这些图都不是原始尺寸。单击’按2参数播放’按钮可查看原始尺寸动图效果。单击’按3参数保存为动图’按钮保存为GIF格式文件。保存的动图根据所选缩小倍数做了缩放。单击’修改3参数’按钮,打开对话框修改每秒播放几帧(fps)、为减小文件字节数保存为gif格式动图时每帧图像缩小倍数(scaling)和完整动图重复播放次数(playNum)。
为了读懂图像编辑程序,必须知道所有这些图像都是在Canvas实例上用create_image方法创建的image实例显示的,所有提示信息都是用create_text方法创建的text实例显示的。为了显示12个小图、2个大图和所有提示信息,在程序运行后,用第418-428行代码创建了14个image实例和14个text实例,如图3。所有image实例没有显示图像,仅显示一个白色小矩形。12个小图下边显示帧号的text实例为空,另2个text实例信息不完整,缺少帧号。当录屏结束或打开图像文件后就会出现界面如图2。使图像在image实例上显示的方法是修改每个image实例的image属性为相应的图像,修改每个text实例的text属性为相应的提示信息。请注意在第421行和第425-428行都设置属性tag值。通过tag使用canvas的itemconfig方法可以很容易修改image实例和text实例的其它属性。例如第144行,是修改tag=‘m3’的image实例的image属性为bigPic1image,让tag=‘m3’的image实例显示指定图像。canvas上创建的多个实例允许有相同tag(例如第91-92行的’allt’),可修改有相同tag的全部实例的同一个属性,例如第191行。12个小图像下边的Text实例显示的数字是帧号,单击数字帧号,选中该帧,该帧图像在左大图显示,右击数字帧号,该帧图像在右大图显示。因此必须为每个显示帧号的Text实例增加鼠标单击和右击事件,为简化程序设计,因此定义了MyText类(第86-106行)。可参见本人博文:数字华容道-将显示数字、单击数字事件绑定及事件处理函数封装在python类中以简化编程。

录屏后的图像保存到Images列表,这些图像是PIL的Image类实例,并不能被Canvas的image实例直接显示,需要转换后才能显示。另外Images列表中保存的是录屏后图像的原始尺寸,可能原始尺寸并不满足在编辑界面的14个图形对于尺寸的要求,可能需要根据实际情况缩小。因此这14个图像都不是原始尺寸图像。只有单击’按2参数播放’按钮播放图像才是原始尺寸图像。方法reformat(第154-159行)完成以上这两个功能。具体缩小倍数是在reSet方法(第120-135行)中获得的,共有两个缩小倍数,scale2是12个小图的缩小倍数,scale1是两个大图的缩小倍数。每当录屏结束或打开图像文件后就会调用reSet方法(第75、116行),出现界面如图2,同时获得缩小倍数,因此这是初始化程序。还有一点需要特别注意,被方法reformat转换后图像必须保存到全局变量中,例如bigPic1image、bigPic2image和shomImage列表,因为Canvas的image实例显示转换后图像后,因某些原因,例如窗体最小化然后最大化,系统重画窗体以及窗体上的图像,为重画图像,系统都会使用这些转换后图像,它们必须是全局变量系统才能找到。
为了更容易读懂有关编辑图像程序,必须明白几个变量的意义,可参见第382-392行的内容。例如frameN0是要显示的12个小图像的第一个图像的帧号,将此帧号传递给方法showAll(),该方法通过循环,修改12个小图的属性image以显示指定图像。方法showAll()在编辑图像程序中经常被使用,例如删除一些帧后就可能调用这个方法。还有如currenframeN0,是被选中的帧号,简称选中帧,由此才能明白按钮’删选中帧’、'删前边帧’和’删后边帧’的意义,选中帧也是左边大图正在显示的图像帧号。右边大图正在显示的图像帧号是:rightBigPicFNo。三者的值为-1,表示还未选定。方法showBigPic使两个大图显示图像。
单击’按2参数播放’按钮,可播放录屏所得多帧图像。这里的两个参数是:视频播放频率fps和完整视频播放几次,可以是有限次数,也可是无限次播放。所谓播放,就是将保存在列表中,用截屏方法得到的多帧图像,以fps速度从列表逐帧取出,以原始尺寸在canvas上image实例显示。该功能是为了模拟gif格式动图效果,以便决定在保存为gif格式动图文件时所设定的参数,包括图像缩小倍数、fps和循环次数3个参数。播放时创建一个独立窗口(第281-282),由于是连续播放,实际的播放在子线程完成,首先创建一个子线程(第290-292行),子线程中运行的程序是方法PlayPic(第312行),它完成实际播放功能。该功能和录屏功能注意事项类似,可参考前边有关内容。但是为避免关闭窗体f3抛出异常,采用录屏中方法仍会抛出异常,因此在关闭窗体函数中,并未关闭窗体,仅是令n9=1,以便使方法PlayPic退出循环后结束线程,然后又延时0.1秒启动新子线程(第303-304行),给播放子线程关闭时间,最后在新子线程中关闭f3窗体(第310行)。
单击’按3参数保存为动图’按钮,可将列表中的图像保存为gif格式的动图(第347行)。在保存前,最好单击’按2参数播放’按钮播放实际尺寸的图像,再决定保存动图图像的缩小倍数,使用不同fps播放,看一下效果,决定保存动图图像的fps。单击’修改3参数’按钮,可修改图像缩小倍数、fps和循环次数3个参数。
完整程序如下。该程序可能有bug,也可能有许多不足之处,希望读者批评指正,非常感谢。再一次提醒,因PIL的问题,显示设置的缩放比例必须调成100%,录屏fsp必须小于所测值,否则截屏所得图像不正确。

import tkinter.tix as tk#导入Tkinter.tix,如开始用import tkinter as tk,后又要使用tix,此改法,前边不用修改
from PIL import ImageGrab,Image,ImageTk
import threading
import time
import tkinter.filedialog,tkinter.messagebox
import shelve

def on_resize(evt):     #窗体大小改变时,调用自定义方法,是为了增加第2条语句,画透明矩形
    f1.configure(width=evt.width,height=evt.height)
    canvas.create_rectangle(0,0,canvas.winfo_width(),canvas.winfo_height(),fill=TRANSCOLOUR,outline=TRANSCOLOUR)

def openDialog():           #打开对话框准备录屏,移动位置,改变大小,将要被录屏的窗体放到本窗体透明区域
    global f1,canvas,TRANSCOLOUR,s,label,tixC     #在Toplevel窗口和主窗口可以互相使用对方的变量和方法。
    root.state('icon')                            #主窗体最小化。
    f1 = tk.Toplevel(root)                        #用Toplevel类创建独立主窗口的新窗口
    f1.grab_set()           #将f1设置为模式对话框,f1不关闭无法操作主窗口,将所有事件由f1接受。        
    f1.geometry('550x400+400+150')
    f1.title('改变窗体大小和位置使屏幕被录制部分在窗体透明矩形中后开始录制')
    f1.bind('<Configure>', on_resize)           #窗体大小改变时,调用自定义方法
    f1.protocol("WM_DELETE_WINDOW", closef1)    #使f1窗口关闭时调用参数2指定函数,关闭其它线程,避免报错    
    frm = tk.Frame(f1)
    frm.pack(fill=tk.BOTH)
    tk.Button(frm,textvariable=s,command=startORstop).pack(side='left')   #开始录屏和停止录屏共用按钮
    tixC=tk.Control(frm,value=5,max=10,min=1,label='fps(1-10,键盘输入回车确认)',integer=True)#选录屏频率
    tixC.pack(side='left')
    label=tk.Label(frm,font=("Arial",15),fg='red',text='0')   #显示录屏的时间
    label.pack(side='right')
    TRANSCOLOUR = 'gray'
    f1.wm_attributes('-transparentcolor', TRANSCOLOUR)  #定义透明颜色
    canvas = tk.Canvas(f1)              #后3句在canvas中画一个和窗体等宽高的透明的矩形,即使窗体变透明
    canvas.pack(fill=tk.BOTH, expand=tk.Y)
    canvas.create_rectangle(0,0,canvas.winfo_width(),canvas.winfo_height(),fill=TRANSCOLOUR,
                            outline=TRANSCOLOUR)
  
def closef1():
    global n,k,f1
    n=1                             #关掉录屏线程
    k=1                             #关掉秒表线程,k=1从aTimer方法while循环退出,线程结束
    root.state('normal')            #使主窗体正常显示
    f1.destroy()                    #关闭对话框

def startORstop(): 
    global s,n,m,k          
    if s.get()=='开始录屏':
        s.set('停止录屏')
        tixC.configure(state="disabled")        #tixC.state="disabled"不报错,但不能使其无效      
        t1 = threading.Thread(target=aTimer)    #新线程,计数器
        t1.setDaemon(True) #如不加此条语句,在截屏停止前,即线程未结束,关闭窗口,会抛出异常
        t1.start()    #将调用aTimer方法在子线程中运行,退出该函数子线程结束,可令k=1结束子线程
        t = threading.Thread(target=RecordScreen)   #多线程录屏
        t.setDaemon(True)
        t.start()        
    else:
        n=1                                 #关掉录屏线程
        k=1                                 #关掉秒表线程,k=1从aTimer方法while循环退出,线程结束
        root.state('normal')                #使主窗体正常显示
        f1.destroy()                        #关闭对话框

def RecordScreen():                         #实际的录屏方法,就是按指定时间间隔多次截屏
    global n,m,fps,images
    x=f1.winfo_rootx()+canvas.winfo_x()     #录屏矩形左上角窗体坐标转换为显示器屏幕坐标(x,y)
    y=f1.winfo_rooty()+canvas.winfo_y()
    x1=x+canvas.winfo_width()               #录屏矩形右下角显示器屏幕坐标(x1,y1)
    y1=y+canvas.winfo_height()    
    n,m=0,0
    SampleCycle=1/int(fps)
    while n==0:                             #n=1,将退出while循环,线程结束,录屏结束,
        start = time.time()                 #以秒为单位,开始时间。下句是截屏语句
        p=ImageGrab.grab((x,y,x1,y1))    #截屏,因PIL的原因,必须将win10显示设置的缩放比例调成100%
        m+=1
        images.append(p)                    #将截屏的image类实例保存到列表
        end = time.time()                   #结束时间。在Windows系统中
        time.sleep(SampleCycle-(start-end)) #延迟时间取样周期(SampleCycle)-(截屏用去的时间)
    saveFile()                              #保存列表images中所有录屏图像为文件,将覆盖上次录屏数据
    reSet()                                 #调用初始化方法

def aTimer():                               #在另一线程中的秒表,记录录屏时间
    global k,label    
    k=0
    seconds=-1
    while k==0:
        seconds+=1                          #每隔一秒+1       
        label['text']=str(seconds)+' '      #在label组件上显示秒数
        time.sleep(1)                       #延迟1秒

class MyText():      #用canvas的text显示12个小图帧号,需要响应鼠标左击和右击事件,为此定义该类。
    canvas=0                            #canvas为类变量,所有类实例共用的一个变量
    functionId=None           #将引用showBigPic方法用来显示大图,参数1是帧号,参数2是大图1或2
    def __init__(self,n):               #构造函数
        self.tagN="t"+str(n)            #保存下句在Canvas中创建的text实例的tag值
        MyText.canvas.create_text(60+n*105,605,activefill='red',#text=' ',
                                    tag=(self.tagN,'allt'),font=("Arial",15))
        MyText.canvas.tag_bind(self.tagN,'<Button-1>',self.leftClick)         #绑定左键单击事件
        MyText.canvas.tag_bind(self.tagN,'<Button-3>',self.rightClick)        #绑定右键单击事件
    def leftClick(self,event):                   #类实例方法,是鼠标左击事件函数,选第1大图显示的图
        s=MyText.canvas.itemcget(self.tagN,'text')         #得到属性'text'的值(字符串)
        k=int(s)                                           #k为帧号
        MyText.functionId(k,1)                             #调用函数在1号大图显示左键单击选的图像                                          
        MyText.canvas.itemconfig('allt',fill="black")      #所有text实例的字体颜色都变黑
        MyText.canvas.itemconfig(self.tagN,fill="red")     #当前选定图像字体颜色变红
        MyText.canvas.itemconfig('m1',text='鼠标左键单击小图下帧号选此帧为当前图像,当前帧号为:'+s)
    def rightClick(self,event):                     #类实例方法,是鼠标左击事件函数,选第2大图显示的图
        s=MyText.canvas.itemcget(self.tagN,'text')  #开始帧号并不显示,只有显示数字才能响应任何事件        
        k=int(s)                                    #因此,从字符串转换为整型数一定成功
        MyText.functionId(k,2)                      #调用函数在2号大图显示右键单击选的图像
        MyText.canvas.itemconfig('m2',text='鼠标右键单击小图下帧号选此帧为比较图像,当前帧号为:'+s)
            
def saveFile():                                 #保存录屏所得图像列表为文件
    with shelve.open('myFile') as f:
        f['myKey'] = images                     #保存列表    
    
def openFile():                                 #取出所保存录屏所得图像列表的文件
    global images
    with shelve.open('myFile') as f:
        images=f.get('myKey')
        reSet()
#tkinter.filedialog.askopenfilename()#选择打开什么文件,返回文件名。defaultextension指定文件后缀,该后缀会自动添加
#filetypes,指定筛选文件类型的下拉菜单选项(如:filetypes=[('PNG’,'png'),('JPG’,'jpg’),('GIF’,'gif’)])

def reSet():    #在获得新录屏图像后调用此函数。两次调用该函数:录屏结束(第75行),打开图像文件后(第116行)
    global currenframeN0,rightBigPicFNo,frameN0,scale2,scale1
    scale1=images[0].height/450             #为了显示大图像,原始图像缩小比例,450是大图允许最大高度
    if scale1<1:                            #比450小,就不用缩小了
        scale1=1
    scale2=images[0].width/100              #为了显示小图像,原始图像缩小比例,小图允许最大宽度为100
    if scale2<1:                            #比100小,就不用缩小了
        scale1=1
    canvasM.itemconfig('m1',text=s1+'0')                #修改左大图上边显示的帧号
    canvasM.itemconfig('m2',text=s2+'1')                #修改右大图上边显示的帧号
    showBigPic(0,1)                                     #在左大图显示第0帧图像    
    currenframeN0=0                                     #当前选中帧号(当前帧)=0
    showBigPic(1,2)                                     #在右大图显示第1帧图像
    rightBigPicFNo=1                                    #右大图显示图像的帧号
    frameN0=0                                           #12个小图的起始帧号
    showAll(frameN0)                                    #从第0帧开始显示12个小图
    
def showBigPic(frameNo,whichBigPic):#显示大图,参数1正数是帧号,-1显示空白;whichBigPic=1,左大图,=2,右大图
    global bigPic1image,bigPic2image,currenframeN0,img,rightBigPicFNo,scale1
    if whichBigPic==1:                                         #whichBigPic=1,左大图
        if frameNo>=0:
            bigPic1image=reformat(frameNo,scale1)   #返回要显示大图,必须保存到全局变量,scale1为缩小倍数
        else:                                                  #如果帧号小于0,显示空白
            bigPic1image=img
        canvasM.itemconfig('m3',image=bigPic1image)            #在左大图显示指定图像,'m3'为canvas实例tag
        currenframeN0=frameNo
    elif whichBigPic==2:                                             #whichBigPic=2,右大图
        if frameNo>=0:
            bigPic2image=reformat(frameNo,scale1)
        else:                                               #如果帧号小于0,显示空白
            bigPic2image=img
        canvasM.itemconfig('m4',image=bigPic2image)         #在右大图显示指定图像,'m4'为canvas实例tag
        rightBigPicFNo=frameNo

def reformat(No,k):               #将列表images[No]图像转换为canvas能显示的格式和合适尺寸
    im=images[No]                 #从列表取出帧号为No的图像
    m=int((im.width/k)//1)        #图像宽缩小k倍
    n=int((im.height/k)//1)       #图像高缩小k倍
    im=im.resize((m,n))           #缩小图像图像尺寸
    return ImageTk.PhotoImage(image=im)         #返回canvas能显示的图像

def moveR(event):               #12个小图像全部向右移
    global frameN0
    if frameN0<=0:
        return    
    frameN0-=1    
    showAll(frameN0)

def moveL(event):               #12个小图像全部向左移
    global shomImage,frameN0,images
    if frameN0==len(images)-12:
        return
    if len(images)<=12:
        frameN0=0
        return
    frameN0+=1    
    showAll(frameN0)

def showAll(N0):                    #N0是要显示的起始帧号,从N0开始显示12个小图
    global shomImage,img,scale2
    shomImage=[]                    #列表保存12个小图要显示的图像,必须是全局变量
    m=12
    if len(images)<12:              #如列表保存图像个数<12,12小图后边有些位置无图像可显示
        m=len(images)                  
        for n in range(12):         #先让所有小图显示白矩形框,小图下边不显示帧号,显示为空
            canvasM.itemconfig('p'+str(n),image=img) #清空显示的所有小图,img是一个白矩形框
            canvasM.itemconfig('t'+str(n),text=' ')  #清空显示的所有小图下边的帧号
    for n in range(m):#m=12,12小图都有图像,m<12,例如=11,前11个有图像,第12个无图像保留白矩形框
        shomImage.append(reformat(N0+n,scale2))   #要显示的小图像添加到全局列表中,否则不能显示
        canvasM.itemconfig('p'+str(n),image=shomImage[n])   #在指定位置显示小图像
        canvasM.itemconfig('t'+str(n),text=str(N0+n))       #在指定位置显示帧号,注意通过tag
    canvasM.itemconfig('allt',fill="black")                 #所有显示帧号的text实例的字体颜色都变黑
    if N0<=currenframeN0<=N0+11:                            #如frameN0+n是被选中的帧,帧号变红    
        canvasM.itemconfig('t'+str(currenframeN0-N0),fill="red")  #字体颜色变红

def delAframe():                            #该方法删除当前帧,即左大图显示的帧
    global curren

以上是关于用Python编写录屏程序将播放的视频用截屏方法转换为多帧图像编辑后保存为GIF格式动图文件的主要内容,如果未能解决你的问题,请参考以下文章

python 自动录屏初步实现

Android 音视频开发 -- Android Mediaprojection 截屏和录屏

Android MediaProjection截屏&录屏-适配AndroidQ以上版本

为啥录屏没声音

mac如何截屏

android开发设置屏蔽录制