2

EDIT: I deeply appreciate the replies. What I need more than anything here is sample code for what I do with the few lines of code in the nested loop, since that's what works right in GetPixel/SetPixel, but also what I can't get to work right using Lockbits. Thank you

I'm trying to convert my image processing filters from GetPixel / SetPixel to Lockbits, to improve processing time. I have seen Lockbits tutorials here on Stack Overflow, MSDN, and other sites as well, but I'm doing something wrong. I'm starting with an exceedingly simple filter, which simply reduces green to create a red and purple effect. Here's my code:

   private void redsAndPurplesToolStripMenuItem_Click(object sender, EventArgs e)
    {
        // Get bitmap from picturebox
        Bitmap bmpMain = (Bitmap)pictureBoxMain.Image.Clone();

        // search through each pixel via x, y coordinates, examine and make changes. Dont let values exceed 255 or fall under 0.  
        for (int y = 0; y < bmpMain.Height; y++)
            for (int x = 0; x < bmpMain.Width; x++)
            {
                bmpMain.GetPixel(x, y);
                Color c = bmpMain.GetPixel(x, y);
                int myRed = c.R, myGreen = c.G, myBlue = c.B;
                myGreen -= 128;
                if (myGreen < 0) myGreen = 0; 
                bmpMain.SetPixel(x, y, Color.FromArgb(255, myRed, myGreen, myBlue));
            }

        // assign the new bitmap to the picturebox
        pictureBoxMain.Image = (Bitmap)bmpMain;

        // Save a copy to the HD for undo / redo.
        string myString = Environment.GetEnvironmentVariable("temp", EnvironmentVariableTarget.Machine);
        pictureBoxMain.Image.Save(myString + "\\ColorAppRedo.png", System.Drawing.Imaging.ImageFormat.Png);
    }

So that GetPixel / SetPixel code works fine, but it's slow. So I tried this:

    private void redsAndPurplesToolStripMenuItem_Click(object sender, EventArgs e)
    {
        // Get bitmap from picturebox
        Bitmap bmpMain = (Bitmap)pictureBoxMain.Image.Clone();

        Rectangle rect = new Rectangle(Point.Empty, bmpMain.Size); 
        BitmapData bmpData = bmpMain.LockBits(rect, ImageLockMode.ReadOnly, bmpMain.PixelFormat); 

        // search through each pixel via x, y coordinates, examine and make changes. Dont let values exceed 255 or fall under 0.  
        for (int y = 0; y < bmpMain.Height; y++)
            for (int x = 0; x < bmpMain.Width; x++)
            {
                bmpMain.GetPixel(x, y);
                Color c = new Color(); 
                int myRed = c.R, myGreen = c.G, myBlue = c.B;
                myGreen -= 128;
                if (myGreen < 0) myGreen = 0; 
                bmpMain.SetPixel(x, y, Color.FromArgb(255, myRed, myGreen, myBlue));

            }

        bmpMain.UnlockBits(bmpData); 

        // assign the new bitmap to the picturebox
        pictureBoxMain.Image = (Bitmap)bmpMain;

        // Save a copy to the HD for undo / redo.
        string myString = Environment.GetEnvironmentVariable("temp", EnvironmentVariableTarget.Machine);
        pictureBoxMain.Image.Save(myString + "\\ColorAppRedo.png", System.Drawing.Imaging.ImageFormat.Png);
    } 

Which throws the error "An unhandled exception of type 'System.InvalidOperationException' occurred in System.Drawing.dll Additional information: Bitmap region is already locked" when it reaches the first line of the nested loop.

I realize this has to be a beginner's error, I'd appreciate if someone could demonstrate the correct way to convert this very simple filter to Lockbits. Thank you very much

MaoTseTongue
  • 71
  • 2
  • 8
  • Have you seen sample in `Bitmap.LockBits`? It is very unclear why you trying to use `GetPixel`/`SetPixel` inside `LockBits` section... – Alexei Levenkov May 30 '15 at 05:19
  • Apparently that's incorrect, thank you. The problem is, I am not understanding the correct way to _get_ the pixel colors, _make the changes (reduce green)_, and _set_ the new pixel colors. I'd deeply appreciate if someone could demonstrate what I need to do for these few lines of code in the nested loop, since I'm not understanding it. Many Thanks – MaoTseTongue May 30 '15 at 06:51
  • It's 3:00 am Eastern Time, I'll be away for the rest of the morning, so I won't be available for further comment replies until tomorrow. Thank you for understanding. – MaoTseTongue May 30 '15 at 07:00
  • Have a look at [this post](http://stackoverflow.com/questions/24411114/c-sharp-copy-bitmaps-pixels-in-the-alpha-channel-on-another-bitmap/24411925?s=1|0.9801#24411925) which shows you a routine in both version, with `GetPixel/SetPixel` and with `Lockbits` so you can see exactly what changes you need to make. One is to be sure about the pixel format because lockbits accesses not pixels but bytes and therefore the indexing must know if there are 3 or 4 bytes to a pixel..! – TaW May 30 '15 at 08:06

2 Answers2

5

The array returned by scan0 is in this format BGRA BGRA BGRA BGRA ... and so on, where B = Blue, G = Green, R = Red, A = Alpha.

Example of a very small bitmap 4 pixels wide and 3 pixels height.

BGRA BGRA BGRA BGRA
BGRA BGRA BGRA BGRA
BGRA BGRA BGRA BGRA 

stride = width*bytesPerPixel = 4*4 = 16 bytes
height = 3
maxLenght = stride*height= 16*3 = 48 bytes

To reach a certain pixel in the image (x, y) use this formula

int certainPixel = bytesPerPixel*x + stride * y;
B = scan0[certainPixel + 0];
G = scan0[certainPixel + 1];
R = scan0[certainPixel + 2];
A = scan0[certainPixel + 3];

    public unsafe void Test(Bitmap bmp)
    {
        int width = bmp.Width;
        int height = bmp.Height;
        //TODO determine bytes per pixel
        int bytesPerPixel = 4; // we assume that image is Format32bppArgb
        int maxPointerLenght = width * height * bytesPerPixel;
        int stride = width * bytesPerPixel;
        byte R, G, B, A;

        BitmapData bData = bmp.LockBits(
            new System.Drawing.Rectangle(0, 0, bmp.Width, bmp.Height),
            ImageLockMode.ReadWrite, bmp.PixelFormat);


        byte* scan0 = (byte*)bData.Scan0.ToPointer();

        for (int i = 0; i < maxPointerLenght; i += 4)
        {
            B = scan0[i + 0];
            G = scan0[i + 1];
            R = scan0[i + 2];
            A = scan0[i + 3];

            // do anything with the colors
            // Set the green component to 0
            G = 0;
            // do something with red
            R = R < 54 ? (byte)(R + 127) : R;
            R = R > 255 ? 255 : R;
        }


        bmp.UnlockBits(bData);
    }

You can test is yourself. Create a very small bitmap ( few pixels wide/height) in paint or any other program and put a breakpoint at the begining of the method.

iulian3000
  • 1,270
  • 13
  • 26
  • Thank you. If I do `// do anything with the colors` `G -= 128;` `if (G < 0) G = 0; ` To reduce the green in the image, am I going to get the problem with bytes versus integers that I'm getting with similar code, and if so, how do I correct that problem? Thanks again for the detailed explanation. – MaoTseTongue Jun 02 '15 at 04:17
  • Cast the result to byte. For example G = (byte)(G + 2); – iulian3000 Jun 02 '15 at 06:26
4

Additional information: Bitmap region is already locked"

You now know why GetPixel() is slow, it also uses Un/LockBits under the hood. But does so for each individual pixel, the overhead steals cpu cycles. A bitmap can be locked only once, that's why you got the exception. Also the basic reason that you can't access a bitmap in multiple threads simultaneously.

The point of LockBits is that you can access the memory occupied by the bitmap pixels directly. The BitmapData.Scan0 member gives you the memory address. Directly addressing the memory is very fast. You'll however have to work with an IntPtr, the type of Scan0, that requires using a pointer or Marshal.Copy(). Using a pointer is the optimal way, there are many existing examples on how to do this, I won't repeat it here.

 ... = bmpMain.LockBits(rect, ImageLockMode.ReadOnly, bmpMain.PixelFormat); 

The last argument you pass is very, very important. It selects the pixel format of the data and that affects the code you write. Using bmpMain.PixelFormat is the fastest way to lock but it is also very inconvenient. Since that now requires you to adapt your code to the specific pixel format. There are many, take a good look at the PixelFormat enum. They differ in the number of bytes taken for each pixel and how the colors are encoded in the bits.

The only convenient pixel format is Format32bppArgb, every pixel takes 4 bytes, the color/alpha is encoded in a single byte and you can very easily and quickly address the pixels with an uint*. You can still deal with Format24bppRgb but you now need a byte*, that's a lot slower. The ones that have a P in the name are pre-multiplied formats, very fast to display but exceedingly awkward to deal with. You may thus be well ahead by taking the perf hit of forcing LockBits() to convert the pixel format. Paying attention to the pixel format up front is important to avoid this kind of lossage.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • Interesting question how one finds out that "GetPixel...uses Un/LockBits"? It is not mentioned in documentation for managed [Bitmap.GetPixel](https://msdn.microsoft.com/en-us/library/system.drawing.bitmap.getpixel%28v=vs.110%29.aspx), nor in [source](http://referencesource.microsoft.com/#System.Drawing/commonui/System/Drawing/Bitmap.cs,e1ba6ce9c451889d) nor in doc for native implementation [Bitmap functions - GdipBitmapGetPixel](https://msdn.microsoft.com/en-us/library/ms533971%28v=vs.85%29.aspx)... – Alexei Levenkov May 30 '15 at 18:58
  • The source code for gdiplus.dll is not available. Its behavior can be reverse-engineered. – Hans Passant May 30 '15 at 19:06
  • I was hoping you know some more novice-friendly way... :( Side note: while I like your answer and indeed upvoted it you may want to consider adding link + shortened sample from [LockBits](https://msdn.microsoft.com/en-us/library/5ey6h79d%28v=vs.110%29.aspx) as OP also asked for sample. – Alexei Levenkov May 30 '15 at 19:12
  • Yes, well said. I certainly appreciate the comments and answers, but what I could use more than anything is sample code: what do I do with those few lines of code in the nested loop to make them lockbit compliant. Thank you. – MaoTseTongue May 30 '15 at 22:18
  • If you want copy/pasta code then that's not hard to find, you'll get lots and lots of it by googling "c# lockbits". There is another answer to this question that addresses it as well. Might work, but odds are so-so, this post tells you what is wrong with it and what to do about it. If you don't understand it then you didn't do a fantastic job explaining the hangup in your comment. – Hans Passant May 30 '15 at 23:31
  • Well, the hangup is pretty simple: I don't see how to get the pixels, change the green channel value, and set the new pixels. - Thanks – MaoTseTongue May 31 '15 at 06:56