OpenCV学习笔记(八)之边缘检测算子上篇(Sobel算子实现原理及源码分析)

  OpenCV中封装了很多函数,一般我们只需要调用它提供的API函数即可实现各种图像处理操作,如滤波、边缘检测等,这对我们项目开发来说是非常方便的。但是一个优秀的算法工程师必然也是一位优秀的程序员,因为就算你熟知各种算法的原理,但是不知道怎么实现也是不行的,(举个例子:假如你是一位销售员,你脑袋里面有很多非常棒的点子,可是你不知道怎么将它表述出来,那么到最后你依然还是一位处在销售底层的人。) 只有既熟悉原理又会用代码将大脑中好的想法实现,那才是优秀的算法工程师。当然,要完全靠自己从头实现OpenCV中的各种函数也是不现实的,就算你有那能力但是也不一定会有那么多精力。因此折中一点,至少要让自己能看懂OpenCV中各种算法的实现代码(比如说你做人脸识别,至少也要能完全理解SIFT是怎么实现的吧),这样如果有一天需要自己开发新的或者优化现有算法,那对自己的帮助就很大了。

一、Sobel算子函数介绍

OpenCV中Sobel算子被封装在

CV_EXPORTS_W void Sobel( InputArray src, OutputArray dst, int ddepth,
                         int dx, int dy, int ksize=3,
                         double scale=1, double delta=0,
                         int borderType=BORDER_DEFAULT );

这个函数里面,其中:

  1. src表示输入原图像;
  2. dst表示输出目标图像;
  3. ddepth表示用来度量每一个像素中每一个通道的精度,但它本身与图像的通道数无关!depth数值越大,精度越高。在Opencv中,Mat.depth()得到的是一个0~6的数字,分别代表不同的位数,对应关系如下:
    enum{CV_8U=0,CV_8S=1,CV_16U=2,CV_16S=3,CV_32S=4,CV_32F=5,CV_64F=6},ddepth 由于Sobel运算时可能会出现比较大的值,因此取值方式有以下几种:
  • 若 ddepth = CV_8U, 取 ddepth = -1 或 CV_16S 或 CV_32F 或 CV_64F
  • 若 ddepth = CV_16U, 或 CV_16S, 取 ddepth = -1 或 CV_32F 或 CV_64F
  • 若 ddepth = CV_32F, 取 ddepth = -1 或 CV_32F 或 CV_64F
  • 若 ddepth = CV_64F, 取 ddepth = -1 或 CV_64F
  • 若 ddepth = -1, 代表输出图像与输入图像相同的深度。
  1. dx表示对X方向进行求导的差分阶数;
  2. dy表示对Y方向进行求导的差分阶数;(其中, dx=1,dy=0,表示计算X方向的导数,检测出的是垂直方向上的边缘;dx=0,dy=1,表示计算Y方向的导数,检测出的是水平方向上的边缘。)
  3. ksize 表示卷积核的大小,只能为奇数 1、3、5、7 等,特殊情况:当ksize <= 0 时,采用的模板为3 * 3 的Scharr内核。
  4. scale 表示缩放尺度,默认为1。
  5. delta:默认0。(还不知道有什么作用,猜测是用来放大的)
    int borderType:滤波时填充的图像边界类型,有如下几种:
enum { BORDER_REPLICATE=IPL_BORDER_REPLICATE,
	   BORDER_CONSTANT=IPL_BORDER_CONSTANT,
       BORDER_REFLECT=IPL_BORDER_REFLECT, 
       BORDER_WRAP=IPL_BORDER_WRAP,
       BORDER_REFLECT_101=IPL_BORDER_REFLECT_101, 
       BORDER_REFLECT101=BORDER_REFLECT_101,
       BORDER_TRANSPARENT=IPL_BORDER_TRANSPARENT,
       BORDER_DEFAULT=BORDER_REFLECT_101, 
       BORDER_ISOLATED=16 };

默认值为BORDER_DEFAULT,具体边界定义方法参考图像剪切的扩展和高级用法:任意裁剪,边界扩充这篇博客,里面对图像边界的扩充说得很详细。

二、Sobel算子实现流程

OpenCV学习笔记(八)之边缘检测算子上篇(Sobel算子实现原理及源码分析)_第1张图片

三、Sobel算子源码分析

openCV中Sobel函数定义如下:

void cv::Sobel( InputArray _src, OutputArray _dst, int ddepth, int dx, int dy,
                int ksize, double scale, double delta, int borderType )
{
    Mat src = _src.getMat();
    if (ddepth < 0)
        ddepth = src.depth();
    _dst.create( src.size(), CV_MAKETYPE(ddepth, src.channels()) );
    Mat dst = _dst.getMat();

#ifdef HAVE_TEGRA_OPTIMIZATION
    if (scale == 1.0 && delta == 0)
    {
        if (ksize == 3 && tegra::sobel3x3(src, dst, dx, dy, borderType))
            return;
        if (ksize == -1 && tegra::scharr(src, dst, dx, dy, borderType))
            return;
    }
#endif

#if defined (HAVE_IPP) && (IPP_VERSION_MAJOR >= 7)
    if(dx < 3 && dy < 3 && src.channels() == 1 && borderType == 1)
    {
        if(IPPDeriv(src, dst, ddepth, dx, dy, ksize,scale))
            return;
    }
#endif
    int ktype = std::max(CV_32F, std::max(ddepth, src.depth()));

    Mat kx, ky;
    getDerivKernels( kx, ky, dx, dy, ksize, false, ktype );
    if( scale != 1 )
    {
        // usually the smoothing part is the slowest to compute,
        // so try to scale it instead of the faster differenciating part
        if( dx == 0 )
            kx *= scale;
        else
            ky *= scale;
    }
    sepFilter2D( src, dst, ddepth, kx, ky, Point(-1,-1), delta, borderType );
}

下面一步一步分解:

Mat src = _src.getMat();
    if (ddepth < 0)
        ddepth = src.depth();
    _dst.create( src.size(), CV_MAKETYPE(ddepth, src.channels()) );
    Mat dst = _dst.getMat();

  这段将InputArray & OutputArray 类转换为 Mat类,需要注意的是InputArray 类可以通过 getMat() 函数直接转换,但是 OutputArray 类的转换必须先使用 create()函数分配内存,再使用getMat()函数才能将OutputArray 类转换为Mat类。

    int ktype = std::max(CV_32F, std::max(ddepth, src.depth()));
    Mat kx, ky;
    getDerivKernels( kx, ky, dx, dy, ksize, false, ktype );
#endif

  前面函数介绍那部分有讲过depth()得到的是一个0~6的数字,分别代表不同的位数,因此可以看出ktype的取值只能为CV_32F或者是CV_64F,下面讲getDerivKernels( kx, ky, dx, dy, ksize, false, ktype );这个函数:

void cv::getDerivKernels( OutputArray kx, OutputArray ky, int dx, int dy, int ksize, bool normalize, int ktype )
{
    if( ksize <= 0 )
        getScharrKernels( kx, ky, dx, dy, normalize, ktype );
    else
        getSobelKernels( kx, ky, dx, dy, ksize, normalize, ktype );
}

可以看出当我们给Sobel()函数输入的 ksize <= 0 时,我们实际上是使用的Scharr内核作为我们的卷积核,Scharr核为
[ − 3 − 10 − 3 0 0 0 + 3 + 10 + 3 ] \left[ \begin{matrix} -3 & -10 & -3 \\ 0 & 0 & 0 \\ +3 & +10 & +3 \end{matrix} \right] 30+3100+1030+3
我们看看getScharrKernels()这个函数是怎么生成Scharr核的:

static void getScharrKernels( OutputArray _kx, OutputArray _ky, int dx, int dy, bool normalize, int ktype )
{
    const int ksize = 3;
    
    CV_Assert( ktype == CV_32F || ktype == CV_64F ); //ktype 只能取CV_32F或者CV_64F ,否则报错 
    _kx.create(ksize, 1, ktype, -1, true);
    _ky.create(ksize, 1, ktype, -1, true);
    Mat kx = _kx.getMat();   //生成3行1列的矩阵并转换为Mat类
    Mat ky = _ky.getMat();

    CV_Assert( dx >= 0 && dy >= 0 && dx+dy == 1 );  //判断求的偏导方向是否正确

 	/***** 为矩阵赋值并转换为浮点数矩阵*******
 	当求水平方向一阶导时,dx = 1, dy = 0;
 	此时 kx = {-1, 0, 1}          ky = {3, 10, 3}
 	当求垂直方向一阶导时,dx = 0, dy = 1;
 	此时 kx = {3, 10, 3}          ky = {-1, 0, 1}	                 
 	***************************************/
    for( int k = 0; k < 2; k++ )
    {
        Mat* kernel = k == 0 ? &kx : &ky;
        int order = k == 0 ? dx : dy;
        int kerI[3];

        if( order == 0 )
            kerI[0] = 3, kerI[1] = 10, kerI[2] = 3;
        else if( order == 1 )
            kerI[0] = -1, kerI[1] = 0, kerI[2] = 1;

        Mat temp(kernel->rows, kernel->cols, CV_32S, &kerI[0]);
        double scale = !normalize || order == 1 ? 1. : 1./32;
        temp.convertTo(*kernel, ktype, scale);
    }
}

  getSobelKernels()函数生成Sobel核与生成Scharr核类似,只是其生成的核的大小更多样性,可以为1、3、5、7、9等奇数核,但最大不能超过31:

static void getSobelKernels( OutputArray _kx, OutputArray _ky, int dx, int dy, int _ksize, bool normalize, int ktype )
{
    int i, j, ksizeX = _ksize, ksizeY = _ksize;
    if( ksizeX == 1 && dx > 0 )
        ksizeX = 3;
    if( ksizeY == 1 && dy > 0 )
        ksizeY = 3;

    CV_Assert( ktype == CV_32F || ktype == CV_64F );

    _kx.create(ksizeX, 1, ktype, -1, true);
    _ky.create(ksizeY, 1, ktype, -1, true);
    Mat kx = _kx.getMat();
    Mat ky = _ky.getMat();

    if( _ksize % 2 == 0 || _ksize > 31 )
        CV_Error( CV_StsOutOfRange, "The kernel size must be odd and not larger than 31" );
    vector<int> kerI(std::max(ksizeX, ksizeY) + 1);

    CV_Assert( dx >= 0 && dy >= 0 && dx+dy > 0 );

    for( int k = 0; k < 2; k++ )
    {
        Mat* kernel = k == 0 ? &kx : &ky;
        int order = k == 0 ? dx : dy;
        int ksize = k == 0 ? ksizeX : ksizeY;

        CV_Assert( ksize > order );

        if( ksize == 1 )
            kerI[0] = 1;
        else if( ksize == 3 )
        {
            if( order == 0 )
                kerI[0] = 1, kerI[1] = 2, kerI[2] = 1;
            else if( order == 1 )
                kerI[0] = -1, kerI[1] = 0, kerI[2] = 1;
            else
                kerI[0] = 1, kerI[1] = -2, kerI[2] = 1;
        }
        else
        {
            int oldval, newval;
            kerI[0] = 1;
            for( i = 0; i < ksize; i++ )
                kerI[i+1] = 0;

            for( i = 0; i < ksize - order - 1; i++ )
            {
                oldval = kerI[0];
                for( j = 1; j <= ksize; j++ )
                {
                    newval = kerI[j]+kerI[j-1];
                    kerI[j-1] = oldval;
                    oldval = newval;
                }
            }

            for( i = 0; i < order; i++ )
            {
                oldval = -kerI[0];
                for( j = 1; j <= ksize; j++ )
                {
                    newval = kerI[j-1] - kerI[j];
                    kerI[j-1] = oldval;
                    oldval = newval;
                }
            }
        }

        Mat temp(kernel->rows, kernel->cols, CV_32S, &kerI[0]);
        double scale = !normalize ? 1. : 1./(1 << (ksize-order-1));
        temp.convertTo(*kernel, ktype, scale);
    }
}

卷积核生成以后就要进行最后一步了,对图像进行卷积滤波,使用sepFilter2D()这个函数

void cv::sepFilter2D( InputArray _src, OutputArray _dst, int ddepth,
                      InputArray _kernelX, InputArray _kernelY, Point anchor,
                      double delta, int borderType )
{
    Mat src = _src.getMat(), kernelX = _kernelX.getMat(), kernelY = _kernelY.getMat();

    if( ddepth < 0 )
        ddepth = src.depth();

    _dst.create( src.size(), CV_MAKETYPE(ddepth, src.channels()) );
    Mat dst = _dst.getMat();

    Ptr<FilterEngine> f = createSeparableLinearFilter(src.type(),
        dst.type(), kernelX, kernelY, anchor, delta, borderType & ~BORDER_ISOLATED ); //创建可分离线系滤波器
    f->apply(src, dst, Rect(0,0,-1,-1), Point(), (borderType & BORDER_ISOLATED) != 0 ); //使用创建的滤波器进行滤波
}

这段的重点在最后两行,创建滤波器并使用创建的滤波器进行滤波,这里就不再深入到滤波器引擎了,后面使用专门的一篇来讲解滤波器引擎FilterEngine。

你可能感兴趣的:(OpenCV学习笔记)