用最简单的方式在C#中使用多线程加速耗时的图像处理算法的执行(多核机器)。

摘要:
可在多核时代,多线程的合理利用可以使得程序速度线性提升。做个速度比较:图像大小单线程时间/ms多线程时间/ms1024*768521600*12001584000*300011760反色和去色都是轻量级的数字图像算法,但是再多核CPU上依然能够发挥多线程的速度优势。

图像处理中,有很多算法由于其内在的复杂性是天然的耗时大户,加之图像本身蕴涵的数据量比一般的对象就大,因此,针对这类算法,执行速度的提在很大程度上依赖于硬件的性能,现在流行的CPU都是至少2核的,稍微好点的4核,甚至8核,因此,如果能充分利用这些资源,必将能发挥机器的强大优势,为算法的执行效果提升一个档次。

在单核时代,多线程程序的主要目的是防止UI假死,而一般情况下此时多线程程序的性能会比单线程的慢,这种情况五六年前是比较普遍的,所有哪个时候用VB6写的图像程序可能比VC6的慢不了多少。可在多核时代,多线程的合理利用可以使得程序速度线性提升。

在一般的编程工具中,都有提供线程操作的相关类。比如在VS2010中,提供了诸如System.Threading、System.Threading.Tasks等命名空间,方便了大家对多线程程序的编制。但是直接的使用Threading类还是很不方便,为此,在C#的几个后续版本中,加入了Parallel这样的并行计算类,在实际的编码中,配合Partitioner.Create方法,我们会发现这个类特别适合于图像处理中的并行计算,比如下面这个简单的代码就实现反色算法的并行计算:

private voidInvert(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) =>
        {
            intX, 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*76842
1600*1200116
4000*30007840

再举个Photoshop中去色算法的例子,如果用并行计算则相应代码为:

private voidDesaturate(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) =>
        {
            intX, Y, Width, Height, Stride;
            byteRed, 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*76852
1600*1200158
4000*300011760

反色和去色都是轻量级的数字图像算法,但是再多核CPU上依然能够发挥多线程的速度优势。

由以上两个简单的例子,我们先总结一下使用Parallel.ForEach结合Partitioner.Create进行并行计算的一些事情。

第一:这种并行编程非常之方便,特别是对于图像这种类似于矩阵方式存储的数据,算法基本都是先行后列或先列后行方式进行计算的。

第二:凡是变量的值会在并行程序改变的变量,都必须定义在Parallel的大括号内,否则会出现莫名的错误。

第三:在并行代码内部直进行读取而不进行复制的单个变量,可以放到Parallel大括号之外,但也建议放在括号内,因为实际表明,这样速度会快,比如上述的Width,Height之类的变量。

第四:内部的for循环的循环起点和终点需要用Item1及Item2代替。

我们在看看复杂点的算法的例子,这里我们举一个缩放模糊的例子。

用过Photoshop的人都知道,PS的大部分滤镜都提供了实时预览的功能,但是有些滤镜,就比如这个缩放模糊,PS没有提供,究其原因,就是其计算量比较大,无法做到实时。如下图所示:

用最简单的方式在C#中使用多线程加速耗时的图像处理算法的执行(多核机器)。第1张用最简单的方式在C#中使用多线程加速耗时的图像处理算法的执行(多核机器)。第2张

同时,我们选择对一副大点的图像,比如上述的4000*3000的图像进行缩放魔术,观察CPU的使用情况,如上图所示,4个核都是在慢复核工作,可见PS也是使用了多线程进行处理。

那我们用C#对改算法进行并行的主要代码如下:

public static void ZoomBlur(Bitmap Bmp, int SampleRadius = 100, int Amount = 100, int CenterX = 256, int CenterY = 256)
{
    intWidth, 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) =>
    {
        intSumRed, SumGreen, SumBlue,Fx, Fy, Fcx, Fcy;
        intX, 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.7
1600*1200298612141.5
4000*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个线程同时执行。

用最简单的方式在C#中使用多线程加速耗时的图像处理算法的执行(多核机器)。第3张用最简单的方式在C#中使用多线程加速耗时的图像处理算法的执行(多核机器)。第4张

系统自动分配 用户指定

我们自定义每个线程的执行范围还有一个好处是针对某些对第一行需要进行特殊处理的图像算法,这些算法在第一行的计算耗时上通常要比其他的行多,如果由系统分配,我们就有冒更多耗时的风险。这也是为什么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#中使用多线程加速耗时的图像处理算法的执行(多核机器)。》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇APP上传流程总结JAVA读取yml配置文件指定key下的所有内容下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

008-多线程-基础-ThreadGroup线程组

线程组ThreadGroup表示一组线程的集合,一旦一个线程归属到一个线程组之中后,就不能再更换其所在的线程组。那么为什么要使用线程组呢?个人认为有以下的好处:方便统一管理,线程组可以进行复制,快速定位到一个线程,统一进行异常设置等。ThreadGroup它其实并不属于Java并发包中的内容,它是java.lang中的内容。但是掌握对其的于理解,在实际应用...

多线程和CPU的关系

什么是CPU (1)         Central  Progressing  Unit 中央处理器,是一块超大规模的集成电路,是一台计算机的运算核心和控制核心。 (2)         CPU包括 运算器,高速缓冲存储器,总线。 (3)         它的工作,主要是解释计算机中的指令,和处理计算机软件中的数据。它在计算机中起着最重要的作用,构成了系...

C#多线程(二)

一、线程池每次创建一个线程,都会花费几百微秒级别的时间来创建一个私有的局部栈,每个线程默认使用1M的内存。这个可以在使用Thread类的构造函数时设置: [csharp] view plaincopyprint?  new Thread(new ThreadStart(Go), 2);   new Thread(new ParameterizedT...

C#编程总结(二)多线程基础

C#编程总结(二)多线程基础 无论您是为具有单个处理器的计算机还是为具有多个处理器的计算机进行开发,您都希望应用程序为用户提供最好的响应性能,即使应用程序当前正在完成其他工作。要使应用程序能够快速响应用户操作,同时在用户事件之间或者甚至在用户事件期间利用处理器,最强大的方式之一是使用多线程技术。 多线程:线程是程序中一个单一的顺序控制流程.在单个程序中同时...

Java多线程 开发中避免死锁的八种方法

1. 设置超时时间 使用JUC包中的Lock接口提供的tryLock方法.该方法在获取锁的时候, 可以设置超时时间, 如果超过了这个时间还没拿到这把锁, 那么就可以做其他的事情, 而不是像synchronized如果没有拿到锁会一直等待下去. boolean tryLock ( long time , T...

delphi之多线程编程(一)

本文的内容取自网络,并重新加以整理,在此留存仅仅是方便自己学习和查阅。所有代码均亲自测试 delphi7下测试有效。图片均为自己制作。 多线程应该是编程工作者的基础技能, 但这个基础我从来没学过,所以仅仅是看上去会一些,明白了2+2的时候,其实我还不知道1+1。 开始本应该是一篇洋洋洒洒的文字, 不过我还是提倡先做起来, 在尝试中去理解. 先试试这个:...