c# - 使用 C# 提高多线程访问的位图检索(从屏幕/窗口)速度和存储

标签 c# multithreading bitmap

我在尝试即时读取屏幕截图以及对这些屏幕截图(位图)进行多线程访问时遇到了一些问题。

对于初学者来说,捕获屏幕数据我有以下代码:

NativeMethods.GetWindowRect(HandleDisplay, out r);
Size s = new Size(r.Right - r.Left, r.Bottom - r.Top);
Bitmap b = new Bitmap(s.Width, s.Height, PixelFormat.Format24bppRgb);
using (Graphics gfxBmp = Graphics.FromImage(b))
{
    try {
        gfxBmp.CopyFromScreen(r.Left, r.Top, 0, 0, s, CopyPixelOperation.SourceCopy);
    }
    finally
    {
        gfxBmp.Dispose();
    }
}

现在代码按预期工作并且创建的位图正确。我在使用这段代码时遇到的第一个问题是速度。我没有超时以毫秒为单位执行,但是我可以说代码被包装并执行以在其自己的线程中重复更新位图,并且在 1000 x 600 像素的屏幕上它更新大约 45 到每秒 50 次。如果屏幕尺寸较大,执行率会显着下降,例如捕获 1440 x 900 的窗口会将更新降至每秒 20 次或更少。

我遇到的第二个问题是位图正在从一个线程重复存储/更新,并且我有其他线程需要访问该位图(或部分位图)。问题是,当主更新线程写入位图,而另一个线程试图读取位图时,我显然会收到“对象已在别处使用”错误。我所说的多线程是指像这样的任务:

Task t = Task.Run(() => {
    while(true) {
       try {
           token.ThrowIfCancellationRequested();
           // update/process the data
           ...
       } catch {
           break;
       }
}, token);

那么存储位图(即作为位图或 byte[] 或其他方式)并从其他线程访问它的最佳方式是什么。我一直在考虑将其转换为 byte[] 数据(使用 LockBits 和 Marshal.Copy),但尚未确定这是否是正确的路线。

问题:

  1. 有没有办法提高获取图像数据的速度(也许 使用 byte[] 并以某种方式仅“更新”已更改的像素数据)。

  2. 如何让它成为线程安全的。

    • 我将有一个主线程不断更新它(只写)
    • 我会有很多线程需要全部或部分阅读(只读)

警告: 窗口可能是也可能不是 DirectX/OpenGL(这就是为什么我最初选择这种方法而不是其他方法的原因)


编辑/更新:

我进行了以下更改,这些更改略微提高了性能。 - gfxBmp 现在是使用实例构造函数初始化的静态属性 - 删除了事件处理程序(类不再需要抛出通知图像已更新的事件) - 对图像属性的访问被 ReaderWriterLockSlim 包裹(并且从其他线程读取似乎工作得很好)

如前所述,所有可能的开销都已消除,任务现在如下所示:

cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
IsRunning = true;
Task t = Task.Run(() =>
{
    while (IsRunning)
    {
        try
        {
            token.ThrowIfCancellationRequested();
            Refresh();
            CountFrame();
        }
        catch
        {
            IsRunning = false;
            break;
        }
    }
}, token);

我的刷新方法是这样的:

private void Refresh()
{
    NativeMethods.Rect r = new NativeMethods.Rect();
    NativeMethods.GetWindowRect(hWndDisplay, out r);
    if (r != rect) Rect = r;
    gfx.CopyFromScreen(rect.Left, rect.Top, 0, 0, size, CopyPixelOperation.SourceCopy);
}

CountFrame 与速度无关,因为我使用和不使用如下基准:

With CountFrame (Screen size captured 872px x 480px ~60fps):
Iterations: 1000, Total: 16.20 s, Average: 16.195 ms
Iterations: 1000, Total: 16.18 s, Average: 16.178 ms
Iterations: 1000, Total: 16.44 s, Average: 16.44 ms

Without CountFrame (Screen size captured 872px x 480px ~60fps):
Iterations: 1000, Total: 16.21 s, Average: 16.213 ms
Iterations: 1000, Total: 16.17 s, Average: 16.17 ms
Iterations: 1000, Total: 16.18 s, Average: 16.183 ms

Without CountFrame (Screen size captured 1402px x 828px ~32fps)
Iterations: 1000, Total: 29.74 s, Average: 29.744 ms
Iterations: 1000, Total: 30.08 s, Average: 30.083 ms
Iterations: 1000, Total: 29.44 s, Average: 29.444 ms

因此,线程安全问题似乎已得到修复,已按照说明进行了优化。所以我现在仍在为速度而苦苦挣扎。正如您所看到的,FrameRate 在 1402 x 828 下下降,更好的捕捉方式的问题仍然不明确。

目前我正在使用 Graphics.CopyFromScreen(...) 来获取像素,据我所知,它在内部使用了 bitblt。是否有另一种方法可以通过像素/字节数据比较之类的方式从屏幕区域获取数据,并且仅更新更改的信息或我应该查看的其他资源?

最佳答案

非常感谢@DiskJunky,因为他的评论才是真正的答案:

<强>1。线程安全 - 将 Graphics.CopyFromScreen() 逻辑移动到每个准备就绪的人,并且只阅读屏幕的必要部分供读者使用。这允许读者“拥有”单个位图,从而消除了线程锁定的需要。

<强>2。性能 - 有了上面的内容,阅读器现在由于包含 CopyFromScreen 而变得有点重,但是不需要(代码)来裁剪所需的共享屏幕部分和锁定逻辑。此外,在可能的情况下,重构代码以尽可能在读取器类中实例化和重用属性(即,不再需要在每次调用 CopyFromScreen 时启动新的 Graphic 对象),从而减少开销和时钟周期。

归结为最终重新思考我处理逻辑的方式:

原始逻辑是让一个线程不断更新位图,并在需要时将其共享给所有需要它的读者。

优化的逻辑让读者处理只获取必要信息的问题,并且在必要时直接从源头获取信息,而不是通过中间人。以更少的开销生成更清晰的代码。

关于c# - 使用 C# 提高多线程访问的位图检索(从屏幕/窗口)速度和存储,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46430977/

相关文章:

c++ - 如何正确使用 std::atomic_signal_fence()?

android - 写入可序列化对象时出错(位图包含)

Ice Cream Sandwich 中的 java.lang.OutOfMemoryError

java - Android - 是否可以在指定位置将图像插入到另一个图像中?

c# - 在 Blazor 上将 Navlink css 类从 Active 更改为 own

python - 我可以触发并忘记提交到线程池的可调用对象吗?

c++ - 何时使用 C++11 mutex、lock、unique_lock、shared_lock 等

c# - Owin 不包含 "UseHangfire"的定义,也没有扩展方法 "UseHangfire"

c# - Linq 表达式树 Any() 问题

c# - 如何避免 Epson EPOS 上的缓冲区溢出(通过串口)?