跳转到主要内容

使用桌面复制API在Windows上实现极快且健壮的屏幕捕获

项目描述

D3DShot

D3DShot 是一个纯Python实现的 Windows桌面复制API。它利用DXGI和Direct3D系统库,为Windows上的Python脚本和应用程序提供极快且健壮的屏幕捕获功能。

D3DShot

  • 在Windows 8.1+上,这是使用Python捕获屏幕的最快方式
  • 非常容易使用。如果您能记住10个左右的方法,您就了解了整个系统。
  • 涵盖所有常见场景和用例
    • 将截图保存到内存
    • 将截图保存到磁盘
    • 每X秒将截图保存到内存缓冲区(线程化;非阻塞)
    • 每X秒将截图保存到磁盘(线程化;非阻塞)
    • 高速将截图保存到内存缓冲区(线程化;非阻塞)
  • 直接将截图保存为PIL图像。如果找到NumPy或PyTorch,则优雅地添加输出选项。
  • 支持几乎所有配置的显示器:单显示器、一个适配器上的多个显示器、多个适配器上的多个显示器。
  • 为您处理显示旋转和缩放
  • 支持捕获屏幕的特定区域
  • 性能强劲且非常稳定。您可以连续运行数小时/数天而不会出现性能下降
  • 甚至能够捕获 DirectX 11/12 专用的全屏应用程序和游戏!

TL;DR 快速代码示例

屏幕截图到内存

import d3dshot

d = d3dshot.create()
d.screenshot()
Out[1]: <PIL.Image.Image image mode=RGB size=2560x1440 at 0x1AA7ECB5C88>

屏幕截图到磁盘

import d3dshot

d = d3dshot.create()
d.screenshot_to_disk()
Out[1]: './1554298682.5632973.png'

屏幕捕获5秒并捕获最新帧

import d3dshot
import time

d = d3dshot.create()

d.capture()
time.sleep(5)  # Capture is non-blocking so we wait explicitely
d.stop()

d.get_latest_frame()
Out[1]: <PIL.Image.Image image mode=RGB size=2560x1440 at 0x1AA044BCF60>

屏幕捕获第二个显示器为NumPy数组,持续3秒,并捕获4个最新帧作为栈

import d3dshot
import time

d = d3dshot.create(capture_output="numpy")

d.display = d.displays[1]

d.capture()
time.sleep(3)  # Capture is non-blocking so we wait explicitely
d.stop()

frame_stack = d.get_frame_stack((0, 1, 2, 3), stack_dimension="last")
frame_stack.shape
Out[1]: (1080, 1920, 3, 4)

这只是触及了冰山一角...继续阅读!

要求

  • Windows 8.1+(64位)
  • Python 3.6+(64位)

安装

pip install d3dshot

D3DShot利用系统上已经存在的DLL,因此依赖项非常轻。具体来说

  • comtypes:内部使用。在处理COM接口时,用于保持开发者的理智。
  • Pillow:默认捕获输出。也用于将图片保存到磁盘作为PNG和JPG。

这些依赖项将与D3DShot自动安装;无需担心它们!

额外步骤:笔记本电脑用户

当在混合GPU系统上使用桌面重复时,Windows有一个怪癖。在尝试在您的系统上使用D3DShot之前,请参阅维基文章

概念

捕获输出

在创建D3DShot实例时定义所需的捕获输出。它定义了所有捕获图像的类型。默认情况下,所有捕获将返回PIL.Image对象。如果您主要打算进行截图,这是一个很好的选项。

# Captures will be PIL.Image in RGB mode
d = d3dshot.create()
d = d3dshot.create(capture_output="pil")

然而,D3DShot相当灵活!随着您的环境满足某些可选的集合要求,将提供更多选项。

如果NumPy可用

# Captures will be np.ndarray of dtype uint8 with values in range (0, 255)
d = d3dshot.create(capture_output="numpy")

# Captures will be np.ndarray of dtype float64 with normalized values in range (0.0, 1.0)
d = d3dshot.create(capture_output="numpy_float")  

如果NumPyPyTorch可用

# Captures will be torch.Tensor of dtype uint8 with values in range (0, 255)
d = d3dshot.create(capture_output="pytorch")

# Captures will be torch.Tensor of dtype float64 with normalized values in range (0.0, 1.0)
d = d3dshot.create(capture_output="pytorch_float")

如果NumPyPyTorch可用且已安装CUDA并且torch.cuda.is_available()

# Captures will be torch.Tensor of dtype uint8 with values in range (0, 255) on device cuda:0
d = d3dshot.create(capture_output="pytorch_gpu")

# Captures will be torch.Tensor of dtype float64 with normalized values in range (0.0, 1.0) on device cuda:0
d = d3dshot.create(capture_output="pytorch_float_gpu")

如果尝试使用您的环境不符合要求的捕获输出,将导致错误。

单例

Windows仅允许每个进程一个桌面重复实例。为了确保我们遵守此限制以避免问题,D3DShot类充当单例。对d3dshot.create()的任何后续调用都将始终返回现有实例。

d = d3dshot.create(capture_output="numpy")

# Attempting to create a second instance
d2 = d3dshot.create(capture_output="pil")
# Only 1 instance of D3DShot is allowed per process! Returning the existing instance...

# Capture output remains 'numpy'
d2.capture_output.backend
# Out[1]: <d3dshot.capture_outputs.numpy_capture_output.NumpyCaptureOutput at 0x2672be3b8e0>

d == d2
# Out[2]: True

帧缓冲区

创建D3DShot实例时,也会初始化帧缓冲区。它旨在以线程安全、先入先出的方式持有一定数量的捕获,并实现为collections.deque

默认情况下,帧缓冲区的大小设置为60。您可以在创建D3DShot对象时自定义它。

d = d3dshot.create(frame_buffer_size=100)

请注意RAM使用情况,较大值可能会消耗更多;您将处理每个图像高达100 MB的未压缩图像,具体取决于分辨率。

可以直接通过d.frame_buffer访问帧缓冲区,但建议使用实用方法。

以下方法使用该缓冲区

  • d.capture()
  • d.screenshot_every()

在开始这些操作之前,总是自动清除。

显示器

创建D3DShot实例时,会自动检测您的可用显示器及其所有相关属性。

d.displays
Out[1]: 
[<Display name=BenQ XL2730Z (DisplayPort) adapter=NVIDIA GeForce GTX 1080 Ti resolution=2560x1440 rotation=0 scale_factor=1.0 primary=True>,
 <Display name=BenQ XL2430T (HDMI) adapter=Intel(R) UHD Graphics 630 resolution=1920x1080 rotation=0 scale_factor=1.0 primary=False>]

默认情况下,您的主显示器将被选中。您始终可以验证哪个显示器被设置为用于捕获。

d.display
Out[1]: <Display name=BenQ XL2730Z (DisplayPort) adapter=NVIDIA GeForce GTX 1080 Ti resolution=2560x1440 rotation=0 scale_factor=1.0 primary=True>

要选择其他显示器进行捕获,只需将d.display设置为来自d.displays的其他值即可

d.display = d.displays[1]
d.display
Out[1]: <Display name=BenQ XL2430T (HDMI) adapter=Intel(R) UHD Graphics 630 resolution=1080x1920 rotation=90 scale_factor=1.0 primary=False>

显示旋转和缩放由D3DShot为您检测和处理

  • 旋转显示的捕获始终保持在正确的方向(即与您在物理显示器上看到的一致)
  • 缩放显示的捕获始终以全分辨率、非缩放形式(例如,在200%缩放下为1280x720的捕获将产生2560x1440的捕获)

区域

所有捕获方法(包括截图)都接受一个可选的region参数。期望值是一个包含4个整数的元组,其结构如下

(left, top, right, bottom)  # values represent pixels

例如,如果您只想捕获一个从左侧和顶部偏移100px的200px x 200px的区域,您将这样做

d.screenshot(region=(100, 100, 300, 300))

如果您正在捕获缩放显示,则区域将根据全分辨率、非缩放分辨率进行计算。

如果您查看源代码,您会注意到区域裁剪是在完整显示捕获之后发生的。这可能看起来不是很理想,但测试表明,使用CopySubresourceRegion将GPU D3D11Texture2D区域复制到目标CPU D3D11Texture2D时,只有当区域非常小的时候才更快。实际上,对于较大的区域,使用这种方法实际上开始变得比使用完整显示捕获慢。更糟糕的是,它增加了许多复杂性,因为表面间距与缓冲区大小不匹配,并且以不同的方式处理旋转显示。因此,决定在所有情况下都坚持使用CopyResource,并在事后进行裁剪。

用法

创建一个D3DShot实例

import d3dshot

d = d3dshot.create()

create接受2个可选参数

  • capture_output:要使用的捕获输出。请参阅概念部分下的捕获输出部分
  • frame_buffer_size:帧缓冲区可以增长到的最大大小。请参阅概念部分下的帧缓冲区部分

不要直接导入D3DShot类并尝试自行初始化!create辅助函数会在幕后为您初始化和验证许多事情。

一旦您有了D3DShot实例,我们就可以开始使用它了!

列出检测到的显示器

d.displays

选择要捕获的显示器

默认情况下,您的主显示器被选中,但如果您有多个显示器设置,您可以在d.displays中选择另一个条目

d.display = d.displays[1]

截图

d.screenshot()

screenshot接受1个可选参数

  • region:一个区域元组。请参阅概念部分下的区域部分

返回:一个与创建您的D3DShot对象时选择的捕获输出格式匹配的截图

截图并保存到磁盘

d.screenshot_to_disk()

screenshot_to_disk接受3个可选参数

  • directory:写入文件的路径/目录。如果省略,则使用程序的当前工作目录
  • file_name:要使用的文件名。允许的扩展名是:.png.jpg。如果省略,则文件名将为<time.time()>.png
  • region:一个区域元组。请参阅概念部分下的区域部分

返回:表示保存图像文件的完整路径的字符串

每X秒截图一次

d.screenshot_every(X)  # Where X is a number representing seconds

此操作是线程化的且非阻塞的。它将一直运行,直到调用d.stop()。捕获被推送到帧缓冲区。

screenshot_every接受1个可选参数

  • region:一个区域元组。请参阅概念部分下的区域部分

返回:一个布尔值,指示是否已启动捕获线程

每X秒截图一次并将其保存到磁盘

d.screenshot_to_disk_every(X)  # Where X is a number representing seconds

此操作是线程化的且非阻塞的。它将一直运行,直到调用d.stop()

screenshot_to_disk_every接受2个可选参数

  • directory:写入文件的路径/目录。如果省略,则使用程序的当前工作目录
  • region:一个区域元组。请参阅概念部分下的区域部分

返回:一个布尔值,指示是否已启动捕获线程

开始高速屏幕捕获

d.capture()

此操作是线程化的且非阻塞的。它将一直运行,直到调用d.stop()。捕获被推送到帧缓冲区。

capture接受2个可选参数

  • target_fps:每秒要达到的捕获次数。如果系统无法跟上,实际捕获率将低于此目标,但永远不会超过此目标。建议为您的用例设置一个合理的值,以避免浪费系统资源。默认设置为60。
  • region:一个区域元组。请参阅概念部分下的区域部分

返回:一个布尔值,指示是否已启动捕获线程

从缓冲区中获取最新帧

d.get_latest_frame()

返回:一个与创建您的D3DShot对象时选择的捕获输出格式匹配的帧

从缓冲区中获取特定帧

d.get_frame(X)  # Where X is the index of the desired frame. Needs to be < len(d.frame_buffer)

返回:一个与创建您的D3DShot对象时选择的捕获输出格式匹配的帧

从缓冲区抓取特定帧

d.get_frames([X, Y, Z, ...])  # Where X, Y, Z are valid indices to desired frames

返回:一个与创建 D3DShot 对象时选择的捕获输出格式匹配的帧列表

以堆栈形式从缓冲区抓取特定帧

d.get_frame_stack([X, Y, Z, ...], stack_dimension="first|last")  # Where X, Y, Z are valid indices to desired frames

仅对 NumPy 和 PyTorch 捕获输出有效。

get_frame_stack 接受 1 个可选参数

  • stack_dimension:为 firstlast 之一。指定要执行堆栈的轴/维度

返回:一个在指定维度上堆叠的单个数组,其格式与创建 D3DShot 对象时选择的捕获输出格式匹配。如果捕获输出不可堆叠,则返回帧列表。

将帧缓冲区导出到磁盘

文件将根据以下约定命名:<frame buffer index>.png

d.frame_buffer_to_disk()

frame_buffer_to_disk 接受 1 个可选参数

  • directory:写入文件的路径/目录。如果省略,则使用程序的当前工作目录

返回:无

性能

测量 Windows 桌面复制 API 的确切性能有些复杂,因为它只有当屏幕内容发生变化时才会返回新的纹理数据。这对于性能来说是最优的,但它使得以每秒帧数(人们倾向于用于基准测试的度量标准)来表示变得困难。最终解决方案是在显示上运行高帧率视频游戏以捕获,以确保在基准测试期间屏幕内容始终不同。

像往常一样,请记住,基准测试固有的缺陷并且高度依赖于您的个人硬件配置和其他情况。以下数字仅供参考,说明您可以从 D3DShot 预期什么,而不是某种绝对真理。

2560x1440 在 NVIDIA GTX 1080 Ti 1920x1080 在 Intel UHD Graphics 630 1080x1920(垂直)在 Intel UHD Graphics 630
"pil" 29.717 FPS 47.75 FPS 35.95 FPS
"numpy" 57.667 FPS 58.1 FPS 58.033 FPS
"numpy_float" 18.783 FPS 29.05 FPS 27.517 FPS
"pytorch" 57.867 FPS 58.1 FPS 34.817 FPS
"pytorch_float" 18.767 FPS 28.367 FPS 27.017 FPS
"pytorch_gpu" 27.333 FPS 35.767 FPS 34.8 FPS
"pytorch_float_gpu" 27.267 FPS 37.383 FPS 35.033 FPS

绝对最快的捕获输出似乎是 "numpy" 和未旋转的 "pytorch";所有平均约为 58 FPS。在 Python 世界里,这已经非常快了!

为什么 "numpy" 捕获输出的性能这么好?

NumPy 数组有一个 ctypes 接口可以提供它们的原始内存地址(X.ctypes.data)。如果您有另一个字节数组的内存地址和大小,这是我们通过处理桌面复制 API 返回的内容得到的,您可以使用 ctypes.memmove 直接将字节数组复制到 NumPy 结构中,从而有效地绕过尽可能多的 Python。

在实践中,它看起来像这样

ctypes.memmove(np.empty((size,), dtype=np.uint8).ctypes.data, pointer, size)

这种低级操作非常快,将其他所有通常与 NumPy 竞争的东西都甩在后面。

为什么旋转显示器的 "pytorch" 捕获输出较慢?

不要告诉任何人,它之所以能够首先与 NumPy 竞争,仅仅是因为... 是从上面提到的方法构建的 NumPy 数组生成的!如果您在代码中查找,您确实会找到散布的 torch.from_numpy()。这基本上与 "numpy" 捕获输出的速度完全匹配,除了处理旋转显示器时。显示旋转是通过 np.rot90() 调用来处理的,这在该数组上产生负步长。NumPy 理解负步长并且在这些数组上表现良好,但 PyTorch 在撰写本文时仍然不支持。为了解决这个问题,需要一个额外的复制操作来将其恢复为连续数组,这会带来性能惩罚。

为什么默认的“pil”捕获输出不是最快的?

PIL没有像NumPy那样的ctypes接口,因此需要先将bytearray读取到Python中,然后再传递给PIL.Image.frombytes()。这在Python中仍然很快,但仍然无法与低级别的NumPy方法的速度相匹配。

它仍然是默认的捕获输出,因为

  1. PIL图像对象对Python用户来说很熟悉
  2. 相比NumPy或PyTorch,它是一个更轻量级/更简单的依赖库

为什么捕获输出的浮点版本较慢?

通过桌面重复API可访问的Direct3D纹理数据格式为字节。为了将此数据表示为归一化的浮点数,需要对包含这些字节的数组进行类型转换和逐元素除法。这会产生巨大的性能损失。有趣的是,您可以在GPU PyTorch张量上看到这种性能损失得到了缓解,因为逐元素除法可以在设备上大规模并行化。

由Serpent.AI 🐍用❤制作
Twitter - Twitch

项目详情


下载文件

下载您平台上的文件。如果您不确定要选择哪个,请了解更多关于安装包的信息。

源分发

dedeshot-0.0.2.tar.gz (26.4 kB 查看哈希值)

上传时间

构建分发

dedeshot-0.0.2-py3-none-any.whl (24.3 kB 查看哈希值)

上传时间 Python 3

支持者: