参考(感谢大佬分享):
数字图像处理(c++ opencv):图像分割-基本边缘检测--canny边缘检测 - 知乎 (zhihu.com)
这里面我觉得非极大值抑制那里的代码不太对,我在下面的代码中改了一下。
具体原理可以去某站学习。
我使用的是Code::Blocks+OpenCV,配置方法可见:
Code::Blocks+Opencv 环境搭建_zzz_zzzz_的博客-优快云博客
Project 的框架:
Canny.h(自定义头文件)
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;
//声明
//1 高斯滤波函数
void Gaussfilter_ly(Mat input, Mat &output, int Gauss_size, double Sigma);
//2 计算梯度幅值图像和方向图像
void Grad_dire_ly(Mat input, Mat& Gradimage, Mat& Direimage);
//3 非极大值抑制图像
void Nonmax_suppression_ly(Mat Gradimage, Mat Direimage, Mat& Suppimage);
//4 滞后阈值处理(双阈值)
void doubleThread_ly(Mat Suppimage, Mat& Edgeimage, int th_high, int th_low);
//5 canny函数
void canny_ly(Mat input_image, Mat& output_image, int th_high, int th_low, int Gauss_size, double sigmma);
Canny.cpp
#include "Canny.h"
//1 进行高斯滤波
/*
传入参数说明:
Mat input_image:原始图像
Mat& output_image:(引用,不需要参数传递,可以被带回)高斯模糊处理后的图像
int Gauss_size:高斯核的大小,高斯核越大,图像越模糊
double sigma:计算高斯核中数值的参数,sigma越大,图像越模糊
*/
void Gaussfilter_ly(Mat input_image, Mat& output_image, int Gauss_size, double Sigma)
{
//保证高斯核大小为大于等于3的奇数
if (Gauss_size < 3) Gauss_size = 3;
else Gauss_size = (int)(Gauss_size / 2) * 2 + 1;
//创建高斯核空间
double** Gausskernel = new double* [Gauss_size];
for (int i = 0; i < Gauss_size; i++)
{
Gausskernel[i] = new double[Gauss_size];
}
//高斯核模板中心到模板边缘的“距离”
int center = Gauss_size / 2;
double sum = 0;
for (int i = 0; i < Gauss_size; i++)
{
for (int j = 0; j < Gauss_size; j++)
{
//通过公式计算模板高斯核的数值
Gausskernel[i][j] = exp(-((i - center) * (i - center) + (j - center) * (j - center)) / (2 * Sigma * Sigma));
sum += Gausskernel[i][j];//为了后续方便归一化
}
}
//高斯卷积核归一化
double sum1 = 1 / sum;
for (int i = 0; i < Gauss_size; i++)
{
for (int j = 0; j < Gauss_size; j++)
{
Gausskernel[i][j] *= sum1;
}
}
//高斯模糊
Mat tem_image = input_image.clone();
int rows = input_image.rows - center;
int cols = input_image.cols - center;
for (int i = center; i < rows; i++)
{
for (int j = center; j < cols; j++)
{
double sum = 0;
for (int m = -center; m <= center; m++)
{
for (int n = -center; n <= center; n++)
{
//体现当前值sum是其周围值的加权平均
sum += Gausskernel[center + m][center + n] * input_image.at<uchar>(i + m, j + n);
}
}
tem_image.at<uchar>(i, j) = static_cast<uchar>(sum);//强制类型转换
}
}
output_image = tem_image;
//释放内存
for (int i =0; i < Gauss_size; i++) delete[] Gausskernel[i];
delete[] Gausskernel;
}
//2 计算梯度幅值图像,方向图像和边缘图像
/*
传入参数说明:
Mat input:高斯模糊处理后的图像
Mat& Gradimage:(引用,不需要参数传递,可以被带回)存贮图像像素的梯度赋值
Mat& Direimage:(引用,不需要参数传递,可以被带回)存贮图像像素的梯度方向
*/
void Grad_dire_ly(Mat input, Mat& Gradimage, Mat& Direimage)
{
Mat tempGrad = Mat(input.size(), CV_16U, Scalar(0));//Scalar(0)表示黑色
Mat tempDire = Mat(input.size(), CV_8U, Scalar(0));//Scalar(0)表示黑色
int width = input.cols;
int height = input.rows;
for (int i = 1; i < height - 1; i++)
{
for (int j = 1; j < width - 1; j++)
{
//计算梯度及梯度幅值
//Preeitt梯度算子 (Sobel算子也可以)
int gx = input.at<uchar>(i + 1, j - 1) + input.at<uchar>(i + 1, j) + input.at<uchar>(i + 1, j + 1)
- input.at<uchar>(i - 1, j - 1) - input.at<uchar>(i - 1, j) - input.at<uchar>(i - 1, j + 1);
int gy = input.at<uchar>(i - 1, j + 1) + input.at<uchar>(i, j + 1) + input.at<uchar>(i + 1, j + 1)
- input.at<uchar>(i - 1, j - 1) - input.at<uchar>(i, j - 1) - input.at<uchar>(i + 1, j - 1);
int sum = gx + gy;//可以是 int sum =sqrt(gx^2+gy^2) //需要import math
//梯度幅值图像
tempGrad.at<ushort>(i, j) = abs(sum);
//梯度方向图像
//atan2(gy, gx)的取值为-PI-PI
//dire的取值为-180-180
double dire = atan2(gy, gx) * 180 / 3.1415926; //转化为度数
if((dire >= -22.5 && dire < 22.5) || (dire >= 157.5 || dire < -157.5) )
tempDire.at<uchar>(i, j) = 1; //1:水平
else if((dire >= 22.5 && dire < 67.5) || (dire >= -157.5 && dire < -122.5))
tempDire.at<uchar>(i, j) = 2; //2:45
else if((dire >= 67.5 && dire < 122.5) || (dire >= -122.5 && dire < -67.5))
tempDire.at<uchar>(i, j) = 3; //3:垂直
else
tempDire.at<uchar>(i, j) = 4; //4:-45
}
}
Gradimage = tempGrad;
Direimage = tempDire;
}
//3 非极大值抑制图像
/*
传入参数说明:
Mat Gradimage:存贮梯度赋值的图像
Mat Direimage:存贮梯度方向的图像
Mat& Suppimage:(引用,不需要参数传递,可以被带回)非极大值抑制后的图像
*/
void Nonmax_suppression_ly(Mat Gradimage, Mat Direimage, Mat& Suppimage)
{
Mat tempSupp = Mat(Gradimage.size(), Gradimage.type(), Scalar(0));
int width = Gradimage.cols;
int height = Gradimage.rows;
for (int i = 1; i < height - 1; i++)
{
for (int j = 1; j < width - 1; j++)
{
switch (Direimage.at<uchar>(i, j))//当前循环到的像素的梯度方向
{
case 1 ://水平
if (Gradimage.at<ushort>(i, j) >= Gradimage.at<ushort>(i - 1, j) && Gradimage.at<ushort>(i, j) >= Gradimage.at<ushort>(i + 1, j))
tempSupp.at<ushort>(i, j) = Gradimage.at<ushort>(i, j);
else
tempSupp.at<ushort>(i, j) = 0;
break;
case 2://45度
if (Gradimage.at<ushort>(i, j) >= Gradimage.at<ushort>(i + 1, j + 1) && Gradimage.at<ushort>(i, j) >= Gradimage.at<ushort>(i - 1, j - 1))
tempSupp.at<ushort>(i, j) = Gradimage.at<ushort>(i, j);
else
tempSupp.at<ushort>(i, j) = 0;
break;
case 3://垂直
if (Gradimage.at<ushort>(i, j) >= Gradimage.at<ushort>(i , j - 1) && Gradimage.at<ushort>(i, j) >= Gradimage.at<ushort>(i , j - 1))
tempSupp.at<ushort>(i, j) = Gradimage.at<ushort>(i, j);
else
tempSupp.at<ushort>(i, j) = 0;
break;
case 4://-45度
if (Gradimage.at<ushort>(i, j) >= Gradimage.at<ushort>(i + 1, j - 1) && Gradimage.at<ushort>(i, j) >= Gradimage.at<ushort>(i - 1, j + 1))
tempSupp.at<ushort>(i, j) = Gradimage.at<ushort>(i, j);
else
tempSupp.at<ushort>(i, j) = 0;
break;
default:
break;
}
}
}
Suppimage = tempSupp;
}
//4 滞后阈值处理(双阈值)
/*
传入参数说明:
Mat Suppimage:存贮梯度赋值的图像
Mat& Edgeimage:(引用,不需要参数传递,可以被带回)边缘位置为255(白色)
int th_high:高阈值
int th_low:低阈值
*/
void doubleThread_ly(Mat Suppimage, Mat& Edgeimage, int th_high, int th_low)
{
//保证高阈值大于低阈值
int temp;
if (th_high < th_low)
{
temp = th_high;
th_high = th_low;
th_low = temp;
}
Mat bw_h = Mat(Suppimage.size(), CV_8UC1, Scalar(0));
Mat bw_l = Mat(Suppimage.size(), CV_8UC1, Scalar(0));
int width = Suppimage.cols;
int height = Suppimage.rows;
for (int i = 0; i < height; i++)
{
for (int j = 0; j < width; j++)
{
//bw_h中将梯度幅值大于等于高阈值的像素点(一定是边缘点)设为255
if (Suppimage.at<ushort>(i, j) >= th_high)
bw_h.at<uchar>(i, j) = 255;
else
bw_h.at<uchar>(i, j) = 0;
//bw_l中将梯度幅值大于等于低阈值且小于高阈值的像素点(可能是边缘点)设为255
if (Suppimage.at<ushort>(i, j) >= th_low && Suppimage.at<ushort>(i, j) < th_high)
bw_l.at<uchar>(i, j) = 255;
else
bw_l.at<uchar>(i, j) = 0;
}
}
Mat bw = bw_h.clone();//bw中将要存贮的是边缘点
for (int i = 1; i < height - 1; i++)
{
for (int j = 1; j < width - 1; j++)
{
if (bw_h.at<uchar>(i, j) == 255)
{
//对于一定是边缘的像素点,看它的八邻域内有没有可能是边缘的点
//如果有,将其加入到边缘点中
if (bw_l.at<uchar>(i - 1, j - 1) == 255)
bw.at<uchar>(i - 1, j - 1) = 255;
if (bw_l.at<uchar>(i - 1, j) == 255)
bw.at<uchar>(i - 1, j) = 255;
if (bw_l.at<uchar>(i - 1, j+ 1) == 255)
bw.at<uchar>(i - 1, j + 1) = 255;
if (bw_l.at<uchar>(i, j - 1) == 255)
bw.at<uchar>(i, j - 1) = 255;
if (bw_l.at<uchar>(i, j + 1) == 255)
bw.at<uchar>(i, j + 1) = 255;
if (bw_l.at<uchar>(i + 1, j - 1) == 255)
bw.at<uchar>(i + 1, j - 1) = 255;
if (bw_l.at<uchar>(i + 1, j) == 255)
bw.at<uchar>(i + 1, j) = 255;
if (bw_l.at<uchar>(i + 1, j + 1) == 255)
bw.at<uchar>(i + 1, j + 1) = 255;
}
}
}
Edgeimage = bw;
}
//5 canny函数
/*
传入参数说明:
Mat input_image:原始图像
Mat& output_image:(引用,不需要参数传递,可以被带回)处理后的图像
int th_high:高阈值
int th_low:低阈值
int Gauss_size:高斯核的大小,高斯核越大,图像越模糊
double sigma:计算高斯核中数值的参数,sigma越大,图像越模糊
*/
void canny_ly(Mat input_image, Mat& output_image, int th_high, int th_low, int Gauss_size, double sigmma)
{
Mat Gaussimage, Gradimage, Direimage, Suppimage, Edgeimage;//过程中生成的图像
//1 高斯滤波函数
Gaussfilter_ly(input_image, Gaussimage, Gauss_size, sigmma);
//2 计算梯度幅值图像和方向图像
Grad_dire_ly(Gaussimage, Gradimage, Direimage);
//3 非极大值抑制图像
Nonmax_suppression_ly(Gradimage, Direimage, Suppimage);
//4 滞后阈值处理(双阈值)
doubleThread_ly(Suppimage, Edgeimage, th_high, th_low);
output_image = Edgeimage;
}
main.cpp
#include "Canny.h"
using namespace std;
using namespace cv;
int main()
{
Mat img1 = imread("D:/1.png", 0);//0表示以灰度方式读入
if (img1.empty())//参数检查
{
cout << "wrong" << endl;
return -1;
}
imshow("img1", img1);
Mat img2;
canny_ly(img1, img2, 60, 20, 3, 1);//Canny边缘检测,参数(原图像,边缘图像,高阈值,低阈值,高斯核,sigma)
imshow("img2", img2);
waitKey();
return 0;
}
最后,结果如下:
也可以直接用opencv的Canny函数,一句代码就可以实现。真的是傻瓜使用方式。