目录

  • 前言:
  • 本篇学习内容:
  • 1.基于OpenCV的边缘检测
  • 1.1 边缘检测一般步骤
  • 1.2 Sobel算子
  • 1.3 Canny边缘检测
  • 2. 源码分析
  • 参考文献:


前言:

笔者目前在校本科大二,有志于进行计算机视觉、计算机图形学方向的研究,准备系统性地、扎实的学习一遍OpenCV的内容,故记录学习笔记,同时,由于笔者同时学习数据结构、机器学习等知识,会尽量根据自己的理解,指出OpenCV的应用,并在加上自己理解的前提下进行叙述。
若有不当之处,希望各位批评、指正。


本篇学习内容:

1.基于OpenCV的边缘检测
2.源码分析


1.基于OpenCV的边缘检测

1.1 边缘检测一般步骤

摘自《OpenCV3编程入门》:
1.滤波
边缘检测的算法主要是基于图像强度的一阶和二阶导数,但导数通常对噪声敏感,因此需要采用滤波器来改善和噪声有关的边缘检测器的性能。
2.增强
增强边缘的基础是确定图像各点邻域强度的变化值。增强算法可以将图像灰度点邻域强度值有显著变化的点凸显出来。在具体编程实现时,可通过计算梯度幅值来确定。
3.检测
经过增强的图像,往往邻域中有很多点的梯度值比较大,而在特定的应用中,这些点并不是要找的边缘点,所以应该采用某种方法来对这些点进行取舍。实际工程中,常用的方法是通过阈值化方法来检测。

1.2 Sobel算子

我之所以要先介绍Sobel算子,是因为在后文的Canny边缘检测函数中调用了Sobel()函数。

*Sobel算子可以计算图像灰度函数的近似梯度。
*Sobel算子结合了高斯平滑和微分,因此结果对噪声有一定的抵抗能力。

下面介绍Sobel()函数:

void cv::Sobel	(	
InputArray 	src,	//输入图像
OutputArray dst,	//输出图像
int 	ddepth,		//输出图像的深度。如果设置为-1,输出深度和输入深入一致
int 	dx,			//对x偏导的阶
int 	dy,			//对y偏导的阶
int 	ksize = 3,	//卷积核尺寸。只能为1/3/5/7!
double 	scale = 1,	//可选的计算导数值的比例因子,一般用不到
double 	delta = 0,	//可选的一个增量值,用于改变输出结果
int 	borderType = BORDER_DEFAULT //边界类型。一般不管它
)

Sobel()用一个卷积核对每个像素的邻域卷积,来得到近似的梯度。一般用Sobel()分别计算出x和y的梯度,然后合成得到最终结果。

官方文档中的举例:

python opencv 边缘骨架 opencv边缘检测代码_学习


如果设置ksize = -1,对应的是Scharr。

python opencv 边缘骨架 opencv边缘检测代码_学习_02


注1:关于Scharr,这个函数的核大小只能是3,Scharr()函数的运算与Sobel()一样快,但结果更加精确。在调用Scharr()函数时,等同于在调用Sobel()时设置ksize = -1。

注2:一般在进行Sobel()计算梯度后,用addWeighted()函数合成,得到最终结果。

1.3 Canny边缘检测

用Canny()进行Canny边缘检测。

void cv::Canny	(	
InputArray 	image,//输入图像
OutputArray edges,//输出图像
double 	threshold1,//阈值1
double 	threshold2,//阈值2
int 	apertureSize = 3,//Sobel操作的卷积核大小
bool 	L2gradient = false //是否使用更精确的L2范数
)

对部分参数进行进一步解释:
image:8位输入图像。可以多通道,但是在多通道时不支持就地操作。单通道时支持就地操作。
edges:8位单通道。Size和输入图像相同。
threshold1:第一个滞后性阈值
threshold2:第二个滞后性阈值。这两个阈值大小顺序可以任意。OpenCV会帮忙排好大小。
L2gradient:这是计算导数的两种方式。L2是计算x梯度和y梯度平方和的二次开方。而L1是计算x梯度和y梯度的绝对值之和。

注1:阈值1和阈值2中较小的用于边缘连接,较大的用于寻找强边缘的初始段。
注2:Canny()还有一个重载,可以直接输入梯度dx、dy,输出edges。

稍微举个例子:

Mat img = imread("E:/program/image/1.jpg");
Mat src,dst;
cvtColor(img, src, COLOR_BGR2GRAY);
GaussianBlur(src, src, Size(3, 3),0,0);
Canny(src, dst, 180, 120, 3);
imshow("src", img);
imshow("dst", dst);
waitKey();
return 0;

python opencv 边缘骨架 opencv边缘检测代码_python opencv 边缘骨架_03


也可以输出彩色边缘:

Mat img = imread("E:/program/image/1.jpg");
Mat gray,mask,dst;
cvtColor(img, gray, COLOR_BGR2GRAY);
GaussianBlur(gray, mask, Size(3, 3), 0, 0);
Canny(mask, mask, 100, 150);
dst = Mat::zeros(img.size(), img.type());
img.copyTo(dst, mask);
imshow("src", img);
imshow("dst", dst);
waitKey();
return 0;

python opencv 边缘骨架 opencv边缘检测代码_计算机视觉_04

2. 源码分析

Canny()函数文件路径:opencv\sources\modules\imgproc\src\canny.cpp 第823行

准备工作:

CV_INSTRUMENT_REGION();

    CV_Assert( _src.depth() == CV_8U );

    const Size size = _src.size();

    // we don't support inplace parameters in case with RGB/BGR src
    CV_Assert((_dst.getObj() != _src.getObj() || _src.type() == CV_8UC1) && "Inplace parameters are not supported");

    _dst.create(size, CV_8U);

    if (!L2gradient && (aperture_size & CV_CANNY_L2_GRADIENT) == CV_CANNY_L2_GRADIENT)
    {
        // backward compatibility
        aperture_size &= ~CV_CANNY_L2_GRADIENT;
        L2gradient = true;
    }

    if ((aperture_size & 1) == 0 || (aperture_size != -1 && (aperture_size < 3 || aperture_size > 7)))
        CV_Error(CV_StsBadFlag, "Aperture size should be odd between 3 and 7");

    if (aperture_size == 7)
    {
        low_thresh = low_thresh / 16.0;
        high_thresh = high_thresh / 16.0;
    }

    if (low_thresh > high_thresh)
        std::swap(low_thresh, high_thresh);

从这里可以看到一些准备工作:包括不支持多通道的就地操作、调整low_thresh和high_thresh的顺序等。
随后调用了ocl_Canny函数:

CV_OCL_RUN(_dst.isUMat() && (_src.channels() == 1 || _src.channels() == 3),
               ocl_Canny<false>(_src, UMat(), UMat(), _dst, (float)low_thresh, (float)high_thresh, aperture_size, L2gradient, _src.channels(), size))

这个函数在同文件的135行:

static bool ocl_Canny(
InputArray _src, 
const UMat& dx_, 
const UMat& dy_, 
OutputArray _dst, 
float low_thresh, 
float high_thresh,
int aperture_size, 
bool L2gradient, 
int cn, 
const Size & size
)

在226行,调用了Sobel()函数:

if (!useCustomDeriv)
       {
           Sobel(_src, dx, CV_16S, 1, 0, aperture_size, scale, 0, BORDER_REPLICATE);
           Sobel(_src, dy, CV_16S, 0, 1, aperture_size, scale, 0, BORDER_REPLICATE);
       }
       else
       {
           dx = dx_;
           dy = dy_;
       }

可以看到,Canny()中通过调用2次Sobel()来分别计算x和y的梯度。

Canny主要做的事情就是利用计算好的梯度来进行一些阈值操作。所以,我们有必要看一下Sobel()的源码:
文件路径:opencv\sources\modules\imgproc\src\deriv.cpp 第414行
在进行了一些准备工作后,出现了:

getDerivKernels( kx, ky, dx, dy, ksize, false, ktype );

这行代码用于生成卷积核。
然后对图像进行卷积:

sepFilter2D(src, dst, ddepth, kx, ky, Point(-1, -1), delta, borderType );

所以,Sobel()的本质上还是一种滤波(广义的滤波)。只是得到的结果可以很好地进行边缘检测(由于边缘检测算法基于图像的一阶、二阶梯度)。

参考文献:

  1. OpenCV官方文档:https://docs.opencv.org/4.x/
  2. 《OpenCV3编程入门》毛星云、冷雪飞等编著
  3. 《OpenCV4快速入门》冯振、郭延宁、吕跃勇著