不安全的每像素访问,1756000个像素需要30毫秒访问时间。

7

我在我的网站上分享了一些关于快速、不安全的像素访问的想法。一个绅士给了我一个C++的粗略示例,但是除非我可以进行互操作并且互操作也很快,否则这对我在C#中没有帮助。我在互联网上找到了一个使用MSDN帮助编写的类来不安全地访问像素。这个类异常地快,但还不够快。以下是这个类:

using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.Drawing.Imaging;

namespace DCOMProductions.Desktop.ScreenViewer {
public unsafe class UnsafeBitmap {
    Bitmap bitmap;

    // three elements used for MakeGreyUnsafe
    int width;
    BitmapData bitmapData = null;
    Byte* pBase = null;

    public UnsafeBitmap(Bitmap bitmap) {
        this.bitmap = new Bitmap(bitmap);
    }

    public UnsafeBitmap(int width, int height) {
        this.bitmap = new Bitmap(width, height, PixelFormat.Format32bppArgb);
    }

    public void Dispose() {
        bitmap.Dispose();
    }

    public Bitmap Bitmap {
        get {
            return (bitmap);
        }
    }

    private Point PixelSize {
        get {
            GraphicsUnit unit = GraphicsUnit.Pixel;
            RectangleF bounds = bitmap.GetBounds(ref unit);

            return new Point((int)bounds.Width, (int)bounds.Height);
        }
    }

    public void LockBitmap() {
        GraphicsUnit unit = GraphicsUnit.Pixel;
        RectangleF boundsF = bitmap.GetBounds(ref unit);
        Rectangle bounds = new Rectangle((int)boundsF.X,
      (int)boundsF.Y,
      (int)boundsF.Width,
      (int)boundsF.Height);

        // Figure out the number of bytes in a row
        // This is rounded up to be a multiple of 4
        // bytes, since a scan line in an image must always be a multiple of 4 bytes
        // in length. 
        width = (int)boundsF.Width * sizeof(Pixel);
        if (width % 4 != 0) {
            width = 4 * (width / 4 + 1);
        }
        bitmapData =
      bitmap.LockBits(bounds, ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);

        pBase = (Byte*)bitmapData.Scan0.ToPointer();
    }

    public Pixel GetPixel(int x, int y) {
        Pixel returnValue = *PixelAt(x, y);
        return returnValue;
    }

    public void SetPixel(int x, int y, Pixel colour) {
        Pixel* pixel = PixelAt(x, y);
        *pixel = colour;
    }

    public void UnlockBitmap() {
        bitmap.UnlockBits(bitmapData);
        bitmapData = null;
        pBase = null;
    }
    public Pixel* PixelAt(int x, int y) {
        return (Pixel*)(pBase + y * width + x * sizeof(Pixel));
    }
}

我正在做的是复制整个屏幕并将每个像素与旧副本进行比较。在一个1680x1050位图上,使用以下代码大约需要300毫秒。

private Bitmap GetInvalidFrame(Bitmap frame) {
        Stopwatch sp = new Stopwatch();
        sp.Start();

        if (m_FrameBackBuffer == null) {
            return frame;
        }

        Int32 pixelsToRead = frame.Width * frame.Height;
        Int32 x = 0, y = 0;

        UnsafeBitmap unsafeBitmap = new UnsafeBitmap(frame);
        UnsafeBitmap unsafeBuffBitmap = new UnsafeBitmap(m_FrameBackBuffer);
        UnsafeBitmap retVal = new UnsafeBitmap(frame.Width, frame.Height);

        unsafeBitmap.LockBitmap();
        unsafeBuffBitmap.LockBitmap();
        retVal.LockBitmap();

        do {
            for (x = 0; x < frame.Width; x++) {
                Pixel newPixel = unsafeBitmap.GetPixel(x, y);
                Pixel oldPixel = unsafeBuffBitmap.GetPixel(x, y);

                if (newPixel.Alpha != oldPixel.Alpha || newPixel.Red != oldPixel.Red || newPixel.Green != oldPixel.Green || newPixel.Blue != oldPixel.Blue) {
                   retVal.SetPixel(x, y, newPixel);
                }
                else {
                    // Skip pixel
                }
            }

            y++;
        } while (y != frame.Height);

        unsafeBitmap.UnlockBitmap();
        unsafeBuffBitmap.UnlockBitmap();
        retVal.UnlockBitmap();

        sp.Stop();

        System.Diagnostics.Debug.WriteLine(sp.Elapsed.Milliseconds.ToString());

        sp.Reset();

        return retVal.Bitmap;
    }

有没有可能采用某种方法/手段/途径,使其速度加快到大约30毫秒?使用Graphics.CopyFromScreen()可以在大约30毫秒内复制屏幕,因此每秒产生大约30帧。但是,程序的速度仅与其更慢的那个部分一样快,因此GetInvalidFrame中的300毫秒延迟会将速度减慢到每秒1-3帧左右,这对于会议软件来说不太好。

如果有任何建议、方法或指引,将会非常棒!同时,下面提供了用于在客户端绘制位图的代码。

关于Dmitriy的回答/评论:

#region RootWorkItem

    private ScreenClient m_RootWorkItem;
    /// <summary>
    /// Gets the RootWorkItem
    /// </summary>
    public ScreenClient RootWorkItem {
        get {
            if (m_RootWorkItem == null) {
                m_RootWorkItem = new ScreenClient();
                m_RootWorkItem.FrameRead += new EventHandler<FrameEventArgs>(RootWorkItem_FrameRead);
            }
            return m_RootWorkItem;
        }
    }

    #endregion

    private void RootWorkItem_FrameRead(Object sender, FrameEventArgs e) {
        if (e.Frame != null) {
            if (uxSurface.Image != null) {
                Bitmap frame = (Bitmap)uxSurface.Image;

                Graphics g = Graphics.FromImage(frame);
                g.DrawImage(e.Frame, 0, 0); // Draw only updated pixels

                uxSurface.Image = frame;
            }
            else {
                uxSurface.Image = e.Frame; // Draw initial, full image
            }
        }
        else {
            uxSurface.Image = null;
        }
    }

1
请注意,如果您正在尝试执行类似远程桌面的操作,它不会比较位图以确定要发送给客户端的内容。它会在绘图命令写入屏幕之前拦截这些命令。 - David
请查看以下网址中的帖子,看看是否有任何有用的技巧:http://www.opensubscriber.com/message/dotnet-cx@discuss.develop.com/2170514.html。 - Brian
@David:我想起了一些远程桌面软件用来处理与你描述的类似问题的策略:以较低的像素深度发送显示信息。如果你使用8位图形(至少在比较部分),你的比较速度会快4倍。 - Brian
1
@David Anderson:http://www.dreamincode.net/code/snippet2859.htm非常快,但它确实使用了哈希。它还允许您获取一个字节数组,而无需编写自己的不安全代码(耶)。 - Brian
1
具有使用GDI+不安全代码的解释的教程:http://www.codeproject.com/KB/GDI-plus/csharpgraphicfilters11.aspx - Brian
5个回答

16

使用整数而非像素和单个循环的不安全方法:

private static Bitmap GetInvalidFrame(Bitmap oldFrame, Bitmap newFrame)
{
    if (oldFrame.Size != newFrame.Size)
    {
        throw new ArgumentException();
    }
    Bitmap result = new Bitmap(oldFrame.Width, oldFrame.Height, oldFrame.PixelFormat);

    Rectangle lockArea = new Rectangle(Point.Empty, oldFrame.Size);
    PixelFormat format = PixelFormat.Format32bppArgb;
    BitmapData oldData = oldFrame.LockBits(lockArea, ImageLockMode.ReadOnly, format);
    BitmapData newData = newFrame.LockBits(lockArea, ImageLockMode.ReadOnly, format);
    BitmapData resultData = result.LockBits(lockArea, ImageLockMode.WriteOnly, format);

    int len = resultData.Height * Math.Abs(resultData.Stride) / 4;

    unsafe
    {
        int* pOld = (int*)oldData.Scan0;
        int* pNew = (int*)newData.Scan0;
        int* pResult = (int*)resultData.Scan0;

        for (int i = 0; i < len; i++)
        {
            int oldValue = *pOld++;
            int newValue = *pNew++;
            *pResult++ = oldValue != newValue ? newValue : 0 /* replace with 0xff << 24 if you need non-transparent black pixel */;
            // *pResult++ = *pOld++ ^ *pNew++; // if you can use XORs.
        }
    }

    oldFrame.UnlockBits(oldData);
    newFrame.UnlockBits(newData);
    result.UnlockBits(resultData);

    return result;
}

我认为你确实可以在这里使用异或帧,我希望这可以在两方面都有更好的性能。

    private static void XorFrames(Bitmap leftFrame, Bitmap rightFrame)
    {
        if (leftFrame.Size != rightFrame.Size)
        {
            throw new ArgumentException();
        }

        Rectangle lockArea = new Rectangle(Point.Empty, leftFrame.Size);
        PixelFormat format = PixelFormat.Format32bppArgb;
        BitmapData leftData = leftFrame.LockBits(lockArea, ImageLockMode.ReadWrite, format);
        BitmapData rightData = rightFrame.LockBits(lockArea, ImageLockMode.ReadOnly, format);

        int len = leftData.Height * Math.Abs(rightData.Stride) / 4;

        unsafe
        {
            int* pLeft = (int*)leftData.Scan0;
            int* pRight = (int*)rightData.Scan0;

            for (int i = 0; i < len; i++)
            {
                *pLeft++ ^= *pRight++;
            }
        }

        leftFrame.UnlockBits(leftData);
        rightFrame.UnlockBits(rightData);
    }
您可以按照以下方式在双方都使用此过程:
在服务器端,您需要评估旧帧和新帧之间的差异,将其发送到客户端并用新帧替换旧帧。服务器代码应该像这样:
  XorFrames(oldFrame, newFrame); // oldFrame ^= newFrame
  Send(oldFrame); // send XOR of two frames
  oldFrame = newFrame;

在客户端上,您需要使用从服务器接收到的异或帧更新当前帧:

  XorFrames((Bitmap)uxSurface.Image, e.Frame);

调用Graphics.CopyFromScreen后,我会在位图上调用GetInvalidFrame以将像素与屏幕的旧副本进行比较。对于每个“未”更改的像素,我都会删除该像素。最终结果是一个透明的位图,其中仅包含更改的像素,大约为4000字节的图像。因此,这对于跨网络使用而言非常轻量级。最大的问题是我需要在约30毫秒内比较所有170万像素,以便我可以产生30fps并发送给客户端。如果1帧=4kb,则4 * 30 = 120kb,1.5mbit连接足以使用1680x1050发送30fps。在局域网上,这甚至更好。 - David Anderson
天啊,那给了我10-30毫秒。你不知道你刚刚为我做了多少事情,我非常感激。真是太棒了! - David Anderson
你如何在客户端绘制这些位图? - okutane
基本上我有一个套接字,它接收所有的字节。解压缩GZipped位图,然后将其发送到名为FrameRead的事件。注释不能包含代码,所以我会在这个答案上面发布一个答案。 - David Anderson
我尝试实现Xor客户端,但似乎它没有更新图像。在调用xor之前,我还在客户端上复制了旧位图,然后将其绘制回来,但它会严重闪烁并留下无效的像素。可能是我在客户端没有正确实现它,使用服务器端的xor并以前的绘制方式也很好。 - David Anderson
如果存在无效像素,则可能存在某些传输问题。闪烁是意外的,但可以通过将控件的DoubleBuffered属性设置为true来解决。 - okutane

3

是的,您可以使用unsafe代码来实现。

BitmapData d = l.LockBits(new Rectangle(0, 0, l.Width, l.Height), ImageLockMode.ReadOnly,l.PixelFormat);
IntPtr scan = d.Scan0;
unsafe
{
    byte* p = (byte*)(void*)scan;
    //dostuff               
}

请查看http://www.codeproject.com/KB/GDI-plus/csharpgraphicfilters11.aspx获取此类基本示例。我的代码是基于这个的。

注意: 这将比你的快得多的原因之一是,你是单独比较每个通道,而不是使用一个操作来比较整个字节。同样,更改PixelAt以给你一个字节来方便这个操作可能会带来改进。


是的,这与unsafebitmap的工作方式相同。但是直接使用此功能将更快。如果您利用了直接使用内存的事实并使用比一次比较多个字节的操作,则可能会获得更快的结果。 - Brian
一个像素由四个字节组成(四个通道 = 红色,绿色,蓝色,透明度),为了得到准确的比较结果,您必须比较所有四个字节。我不知道有什么方法可以在没有相同底层代码的情况下同时比较所有四个字节。除非我误解了您的意思。ARGB 蓝色 = 第一个字节 绿色 = 第二个字节 红色 = 第三个字节 透明度 = 第四个字节然而,我尝试每次循环比较多达30个像素,但性能仅提高了约60毫秒,并且仍然维持在约240毫秒左右,这仍然相当慢速。 - David Anderson
2
@David:你可以使用长指针,在每一步增加4来同时比较这4个。 - Brian
哦,很酷。你能给我展示一个简短的例子吗?到目前为止,我还没有深入使用过C#中的指针。 - David Anderson
我也没有,但你应该可以用 long* p = (long*)(void*)scan; 替换 byte* p = (byte*)(void*)scan;。你将使用 p[0] 来引用长整型,并将 p 设置为 p + 4 来处理下一个像素。话虽如此,更简单的方法是通过将 Pixel 强制转换为 long 来更改 unsafebitmap 以返回 long(这样做不够通用;如果您的像素不是精确的32位,则会出现问题)。 - Brian
可以通过指定正确的像素格式来确保像素大小。 - okutane

3

现在我正在尝试避免使用GPU,因为我在这个领域没有太多的知识。我在寻找如何使用Managed DirectX使用GPU比较像素的资源方面并不太成功。我避免这样做的另一个原因是因为我不打算在我的应用程序中包含任何第三方库。我非常重视那个链接,我将会研究Microsoft Accelerator。这是一个很好的资源,感谢您指引我朝着这个方向。 - David Anderson

1

与其逐个检查每一个像素,不如只执行两个位图的基本内存比较。在C语言中,可以使用memcmp()函数。

这将为您提供更快速的测试,让您知道图像是否相同。只有在您知道它们不同的时候,才需要使用更昂贵的代码来帮助您确定它们的不同之处(如果您甚至需要知道这一点)。

虽然我不是C#的专家,但我不知道获取原始内存有多容易。


由于这是会议软件,屏幕将会频繁变化,因此我必须比较像素,只写入更改的像素到输出帧以发送给客户端。这是不可避免的,因为逐像素比较是获取网络传输最佳位图大小的唯一方法。 - David Anderson

0

成功减少了大约60毫秒。我认为这需要GPU。我没有看到任何利用CPU解决此问题的方案,即使一次比较多于一个字节/像素,除非有人能够快速编写代码示例。仍然保持在约200-260ms左右,对于30fps来说太慢了。

private static BitmapData m_OldData;
private static BitmapData m_NewData;
private static unsafe Byte* m_OldPBase;
private static unsafe Byte* m_NewPBase;
private static unsafe Pixel* m_OldPixel;
private static unsafe Pixel* m_NewPixel;
private static Int32 m_X;
private static Int32 m_Y;
private static Stopwatch m_Watch = new Stopwatch();
private static GraphicsUnit m_GraphicsUnit = GraphicsUnit.Pixel;
private static RectangleF m_OldBoundsF;
private static RectangleF m_NewBoundsF;
private static Rectangle m_OldBounds;
private static Rectangle m_NewBounds;
private static Pixel m_TransparentPixel = new Pixel() { Alpha = 0x00, Red = 0, Green = 0, Blue = 0 };

private Bitmap GetInvalidFrame(Bitmap frame) {
    if (m_FrameBackBuffer == null) {
        return frame;
    }

    m_Watch.Start();

    unsafe {
        m_OldBoundsF = m_FrameBackBuffer.GetBounds(ref m_GraphicsUnit);
        m_OldBounds = new Rectangle((Int32)m_OldBoundsF.X, (Int32)m_OldBoundsF.Y, (Int32)m_OldBoundsF.Width, (Int32)m_OldBoundsF.Height);
        m_OldData = m_FrameBackBuffer.LockBits(m_OldBounds, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);

        m_NewBoundsF = m_FrameBackBuffer.GetBounds(ref m_GraphicsUnit);
        m_NewBounds = new Rectangle((Int32)m_NewBoundsF.X, (Int32)m_NewBoundsF.Y, (Int32)m_NewBoundsF.Width, (Int32)m_NewBoundsF.Height);
        m_NewData = frame.LockBits(m_NewBounds, ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);

        m_OldPBase = (Byte*)m_OldData.Scan0.ToPointer();
        m_NewPBase = (Byte*)m_NewData.Scan0.ToPointer();

        do {
            for (m_X = 0; m_X < frame.Width; m_X++) {

                m_OldPixel = (Pixel*)(m_OldPBase + m_Y * m_OldData.Stride + 1 + m_X * sizeof(Pixel));
                m_NewPixel = (Pixel*)(m_NewPBase + m_Y * m_NewData.Stride + 1 + m_X * sizeof(Pixel));

                if (m_OldPixel->Alpha == m_NewPixel->Alpha // AccessViolationException accessing Property in get {}
                    || m_OldPixel->Red == m_NewPixel->Red
                    || m_OldPixel->Green == m_NewPixel->Green
                    || m_OldPixel->Blue == m_NewPixel->Blue) {

                    // Set the transparent pixel
                    *m_NewPixel = m_TransparentPixel;
                }
            }

            m_Y++; //Debug.WriteLine(String.Format("X: {0}, Y: {1}", m_X, m_Y));
        } while (m_Y < frame.Height);
    }

    m_Y = 0;

    m_Watch.Stop();
    Debug.WriteLine("Time elapsed: " + m_Watch.ElapsedMilliseconds.ToString());
    m_Watch.Reset();

    return frame;
}

这个“Pixel”结构体是从哪里来的? - Sam

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接