手把手教你使用opencv-python库制作屏幕录制工具(附源码)

目录

目录

应用平台

屏幕录制部分

计算视频最优fps及使用numpy计算中间帧数组

使用pynput监听键盘按键

如何保存MP4格式视频

源码

总结

最近有在使用屏幕录制软件录制桌面,在用的过程中突发奇想,使用python能不能做屏幕录制工具,也锻炼下自己的动手能力。接下准备写使用python如何做屏幕录制工具的系列文章:

录制屏幕制作视频

录制音频

合成视频,音频

基于pyqt5制作可视化窗口

大概上述四个部分,希望自己能够尽快完善,接下来开始使用python制作屏幕录制部分。

应用平台

windows 10

python 3.7

屏幕录制部分

屏幕录制可以简单地理解为将屏幕快照以动图的形式播放,这里我选用PIL下的ImageGrab来截取屏幕画面,首先

pip install Pillow

之后需要将截取到的快照数组合成为视频,使用cv2模块

pip install opencv-python

ImageGrab类不能直接存储为视频,使用numpy模块进行数组化,再通过cv2.COLOR_BGR2RGB转换为cv2色彩通道。

pip install numpy

屏幕录制主要代码:

import numpy as np

from PIL import ImageGrab

import cv2

im = ImageGrab.grab()

width, high = im.size  # 获取屏幕的宽和高

fourcc = cv2.VideoWriter_fourcc(*'I420')  # 设置视频编码格式

fps = 15  # 设置帧率

video = cv2.VideoWriter('test.avi', fourcc, fps, (width, high))

while True:  # 开始录制

    im = ImageGrab.grab()

    im_cv = cv2.cvtColor(np.array(im), cv2.COLOR_BGR2RGB)

    # 图像写入

    video.write(im_cv)

    if xx:  # 当某某条件满足中断循环

        break

video.release()  # 释放缓存,持久化视频

测试运行可以保存屏幕快照为视频,但操作起来不优雅,也不利于后续的操作。

封装成类,继承线程父类,方便使用键盘来控制视频录制的结束。

from threading import Thread

class ScreenshotVideo(Thread):

    def __init__(self):

     """初始化参数"""

        super().__init__()

详细代码将在文末给出。

计算视频最优fps及使用numpy计算中间帧数组

实际操作中视频录制在不同电脑中会出现不一样的帧率,导致视频播放或快或慢,需要根据不同的电脑计算出相应的最优fps值。

def video_best_fps(self, path):

    """获取电脑录制视频的最优帧率"""

    video = cv2.VideoCapture(path)  # 读取视频

    fps = video.get(cv2.CAP_PROP_FPS)  # 获取当前视频的帧率

    count = video.get(cv2.CAP_PROP_FRAME_COUNT)  # 获取视频帧数,即该视频有多少幅画面

    self.best_fps = int(fps * ((int(count) / fps) / self.spend_time))   # 计算播放时间与录制时间对比得到最优帧率

    video.release()

再调整帧率参数进行录制视频就减弱了视频播放太快或者太慢。也可以给视频增加帧数从而延长播放时间,这里我采用一种很简单的方法增加视频帧,仅供参考。

from numba import jit

# 使用numpy计算相邻两帧图像且更接近于后一帧的图像

# 调用jit方法加速数组计算

@jit(nopython=True)

def average_n(x, y):

    """Numpy计算趋近值"""

    return ((x + y + y) // 3).astype(x.dtype)

该方法仅针对于设置的fps比最优fps要高时,处理后的视频观感,视频还是较为急促,但是细节帧增多,所以播放时长会比未处理前的要长,略有残影。

使用pynput监听键盘按键

在视频录制中,并不知道视频何时结束,所以用while循环包裹录制代码,但也不可能让代码无休止的运行下去,在此使用监听键盘模块来中断录制代码的运行。

from pynput import keyboard  # pip install pynput

def hotkey(self):

    """热键监听"""

    with keyboard.Listener(on_press=self.on_press) as listener:

        listener.join()

def on_press(self, key):

    try:

        if key.char == 't':  # 录屏结束,保存视频

            self.flag = True

        elif key.char == 'k':  # 录屏中止,删除文件

            self.flag = True

            self.kill = True

    except Exception as e:

        print(e)

按下键盘“T”键时,结束录制,保存视频。“K”键则是停止录制,删除缓存文件。

如何保存MP4格式视频

视频编码格式应该为('a', 'v', 'c', '1'),文件后缀为'.mp4',在录制前先去下下载对应平台的dll.bz2文件,将压缩包解压放在项目文件夹下。

源码

本文实现的源码如下:

import time

from PIL import ImageGrab

import cv2

from pathlib import Path

import numpy as np

from numba import jit

from pynput import keyboard

from threading import Thread

@jit(nopython=True)

def average_n(x, y):

    """Numpy计算趋近值"""

    return ((x + y + y) // 3).astype(x.dtype)

class ScreenshotVideo(Thread):

    def __init__(self, width, high, path='', fps=15):

        """初始化参数"""

        super().__init__()

        self.save_file = path

        self.best_fps = fps

        self.fps = fps

        self.width = width

        self.high = high

        self.spend_time = 1

        self.flag = False

        self.kill = False

        self.video = None

    def __call__(self, path):

        """重载视频路径,便于类的二次调用"""

        self.save_file = Path(path)

        self.video = self.init_videowriter(self.save_file)

    @staticmethod

    def screenshot():

        """静态方法,屏幕截图,并转换为np.array数组"""

        return np.array(ImageGrab.grab())

    @staticmethod

    def get_fourcc(name):

        """视频编码字典"""

        fourcc_maps = {'.avi': 'I420',

                       '.m4v': 'mp4v',

                       '.mp4': 'avc1',

                       '.ogv': 'THEO',

                       '.flv': 'FLV1',

                       }

        return fourcc_maps.get(name)

    def init_videowriter(self, path):

        """获取视频编码并新建视频文件"""

        if not path:

            raise Exception('视频路径未设置,请设置\nvideo = ScreenshotVideo(fps,width,high)\nvideo = video(video_path)')

        path = Path(path) if isinstance(path, str) else path

        fourcc = cv2.VideoWriter_fourcc(*self.get_fourcc(path.suffix))

        return cv2.VideoWriter(path.as_posix(), fourcc, self.fps, (self.width, self.high))

    def video_record_doing(self, img):

        """将BGR数组转换为RGB数组"""

        im_cv = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

        self.video.write(im_cv)

    def video_record_end(self):

        """录制结束,根据条件判断文件是否保存"""

        self.video.release()

        cv2.destroyAllWindows()

        if self.save_file and self.kill:

            Path(self.save_file).unlink()

    def video_best_fps(self, path):

        """获取电脑录制视频的最优帧率"""

        video = cv2.VideoCapture(path)

        fps = video.get(cv2.CAP_PROP_FPS)

        count = video.get(cv2.CAP_PROP_FRAME_COUNT)

        self.best_fps = int(fps * ((int(count) / fps) / self.spend_time))

        video.release()

    def pre_video_record(self):

        """预录制,以获取最佳fps值"""

        self.video = self.init_videowriter('test.mp4')

        start_time = time.time()

        for _ in range(10):

            im = self.screenshot()

            self.video_record_doing(im)

        self.spend_time = round(time.time() - start_time, 4)

        self.video_record_end()

        time.sleep(2)

        self.video_best_fps('test.mp4')

        Path('test.mp4').unlink()

    def insert_frame_array(self, frame_list):

        """Numpy增强截图信息"""

        fps_n = round(self.fps / self.best_fps)

        if fps_n <= 0:

            return frame_list

        times = int(np.log2(fps_n))  # 倍率

        for _ in range(times):

            frame_list2 = map(average_n, [frame_list[0]] + frame_list[:-1], frame_list)

            frame_list = [[x, y] for x, y in zip(frame_list2, frame_list)]

            frame_list = [j for i in frame_list for j in i]

        return frame_list

    def frame2video_run(self):

        """使用opencv将连续型截图转换为视频"""

        self.video = self.init_videowriter(self.save_file)

        start_time = time.time()

        frame_list = []

        while True:

            frame_list.append(self.screenshot())

            if self.flag:

                break

        self.spend_time = round(time.time() - start_time, 4)

        if not self.kill:  # 视频录制不被终止将逐帧处理图像

            frame_list = self.insert_frame_array(frame_list)

            for im in frame_list:

                self.video_record_doing(im)

        self.video_record_end()

    def hotkey(self):

        """热键监听"""

        with keyboard.Listener(on_press=self.on_press) as listener:

            listener.join()

    def on_press(self, key):

        try:

            if key.char == 't':  # 录屏结束,保存视频

                self.flag = True

            elif key.char == 'k':  # 录屏中止,删除文件

                self.flag = True

                self.kill = True

        except Exception as e:

            print(e)

    def run(self):

        # 运行函数

        # 设置守护线程

        Thread(target=self.hotkey, daemon=True).start()

        # 运行截图函数

        self.frame2video_run()

screen = ImageGrab.grab()

width, high = screen.size

video = ScreenshotVideo(width, high, fps=60)

video.pre_video_record()  # 预录制获取最优fps

video('test1.mp4')

video.run()

总结

本文目前使用了opencv和相关模块对屏幕进行录制并转换为视频保存,学习将多个函数封装为类,方便后续功能开发。学习的道路是无止境的,大胆的迈步走吧!

小伙伴们,快快用实践一下吧!如果在学习过程中,有遇到任何问题,欢迎关注我,我拉你进Python学习交流群共同探讨学习。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339

推荐阅读更多精彩内容