用最简单的方式在C#中使用多线程加速耗时的图像处理算法的执行(多核机器)。
来源:互联网 发布:风暴电影结局 知乎 编辑:程序博客网 时间:2024/05/25 16:37
在单核时代,多线程程序的主要目的是防止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多线程时间/ms1024*768421600*12001164000*30007840
再举个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多线程时间/ms1024*768521600*12001584000*300011760
反色和去色都是轻量级的数字图像算法,但是再多核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*7689265560.71600*1200298612141.54000*30002124960477.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 转载请保留本行信息************************
- 用最简单的方式在C#中使用多线程加速耗时的图像处理算法的执行(多核机器)。
- 最简单解决c#在UI线程中执行耗时方法导致界面假死的方法
- 最简单解决c#在UI线程中执行耗时方法导致界面假死的方法
- 多核计算中多线程的退出算法
- c# 中图像的简单二值化处理
- 如何在web页面中,实现耗时操作的后台执行?多线程吗?
- C# 执行cmd命令,以进程的形式执行应用程序,在新的线程中执行耗时的功能逻辑
- Intel 1Gb/10Gb网卡在多核处理器中使用的加速技术
- 多线程处理耗时的业务逻辑
- C#图片处理之:最简单的柔化算法
- C#图片处理之:最简单的柔化算法
- [OpenCV2]执行简单的图像算法
- kmeans(最简单的机器学习算法)
- C#中委托的最简单理解方式
- 在flex中执行一个javascript方法的简单方式
- service是否在main thread中执行, service里面是否能执行耗时的操作?
- tomcat 超时最简单的处理方式
- 适配器中加载耗时图片的处理
- 双指数边缘平滑滤波器用于磨皮算法的尝试。
- 基于模糊集理论的一种图像二值化算法的原理、实现效果及代码
- 十三种基于直方图的图像全局二值化算法原理、实现、代码及效果。
- 二值图像中封闭孔洞的高效填充算法(附源码)。
- 使用局部标准差实现图像的局部对比度增强算法。
- 用最简单的方式在C#中使用多线程加速耗时的图像处理算法的执行(多核机器)。
- 基于总变差模型的纹理图像中图像主结构的提取方法。
- <<一种基于δ函数的图象边缘检测算法>>一文算法的实现。
- 搬瓦工
- 局部自适应自动色阶/对比度算法在图像增强上的应用。
- o(1)复杂度之双边滤波算法的原理、流程、实现及效果。
- Hiberante3.6的入门使用
- 一种可实时处理 O(1)复杂度图像去雾算法的实现。
- Java List链表clear()方法详解