使用python录制屏幕并生成gif

写文章时,为了让内容生动有趣不枯燥,可以加入一些图片,而对于一些需要动态演示的内容,就需要录制屏幕来完成了。无奈mac上找不到很好用的免费的录制软件,于是想到用python写一个屏幕录制程序,可以录制整个屏幕,也可以录制指定区域。

想法虽然好,但实现起来着实困难,查阅了很多资料,总算找到一个比较简单的方案:
在屏幕上画出指定区域,在这个区域内多次截图,最后将这些图片制作成gif

1. 截取屏幕指定区域

现在,解决问题的第一步是针对指定区域做截图,经过谷歌搜索,找到了一篇非常好的截图屏幕指定区域的技术文章,[Python]用Tkinter实现一个简单的屏幕截图软件 ,在此感谢这位作者。

文章作者只对如何做到全屏无边框和截取指定区域做了说明,对于程序整体设计并没有做过多说明,我对tkinter也并不是特殊熟练,勉强做点解释吧。

程序的首要功能是启动一个窗口,这个窗口要没有边框,而且要有一定的透明度,这样才能在窗口上画出一个矩形区域,不然你只能看到窗口,窗口后面的桌面是看不到的。

为了能在窗口上画出矩形,创建了一个Canvas,并附着在窗体上, 下图中的矩形区域就是Canvas画出来的
image

Box类,则是为了存储矩形的坐标信息,ImageGrab根据这个矩形的坐标进行截图。

界面上没有按钮,对于软件的操作,都是通过绑定事件实现的

# 绑定按 Enter 确认, Esc 退出
self.win.bind('<KeyPress-Escape>', self.exit)
self.win.bind('<KeyPress-Return>', self.confirmScreenShot)
self.win.bind('<Button-1>', self.selectStart)
self.win.bind('<ButtonRelease-1>', self.selectDone)
self.win.bind('<Motion>', self.changeSelectionArea)

是鼠标左键点击,是坐标左键松开, 是鼠标滑动,通过这些事件,确定了矩形的区域坐标,Canvas画出了矩形。最后点击回车按钮,实现截取屏幕指定区域。

2. 录制屏幕指定区域

既然已经实现截取屏幕指定区域,那么只需要修改confirmScreenShot方法,变一次截取为多次截取,就可以得到n张连续的图片,再将这些图片制作成gif,就实现了录制屏幕指定区域的功能。

不同于windows,mac下的屏幕分辩率只有一个,因此可以省去换算缩放比例的代码。

对于全屏,mac下使用overrideredirect不生效,必须使用self.win.attributes('-fullscreen', True), 而且,经过实验,在python3.6环境下效果可接受,3.8环境下会切换屏幕,不止如此,self.win.attributes('-alpha', 0.25) 也不能让窗体透明。

2.1 连续截取

为了实现连续截取,我需要在confirmScreenShot启动一个线程,线程执行的函数里,使用while循环不停的截取指定区域,直到按下Esc退出键。如果不使用线程,程序会在这个处理按下回车键的函数里卡住,后续无法接收到按下Esc的事件。

def screen_shot(win):
    win._thread_start = True
    png_lst = []
    while not win._stop:
        img = win.captureImage()
        png_name = str(time.time()) + ".jpg"
        filename = pic_path + "/" + png_name
        png_lst.append((filename, img))


    for filename, img in png_lst:
        rgb_im = img.convert('RGB')
        rgb_im.save(filename)
        print(filename)

    win._thread_stop = True

一定要保存成jpg格式,如果是png格式,图片会很大,最终制作成的gif也会很大。如果你想对jpg文件进行压缩,可以参考下面的代码

import cv2

img=cv2.imread("test.jpg",1)
cv2.imwrite("test2.jpg", img, [cv2.IMWRITE_JPEG_QUALITY, 50])

50不是压缩率,而是保留50%的图片质量,这个数值越低,压缩的程度越高,我没有对图片进行压缩,因为实验时,生成的jpg质量已经很小,设置成20进行压缩,图片质量损失太多。

2.2 制作gif文件

def create_gif():
    image_path = Path(pic_path)
    images = list(image_path.glob('*.jpg'))
    image_list = []

    for file_name in images:
        image_list.append(imageio.imread(file_name))

    imageio.mimwrite("/Users/kwsy/PycharmProjects/pythonclass/mytest/a.gif", image_list, duration=0.3)

这一步没什么可讲的,都是十分简单的常规操作

2.3 美中不足

下面是我录制的一段视频,浏览器里打开优酷的《长安十二时辰》,启动程序,在视频区域画出大小合适的矩形,点击回车开始录制,点击Esc结束录制。
a

美中不足之处,屏幕截图速度慢,一秒钟最多截取5张图片,画面连续性不好,最终生成的gif,勉强可看吧。

3. 全部代码

import shutil
import os
import time
import imageio
from pathlib import Path
from threading import Thread
import tkinter as tk
from PIL import ImageGrab


pic_path = "/Users/kwsy/PycharmProjects/pythonclass/mytest/pic"

class Box:

    def __init__(self):
        self.start_x = None
        self.start_y = None
        self.end_x = None
        self.end_y = None

    def isNone(self):
        return self.start_x is None or self.end_x is None

    def setStart(self, x, y):
        self.start_x = x
        self.start_y = y

    def setEnd(self, x, y):
        self.end_x = x
        self.end_y = y

    def box(self):
        lt_x = min(self.start_x, self.end_x)
        lt_y = min(self.start_y, self.end_y)
        rb_x = max(self.start_x, self.end_x)
        rb_y = max(self.start_y, self.end_y)
        return lt_x, lt_y, rb_x, rb_y

    def center(self):
        center_x = (self.start_x + self.end_x) / 2
        center_y = (self.start_y + self.end_y) / 2
        return center_x, center_y


class SelectionArea:

    def __init__(self, canvas: tk.Canvas):
        self.canvas = canvas
        self.area_box = Box()

    def empty(self):
        return self.area_box.isNone()

    def setStartPoint(self, x, y):
        self.canvas.delete('area', 'lt_txt', 'rb_txt')
        self.area_box.setStart(x, y)
        # 开始坐标文字
        self.canvas.create_text(
            x, y - 10, text=f'({x}, {y})', fill='red', tag='lt_txt')

    def updateEndPoint(self, x, y):
        self.area_box.setEnd(x, y)
        self.canvas.delete('area', 'rb_txt')
        box_area = self.area_box.box()
        # 选择区域
        self.canvas.create_rectangle(
            *box_area, fill='black', outline='red', width=2, tags="area")
        self.canvas.create_text(
            x, y + 10, text=f'({x}, {y})', fill='red', tag='rb_txt')


class ScreenShot():

    def __init__(self, scaling_factor=2):
        self.win = tk.Tk()
        #self.win.tk.call('tk', 'scaling', scaling_factor)
        self.width = self.win.winfo_screenwidth()
        self.height = self.win.winfo_screenheight()
        print(self.width, self.height)

        # 无边框,没有最小化最大化关闭这几个按钮,也无法拖动这个窗体,程序的窗体在Windows系统任务栏上也消失
        # self.win.overrideredirect(True)           #  windows上有效
        self.win.attributes('-fullscreen', True)    # mac 上有效
        self.win.attributes('-alpha', 0.25)

        self.is_selecting = False
        self._stop = False
        self._thread_stop = False
        self._thread_start = False
        # 绑定按 Enter 确认, Esc 退出
        self.win.bind('<KeyPress-Escape>', self.exit)
        self.win.bind('<KeyPress-Return>', self.confirmScreenShot)
        self.win.bind('<Button-1>', self.selectStart)
        self.win.bind('<ButtonRelease-1>', self.selectDone)
        self.win.bind('<Motion>', self.changeSelectionArea)

        self.canvas = tk.Canvas(self.win, width=self.width,
                                height=self.height)
        self.canvas.pack()
        self.area = SelectionArea(self.canvas)
        self.win.mainloop()

    def exit(self, event):
        self._stop = True
        while self._thread_start and not self._thread_stop:
            time.sleep(1)

        create_gif()
        self.win.destroy()

    def clear(self):
        self.canvas.delete('area', 'lt_txt', 'rb_txt')
        self.win.attributes('-alpha', 0)

    def captureImage(self):
        if self.area.empty():
            return None
        else:
            box_area = [x  for x in self.area.area_box.box()]
            self.clear()
            print(f'Grab: {box_area}')
            img = ImageGrab.grab(box_area)
            return img

    def confirmScreenShot(self, event):
        if os.path.exists(pic_path):
            shutil.rmtree(pic_path)

        os.mkdir(pic_path)
        t = Thread(target=screen_shot, args=(self, ))
        t.start()

    def selectStart(self, event):
        self.win.attributes('-alpha', 0.25)
        self.is_selecting = True
        self.area.setStartPoint(event.x, event.y)
        # print('Select', event)

    def changeSelectionArea(self, event):
        if self.is_selecting:
            self.area.updateEndPoint(event.x, event.y)
            # print(event)

    def selectDone(self, event):
        # self.area.updateEndPoint(event.x, event.y)
        self.win.attributes('-alpha', 0)
        self.is_selecting = False


def screen_shot(win):
    win._thread_start = True
    png_lst = []
    while not win._stop:
        img = win.captureImage()
        png_name = str(time.time()) + ".jpg"
        filename = pic_path + "/" + png_name
        png_lst.append((filename, img))


    for filename, img in png_lst:
        rgb_im = img.convert('RGB')
        rgb_im.save(filename)
        print(filename)

    win._thread_stop = True


def main():
    ScreenShot()

def create_gif():
    image_path = Path(pic_path)
    images = list(image_path.glob('*.jpg'))
    image_list = []

    for file_name in images:
        image_list.append(imageio.imread(file_name))

    imageio.mimwrite("/Users/kwsy/PycharmProjects/pythonclass/mytest/a.gif", image_list, duration=0.3)

if __name__ == '__main__':
    #create_gif()
    main()

扫描关注, 与我技术互动

QQ交流群: 211426309

加入知识星球, 每天收获更多精彩内容

分享日常研究的python技术和遇到的问题及解决方案