写文章时,为了让内容生动有趣不枯燥,可以加入一些图片,而对于一些需要动态演示的内容,就需要录制屏幕来完成了。无奈mac上找不到很好用的免费的录制软件,于是想到用python写一个屏幕录制程序,可以录制整个屏幕,也可以录制指定区域。
想法虽然好,但实现起来着实困难,查阅了很多资料,总算找到一个比较简单的方案:
在屏幕上画出指定区域,在这个区域内多次截图,最后将这些图片制作成gif
现在,解决问题的第一步是针对指定区域做截图,经过谷歌搜索,找到了一篇非常好的截图屏幕指定区域的技术文章,[Python]用Tkinter实现一个简单的屏幕截图软件 ,在此感谢这位作者。
文章作者只对如何做到全屏无边框和截取指定区域做了说明,对于程序整体设计并没有做过多说明,我对tkinter也并不是特殊熟练,勉强做点解释吧。
程序的首要功能是启动一个窗口,这个窗口要没有边框,而且要有一定的透明度,这样才能在窗口上画出一个矩形区域,不然你只能看到窗口,窗口后面的桌面是看不到的。
为了能在窗口上画出矩形,创建了一个Canvas,并附着在窗体上, 下图中的矩形区域就是Canvas画出来的
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)
既然已经实现截取屏幕指定区域,那么只需要修改confirmScreenShot方法,变一次截取为多次截取,就可以得到n张连续的图片,再将这些图片制作成gif,就实现了录制屏幕指定区域的功能。
不同于windows,mac下的屏幕分辩率只有一个,因此可以省去换算缩放比例的代码。
对于全屏,mac下使用overrideredirect不生效,必须使用self.win.attributes('-fullscreen', True), 而且,经过实验,在python3.6环境下效果可接受,3.8环境下会切换屏幕,不止如此,self.win.attributes('-alpha', 0.25) 也不能让窗体透明。
为了实现连续截取,我需要在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进行压缩,图片质量损失太多。
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)
这一步没什么可讲的,都是十分简单的常规操作
下面是我录制的一段视频,浏览器里打开优酷的《长安十二时辰》,启动程序,在视频区域画出大小合适的矩形,点击回车开始录制,点击Esc结束录制。
美中不足之处,屏幕截图速度慢,一秒钟最多截取5张图片,画面连续性不好,最终生成的gif,勉强可看吧。
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