开发者

Optimise manual image processing (.Net 4)

I'm writing an image effects library which exposes the functionality using fluent notation.

Some simple effects are fast (borders, drop shadows, etc.) but some of the more CPU intensive calls are slow (blur I'm looking at you)

Now, taking blur as an example, I've got the following method:

    Public Function Process(ByRef ImageEffect As Interfaces.IImageEffect) As Interfaces.IImageEffect Implements Interfaces.IEffect.Process
        Dim Image As Bitmap = CType(ImageEffect.Image, Bitmap)
        Dim SourceColors As New List(Of Drawing.Color)
        For X = 0 To ImageEffect.Image.Width - 1
            For Y = 0 To ImageEffect.Image.Height - 1
                SourceColors.Clear()
                For ScanX = Math.Max(0, X - Strength) To Math.Min(Image.Width - 1, X + Strength)
                    For ScanY = Math.Max(0, Y - Strength) To Math.Min(Image.Height - 1, Y + Strength)
                        SourceColors.Add(Image.GetPixel(ScanX, ScanY))
                    Next
                Next
                Dim NewColor = Color.FromArgb(
                 CInt(SourceColors.Average(Function(Z) Z.A)),
                 CInt(SourceColors.Average(Function(Z) Z.R)),
                 CInt(SourceColors.Average(Function(Z) Z.G)),
                 CInt(SourceColors.Average(Function(Z) Z.B))
                 )

                Image.SetPixel(X, Y, NewColor)

            Next
        Next
        Return ImageEffect
    End Function

I'm aware that my code can be improved (array not a list to store colors, etc.) but by FAR the most CPU-intensive method call is to Image.GetPixel - and I'd prefer to fix that before touching the rest of my code.

Currently the breakdown is:

  • Image.GetPixel : 47%
  • Image.SetPixel : 13%
  • Linq Average: 11%
  • Misc: 29%

That's assuming a blur strength of 1 eg reading <= 9 pixels for each set pixel.

Now with other languages, I've read images from disk and skipped to the appropriate pixel by doing something like: (Y*Width+X)*PixelBytes which has been pretty fast. Is there an equivalent in .Net (bearing in mind my image may only be in memory). Does GetPixel already do this? If so, how can I improve my method?

Am I missing an obvious trick to optimise this?

Solution:

Public Function Process(ByRef ImageEffect As Interfaces.IImageEffect) As Interfaces.IImageEffect Implements Interfaces.IEffect.Process
    Dim bmp = DirectCast(ImageEffect.Image, Bitmap)

    '' Lock the bitmap's bits.  
    Dim Dimensions As New Rectangle(0, 0, bmp.Width, bmp.Height)
    Me.Dimensions = Dimensions
    Dim bmpData As System.Drawing.Imaging.BitmapData = bmp.LockBits(Dimensions, Drawing.Imaging.ImageLockMode.ReadWrite, bmp.PixelFormat)

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

    '' Declare an array to hold the bytes of the bitmap.
    '' This code is specific to a bitmap with 24 bits per pixels.
    Dim bytes As Integer = Math.Abs(bmpData.Stride) * bmp.Height
    Dim ARGBValues(bytes - 1) As Byte

    '' Copy the ARGB values into the array.
    System.Runtime.InteropServi开发者_Go百科ces.Marshal.Copy(ptr, ARGBValues, 0, bytes)

    '' Call the function to actually manipulate the data (next code block)
    ProcessRaw(bmpData, ARGBValues)

    System.Runtime.InteropServices.Marshal.Copy(ARGBValues, 0, ptr, bytes)

    bmp.UnlockBits(bmpData)

    Return ImageEffect
End Function

And the function to actually manipulate the image (I know this is verbose but it's fast):

    Protected Overrides Sub ProcessRaw(ByVal BitmapData As System.Drawing.Imaging.BitmapData, ByRef ARGBData() As Byte)
        Dim SourceColors As New List(Of Byte())
        For Y = 0 To Dimensions.Height - 1
            For X = 0 To Dimensions.Width - 1
                Dim FinalA = 0.0
                Dim FinalR = 0.0
                Dim FinalG = 0.0
                Dim FinalB = 0.0

                SourceColors.Clear()
                Dim SamplesCount =
                    (Math.Min(Dimensions.Height - 1, Y + Strength) - Math.Max(0, Y - Strength) + 1) *
                    (Math.Min(Dimensions.Width - 1, X + Strength) - Math.Max(0, X - Strength) + 1)
                For ScanY = Math.Max(0, Y - Strength) To Math.Min(Dimensions.Height - 1, Y + Strength)
                    For ScanX = Math.Max(0, X - Strength) To Math.Min(Dimensions.Width - 1, X + Strength)
                        Dim StartPos = CalculatePixelPosition(ScanX, ScanY)
                        FinalB += ARGBData(StartPos + 0) / SamplesCount
                        FinalG += ARGBData(StartPos + 1) / SamplesCount
                        FinalR += ARGBData(StartPos + 2) / SamplesCount
                        FinalA += ARGBData(StartPos + 3) / SamplesCount
                    Next
                Next

                Dim OutputPos = CalculatePixelPosition(X, Y)
                ARGBData(OutputPos + 0) = CByte(CInt(FinalB))
                ARGBData(OutputPos + 1) = CByte(CInt(FinalG))
                ARGBData(OutputPos + 2) = CByte(CInt(FinalR))
                ARGBData(OutputPos + 3) = CByte(CInt(FinalA))

            Next
        Next
    End Sub

The performance increase is HUGE - At least 30-40x faster. The most CPU-intensive call now is to calculate the position in the array to modify:

Protected Function CalculatePixelPosition(ByVal X As Integer, ByVal Y As Integer) As Integer
    Return ((Dimensions.Width * Y) + X) * 4
End Function

Which seems pretty optimised to me :)

I can now process 20x20 blurs in under 3 seconds for an 800x600 image :)


You can use a bytearray like Ross suggested. You can use Marshal.Copy to copy data from an unmanaged pointer to a byte array. You access to the unmanaged memory of a bitmap with LockBits/UnlockBits.

But personally I prefer having an array of a meaningful 32 bit color struct. (You can't use System.Drawing.Color for that, it's much larger and slower). If you want to copy to an array type for which Marshal.Copy isn't defined you can do it like my Pixel.LoadFromBitmap function.

You shouldn't allocated to many big arrays/allocate too often, since the GC doesn't cope well with that. So you might need to implement manual pooling.


You should get the Bytes array directly , not using the GetPixel.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜