圖像處理中,有很多算法由於其內在的復雜性是天然的耗時大戶,加之圖像本身蘊涵的數據量比一般的對象就大,因此,針對這類算法,執行速度的提在很大程度上依賴於硬件的性能,現在流行的CPU都是至少2核的,稍微好點的4核,甚至8核,因此,如果能充分利用這些資源,必將能發揮機器的強大優勢,為算法的執行效果提升一個檔次。 在單核時代,多線程程序的主要目的是防止UI假死,而一般情況下此時多線程程序的性能會比單線程的慢,這種情況五六年前是比較普遍的,所有哪個時候用VB6寫的圖像程序可能比VC6的慢不了多少。可在多核時代,多線程的合理利用可以使得程序速度線性提升。 在一般的編程工具中,都有提供線程操作的相關類。比如在VS2010中,提供了諸如System.Threading、System.Threading.Tasks等命名空間,方便了大家對多線程程序的編制。但是直接的使用Threading類還是很不方便,為此,在C#的幾個後續版本中,加入了Parallel這樣的並行計算類,在實際的編碼中,配合Partitioner.Create方法,我們會發現這個類特別適合於圖像處理中的並行計算,比如下面這個簡單的代碼就實現反色算法的並行計算: private void Invert(Bitmap Bmp) { if (Bmp.PixelFormat == PixelFormat.Format24bppRgb) { BitmapData BmpData = Bmp.LockBits(new Rectangle(0, 0, Bmp.Width, Bmp.Height), ImageLockMode.ReadOnly, Bmp.PixelFormat); Parallel.ForEach(Partitioner.Create(0, BmpData.Height), (H) => { int X, Y, Width, Height, Stride; byte* Scan0, CurP; Width = BmpData.Width; Height = BmpData.Height; Stride = BmpData.Stride; Scan0 = (byte*)BmpData.Scan0; for (Y = H.Item1; Y < H.Item2; Y++) { CurP = Scan0 + Y * Stride; for (X = 0; X < Width; X++) { *CurP = (byte)(255 - *CurP); *(CurP + 1) = (byte)(255 - *(CurP + 1)); *(CurP + 2) = (byte)(255 - *(CurP + 2)); CurP += 3; } } }); Bmp.UnlockBits(BmpData); } } 和經典的反色代碼相比,只是增加了 Parallel.ForEach(Partitioner.Create(0, BmpData.Height), (H) => 以及將 for (Y = 0; Y < Height; Y++) 修改為 for (Y = H.Item1; Y < H.Item2; Y++) 但是在效率上我們做如下對比(筆記本I3cpu): 圖像大小 單線程時間/ms 多線程時間/ms 1024*768 4 2 1600*1200 11 6 4000*3000 78 40 再舉個Photoshop中去色算法的例子,如果用並行計算則相應代碼為: private void Desaturate(Bitmap Bmp) { if (Bmp.PixelFormat == PixelFormat.Format24bppRgb) { BitmapData BmpData = Bmp.LockBits(new Rectangle(0, 0, Bmp.Width, Bmp.Height), ImageLockMode.ReadOnly, Bmp.PixelFormat); Parallel.ForEach(Partitioner.Create(0, BmpData.Height), (H) => { int X, Y, Width, Height, Stride; byte Red, Green, Blue, Max, Min, Value; byte* Scan0, CurP; Width = BmpData.Width; Height = BmpData.Height; Stride = BmpData.Stride; Scan0 = (byte*)BmpData.Scan0; for (Y = H.Item1; Y < H.Item2; Y++) { CurP = Scan0 + Y * Stride; for (X = 0; X < Width; X++) { Blue = *CurP; Green = *(CurP + 1); Red = *(CurP + 2); if (Blue > Green) { Max = Blue; Min = Green; } else { Max = Green; Min = Blue; } if (Red > Max) Max = Red; else if (Red < Min) Min = Red; Value = (byte)((Max + Min) >> 1); *CurP = Value; *(CurP + 1) = Value; *(CurP + 2) = Value; CurP += 3; } } }); Bmp.UnlockBits(BmpData); } 去色的原理就是取彩色圖像RGB通道最大值和最小值的平均值作為新的三通道的顏色值。 做個速度比較: 圖像大小 單線程時間/ms 多線程時間/ms 1024*768 5 2 1600*1200 15 8 4000*3000 117 60 反色和去色都是輕量級的數字圖像算法,但是再多核CPU上依然能夠發揮多線程的速度優勢。 由以上兩個簡單的例子,我們先總結一下使用Parallel.ForEach結合Partitioner.Create進行並行計算的一些事情。 第一:這種並行編程非常之方便,特別是對於圖像這種類似於矩陣方式存儲的數據,算法基本都是先行後列或先列後行方式進行計算的。 第二:凡是變量的值會在並行程序改變的變量,都必須定義在Parallel的大括號內,否則會出現莫名的錯誤。 第三:在並行代碼內部直進行讀取而不進行復制的單個變量,可以放到Parallel大括號之外,但也建議放在括號內,因為實際表明,這樣速度會快,比如上述的Width,Height之類的變量。 第四:內部的for循環的循環起點和終點需要用Item1及Item2代替。 我們在看看復雜點的算法的例子,這裡我們舉一個縮放模糊的例子。 用過Photoshop的人都知道,PS的大部分濾鏡都提供了實時預覽的功能,但是有些濾鏡,就比如這個縮放模糊,PS沒有提供,究其原因,就是其計算量比較大,無法做到實時。如下圖所示: 同時,我們選擇對一副大點的圖像,比如上述的4000*3000的圖像進行縮放魔術,觀察CPU的使用情況,如上圖所示,4個核都是在慢復核工作,可見PS也是使用了多線程進行處理。 那我們用C#對改算法進行並行的主要代碼如下: public static void ZoomBlur(Bitmap Bmp, int SampleRadius = 100, int Amount = 100, int CenterX = 256, int CenterY = 256) { int Width, Height, Stride; BitmapData BmpData = Bmp.LockBits(new Rectangle(0, 0, Bmp.Width, Bmp.Height), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb); Width = BmpData.Width; Height = BmpData.Height; Stride = BmpData.Stride; byte* BitmapClone = (byte*)Marshal.AllocHGlobal(BmpData.Stride * BmpData.Height); CopyMemory(BitmapClone, BmpData.Scan0, BmpData.Stride * BmpData.Height); Parallel.ForEach(Partitioner.Create(0, Height, Height / Environment.ProcessorCount), (H) => { int SumRed, SumGreen, SumBlue,Fx, Fy, Fcx, Fcy; int X, Y, I; byte* Pointer, PointerC; uint* Row, RowP; Fcx = CenterX << 16 + 32768; Fcy = CenterY << 16 + 32768; Row = (uint*)Marshal.AllocHGlobal(SampleRadius * 4); for (Y = H.Item1; Y < H.Item2; Y++) { Pointer = (byte*)BmpData.Scan0 + Stride * Y; Fy = (Y << 16) - Fcy; RowP = Row; for (I = 0; I < SampleRadius; I++) { Fy -= ((Fy >> 4) * Amount) >> 10; *RowP = (uint)(BitmapClone + Stride * ((Fy + Fcy) >> 16)); RowP++; } for (X = 0; X < Width; X++) { Fx = (X << 16) - Fcx; SumRed = 0; SumGreen = 0; SumBlue = 0; RowP = Row; for (I = 0; I < SampleRadius; I++) { Fx -= ((Fx >> 4) * Amount) >> 10; PointerC = (byte*)*RowP + ((Fx + Fcx) >> 16) * 3; // *3不需要優化,編譯器會變為lea eax,[eax+eax*2] SumBlue += *(PointerC); SumGreen += *(PointerC + 1); SumRed += *(PointerC + 2); RowP++; } *(Pointer) = (byte)(SumBlue / SampleRadius); *(Pointer + 1) = (byte)(SumGreen / SampleRadius); *(Pointer + 2) = (byte)(SumRed / SampleRadius); Pointer += 3; } } Marshal.FreeHGlobal((IntPtr)Row); }); Marshal.FreeHGlobal((IntPtr)BitmapClone); // 釋放掉備份數據 Bmp.UnlockBits(BmpData); } 其中的CopyMemory函數聲明如下: [DllImport("Kernel32.dll", EntryPoint = "RtlMoveMemory", SetLastError = true)] internal static extern void CopyMemory(byte* Dest, byte* src, int Length); 我們先看看速度提升: 圖像大小 單線程時間(ms) 多線程時間(ms) PS用時(s) 1024*768 926 556 0.7 1600*1200 2986 1214 1.5 4000*3000 21249 6047 7.2 從上圖中可以看到,圖像越大,單線程和多線程之間的時間比例就越大,也越能發揮多線程的優勢。C#中多線程比PS的快,並不能完全說明PS做的不夠好,那是因為可能一個是算法不完全一致,二是PS還需要做其他的一些處理。 具體分析的上面的代碼,可以注意到Parallel.ForEach(Partitioner.Create(0, Height, Height / Environment.ProcessorCount), (H) =>這句多了一個Height / Environment.ProcessorCount的代碼,我這樣做的主要目的是強制使得並行計算只使用Environment.ProcessorCount個線程,一方面讓性能最大化,另外一方面的主要原因是讓Row = (uint*)Marshal.AllocHGlobal(SampleRadius * 4)這句代碼少執行一些,從而少占用些內存。