我一直用GDI+做Winform 的基於指針的圖片處理,這次下決心全部移到wpf上(主要是顯示布局很方便)
采用的圖片是
2512*3307 的大圖 830萬像素
類庫基於WritableBitmapEx 的wpf版本
函數是我自己寫的擴展方法,只是利用了 writableBitmapEx提供的環境 ,我懶得從頭到尾自己寫了
1.標准int32數組遍歷計算 release
0.28s
unsafe public static void TestGray1(this WriteableBitmap bmp)
{
using (var context = bmp.GetBitmapContext())
{
int height = context.Height;
int width = context.Width;
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
int pos=y * context.Width + x;
var c = context.Pixels[pos];
var r = (byte)(c >> 16);
var g = (byte)(c >> 8);
var b = (byte)(c);
var gray = ((r * 38 + g * 75 + b * 15) >> 7);
var color=(255 << 24) | (gray << 16) | (gray << 8) | gray;
context.Pixels[pos]=color;
}
}
}
}
2.標准int32指針遍歷計算 release
0.04s
unsafe public static void TestGray2(this WriteableBitmap bmp)
{
using (var context = bmp.GetBitmapContext())
{
var ptr = context.Pixels;
int height = context.Height;
int width = context.Width;
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
var c = *ptr;
var r = (byte)(c >> 16) ;
var g = (byte)(c >> 8) ;
var b = (byte)(c) ;
var gray = ((r * 38 + g * 75 + b * 15) >> 7);
var color = (255 << 24) | (gray << 16) | (gray << 8) | gray;
*ptr = color;
ptr++;
}
}
}
}
3.colorstruct指針 遍歷計算
0.02 s
應該是已經到極限速度了[除了後面的並行方式],我已經想不出還有什麼方法可以提高處理速度
而且這種方式是最直觀的,最容易理解的處理方式,也便於以後維護
[StructLayout(LayoutKind.Sequential)]
public struct PixelColor
{
public byte Blue;
public byte Green;
public byte Red;
public byte Alpha;
}
unsafe public static void TestGray3(this WriteableBitmap bmp)
{
using (var context = bmp.GetBitmapContext())
{
var ptr = (PixelColor*)context.Pixels;
int height = context.Height;
int width = context.Width;
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
var c = *ptr;
var gray = ((c.Red * 38 + c.Green * 75 + c.Blue * 15) >> 7);
(*ptr).Green=(*ptr).Red=(*ptr).Blue = (byte)gray;
ptr++;
}
}
}
}
4.作為對比,我又測試了一下 GDI+的 指針處理圖片的速度
0.06s
public static unsafe Bitmap ToGray(Bitmap img)
{
var rect = new System.Drawing.Rectangle(0, 0, img.Width, img.Height);
var data = img.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
var ptr = (ColorType*)data.Scan0.ToPointer();
var bytes = new Int32[img.Width * img.Height];
var height = img.Height;
var width = img.Width;
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
var color = *ptr;
var gray = ((color.R * 38 + color.G * 75 + color.B * 15) >> 7);
(*ptr).R = (*ptr).G = (*ptr).B = (byte)gray;
ptr++;
}
}
img.UnlockBits(data);
return img;
}
5.重頭戲來了。我一直對Parallel.For 很迷惑,為什麼他的消耗時間是普通for的好幾倍。今天仔細研究了一下,發現原來是用錯了
0.01秒 release
筆記本i5cpu,如果台式機的I7會更加強悍,速度會成半成半降低。
主要是利用了微軟的任務並行庫的循環並行化的方法。
注意:默認的並行循環對於函數體很小的情況是很慢的,這種情況必須用Partitioner 創建循環體,這在MSDN有介紹,是關鍵之中的關鍵
unsafe public static void TestGray5(this WriteableBitmap bmp)
{
using (var context = bmp.GetBitmapContext())
{
int height = context.Height;
int width = context.Width;
Parallel.ForEach(Partitioner.Create(0, height), (h) =>
{
var ptr = (PixelColor*)context.Pixels;
ptr += h.Item1 * width;
for (int y = h.Item1; y < h.Item2; y++)
{
for (int x = 0; x < width; x++)
{
var c = *ptr;
var gray = ((c.Red * 38 + c.Green * 75 + c.Blue * 15) >> 7);
(*ptr).Green = (*ptr).Red = (*ptr).Blue = (byte)gray;
ptr++;
}
}
});
}
}
感想
1.絕對不要在循環體內使用屬性或函數,很有可能會降低數倍計算速度。
因為屬性本質上是個函數,而在循環體內最好不要再調用函數,如果確實需要用內聯代碼的方式,c#沒有inline,那麼copy代碼吧,反正為了速度。
2. 用指針移位操作 似乎比 直接數組訪問要快10倍啊
我感覺要麼是cache命中的原因,要麼是 數組本身存取被屬性封裝了。相當於又調用了函數。
3.TPL 任務並行庫果真好用,看來微軟早已考慮過大量數據並行的循環優化問題09年,只是我一直用錯了方法,才覺得很慢。
天氣真好,寫完代碼後心情舒暢。
摘自 苦力熊