47

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.

CodyF
  • 4,977
  • 3
  • 26
  • 38
AndreyAkinshin
  • 18,603
  • 29
  • 96
  • 155
  • 1
    Following link contains bitmap pixel access methods comparatively. http://csharpexamples.com/fast-image-processing-c/ – turgay Sep 03 '14 at 11:34
  • The answers here recommend using `LockBits`, which needs different access for all possible pixel formats. [Here](https://github.com/koszeggy/KGySoft.Drawing#fast-bitmap-manipulation) is a library (disclaimer: written by me), which provides fast, unified managed access for all possible formats, including indexed ones. – György Kőszeg Dec 14 '21 at 10:47

7 Answers7

93

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;
}
user276648
  • 6,018
  • 6
  • 60
  • 86
davidtbernal
  • 13,434
  • 9
  • 44
  • 60
  • 2
    Thanks, and I didn't even have to goad anybody. :) – MusiGenesis Oct 13 '09 at 22:10
  • 13
    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 Dec 28 '12 at 22:33
  • Thanks for you effort! – freakinpenguin Jul 01 '14 at 08:22
  • bData.Height seems a bit expensive. I got some performance boost skipping it – GorillaApe Sep 30 '14 at 11:50
  • 2
    @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 Feb 19 '15 at 14:55
  • 4
    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 Jun 26 '15 at 23:04
  • whats the purpose of using the variable magnitude? – Md Sifatul Islam Feb 15 '17 at 08:50
  • For >=8bpp images I usually get around the stride difference by copying per line, increasing the source offset by the stride with each line but copying to a new array with the actual width (well, `width * BPP / 8` of course) * height as length. For images with <8bpp though, stuff gets... interesting. A good rule is to always include the stride with your image processing functions, preferably as ref parameter in case it gets changed for your output. – Nyerguds Apr 24 '17 at 07:27
  • Link is ded, can anyone summarize what it said? – Felipe Nov 08 '17 at 19:00
  • 1
    @Pipe fixed the link – davidtbernal Nov 24 '17 at 05:24
  • 3
    What is `GetBitsPerPixel()` here? – John Alexiou Jan 30 '18 at 01:06
  • @ja72 `System.Drawing.Bitmap.GetPixelFormatSize(...);` – rotgers Mar 20 '18 at 11:25
  • @Jargon Isn´t the order of the color channels depending on the endianness rather than the pixel format? – royalTS Sep 07 '18 at 09:19
  • @davidtbernal in your second example using marshalling, is there any way to get the x/y of current pixel (based on `i` I think?) – Dennis van Gils Apr 10 '20 at 12:51
7

If you're on C# 8.0 I'll suggest to use the new Span<T> for higher efficiency.

Here's a rough implementation

public unsafe class FastBitmap : IDisposable
{
    private Bitmap _bmp;
    private ImageLockMode _lockmode;
    private int _pixelLength;

    private Rectangle _rect;
    private BitmapData _data;
    private byte* _bufferPtr;

    public int Width { get => _bmp.Width; }
    public int Height { get => _bmp.Height; }
    public PixelFormat PixelFormat { get => _bmp.PixelFormat; }

    public FastBitmap(Bitmap bmp, ImageLockMode lockMode)
    {
        _bmp = bmp;
        _lockmode = lockMode;

        _pixelLength = Image.GetPixelFormatSize(bmp.PixelFormat) / 8;
        _rect = new Rectangle(0, 0, Width, Height);
        _data = bmp.LockBits(_rect, lockMode, PixelFormat);
        _bufferPtr = (byte*)_data.Scan0.ToPointer();
    }

    public Span<byte> this[int x, int y]
    {
        get
        {
            var pixel = _bufferPtr + y * _data.Stride + x * _pixelLength;
            return new Span<byte>(pixel, _pixelLength);
        }
        set
        {
            value.CopyTo(this[x, y]);
        }
    }

    public void Dispose()
    {
        _bmp.UnlockBits(_data);
    }
}
Shaamil Ahmed
  • 378
  • 2
  • 7
  • how would i go about using this if i wanted to write to the bitmap ? – jklw10 May 19 '20 at 07:54
  • I've updated the code and should work like a charm. Now you access the pixels by saying `new FastBitmap(.....)[100, 200]` where 100 is x and 200 is y. You can set the value of the pixel by writing `new FastBitmap(......)[100, 200] = new Span(array);` – Shaamil Ahmed May 19 '20 at 12:48
  • how would you go about converting this back into a bitmap? could i just make _data public? – jklw10 May 20 '20 at 22:45
  • https://gyazo.com/d5ab06cf35a3bb84a56d8330fa25517b i made my own cast but encountered this error when trying `fastbmp[x,y] = new Span(BitConverter.GetBytes(color.ToArgb()));` – jklw10 May 20 '20 at 23:23
  • 4
    isn't `var pixel = _bufferPtr + y * _data.Stride * x * _pixelLength;` supposed to be `var pixel = _bufferPtr + y * _data.Stride` + `x * _pixelLength` – jklw10 May 20 '20 at 23:32
6

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.

turgay
  • 438
  • 4
  • 7
4

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

Andrew Morton
  • 24,203
  • 9
  • 60
  • 84
David Seiler
  • 9,557
  • 2
  • 31
  • 39
  • 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 Oct 13 '09 at 21:46
  • 1
    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 Oct 13 '09 at 21:52
1

Building on @notJim answer (and with help from https://web.archive.org/web/20141229164101/http://bobpowell.net/lockingbits.aspx), 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 its 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
Andrew Morton
  • 24,203
  • 9
  • 60
  • 84
cjbarth
  • 4,189
  • 6
  • 43
  • 62
1

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.

Daniklad
  • 945
  • 8
  • 10
0

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.

WonderWorker
  • 8,539
  • 4
  • 63
  • 74