Skip to main content

使用 Desktop Duplication API 在 Windows 上实现极其快速和强大的屏幕捕获

项目描述

D3DShot

D3DShotWindows 桌面复制 API的纯 Python 实现。它利用 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 接口时保持开发人员的理智。
  • 枕头:默认捕获输出。也用于以 PNG 和 JPG 格式保存到磁盘。

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

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

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

概念

捕获输出

创建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 仅允许每个进程有 1 个桌面复制实例。为了确保我们符合该限制以避免出现问题,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 . 显示 =  d 显示[ 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 会为您检测和处理显示旋转和缩放:

  • 旋转显示器上的捕捉将始终处于正确的方向(即与您在物理显示器上看到的相匹配)
  • 缩放显示器上的捕获将始终为完整的非缩放分辨率(例如 1280x720 以 200% 缩放将产生 2560x1440 捕获)

地区

所有捕获方法(包括屏幕截图)都接受可选的regionkwarg。期望值是一个 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 个可选的 kwargs:

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

不要直接导入D3DShot类并尝试自己初始化它!辅助函数在create幕后为您初始化和验证一堆东西。

一旦你在范围内有了一个D3DShot实例,我们就可以开始使用它了!

列出检测到的显示器

d.displays

选择要捕获的显示器

默认情况下选择您的主显示器,但如果您有多显示器设置,您可以在d.displays

d.display = d.displays[1]

截图

d.screenshot()

screenshot接受 1 个可选的 kwarg:

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

返回:屏幕截图,其格式与您在创建D3DShot对象时选择的捕获输出相匹配

截取屏幕截图并将其保存到磁盘

d.screenshot_to_disk()

screenshot_to_disk接受 3 个可选的 kwargs:

  • 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 个可选的 kwarg:

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

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

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

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

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

screenshot_to_disk_every接受 2 个可选的 kwargs:

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

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

开始高速截屏

d.capture()

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

capture接受 2 个可选的 kwargs:

  • 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 个可选的 kwarg:

  • stack_dimension:第一个最后一个。在哪个轴/维度上执行堆栈

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

将帧缓冲区转储到磁盘

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

d.frame_buffer_to_disk()

frame_buffer_to_disk接受 1 个可选的 kwarg:

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

返回:无

表现

测量 Windows 桌面复制 API 的确切性能被证明有点复杂,因为它只会在屏幕内容发生变化时返回新的纹理数据。这对于性能来说是最佳的,但它很难用每秒帧数来表示,这是人们对基准测试的期望值。最终解决方案是在显示器上运行高 FPS 视频游戏进行捕捉,以确保在基准测试时屏幕内容始终不同。

与往常一样,请记住,基准测试本身就存在缺陷,并且高度依赖于您的个人硬件配置和其他情况。使用下面的数字作为对D3DShot 期望值的相对指示,而不是某种绝对真理。

NVIDIA GTX 1080 Ti上 2560x1440 1920x1080 在英特尔 UHD 显卡 630上 1080x1920(垂直)英特尔 UHD 显卡 630
“皮尔” 29.717 帧/秒 47.75 帧/秒 35.95 帧/秒
“麻木” 57.667 帧/秒 58.1 帧/秒 58.033 帧/秒
“numpy_float” 18.783 帧/秒 29.05 帧/秒 27.517 帧/秒
“火炬” 57.867 帧/秒 58.1 帧/秒 34.817 帧/秒
“pytorch_float” 18.767 帧/秒 28.367 帧/秒 27.017 帧/秒
“pytorch_gpu” 27.333 帧/秒 35.767 帧/秒 34.8 帧/秒
“pytorch_float_gpu” 27.267 帧/秒 37.383 帧/秒 35.033 帧/秒

绝对最快的捕获输出似乎是“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”捕获输出 1:1 的速度相匹配,但处理旋转显示时除外。显示旋转由np.rot90()在该数组上产生负步幅的调用处理。在 NumPy 下负跨步被理解并表现良好,但在撰写本文时 PyTorch 仍然不支持。为了解决这个问题,需要额外的复制操作将其带回一个连续的数组,这会带来性能损失。

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

PIL 没有像 NumPy 这样的 ctypes 接口,因此需要先将 bytearray 读入 Python,然后再输入PIL.Image.frombytes(). 这在 Python 方面仍然很快,但它无法与低级 NumPy 方法的速度相提并论。

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

  1. PIL Image 对象往往是 Python 用户所熟悉的
  2. 与 NumPy 或 PyTorch 相比,这是一种更轻/更简单的库依赖

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

桌面复制 API 可访问的 Direct3D 纹理数据被格式化为字节。为了将此数据表示为规范化的浮点数,需要对保存这些字节的数组执行类型转换和元素除法。这会带来很大的性能损失。有趣的是,您可以看到这种性能损失在 GPU PyTorch 张量上得到了缓解,因为元素方面的划分可以在设备上大规模并行化。

Serpent.AI 用 ❤ 制作