使用桌面复制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,因此依赖项非常轻。具体来说
这些依赖项将与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")
如果NumPy和PyTorch可用
# 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")
如果NumPy和PyTorch可用且已安装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
:为 first 或 last 之一。指定要执行堆栈的轴/维度
返回:一个在指定维度上堆叠的单个数组,其格式与创建 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方法的速度相匹配。
它仍然是默认的捕获输出,因为
- PIL图像对象对Python用户来说很熟悉
- 相比NumPy或PyTorch,它是一个更轻量级/更简单的依赖库
为什么捕获输出的浮点版本较慢?
通过桌面重复API可访问的Direct3D纹理数据格式为字节。为了将此数据表示为归一化的浮点数,需要对包含这些字节的数组进行类型转换和逐元素除法。这会产生巨大的性能损失。有趣的是,您可以在GPU PyTorch张量上看到这种性能损失得到了缓解,因为逐元素除法可以在设备上大规模并行化。
项目详情
下载文件
下载您平台上的文件。如果您不确定要选择哪个,请了解更多关于安装包的信息。
源分发
构建分发
dedeshot-0.0.2.tar.gz 的哈希值
算法 | 哈希摘要 | |
---|---|---|
SHA256 | 8768de0f22c14ebd1eefe6726690fe147b0252725e7cb6b27f2d3b5dc734e548 |
|
MD5 | 9a8595e7dfede8c5d49eb55ff9a590b9 |
|
BLAKE2b-256 | b9d0c20091b21aa20040a6142a0a62c804b14474668caba7956c6a24f2f5bbd6 |
dedeshot-0.0.2-py3-none-any.whl 的哈希值
算法 | 哈希摘要 | |
---|---|---|
SHA256 | da0236218f5d5e6b57157165ec124e2d9f2e23ae65ee6fdbce5feaf24b9202bf |
|
MD5 | 046015e2c94c97bf419dbf829e55f0cd |
|
BLAKE2b-256 | d5c71ba1b2282dc7410a84e31d0681198df51a4cf737c457f303059d9a1dab29 |