程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> 用最簡單的方式在C#中使用多線程加速耗時的圖像處理算法的執行(多核機器)。

用最簡單的方式在C#中使用多線程加速耗時的圖像處理算法的執行(多核機器)。

編輯:C#入門知識

圖像處理中,有很多算法由於其內在的復雜性是天然的耗時大戶,加之圖像本身蘊涵的數據量比一般的對象就大,因此,針對這類算法,執行速度的提在很大程度上依賴於硬件的性能,現在流行的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)這句代碼少執行一些,從而少占用些內存。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved