文章目录
- 一、算法原理
- 二、环境配置
- 三、算法详解
- 3.1、数据结构 Mat
- 3.2、高斯滤波器的C++实现
- 3.3、用一阶偏导有限差分计算梯度幅值和方向
- 三、项目实战:C++实现Canny边缘检测
一、算法原理
canny边缘检测算法步骤:
1、使用高斯滤波器对图像进行平滑处理。
2、利用一阶偏导算子找到灰度图像沿着水平方向Gx和垂直方向Gy的偏导数,并计算梯度的幅值和方向。
3、对梯度幅值进行NMS非极大值抑制,获取局部梯度的最大值。在3X3窗口中,将给定像素P与沿着梯度线方向的两个像素进行比较,若P的梯度幅值小于该两个像素的梯度幅值,则令P=0;否则保留原幅值。
备注:将梯度方向分为4种来比较梯度幅值的强度:水平方向、垂直方向、正方向、-45°方向。==
4、用双阈值算法检测和边缘连接。分三种情况:
(1)若像素值大于高阈值,则该像素一定是边缘像素(强边缘点),置为255;
(2)若小于低阈值,则一定不是,置为0;
(3)若像素值大于低阈值但小于高阈值,则观察该像素的(3X3)8邻域像素中是否有大于高阈值的像素点,若有则该像素是边缘像素,并将该点置为255,用以连接强边缘点;否则不是,则该点置为0。Canny边缘检测算法原理
二、环境配置
详细请看博主这篇文章:【深度学习环境配置】Anaconda +Pycharm + CUDA +cuDNN + Pytorch + Opencv(资源已上传)
三、算法详解
3.1、数据结构 Mat
Opencv C++ 基本数据结构 Mat
/*
函数说明:Mat(int rows, int cols, int type)
函数输入: rows 代表行数
cols 代表列数
type 可以设置为:CV_8UC(n)、CV_8SC(n)、CV_16SC(n)、CV_16UC(n)、CV_32FC(n)、CV_32SC(n)、CV_64FC(n)
其中:8U、8S、16S、16U、32S、32F、64F中的数字,代表Mat中每一个数值的位数
U代表uchar类型、S代表int类型、F代表浮点型(32F为float、64F为double)其他类似。
备注:Mat代表矩阵,该类声明在头文件opencv2/core/core.hpp中
*/
3.2、高斯滤波器的C++实现
【图像处理】高斯模糊、高斯函数、高斯核、高斯卷积操作
将二维图像先按水平方向进行一维高斯卷积(行卷积),再按垂直方向进行一维高斯卷积(列卷积)。同理:将二维高斯卷积核拆分为一维高斯卷积核。
- (1)对图像使用一维高斯卷积核,在一个方向上进行滤波(例如水平方向);
- (2)将图像进行转置,并使用相同一维高斯卷积核,在相同方向上进行滤波;(由于转置,其实是对垂直方向进行滤波)
- (3)再次转置,将图像还原为原来位置,最终得到二维高斯滤波后的图像。
3.3、用一阶偏导有限差分计算梯度幅值和方向
三、项目实战:C++实现Canny边缘检测
Canny边缘检测算法(C++实现)
#include <opencv2/opencv.hpp>
#include <math.h>
#include <corecrt_math_defines.h>
#define _USE_MATH_DEFINES
using namespace cv; // 使用命名空间cv。例如:cv::Mat可以省略写为 Mat
/*
函数说明:将两个图像拼接,以便在同一个窗口显示
函数输入: dst 输出的拼接后的图像
src1 拼接的第一幅图
src2 拼接的第二幅图
*/
void mergeImg(Mat& dst, Mat& src1, Mat& src2)
{
int rows = src1.rows;
int cols = src1.cols + 5 + src2.cols;
CV_Assert(src1.type() == src2.type());
dst.create(rows, cols, src1.type());
src1.copyTo(dst(Rect(0, 0, src1.cols, src1.rows)));
src2.copyTo(dst(Rect(src1.cols + 5, 0, src2.cols, src2.rows)));
}
/*
函数说明:一维高斯卷积,对每行进行高斯卷积
函数输入: img 输入原图像
dst 一维高斯卷积后的输出图像
*/
void gaussianConvolution(Mat& img, Mat& dst)
{
int nr = img.rows;
int nc = img.cols;
int templates[3] = { 1, 2, 1 };
// 按行遍历除每行边缘点的所有点
for (int j = 0; j < nr; j++)
{
uchar* data = img.ptr<uchar>(j); //提取该行地址
for (int i = 1; i < nc - 1; i++)
{
int sum = 0;
for (int n = 0; n < 3; n++)
{
sum += data[i - 1 + n] * templates[n]; //相乘累加
}
sum /= 4;
dst.ptr<uchar>(j)[i] = sum;
}
}
}
/*
函数说明:高斯滤波器,利用3*3的高斯模版进行高斯卷积
函数输入: img 输入原图像
dst 高斯滤波后的输出图像
*/
void gaussianFilter(Mat& img, Mat& dst)
{
// 对水平方向进行滤波
Mat dst1 = img.clone();
gaussianConvolution(img, dst1);
// 图像矩阵转置
Mat dst2;
transpose(dst1, dst2);
// 对垂直方向进行滤波
Mat dst3 = dst2.clone();
gaussianConvolution(dst2, dst3);
// 再次转置
transpose(dst3, dst);
}
/*
函数说明:用一阶偏导有限差分计算梯度幅值和方向
函数输入: img 输入原图像
gradXY 输出的梯度幅值
theta 输出的梯度方向
*/
void getGrandient(Mat& img, Mat& gradXY, Mat& theta)
{
gradXY = Mat::zeros(img.size(), CV_8U);
theta = Mat::zeros(img.size(), CV_8U);
for (int j = 1; j < img.rows - 1; j++)
{
for (int i = 1; i < img.cols - 1; i++)
{
double gradY = double(img.ptr<uchar>(j - 1)[i - 1] + 2 * img.ptr<uchar>(j - 1)[i] + img.ptr<uchar>(j - 1)[i + 1] - img.ptr<uchar>(j + 1)[i - 1] - 2 * img.ptr<uchar>(j + 1)[i] - img.ptr<uchar>(j + 1)[i + 1]);
double gradX = double(img.ptr<uchar>(j - 1)[i + 1] + 2 * img.ptr<uchar>(j)[i + 1] + img.ptr<uchar>(j + 1)[i + 1] - img.ptr<uchar>(j - 1)[i - 1] - 2 * img.ptr<uchar>(j)[i - 1] - img.ptr<uchar>(j + 1)[i - 1]);
gradXY.ptr<uchar>(j)[i] = sqrt(gradX * gradX + gradY * gradY); //计算梯度
theta.ptr<uchar>(j)[i] = atan(gradY / gradX); //计算梯度方向
}
}
}
/*
函数说明:NMS非极大值抑制
函数输入: gradXY 输入的梯度幅值
theta 输入的梯度方向
dst 输出的经局部非极大值抑制后的图像
*/
void nonLocalMaxValue(Mat& gradXY, Mat& theta, Mat& dst)
{
dst = gradXY.clone();
for (int j = 1; j < gradXY.rows - 1; j++)
{
for (int i = 1; i < gradXY.cols - 1; i++)
{
double t = double(theta.ptr<uchar>(j)[i]);
double g = double(dst.ptr<uchar>(j)[i]);
if (g == 0.0)
{
continue;
}
double g0, g1;
if ((t >= -(3 * M_PI / 8)) && (t < -(M_PI / 8)))
{
g0 = double(dst.ptr<uchar>(j - 1)[i - 1]);
g1 = double(dst.ptr<uchar>(j + 1)[i + 1]);
}
else if ((t >= -(M_PI / 8)) && (t < M_PI / 8))
{
g0 = double(dst.ptr<uchar>(j)[i - 1]);
g1 = double(dst.ptr<uchar>(j)[i + 1]);
}
else if ((t >= M_PI / 8) && (t < 3 * M_PI / 8))
{
g0 = double(dst.ptr<uchar>(j - 1)[i + 1]);
g1 = double(dst.ptr<uchar>(j + 1)[i - 1]);
}
else
{
g0 = double(dst.ptr<uchar>(j - 1)[i]);
g1 = double(dst.ptr<uchar>(j + 1)[i]);
}
if (g <= g0 || g <= g1)
{
dst.ptr<uchar>(j)[i] = 0.0;
}
}
}
}
/*
函数说明:弱边缘点补充连接强边缘点
函数输入:img 弱边缘点补充连接强边缘点的输入和输出图像
*/
void doubleThresholdLink(Mat& img)
{
// 循环找到强边缘点,把其领域内的弱边缘点变为强边缘点
for (int j = 1; j < img.rows - 2; j++)
{
for (int i = 1; i < img.cols - 2; i++)
{
// 如果该点是强边缘点
if (img.ptr<uchar>(j)[i] == 255)
{
// 遍历该强边缘点领域
for (int m = -1; m < 1; m++)
{
for (int n = -1; n < 1; n++)
{
// 该点为弱边缘点(不是强边缘点,也不是被抑制的0点)
if (img.ptr<uchar>(j + m)[i + n] != 0 && img.ptr<uchar>(j + m)[i + n] != 255)
{
img.ptr<uchar>(j + m)[i + n] = 255; //该弱边缘点补充为强边缘点
}
}
}
}
}
}
for (int j = 0; j < img.rows - 1; j++)
{
for (int i = 0; i < img.cols - 1; i++)
{
// 如果该点依旧是弱边缘点,及此点是孤立边缘点
if (img.ptr<uchar>(j)[i] != 255 && img.ptr<uchar>(j)[i] != 255)
{
img.ptr<uchar>(j)[i] = 0; //该孤立弱边缘点抑制
}
}
}
}
/*
函数说明:用双阈值算法检测和连接边缘
函数输入: low 输入的低阈值
high 输入的高阈值
img 输入的原图像
dst 输出的用双阈值算法检测和连接边缘后的图像
*/
void doubleThreshold(double low, double high, Mat& img, Mat& dst)
{
dst = img.clone();
// 区分出弱边缘点和强边缘点
for (int j = 0; j < img.rows - 1; j++)
{
for (int i = 0; i < img.cols - 1; i++)
{
double x = double(dst.ptr<uchar>(j)[i]);
// 像素点为强边缘点,置255
if (x > high)
{
dst.ptr<uchar>(j)[i] = 255;
}
// 像素点置0,被抑制掉
else if (x < low)
{
dst.ptr<uchar>(j)[i] = 0;
}
}
}
// 弱边缘点补充连接强边缘点
doubleThresholdLink(dst);
}
int main()
{
// (1)读取图片
Mat img = imread("test.jpg");
if (img.empty())
{
printf("读取图像失败!");
system("pause");
return 0;
}
// 转换为灰度图
Mat img_gray;
if (img.channels() == 3)
cvtColor(img, img_gray, COLOR_RGB2GRAY);
else
img_gray = img.clone();
// (2)高斯滤波
Mat gauss_img;
gaussianFilter(img_gray, gauss_img);
// (3)用一阶偏导有限差分计算梯度幅值和方向
Mat gradXY, theta;
getGrandient(gauss_img, gradXY, theta);
// (4)NMS非极大值抑制
Mat local_img;
nonLocalMaxValue(gradXY, theta, local_img);
// (5)用双阈值算法检测和连接边缘
Mat dst;
doubleThreshold(40, 80, local_img, dst);
// (6)图像显示
Mat outImg;
//namedWindow("原始图", 0);
//imshow("原始图", img);
//namedWindow("灰度图", 0);
//imshow("灰度图", img_gray);
mergeImg(outImg, img_gray, dst); //图像拼接(维度需相同)
namedWindow("img_gray"); //图窗名称
imshow("img_gray", outImg); //图像显示
imwrite("canny.jpg", outImg); //图像保存
waitKey(); //等待键值输入
return 0;
}