美文网首页
OpenCV源码分析(四):中值滤波

OpenCV源码分析(四):中值滤波

作者: 奔向火星005 | 来源:发表于2018-12-10 20:14 被阅读0次

    注:为方便分析和解释,文章中的代码是OpenCV源码的简化版,会略去源码中一些如断言,OpenCL,硬件加速等代码。因此详情最好参看源码。

    中值滤波对于去除与原图像差异较大的噪声有很好的效果,因为它是通过查找邻域中的所有像素的中值作为目标像素,而噪声被选中的概率很小。

    先看下效果:


    左边图像加了椒盐噪声,右图为中值滤波处理后的图像,可见效果不错。

    OpenCV的中值滤波的接口为为:

    void cv::medianBlur( InputArray _src0, OutputArray _dst, int ksize );
    

    可以看出接口非常简单,_src0为输入,_dst为输出,ksize为滤波窗口大小。下面以ksize为5为例分析源码。

    void cv::medianBlur( InputArray _src0, OutputArray _dst, int ksize )
    {
        Mat src0 = _src0.getMat();
        _dst.create( src0.size(), src0.type() );
        Mat dst = _dst.getMat();
    
        bool useSortNet = ksize == 3 || (ksize == 5
    #if !(CV_SIMD128)
                && ( src0.depth() > CV_8U || src0.channels() == 2 || src0.channels() > 4 )
    #endif
            );
    
        Mat src;
        if( useSortNet )
        {
            if( dst.data != src0.data )
                src = src0;
            else
                src0.copyTo(src);
    
            if( src.depth() == CV_8U )  //图像元素为字节
                medianBlur_SortNet<MinMax8u, MinMaxVec8u>( src, dst, ksize );
            else if( src.depth() == CV_16U )
                medianBlur_SortNet<MinMax16u, MinMaxVec16u>( src, dst, ksize );
            else if( src.depth() == CV_16S )
                medianBlur_SortNet<MinMax16s, MinMaxVec16s>( src, dst, ksize );
            else if( src.depth() == CV_32F )
                medianBlur_SortNet<MinMax32f, MinMaxVec32f>( src, dst, ksize );
            else
                CV_Error(CV_StsUnsupportedFormat, "");
    
            return;
        }
        else
        {
            //省略...
        }
    }
    

    当输入ksize选5,图像每个元素为uchar类型时,会调用medianBlur_SortNet<MinMax8u, MinMaxVec8u>( src, dst, ksize )函数,下面我们看下它的实现:

    template<class Op, class VecOp>
    static void
    medianBlur_SortNet( const Mat& _src, Mat& _dst, int m )
    {
        typedef typename Op::value_type T;
        typedef typename Op::arg_type WT;
        typedef typename VecOp::arg_type VT;
    
        const T* src = _src.ptr<T>();  //const unsigned char*
        T* dst = _dst.ptr<T>();
        int sstep = (int)(_src.step/sizeof(T));  //2250
        int dstep = (int)(_dst.step/sizeof(T));
        Size size = _dst.size();  //750,1024
        int i, j, k, cn = _src.channels();  //cn == 3
        Op op;
        VecOp vop;
        volatile bool useSIMD = hasSIMD128();
    
        if( m == 3 )
        {
            //省略...
        }
        else if( m == 5 )
        {
            if( size.width == 1 || size.height == 1 )  //跳过
            {
                int len = size.width + size.height - 1;
                int sdelta = size.height == 1 ? cn : sstep;
                int sdelta0 = size.height == 1 ? 0 : sstep - cn;
                int ddelta = size.height == 1 ? cn : dstep;
    
                for( i = 0; i < len; i++, src += sdelta0, dst += ddelta )
                    for( j = 0; j < cn; j++, src++ )
                    {
                        int i1 = i > 0 ? -sdelta : 0;
                        int i0 = i > 1 ? -sdelta*2 : i1;
                        int i3 = i < len-1 ? sdelta : 0;
                        int i4 = i < len-2 ? sdelta*2 : i3;
                        WT p0 = src[i0], p1 = src[i1], p2 = src[0], p3 = src[i3], p4 = src[i4];
    
                        op(p0, p1); op(p3, p4); op(p2, p3); op(p3, p4); op(p0, p2);
                        op(p2, p4); op(p1, p3); op(p1, p2);
                        dst[j] = (T)p2;
                    }
                return;
            }
    
            size.width *= cn;
            for( i = 0; i < size.height; i++, dst += dstep )
            {
                const T* row[5];  //row[5]的每个元素指向一行
                row[0] = src + std::max(i - 2, 0)*sstep;   //当i小于2时,说明是上边界,直接用src+0位置的像素
                row[1] = src + std::max(i - 1, 0)*sstep;   //当i小于2时,~
                row[2] = src + i*sstep;
                row[3] = src + std::min(i + 1, size.height-1)*sstep;
                row[4] = src + std::min(i + 2, size.height-1)*sstep;
                int limit = useSIMD ? cn*2 : size.width;  //如果使用useSIMD,则需要先单组对比cn*2个数据,因为开头cn*2 个数据实际上是补边数据(它实际上是不存在的,是利用指针指向边界内的数据的),而vop操作(useSIMD)需要一次加载16个连续内存数据,不能在边界进行。
    
                for(j = 0;; )
                {
                    for( ; j < limit; j++ )
                    {
                        WT p[25];  //5x5的窗口,WT是
                        //没有j2?因为j就是j2
                        int j1 = j >= cn ? j - cn : j;
                        int j0 = j >= cn*2 ? j - cn*2 : j1;
                        int j3 = j < size.width - cn ? j + cn : j;
                        int j4 = j < size.width - cn*2 ? j + cn*2 : j3;
                        for( k = 0; k < 5; k++ )  //把5x5窗口内的数据赋给p[0]~p[24]
                        {
                            const T* rowk = row[k];
                            p[k*5] = rowk[j0]; p[k*5+1] = rowk[j1];
                            p[k*5+2] = rowk[j]; p[k*5+3] = rowk[j3];
                            p[k*5+4] = rowk[j4];
                        }
    
                        //op是优化过的比较函数,将较小的赋给第一个参数,较大的赋给第二个参数
                        op(p[1], p[2]); op(p[0], p[1]); op(p[1], p[2]); op(p[4], p[5]); op(p[3], p[4]);
                        op(p[4], p[5]); op(p[0], p[3]); op(p[2], p[5]); op(p[2], p[3]); op(p[1], p[4]);
                        op(p[1], p[2]); op(p[3], p[4]); op(p[7], p[8]); op(p[6], p[7]); op(p[7], p[8]);
                        op(p[10], p[11]); op(p[9], p[10]); op(p[10], p[11]); op(p[6], p[9]); op(p[8], p[11]);
                        op(p[8], p[9]); op(p[7], p[10]); op(p[7], p[8]); op(p[9], p[10]); op(p[0], p[6]);
                        op(p[4], p[10]); op(p[4], p[6]); op(p[2], p[8]); op(p[2], p[4]); op(p[6], p[8]);
                        op(p[1], p[7]); op(p[5], p[11]); op(p[5], p[7]); op(p[3], p[9]); op(p[3], p[5]);
                        op(p[7], p[9]); op(p[1], p[2]); op(p[3], p[4]); op(p[5], p[6]); op(p[7], p[8]);
                        op(p[9], p[10]); op(p[13], p[14]); op(p[12], p[13]); op(p[13], p[14]); op(p[16], p[17]);
                        op(p[15], p[16]); op(p[16], p[17]); op(p[12], p[15]); op(p[14], p[17]); op(p[14], p[15]);
                        op(p[13], p[16]); op(p[13], p[14]); op(p[15], p[16]); op(p[19], p[20]); op(p[18], p[19]);
                        op(p[19], p[20]); op(p[21], p[22]); op(p[23], p[24]); op(p[21], p[23]); op(p[22], p[24]);
                        op(p[22], p[23]); op(p[18], p[21]); op(p[20], p[23]); op(p[20], p[21]); op(p[19], p[22]);
                        op(p[22], p[24]); op(p[19], p[20]); op(p[21], p[22]); op(p[23], p[24]); op(p[12], p[18]);
                        op(p[16], p[22]); op(p[16], p[18]); op(p[14], p[20]); op(p[20], p[24]); op(p[14], p[16]);
                        op(p[18], p[20]); op(p[22], p[24]); op(p[13], p[19]); op(p[17], p[23]); op(p[17], p[19]);
                        op(p[15], p[21]); op(p[15], p[17]); op(p[19], p[21]); op(p[13], p[14]); op(p[15], p[16]);
                        op(p[17], p[18]); op(p[19], p[20]); op(p[21], p[22]); op(p[23], p[24]); op(p[0], p[12]);
                        op(p[8], p[20]); op(p[8], p[12]); op(p[4], p[16]); op(p[16], p[24]); op(p[12], p[16]);
                        op(p[2], p[14]); op(p[10], p[22]); op(p[10], p[14]); op(p[6], p[18]); op(p[6], p[10]);
                        op(p[10], p[12]); op(p[1], p[13]); op(p[9], p[21]); op(p[9], p[13]); op(p[5], p[17]);
                        op(p[13], p[17]); op(p[3], p[15]); op(p[11], p[23]); op(p[11], p[15]); op(p[7], p[19]);
                        op(p[7], p[11]); op(p[11], p[13]); op(p[11], p[12]);
                        dst[j] = (T)p[12];
                    }
    
                    if( limit == size.width )
                        break;
    
                    //下面的实现是个人猜测,没经过严格认证
                    //VecOp应该是可以一次性处理VecOp::SIZE对数值的比较,利用opencv的优化指令
                    for( ; j <= size.width - VecOp::SIZE - cn*2; j += VecOp::SIZE )  //每次步进VecOp::SIZE个数,断点看VecOp::SIZE是16
                    {
                        VT p[25];
                        for( k = 0; k < 5; k++ )
                        {
                            const T* rowk = row[k];
                            //猜测vop.load是一次性加载VecOp::SIZE(16)个内存数据
                            p[k*5] = vop.load(rowk+j-cn*2); p[k*5+1] = vop.load(rowk+j-cn);
                            p[k*5+2] = vop.load(rowk+j); p[k*5+3] = vop.load(rowk+j+cn);
                            p[k*5+4] = vop.load(rowk+j+cn*2);
                        }
    
                        //p[i]指向连续的16个内存数据,vop一次性对比16个数,并将较小的放到第一个参数,较大的放到第二个参数
                        vop(p[1], p[2]); vop(p[0], p[1]); vop(p[1], p[2]); vop(p[4], p[5]); vop(p[3], p[4]);
                        vop(p[4], p[5]); vop(p[0], p[3]); vop(p[2], p[5]); vop(p[2], p[3]); vop(p[1], p[4]);
                        vop(p[1], p[2]); vop(p[3], p[4]); vop(p[7], p[8]); vop(p[6], p[7]); vop(p[7], p[8]);
                        vop(p[10], p[11]); vop(p[9], p[10]); vop(p[10], p[11]); vop(p[6], p[9]); vop(p[8], p[11]);
                        vop(p[8], p[9]); vop(p[7], p[10]); vop(p[7], p[8]); vop(p[9], p[10]); vop(p[0], p[6]);
                        vop(p[4], p[10]); vop(p[4], p[6]); vop(p[2], p[8]); vop(p[2], p[4]); vop(p[6], p[8]);
                        vop(p[1], p[7]); vop(p[5], p[11]); vop(p[5], p[7]); vop(p[3], p[9]); vop(p[3], p[5]);
                        vop(p[7], p[9]); vop(p[1], p[2]); vop(p[3], p[4]); vop(p[5], p[6]); vop(p[7], p[8]);
                        vop(p[9], p[10]); vop(p[13], p[14]); vop(p[12], p[13]); vop(p[13], p[14]); vop(p[16], p[17]);
                        vop(p[15], p[16]); vop(p[16], p[17]); vop(p[12], p[15]); vop(p[14], p[17]); vop(p[14], p[15]);
                        vop(p[13], p[16]); vop(p[13], p[14]); vop(p[15], p[16]); vop(p[19], p[20]); vop(p[18], p[19]);
                        vop(p[19], p[20]); vop(p[21], p[22]); vop(p[23], p[24]); vop(p[21], p[23]); vop(p[22], p[24]);
                        vop(p[22], p[23]); vop(p[18], p[21]); vop(p[20], p[23]); vop(p[20], p[21]); vop(p[19], p[22]);
                        vop(p[22], p[24]); vop(p[19], p[20]); vop(p[21], p[22]); vop(p[23], p[24]); vop(p[12], p[18]);
                        vop(p[16], p[22]); vop(p[16], p[18]); vop(p[14], p[20]); vop(p[20], p[24]); vop(p[14], p[16]);
                        vop(p[18], p[20]); vop(p[22], p[24]); vop(p[13], p[19]); vop(p[17], p[23]); vop(p[17], p[19]);
                        vop(p[15], p[21]); vop(p[15], p[17]); vop(p[19], p[21]); vop(p[13], p[14]); vop(p[15], p[16]);
                        vop(p[17], p[18]); vop(p[19], p[20]); vop(p[21], p[22]); vop(p[23], p[24]); vop(p[0], p[12]);
                        vop(p[8], p[20]); vop(p[8], p[12]); vop(p[4], p[16]); vop(p[16], p[24]); vop(p[12], p[16]);
                        vop(p[2], p[14]); vop(p[10], p[22]); vop(p[10], p[14]); vop(p[6], p[18]); vop(p[6], p[10]);
                        vop(p[10], p[12]); vop(p[1], p[13]); vop(p[9], p[21]); vop(p[9], p[13]); vop(p[5], p[17]);
                        vop(p[13], p[17]); vop(p[3], p[15]); vop(p[11], p[23]); vop(p[11], p[15]); vop(p[7], p[19]);
                        vop(p[7], p[11]); vop(p[11], p[13]); vop(p[11], p[12]);
                        vop.store(dst+j, p[12]);
                    }
    
                    limit = size.width;
                }
            }
        }
    }
    

    可以看到,medianBlur_SortNet函数有两个模板类Op和VecOp,也就是MinMax8u和MinMaxVec8u,它们的作用是可以快速的比较两个数,并把较小的数赋给第一个参数,较大的数赋给第二个参数,MinMax8u只对比一组数据,而MinMaxVec8u可以同时(并行)对比16个数据,它应该是采用了SIMD指令。他们的实现会根据不同硬件调用不同的优化指令接口。

    函数中有一个大循环

     int limit = useSIMD ? cn*2 : size.width;  //根据是否使用SIMD来决定limit大小
    for(j = 0;; )
    {
        //用op(一次比较一组数据)
        for( ; j < limit; j++ )   
        {
            //...
        }
    
         //用vop(一次比较VecOp::SIZE个数据)
         for( ; j <= size.width - VecOp::SIZE - cn*2; j += VecOp::SIZE )  
        {
            //...
        }
    }
    

    我们一定会有个疑问,为什么非要先用op对比limit个数,后面才用vop?一开始就用vop不是更快吗?我个人的猜测,是因为需要补边的原因。如下图


    如图,在求边界上的像素的中值时,如图中的红点,边界外的邻域实际上是不存在的,如图中虚线部分,代码中row[0]和row[1]代表边界外的像素,其实他们都是指向图像第一行,也就是row[2]。利用vop时,会一次性加载16个连续内存数据,因此在计算边界上的中值时,不能用vop。

    再看下第二个循环,用vop,顾名思义,就是数组的操作(vector operation)。它可以一次性处理16个(VecOp::SIZE)个数据,可以加速处理,如下图:


    相关文章

      网友评论

          本文标题:OpenCV源码分析(四):中值滤波

          本文链接:https://www.haomeiwen.com/subject/sbwdhqtx.html