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.
精彩评论