다음을 통해 공유


.Net: Bitmap.Lockbits De-Mystified

Many people may have noticed in the past how intimidating using Bitmap.Lockbits has been due to a lack of clarification/documentation on its usage. This article attempts to do an in depth review of its usage. It does not focus on alternate methods to lockbits.

I remember sometime back, the very first thing ever that turned me onto Bitmap.LockBits was when I went to draw a single pixel using the graphics object, that was when I saw that using SetPixel (or even drawing 1x1px rectangles with the graphics object) was extremely costly in terms of CPU usage, and performance was poor. I remember sometime back reading Bob Powell's article on LockBits, which seemed to me the only in-depth coverage of this I could find anywhere. I however was still left with a little confusion, and later I was able to iron out the details of that confusion. Many thanks to Bob for that original article. The examples in this article will start simple, and grow in complexity as an understanding is established, it is recommended you start from the beginning. This article can be applicable to any .Net language, although this sample will be shown in Visual Basic.Net.

Example 1(SetPixel)

This example will only change a single pixel of a bitmap.

Create a Public Shared Function named SetPixel with a return type of Bitmap and the following ByRef input parameters:

  • ByRef OriginalBitmap As Bitmap - We will be cloning this, changing the specified pixel's color within the clone, then returning the modified clone.
  • ByRef PixelLocation As Point - This is the location within the OriginalBitmap that we will be drawing the pixel to.
  • ByRef PixelColor As Color - This is the color that we will set the pixel to.

You should now have something that looks like this:

 
1.Public  Shared Function  SetPixel(ByRef  OriginalBitmap As  Bitmap, _
2.                     ByRef PixelLocation As Point, _
3.                     ByRef PixelColor As Color) As Bitmap
4. 
5.End Function

​Add the following Constants to your function:

  • ***Const px As System.Drawing.GraphicsUnit = GraphicsUnit.Pixel        ***
  • Const fmtArgb As Imaging.PixelFormat = Imaging.PixelFormat.Format32bppArgb
    • The reason we have added these constants is to make the code more wiki readable(Later you can use them directly) 
    • These constants will be used for the Bitmap.Clone function.

Add the following lines of code to your function:

  • Dim boundsF As RectangleF = OriginalBitmap.GetBounds(px)
    • The Bitmap.GetBounds Function returns a rectangle of the type 'RectangleF', which uses floats for all of the measurements
  • Dim bounds As New Rectangle
    • We will need a rectangle that uses integers for its measurements, so later we will convert 'boundsF' from a RectangleF to a Rectangle, and the result of that conversion will be stored in 'bounds'.
  • bounds.Location = New Point(CInt(boundsF.X), CInt(boundsF.Y))
    • We use CInt to convert all of the constituents of the location from Single's to Integer's, and set the location point of 'bounds'.
    • bounds.Size = New Size(CInt(boundsF.Width), CInt(boundsF.Height))
      • We use CInt to convert all of the constituents of the Size from Single's to Integer's, and set the location point of 'bounds'.
    • Dim bmClone As Bitmap = OriginalBitmap.Clone(bounds, fmtArgb)
      • Now we use the Bitmap.Clone function to create a copy of the original bitmap(so we do not modify the original!)
      • We now have cloned our original bitmap!
    • Dim bmData As System.Drawing.Imaging.BitmapData = bmClone.LockBits(bounds, Imaging.ImageLockMode.ReadWrite, bmClone.PixelFormat)

      • Here we use the rectangle(bounds) that we have made, set the ImageLockMode to ReadWrite, and use the clone's pixel format when calling the Bitmap.LockBits function. Lockbits Locks the bitmap from being modified until Bitmap.UnlockBits is called.
      • The Bitmap.Lockbits function returns an Object of the DataType 'BitmapData', we capture the result of Bitmap.LockBits to an object called 'bmData'.
    •  Dim offsetToFirstPixel As IntPtr = bmData.Scan0

      • The BitmapData.Scan0 property is a pointer that points to the location in unmanaged memory of the first byte of the first pixel(Top left corner) of the bitmap.
      • We put this in to a variable to elaborate on the importance of knowing what "Scan0" is.
    • Dim byteCount As Integer = Math.Abs(bmData.Stride) * bmClone.Height

      • byteCount will hold the total quantity of bytes that the image consumes.
      • BitmapData.Stride returns a value that indicates the total quantity of bytes per horizontal line of pixels.
      • Multiplyings the height of the bitmap by the total quantity of bytes per horizontal line of pixels will tell you the total quantity of bytes in the picture.
    • Dim bitmapBytes(byteCount - 1) As Byte

      • Declare an empty 1 dimensional array of bytes big enough to hold the exact quantity of bytes that the images pixel information consumes.
    • System.Runtime.InteropServices.Marshal.Copy(offsetToFirstPixel, bitmapBytes, 0, byteCount)

      • Use Marshal to copy(8/16) from unmanaged memory.
      • the bitmapBytes array is now populated with image data, so we can finally begin editing the image.
    • Dim alpha As Byte = PixelColor.A


    • Dim red As Byte = PixelColor.R

    • Dim green As Byte = PixelColor.G

    • Dim blue As Byte = PixelColor.B

      • We now split that PixelColor parameter into its constituent parts(alpha, red, green, blue)
    • Dim baseOffset As Integer = bmData.Stride * PixelLocation.Y


    • this calculates the offset to the first byte of data for the first pixel in the line that the PixelLocation.Y coordinate corresponds to.

    • Dim addOffset As Integer = PixelLocation.X * 4


    • this calculates the quantity of bytes after in that corresponds to the PixelLocation.X coordinate

      • Please note that we are multiplying by 4 because the ARGB32bpp format stands for the following:
        • A = alpha
        • R = red
        • G = green
        • B = blue
        • 32bpp = 32 bits per pixel, and since a byte has 8 bits, and 32 divided by 8 equals 4, and that we have an array of bytes, we multiply the pixel's x coordinate by 4 to account that each pixel consumes 4 bytes, therefore the beginning of each pixel is every 4 bytes, and the width of the stride is 4 times the width of the image. You would apply a similar formula for images that do not support the alpha component.
    • Dim offset As Integer = baseOffset + addOffset

      • Now we add the two offests together, this will be the offset to the first byte that corresponds to the exact pixel we want to edit.
    • bitmapBytes(offset) = blue


    • Set the blue component

    • bitmapBytes(offset + 1) = green


    • Set the green component

    • bitmapBytes(offset + 2) = red


    • Set the red component

    • bitmapBytes(offset + 3) = alpha

      • Set the alpha component
      • Notice that even though we say "ARGB"(in that order), that blue comes first in the array, this has to do with the endienness of this datatype and which order the bytes are stored in unmanaged memory.
      • We are done modifying our pixel's color, and all ready to wrap things up.
    • System.Runtime.InteropServices.Marshal.Copy(bitmapBytes, 0, offsetToFirstPixel, byteCount)

      • Again, now we use Marshal to copy the modified array back into unmanaged memory.
    • bmClone.UnlockBits(bmData)

      • Now we unlock the cloned bitmap(and modified using Marshal), the BitmapData will identify what to unlock.
    • Return bmClone

      • Return the cloned, modified bitmap

You should have a complete function that looks something like this now:

01.Public Shared  Function SetPixel(ByRef OriginalBitmap As Bitmap, _
02.                                ByRef PixelLocation As Point, _
03.                                ByRef PixelColor As Color) As Bitmap
04.    Const px As System.Drawing.GraphicsUnit = GraphicsUnit.Pixel
05.    Const fmtArgb As Imaging.PixelFormat = Imaging.PixelFormat.Format32bppArgb
06.    Dim boundsF As RectangleF = OriginalBitmap.GetBounds(px)
07.    Dim bounds As New Rectangle
08.    bounds.Location = New  Point(CInt(boundsF.X), CInt(boundsF.Y))
09.    bounds.Size = New  Size(CInt(boundsF.Width), CInt(boundsF.Height))
10.    Dim bmClone As Bitmap = OriginalBitmap.Clone(bounds, fmtArgb)
11.    Dim bmData As System.Drawing.Imaging.BitmapData = bmClone.LockBits(bounds, Imaging.ImageLockMode.ReadWrite, bmClone.PixelFormat)
12.    Dim offsetToFirstPixel As IntPtr = bmData.Scan0
13.    Dim byteCount As Integer  = Math.Abs(bmData.Stride) * bmClone.Height
14.    Dim bitmapBytes(byteCount - 1) As Byte
15.    System.Runtime.InteropServices.Marshal.Copy(offsetToFirstPixel, bitmapBytes, 0, byteCount)
16.    Dim alpha As Byte  = PixelColor.A
17.    Dim red As Byte  = PixelColor.R
18.    Dim green As Byte  = PixelColor.G
19.    Dim blue As Byte  = PixelColor.B
20.    Dim baseOffset As Integer  = bmData.Stride * PixelLocation.Y
21.    Dim addOffset As Integer  = PixelLocation.X * 4
22.    Dim offset As Integer  = baseOffset + addOffset
23.    bitmapBytes(offset) = blue
24.    bitmapBytes(offset + 1) = green
25.    bitmapBytes(offset + 2) = red
26.    bitmapBytes(offset + 3) = alpha
27.    System.Runtime.InteropServices.Marshal.Copy(bitmapBytes, 0, offsetToFirstPixel, byteCount)
28.    bmClone.UnlockBits(bmData)
29.    Return bmClone
30.End Function

Here is an example use of your newly created SetPixel function:

01.Private Sub Button1_Click(ByVal sender As System.Object, ByVal  e As  System.EventArgs) Handles Button1.Click
02.    Dim sourceBitmap As New Bitmap(100, 100)
03.    Dim graphics As Graphics = graphics.FromImage(SourceBitmap)
04.    graphics.FillRectangle(Brushes.DarkGray, New  Rectangle(New  Point(0, 0), New  Size(100, 100)))
05.    graphics.Dispose()
06.    PictureBox1.BackgroundImageLayout = ImageLayout.None
07.    'Look in the bottom right corner, its tiny...
08.    PictureBox1.BackgroundImage = SetPixel(sourceBitmap, New  Point(99, 99), Color.Red)
09.    sourceBitmap.Dispose()
10.End Sub

Notes:

  • Be sure to turn Option Strict On!
  • Calling this function for a pixel at a time would be just as costly as using the Graphics object or api, the trick to seeing a performance gain is to create a lockbits function that will change many pixels in-between calling Bitmap.LockBits, and Bitmap.Unlockbits.
  • Calling Lockbits and Unlockbits just to change one pixel at a time defeats the purpose of lockbits, but this example focuses on showing how to change a pixel, and not confusing you with iteration algorithms, you can advance to those as your understanding improves.
  • Examples after this will not be commented.

Example 2(Fill Rectangle)

  • FillRectangle Function
  • Code
Public Shared  Function FillRectangle(ByRef OriginalBitmap As Bitmap, _
                                     ByRef Rectangle As Rectangle, _
                                     ByRef FillColor As Color) As Bitmap
    Const px As System.Drawing.GraphicsUnit = GraphicsUnit.Pixel
    Const fmtArgb As Imaging.PixelFormat = Imaging.PixelFormat.Format32bppArgb
    Dim boundsF As RectangleF = OriginalBitmap.GetBounds(px)
    Dim bounds As New Rectangle
    bounds.Location = New  Point(CInt(boundsF.X), CInt(boundsF.Y))
    bounds.Size = New  Size(CInt(boundsF.Width), CInt(boundsF.Height))
    Dim bmClone As Bitmap = OriginalBitmap.Clone(bounds, fmtArgb)
    Dim bmData As System.Drawing.Imaging.BitmapData = bmClone.LockBits(bounds, Imaging.ImageLockMode.ReadWrite, bmClone.PixelFormat)
    Dim offsetToFirstPixel As IntPtr = bmData.Scan0
    Dim byteCount As Integer  = Math.Abs(bmData.Stride) * bmClone.Height
    Dim bitmapBytes(byteCount - 1) As Byte
    System.Runtime.InteropServices.Marshal.Copy(offsetToFirstPixel, bitmapBytes, 0, byteCount)
    Dim StartOffset As Integer  = (Rectangle.Top * bmData.Stride)
    Dim EndOffset As Integer  = StartOffset + (Rectangle.Height * bmData.Stride)
    Dim RectLeftOffset As Integer  = (Rectangle.Left * 4), RectRightOffset As Integer  = ((Rectangle.Left + Rectangle.Width) * 4)
    Dim X As Integer  = Rectangle.Left, Y As  Integer = Rectangle.Top - 1
    For FirstOffsetInEachLine As Integer  = StartOffset To  EndOffset Step  bmData.Stride
        Y += 1
        X = Rectangle.Left - 1
        For PixelOffset As Integer  = RectLeftOffset To  RectRightOffset Step  4
            X += 1
            Dim PixelLocation As New Point(X, Y)
            bitmapBytes(FirstOffsetInEachLine + PixelOffset) = FillColor.B
            bitmapBytes(FirstOffsetInEachLine + PixelOffset + 1) = FillColor.G
            bitmapBytes(FirstOffsetInEachLine + PixelOffset + 2) = FillColor.R
            bitmapBytes(FirstOffsetInEachLine + PixelOffset + 3) = FillColor.A
        Next
    Next
    System.Runtime.InteropServices.Marshal.Copy(bitmapBytes, 0, offsetToFirstPixel, byteCount)
    bmClone.UnlockBits(bmData)
    Return bmClone
End Function

Example Usage

Private Sub Button1_Click(ByVal sender As System.Object, ByVal  e As  System.EventArgs) Handles Button1.Click
    Dim sourceBitmap As New Bitmap(100, 100)
    Dim graphics As Graphics = graphics.FromImage(SourceBitmap)
    graphics.FillRectangle(Brushes.DarkGray, New  Rectangle(New  Point(0, 0), New  Size(100, 100)))
    graphics.Dispose()
    PictureBox1.BackgroundImageLayout = ImageLayout.None
    Dim FillRect As New Rectangle(New Point(10, 10), New Size(80, 80))
    PictureBox1.BackgroundImage = FillRectangle(sourceBitmap, FillRect, Color.Red)
    sourceBitmap.Dispose()
End Sub

Example 3(Grayscale Rectangle)

  • Example Code
  • Please note the example below contains a Public Enum
Public Shared  Function GrayScaleFillRectangle(ByVal OriginalBitmap As Bitmap, ByRef Rectangle As Rectangle, ByVal GrayScaleMode As GrayScaleMode) As Image
    Const px As System.Drawing.GraphicsUnit = GraphicsUnit.Pixel
    Const fmtArgb As Imaging.PixelFormat = Imaging.PixelFormat.Format32bppArgb
    Dim boundsF As RectangleF = OriginalBitmap.GetBounds(px)
    Dim bounds As New Rectangle
    bounds.Location = New  Point(CInt(boundsF.X), CInt(boundsF.Y))
    bounds.Size = New  Size(CInt(boundsF.Width), CInt(boundsF.Height))
    Dim bmClone As Bitmap = OriginalBitmap.Clone(bounds, fmtArgb)
    Dim bmData As System.Drawing.Imaging.BitmapData = bmClone.LockBits(bounds, Imaging.ImageLockMode.ReadWrite, bmClone.PixelFormat)
    Dim offsetToFirstPixel As IntPtr = bmData.Scan0
    Dim byteCount As Integer  = Math.Abs(bmData.Stride) * bmClone.Height
    Dim bitmapBytes(byteCount - 1) As Byte
    System.Runtime.InteropServices.Marshal.Copy(offsetToFirstPixel, bitmapBytes, 0, byteCount)
    Dim StartOffset As Integer  = (Rectangle.Top * bmData.Stride)
    Dim EndOffset As Integer  = StartOffset + (Rectangle.Height * bmData.Stride)
    Dim RectLeftOffset As Integer  = (Rectangle.Left * 4), RectRightOffset As Integer  = ((Rectangle.Left + Rectangle.Width) * 4)
    Dim X As Integer  = Rectangle.Left, Y As  Integer = Rectangle.Top - 1
    For FirstOffsetInEachLine As Integer  = StartOffset To  EndOffset Step  bmData.Stride
        Y += 1
        X = Rectangle.Left - 1
        For PixelOffset As Integer  = RectLeftOffset To  RectRightOffset Step  4
            X += 1
            Dim PixelLocation As New Point(X, Y)
            Dim C As Color = Color.FromArgb(bitmapBytes(FirstOffsetInEachLine + PixelOffset + 3), _
                                bitmapBytes(FirstOffsetInEachLine + PixelOffset + 2), _
                                bitmapBytes(FirstOffsetInEachLine + PixelOffset + 1), _
                                bitmapBytes(FirstOffsetInEachLine + PixelOffset))
            Dim RGB As Integer  = 0
            Dim FillColor As Color
            Select Case  GrayScaleMode
                Case Form1.GrayScaleMode.GrayScaleLuminosity
                    RGB = CInt(C.R * 0.21 + C.G * 0.71 + C.B * 0.07)
                    FillColor = Color.FromArgb(RGB)
                Case Form1.GrayScaleMode.GrayScaleAverage
                    RGB = (CInt(C.R) + CInt(C.G) + CInt(C.B)) \ 3
                    FillColor = Color.FromArgb(RGB)
                Case Else
                    bmClone.UnlockBits(bmData)
                    bmClone.Dispose()
                    Return OriginalBitmap
            End Select
            bitmapBytes(FirstOffsetInEachLine + PixelOffset) = FillColor.A
            bitmapBytes(FirstOffsetInEachLine + PixelOffset + 1) = FillColor.R
            bitmapBytes(FirstOffsetInEachLine + PixelOffset + 2) = FillColor.G
            bitmapBytes(FirstOffsetInEachLine + PixelOffset + 3) = FillColor.B
        Next
    Next
    System.Runtime.InteropServices.Marshal.Copy(bitmapBytes, 0, offsetToFirstPixel, byteCount)
    bmClone.UnlockBits(bmData)
    Return bmClone
End Function
Public Enum  GrayScaleMode As  Integer
    GrayScaleLuminosity = 1
    GrayScaleAverage = 2
End Enum
  • Example Usage:
Private Sub Button1_Click(ByVal sender As System.Object, ByVal  e As  System.EventArgs) Handles Button1.Click
    Dim sourceBitmap As New Bitmap(100, 100)
    Dim graphics As Graphics = graphics.FromImage(sourceBitmap)
    graphics.FillRectangle(Brushes.DarkGray, New  Rectangle(New  Point(0, 0), New  Size(100, 100)))
    graphics.DrawLine(Pens.Red, New  Point(0, 0), New  Point(99, 99))
    graphics.Dispose()
    PictureBox1.BackgroundImageLayout = ImageLayout.None
    Dim FillRect As New Rectangle(New Point(10, 10), New Size(80, 80))
    PictureBox1.BackgroundImage = GrayScaleFillRectangle(sourceBitmap, FillRect, GrayScaleMode.GrayScaleLuminosity)
    sourceBitmap.Dispose()
End Sub
  • Note*
    • I suggest trying this on an actual image....

More Examples

This class contains shared functions(some of which are listed above, and others that are not...) that can be used for lockbits.

Download

Due to the size of this example, a MSDN Gallery link will be posted. Please remember to vote!

Download Example Project Here

Summary

You can use iteration to set more than one pixel at a time, but the main focus of this article is on how to use LockBits.... There are many more things you can do with LockBits, and I will update this later, but for now, I hope you enjoy the 3 examples.

References

Please view my other Technet Wiki articles

I hope you find this helpful!

See Also

An important place to find a huge amount of Visual Basic related articles is the TechNet Wiki itself. The best entry point is Visual Basic Resources on the TechNet Wiki

Stats Total