写在前面


Sobel算子同样是一种一阶微分算子,它的卷积算子和Prewitt算子非常类似,仅仅是系数不同,但Sobel算子对于像素位置的影响做了加权,与Prewitt算子、Roberts算子相比效果更好。

优点
对边缘定位较为准确,能较好地处理灰度渐变和噪声较多的图像,计算简单,可分别计算水平和垂直边缘,如EasyPR用其定位车牌。

原理
首先我们看Sobel算子:

我们发现:相比Prewitt算子,就是平滑系数不同,sobel进行了加权。而这个平滑系数其实就是窗口为3的非归一化的高斯平滑。

平滑算子:对于二项式展开式的系数可以作为非归一化的高斯平滑算子,具体原理推导可见《OpenCV算法精讲》(张平著)的第五章:图像平滑。二项式展开系数为:

窗口为w=3 ,平滑算子就为阶数n=w-1=2的展开式的系数,把2代入上式计算一下即可。

所以我们还可以自己构建任意窗口大小的平滑算子,可总结如下:

上表中的“平滑算子”一列其实就是杨辉三角(帕斯卡三角形)。

差分算子:窗口为w的差分算子就是在w-2阶的二项式展开系数两侧补0,然后后向差分得到。

举例: 构建窗口为5的平滑算子,则对应差分算子就为n=3阶展开式系数两侧补0,然后后向差分:

                                                              0      1     3     3     1      0

       差分:                                               1-0   3-1  3-3   1-3  0-1

     差分后:                                               1      2     0     -2   -1  (差分算子)

 

显然,Soble边缘检测是可分离的,可以分离实现提高效率。

代码实现

#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
 
//阶乘
int factorial(int n){
	int fac = 1;
	//0的阶乘
	if (n == 0)
		return fac;
	for (int i = 1; i <= n; ++i){
		fac *= i;
	}
	return fac;
}
 
//获得Sobel平滑算子
cv::Mat getSobelSmoooth(int wsize){
	int n = wsize - 1;
	cv::Mat SobelSmooothoper=cv::Mat::zeros(cv::Size(wsize,1),CV_32FC1);
	for (int k = 0; k <= n; k++){
		float *pt = SobelSmooothoper.ptr<float>(0);
		pt[k] = factorial(n) / (factorial(k)*factorial(n - k));
	}
	return SobelSmooothoper;
}
 
//获得Sobel差分算子
cv::Mat getSobeldiff(int wsize){
	cv::Mat Sobeldiffoper = cv::Mat::zeros(cv::Size(wsize, 1), CV_32FC1);
	cv::Mat SobelSmoooth = getSobelSmoooth(wsize - 1);
	for (int k = 0; k < wsize; k++){
		if (k == 0)  
			Sobeldiffoper.at<float>(0, k) = 1;
		else if (k == wsize - 1)
			Sobeldiffoper.at<float>(0, k) = -1;
		else
			Sobeldiffoper.at<float>(0, k) = SobelSmoooth.at<float>(0, k) - SobelSmoooth.at<float>(0, k - 1);
	}      
	return Sobeldiffoper;
}
 
//卷积实现
void conv2D(cv::Mat& src, cv::Mat& dst, cv::Mat kernel, int ddepth, cv::Point anchor = cv::Point(-1, -1), int delta = 0, int borderType = cv::BORDER_DEFAULT){
	cv::Mat  kernelFlip;
	cv::flip(kernel, kernelFlip, -1);
	cv::filter2D(src, dst, ddepth, kernelFlip, anchor, delta, borderType);
}
 
 
//可分离卷积———先垂直方向卷积,后水平方向卷积
void sepConv2D_Y_X(cv::Mat& src, cv::Mat& dst, cv::Mat kernel_Y, cv::Mat kernel_X, int ddepth, cv::Point anchor = cv::Point(-1, -1), int delta = 0, int borderType = cv::BORDER_DEFAULT){
	cv::Mat dst_kernel_Y;
	conv2D(src, dst_kernel_Y, kernel_Y, ddepth, anchor, delta, borderType); //垂直方向卷积
	conv2D(dst_kernel_Y, dst, kernel_X, ddepth, anchor, delta, borderType); //水平方向卷积
}
 
//可分离卷积———先水平方向卷积,后垂直方向卷积
void sepConv2D_X_Y(cv::Mat& src, cv::Mat& dst, cv::Mat kernel_X, cv::Mat kernel_Y, int ddepth, cv::Point anchor = cv::Point(-1, -1), int delta = 0, int borderType = cv::BORDER_DEFAULT){
	cv::Mat dst_kernel_X;
	conv2D(src, dst_kernel_X, kernel_X, ddepth, anchor, delta, borderType); //水平方向卷积
	conv2D(dst_kernel_X, dst, kernel_Y, ddepth, anchor, delta, borderType); //垂直方向卷积
}
 
 
//Sobel算子边缘检测
//dst_X 垂直方向
//dst_Y 水平方向
void Sobel(cv::Mat& src, cv::Mat& dst_X, cv::Mat& dst_Y, cv::Mat& dst, int wsize, int ddepth, cv::Point anchor = cv::Point(-1, -1), int delta = 0, int borderType = cv::BORDER_DEFAULT){
 
	cv::Mat SobelSmooothoper = getSobelSmoooth(wsize); //平滑系数
	cv::Mat Sobeldiffoper = getSobeldiff(wsize); //差分系数
 
	//可分离卷积———先垂直方向平滑,后水平方向差分——得到垂直边缘
	sepConv2D_Y_X(src, dst_X, SobelSmooothoper.t(), Sobeldiffoper, ddepth);
 
	//可分离卷积———先水平方向平滑,后垂直方向差分——得到水平边缘
	sepConv2D_X_Y(src, dst_Y, SobelSmooothoper, Sobeldiffoper.t(), ddepth);
 
	//边缘强度(近似)
	dst = abs(dst_X) + abs(dst_Y);
	cv::convertScaleAbs(dst, dst); //求绝对值并转为无符号8位图
	cv::convertScaleAbs(dst_X, dst_X); //求绝对值并转为无符号8位图
	cv::convertScaleAbs(dst_Y, dst_Y);
	//cv::pow(dst_X, 2.0, dst_X);
	//cv::pow(dst_Y, 2.0, dst_Y);
	//cv::sqrt(dst_X + dst_Y, dst); 
	//dst.convertTo(dst, CV_8UC1);
}
 
 
int main(){
	cv::Mat src = cv::imread("I:\\Learning-and-Practice\\2019Change\\Image process algorithm\\Img\\Fig1025(a)(building_original).tif");
	if (src.empty()){
		return -1;
	}
	if (src.channels() > 1) cv::cvtColor(src, src, CV_RGB2GRAY);
	int wsize = 3;
	cv::Mat Sobeldiffoper, SobelSmooothoper;
	SobelSmooothoper = getSobelSmoooth(wsize);
	Sobeldiffoper = getSobeldiff(wsize);
 
	//注意:要采用CV_32F,因为卷积后可能为负数,若用8位无符号,则会导致这些地方为0
	cv::Mat dst, dst_X, dst_Y;
	Sobel(src, dst_X, dst_Y, dst, wsize, CV_32FC1);
	cv::namedWindow("src", CV_WINDOW_NORMAL);
	imshow("src", src);
	cv::namedWindow("水平边缘", CV_WINDOW_NORMAL);
	imshow("水平边缘", dst_Y);
	cv::namedWindow("垂直边缘", CV_WINDOW_NORMAL);
	imshow("垂直边缘", dst_X);
	//cv::namedWindow("边缘强度", CV_WINDOW_NORMAL);
	imshow("边缘强度", dst);
	cv::namedWindow("边缘强度取反-铅笔素描", CV_WINDOW_NORMAL);
	imshow("边缘强度取反-铅笔素描",255-dst);
 
	std::cout <<"SobelSmooothoper: "<< SobelSmooothoper << std::endl;
	std::cout << "Sobeldiffoper: " << Sobeldiffoper << std::endl;
	cv::waitKey(0);
	return 0;
}

效果

划重点:有一个有趣的应用:生成铅笔画素描的效果,将边缘检测结果图取反即可!

参考:

《OpenCV算法精解》---张平

https://blog.csdn.net/weixin_40647819/article/list/2?