Canny算法介绍
Canny边缘检测算法是一种多级边缘检测算法,John F. Canny于 1986 年开发出来。在许多方面的应用都有着它的身影。因此,不管是为了学习或者工作,学习Canny算法的原理和实现都是非常有必要的。废话不多说了,下面我将带领大家从原理和代码方面为您讲解Canny算法的基本思想。在本文的最后,会有一个基于Canny算法比较酷炫的简单项目,有兴趣的可以试试,比较有意思。
Canny算法原理
Canny边缘检测主要分为四步来实现的,每一步都有着其主要意义,基于这四个步骤,我将一一讲解其实现的细节。
第一步:高斯模糊
对于原图像我们需要对其进行高斯模糊,高斯模糊在之前我们已经实现过了,或者你可以使用Opencv自带的函数也行。对图像进行模糊能够去除图像的细节部分,图像噪声可大大减少,而图像的边缘信息不会损失什么,这样对后续检测的效果会更好。
/* 获取高斯分布数组 (核大小, sigma值) */
double **getGaussianArray(int arr_size, double sigma)
{
int i, j;
// [1] 初始化权值数组
double **array = new double*[arr_size];
for (i = 0; i < arr_size; i++) {
array[i] = new double[arr_size];
}
// [2] 高斯分布计算
int center_i, center_j;
center_i = center_j = arr_size / 2;
double pi = 3.141592653589793;
double sum = 0.0f;
// [2-1] 高斯函数
for (i = 0; i < arr_size; i++) {
for (j = 0; j < arr_size; j++) {
array[i][j] =
//后面进行归一化,这部分可以不用
//0.5f *pi*(sigma*sigma) *
exp(-(1.0f)* (((i - center_i)*(i - center_i) + (j - center_j)*(j - center_j)) /
(2.0f*sigma*sigma)));
sum += array[i][j];
}
}
// [2-2] 归一化求权值
for (i = 0; i < arr_size; i++) {
for (j = 0; j < arr_size; j++) {
array[i][j] /= sum;
//printf(" [%.15f] ", array[i][j]);
}
// printf("\n");
}
return array;
}
// 高斯模糊
Mat gaussianFilter(Mat &src, int size, double sigma)
{
// 深拷贝
Mat dst = src.clone();
double** model = getGaussianArray(size, sigma);
double sum;
// 遍历像素
for (int i = 0; i < src.rows; i++)
for (int j = 0; j < src.cols; j++)
{
// 初始化
sum = 0;
// 遍历模板
for (int n = -size / 2; n <= size / 2; n++)
for (int m = -size / 2; m <= size / 2; m++)
{
// 边缘补零
if (i + n < 0 || j + m < 0 || i + n >= src.rows || j + m >= src.cols)
sum += 0;
else
sum += model[n + size / 2][m + size / 2] * src.at<uchar>(i + n, j + m);
}
dst.at<uchar>(i, j) = pixesDeal(sum);
}
// imshow("高斯模糊", dst);
return dst;
}
// 像素判定
uchar pixesDeal(double n)
{
if (n < 0)
return 0;
else if (n > 255)
return 255;
else
return static_cast<uchar>(n);
}
第二步:Sobel梯度算子
我们需要对模糊后的图像进行Sobel梯度算子的卷积,但这里的处理和之前我们实现过的Sobel图像锐化稍稍有点不同,之前我们是将x方向的Sobel模板和y方向的Sobel模板进行绝对值的相加(L1范数),但现在我们需要进行平方相加后开平方(L2范数),即幅值。还需要梯度方向的信息为下一步处理提供条件。获取的幅值图像大概描绘了整个图像的轮廓,但是这个边缘存在着很多不必要的信息,例如:边缘描绘过宽和噪声信息。
// sobel梯度计算
Mat sobelFilter(Mat &src)
{
// 深拷贝
Mat gradValue = src.clone();
Mat gradDirect = src.clone();
double sum1, sum2;
// x方向边缘的y模板
double model1[9] = { -1, -2, -1, 0, 0, 0, 1, 2, 1 };
// y方向边缘的x模板
double model2[9] = { -1, 0, 1, -2, 0, 2, -1, 0, 1 };
int p;
// 遍历像素
for (int i = 0; i < src.rows; i++)
for (int j = 0; j < src.cols; j++)
{
// 初始化
p = 0, sum1 = 0, sum2 = 0;
// 遍历模板
for (int n = -1; n <= 1; n++)
for (int m = -1; m <= 1; m++)
{
// 边缘补零
if (i + n < 0 || j + m < 0 || i + n >= src.rows || j + m >= src.cols)
p++;
else
{
sum1 += src.at<uchar>(i + n, j + m) * model1[p];
sum2 += src.at<uchar>(i + n, j + m) * model2[p];
p++;
}
}
gradValue.at<uchar>(i, j) = pixesDeal(pow(pow(sum1, 2) + pow(sum2, 2), 1.0 / 2));
// 0,7垂直方向, 1,2斜对角方向\, 3,4水平方向, 5,6斜对角方向/
gradDirect.at<uchar>(i, j) = static_cast<uchar>(atan(sum1 / (sum2 + 0.001)) * 8 / CV_PI + 4);
}
Mat temp[] = { gradValue, gradDirect };
Mat grad;
merge(temp, 2, grad);
// imshow("梯度图像", gradValue);
return grad;
}
第三步:非极大值抑制(NMS)
非极大值抑制是一种将宽边缘带细化,只保留边缘峰值的处理。主要的手法是根据上一步获取的梯度方向,将图像像素点沿梯度方向或逆方向的邻域像素点进行比较,如果为最大值则保留,否则抑制,即设置像素点为0。
Mat NMS(Mat &src)
{
vector<Mat> grad;
// 梯度通道和方向通道
split(src, grad);
// 遍历中梯度值会改变,先拷贝一份
Mat gradValue = grad[0].clone();
// 遍历像素
for (int i = 0; i < gradValue.rows; i++)
for (int j = 0; j < gradValue.cols; j++)
{
if (grad[1].at<uchar>(i, j) == 0 || grad[1].at<uchar>(i, j) == 7)
{
if (i < gradValue.rows - 1)
if (gradValue.at<uchar>(i, j) <= gradValue.at<uchar>(i + 1, j))
{
grad[0].at<uchar>(i, j) = 0;
continue;
}
if (i > 0)
if (gradValue.at<uchar>(i, j) <= gradValue.at<uchar>(i - 1, j))
{
grad[0].at<uchar>(i, j) = 0;
continue;
}
}
if (grad[1].at<uchar>(i, j) == 1 || grad[1].at<uchar>(i, j) == 2)
{
if (i < gradValue.rows - 1 && j < gradValue.cols - 1)
if (gradValue.at<uchar>(i, j) <= gradValue.at<uchar>(i + 1, j + 1))
{
grad[0].at<uchar>(i, j) = 0;
continue;
}
if (i > 0 && j > 0)
if (gradValue.at<uchar>(i, j) <= gradValue.at<uchar>(i - 1, j - 1))
{
grad[0].at<uchar>(i, j) = 0;
continue;
}
}
if (grad[1].at<uchar>(i, j) == 3 || grad[1].at<uchar>(i, j) == 4)
{
if (j < gradValue.cols - 1)
if (gradValue.at<uchar>(i, j) <= gradValue.at<uchar>(i, j + 1))
{
grad[0].at<uchar>(i, j) = 0;
continue;
}
if (j > 0)
if (gradValue.at<uchar>(i, j) <= gradValue.at<uchar>(i, j - 1))
{
grad[0].at<uchar>(i, j) = 0;
continue;
}
}
if (grad[1].at<uchar>(i, j) == 5 || grad[1].at<uchar>(i, j) == 6)
{
if (i < gradValue.rows - 1 && j > 0)
if (gradValue.at<uchar>(i, j) <= gradValue.at<uchar>(i + 1, j - 1))
{
grad[0].at<uchar>(i, j) = 0;
continue;
}
if (i > 0 && j < gradValue.cols - 1)
if (gradValue.at<uchar>(i, j) <= gradValue.at<uchar>(i - 1, j + 1))
{
grad[0].at<uchar>(i, j) = 0;
continue;
}
}
}
//imshow("非极大值处理", grad[0]);
merge(grad, src);
return src;
}
第四步:双阈值连接
设置两个阈值,一个高阈值和一个低阈值。图像像素点如果高于高阈值则表示是强边缘点,是真实边缘点,高于低阈值但小于高阈值表示是弱边缘点,其它不是边缘,抑制。之后需要对弱边缘点进行处理,若其邻域存在强边缘点则表示是真实边缘点,否则不是。这样设置两个阈值可以滤除图像中噪声,改善图像质量。弱边缘的处理原则是因为真实边缘的弱边缘点都存在强边缘点。
Mat doubleThrCon(Mat &src, uchar high, uchar low)
{
vector<Mat> grad;
// 梯度通道和方向通道
split(src, grad);
for (int i = 0; i < grad[0].rows; i++)
for (int j = 0; j < grad[0].cols; j++)
{
if (grad[0].at<uchar>(i, j) > high)
{
grad[0].at<uchar>(i, j) = 255;
// 标记强边缘点的位置
grad[1].at<uchar>(i, j) = 2;
}
else if (grad[0].at<uchar>(i, j) > low)
{
grad[0].at<uchar>(i, j) = 0;
// 标记弱边缘点的位置
grad[1].at<uchar>(i, j) = 1;
}
else
{
grad[0].at<uchar>(i, j) = 0;
grad[1].at<uchar>(i, j) = 0;
}
}
// 真实的边缘会在弱边缘点的邻域内存在强边缘点
for (int i = 0; i < grad[0].rows; i++)
for (int j = 0; j < grad[0].cols; j++)
{
if (grad[1].at<uchar>(i, j) == 1)
{
for (int n = -1; n <= 1; n++)
for (int m = -1; m <= 1; m++)
{
if (i + n >= 0 && j + m >= 0 && i + n < src.rows && j + m < src.cols && grad[1].at<uchar>(i + n, j + m) == 2)
grad[0].at<uchar>(i, j) = 255;
}
}
}
return grad[0];
}
视频流Canny边缘检测(阈值可调)
应用我们自己编写的Canny算法速度比较慢,因此我们选择系统提供的Canny算法效果会流畅。可以减少对摄像头的帧数处理来加快速度。
// 主函数
int main()
{
Mat frameImage;
VideoCapture capture1(0 + CAP_DSHOW);
// 我用的是手机摄像头,用电脑摄像头的不用填open()里面的参数
capture1.open("http://admin:[email protected]:8081");
if (!capture1.isOpened())
return 0;
int n = 0;
int highThreshold=70, lowThreshold=70;
//创建窗口
namedWindow("out", 1);
//创建轨迹条
createTrackbar("high threshold", "out", &highThreshold, 255);
createTrackbar("low threshold", "out", &lowThreshold, 255);
while (true)
{
capture1 >> frameImage;
if (n % 3 == 0)
{
cvtColor(frameImage, frameImage, COLOR_BGR2GRAY);
Canny(frameImage, frameImage, lowThreshold, highThreshold);
//frameImage = myCanny(frameImage);
imshow("out", frameImage);
}
if (char(waitKey(1)) == 'q') break;
n++;
}
return 0;
}
结果视频我就不发了,有兴趣的可以自己运行一下~