views:

905

answers:

6

So I've been sharing some thoughts on the above topic title on my website about fast, unsafe pixel access. A gentlemen gave me a rough example of how he'd do it in C++, but that doesn't help me in C# unless I can interop it, and the interop is fast as well. I had found a class in the internet that was written using MSDN help, to unsafely access pixels. The class is exceptionally fast, but it's not fast enough. Here's the class:

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));
    }
}

}

Basically what I am doing is copying the entire screen and comparing each pixel to and old copy. On a 1680x1050 bitmap, this takes approximately 300 milliseconds using the following code.

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;
    }

Is there any possible method/means/approach that I could speed this up to about 30ms? I can copy the screen in about 30ms using Graphics.CopyFromScreen(), so that produces approximately 30 frames each second. However, a program only runs as fast as its slower counterpart, so the 300ms delay in GetInvalidFrame, slows this down to about 1 - 3 frames each second. This isn't good for a meeting software.

Any advice, approaches, pointers in the right direction would be absolutely wonderful! Also, the code that is used to draw the bitmap on the client-side is below as well.

To comment on Dmitriy's answer/comment:

#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;
        }
    }
+2  A: 

Yes, you can do so by using unsafe code.

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               
}

Check out http://www.codeproject.com/KB/GDI-plus/csharpgraphicfilters11.aspx for some basic examples of this kind of stuff. My code is based on that.

Note: One of the reasons this will be much faster than yours is that you are separately comparing each channel instead of just comparing the entire byte using one operation. Similarly, changing PixelAt to give you a byte to facilitate this would probably give you an improvement.

Brian
Yes, this works the same way as unsafebitmap. But using this stuff directly will be faster. You'll probably get even faster results if you take advantage of the fact that you are now working directly with the memory and use operations that compare more than one byte at a time.
Brian
A pixel is made up of four bytes (four channels = red, green, blue, alpha), to get accurate comparision you have to compare all four bytes. I don't know of any way to compare all four at the same time without even the underlying code being the same as I already have. Unless I'm missing what you meanARGBBlue = First ByteGreen = Second ByteRed = Third ByteAlpha = Fourth Byte I did however try comparing up to 30 pixels each loop, but performance was only increased by about 60ms, and still sat around 240ms, which is still pretty slow
David Anderson
@David: You compare all 4 at the same time by using a long pointer and incrementing by 4 at each step.
Brian
Oh, cool. Could (please, heh) you show me a brief example on how to do that? Haven't worked with pointers in depth in C# up to now.
David Anderson
I haven't either, but you should be able to just replace byte* p = (byte*)(void*)scan; with long* p = (long*)(void*)scan; . You'd use p[0] to refer to th elong and would set p to p + 4 to deal with the next pixel. That said, it'd be even simpler to change unsafebitmap to return a long by casting to long instead of Pixel (this is less generic; it will break if your pixel is not exactly 32 bit).
Brian
Pixel size can be ensured by specification of correct pixel format.
Dmitriy Matveev
+1  A: 

Instead of checking each and every pixel, you can just perform a basic memory compare of the 2 bitmaps. In C, something like memcmp().

This would give you a much quicker test to let you know that the images are the same or not. Only when you know they are different do you need to resort to the more expensive code that will help you determine where they are different (if you even need to know that).

I am not a C# person though, so I don't know how easy it is to get access to the raw memory.

rikh
Since this is meeting software, the screen will be changing quite a bit, so I have to compare the pixels to write only the changed pixels to the output frame to send to the client. this isn't really avoidable as doing per pixel comparision is the only way to get optimal bitmap size for network travel
David Anderson
+3  A: 

Here: Utilizing the GPU with c# there are mentioned some librarys for using the GPU from C#.

Allan Simonsen
Right now I am trying to avoid using the GPU as I don't have that much knowledge in that area. I haven't been too successful on finding any resources on how to compare pixels using the GPU using MAnaged DirectX. The other reason I'm avoiding this is because I don't plan on including any third party libraries in my app.I highly value that link though, I will be looking into the Microsoft Accelerator. Great resource, thanks for pointing me in that direction
David Anderson
A: 

Was able to slice off about 60ms. I think this is going to require the GPU. I'm not seeing any solution to this utilizing the CPU, even by comparing more than one byte/pixel at a time, unless someone can whip up a code sample to show me otherwise. Still sits at about 200-260ms, far too slow for 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;
}
David Anderson
+7  A: 

Unsafe approach with usage of integers instead of Pixels and single loop:

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;
}

I think you really can use XORed frames here and I hope that this can have better performance on both sides.

    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);
    }

You can use this procedure on both sides in following way:
On server side you need to evaluate difference between old and new frame, send it to client and replace old frame by new. The server code should look something like this:

  XorFrames(oldFrame, newFrame); // oldFrame ^= newFrame
  Send(oldFrame); // send XOR of two frames
  oldFrame = newFrame;

On client side you need to update your current frame with xor frame recieved from server:

  XorFrames((Bitmap)uxSurface.Image, e.Frame);
Dmitriy Matveev
After Graphics.CopyFromScreen is called, I call GetInvalidFrame on the bitmap to compare the pixels with an old copy of the screen. For each pixel that is "not" changed, I remove that pixel. The end result is a transparent bitmap containing only the changed pixels, and about a 4000 byte image. So this is lightweight for use across a network. Biggest problem is I need to compare all 1.7~million pixels in about 30 ms so I can produce 30 fps to send to the client. if 1 frame = 4kb, 4*30 = 120kb, a 1.5mbit connection is well enough to send 30fps using 1680x1050. On a lan, this is even better
David Anderson
Well I'll be damned. That gave me 10 - 30ms. You have no idea how much I appreciate what you just did for me. Absolutely wonderful!
David Anderson
How do you draw these bitmaps on client side?
Dmitriy Matveev
Basically I have a socket that receives all the bytes of course. Decompresses the GZipped bitmap, and then its sent to an event called FrameRead. Comments can't contain code, so I'll post an answer above this one
David Anderson
I've updated by answer.
Dmitriy Matveev
Will implement the updated code and post back results.
David Anderson
Simple, but chock-full of chocolatey goodness. :)
Greg D
I tried to implement the Xor client side, but it doesn't seem to update the image. I also made a copy of the old bitmap on the client before calling xor, then drawing the copy back, but it flashes severely and leaves invalid pixels. possible im not implementing it correctly client side, using xor server side and drawing it how i was before works great too.
David Anderson
Hmm. If it there is invalid pixels then there can be some of transport problem. Flashing is unexpected at all, but it can be fixed by setting DoubleBuffered property to true for your control.
Dmitriy Matveev
A: 

were is Pixel class??!!!!

mido