Fast work with Bitmaps in C#

I need to access each pixel of a Bitmap, work with them, then save them to a Bitmap.

Using Bitmap.GetPixel() and Bitmap.SetPixel(), my program runs slowly.

How can I quickly convert Bitmap to byte[] and back?

I need a byte[] with length = (4 * width * height), containing RGBA data of each pixel.

13.10.2009 21:36:09
Following link contains bitmap pixel access methods comparatively. csharpexamples.com/fast-image-processing-c
turgay 3.09.2014 11:34:29
7 ОТВЕТОВ
РЕШЕНИЕ

You can do it a couple of different ways. You can use unsafe to get direct access to the data, or you can use marshaling to copy the data back and forth. The unsafe code is faster, but marshaling doesn't require unsafe code. Here's a performance comparison I did a while back.

Here's a complete sample using lockbits:

/*Note unsafe keyword*/
public unsafe Image ThresholdUA(float thresh)
{
    Bitmap b = new Bitmap(_image);//note this has several overloads, including a path to an image

    BitmapData bData = b.LockBits(new Rectangle(0, 0, _image.Width, _image.Height), ImageLockMode.ReadWrite, b.PixelFormat);

    byte bitsPerPixel = GetBitsPerPixel(bData.PixelFormat);

    /*This time we convert the IntPtr to a ptr*/
    byte* scan0 = (byte*)bData.Scan0.ToPointer();

    for (int i = 0; i < bData.Height; ++i)
    {
        for (int j = 0; j < bData.Width; ++j)
        {
            byte* data = scan0 + i * bData.Stride + j * bitsPerPixel / 8;

            //data is a pointer to the first byte of the 3-byte color data
            //data[0] = blueComponent;
            //data[1] = greenComponent;
            //data[2] = redComponent;
        }
    }

    b.UnlockBits(bData);

    return b;
}

Here's the same thing, but with marshaling:

/*No unsafe keyword!*/
public Image ThresholdMA(float thresh)
{
    Bitmap b = new Bitmap(_image);

    BitmapData bData = b.LockBits(new Rectangle(0, 0, _image.Width, _image.Height), ImageLockMode.ReadWrite, b.PixelFormat);

    /* GetBitsPerPixel just does a switch on the PixelFormat and returns the number */
    byte bitsPerPixel = GetBitsPerPixel(bData.PixelFormat);

    /*the size of the image in bytes */
    int size = bData.Stride * bData.Height;

    /*Allocate buffer for image*/
    byte[] data = new byte[size];

    /*This overload copies data of /size/ into /data/ from location specified (/Scan0/)*/
    System.Runtime.InteropServices.Marshal.Copy(bData.Scan0, data, 0, size);

    for (int i = 0; i < size; i += bitsPerPixel / 8 )
    {
        double magnitude = 1/3d*(data[i] +data[i + 1] +data[i + 2]);

        //data[i] is the first of 3 bytes of color

    }

    /* This override copies the data back into the location specified */
    System.Runtime.InteropServices.Marshal.Copy(data, 0, bData.Scan0, data.Length);

    b.UnlockBits(bData);

    return b;
}
80
24.10.2018 04:22:02
Thanks, and I didn't even have to goad anybody. :)
MusiGenesis 13.10.2009 22:10:27
You should be aware when doing this, that the order of the color channels may be different depending on what PixelFormat you're using. For example, with 24bit Bitmaps, the first byte of the pixel is the blue channel, followed by green, then red, as opposed to the commonly expected Red-Green-Blue order.
Steve Niles 28.12.2012 22:33:57
@Parhs sure, skipping bData.Height will speed things up, because you're only taking the first row of your data. Try size = 1 that'll make things even faster!
danodonovan 19.02.2015 14:55:55
You should also be aware that this may not get expected byte data because generally bData.Stride does not have to be equal to b.Width * bytePerPix.
wondra 26.06.2015 23:04:07
@Pipe fixed the link
davidtbernal 24.11.2017 05:24:18

You want LockBits. You can then extract the bytes you want from the BitmapData object it gives you.

3
13.10.2009 21:39:04
I think it's faster if you use the BitmapData object returned from LockBits inside an unsafe block with a pointer cast (NOTE: I actually have no idea if this is faster or not, but I'm trying to goad someone else into benchmarking it).
MusiGenesis 13.10.2009 21:46:56
It'd save you the copies into and out of the BitmapData, which is nice. Not sure how much savings that would grant in practice, though; memcpy() is really fast these days.
David Seiler 13.10.2009 21:52:29

Building on @notJim answer (and with help from http://www.bobpowell.net/lockingbits.htm), I developed the following that makes my life a lot easier in that I end up with an array of arrays that allows me to jump to a pixel by it's x and y coordinates. Of course, the x coordinate needs to be corrected for by the number of bytes per pixel, but that is an easy extension.

Dim bitmapData As Imaging.BitmapData = myBitmap.LockBits(New Rectangle(0, 0, myBitmap.Width, myBitmap.Height), Imaging.ImageLockMode.ReadOnly, myBitmap.PixelFormat)

Dim size As Integer = Math.Abs(bitmapData.Stride) * bitmapData.Height
Dim data(size - 1) As Byte

Marshal.Copy(bitmapData.Scan0, data, 0, size)

Dim pixelArray(myBitmap.Height)() As Byte

'we have to load all the opacity pixels into an array for later scanning by column
'the data comes in rows
For y = myBitmap.Height - 1 To 0 Step -1
    Dim rowArray(bitmapData.Stride) As Byte
    Array.Copy(data, y * bitmapData.Stride, rowArray, 0, bitmapData.Stride)
    'For x = myBitmap.Width - 1 To 0 Step -1
    '   Dim i = (y * bitmapData.Stride) + (x * 4)
    '   Dim B = data(i)
    '   Dim G = data(i + 1)
    '   Dim R = data(i + 2)
    '   Dim A = data(i + 3)
    'Next
    pixelArray(y) = rowArray
Next
2
28.02.2013 22:54:45

There is another way that is way faster and much more convenient. If you have a look at the Bitmap constructors you will find one that takes and IntPtr as the last parameter. That IntPtr is for holding pixel data. So how do you use it?

Dim imageWidth As Integer = 1920
Dim imageHeight As Integer = 1080

Dim fmt As PixelFormat = PixelFormat.Format32bppRgb
Dim pixelFormatSize As Integer = Image.GetPixelFormatSize(fmt)

Dim stride As Integer = imageWidth * pixelFormatSize
Dim padding = 32 - (stride Mod 32)
If padding < 32 Then stride += padding

Dim pixels((stride \ 32) * imageHeight) As Integer
Dim handle As GCHandle = GCHandle.Alloc(pixels, GCHandleType.Pinned)
Dim addr As IntPtr = Marshal.UnsafeAddrOfPinnedArrayElement(pixels, 0)

Dim bitmap As New Bitmap(imageWidth, imageHeight, stride \ 8, fmt, addr)

What you have now is a simple Integer array and a Bitmap referencing the same memory. Any changes you make to the Integer array will be directly affecting the Bitmap. Let us try this with a simple brightness transform.

Public Sub Brightness(ByRef pixels() As Integer, ByVal scale As Single)
    Dim r, g, b As Integer
    Dim mult As Integer = CInt(1024.0f * scale)
    Dim pixel As Integer

    For i As Integer = 0 To pixels.Length - 1
        pixel = pixels(i)
        r = pixel And 255
        g = (pixel >> 8) And 255
        b = (pixel >> 16) And 255

        'brightness calculation
        'shift right by 10 <=> divide by 1024
        r = (r * mult) >> 10
        g = (g * mult) >> 10
        b = (b * mult) >> 10

        'clamp to between 0 and 255
        If r < 0 Then r = 0
        If g < 0 Then g = 0
        If b < 0 Then b = 0
        r = (r And 255)
        g = (g And 255)
        b = (b And 255)

        pixels(i) = r Or (g << 8) Or (b << 16) Or &HFF000000
    Next
End Sub

You may notice that I have used a little trick to avoid doing floating point math within the loop. This improves performance quite a bit. And when you are done you need to clean up a little of course...

addr = IntPtr.Zero
If handle.IsAllocated Then
    handle.Free()
    handle = Nothing
End If
bitmap.Dispose()
bitmap = Nothing
pixels = Nothing

I have ignored the alpha component here but you are free to use that as well. I have thrown together a lot of bitmap editing tools this way. It is much faster and more reliable than Bitmap.LockBits() and best of all, it requires zero memory copying to start editing your bitmap.

2
13.05.2014 17:51:48

You can use Bitmap.LockBits method. Also if you want to use parallel task execution, you can use the Parallel class in System.Threading.Tasks namespace. Following links have some samples and explanations.

5
3.09.2014 11:39:49

Try this C# solution.

Create a winforms app for testing.

Add a Button and a PictureBox, and a click event and a form closing event.

Use the following code for your form:

public partial class Form1 : Form
{
    uint[] _Pixels { get; set; }

    Bitmap _Bitmap { get; set; }

    GCHandle _Handle { get; set; }

    IntPtr _Addr { get; set; }


    public Form1()
    {
        InitializeComponent();

        int imageWidth = 100; //1920;

        int imageHeight = 100; // 1080;

        PixelFormat fmt = PixelFormat.Format32bppRgb;

        int pixelFormatSize = Image.GetPixelFormatSize(fmt);

        int stride = imageWidth * pixelFormatSize;

        int padding = 32 - (stride % 32);

        if (padding < 32)
        {
            stride += padding;
        }

        _Pixels = new uint[(stride / 32) * imageHeight + 1];

         _Handle = GCHandle.Alloc(_Pixels, GCHandleType.Pinned);

        _Addr = Marshal.UnsafeAddrOfPinnedArrayElement(_Pixels, 0);

        _Bitmap = new Bitmap(imageWidth, imageHeight, stride / 8, fmt, _Addr);

        pictureBox1.Image = _Bitmap;

    }

    private void button1_Click(object sender, EventArgs e)
    {
        for (int i = 0; i < _Pixels.Length; i++)
        {
            _Pixels[i] = ((uint)(255 | (255 << 8) | (255 << 16) | 0xff000000));

        }

    }

    private void Form1_FormClosing(object sender, FormClosingEventArgs e)
    {
        _Addr = IntPtr.Zero;

        if (_Handle.IsAllocated)
        {
            _Handle.Free();

        }

        _Bitmap.Dispose();

        _Bitmap = null;

        _Pixels = null;

    }

}

Now, any edits you make to the array will automatically update the Bitmap.

You will need to call the refresh method on the picturebox to see these changes.

0
8.09.2016 10:11:36

If you're on .NET Core or .NET Standard I'll suggest to use the new Span<T> for higher efficiency.

Here's a rough implementation

public unsafe class UnmanagedBitmap : IDisposable
{
    private Bitmap bmp;
    private Rectangle rect;
    private int bytesPerPixel;
    private IntPtr bufferPtr;
    private byte* bufferBytes;
    private int size;
    private BitmapData bData;
    private ImageLockMode lockMode;
    private byte* scan0;

    public UnmanagedBitmap(Bitmap bmp, ImageLockMode lockMode)
    {
        this.bmp = bmp;
        this.lockMode = lockMode;

        bytesPerPixel = Image.GetPixelFormatSize(bmp.PixelFormat) / 8;
        size = bmp.Width * bmp.Height * bytesPerPixel;
        bufferPtr = Marshal.AllocHGlobal(size);
        bufferBytes = (byte*)bufferPtr;

        rect = new Rectangle(0, 0, bmp.Width, bmp.Height);
        bData = bmp.LockBits(rect, lockMode, bmp.PixelFormat);
        scan0 = (byte*)bData.Scan0.ToPointer();
    }

    public void Dispose()
    {
        bmp.UnlockBits(bData);
        Marshal.FreeHGlobal(bufferPtr);
    }

    public Span<byte> GetPixel(int x, int y)
    {
        var pixel = scan0 + y * bData.Stride * x * bytesPerPixel;
        return new Span<byte>(pixel, bytesPerPixel);
    }
}

You can also modify the code to return a byte array instead if you're on .NET framework

2
9.01.2020 06:55:59