在 C# 中获取屏幕像素颜色的最快方法

The*_*RFG 3 c# windows optimization screen-scraping screen-capture

我正在寻找在 C# 中捕获单个屏幕像素颜色的最快方法到目前为止,我正在使用带有 System.Threading.Timer 的 GDI+ 方法,该方法在回调中调用捕获函数,但我正在寻找最实现我的目标的最佳方式

我当前的代码运行如下

System.Threading.Timer stTimer = new System.Threading.Timer(timerFired, null, 0, 1);
Run Code Online (Sandbox Code Playgroud)

它调用包含此方法的函数

[DllImport("gdi32.dll")]
private static extern int BitBlt(IntPtr srchDC, int srcX, int srcY, int srcW, int srcH, IntPtr desthDC, int destX, int destY, int op);

[DllImport("user32.dll")]
static extern bool ReleaseDC(IntPtr hWnd, IntPtr hDC);

Bitmap screenPixel = new Bitmap(1, 1);
IntPtr hdcMem = CreateCompatibleDC(IntPtr.Zero);

using (Graphics gdest = Graphics.FromImage(screenPixel))
{
    using (Graphics gsrc = Graphics.FromHwnd(appWindow))
    {
        int y = 540;
        Point loc = new Point(xVal, y);

        IntPtr hSrcDC = gsrc.GetHdc();
        IntPtr hDC = gdest.GetHdc();
        int retval = BitBlt(hDC, 0, 0, 1, 1, hSrcDC, loc.X, loc.Y, (int)CopyPixelOperation.SourceCopy);
        gdest.ReleaseHdc();
        gsrc.ReleaseHdc();

    }
}
Color c = screenPixel.GetPixel(0, 0);
Run Code Online (Sandbox Code Playgroud)

但我也想知道 GetPixel 方法是否......

[DllImport("gdi32.dll")]
static extern uint GetPixel(IntPtr hdc, int nXPos, int nYPos);
Run Code Online (Sandbox Code Playgroud)

...在这种情况下,仅获取单个像素的颜色实际上可能会更快

我也在考虑尝试

[DllImport("user32.dll")]
static extern IntPtr GetWindowDC(IntPtr hWnd);

IntPtr hDC = GetWindowDC(appWindow);
int retval = BitBlt(hDC, 0, 0, 1, 1, hSrcDC, loc.X, loc.Y, (int)CopyPixelOperation.SourceCopy);
Run Code Online (Sandbox Code Playgroud)

甚至尝试

[DllImport("gdi32.dll")]
private static extern int BitBlt(IntPtr srchDC, int srcX, int srcY, int srcW, int srcH, IntPtr desthDC, int destX, int destY, int op);

[DllImport("gdi32.dll", SetLastError = true)]
static extern IntPtr CreateCompatibleDC(IntPtr hdc);

[DllImport("user32.dll")]
static extern IntPtr GetWindowDC(IntPtr hWnd);

IntPtr hDC = CreateCompatibleDC(GetWindowDC(appWindow));
int retval = BitBlt(hDC, 0, 0, 1, 1, hSrcDC, loc.X, loc.Y, (int)CopyPixelOperation.SourceCopy);
Run Code Online (Sandbox Code Playgroud)

但我不太确定如何在 C# 上下文中使用 CreateCompatibleDC 函数,或者它此时是否实际上在做任何有用的事情......

我真的很愿意接受任何优化建议,包括 GDI+ 库之外的方法,只要解决方案与 C# 兼容并包含备受赞赏的代码示例

另外,我不太关心计时器的优化,但如果您确实有这方面的优化,请随时分享

The*_*RFG 5

通过使用 timespan 类记录在 GDI+ 和 Managed DirectX 之间从屏幕抓取一个像素所需的时间,事实证明 GDI+ 实际上要快得多。

两种方法均使用以下方法进行测试:

TimeSpan ts = new TimeSpan();

for (int tCount = 0; tCount < 1001; tCount++)
{
    DateTime then = DateTime.Now;

    METHOD_TO_TEST()

    DateTime nNow = DateTime.Now;
    TimeSpan tt = nNow - then;
    ts += tt;
}

return (float)ts.Ticks / (float)(tCount - 1);
Run Code Online (Sandbox Code Playgroud)

这将报告每个操作需要超过 1000 次迭代的平均刻度数

比较时:

//global scope
Bitmap screenPixel = new Bitmap(1, 1);
Color c = Color.Black 

//method to test
using (Graphics gdest = Graphics.FromImage(screenPixel))
{
    using (Graphics gsrc = Graphics.FromHwnd(hWnd))
    {
        IntPtr hSrcDC = gsrc.GetHdc();
        IntPtr hDC = gdest.GetHdc();
        int retval = BitBlt(hDC, 0, 0, 1, 1, hSrcDC, xVal, 540, (int)CopyPixelOperation.SourceCopy);
        gdest.ReleaseHdc();
        gsrc.ReleaseHdc();

    }
}
c = screenPixel.GetPixel(0, 0);
Run Code Online (Sandbox Code Playgroud)

GDI+,至:

//global scope
Color c = Color.Black
PresentParameters parameters = new PresentParameters();
parameters.Windowed = true;
parameters.SwapEffect = SwapEffect.Discard;
Device d = new Device(0, DeviceType.Hardware, hWnd, CreateFlags.HardwareVertexProcessing, parameters);
Surface s = d.CreateOffscreenPlainSurface(Manager.Adapters.Default.CurrentDisplayMode.Width,  Manager.Adapters.Default.CurrentDisplayMode.Height, Format.A8R8G8B8,
                                              Pool.Scratch);

//method to test
d.GetFrontBufferData(0, s);

GraphicsStream gs = s.LockRectangle(LockFlags.None);
byte[] bu = new byte[4];
gs.Position = readPos;
gs.Read(bu, 0, 4);
int r = bu[2];
int g = bu[1];
int b = bu[0];
c = return Color.FromArgb(r, g, b);

s.UnlockRectangle();
s.ReleaseGraphics();
Run Code Online (Sandbox Code Playgroud)

托管 DirectX

GDI+ 运行的平均值为 20831.1953、18611.0566 和 20761.1914 刻度,总平均值为 20,067.814433333333333333333333333 刻度超过 3000 次迭代

而 Managed DirectX 的平均运行次数为 489297.969、496458.4 和 494268.281,在 3000 次迭代中总平均次数为 493,341.55

这意味着,我使用的 Managed DirectX 设置完成 GDI+ 设置所需的时间大约是 24 倍

现在,需要注意的事项...完全有可能存在更有效的方法来使用 DirectX 提取屏幕数据。我尝试从后台缓冲区而不是前台缓冲区中提取数据,但对于这个特定的示例,后台缓冲区没有产生任何有价值的东西(它本质上只是一个黑屏)。另一件需要注意的事情是我实现设备句柄的方式,我很确定它捕获了整个桌面。这可能比仅仅抓取我试图捕获的任何特定窗口的前缓冲区数据效率低...我没有这样做的唯一原因是因为我自己解决这个问题的所有尝试都失败了(DirectX设备实例化中某处的无效调用异常),而且因为我通过任何资源与之交谈的人都不知道有关托管 DirectX 的事情,更不用说如何将其用于我的目的了。

还有一件事需要注意,我听说并读到可以连接到已运行程序的 DirectX api。这可能会产生更快的结果,并且对于其他人来说是一个更好的解决方案,但由于这种挂钩通常具有恶意性质,以及我试图从中捕获的程序用来防止它的措施,这不适用于我的解决方案。

最后,对于仅捕获单个屏幕像素的特殊情况,GDI+ 的 BitBlt 似乎比 Managed DirectX 或至少我的实现更快。