Opencv 笔记5 边缘处理-canny、sobel、Laplacian、Prewitt
一、边缘检测概述
边缘检测是计算视觉中的基本问题,边缘检测的目的是标识图像中亮度变换明显的点。边缘检测大幅度的减少了图像的数据量(分为两种:灰度图像边缘检测和彩色图像边缘检测),并且剔除了不相关的信息,保留了重要的结构属性。总之,图像的边缘检测是图像分割、目标区域识别和区域形状提取等图像分析的基石,也是图像中特征提取的很重要的方法。如何来实现?可以分为大的两步,一是图像边缘和背景的分离,二是 辨别出轮廓。实际的图像不像我们说的那么简单,往往是各种类型的边缘和他们模糊之后结果的结合,并且实际图像中存在着噪声,而噪声和边缘都属于高频信号。
为了得到图像轮廓清晰的图像,我们一般要进行锐化,要说锐化,就要从锐度说起,锐度其实就是边缘的对比度,锐度的提高是在不增加图像的像素的基础上造成的提高图像清晰度的假象。锐化的目的是使图像边缘、轮廓线、细节变的清晰,因为当我们去除噪声的时候源图像经过了一系列的平滑处理之后图像和边缘变的模糊,因此可以对平滑后的图像进行逆运算。锐化的方法有两种:1、高通滤波;2、空域微分法。
常见的噪声:加性噪声、乘性噪声、量化噪声
边缘的特征和分类:边缘有方向和幅度两个特征,沿着边缘方向走像素值逐渐平稳,垂直于边缘方向,像素值变化剧烈。
分类:1、阶跃性边缘,它两边的像素值有明显的差距,方向导数在边缘处是零交叉;
2、屋顶状边缘,它是从增加到减少的转折点,二阶方向导数在这里取得极致;
常见边缘检测类型:
1、一阶微分边缘检测,通过计算图像的梯度值来检测图像边缘,常见的算子有Sobel 、Prewitt 、Roberts、差分算子,canny(在一阶的基础上进行改进)
2、二阶微分边缘检测,求二阶导数的过零点来检测边缘,如拉普拉斯、高斯拉普拉斯
二、梯度以及边缘检测思想
如上图所示,边缘一般是以一阶导和二阶导数来检测。
术语介绍:
(1)、边缘点:灰度值显著变化的点
(2)、边缘段:边缘点坐标和方向的总和,边缘的方向可以是梯度角
(3)、轮廓:边缘列表
二阶偏导数-拉普拉斯算子数学原理
梯度以及Roberts 、Sobel 数学原理
导数总结如下:
a、一阶导数产生比较粗劣的边缘,二阶导数则比较精致,对细节的把控比较好,如细线,孤立的亮点等。
b、二阶导在灰度斜坡和台阶出会产生双边边缘响应,二阶的符号可以判断亮暗走势,和halcon 里面的双边算子是一样的。
设计到梯度的东西,后续会在PCA中详细介绍。下面是常见的几种边缘检测的详解
三、经典图像边缘检测算法
差分边缘检测
Roberts
Sobel
Prewitt
Log
Dog
Canny
Laplacian
Scharr、Kirsch、Robinson (了解)
1、 差分边缘检测
当我们处理图像的时候,可以用一阶差分来代替图像的导数,在x和y方向上各种差分得到一个x和y方向上的矩阵,还有一个是对角线的矩阵,他和x和有方向上的差分的推导过程是一样的,矩阵如下:
如何实现的先不写了,重点放在后面的几个算子上
2、 Roberts
从上面可以知道Roberts 算子的矩阵推导,Roberts 算子又被称为交叉微分算子,基于交叉差分的梯度算法(一阶导数),矩阵如下:
这个是用opencv已经集成的算子得出的结果(为了找一个能说明的图片我费了好长的时间)
src = imread("C:\\Users\\19473\\Desktop\\opencv_images\\305.jpg");if (!src.data){printf("could not load image....\n");}cvtColor(src, src_gray,COLOR_BGR2GRAY);imshow("原图", src_gray);// Robert X方向上的算子Mat kernel = (Mat_<int>(2, 2) << -1, 0, 0, 1);filter2D(src_gray, dest1, -1, kernel, Point(-1, -1), 0.0);imshow("45度", dest1);// Robert Y方向上的算子Mat kernel_y = (Mat_<int>(2, 2) << 0, -1, 1, 0);filter2D(src_gray, dest2, -1, kernel_y, Point(-1, -1), 0.0);imshow("135度", dest2);// 图像融合convertScaleAbs(dest2, dest2);convertScaleAbs(dest1, dest1);Mat RobertImage;addWeighted(dest1, 0.5, dest2, 0.5, 0, RobertImage);imshow("RobertImage", RobertImage);
下面是自定义并实现一个Roberts 算子:
从效果上看我拟合的没有opencv集成的好,但是原理是一样的。
代码实现:
Mat Robertsoperation(Mat &src,int type);
Mat Robertsoperation(Mat& src, int type)
{//type =1,2,3 分别表示 135 35 和合并后的结果Mat dstImage = src.clone();int nrows = src.rows;int ncols = src.cols;for (int i = 0; i < nrows-1; i++){for (int j = 0; j < ncols - 1; j++){// 135度int t1 = ((src.at<uchar>(i, j) - (src.at<uchar>(i + 1, j + 1)))* (src.at<uchar>(i, j) - (src.at<uchar>(i + 1, j + 1))));// 45 度int t2 = ((src.at<uchar>(i+1, j) - (src.at<uchar>(i , j + 1 ))) * (src.at<uchar>(i + 1, j) - (src.at<uchar>(i, j + 1))));if ( type == 1 ){dstImage.at<uchar>(i, j) = (uchar)t1;}else if (type == 2){dstImage.at<uchar>(i, j) = (uchar)t2;}else {dstImage.at<uchar>(i, j) = (uchar)sqrt(t1+t2);}}}return dstImage;}
3、 Sobel
具体步骤:
a:分解矩阵,将矩阵分解成一个二项式系数*差分算子的过程
b: 先求出二项式系数,然后再用这个系数计算差分算子
c:卷积运算 :
算子解释:
Sobel(inputArray,outputArray,int ddepth,int dx,int dy,int ksize=3,double scale=1,double delta=0,int borderType=BORDER_DEFAULT)
*第一个参数,输入图像。
*第二个参数,输出图像。
*第三个参数,输出图像深度。
*第四个参数,x方向上的差分阶数。
*第五个参数,y方向上的差分阶数。
*第六个参数,Sobel核的大小,必须是奇数。
*第七个参数,计算导数值时可选的缩放因子,默认值1,表示默认情况下没用应用缩放。
*第八个参数,表示在结果存入输出图像之前可选的delta值 (这个参数默认是0.0)
*第九个参数,扩充边界模式。
效果如下:
代码显示:
int factorial(int n)
{int fact = 1;if (n==0){return fact = 1;}for (int i = 1; i < n; i++){fact *= i;}return fact;
}
Mat CreateDiff(int n);
void my_sobel(Mat& src, Mat dst, int x_flag, int y_flag, int winsize, int borderType);
// 得到一个平滑算子 n为 窗口的大小
Mat Getsmooth(int n)
{Mat smooth = Mat::zeros(Size(n,1),CV_32FC1);for (int i = 0; i < n; i++){smooth.at<float>(0, i) = factorial(n - 1) / (factorial(i) * (factorial(n - i - 1)));}//当窗口 3 二项式展开的系数是 1 2 1 差分算子是 1 0 -1 //当窗口 5 二项式展开的系数是 1 4 6 4 1 差分算子是 1 2 0 -2 -1return smooth;
}//从上面的的二项式系数中 得到 差分算子
Mat CreateDiff(int n)
{Mat Diff = Mat::zeros(Size(n, 1), CV_32FC1);Mat Diff_pres = Getsmooth(n-1);// n=5 是返回的是1 3 3 1for (int i = 0; i < n; i++){if (i == 0){Diff.at <float>(0, i) = 1;}else if (i == n - 1){Diff.at <float>(0, i) = -1;}else{Diff.at <float>(0, i) = Diff_pres.at<float>(0, i) - Diff_pres.at<float>(0, i - 1);}}//当n=3 时候,Diff_pres= 1, 1 Diff_pres.at<float>(0, i)=1, Diff_pres.at<float>(0, i - 1)=1 // Diff =1 0 -1//当n=5 时候,Diff_pres= 1 3,3 1 // Diff =1 2 0 -2 -1return Diff; }
// 是用sobel 算子,对图像完成卷积 ,当 x-flag=0,-->x的
void my_sobel(Mat& src, Mat dst, int x_flag, int y_flag, int winsize, int borderType)
{// winsize 应该是》=3的奇数CV_Assert(winsize>=3&& winsize%2==1);// 得到 二项式的系数Mat smooth = Getsmooth(winsize);// 得到差分系数Mat smoothdiff = CreateDiff(winsize);//当x_flag!=0 是, 返回图像与水平方向上的卷积if (x_flag != 0){// smooth.t() 表示矩阵的转置sepFilter2D(src, dst, CV_32FC1, smooth.t(), smoothdiff, Point(-1,-1), borderType);}// 当x_flag==0 && y_flag!=0 的时候 垂直方向的if (x_flag == 0 && y_flag != 0){sepFilter2D(src, dst, CV_32FC1, smooth, smoothdiff.t(), Point(-1, -1), borderType);}}int main(int args, char* arg)
{Mat src, src_gray, dest1, dest2, dest3, dest4, dest5, dest6;// point src = imread("C:\\Users\\19473\\Desktop\\opencv_images\\305.jpg");if (!src.data){printf("could not load image....\n");}cvtColor(src, src_gray, COLOR_BGR2GRAY);imshow("原图", src_gray);// sobel X方向上的算子Mat sobel_kernel_x = (Mat_<int>(3, 3) << 1, 0, -1, 2, 0, -2, 1, 0, -1);filter2D(src_gray, dest3, -1, sobel_kernel_x, Point(-1, -1), 0.0);imshow("Opencv sobel_X", dest3);// sobel Y 方向上的算子Mat sobel_kernel_y = (Mat_<int>(3, 3) << 1, 2, 1 , 0, 0, 0, -1, -2, -1);filter2D(src_gray, dest4, -1, sobel_kernel_y, Point(-1, -1), 0.0);imshow("Opencv sobel_Y", dest4);//================================================================// 自定义sobel算法my_sobel(src_gray, dest3, 1, 0, 3, BORDER_DEFAULT);imshow("自定义-水平方向", dest3);my_sobel(src_gray, dest3, 0, 1, 3, BORDER_DEFAULT);imshow("自定义-垂直方向", dest4);waitKey(0);return -1;
}
4、 Prewitt 边缘检测
代码如下:
// 分离卷积运算void conv2d(InputArray src, InputArray kernel, OutputArray dst, int ddepth,Point achor = Point(-1, -1),int borderType = BORDER_DEFAULT)
{// 卷积运算的第一步,将卷积核逆时针旋转180度 。。 主要是为了计算方便Mat kernelFlip;flip(kernel, kernelFlip,-1);// 第二步才开始计算filter2D(src,dst, ddepth, kernelFlip, achor,0.0, borderType);//InputArray src, OutputArray dst, int ddepth,// InputArray kernel, Point anchor = Point(-1, -1),// double delta = 0, int borderType = BORDER_DEFAULT
}
// 卷积的顺序不一样void Mysepfilter2D_YX_order( InputArray src, OutputArray src_xy, int ddepth, InputArray kernel_y,InputArray kernel_x, Point achor = Point(-1, -1), int border_type = BORDER_DEFAULT)
{Mat tempk;conv2d(src, kernel_y, tempk, ddepth, achor, border_type);conv2d(tempk, kernel_x, src_xy, ddepth, achor, border_type);
}void Mysepfilter2D_XY_order(InputArray src, OutputArray src_xy, int ddepth, InputArray kernel_x,InputArray kernel_y, Point achor = Point(-1, -1), int border_type = BORDER_DEFAULT)
{Mat tempk;conv2d(src, kernel_x, tempk, ddepth, achor, border_type); // 垂直conv2d(tempk, kernel_y, src_xy, ddepth, achor, border_type);// 水平
}void myPrewitt( InputArray src, OutputArray dst, int ddepth, int x, int y, Point achor = Point(-1, -1), int border_type = BORDER_DEFAULT)
{Mat prewitt_xy = (Mat_<float>(3, 1) << 1, 1, 1);Mat prewitt_xx = (Mat_<float>(1, 3) << 1, 1, 1);Mat prewitt_yx = (Mat_<float>(3, 1) << 1, 1, 1);Mat prewitt_yy = (Mat_<float>(1, 3) << 1, 1, 1);if (x!=0&&y==0){Mysepfilter2D_YX_order(src,dst, ddepth, prewitt_xy, prewitt_xx);}if (y != 0 && x == 0){Mysepfilter2D_YX_order(src, dst, ddepth, prewitt_yx, prewitt_yy);}}
int main(int args, char* arg)
{Mat src, src_gray, dest1, dest2, dest3, dest4, dest5, dest6;// point src = imread("C:\\Users\\19473\\Desktop\\opencv_images\\305.jpg");if (!src.data){printf("could not load image....\n");}cvtColor(src, src_gray, COLOR_BGR2GRAY);imshow("原图", src_gray);// prewitt 卷积Mat p_x;myPrewitt(src_gray, p_x,CV_32FC1,1,0);Mat p_y;myPrewitt(src_gray, p_y, CV_32FC1, 0, 1);// 水平方向和垂直方向的 边缘强度// 数据类型转换。边缘强度的灰度显示Mat abs_image_prewitt_x, abs_image_prewitt_y;convertScaleAbs(p_x, abs_image_prewitt_x,1,0);convertScaleAbs(p_y, abs_image_prewitt_y,1,0);imshow("垂直方向边缘",abs_image_prewitt_x);imshow("水平方向边缘", abs_image_prewitt_y);// 通过上面求得的两个方向上的边缘求得最终的边缘强度//Mat abs_image_prewitt_x2, abs_image_prewitt_y2;//pow(abs_image_prewitt_x, 2.0, abs_image_prewitt_x2);//pow(abs_image_prewitt_y, 2.0, abs_image_prewitt_y2);//Mat edge=Mat::zeros(src.size(),CV_32F);//sqrt((abs_image_prewitt_x2 + abs_image_prewitt_y2), edge);edge.convertTo(edge,CV_8UC1);imshow("边缘强度", edge);waitKey(0);return -1;
}
5、 Canny(最常用的算子)
基于卷积运算的边缘检测算法,比如sobel prewitt算子有几个缺点:
a、没有充分的利用梯度的方向
b、最后输出的边缘二值图只是简单的利用了阈值进行处理,当阈值很大的时候就会损失很多的边缘,反之亦然
那么canny 是这这个基础上进行了一部分的优化:(1) 基于边缘梯度方向的非极大值抑制。
(2) 双阈值的滞后阈值处理。
Canny 边缘检测的近似算法的步骤如下
第一步:图像矩阵I 分别与水平方向上的卷积核和垂直方向上的卷积核卷积得到dx和dy ,然后利用平方和的开方magnitude=sqrt(dx^2+dy^2)得到边缘强度。举例如下:
sobel 在边缘检测的过程中将值大于255的截断为255然后得到一个二值图像。
第二步:利用第一步计算出的dx和dy ,计算出梯度方向angle=arctan 2(dy ,dx), 即对每一个位置(r,c),angle(r,c)=arctan 2(dy (r,c),dx(r,c))代表该 位置的梯度方向,一般用角度表示,即angle(r,c)∈[0,180]∪[-180,0]。得到一个关于角度的矩阵angle:
以上图133中心点为例
水平方向:dx=(154-133)
垂直方向:dx=(175-133)
第三步:对每一个位置进行非极大值抑制的处理 ,magnitude是从第一步里面得到的及边缘矩阵,在这里要进行一个非极大值抑制的处理,所以在边缘扩充的时候用的是补零的方式。
上图右边的图像中,左边为nonMaxSup(1,1)的值为912,那么现在需要做的是严重这条梯度方向的线走,经过邻域区域(我们目前默认是3x3的邻域)的值和(1,1)的值(912)做对比,若nonMaxSup(1,1)>是由于的这条线上邻域的值,那么(1,1)就是极大值,则nonMaxSup(1,1)=magnitude(1,1)=912,若nonMaxSup(1,1)不全大于是由于的这条线上邻域的值,则nonMaxSup(1,1)=0。用同样的方法计算nonMaxSup(1,2)的值得到nonMaxSup(1,2)=0。
总结上述非极大值抑制的过程:如果magnitude(r,c)在沿着梯度方向angle(r, c)上的邻域内是最大的则为极大值;否则,设置为0。
非极大值抑制的实现:
非极大值抑制的第二种方式:用插值法拟 合梯度方向上的边缘强度,这样会更加准确地衡量梯度方向上的边缘强度
假设 M=
插值方式:L1::L2=M:(1-M),那么可以近似的算出左上角的插值 P1= M*292+(1-M)*720,同理可以算出右下角:P1= M*276+(1-M)*560,然后比较P1 和P2再和920比较大小,若920>P1&&920>P2,则为极大值,否则不是极大值。一般将梯度方向离散化为以下四种情况:
· angle(r,c)∈(45,90]∪(-135,-90] · angle(r,c)∈(90,135]∪(-90,-45] · angle(r,c)∈[0,45]∪[-180,-135] · angle(r,c)∈(135,180]∪(-45,0)
第四步:双阈值的滞后阈值处理。经过上一步的极大值抑制处理之后,一般需要阈值化处理(threshold 和 adaptiveThreshold),常用阈值滞后方法:高阈值 低阈值,具体方法如下:(1) 边缘强度大于高阈值的那些点作为确定边缘点。
(2) 边缘强度比低阈值小的那些点立即被剔除。
(3) 边缘强度在低阈值和高阈值之间的那些点,可以理解为,首先选定边缘强度大于高阈 值的所有确定边缘点,然后在边缘强度大于低阈值的情况下尽可能延长边缘(该像素仅仅在连接到一个高于高阈值的像素时被保留)。
上述所说的高阈值一般是低阈值的2~3倍。
Canny算子边缘检测流程
算子解释:
void Canny(inputArray,outputArray,double threshold1,double threshold2,int apertureSize=3,bool L2gradient=false)
*第一个参数,输入图像,且需为单通道8位图像。
*第二个参数,输出的边缘图。
*第三个参数,第一个滞后性阈值。用于边缘连接。
*第四个参数,第二个滞后性阈值。用于控制强边缘的初始段,高低阈值比在2:1到3:1之间。
*第五个参数,表明应用sobel算子的孔径大小,默认值为3。
*第六个参数,bool类型L2gradient,一个计算图像梯度幅值的标识,默认值false。
代码显示:
Mat src, src_gray, dst,dx,dy;
int t1_value = 50;
int max_value = 255;
const char* OUTPUT_TITLE = "Canny Reult";void CannyCallback(int, void*);
void CannyCallback(int, void*)
{Mat edge_output;// 2 模糊之后的图像 8位的灰度图图像blur(src_gray, src_gray, Size(3, 3), Point(-1, -1), BORDER_DEFAULT);// 3 使用canny 算子 阈值越低 细节越多Canny(src_gray, edge_output, t1_value, t1_value * 2, 3, false);dst.create(src.size(), src.type());src.copyTo(dst, edge_output);imshow(OUTPUT_TITLE, ~dst);// ~dst
}Mat non_max_supprusion(Mat dx ,Mat dy) //传进来的是两个方向上的差分矩阵 3*3 的掩膜
{//边缘强度=sqrt(dx 的平方+dy 的平方) Mat edge;magnitude(dx, dy, edge);// 计算幅度值int rows = dx.rows;int cols = dy.cols;//边缘强度的非极大值抑制Mat edgemag_nonMaxSup = Mat::zeros(dx.size(),dx.type());// 用两个循序计算出 和梯度方向 并且转换成angleMatrixfor (int row = 1; row < rows-1; row++){for (int col = 1; col < cols-1; col++){float x = dx.at<float>(row, col);float y = dx.at<float>(row, col);// 梯度的方向---atan2ffloat angle = atan2f(y, x) / CV_PI * 180;// 当前位置的边缘强度float mag = edge.at<float>(row, col);// 找到左右两个方向if (abs(angle)<22.5||abs(angle)>157.5) {float left = edge.at<float>(row, col - 1);float right = edge.at<float>(row, col + 1);// 判断两个方向上的if (mag>left&&mag>right) {edgemag_nonMaxSup.at<float>(row, col) = mag;}}// 左上和右下两个方向if ((abs(angle) >= 22.5 && abs(angle) < 67.5)|| (abs(angle) <-112.5 && abs(angle) > 157.5)){float lefttop = edge.at<float>(row-1, col - 1);float rightbottom = edge.at<float>(row+1, col + 1);// 判断两个方向上的if (mag > lefttop && mag > rightbottom) {edgemag_nonMaxSup.at<float>(row, col) = mag;}}// 上 下 方向if ((abs(angle) >= 67.5 && abs(angle) <= 112.5) || (abs(angle) >= -112.5 && abs(angle) <= -67.5)){float top = edge.at<float>(row - 1, col);float down = edge.at<float>(row + 1, col);// 判断两个方向上的if (mag > top && mag > down) {edgemag_nonMaxSup.at<float>(row, col) = mag;}}// 右上 左下 方向if ((abs(angle) > 122.5 && abs(angle) < 157.5) || (abs(angle) > -67.5 && abs(angle) <=-22.5)){float leftdown = edge.at<float>(row - 1, col+1);float rightup = edge.at<float>(row + 1, col-1);// 判断两个方向上的if (mag > leftdown && mag > rightup) {edgemag_nonMaxSup.at<float>(row, col) = mag;}}}}return edgemag_nonMaxSup;
}Mat non_max_supprusion_inter(Mat dx, Mat dy) //non_max_supprusion的改进,使得非极大值抑制更加完整
{//边缘强度=sqrt(dx 的平方+dy 的平方) Mat edge;dx.convertTo(dx,CV_32F);// CV_32FC1dy.convertTo(dy, CV_32F);// CV_32FC1magnitude(dx, dy, edge);// 计算幅度值int rows = dx.rows;int cols = dy.cols;//边缘强度的非极大值抑制Mat edgemag_nonMaxSup = Mat::zeros(dx.size(), dx.type());// 用两个循序计算出 和梯度方向 并且转换成angleMatrixfor (int row = 1; row < rows - 1; row++){for (int col = 1; col < cols - 1; col++){float x = dx.at<float>(row, col);float y = dx.at<float>(row, col);// 梯度的方向---atan2fif (x==0 && y==0){continue;}float angle = atan2f(y, x) / CV_PI * 180;// 邻域内8个方向上的边缘处理float left = edge.at<float>(row, col - 1); // 左float right = edge.at<float>(row, col + 1); // 右float lefttop = edge.at<float>(row - 1, col - 1); // 左上float rightbottom = edge.at<float>(row + 1, col + 1); //右下float top = edge.at<float>(row - 1, col); // 上float bottom = edge.at<float>(row + 1, col); // 下float leftdown = edge.at<float>(row - 1, col + 1); // 做下float rightup = edge.at<float>(row + 1, col - 1);// 右上// 当前位置的边缘强度float mag = edge.at<float>(row, col);// 找到左上 和上面的方向 以及 右下 下面if ((abs(angle) >45 || abs(angle) <=90)|| (abs(angle) > -135 || abs(angle) <= -90)){float ratio = x / y;float top= edge.at<float>(row - 1, col);// c插值float lefttop_top = ratio * lefttop + (1 - ratio) * top;float rightdown_down = ratio * rightbottom + (1 - ratio) * bottom;// 判断两个方向上的if (mag > lefttop_top && mag > rightdown_down) {edgemag_nonMaxSup.at<float>(row, col) = mag;}}// 找到右上 和上面的方向 以及 左下 下面if ((abs(angle) > 90 || abs(angle) <= 135) || (abs(angle) > -90 || abs(angle) <= -45)){float ratio =abs( x / y);float top = edge.at<float>(row - 1, col);// c插值float rithttop_top = ratio * rightup + (1 - ratio) * top;float leftdown_down = ratio * leftdown + (1 - ratio) * bottom;// 判断两个方向上的if (mag > rithttop_top && mag > leftdown_down) {edgemag_nonMaxSup.at<float>(row, col) = mag;}}// 找到左上 和左的方向 以及 右下 右面if ((abs(angle) > 0 || abs(angle) <= 45) || (abs(angle) > -180 || abs(angle) <= -135)){float ratio = x / y ;float top = edge.at<float>(row - 1, col);// c插值float rithtbottom_right = ratio * rightbottom + (1 - ratio) * right;float leftup_left = ratio * lefttop + (1 - ratio) * left;// 判断两个方向上的if (mag > rithtbottom_right && mag > leftup_left) {edgemag_nonMaxSup.at<float>(row, col) = mag;}}// 找到右上 和右 的方向 以及 左下 左面if ((abs(angle) > 135 || abs(angle) <= 180) || (abs(angle) > -45 || abs(angle) <= -0)){float ratio = abs(x / y);float top = edge.at<float>(row - 1, col);// c插值float rithttop_right = ratio * rightup + (1 - ratio) * right;float leftdown_left = ratio * leftdown + (1 - ratio) * bottom;// 判断两个方向上的if (mag > rithttop_right && mag > leftdown_left) {edgemag_nonMaxSup.at<float>(row, col) = mag;}}}}printf("edgemag_nonMaxSup is over");return edgemag_nonMaxSup;
}bool CheckInRange(int r,int c,int rows,int cols) { // 确定一个点是不是在给出的图像范围里面if (r >= 0 && r < rows && c>= 0 && c < cols){return true;}else{return false;}
}void trace(Mat edge_nonMaxSup, Mat& edge, float lower, int r, int c, int rows, int cols) {// 从确定边缘点出发,延长边缘if (edge.at<uchar>(r,c)==0){edge.at<uchar>(r, c) = 255;// 3*3 的掩膜for (int i = -1; i <= 1; i++){for (int j = -1; j <= 1; j++){float mag = edge_nonMaxSup.at<float>(r + i, c + j);if (CheckInRange(r + i, c + j,rows,cols)&&mag>=lower){//printf("trace");trace(edge_nonMaxSup,edge, lower,r+i,c+j,rows,cols); // 用递归来找边缘的点}}}}
}Mat hysteresisThreshold(Mat edge_mag_momMaxSup,float lower,float hight)
{int rows = edge_mag_momMaxSup.rows;int cols = edge_mag_momMaxSup.cols;// 最后输出的图像Mat edgeImage = Mat::zeros(Size(rows,cols),CV_8UC1);// 滞后阈值处理for (int i = 1; i < rows-1; i++){for (int j = 1; j < cols - 1; j++){if (i == 349&&j>=354)printf("baocuo %d\n",j);float mag = edge_mag_momMaxSup.at<float>(i, j);// mag >hight --->肯定是边缘点,并将这个点做为起始点的延长点,点集合依次扩充if (mag >= hight){trace(edge_mag_momMaxSup, edgeImage, lower, i, j, rows, cols);}// 小于低阈值的肯定不是if (mag < lower) {edgeImage.at<uchar>(i, j) = 0;}}}printf("hysteresisThreshold");return edgeImage;
}int main(int args, char* arg)
{// point src = imread("C:\\Users\\19473\\Desktop\\opencv_images\\lover.jpg");if (!src.data){printf("could not load image....\n");}namedWindow("input_demo", CV_WINDOW_AUTOSIZE);namedWindow(OUTPUT_TITLE, CV_WINDOW_AUTOSIZE);imshow("input_demo", src);cvtColor(src, src_gray, CV_BGR2GRAY);//createTrackbar("Thread_Value:", OUTPUT_TITLE, &t1_value, max_value, CannyCallback);//CannyCallback(0, 0);// 自定义cannY算子//==================================================================// 1 , sobel X方向上的算子// sobel X方向上的算子Mat sobel_kernel_x = (Mat_<int>(3, 3) << 1, 0, -1, 2, 0, -2, 1, 0, -1);filter2D(src_gray, dx, -1, sobel_kernel_x, Point(-1, -1), 0.0);imshow("solel_dx", dx);// sobel Y 方向上的算子Mat sobel_kernel_y = (Mat_<int>(3, 3) << 1, 2, 1, 0, 0, 0, -1, -2, -1);filter2D(src_gray, dy, -1, sobel_kernel_y, Point(-1, -1), 0.0);imshow("solel_dy", dy);// 2 生产 非极大值抑制滞后的矩阵Mat edge_mag_momMaxSup,edgeImage;edge_mag_momMaxSup=non_max_supprusion_inter(dx,dy);// 调用这个阈值滞后处理函数edgeImage=hysteresisThreshold(edge_mag_momMaxSup,100,200);imshow("自定义Canny", edgeImage);waitKey(0);return 0;
}
上面的代码中hysteresisThreshold中有点问题,找了好几个小时没有找出问题,为了不耽搁后面的学习,这问题暂时留下,后续更新
6、拉普拉斯算子(二阶算子)
仔细上圈出来的图像,拉普拉斯算子不可以分解为水平方向和竖直方向的分量来计算,最常用的任然是上面4和-4的核,所以计算的时候也是直接用卷积来计算,也就是fliter2D这个算子,后面补一期filter2d的源码,这里不多做赘述。
注意:Laplacian边缘检测算子不像Sobel和Prewitt算子那样对图像进行了平滑处理,所以它 会对噪声产生较大的响应,误将噪声作为边缘,并且得不到有方向的边缘,所以在做拉普拉斯卷积之前要进行高斯模糊
算子解释:
Laplacian( src_gray, dst, ddepth, kernel_size, scale, delta, BORDER_DEFAULT );
src_gray: 输入图像的深度是 CV_8U
dst: 输出图像
ddepth: 输出图像的深度。 因为输入图像的深度是 CV_8U ,这里我们必须定义 ddepth = CV_16S 以避免外溢
kernel_size: 从上面的推导公式中可以看出默认是3*3的核,如果是5*5的那么推导公式就很复杂,但是原理一样。
scale,这个的值是-1或者1,如果是-1 则便是上面中心位置的值是4 ,如果是1,则表示为-4
delta,
BORDER_DEFAULT: 使用默认值。
CV_8U : convertScaleAbs( dst, abs_dst );主要是将灰度值放到0~255之间,是一个截断值
图像显示:
代码显示:
int main(int args, char* arg)
{Mat src, src_gray, edge_laplance, laplance, edge_mohu;// point src = imread("C:\\Users\\19473\\Desktop\\opencv_images\\88.jpg");if (!src.data){printf("could not load image....\n");}namedWindow("input_demo", CV_WINDOW_AUTOSIZE);imshow("input_demo", src);//0 高斯模糊GaussianBlur(src, edge_mohu,Size(3,3),0.0,0);//1. 将这个图像转换成灰度图像cvtColor(edge_mohu, src_gray, CV_BGR2GRAY);namedWindow("Gray_demo", CV_WINDOW_AUTOSIZE);imshow("Gray_demo", src_gray);//2 laplace Mat scharr_x, scharr_y, scharr;Laplacian(src_gray, edge_laplance,CV_16S,3);// 这里一定是CV_16S// 3 去绝对值convertScaleAbs(edge_laplance, laplance);namedWindow("Output_Lapalce_demo", CV_WINDOW_AUTOSIZE);// 4 显示imshow("Output_Lapalce_demo", laplance);waitKey(0);return 0;
}
7、Scharr、Kirsch、Robinson (了解)
Scharr:标准的Scharr边缘检测算子与Prewitt边缘检测算子和3阶的Sobel边缘检测算子类似, 由以下两个卷积核:
Scharr边缘检测算子也可以扩展到其他方向,比如:
Kirsch算子[6]由以下8个卷积核:
Robinson算子[4]也是由8个卷积核:
8、Log-高斯拉普拉斯边缘检测
拉普拉斯边缘检测算子没有对图像做平滑处理,会对噪声产生明显的响应,所以在 用拉普拉斯核进行边缘检测时,首先要对图像进行高斯平滑处理,然后再与拉普拉斯核 进行卷积运算,有二维高斯函数如下:
高斯边缘检测的步骤如下:
第一步:构建窗口大小为H×W、标准差为σ的LoG卷积核,如上所示,
第二步:图像矩阵与LoGH×W 核卷积,结果记为I_Cov_LoG
第三步:边缘二值化显示(有白色显示和黑色显示两种)
c++实现:在实现的过程中还是用到了高斯核分离的性质(Opencv 笔记3 图像平滑_Σίσυφος1900的博客-CSDN博客),二阶分离是一个比较麻烦推导,由于推导和实现都比较复杂,因此Opencv提出了一个一阶近似模型(Dog),可以简化计算,这里知道原理就可以了。
实现步骤:第一步:构建高斯核
第二步 :图像矩阵先与水平方向上的卷积核卷积,然后再与垂直方向上的卷积核卷 积。
第三步:与第一步相反,图像矩阵先与垂直方向上的卷积核卷积,然后再与水平方 向上的卷积核卷积。
第四步:将第一步和第二步得到的卷积结果相加,其中对于分离卷积仍然使用“图像 平滑”,具体代码如下:
代码实现
void conv2d(InputArray src, InputArray kernel, OutputArray dst, int ddepth,Point achor = Point(-1, -1),int borderType = BORDER_DEFAULT)
{// 卷积运算的第一步,将卷积核逆时针旋转180度 。。 主要是为了计算方便Mat kernelFlip;flip(kernel, kernelFlip, -1);// 第二步才开始计算filter2D(src, dst, ddepth, kernelFlip, achor, 0.0, borderType);//InputArray src, OutputArray dst, int ddepth,// InputArray kernel, Point anchor = Point(-1, -1),// double delta = 0, int borderType = BORDER_DEFAULT
}// 卷积的顺序不一样
void sepConv2D_Y_X(InputArray src, OutputArray src_xy, int ddepth, InputArray kernel_y,InputArray kernel_x, Point achor = Point(-1, -1), int border_type = BORDER_DEFAULT)
{Mat tempk;conv2d(src, kernel_y, tempk, ddepth, achor, border_type);conv2d(tempk, kernel_x, src_xy, ddepth, achor, border_type);
}void sepConv2D_X_Y(InputArray src, OutputArray src_xy, int ddepth, InputArray kernel_x,InputArray kernel_y, Point achor = Point(-1, -1), int border_type = BORDER_DEFAULT)
{Mat tempk;conv2d(src, kernel_x, tempk, ddepth, achor, border_type); // 垂直conv2d(tempk, kernel_y, src_xy, ddepth, achor, border_type);// 水平
}void getSepLoGKernel(Mat &kx,Mat &ky,float sigma,int ksize) // 构建高斯核
{kx.create(Size(ksize, 1),CV_32FC1);ky.create(Size(1,ksize), CV_32FC1);int center = (ksize - 1) / 2;double sigma2 = pow(sigma, 2.0);// 构建可分离的高斯拉普拉斯核 for (int i = 0; i < ksize; i++){float dist2 = pow(i - center, 2.0);// ky.at<float>(i, 0) = exp(-dist2 / (sigma2 * 2));kx.at<float>(0, i) = (dist2 / sigma2 - 1.0) * ky.at<float>(i, 0);}
}
// 开始卷积
Mat Log(InputArray image, float sigma, int ksize)
{Mat kx, ky;// 1,先得到两个卷积分离核getSepLoGKernel(kx,ky, sigma, ksize);// 2、水平---垂直Mat conXY;sepConv2D_X_Y(image, conXY,CV_32FC1,kx,ky); //3、垂直 -水平 ,如果是一阶的话就没有这一步了Mat kx_t, ky_t;kx_t = kx.t();ky_t = ky.t(); // 矩阵的转置Mat conYX;sepConv2D_Y_X(image, conYX, CV_32FC1, kx_t, ky_t);Mat logCov;add(conXY, conYX, logCov);return logCov;
}
9、Gog-高斯差分
二维高斯函数对σ的一阶偏导数如下
第一步:构建窗口大小为H×W 的高斯差分卷积核。
第二步:图像矩阵与DoGH×W 核卷积,结果记为I_Cov_DoG。高斯拉普拉斯与高斯差分
第三步:与拉普拉斯边缘检测相同的二值化的显示。
高斯差分核是两个非归一化的高斯核的差,已知高斯核又是可分离的,所以真正在 用程序实现时,为了减少计算量,可以不用创建高斯差分核,而是根据卷积的加法分配 率和结合律的性质,图像矩阵分别与两个高斯核卷积,然后做差,用来代替第一步和第 二步操作。
效果显示
代码显示
void conv2d(InputArray src, InputArray kernel, OutputArray dst, int ddepth,Point achor = Point(-1, -1),int borderType = BORDER_DEFAULT)
{// 卷积运算的第一步,将卷积核逆时针旋转180度 。。 主要是为了计算方便Mat kernelFlip;flip(kernel, kernelFlip, -1);// 第二步才开始计算filter2D(src, dst, ddepth, kernelFlip, achor, 0.0, borderType);//InputArray src, OutputArray dst, int ddepth,// InputArray kernel, Point anchor = Point(-1, -1),// double delta = 0, int borderType = BORDER_DEFAULT
}// 卷积的顺序不一样
void sepConv2D_Y_X(InputArray src, OutputArray src_xy, int ddepth, InputArray kernel_y,InputArray kernel_x, Point achor = Point(-1, -1), int border_type = BORDER_DEFAULT)
{Mat tempk;conv2d(src, kernel_y, tempk, ddepth, achor, border_type);conv2d(tempk, kernel_x, src_xy, ddepth, achor, border_type);
}void sepConv2D_X_Y(InputArray src, OutputArray src_xy, int ddepth, InputArray kernel_x,InputArray kernel_y, Point achor = Point(-1, -1), int border_type = BORDER_DEFAULT)
{Mat tempk;conv2d(src, kernel_x, tempk, ddepth, achor, border_type); // 垂直conv2d(tempk, kernel_y, src_xy, ddepth, achor, border_type);// 水平
}Mat getSepDoGKernel(Mat image, float sigma, int ksize) // 构建高斯核 非归一化的
{// 1、构建一个水平方向的Mat xk = Mat::zeros(1, ksize, CV_32FC1);int center = (ksize - 1) / 2;float sg2 = pow(sigma, 2.0); // 方差for (int i = 0; i < ksize; i++){float n2 = pow(i-center,2.0);xk.at<float>(0,i) = exp(-n2 / (2 * sg2));}// 因为高斯核是对称的,所以可以可以用转置来计算y方向的Mat yk = xk.t();Mat gausscore;sepConv2D_X_Y(image, gausscore, CV_32FC1, xk, yk);gausscore.convertTo(gausscore, CV_32F,1.0/sg2);return gausscore;
}
// 开始卷积
Mat DoG(Mat image, float sigma, int ksize,float k=1.1)
{// 1,得到高斯核Mat kernel = getSepDoGKernel(image,sigma,ksize);// 2、与标准差k*sigma 的非归一化高斯卷积Mat kernelK = getSepDoGKernel(image, k*sigma, ksize);// 3 两个做差分Mat dogCov= kernelK- kernel;return dogCov;
}
Mat src, src_gray, dest1, dest2, dest3, dest4, dest5, dest6;
const char* win = "自定义Dog";int main(int args, char* arg)
{// point src = imread("C:\\Users\\19473\\Desktop\\opencv_images\\88.jpg");if (!src.data){printf("could not load image....\n");}//1 显示灰色图像namedWindow("input_demo", CV_WINDOW_AUTOSIZE);cvtColor(src, src_gray, CV_BGR2GRAY);imshow("input_demo", src_gray);Mat dogCov = DoG(src_gray, 2, 13,1.05);Mat edge;threshold(dogCov, edge, 0, 255, THRESH_BINARY);imshow(win, edge);waitKey(0);return 0;
}
遗留问题;canny 自定义计算的时候有问题
Opencv 笔记5 边缘处理-canny、sobel、Laplacian、Prewitt相关推荐
- OpenCV 坎尼边缘检测器Canny Edge Detector
OpenCV 拉普拉斯算子Laplace Operator 坎尼边缘检测器Canny Edge Detector 目标 理论 步骤 代码 这个程序做什么? 说明(C ++代码) 结果 坎尼边缘检测器C ...
- OpenCV 笔记 -- 边缘检测(Sobel、Laplace、Canny)
OpenCV 笔记 – 边缘检测(Sobel.Laplace.Canny) 参考文档 一.Sobel 算子 1.简介 Sobel 算子是一个离散的一阶差分算子,用来计算图像亮度函数的一阶梯度近似值.在 ...
- OpenCV学习笔记(十二):边缘检测:Canny(),Sobel(),Laplace(),Scharr滤波器
OpenCV学习笔记(十二):边缘检测:Canny(),Sobel(),Laplace(),Scharr滤波器 1)滤波:边缘检测的算法主要是基于图像强度的一阶和二阶导数,但导数通常对噪声很敏感,因此 ...
- 《opencv学习笔记》-- 边缘检测和canny算子、sobel算子、LapIacian 算子、scharr滤波器
目录 边缘检测 canny算子 sobel算子 LapIacian 算子 (拉普拉斯) scharr滤波器 边缘检测 边缘检测步骤: 1.滤波: 边缘检测的算法主要是基于图像强度的一阶和二阶导数,但导 ...
- python+OpenCv笔记(十三):边缘检测——Sobel检测算子
Sobel检测算子 概述: Sobel边缘检测算法比较简单,实际应用中效率比canny边缘检测效率要高,但是边缘不如Canny检测的准确,但是很多实际应用的场合,sobel边缘却是首选,Sobel算子 ...
- 边缘检测Sobel、Canny、Laplacian
1.Sobel算子 1.1Sobel 步骤 1.分别在x和y两个方向求导. 2.在图像的每一点,结合以上两个结果求出近似梯度: 1.1.1 分别在x和y两个方向求导. x方向 y方向 1.1.2 在图 ...
- 边缘检测的各种微分算子比较(Sobel,Robert,Prewitt,Laplacian,Canny)
边缘检测的各种微分算子比较(Sobel,Robert,Prewitt,Laplacian,Canny) 不同图像灰度不同,边界处一般会有明显的边缘,利用此特征可以分割图像.需要说明的是:边缘和物体间的 ...
- OpenCV笔记—进阶篇(图像效果处理)
OpenCV笔记-进阶(图像效果处理) 此章节是基于本人OpenCV笔记-基础篇的延续,如果对此章节有疑惑的话,可以先看基础篇学习一下. 通过此篇可以学习到美图秀秀等P图软件实现图片效果原理及实现过程 ...
- c++gdal如何在大图像中截取小图像并获取其图像信息_【图像处理】OpenCV系列十 --- 边缘检测之Canny算子...
上一篇我们学习了图像处理形态学相关知识点,相信大家学习之后已经对形态学有了足够的理解了,那么接下来,我们一起来学习一下图像处理中的边缘检测吧!我们将会重点学习边缘检测各种算子和滤波器 --- Cann ...
最新文章
- 《40期》 我们要把世纪末日变成重生日
- Java 理论与实践: 非阻塞算法简介——看吧,没有锁定!(转载)
- Java 8 一行代码解决了空指针问题,太厉害了...
- python操作手机京东_Python实现自动上京东抢手机
- 批量 杀掉 mysql 连接_批量杀死MySQL连接的四种方法详解
- WINHEX的数据结构模板
- ThreadLocal使用和原理
- 闲谈:我为什么热爱开源
- JVM004_字节码指令简介
- php字符串原地反转,php反转字符串方法
- 原力计划·精英季来了!第一周周榜揭晓,你喜欢的博主上榜了吗?
- 周志华《机器学习》读书笔记与习题答案(持续更新)
- 【Unity】Geometry Shader实现
- 机器学习数学基础--偏导数
- 【笔记】逻辑门图解—与门、或门、非门、与非门、或非门、异或门、同或门
- 智慧城市系列之智能交通系统(ITS)
- Couchbase集群
- coodblock调试_codeblocks怎么调试?
- 数字信号处理基础----正交基与正交函数集
- [系统安全] 十八.病毒攻防机理及WinRAR恶意劫持漏洞(bat病毒、自启动、定时关机、蓝屏攻击)