SSE图像算法优化系列三十二:Zhang\Guo图像细化算法的C语言以及SIMD指令优化
阅读原文时间:2022年05月27日阅读:3

二值图像的细化算法也有很多种,比较有名的比如Hilditch细化、Rosenfeld细化、基于索引表的细化、还有Opencv自带的THINNING_ZHANGSUEN、THINNING_GUOHALL喜欢等等。这些都属于迭代的细化方式,当然还有一种是基于二值图像距离变换的细化方法,二值想比较,我个人认为是基于迭代的效果稳定、可靠,但是速度较慢,且速度和图片的内容有关,基于距离变换的版本,优点是速度稳定,但是效果差强人意。本文这里还是选择基于迭代的方式予以实现。

相关的参考文章有:http://cgm.cs.mcgill.ca/~godfried/teaching/projects97/azar/skeleton.html     Hilditch细化

            http://www.cnblogs.com/xiaotie/archive/2010/08/12/1797760.html    对Hilditch细化的改进版

                http://cgm.cs.mcgill.ca/~godfried/teaching/projects97/azar/skeleton.html  Rosenfeld细化

              https://github.com/opencv/opencv_contrib/blob/4.x/modules/ximgproc/src/thinning.cpp    Opencv的Zhang\guo细化

  我们尝试的看下了Hilditch细化以及改进版本的Hilditch细化算法,发现其在某一个行的计算过程中,有着严重的前后依赖,非常不利于SIMD指令的并行化,这里我们优化了Opencv的两个算子。

一、原始方案

在上述的Opencv代码的链接中,以Zhang细化算法为例,其核心代码如下所示:

if(thinningType == THINNING_ZHANGSUEN){
for (int i = 1; i < img.rows-1; i++) { for (int j = 1; j < img.cols-1; j++) { uchar p2 = img.at(i-1, j);
uchar p3 = img.at(i-1, j+1);
uchar p4 = img.at(i, j+1);
uchar p5 = img.at(i+1, j+1);
uchar p6 = img.at(i+1, j);
uchar p7 = img.at(i+1, j-1);
uchar p8 = img.at(i, j-1);
uchar p9 = img.at(i-1, j-1);

            int A  = (p2 == 0 && p3 == 1) + (p3 == 0 && p4 == 1) +  
                     (p4 == 0 && p5 == 1) + (p5 == 0 && p6 == 1) +  
                     (p6 == 0 && p7 == 1) + (p7 == 0 && p8 == 1) +  
                     (p8 == 0 && p9 == 1) + (p9 == 0 && p2 == 1);  
            int B  = p2 + p3 + p4 + p5 + p6 + p7 + p8 + p9;  
            int m1 = iter == 0 ? (p2 \* p4 \* p6) : (p2 \* p4 \* p8);  
            int m2 = iter == 0 ? (p4 \* p6 \* p8) : (p2 \* p6 \* p8);

            if (A == 1 && (B >= 2 && B <= 6) && m1 == 0 && m2 == 0)  
                marker.at<uchar>(i,j) = 1;  
        }  
    }  
}

  非常之简洁啊,简洁的没有朋友,也没有效率的。 这样的代码其实只适合于新手学习算法的原理。无法用于实际的项目的。

  可以明显的看出,A\B\m1\m2的判断并不要放在一起,而是可以分开,分开的话在很多的情况下后续的计算就可以不用做了,要知道,这是一个迭代的算法,而且通常要迭代几百次,因此,每一个迭代里能少一次计算量,整体下来的时间是非常可观的。

二、稍微改进版本

  我们稍微做一个脱离Opencv版本的代码版本:

int IM_Thinning_Zhangsuen_PureC(unsigned char *Src, unsigned char *Dest, int Width, int Height, int Stride)
{
int Channel = Stride / Width;
if ((Src == NULL) || (Dest == NULL)) return IM_STATUS_NULLREFRENCE;
if ((Width <= 0) || (Height <= 0)) return IM_STATUS_INVALIDPARAMETER;
if (Channel != 1) return IM_STATUS_INVALIDPARAMETER;
int Status = IM_STATUS_OK;
const int MaxIter = 2000;

unsigned char \*Clone = (unsigned char \*)calloc((Height + 2) \* (Width + 2), sizeof(unsigned char));  
unsigned short \*IndexX = (unsigned short \*)malloc(Width \* Height / 4 \* sizeof(unsigned short));  
unsigned short \*IndexY = (unsigned short \*)malloc(Width \* Height / 4 \* sizeof(unsigned short));  
if ((Clone == NULL) || (IndexX == NULL) || (IndexY == NULL))  
{  
    Status = IM\_STATUS\_OUTOFMEMORY;  
    goto FreeMemory;  
}

for (int Y = 0; Y < Height; Y++)  
{  
    unsigned char \*LinePS = Src + Y \* Stride;  
    unsigned char \*LinePD = Clone + (Y + 1) \* (Width + 2) + 1;  
    for (int X = 0; X < Width; X++)  
    {  
        LinePD\[X\] = LinePS\[X\] & 1;        //    全部量化为0和1两个数值  
    }  
}  
int Iter = 0;  
while (true)  
{  
    int Amount = 0;  
    for (int Y = 0; Y < Height; Y++)  
    {  
        unsigned char \*LinePF = Clone + Y \* (Width + 2) + 1;  
        unsigned char \*LinePS = Clone + (Y + 1) \* (Width + 2) + 1;  
        unsigned char \*LinePT = Clone + (Y + 2) \* (Width + 2) + 1;  
        for (int X = 0; X < Width; X++)  
        {  
            int P1 = LinePS\[X\];  
            if (P1 == 0)    continue;  
            // P9 P2 P3  
            // P8 P1 P4  
            // P7 P6 P5  
            //  
            //int P9 = LinePF\[X - 1\];  
            //int P2 = LinePF\[X\];  
            //int P3 = LinePF\[X + 1\];  
            //int P8 = LinePS\[X - 1\];  
            //int P4 = LinePS\[X + 1\];  
            //int P7 = LinePT\[X - 1\];  
            //int P6 = LinePT\[X\];  
            //int P5 = LinePT\[X + 1\];  
            //int Sum = P2 + P3 + P4 + P5 + P6 + P7 + P8 + P9;  
            //if ((Sum < 2) || (Sum > 6)) continue;                            

            int P2 = LinePF\[X\];  
            int P8 = LinePS\[X - 1\];  
            int P4 = LinePS\[X + 1\];  
            int P6 = LinePT\[X\];  
            int Sum = P2 + P8 + P4 + P6;  
            if (Sum == 4) continue;  
            int P3 = LinePF\[X + 1\];  
            int P9 = LinePF\[X - 1\];  
            int P5 = LinePT\[X + 1\];  
            int P7 = LinePT\[X - 1\];  
            Sum += P3 + P5 + P7 + P9;  
            if (Sum < 2) continue;                             

            int Count = 0;

            if ((P2 == 0) && (P3 == 1))        Count++;  
            if ((P3 == 0) && (P4 == 1))        Count++;  
            if ((P4 == 0) && (P5 == 1))        Count++;  
            if ((P5 == 0) && (P6 == 1))        Count++;  
            if ((P6 == 0) && (P7 == 1))        Count++;  
            if ((P7 == 0) && (P8 == 1))        Count++;  
            if ((P8 == 0) && (P9 == 1))        Count++;  
            if ((P9 == 0) && (P2 == 1))        Count++;

            if ((Count == 1) && ((P2 & P4 & P6) == 0) && ((P4 & P6 & P8) == 0))  
            {  
                IndexX\[Amount\] = X;  
                IndexY\[Amount\] = Y;  
                Amount++;  
            }  
        }  
    }

    if (Amount == 0) break;  
    for (int Y = 0; Y < Amount; Y++)  
    {  
        Clone\[(IndexY\[Y\] + 1) \* (Width + 2) + IndexX\[Y\] + 1\] = 0;  
    }

    Amount = 0;  
    for (int Y = 0; Y < Height; Y++)  
    {  
      //  后续的第二次循环,仅仅是几个变量判断不一样,自行添加  
    }  
    if (Amount == 0)    break;  
    for (int Y = 0; Y < Amount; Y++)  
    {  
        Clone\[(IndexY\[Y\] + 1) \* (Width + 2) + IndexX\[Y\] + 1\] = 0;  
    }  
    Iter++;  
    if (Iter >= MaxIter)    break;  
}  
for (int Y = 0; Y < Height; Y++)  
{  
    unsigned char \*LinePD = Dest + Y \* Stride;  
    unsigned char \*LinePS = Clone + (Y + 1) \* (Width + 2) + 1;

    for (int X = 0; X < Width; X++)  
    {  
        LinePD\[X\] = LinePS\[X\] == 1 ? 255 : 0;  
    }  
}  

FreeMemory:
if (Clone != NULL) free(Clone);
if (IndexX != NULL) free(IndexX);
if (IndexY != NULL) free(IndexY);
return Status;
}

  几个方面的改进和改动:

1、使用了一个扩展边界的图像(高度和宽度在四周各扩散一个像素,类似于哨兵边界),用于减少每次取3*3领域时的边界判断。这个虽然占用了内存,但是可以很大的提高速度。

2、把A1\B\M的判断分开写,这样可以让有些循环提前退出,提高速度。

3、没有使用Vector,直接使用数组保存哪些需要改变值的位置(因为计算量小,对速度基本没有影响)。

4、对B的判断分了2步走,可以稍微提高下速度。

  我们选择下面这个测试图:

  

  一副1024*1024大小的测试图,在我本机上上述测试代码的平均耗时大约是180ms,这个速度谈不上快。

三、再次改进版本

在我们进行SIMD优化前,我们还尝试了从算法层面上的另外一种优化。

  我们知道,在细化的算法中,本身已经是背景的像素是不要参与计算的,也就是上述代码中if (P1 == 0) continue; 的含义,那么如果没在迭代前,就算好了哪些部位不要计算,是不是迭代后就可以直接计算那些需要计算部分呢,这样就可以少了很多判断,虽然只是一个判断,但是在全图里如果有50%是背景,那就意味着要进行W*H/2判断的,再加上这个是在迭代里进行判断,计算量也是相当可观的。

  这个实现计算好哪些是前景的工作,针对二值图像,其实就是类似于传统的RLE行程编码,我们计算出每行前景的起点终点,等等。这个算法大家自行去研究。

  如果在每次迭代前都进行这个RLE行程编码,那也会带来新的问题,因为行程编码也是全图处理,也是一个需要时间的工作,那这样后续带来的速度优化反而会被行程编码给抵消甚至导致减速。但是,如果只进行迭代前的一次编码,随着迭代的进行,更多的像素被判定为背景,之前计算的行程编码里已经有很多是不需要计算的了, 为了解决这个矛盾,一个建议的处理方法就是,每个若干次迭代,更新下行程编码的结果,比如20次或者50次,这样的话,即不会因为行程编码的耗时影响整体速度,又在一定程度上逐次的减少了计算量。

  相关代码如下所示:

int IM_Thinning_Zhangsuen_PureC_Opt(unsigned char *Src, unsigned char *Dest, int Width, int Height, int Stride)
{
int Channel = Stride / Width;
if ((Src == NULL) || (Dest == NULL)) return IM_STATUS_NULLREFRENCE;
if ((Width <= 0) || (Height <= 0)) return IM_STATUS_INVALIDPARAMETER;
if (Channel != 1) return IM_STATUS_INVALIDPARAMETER;
int Status = IM_STATUS_OK;
const int MaxIter = 2000;

unsigned char \*Clone = (unsigned char \*)calloc((Height + 2) \* (Width + 2), sizeof(unsigned char));  
unsigned short \*IndexX = (unsigned short \*)malloc(Width \* Height / 4 \* sizeof(unsigned short));  
unsigned short \*IndexY = (unsigned short \*)malloc(Width \* Height / 4 \* sizeof(unsigned short));  
RLE\_Line \*RL\_H = (RLE\_Line \*)malloc((Height + 2) \* sizeof(RLE\_Line));

if ((Clone == NULL) || (IndexX == NULL) || (IndexY == NULL) || (RL\_H == NULL))  
{  
    Status = IM\_STATUS\_OUTOFMEMORY;  
    goto FreeMemory;  
}

for (int Y = 0; Y < Height; Y++)  
{  
    unsigned char \*LinePS = Src + Y \* Stride;  
    unsigned char \*LinePD = Clone + (Y + 1) \* (Width + 2) + 1;  
    for (int X = 0; X < Width; X++)  
    {  
        LinePD\[X\] = LinePS\[X\] & 1;  
    }  
}  
Status = IM\_GetMaskRLE\_Hori(Clone, Width + 2, Height + 2, Width + 2, RL\_H);  
if (Status != IM\_STATUS\_OK)    goto FreeMemory;  
int Iter = 0;  
while (true)  
{  
    if (Iter % 50 == 0)    //    每迭代50次更细一下  
    {  
        for (int Z = 0; Z < Height + 2; Z++)  
        {  
            if ((RL\_H\[Z\].Amount != 0) && (RL\_H\[Z\].SE != NULL))    free(RL\_H\[Z\].SE);  
        }  
        Status = IM\_GetMaskRLE\_Hori(Clone, Width + 2, Height + 2, Width + 2, RL\_H);  
    }  
    int Amount = 0;  
    for (int Y = 0; Y < Height; Y++)  
    {  
        unsigned char \*LinePF = Clone + Y \* (Width + 2) + 1;  
        unsigned char \*LinePS = Clone + (Y + 1) \* (Width + 2) + 1;  
        unsigned char \*LinePT = Clone + (Y + 2) \* (Width + 2) + 1;  
        for (int K = 0; K < RL\_H\[Y + 1\].Amount; K++)  
        {  
            for (int X = RL\_H\[Y + 1\].SE\[K\].Start - 1 ; X <= RL\_H\[Y + 1\].SE\[K\].End - 1; X++)  
            {  
                int P1 = LinePS\[X\];  
                if (P1 == 0)    continue;  
                // P9 P2 P3  
                // P8 P1 P4  
                // P7 P6 P5                          // 条件3:至少有两个是前景点

                int P2 = LinePF\[X\];  
                int P8 = LinePS\[X - 1\];  
                int P4 = LinePS\[X + 1\];  
                int P6 = LinePT\[X\];  
                int Sum = P2 + P8 + P4 + P6;  
                if (Sum == 4) continue;                             // 条件2: P1,P3,P5,P7不全部为前景点  
                int P3 = LinePF\[X + 1\];  
                int P9 = LinePF\[X - 1\];  
                int P5 = LinePT\[X + 1\];  
                int P7 = LinePT\[X - 1\];                    // 以方便计算8连通联结数。  
                Sum += P3 + P5 + P7 + P9;  
                if (Sum < 2) continue;                              // 条件3:至少有两个是前景点

                int Count = 0;

                if ((P2 == 0) && (P3 == 1))        Count++;  
                if ((P3 == 0) && (P4 == 1))        Count++;  
                if ((P4 == 0) && (P5 == 1))        Count++;  
                if ((P5 == 0) && (P6 == 1))        Count++;  
                if ((P6 == 0) && (P7 == 1))        Count++;  
                if ((P7 == 0) && (P8 == 1))        Count++;  
                if ((P8 == 0) && (P9 == 1))        Count++;  
                if ((P9 == 0) && (P2 == 1))        Count++;

                if ((Count == 1) && ((P2 & P4 & P6) == 0) && ((P4 & P6 & P8) == 0))  
                {  
                    IndexX\[Amount\] = X;  
                    IndexY\[Amount\] = Y;  
                    Amount++;  
                }  
            }  
        }  
    }  
    if (Amount == 0) break;  
    for (int Y = 0; Y < Amount; Y++)  
    {  
        Clone\[(IndexY\[Y\] + 1) \* (Width + 2) + IndexX\[Y\] + 1\] = 0;  
    }  
    Amount = 0;  
    for (int Y = 0; Y < Height; Y++)  
    {  
        // 后续的第二次循环,自行添加  
    }  
    if (Amount == 0) break;  
    for (int Y = 0; Y < Amount; Y++)  
    {  
        Clone\[(IndexY\[Y\] + 1) \* (Width + 2) + IndexX\[Y\] + 1\] = 0;  
    }  
    Iter++;  
    if (Iter >= MaxIter)    break;  
}  
for (int Y = 0; Y < Height; Y++)  
{  
    unsigned char \*LinePD = Dest + Y \* Stride;  
    unsigned char \*LinePS = Clone + (Y + 1) \* (Width + 2) + 1;

    for (int X = 0; X < Width; X++)  
    {  
        LinePD\[X\] = LinePS\[X\] == 1 ? 255 : 0;  
    }  
}  

FreeMemory:
if (Clone != NULL) free(Clone);
if (IndexX != NULL) free(IndexX);
if (IndexY != NULL) free(IndexY);
if (RL_H != NULL)
{
for (int Z = 0; Z < Height + 2; Z++)
{
if ((RL_H[Z].Amount != 0) && (RL_H[Z].SE != NULL)) free(RL_H[Z].SE);
}
free(RL_H);
}
return Status;
}

  同样的图像,速度可以提高到55ms,有将近3倍额速度提高。

  不过这里的提速比例不是很固定的,对于不同的类型的图像结果不禁相同,对于那些有大块连续的二值图,提速就越明显,而对于毫无规律的随机图,可能就不是很明显了。

四、SSE改进版本

     上述改进版本还可以通过SIMD指令进一步优化,类似于我在Sobel优化里使用的方法,我们一次性加载16个字节以及他周边的8个位置连续的16个字节,但是核心的技巧在于如何实现那些分支预测,特别是continue。

  因为一次性加载了16个像素,在利用了行程编码后,案例说这16个字节都是需要进行处理的目标,但是由于前述不是每次迭代都要更新行程编码的缘故,总会有部分是无效像素,也有可能是全部的无效像素,因此,我们处理的代码中就可能同时存在前景和背景,但是对于背景我们是不需要处理的,而前景像素也有可能在中间条件判断时退出循环,但是对于SIMD质量来说,他无法局部退出,要么大家一起计算,要么大家一起退出,因此,我们必须将须有的计算都完成,而不能提前退出,但是有一点特殊的就是,如果所有的像素都已经满足了某个提提前退出的条件,那也是可以退出的。

因为我们知道,在每次迭代时,对于前景中哪些大块的范围,其中间的区域都是不满足要改变的条件的,也就是说在上面的 if (Sum == 4) continue;     if (Sum < 2) continue;  就满足了退出条件,因此利用SIMD一次性就可以做16次判断和计算。这样也可以提高速度。

\_\_m128i P1 = \_mm\_loadu\_si128((\_\_m128i \*)(LinePS + X));  
\_\_m128i FlagA = \_mm\_cmpeq\_epi8(P1, \_mm\_setzero\_si128());            //    全部为0,退出  
if (\_mm\_movemask\_epi8(FlagA) == 65535)    continue;

\_\_m128i Flag = \_mm\_andnot\_si128(FlagA, \_mm\_set1\_epi8(255));            //    记录下那些是不为0的

\_\_m128i P2 = \_mm\_loadu\_si128((\_\_m128i \*)(LinePF + X + 0));  
\_\_m128i P8 = \_mm\_loadu\_si128((\_\_m128i \*)(LinePS + X - 1));  
\_\_m128i P4 = \_mm\_loadu\_si128((\_\_m128i \*)(LinePS + X + 1));  
\_\_m128i P6 = \_mm\_loadu\_si128((\_\_m128i \*)(LinePT + X + 0));

\_\_m128i Sum = \_mm\_add\_epi8(\_mm\_add\_epi8(P2, P4), \_mm\_add\_epi8(P6, P8));  
\_\_m128i FlagB = \_mm\_cmpeq\_epi8(Sum, \_mm\_set1\_epi8(4));  
if (\_mm\_movemask\_epi8(FlagB) == 65535)    continue;                    //    全部都等于4,退出  
Flag = \_mm\_andnot\_si128(FlagB, Flag);                                //    记录下那些不为0,且Sum不等于4的

\_\_m128i P9 = \_mm\_loadu\_si128((\_\_m128i \*)(LinePF + X - 1));  
\_\_m128i P3 = \_mm\_loadu\_si128((\_\_m128i \*)(LinePF + X + 1));  
\_\_m128i P7 = \_mm\_loadu\_si128((\_\_m128i \*)(LinePT + X - 1));  
\_\_m128i P5 = \_mm\_loadu\_si128((\_\_m128i \*)(LinePT + X + 1));

Sum = \_mm\_add\_epi8(Sum, \_mm\_add\_epi8(\_mm\_add\_epi8(P9, P3), \_mm\_add\_epi8(P7, P5)));  
\_\_m128i FlagC = \_mm\_cmplt\_epi8(Sum, \_mm\_set1\_epi8(2));  
if (\_mm\_movemask\_epi8(FlagC) == 65535)    continue;                    //    全部都小于2,退出  
Flag = \_mm\_andnot\_si128(FlagC, Flag);                                //    记录下那些不为0,且Sum不等于4的,后续的Sum小于2的

\_\_m128i Count = \_mm\_setzero\_si128();

Count = \_mm\_sub\_epi8(Count, \_mm\_and\_si128(\_mm\_cmpeq\_epi8(P2, Zero), \_mm\_cmpeq\_epi8(P3, One)));  
Count = \_mm\_sub\_epi8(Count, \_mm\_and\_si128(\_mm\_cmpeq\_epi8(P3, Zero), \_mm\_cmpeq\_epi8(P4, One)));  
Count = \_mm\_sub\_epi8(Count, \_mm\_and\_si128(\_mm\_cmpeq\_epi8(P4, Zero), \_mm\_cmpeq\_epi8(P5, One)));  
Count = \_mm\_sub\_epi8(Count, \_mm\_and\_si128(\_mm\_cmpeq\_epi8(P5, Zero), \_mm\_cmpeq\_epi8(P6, One)));  
Count = \_mm\_sub\_epi8(Count, \_mm\_and\_si128(\_mm\_cmpeq\_epi8(P6, Zero), \_mm\_cmpeq\_epi8(P7, One)));  
Count = \_mm\_sub\_epi8(Count, \_mm\_and\_si128(\_mm\_cmpeq\_epi8(P7, Zero), \_mm\_cmpeq\_epi8(P8, One)));  
Count = \_mm\_sub\_epi8(Count, \_mm\_and\_si128(\_mm\_cmpeq\_epi8(P8, Zero), \_mm\_cmpeq\_epi8(P9, One)));  
Count = \_mm\_sub\_epi8(Count, \_mm\_and\_si128(\_mm\_cmpeq\_epi8(P9, Zero), \_mm\_cmpeq\_epi8(P2, One)));

\_\_m128i P246 = \_mm\_and\_si128(\_mm\_and\_si128(P2, P4), P6);  
\_\_m128i P468 = \_mm\_and\_si128(\_mm\_and\_si128(P4, P6), P8);

\_\_m128i FlagD = \_mm\_and\_si128(\_mm\_cmpeq\_epi8(Count, One), \_mm\_and\_si128(\_mm\_cmpeq\_epi8(P246, Zero), \_mm\_cmpeq\_epi8(P468, Zero)));  
if (\_mm\_movemask\_epi8(FlagD) == 0)    continue;

Flag = \_mm\_and\_si128(FlagD, Flag);

//    用Flag.m128i\_u8或者写入到一个临时数组里速度没啥区别  
if (\_mm\_extract\_epi8(Flag, 0) == 255)  
{  
    IndexX\[Amount\] = X + 0;  
    IndexY\[Amount\] = Y;  
    Amount++;  
}  

if (\_mm\_extract\_epi8(Flag, 1) == 255)  
{  
    IndexX\[Amount\] = X + 1;  
    IndexY\[Amount\] = Y;  
    Amount++;  
}  

  /////////////////////////////////////////////////  

////////////////////////////////////////////////////////////////

  上面的细节有几个地方值的学习。

  第一、_mm_movemask_epi8的使用,这个我在很多场合下都提过,可用于批量判断一个SIMD寄存器里的状态。本例只用他做判断是否SSE寄存器都符合某一个指标。

第二、Flag 变量的作用,Flag用于来记录下满足所有条件的像素,这样才能知道经过多个判断后最终还剩下那些像素需要真正的处理。其中_mm_andnot_si128也是一个灵活的应用。

  第三、if ((P2 == 0) && (P3 == 1)) Count++;   这样的语句如果直接翻译到SSE代码,是比较麻烦的(可以使用_mm_blendv_si128),我这里巧妙的使用了u8和i8数据类型的特点,u8的255就对应了i8的-1,0还是对应0,然后加法就可以变为减法了。

    第四、填写IndeX和IndexY的过程确实是无法用SIMD指令实现的,这里只能去拆解SIMD变量,这个有几个方法,一个就是用想本例中直接使用_mm_extract_epi8,另外一种方式可以是使用SIMD变量的m128i_u8成员,但是这个有可能对性能有所影响。

  使用SIMD优化后,上述相同的图片大概耗时在28ms左右,速度有进一步的提高。

五、其他说明

  虽然较原始版本速度有较大的提高,但是和商业软件相比,还是有很大的差距,人家这个图用时5ms,直接悲剧。

  至少目前从公开的资料中还没有看到halcon所用的算法的为什么这么快,待有缘了在研究这个算法吧。

  另外,halcon的计算结果和opencv的GulHALL的结果比较类似,但是那个算法要比Zhang还要慢。

  当然,CV自带的这两个算法是可以并行的,当然这里的必行是指迭代内部的并行,而不是迭代之间的并行,但是由于每次迭代的计算量相对于来说比较小,这种并行对CPU级别的线程来说是不太划算的,但是GPU级别的还是很友好的。不过HALONC这个算法可没有用GPU哦。

  测试Demo: Zhang 以及  Guo 图像细化

  

如果想时刻关注本人的最新文章,也可关注公众号或者添加本人微信:  laviewpbt

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章