C# - Faster Alternatives to SetPixel and GetPixel for Bitmaps for Windows Forms App
Asked Answered
O

5

63

I am trying to teach myself C# and have heard from a variety of sources that the functions get and setpixel can be horribly slow. What are some of the alternatives and is the performance improvement really that significant?

A chunk of my code for reference:

public static Bitmap Paint(Bitmap _b, Color f)
{
  Bitmap b = new Bitmap(_b);
  for (int x = 0; x < b.Width; x++) 
  {
    for (int y = 0; y < b.Height; y++) 
    {
      Color c = b.GetPixel(x, y);
      b.SetPixel(x, y, Color.FromArgb(c.A, f.R, f.G, f.B));
    }
  }
  return b;
}
Orchestrate answered 11/7, 2014 at 15:49 Comment(1)
All of the answers here support a specific pixel format only. If both fast and simple solution is required you can use this library (disclaimer: written by me).Endblown
R
138

The immediately usable code

public class DirectBitmap : IDisposable
{
    public Bitmap Bitmap { get; private set; }
    public Int32[] Bits { get; private set; }
    public bool Disposed { get; private set; }
    public int Height { get; private set; }
    public int Width { get; private set; }

    protected GCHandle BitsHandle { get; private set; }

    public DirectBitmap(int width, int height)
    {
        Width = width;
        Height = height;
        Bits = new Int32[width * height];
        BitsHandle = GCHandle.Alloc(Bits, GCHandleType.Pinned);
        Bitmap = new Bitmap(width, height, width * 4, PixelFormat.Format32bppPArgb, BitsHandle.AddrOfPinnedObject());
    }

    public void SetPixel(int x, int y, Color colour)
    {
        int index = x + (y * Width);
        int col = colour.ToArgb();

        Bits[index] = col;
    }

    public Color GetPixel(int x, int y)
    {
        int index = x + (y * Width);
        int col = Bits[index];
        Color result = Color.FromArgb(col);

        return result;
    }

    public void Dispose()
    {
        if (Disposed) return;
        Disposed = true;
        Bitmap.Dispose();
        BitsHandle.Free();
    }
}

There's no need for LockBits or SetPixel. Use the above class for direct access to bitmap data.

With this class, it is possible to set raw bitmap data as 32-bit data. Notice that it is PARGB, which is premultiplied alpha. See Alpha Compositing on Wikipedia for more information on how this works and examples on the MSDN article for BLENDFUNCTION to find out how to calculate the alpha properly.

If premultiplication might overcomplicate things, use PixelFormat.Format32bppArgb instead. A performance hit occurs when it's drawn, because it's internally being converted to PixelFormat.Format32bppPArgb. If the image doesn't have to change prior to being drawn, the work can be done before premultiplication, drawn to a PixelFormat.Format32bppArgb buffer, and further used from there.

Access to standard Bitmap members is exposed via the Bitmap property. Bitmap data is directly accessed using the Bits property.

Using byte instead of int for raw pixel data

Change both instances of Int32 to byte, and then change this line:

Bits = new Int32[width * height];

To this:

Bits = new byte[width * height * 4];

When bytes are used, the format is Alpha/Red/Green/Blue in that order. Each pixel takes 4 bytes of data, one for each channel. The GetPixel and SetPixel functions will need to be reworked accordingly or removed.

Benefits to using the above class

  • Memory allocation for merely manipulating the data is unnecessary; changes made to the raw data are immediately applied to the bitmap.
  • There are no additional objects to manage. This implements IDisposable just like Bitmap.
  • It does not require an unsafe block.

Considerations

  • Pinned memory cannot be moved. It's a required side effect in order for this kind of memory access to work. This reduces the efficiency of the garbage collector (MSDN Article). Do it only with bitmaps where performance is required, and be sure to Dispose them when you're done so the memory can be unpinned.

Access via the Graphics object

Because the Bitmap property is actually a .NET Bitmap object, it's straightforward to perform operations using the Graphics class.

var dbm = new DirectBitmap(200, 200);
using (var g = Graphics.FromImage(dbm.Bitmap))
{
    g.DrawRectangle(Pens.Black, new Rectangle(50, 50, 100, 100));
}

Performance comparison

The question asks about performance, so here's a table that should show the relative performance between the three different methods proposed in the answers. This was done using a .NET Standard 2 based application and NUnit.

* Time to fill the entire bitmap with red pixels *
- Not including the time to create and dispose the bitmap
- Best out of 100 runs taken
- Lower is better
- Time is measured in Stopwatch ticks to emphasize magnitude rather than actual time elapsed
- Tests were performed on an Intel Core i7-4790 based workstation

              Bitmap size
Method        4x4   16x16   64x64   256x256   1024x1024   4096x4096
DirectBitmap  <1    2       28      668       8219        178639
LockBits      2     3       33      670       9612        197115
SetPixel      45    371     5920    97477     1563171     25811013

* Test details *

- LockBits test: Bitmap.LockBits is only called once and the benchmark
                 includes Bitmap.UnlockBits. It is expected that this
                 is the absolute best case, adding more lock/unlock calls
                 will increase the time required to complete the operation.
Ruffianism answered 14/1, 2016 at 22:47 Comment(21)
This is brilliant, thank You. It is the same speed or even a bit faster as with LockBits and pointers without the need to unsafe code. I wonder why this is not provided as preferred approach.Scutellation
It is probably not provided as a default because it is an unmanaged object (rather, the underlying data is unmanaged) and runs counter to the philosophy of the framework. But this version is certainly more useful for frequent image manipulations.Aleron
How do you make an instance of DirectBitmap with the original bitmap? I noticed the byte array is coming from GCHandle.Alloc (which I'm kind of new to).Taille
DirectBitmap needs to be created from the ground up. If you need to create one from an existing Bitmap, you need to create a DirectBitmap with the same dimensions, and use a Graphics object to copy it over.Ruffianism
@SaxxonPike Can you explain how you copy over from Graphics to DirectBitmap? If possible, a use-case sample for DirectBitmap would be great.Charbonnier
@Charbonnier You can do Graphics.FromImage(directBitmap.Bitmap) if you need a Graphics object for the DirectBitmap.Ruffianism
@SaxxonPike So, where does the "original" bitmap come into play? That just creates a Graphics object from DirectBitmap. You said we need Graphics to copy original into DirectBitmap. With that being said, say we have a bitmap image on disk. Normally, I would load it up with a FileStream, copy it over to the MemoryStream and if needed, convert to Bitmap. How do we work with your DirectBitmap? How do we get image data into DirectBitmap?Charbonnier
@SaxxonPike Hmm, I think I misunderstood that intent of DirectBitmap in the first place (I haven't done much image processing in the past, beyond basic stuff in WPF). It's just a substitute for BitmapData, so that we don't have to use locking, unlocking, right?Charbonnier
@Charbonnier Correct. Normally, you would be required to lock and unlock bits, which actually creates a copy and is slow for frequent operations. Bitmap is a sealed class. Therefore, creating this kind of wrapper is the only fast way to have direct access to the bits while still being able to use any other functionality that requires the Bitmap class.Ruffianism
@SaxxonPike Sorry to ask, but how exactly would I use this class to do someling like 'img.SetPixel(x,y,Color.Transparent);' ? I know how to create the class and adapt the constructor to my needs, but I don't seem to figure out how to use the bits to change the color of a pixel. Again, sorry for asking, I never worked on bits or bytes.Revolting
@Dark: A little bit of math is involved. To find out where in the array you need to change, use index = x + (y * width) and use Color.Transparent.ToArgb() to get the Int32 value of the color.Ruffianism
@SaxxonPike that is exactly what I have tried before, maybe I'm not using the Bits as I should. I'm going to simplify the goal and give you the code I'm using to make the change. for(int x=init_x; x<=final_x; x++) for(int y=init_y; y<=final_y; y++) directBmp.Bits[ x + y * directBmp.Bitmap.Width ] = Color.Transparent.ToArbg(); pictureBox.Image=directBmp.Bitmap; First of all I tried updating the picture box after every x and pause to see the effect but nothing happens. Secondly, at some point it crashed with an ArgumentException in System.Drawing.dllRevolting
I suggest modifying this code to call Dispose() in the ~DirectBitmap() finalize method, or providing an example usage which creates the DirectBitmap in a using (DirectBitmap bmp = new DirectBitmap()) { ... } block.Remembrancer
@ThomasWeller I have added a performance comparison for multiple bitmap sizes for each of the three methods. I hope this answers your question adequately. Let me know if you'd like me to toss the test code up online or something.Ruffianism
@SaxxonPike sorry to ask, but how to call this class to compare 2 images? something like using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read)) { using (DirectBitmap img = DirectBitmap.FromStream(fs, false, false)) { //some code } }Row
@Row What I do in this case is use Bitmap.FromStream() to load the bitmap from disk, then create a DirectBitmap of the same size, and use Graphics.DrawImage() from the Bitmap to the DirectBitmap. Now, you have direct access to the pixels from an existing Bitmap.Ruffianism
Thanks alot. This immediately reduced the time to set all pixels on my 1000*10000 bitmap from 5s to 300ms. Good enough for me.Gilcrest
Excellent work! I've found immediate use in an image converter for my AI work. Huge performance boost. Thank you.Cercaria
Is there a specific reason to use int to store bits instead of uint ?Investiture
@Investiture I don't see a reason why uint wouldn't work. Pinning an array of any type should work fine. I've chosen to use int here because many arithmetic operations I would do accept and natively return int anyway.Ruffianism
Excellent. Thank you. Here are the usings ... using System; using System.Drawing; using System.Drawing.Imaging; using System.Runtime.InteropServices;Entryway
O
20

The reason bitmap operations are so slow in C# is due to locking and unlocking. Every operation will perform a lock on the required bits, manipulate the bits, and then unlock the bits.

You can vastly improve the speed by handling the operations yourself. See the following example.

using (var tile = new Bitmap(tilePart.Width, tilePart.Height))
{
  try
  {
      BitmapData srcData = sourceImage.LockBits(tilePart, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
      BitmapData dstData = tile.LockBits(new Rectangle(0, 0, tile.Width, tile.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);

      unsafe
      {
          byte* dstPointer = (byte*)dstData.Scan0;
          byte* srcPointer = (byte*)srcData.Scan0;

          for (int i = 0; i < tilePart.Height; i++)
          {
              for (int j = 0; j < tilePart.Width; j++)
              {
                  dstPointer[0] = srcPointer[0]; // Blue
                  dstPointer[1] = srcPointer[1]; // Green
                  dstPointer[2] = srcPointer[2]; // Red
                  dstPointer[3] = srcPointer[3]; // Alpha

                  srcPointer += BytesPerPixel;
                  dstPointer += BytesPerPixel;
              }
              srcPointer += srcStrideOffset + srcTileOffset;
              dstPointer += dstStrideOffset;
          }
      }

      tile.UnlockBits(dstData);
      aSourceImage.UnlockBits(srcData);

      tile.Save(path);
  }
  catch (InvalidOperationException e)
  {

  }
}
Osteen answered 11/7, 2014 at 16:17 Comment(9)
buy why is it locking and unlocking them why not treat them just as an normal array? I mean is not that they are on the screen or something.Houseroom
My guess is that it wouldn't be efficient to hold the entire bitmap in memory normally. 1024*1024*4 = 4,194,304 bytes = 4 megabytes.Osteen
Sorry, didn't expect that to send when pressing enter. The bits are in memory at all times. The problem is the overhead of the function call and looking up the pixel format and location of the desired pixel. With a loop based on LockBits, you only have to do that once, instead of once per pixel. The performance improvement depends on your use case (including the image size), but be advised GDI+ performs poorly in general and is not suitable for real-time applications.Jonquil
The answers here are wrong. Why locking? Because .NET uses a Garbage Collector that asynchronously frees unused memory. After freeing a block of memory it moves the remaining memory to other locations to get longer consistent blocks of free memory. If the garbage collector would move your bitmap to another location just in the moment while you are reading the pixels you would read nonsense values. So .NET forces you to lock the bitmap which prohibits the garbage collector to move it. The bitmap data stays in memory at the same location until you unlock it.Barbur
But there is still a little performance issue in this code: .NET allocates a new temporary memory block (slow) when calling LockBits, the bitmap data is copied there, you modify the pixels in the temp buffer and when unlocking this data is copied back to the bitmap. You do NOT work directly on the bitmap's data. So if you want to have the best performance you have to do these operations in C++.Barbur
The two strides can be taken directly from the BitmapData objects, by the way. This code offers no insight into where the stride comes from. Nor is it clear what srcTileOffset is, for that matter.Cyr
tilePart.Width and tilePart.Weight are extremely slow. Consider putting their result in a separate width/height variable. In my case this boosted performance 40x on 2048x2048 images.Pesthole
RE: "My guess is that it wouldn't be efficient to hold the entire bitmap in memory normally. 1024*1024*4 = 4,194,304 bytes = 4 megabytes. " => Message from the future: 4 megabytes is no longer lots of memory :)Odontalgia
@Pesthole thank you so much, spent hours on this. What you said is extremely important and nowhere to be found. So I'm gonna write it again: .Width and .Height must be stored in two int before using them thousands of times in the nested for loops.Bogle
R
9

It's been some time, but I found an example that might be useful.

var btm = new Bitmap("image.png");

BitmapData btmDt = btm.LockBits(
    new Rectangle(0, 0, btm.Width, btm.Height),
    ImageLockMode.ReadWrite,
    btm.PixelFormat
);
IntPtr pointer = btmDt.Scan0;
int size = Math.Abs(btmDt.Stride) * btm.Height;
byte[] pixels = new byte[size];
Marshal.Copy(pointer, pixels, 0, size);
for (int b = 0; b < pixels.Length; b++)
{
    pixels[b] = 255; //Do something here 
}

Marshal.Copy(pixels, 0, pointer, size);
btm.UnlockBits(btmDt);
Reserved answered 5/4, 2018 at 5:15 Comment(0)
Y
1

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.

Yacketyyak answered 3/9, 2014 at 14:12 Comment(0)
V
1

This code should be parallelized, there is a massive performance gain being missed by running this synchronously. Almost no modern Microchip will have less than 4 threads available and some chips will have 40 threads available.

There is absolutely no reason to run that first loop synchronously. You can go through either the width or the length using many, many threads.

        private void TakeApart_Fast(Bitmap processedBitmap)
        {
            
            BitmapData bitmapData = processedBitmap.LockBits(new Rectangle(0, 0, processedBitmap.Width, processedBitmap.Height), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
            ConcurrentBag<byte> points = new ConcurrentBag<byte>();   
            unsafe
            {
                int bytesPerPixel = System.Drawing.Bitmap.GetPixelFormatSize(processedBitmap.PixelFormat) / 8;
                int heightInPixels = bitmapData.Height;
                int widthInBytes = bitmapData.Width * bytesPerPixel;
                _RedMin = byte.MaxValue;
                _RedMax = byte.MinValue;
                byte* PtrFirstPixel = (byte*)bitmapData.Scan0;
              
                Parallel.For(0, heightInPixels, y =>
                {
                    byte* currentLine = PtrFirstPixel + (y * bitmapData.Stride);
                    for (int x = 0; x < widthInBytes; x = x + bytesPerPixel)
                    {
                        // red
                        byte redPixel = currentLine[x + 2];
                        //save information with the concurrentbag
    
                    }
                });
                processedBitmap.UnlockBits(bitmapData);
            }
        }`
  

a benchmark wouldn't mean much because the answer to how much this will speed up the proccess depends 100% on what hardware you are using, and what else is running in the background, it all depends on how many free threads are available. If your running this on a 4000 series graphics card with thousands of streaming proccessors you may be able to do iterate through every column of the image at the same time.

if your running it with and old quad core you may only have 5 or 6 threads which is still incredibly significant.

Veil answered 5/11, 2022 at 20:26 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.