前言:
笔者目前在校本科大二,有志于进行计算机视觉、计算机图形学方向的研究,准备系统性地、扎实的学习一遍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的梯度,然后合成得到最终结果。
官方文档中的举例:
如果设置ksize = -1,对应的是Scharr。
注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;
也可以输出彩色边缘:
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;
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()的本质上还是一种滤波(广义的滤波)。只是得到的结果可以很好地进行边缘检测(由于边缘检测算法基于图像的一阶、二阶梯度)。
参考文献:
- OpenCV官方文档:https://docs.opencv.org/4.x/
- 《OpenCV3编程入门》毛星云、冷雪飞等编著
- 《OpenCV4快速入门》冯振、郭延宁、吕跃勇著