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;
}

猜你喜欢

转载自blog.csdn.net/shinuone/article/details/129672919