• 腐蚀膨胀的快速实现


    腐蚀、膨胀作为一种简单、基础的形态学操作,我之前没有过多的关注,直到最近发现OpenCV的实现要比自己的实现快几十倍,才进行了深入研究,发现这个操作也并没有想象中的那么简单。

    0.准备工作

    一般来说,腐蚀和膨胀都是基于二值图像做的,因此我把经典的lena.jpg转换成了二值图像,用于测试效果和性能。代码如下:

        //convert a RGB image to binary
        Mat image=imread("/Users/lena.jpg");
        Mat gray(image.cols,image.rows,CV_8UC1);
        cvtColor(image,gray,CV_BGR2GRAY);
        Mat binary(image.cols,image.rows,CV_8UC1);
        for(int i=0;i<gray.cols*gray.rows*gray.channels();++i)
        {
            if(gray.data[i]>128)
                binary.data[i]=255;
            else
                binary.data[i]=0;
        }
        imshow("lena",binary);
        waitKey(0);

    转换后的图片如下:

    1.OpenCV函数接口及其实现方式

    OpenCV中有dilate和erode两个函数用于膨胀和腐蚀,调用方法如下:

        //opencv dilate&erode functions
        Mat dilateImg(image.cols,image.rows,CV_8UC1);
        Mat erodeImg(image.cols,image.rows,CV_8UC1);
        double dur;
        clock_t start,end;
        
        start = clock();
        dilate(binary,dilateImg,Mat(),cv::Point(-1,-1),5);
        end = clock();
        dur = (double)(end - start);
        printf("OpenCV dilate Use Time:%f ms
    ",(dur/CLOCKS_PER_SEC*1000));
        
        start = clock();
        erode(binary,erodeImg,Mat(),cv::Point(-1,-1),5);
        end = clock();
        dur = (double)(end - start);
        printf("OpenCV erode Use Time:%f ms
    ",(dur/CLOCKS_PER_SEC*1000));
        
        imshow("dilate",dilateImg);
        waitKey(0);
        imshow("erode",erodeImg);
        waitKey(0);

    在腐蚀膨胀时,均使用了半径/迭代次数=5。在Release模式下,在我的电脑上运行时间大概在0.35-0.5ms之间。

    实际上,OpenCV提供的erode和dilate的实现均在/opencv-2.4.11/modules/imgproc/src/morph.cpp这个文件中,腐蚀和膨胀操作只是针对的像素不同而已,原理是一样的。最终调用的是morphOp这个函数,这个函数又使用

    MorphologyRunner来完成真正的操作,而MorphologyRunner又使用了Ptr<FilterEngine>。总之,代码写的错综复杂,但基本可以确定的是,OpenCV是利用多线程和SSE优化来完成的腐蚀膨胀操作。

     

    2.自己实现v1

    假设做的是膨胀(腐蚀其实同理),考虑最简单的思路,有两种:

    1.遍历每个像素,如果该像素值不是0,则其k*k临域内的所有像素,将被设置成和该像素相同的值(边缘像素需要单独考虑)。

    2.遍历每个像素,如果该像素值不是0,或者如果该像素k*k临域内,有像素值不为0的,则将该像素值设置成非0值。

    设图像宽高分别为w,h,膨胀半径/膨胀迭代次数为k。则以上思路时间复杂度为:o(w*h*k*k)。空间复杂度o(w*h)

    下面是实现代码,因为膨胀和腐蚀的原理相同,因此后面的代码都是实现膨胀算法。

    void myDilateV1(Mat&input,Mat&output,int radius)
    {
        for(int i=0;i<input.rows;++i)
        {
            for(int j=0;j<input.cols;++j)
            {
                if(input.data[i*input.cols+j]==255)
                {
                    int startX=0>j-radius?0:j-radius;
                    int startY=(i-radius)>0?(i-radius):0;
                    int endX=input.cols-1<j+radius?input.cols-1:j+radius;
                    int endY=(i+radius)<input.rows?(i+radius):(input.rows-1);
                    for(int m=startY;m<=endY;++m)
                    {
                        for(int n=startX;n<=endX;++n)
                        {
                            output.data[m*input.cols+n]=255;
                        }
                    }
                }
            }
        }
    }

    在我的电脑上运行大概13-15ms之间,比openCV的实现大概慢了30倍。

    3.自己实现v2

    把每个像素膨胀的区域先设置成另一个值,例如128,遍历完整个图像后,再把这些128值得像素设置成255,这样就可以在原始图像上做膨胀了,代码如下:

    void myDilateV2(Mat&input,int radius)
    {
        for(int i=0;i<input.rows;++i)
        {
            for(int j=0;j<input.cols;++j)
            {
                if(input.data[i*input.cols+j]==255)
                {
                    int startX=0>j-radius?0:j-radius;
                    int startY=(i-radius)>0?(i-radius):0;
                    int endX=input.cols-1<j+radius?input.cols-1:j+radius;
                    int endY=(i+radius)<input.rows?(i+radius):(input.rows-1);
                    for(int m=startY;m<=endY;++m)
                    {
                        for(int n=startX;n<=endX;++n)
                        {
                            if(input.data[m*input.cols+n]==0)
                                input.data[m*input.cols+n]=128;
                        }
                    }
                }
            }
        }
        
        for(int i=0;i<input.rows;++i)
        {
            for(int j=0;j<input.cols;++j)
            {
                if(input.data[i*input.cols+j]==128)
                    input.data[i*input.cols+j]=255;
            }
        }
    }

    时间复杂度没变,运行耗时减慢到了18-20ms,空间复杂度变成了o(1)

    4.自己实现v3

    换个思路,以k为半径做膨胀,可以转换为连续k次半径为1的膨胀,基于v2版本,修改代码如下:

    void myDilateBy1(Mat&input)
    {
        for(int i=0;i<input.rows;++i)
        {
            for(int j=0;j<input.cols;++j)
            {
                if(input.data[i*input.cols+j]==255)
                {
                    int startX=0>j-1?0:j-1;
                    int startY=(i-1)>0?(i-1):0;
                    int endX=input.cols-1<j+1?input.cols-1:j+1;
                    int endY=(i+1)<input.rows?(i+1):(input.rows-1);
                    for(int m=startY;m<=endY;++m)
                    {
                        for(int n=startX;n<=endX;++n)
                        {
                            if(input.data[m*input.cols+n]==0)
                                input.data[m*input.cols+n]=128;
                        }
                    }
                }
            }
        }
        
        for(int i=0;i<input.rows;++i)
        {
            for(int j=0;j<input.cols;++j)
            {
                if(input.data[i*input.cols+j]==128)
                    input.data[i*input.cols+j]=255;
            }
        }
    }
    
    void myDilateV3(Mat&input,int radius)
    {
        for(int i=0;i<radius;++i)
        {
            myDilateBy1(input);
        }
    }

    时间复杂度还是没变,运行时间14-16ms

    5.自己实现v4

    看到这里,大家应该发现,一直按照之前的思路做下去是不行的,必须的换个角度思考问题了。假设这么一种情况:如果我们预先知道每个像素和离它最近的值为255的像素间的距离,那么我们只需要遍历每个像素,判断距离是否大于给定的radius,就可以完成膨胀算法,时间复杂度只有o(w*h)。那么这个预先知道的信息能不能获得呢?

    先考虑一维情况,假设有一个长度为n的一维数组,我先从左到右遍历一遍,寻找某个元素和它所有左边值为255像素的最近距离,并保存下来,然后我们再从右到左遍历一遍,寻找某个元素和它所有右边值为255像素的最近距离,如果比第一遍的值小,那么我们就更新。这样两遍下来,上面说的预先知道的信息就得到了。推广到二维情况,代码如下:

    void myDilateV4(Mat&src,Mat&dst,int radius)
    {
        int i, j;
        unsigned char maxDist=254;
        //step1 forward
        //the top left pixel
        dst.data[0]=(src.data[0]==255)?0:maxDist;
        //the first row
        for (i=1;i<src.cols;++i)
        {dst.data[i]=(src.data[i]==255)?0:MIN(maxDist,dst.data[i-1]+1);}
        //rest pixles
        for (i=1;i<src.rows;++i) // traverse from top left to bottom right
        {
            //left-most pixel
            dst.data[i*src.cols]=(src.data[i*src.cols]==255)?0:MIN(MIN(dst.data[(i-1)*src.cols],dst.data[(i-1)*src.cols+1])+1,maxDist);
            for (j=1;j<src.cols-1;++j)
            {
                if (src.data[i*src.cols+j]==255)
                {
                    dst.data[i*src.cols+j]=0;//first pass and pixel was on, it gets a zero
                }
                else
                {
                    // pixel was off
                    dst.data[i*src.cols+j] = MIN(maxDist, dst.data[(i-1)*src.cols+j]+1);
                    dst.data[i*src.cols+j] = MIN(MIN(dst.data[i*src.cols+j-1]+1,dst.data[i*src.cols+j]),MIN(dst.data[(i-1)*src.cols+j-1]+1, dst.data[(i-1)*src.cols+j+1]+1));
                }
            }
            //right-most pixle
            dst.data[(i+1)*src.cols-1]=(src.data[(i+1)*src.cols-1]==255)?0:MIN
            (MIN(dst.data[i*src.cols-1]+1,dst.data[i*src.cols-2]+1),
             MIN(dst.data[(i+1)*src.cols-2]+1,maxDist));
        }
        //step2 backward
        //    the bottom right pixel
        dst.data[src.cols*src.rows-1]=(src.data[src.cols*src.rows-1]==255)?0:dst.data[src.cols*src.rows-1];
        //the last row
        for (i=src.cols-2;i>=0;--i)
        {dst.data[(src.rows-1)*src.cols+i]=(src.data[(src.rows-1)*src.cols+i]==255)?0:MIN(dst.data[(src.rows-1)*src.cols+i],dst.data[(src.rows-1)*src.cols+i+1]+1);}
        //rest pixles
        for (i=src.rows-2; i>=0; --i){
            //right-most pixel
            dst.data[(i+1)*src.cols-1]=(src.data[(i+1)*src.cols-1]==255)?0:MIN(MIN(dst.data[(i+2)*src.cols-1],dst.data[(i+2)*src.cols-2])+1,dst.data[(i+1)*src.cols-1]);
            for (j=src.cols-2; j>0; --j)
            {
                if(src.data[i*src.cols+j]!=255)
                {
                    dst.data[i*src.cols+j] = MIN(dst.data[i*src.cols+j], dst.data[i*src.cols+j+1]+1);
                    dst.data[i*src.cols+j] = MIN(
                                         MIN(dst.data[i*src.cols+j], dst.data[(i+1)*src.cols+j]+1),
                                         MIN(dst.data[(i+1)*src.cols+j+1]+1, dst.data[(i+1)*src.cols+j-1]+1)
                                         );
                }
            }
            //left-most pixel
            dst.data[i*src.cols]=(src.data[i*src.cols]==255)?0:MIN
            (MIN(dst.data[i*src.cols+1],dst.data[(i+1)*src.cols])+1,
             MIN(dst.data[(i+1)*src.cols+1]+1,dst.data[i*src.cols]));
        }
        for (i=0;i<src.rows;++i)
        {
            for (j=0;j<src.cols;++j)
            {
                dst.data[i*src.cols+j] = ((dst.data[i*src.cols+j]<=radius)?255:src.data[i*src.cols+j]);
            }
        }
    }

    在我的电脑上耗时大概2-3ms,空间复杂度o(w*h)。虽然还是比opencv慢了5-6倍,但比之前几个版本的实现快了5倍以上。

    参考资料:

    http://blog.ostermiller.org/dilate-and-erode

  • 相关阅读:
    正当防卫与互殴的界限在哪里
    [php入门] 5、初学CSS从中记下的一些基础点(For小白)
    [ZigBee] 13、ZigBee基础阶段性回顾与加深理解——用定时器1产生PWM来控制LED亮度(七色灯)
    [ZigBee] 12、ZigBee之看门狗定时器——饿了就咬人的GOOD DOG
    [ZigBee] 11、ZigBee之睡眠定时器二
    [ZigBee] 10、ZigBee之睡眠定时器
    [ZigBee] 9、ZigBee之AD剖析——AD采集CC2530温度串口显示
    [ZigBee] 8、ZigBee之UART剖析·二(串口收发)
    [php入门] 4、HTML基础入门一篇概览
    [ZigBee] 2、 ZigBee开发环境搭建
  • 原文地址:https://www.cnblogs.com/hrlnw/p/5044402.html
Copyright © 2020-2023  润新知