边缘检测
一、边缘检测的理解
边缘一般是指图像在某一局部强度剧烈变化的区域。强度变化一般有两种情况:
阶跃变化: 像数值从低到高变化,图像从暗到亮
山顶变化:由暗到亮再到暗,两边较暗,中心突出
边缘检测实际上是找到两种强度变化的集合。
既然边缘是灰度变化最剧烈的位置,最直观的想法就是求差分(相邻像素点的差值)。
用差分的方法进行边缘检测必须使差分的方向和边缘的方向相垂直,这就需要对图像的不同方向分别进行差分运算,增加了运算量。一般可将边缘分为水平边缘、垂直边缘和对角线边缘。
对于第一种情况:一阶差分的峰值为边缘点,二阶差分的零点为边缘点。
对于第二种情况:一阶差分的零点为边缘点,二阶差分的峰值为边缘点。
二、常用边缘检测算子
1、普通梯度算子:
普通梯度算子也叫正交梯度算子,分别求像素点上方与像素点的梯度和像素点左边与像素点的梯度值。
[ 0 0 [0 1
−1 1] 0 -1]
2、Roberts算子:
Roberts算子与普通梯度算子类似,都是取一阶的差分作为梯度,区别在于取值的位置:对角取值
3、Prewitt算子:
Prewitt结合了差分与邻域平均的思想,其卷积核如下
水平卷积核: 垂直卷积核
4、Sobel算子:
Sobel算子是在Prewitt算子的基础上加入了权值的思想,离像素点越近权值越高
5、拉普拉斯算子:
拉普拉斯变换是工程数学中常用的一种积分变换;拉普拉斯算子是n维欧几里得空间的一个二阶微分算子;
6、LoG算子:
Laplacian of Gaussian,就是高斯拉普拉斯算子,原理为对高斯分布公式进行二阶微分
高斯函数为:
7、Canny算子:
Canny算子分为四步:图像降噪、计算图像梯度、非极大值抑制、阈值筛选
1.图像降噪
梯度算子本质上是描述图像灰度突出值的算子,所以受到噪声影响很大,因为噪声表现为突出的异常数据点,所以第一步需要降噪,一般使用高斯滤波降噪;
2.计算图像梯度
计算图像梯度能够得到图像的边缘,因为梯度是灰度变化明显的地方,而边缘也是灰度变化明显的地方。当然这一步只能得到可能的边缘。因为灰度变化的地方可能是边缘,也可能不是边缘。这一步就有了所有可能是边缘的集合。
在OpenCV中默认使用Sobel算子作为梯度算子。
3.非极大值抑制
通常灰度变化的地方都比较集中,将局部范围内的梯度方向上,灰度变化最大的保留下来,其它的不保留,这样可以剔除掉一大部分的点。将有多个像素宽的边缘变成一个单像素宽的边缘。这一步的主要目的为瘦边缘,需要结合梯度方向与梯度值来判断;
将梯度分为8个方向,分别为E、NE、N、NW、W、SW、S、SE(这实际上也为像素点),
其中0代表0度到45度,1代表45度到90度,2代表-90度到-45度,3代表-45度到0度。
4.阈值筛选
通过非极大值抑制后,仍然有很多的可能边缘点,进一步的设置一个双阈值,即低阈值(low),高阈值(high)。灰度变化大于high的,设置为强边缘像素,低于low的,剔除。在low和high之间的设置为弱边缘。进一步判断,如果其领域内有强边缘像素,保留,如果没有,剔除。
OpenCV的函数有两种:
第一种为输入两个不同的互相正交的梯度图:
CV_EXPORTS_W void Canny( InputArray dx, InputArray dy,
OutputArray edges,
double threshold1, double threshold2,
bool L2gradient = false );
InputArray dx:x方向的梯度算子,如sobel算子的x算子;
InputArray dx:y方向的梯度算子,如sobel算子的y算子;
double threshold1,最小阈值,小于此阈值这不为边缘;
double threshold2,最大阈值,大于此阈值为强边缘;
第二种输入为8位灰度图
CV_EXPORTS_W void Canny( InputArray image, OutputArray edges,
double threshold1, double threshold2,
int apertureSize = 3, bool L2gradient = false );
默认梯度算子为Sobel算子apertureSize为Sobel算子的大小
#include <opencv.hpp>
using namespace std;
using namespace cv;
void Grad(Mat src_gradient,Mat& dst);
void Roberts(Mat src_roberts, Mat& dst);
void Prewiit(Mat src_Prewiit, Mat& dst);
void Sobel(Mat src_sobel, Mat& dst);
void Laplacian(Mat src_lap, Mat& dst);
void LapofGaussi(Mat src_log, Mat& dst, int size,double sigma);
void Canny(Mat src_canny, Mat& dst, int value_low, int value_high);
void main()
{
Mat src = imread("test.jpg");
//常用7种边缘检测算法
Mat gray,gradient, roberts, prewiit, sobel, laplacian, LoG, canny,cv_canny;
Grad(src, gradient);
Roberts(src, roberts);
Prewiit(src, prewiit);
Sobel(src, sobel);
Laplacian(src, laplacian);
LapofGaussi(src, LoG,5,1.0);
Canny(src, canny, 10,100);
cvtColor(src, gray, COLOR_BGR2GRAY);
cv::Canny(gray, cv_canny, 10, 100);
imshow("src", src);
imshow("gradient", gradient);
imshow("prewiit", prewiit);
imshow("roberts", roberts);
imshow("sobel", sobel);
imshow("laplacian", laplacian);
imshow("canny", canny);
imshow("cv_canny", cv_canny);
imshow("LoG", LoG);
waitKey(0);
}
//普通梯度法
void Grad(Mat src_gradient, Mat& dst)
{
Mat gauss,gray;
GaussianBlur(src_gradient, gauss, Size(3, 3), 0.8, 0.8);
cvtColor(gauss, gray, COLOR_BGR2GRAY);
Mat dst_x, dst_y;
Mat out(Size(gray.size()), gray.type());
dst = out;
Mat kernel_x = (Mat_<double>(2, 2) << 0, 0, -1, 1);
Mat kernel_y = (Mat_<double>(2, 2) << 0, -1, 0, 1);
filter2D(gray, dst_x, -1, kernel_x);
filter2D(gray, dst_y, -1, kernel_y);
double s = 0.0;
int n = gray.cols*gray.rows;
for (int i = 0 ;i<src_gradient.rows;i++)
{
uchar* ptr_x = dst_x.ptr<uchar>(i);
uchar* ptr_y = dst_y.ptr<uchar>(i);
uchar* ptr_dst = dst.ptr<uchar>(i);
for (int j = 0; j < src_gradient.cols; j++)
{
ptr_dst[j] = sqrt(ptr_x[j]* ptr_x[j] + ptr_y[j]* ptr_y[j]);
s += ptr_dst[j];
}
}
s = s / n;
// for (int i = 0; i < src_gradient.rows; i++)
// {
// for (int j = 0; j < src_gradient.cols; j++)
// {
// if (dst.at<uchar>(i, j) >= s)
// {
// dst.at<uchar>(i, j) = 255;
// }
//
// }
//
// }
}
void Roberts(Mat src_roberts, Mat & dst)
{
Mat gauss, gray;
GaussianBlur(src_roberts, gauss, Size(3, 3), 0.8, 0.8);
cvtColor(src_roberts, gray, COLOR_BGR2GRAY);
Mat dst_x, dst_y;
Mat out(Size(gray.size()), gray.type());
dst = out;
Mat kernel_x = (Mat_<double>(2, 2) << -1, 0, 0, 1);
Mat kernel_y = (Mat_<double>(2, 2) << 0, -1, 1, 0);
filter2D(gray, dst_x, -1, kernel_x);
filter2D(gray, dst_y, -1, kernel_y);
double s = 0.0;
int n = src_roberts.cols*src_roberts.rows;
for (int i = 0; i < src_roberts.rows; i++)
{
uchar* ptr_x = dst_x.ptr<uchar>(i);
uchar* ptr_y = dst_y.ptr<uchar>(i);
uchar* ptr_dst = dst.ptr<uchar>(i);
for (int j = 0; j < src_roberts.cols; j++)
{
ptr_dst[j] = sqrt(ptr_x[j] * ptr_x[j] + ptr_y[j] * ptr_y[j]);
s += ptr_dst[j];
}
}
s = s / n;
// for (int i = 0; i < src_roberts.rows; i++)
// {
// for (int j = 0; j < src_roberts.cols; j++)
// {
// if (dst.at<uchar>(i,j)>=s)
// {
// dst.at<uchar>(i, j) = 255;
// }
//
// }
//
// }
}
void Prewiit(Mat src_Prewiit, Mat & dst)
{
Mat gauss, gray;
GaussianBlur(src_Prewiit, gauss, Size(3, 3), 0.8, 0.8);
cvtColor(gauss, gray, COLOR_BGR2GRAY);
Mat dst_x, dst_y;
Mat out(Size(gray.size()), gray.type());
dst = out;
Mat kernel_x = (Mat_<double>(3, 3) << 1.0/3, 0, -1.0/3, 1.0 / 3, 0, -1.0 / 3, 1.0 / 3, 0, -1.0 / 3);
Mat kernel_y = (Mat_<double>(3, 3) << 1.0 / 3, 1.0/3, 1.0 / 3,0,0,0, -1.0 / 3, -1.0 / 3, -1.0 / 3);
filter2D(gray, dst_x, -1, kernel_x);
filter2D(gray, dst_y, -1, kernel_y);
double s = 0.0;
int n = gray.cols*gray.rows;
for (int i = 0; i < src_Prewiit.rows; i++)
{
uchar* ptr_x = dst_x.ptr<uchar>(i);
uchar* ptr_y = dst_y.ptr<uchar>(i);
uchar* ptr_dst = dst.ptr<uchar>(i);
for (int j = 0; j < src_Prewiit.cols; j++)
{
ptr_dst[j] = sqrt(ptr_x[j] * ptr_x[j] + ptr_y[j] * ptr_y[j]);
s += ptr_dst[j];
}
}
s = s / n;
// for (int i = 0; i < src_Prewiit.rows; i++)
// {
// for (int j = 0; j < src_Prewiit.cols; j++)
// {
// if (dst.at<uchar>(i, j) >= s)
// {
// dst.at<uchar>(i, j) = 255;
// }
//
// }
//
// }
}
void Sobel(Mat src_sobel, Mat & dst)
{
Mat gauss, gray;
GaussianBlur(src_sobel, gauss, Size(3, 3), 0.8, 0.8);
cvtColor(gauss, gray, COLOR_BGR2GRAY);
Mat dst_x, dst_y;
Mat out(Size(gray.size()), gray.type());
dst = out;
Mat kernel_x = (Mat_<double>(3, 3) << 1.0 / 5, 0, -1.0 / 3, 2.0 / 5, 0, -2.0 / 5, 1.0 / 3, 0, -1.0 / 3);
Mat kernel_y = (Mat_<double>(3, 3) << 1.0 / 5, 2.0 / 5, 1.0 / 5, 0, 0, 0, -1.0 / 5, -2.0 / 5, -1.0 / 5);
filter2D(gray, dst_x, -1, kernel_x);
filter2D(gray, dst_y, -1, kernel_y);
double s = 0.0;
int n = gray.cols*gray.rows;
for (int i = 0; i < src_sobel.rows; i++)
{
uchar* ptr_x = dst_x.ptr<uchar>(i);
uchar* ptr_y = dst_y.ptr<uchar>(i);
uchar* ptr_dst = dst.ptr<uchar>(i);
for (int j = 0; j < src_sobel.cols; j++)
{
ptr_dst[j] = sqrt(ptr_x[j] * ptr_x[j] + ptr_y[j] * ptr_y[j]);
s += ptr_dst[j];
}
}
s = s / n;
// for (int i = 0; i < src_sobel.rows; i++)
// {
// for (int j = 0; j < src_sobel.cols; j++)
// {
// if (dst.at<uchar>(i, j) >= s)
// {
// dst.at<uchar>(i, j) = 255;
// }
//
// }
//
// }
}
void Laplacian(Mat src_lap, Mat & dst)
{
Mat gauss, gray;
GaussianBlur(src_lap, gauss, Size(3, 3), 0.8, 0.8);
cvtColor(gauss, gray, COLOR_BGR2GRAY);
Mat kernel = (Mat_<double>(3, 3) << 0, -1.0, 0, -1.0, 4, -1.0,0, -1.0, 0);
filter2D(gray, dst, -1, kernel);
double s = 0.0;
double n = gray.cols*gray.rows/4;
for (int i = 0; i < src_lap.rows; i++)
{
uchar* ptr_dst = dst.ptr<uchar>(i);
for (int j = 0; j < src_lap.cols; j++)
{
s += ptr_dst[j];
}
}
s = s / n;
for (int i = 0; i < src_lap.rows; i++)
{
for (int j = 0; j < src_lap.cols; j++)
{
if (dst.at<uchar>(i, j) >= s)
{
dst.at<uchar>(i, j) = 255;
}
}
}
}
void LapofGaussi(Mat src_log, Mat & dst,int size, double sigma)
{
Mat gauss, gray;
GaussianBlur(src_log, gauss, Size(3, 3), 0.8, 0.8);
cvtColor(gauss, gray, COLOR_BGR2GRAY);
double N = (size - 1) / 2;
double C = -1.0 / (CV_PI*pow(sigma,4));
Mat kernel = (Mat_<double>(5, 5) << -2, -4, -4, -4, -2,
-4, 0, 8, 0, -4,
-4, 8, 24, 8, -4,
-4, 0, 8, 0, -4,
-2, -4, -4, -4, -2);
// double theta = 0.0;
// for (int y = 0;y<size;y++)
// {
// double* ptr = kernel.ptr<double>(y);
//
// for (int x = 0; x < size; x++)
// {
//
// ptr[x] = C*(1.0- (pow((double)x - N, 2) + pow((double)y - N, 2)) / 2.0*sigma*sigma)*exp(-(pow((double)x - N, 2) + pow((double)y - N, 2))/2.0*sigma*sigma);
// theta += ptr[x];
//
//
// }
//
//
// }
// for (int y = 0; y < size; y++)
// {
// double* ptr = kernel.ptr<double>(y);
//
// for (int x = 0; x < size; x++)
// {
//
// ptr[x] /= theta;
//
//
//
// }
//
//
// }
filter2D(gray, dst, -1, kernel);
}
void Canny(Mat src_canny, Mat & dst_canny,int value_low,int value_high)
{
Mat gauss, gray,dst;
//第一步噪声处理
GaussianBlur(src_canny, gauss, Size(3, 3), 1, 1);
cvtColor(gauss, gray, COLOR_BGR2GRAY);
Mat dst_x(src_canny.rows,src_canny.cols,CV_32FC1), dst_y(src_canny.rows, src_canny.cols, CV_32FC1);
Mat out(Size(gray.size()), gray.type());
dst = out;
dst_canny = cv::Mat::zeros(src_canny.rows,src_canny.cols,CV_8UC1);
//第二步Sobel算子检测边缘
Mat kernel_x = (Mat_<double>(3, 3) << 1.0 / 5, 0, -1.0 / 3, 2.0 / 5, 0, -2.0 / 5, 1.0 / 3, 0, -1.0 / 3);
Mat kernel_y = (Mat_<double>(3, 3) << 1.0 / 5, 2.0 / 5, 1.0 / 5, 0, 0, 0, -1.0 / 5, -2.0 / 5, -1.0 / 5);
filter2D(gray, dst_x, -1, kernel_x);
filter2D(gray, dst_y, -1, kernel_y);
for (int i = 0; i < src_canny.rows; i++)
{
uchar* ptr_x = dst_x.ptr<uchar>(i);
uchar* ptr_y = dst_y.ptr<uchar>(i);
uchar* ptr_dst = dst.ptr<uchar>(i);
for (int j = 0; j < src_canny.cols; j++)
{
ptr_dst[j] = sqrt(ptr_x[j] * ptr_x[j] + ptr_y[j] * ptr_y[j]);
}
}
dst_x.convertTo(dst_x, CV_32FC1, 1.0 / 255);
dst_y.convertTo(dst_y, CV_32FC1, 1.0 / 255);
Mat mag, angle;
//计算梯度幅度与梯度方向
//转极坐标函数,可计算r与theta
cartToPolar(dst_x, dst_y, mag, angle, 1);
for (int i = 0; i < src_canny.rows; i++)
{
for (int j = 0; j < src_canny.cols; j++)
{
double value = angle.at<float>(i, j);
if ((value <= 22.5 && value> -22.5) || (value >= 157.5 && value < -157.5))
{
angle.at<float>(i, j) = 0;//0度
}
if ((value <= 67.5 && value > 22.5) || (value < -112.5 && value >= -157.5))
{
angle.at<float>(i, j) = 45;//45度
}
if ((value > 67.5 && value <= 112.5) || (value < -67.5 && value >= -112.5))
{
angle.at<float>(i, j) = 90;//90度
}
if ((value > 112.5 && value < 157.5) || (value < -112.5 && value >= -157.5))
{
angle.at<float>(i, j) = 135;//135度
}
}
}
//简单讨论一下四个梯度方向的非极大值抑制
for (int i = 1; i < src_canny.rows-1; i++)
{
for (int j = 1; j < src_canny.cols-1; j++)
{
uchar t = dst.at<uchar>(i, j);
uchar a = dst.at<uchar>(i-1, j);
uchar b = dst.at<uchar>(i+1, j);
uchar c = dst.at<uchar>(i, j-1);
uchar d = dst.at<uchar>(i, j+1);
uchar e = dst.at<uchar>(i + 1, j+1);
uchar f = dst.at<uchar>(i - 1, j - 1);
uchar g = dst.at<uchar>(i - 1, j + 1);
uchar h = dst.at<uchar>(i + 1, j - 1);
double value = angle.at<float>(i, j);
if (dst.at<uchar>(i, j) <= value_low)
{
dst_canny.at<uchar>(i, j) = 0;
}
// if (dst.at<uchar>(i, j) >= value_low )
// {
// if (dst.at<uchar>(i, j) <= value_high)//弱边
// {
if (value == 135 && t == std::max(std::max(t, g), h))//梯度方向为135度
{
dst_canny.at<uchar>(i,j) = t;
}
if (value == 90 && t == std::max(std::max(t, a), b))//梯度方向为90度
{
dst_canny.at<uchar>(i, j) = t;
}
if (value == 45 && t == std::max(std::max(t, e), f))//梯度方向为45度
{
dst_canny.at<uchar>(i, j) = t;
}
if (value == 0 && t == std::max(std::max(t, c), d))//梯度方向为0度
{
dst_canny.at<uchar>(i, j) = t;
}
/*}*/
/*}*/
}
}
//双阈值处理有点问题
for (int i = 0 ; i< src_canny.rows;i++)
{
for (int j = 0; j < src_canny.cols; j++)
{
uchar t = dst.at<uchar>(i, j);
if (dst_canny.at<uchar>(i, j) >= value_low)
{
dst_canny.at<uchar>(i, j) = 255;
// if (dst_canny.at<uchar>(i, j) >= value_high)//强边
// {
//
// for (int n = 3 ;n > 0 ;n--)
// {
// for (int m = 3; m > 0; m--)
// {
// if ((i > 0 && j > 0) && (i <src_canny.rows-1 && j<src_canny.cols-1))
// {
//
// uchar k = dst_canny.at<uchar>(i - n + 2, j - m + 2);
// if (k >= 0 && k < value_high)
// {
// dst_canny.at<uchar>(i - n + 2, j - m + 2) = 255;
// }
// }
// }
//
// }
//
// }
// // if (dst_canny.at<uchar>(i, j) <= value_high)//弱边
// // {
// // dst_canny.at<uchar>(i, j) = 127;
// //
// // }
}
else
{
dst_canny.at<uchar>(i, j) = 0;
}
}
}
}