0

I have this function that creates a region from a Bitmap:

Public Shared Function GetRegion(ByVal sender As Bitmap, ByVal transperancyKey As Color, ByVal tolerance As Integer) As Region

    ' Stores all the rectangles for the region.
    Using path As New GraphicsPath()

        ' Scan the image
        For x As Integer = 0 To (sender.Width - 1)
            For y As Integer = 0 To (sender.Height - 1)
                If Not ColorsMatch(sender.GetPixel(x, y), transperancyKey, tolerance) Then
                    path.AddRectangle(New Rectangle(x, y, 1, 1))
                End If
            Next
        Next

        Return New Region(path)
    End Using

End Function

Public Shared Function ColorsMatch(ByVal color1 As Color, ByVal color2 As Color, ByVal tolerance As Integer) As Boolean
    If (tolerance < 0) Then
        tolerance = 0
    End If

    Return Math.Abs(color1.R - color2.R) <= tolerance AndAlso
           Math.Abs(color1.G - color2.G) <= tolerance AndAlso
           Math.Abs(color1.B - color2.B) <= tolerance
End Function

I would like to improve performance using Bitmap.LockBits instead of Bitmap.GetPixel.

What I have by the moment is this incomplete code below. I'm a little bit confused trying to iterate the pixels/colors in the same (efficient)way that the original function does.

Public Shared Function GetRegion(ByVal sender As Bitmap, ByVal transperancyKey As Color, ByVal tolerance As Integer) As Region

    ' Stores all the rectangles for the region.
    Using path As New GraphicsPath()

        ' Lock the bitmap's bits.  
        Dim rect As New Rectangle(0, 0, sender.Width, sender.Height)
        Dim bmpData As BitmapData = sender.LockBits(rect, ImageLockMode.ReadWrite, sender.PixelFormat)

        ' Get the address of the first line.
        Dim ptr As IntPtr = bmpData.Scan0

        ' Declare an array to hold the bytes of the bitmap.
        ' Assume PixelFormat.Format32bppArgb (4 bytes per pixel)
        Dim bytes As Integer = Math.Abs(bmpData.Stride) * sender.Height
        ' Note that I'm not sure whether the above is the proper calculation for that assumption.
        Dim rgbValues(bytes - 1) As Byte

        ' Copy the RGB values into the array.
        Marshal.Copy(ptr, rgbValues, 0, bytes)

        ' Scan the image
        For x As Integer = 0 To (sender.Width - 1)
            For y As Integer = 0 To (sender.Height - 1)
                ' ...
            Next
        Next

        ' Unlock the bits.
        bmp.UnlockBits(bmpData)

        Return New Region(path)
    End Using

End Function

How can I do it?.

ElektroStudios
  • 19,105
  • 33
  • 200
  • 417
  • Are you looking for a simplified `GetPixel` and `SetPixel` using `LockBits` like [this post](http://stackoverflow.com/a/42602506/3110834)? – Reza Aghaei May 07 '17 at 15:42

1 Answers1

1

Something like this should do the trick. I only lock one pixel row at a time to avoid using to much memory with large bitmaps and it makes the logic easy to follow.

Public Shared Function GetRegion(ByVal bm As Bitmap, ByVal transperancyKey As Color, ByVal tolerance As Integer) As Region
    Using path As New GraphicsPath()
        Dim rect As New Rectangle(0, 0, bm.Width, 1)    ' one row high
        Dim pixelData As Int32() = New Int32(0 To bm.Width - 1) {}
        For row As Integer = 0 To (bm.Height - 1)
            ' get the bitmap data as 32bppArgb so that we can read Int32 values
            ' this does not need to match the bm.PixelFormat
            Dim bmData As BitmapData = bm.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb)
            ' copy the pixel data for this row
            Marshal.Copy(bmData.Scan0, pixelData, 0, pixelData.Length)
            bm.UnlockBits(bmData)
            For col As Integer = 0 To (bm.Width - 1)
                Dim px As Color = Color.FromArgb(pixelData(col))
                ' do something with this pixel color
            Next col
            rect.Offset(0, 1)   ' offset to select next row
        Next row

        Return New Region(path)
    End Using
End Function
TnTinMn
  • 11,522
  • 3
  • 18
  • 39
  • I'm getting an **ArgumentException** at the **bm.LockBits()** call with this message info: **Parameter is not valid**. This is the image I'm testing: http://i.imgur.com/Ng1QXLq.png – ElektroStudios Apr 08 '17 at 16:23
  • @ElektroStudios, I missed flipping the Width/Height properties in the For-Next blocks when I rewrote your original code. That's what I get for not running a simple test on it. – TnTinMn Apr 08 '17 at 16:26
  • Thankyou for updating the code. I think something still going bad in the code or maybe is me and the bad way I'm trying to fill/adapt the missing part of your code, where I should add a rectangle using the **GraphicsPath.AddRectangle()** method like I do in the code that I published in my question. I tried several ways but maybe I'm missing something. Please could you clarify in your code which would be the equivalent for X,Y coords. to add the rectangle like I do in the code of my question? (something like **path.AddRectangle(New Rectangle(rect.X * row, rect.Y, 1, 1))** this dont work) – ElektroStudios Apr 08 '17 at 17:08
  • @ElektroStudios, `col` corresponds to `x` and row corresponds to `y'. The origin is top-left with y increasing from top to bottom and x increasing left to right. I probably should have explained that I switched up your loop order to accommodate pulling one row of data at a time. If you need to process the values in the original order, this solution is not valid. The entire bitmap could just as easily be read into an array and accessed by pixel(x,y) if that is what you need. Let me know if you need that type of solution; I have a class defined for doing that. – TnTinMn Apr 08 '17 at 18:14
  • 1
    It's probably more efficient to lock the whole bitmap once for the whole procedure, but only _copy out_ one line at the time. You can get the start pointer for `Marshal.Copy` on each line with `new IntPtr((bmData.Scan0 + row * bmData.Stride)`. – Nyerguds Feb 01 '18 at 15:28