我正在开发一个项目,我必须使用 CreateCompatibleBitmap 和 Bitblt 读取屏幕上像素的颜色。不幸的是,这个方法非常慢,我在一个循环中得到了大约 60 到 100 毫秒的时间。我使用这个代码:
HDC hdc = GetDC(NULL), hdcMem = CreateCompatibleDC(hdc);
HBITMAP hBitmap = CreateCompatibleBitmap(hdc, ScreenX, ScreenY);
BITMAPINFOHEADER bmi = { 0 };
bmi.biSize = sizeof(BITMAPINFOHEADER);
bmi.biPlanes = 1;
bmi.biBitCount = 24;
bmi.biWidth = ScreenX;
bmi.biHeight = -ScreenY;
bmi.biCompression = BI_RGB;
SelectObject(hdcMem, hBitmap);
BitBlt(hdcMem, 0, 0, ScreenX, ScreenY, hdc, 0, 0, SRCCOPY);
GetDIBits(hdc, hBitmap, 0, ScreenY, ScreenData, (BITMAPINFO*)&bmi, DIB_RGB_COLORS);
DeleteObject(hBitmap);
DeleteDC(hdcMem);
ReleaseDC(NULL, hdc);
大部分时间(95%)花在 Bitblt 函数上。我读到它需要很长时间,因为 Bitblt 必须转换颜色格式,但我不明白如何避免这种情况......
我使用 Windows 11,屏幕分辨率为 FHD 1920x1080
有什么建议可以加快这个程序的速度吗?
您可以使用
CreateDIBSection
创建一种位图,让您可以访问像素数据,而不是创建常规的“兼容”位图,然后使用 GetDIBits
将像素数据复制到缓冲区中,但我运行了一个基准测试这样可以节省时间,但并不多。我的速度提高了大约 10%,但这并不多。
下面的代码,我写得很快,所以可能犯了一个错误。下面的变量
dummy
的目的是让一切都不会因为没有输出而被优化掉。
#include <vector>
#include <iostream>
#include <Windows.h>
#include <chrono>
namespace c = std::chrono;
void get_screen_bytes1(void* ScreenData, int ScreenX, int ScreenY) {
HDC hdc = GetDC(NULL), hdcMem = CreateCompatibleDC(hdc);
HBITMAP hBitmap = CreateCompatibleBitmap(hdc, ScreenX, ScreenY);
BITMAPINFOHEADER bmi = { 0 };
bmi.biSize = sizeof(BITMAPINFOHEADER);
bmi.biPlanes = 1;
bmi.biBitCount = 24;
bmi.biWidth = ScreenX;
bmi.biHeight = -ScreenY;
bmi.biCompression = BI_RGB;
SelectObject(hdcMem, hBitmap);
BitBlt(hdcMem, 0, 0, ScreenX, ScreenY, hdc, 0, 0, SRCCOPY);
GetDIBits(hdc, hBitmap, 0, ScreenY, ScreenData, (BITMAPINFO*)&bmi, DIB_RGB_COLORS);
DeleteObject(hBitmap);
DeleteDC(hdcMem);
ReleaseDC(NULL, hdc);
}
struct bitmap_info {
HBITMAP handle;
uint8_t* data;
};
bitmap_info get_screen_bytes2(int wd, int hgt) {
HDC hdc_scr = GetDC(NULL);
BITMAPINFO bmi;
memset(&bmi, 0, sizeof(BITMAPINFO));
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = wd;
bmi.bmiHeader.biHeight = -hgt;
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 24;
bmi.bmiHeader.biCompression = BI_RGB;
bitmap_info bi;
bi.handle = CreateDIBSection(hdc_scr, &bmi, DIB_RGB_COLORS, (void**)bi.data, NULL, NULL);
HDC hdc = CreateCompatibleDC(hdc_scr);
auto hbm_old = SelectObject(hdc, bi.handle);
BitBlt(hdc, 0, 0, wd, hgt, hdc_scr, 0, 0, SRCCOPY);
SelectObject(hdc, hbm_old);
DeleteDC(hdc);
ReleaseDC(NULL, hdc_scr);
return bi;
}
int main() {
constexpr auto scr_wd = 2560;
constexpr auto scr_hgt = 1440;
std::vector<uint8_t> buffer(scr_wd * scr_hgt * 3);
std::chrono::high_resolution_clock timer;
int dummy;
double sum = 0.0;
int n = 100;
for (int i = 0; i < n; ++i) {
auto start = timer.now();
get_screen_bytes1(buffer.data(), scr_wd, scr_hgt);
sum += c::duration_cast<c::microseconds>(timer.now() - start).count();
dummy += buffer[0];
}
double time1 = sum / n;
std::cout << "get_screen_bytes1 => " << time1 << "\n";
sum = 0.0;
for (int i = 0; i < n; ++i) {
auto start = timer.now();
auto bmp_info = get_screen_bytes2( scr_wd, scr_hgt);
sum += c::duration_cast<c::microseconds>(timer.now() - start).count();
dummy += bmp_info.data[0];
DeleteObject(bmp_info.handle);
}
double time2 = sum / n;
std::cout << "get_screen_bytes2 => " << time2 << "\n";
std::cout << dummy << "\n";
std::cout << "pcnt speed improvement => " << time2 / time1 << "\n";
}
解决方案非常简单:除非您确实需要捕获整个屏幕,否则不要使用
GetDC(NULL)
。从窗口读取像素的成本远低于从屏幕。
HWND hwnd = FindWindow(nullptr, TEXT("Your window name")); // or HWND hwnd = GetForegroundWindow() or another way to get the needed HWND
然后:
HDC hdc = GetDC(hwnd);
在我的电脑上,速度提高了大约 50 倍。
不过,值得注意的是,这么快
BitBlt
并不是没有成本的。虽然每个BitBlt
的成本降低了,但由于调用更加频繁,会消耗大量的GPU资源。所以,请适当限制其速度,因为多次捕获同一帧是没有意义的。
如果您想要捕获的窗口无法使用 BitBlt,请尝试禁用硬件加速、关闭专用显卡或切换目标窗口以使用 DirectX 渲染。或者,您可以考虑使用 Windows.Graphics.Capture API。然而,这比 GDI 方法复杂得多。
奖励喋喋不休:在任务管理器中,BitBlt 的 GPU 消耗不会归因于调用进程。相反,对 BitBlt 使用 GetDC(NULL) 会增加 dwm.exe 的 GPU 消耗,而对 BitBlt 使用 GetDC(hwnd) 会增加 csrss.exe.
的 GPU 消耗