圖像處理中,有很多算法由於其內在的復雜性是天然的耗時大戶,加之圖像本身蘊涵的數據量比一般的對象就大,因此,針對這類算法,執行速度的提在很大程度上依賴於硬件的性能,現在流行的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)這句代碼少執行一些,從而少占用些內存。
注釋:Partitioner.Create的第三個參數是指定某個單個線程處理的范圍,對於這里的例子就是一個線程一次性負責處理Height / Environment.ProcessorCount個行。對於不足的部分系統會自動取舍。如果用戶未指明這個參數,則由系統自動分配,如下圖所示,系統分配了7個線程同時執行。
系統自動分配 用戶指定
我們自定義每個線程的執行范圍還有一個好處是針對某些對第一行需要進行特殊處理的圖像算法,這些算法在第一行的計算耗時上通常要比其他的行多,如果由系統分配,我們就有冒更多耗時的風險。這也是為什么Parallel類中的Parallel.ForEach+Partitioner.Create是最適合圖像處理的並行語法。
實際上,在一個耗時的操作中,一般情況下,都需要至少還應該有如下幾個功能:
1、UI界面必須能響應用戶的輸入,不能出現假死現象。
2、必須有能告知用戶程序目前處於什么狀態,最簡單就是進度條。
3、如果用戶無耐心等待下去,或發現處理的效果不理想,可以立即中斷。
由於Parallel類內部使用了類似於線程的Join方法來實現其內部分配內存的同步問題,因此如果想讓UI能及時響應,還需要在開一個線程來執行算法。用戶中斷這一塊則比較復雜,需要根據具體的操作類型來恢復數據,而進度條這一塊則稍微簡單點,只要用一個全局變量累積計算了多少行就可以了,比如在上述代碼的 Pointer += 3;后加上如下語句就可以了:
lock (this) { ProcessedLine++; Progress.Value = ProcessedLine * 100 / Height; }
上述第一條和第三條我在附件中未做實現,有興趣的朋友可以自己研究下(其實我實現了,不過我對這一塊的操作不是很熟悉,因此不想獻丑)。
附件參考代碼: http://files.cnblogs.com/Imageshop/MultiThreadZoomBlur.rar
*********************************作者: laviewpbt 時間: 2013.9.28 聯系QQ: 33184777 轉載請保留本行信息************************