基于Canny算子边缘检测的基本原理和C++/C源码实现

基于canny算子的边缘检测

  • 边缘检测是基于灰度突变来分割图像的常用方法。Canny边缘检测于1986年由JOHN CANNY首次在论文《A Computational Approach to Edge Detection》中提出,就此拉开了Canny边缘检测算法的序幕。
  • canny边缘检测算法处理5个步骤
    • 使用高斯滤波算法,以平滑图像,滤除噪声。
    • 计算图像中每个像素点的梯度强度和方向。
    • 应用非极大值(NMS)抑制,以消除边缘检测带来的杂散响应。
    • 应用双阈值(DT)检测来确定真实的和潜在的边缘。
    • 通过抑制孤立的弱边缘最终完成边缘检测
  • 5个步骤的详细解析

    • 1)高斯滤波
      • a.使用高斯滤波器与图像进行卷积,平滑图像,去除噪声。高斯滤波器核大小为s*s(s为奇数)。滤波器中心位置为k, 高斯滤波器核的产生式:
        guassFilterij=12πδexp(i(k+1))2+(j(k+1))22δ1i,js(11) g u a s s F i l t e r i j = 1 2 π δ e x p ⟮ − ( i − ( k + 1 ) ) 2 + ( j − ( k + 1 ) ) 2 2 δ ⟯ 1 ≤ i , j ≤ s ( 1 − 1 )
      • b.归一化高斯滤波器核
        guassFilter=guassFiltersisjguassFilterij(12) g u a s s F i l t e r = g u a s s F i l t e r ∑ i s ∑ j s g u a s s F i l t e r i j ( 1 − 2 )
      • c.计算卷积
        guaResult=isjsguassFilteri,jimgi,j(13) g u a R e s u l t = ∑ i s ∑ j s g u a s s F i l t e r i , j i m g i , j ( 1 − 3 )

        这里写图片描述
    • 2)计算梯度强度和方向
      - 利用Sobel算子返回水平Gx和垂直Gy的一阶导数值。以此用来计算梯度强度G和方向thead。

      G=G2x+G2y(21) G = G x 2 + G y 2 ( 2 − 1 )

      θ=arctan(GyGx)(22) θ = a r c t a n ( G y G x ) ( 2 − 2 )

      Gx=ijSobelxi,jimgi,j(23) G x = ∑ i ∑ j S o b e l x i , j ∗ i m g i , j ( 2 − 3 )

      Gy=ijSobelyi,jimgi,j(24) G y = ∑ i ∑ j S o b e l y i , j ∗ i m g i , j ( 2 − 4 )

      这里写图片描述

      • 3) 应用非极大值(Non-Maximum Suppression)抑制,以消除边缘检测带来的杂散响应
        非极大值抑制是一种边缘稀疏技术,非极大值抑制的作用在于“瘦”边。对图像进行梯度计算后,仅仅基于梯度值提取的边缘仍然很模糊。对于标准3,对边缘有且应当只有一个准确的响应。而非极大值抑制则可以帮助将局部最大值之外的所有梯度值抑制为0,对梯度图像中每个像素进行非极大值抑制的算法是:

        1) 将当前像素的梯度强度与沿正负梯度方向上的两个像素进行比较。

        2) 如果当前像素的梯度强度与另外两个像素相比最大,则该像素点保留为边缘点, 否则该像素点将被抑制。

        通常为了更加精确的计算,在跨越梯度方向的两个相邻像素之间使用线性插值来得到要 比较的像素梯度,现举例如下:
        这里写图片描述

    • 4)双阈值检测
      • 在施加非极大值抑制之后,剩余的像素可以更准确地表示图像中的实际边缘。然而,仍然存在由于噪声和颜色变化引起的一些边缘像素。为了解决这些杂散响应,必须用弱梯度值过滤边缘像素,并保留具有高梯度值的边缘像素,可以通过选择高低阈值来实现。如果边缘像素的梯度值高于高阈值,则将其标记为强边缘像素;如果边缘像素的梯度值小于高阈值并且大于低阈值,则将其标记为弱边缘像素;如果边缘像素的梯度值小于低阈值,则会被抑制。阈值的选择取决于给定输入图像的内容。
      • 高低阈值分别为heightThes 和 lowThes.
        确定阈值方法有:全局阈值、Otsu等方法。
    • 5)到目前为止,被划分为强边缘的像素点已经被确定为边缘,因为它们是从图像中的真实 边缘中提取出来的。然而,对于弱边缘像素,将会有一些争论,因为这些像素可以从真实边缘提取也可以是因噪声或颜色变化引起的。为了获得准确的结果,应该抑制由后者引起的弱边缘。通常,由真实边缘引起的弱边缘像素将连接到强边缘像素,而噪声响应未连接。为了跟踪边缘连接,通过查看弱边缘像素及其8个邻域像素,只要其中一个为强边缘像素,则该弱边缘点就可以保留为真实的边缘。

抑制孤立边缘点的伪代码描述如下:
这里写图片描述

源码解析

/**
 * @brief cannyEdgeDetection    基础canny算子边缘检测
 * @param img                   原图
 * @param result                结果图片
 * @param guaSize               高斯核大小 奇数
 * @param hightThres            高阈值 0-1
 * @param lowThres              低阈值 0-1
 * @return null
 * @note code by jmu-stu jsc 2018-06
 *                  1.高斯滤波
 *                  2.计算梯度强度和方向
 *                  3.非极大值抑制
 *                  4.双阈值检测
 *                  5.抑制孤立低阈值点
 */
 #define pi 3.14159
void cannyEdgeDetection(cv::Mat img, cv::Mat &result, int guaSize, double hightThres, double lowThres  ){
    // 高斯滤波
    cv::Rect rect; // IOU区域
    cv::Mat filterImg = cv::Mat::zeros(img.rows, img.cols, CV_64FC1);
    img.convertTo(img, CV_64FC1);
    result = cv::Mat::zeros(img.rows, img.cols, CV_64FC1);
    int guassCenter = guaSize / 2; // 高斯核的中心 // (2* guassKernelSize +1) * (2*guassKernelSize+1)高斯核大小
    double sigma = 1;   // 方差大小
    cv::Mat guassKernel = cv::Mat::zeros(guaSize, guaSize, CV_64FC1);
    for(int i = 0; i< guaSize; i++){
        for(int j = 0; j < guaSize; j++){
            guassKernel.at<double>(i, j) =  (1.0 / (2.0 * pi * sigma * sigma)) *
            (double)exp(-(((double)pow((i - (guassCenter+ 1)),2) + (double)pow((j - (guassCenter + 1)),2)) / (2.0*sigma*sigma)));
           // std::cout<<guassKernel.at<double>(i, j) << " ";
        }
       // std::cout<<std::endl;
    }
    cv::Scalar sumValueScalar = cv::sum(guassKernel);
    double sum = sumValueScalar.val[0];
    std::cout<<sum<<std::endl;
    guassKernel = guassKernel / sum;
//    for(int i = 0; i< guaSize; i++){
//        for(int j = 0; j < guaSize; j++){
//            std::cout<<guassKernel.at<double>(i, j) << " ";
//        }
//        std::cout<<std::endl;
//    }
    for(int i = guassCenter; i< img.rows - guassCenter; i++){
        for(int j = guassCenter; j < img.cols - guassCenter; j++){
            rect.x = j - guassCenter;
            rect.y = i - guassCenter;
            rect.width = guaSize;
            rect.height = guaSize;
            filterImg.at<double>(i, j) = cv::sum(guassKernel.mul(img(rect))).val[0];
            // std::cout<<filterImg.at<double>(i,j) << " ";
        }
        // std::cout<<std::endl;
    }
    cv::Mat guassResult;
    filterImg.convertTo(guassResult, CV_8UC1);
    cv::imshow("guass-result", guassResult);
    // std::cout<<cv::sum(guassKernel).val[0]<<std::endl;
    // 计算梯度,用sobel算子
    cv::Mat gradX = cv::Mat::zeros(img.rows, img.cols, CV_64FC1); // 水平梯度
    cv::Mat gradY = cv::Mat::zeros(img.rows, img.cols, CV_64FC1); // 垂直梯度
    cv::Mat grad = cv::Mat::zeros(img.rows, img.cols, CV_64FC1);  // 梯度幅值
    cv::Mat thead = cv::Mat::zeros(img.rows, img.cols, CV_64FC1); // 梯度角度
    cv::Mat locateGrad = cv::Mat::zeros(img.rows, img.cols, CV_64FC1); //区域
    // x方向的sobel算子
    cv::Mat Sx = (cv::Mat_<double>(3,3) << -1,0,1,
                                          -2,0,2,
                                          -1,0,1
                                          );
    // y方向sobel算子
    cv::Mat Sy = (cv::Mat_<double>(3,3) <<  1,2,1,
                                           0,0,0,
                                          -1,-2,-1
                                        );
    // 计算梯度赋值和角度
    for(int i = 1 ; i < img.rows-1; i++ ){
        for( int j = 1; j<img.cols-1; j++){
            // 卷积区域 3*3
            rect.x = j-1;
            rect.y = i-1;
            rect.width = 3;
            rect.height = 3;
            cv::Mat rectImg = cv::Mat::zeros(3,3,CV_64FC1);
            filterImg(rect).copyTo(rectImg);
            // 梯度和角度
            gradX.at<double>(i,j) += cv::sum(rectImg.mul(Sx)).val[0];
            gradY.at<double>(i,j) += cv::sum(rectImg.mul(Sy)).val[0];
            grad.at<double>(i,j) = sqrt(pow(gradX.at<double>(i,j),2) + pow(gradY.at<double>(i,j),2));
            thead.at<double>(i, j) = atan(gradY.at<double>(i,j)/gradX.at<double>(i,j));
            // 设置四个区域
            if(0 <= thead.at<double>(i,j) <= (pi/4.0)){
                locateGrad.at<double>(i, j) = 0;
            }
            else if(pi/4.0 < thead.at<double>(i,j) <= (pi/2.0)){
                locateGrad.at<double>(i, j) = 1;
            }
            else if(-pi/2.0 <= thead.at<double>(i,j) <= (-pi/4.0)){
                locateGrad.at<double>(i, j) = 2;
            }
            else if(-pi/4.0 < thead.at<double>(i,j) < 0){
                locateGrad.at<double>(i, j) = 3;
            }
        }
    }
    // debug
    cv::Mat tempGrad;
    grad.convertTo(tempGrad, CV_8UC1);
    imshow("grad", tempGrad);
    // 梯度归一化
    double gradMax;
    cv::minMaxLoc(grad, &gradMax); // 求最大值
    if (gradMax != 0){
        grad = grad / gradMax;
    }
    // debug
    cv::Mat tempGradN;
    grad.convertTo(tempGradN, CV_8UC1);
    imshow("gradN", tempGradN);

    // 双阈值确定
    cv::Mat caculateValue = cv::Mat::zeros(img.rows, img.cols, CV_64FC1); // grad变成一维
    cv::resize(grad, caculateValue,cv::Size(1,(grad.rows * grad.cols)));
    // caculateValue.convertTo(caculateValue, CV_64FC1);
    cv::sort(caculateValue, caculateValue,CV_SORT_EVERY_COLUMN + CV_SORT_ASCENDING); // 升序
    long long highIndex = img.rows * img.cols * hightThres;
    double highValue = caculateValue.at<double>(highIndex, 0) ; // 最大阈值
    // debug
    // std::cout<< "highValue: "<<highValue<<" "<<  caculateValue.cols << " "<<highIndex<< std::endl;

    double lowValue = highValue * lowThres; // 最小阈值
    // 3.非极大值抑制, 采用线性插值
    for(int i = 1 ; i < img.rows-1; i++ ){
        for( int j = 1; j<img.cols-1; j++){
            // 八个方位
            double N = grad.at<double>(i-1, j);
            double NE = grad.at<double>(i-1, j+1);
            double E = grad.at<double>(i, j+1);
            double SE = grad.at<double>(i+1, j+1);
            double S = grad.at<double>(i+1, j);
            double SW = grad.at<double>(i-1, j-1);
            double W = grad.at<double>(i, j-1);
            double NW = grad.at<double>(i -1, j -1);
            // 区域判断,线性插值处理
            double tanThead; // tan角度
            double Gp1; // 两个方向的梯度强度
            double Gp2;
            // 求角度,绝对值
            tanThead = abs(tan(thead.at<double>(i,j)));
            switch ((int)locateGrad.at<double>(i,j)) {
            case 0:
                   Gp1 = (1- tanThead) * E + tanThead * NE;
                   Gp2 = (1- tanThead) * W + tanThead * SW;
                   break;
            case 1:
                   Gp1 = (1- tanThead) * N + tanThead * NE;
                   Gp2 = (1- tanThead) * S + tanThead * SW;
                   break;
            case 2:
                   Gp1 = (1- tanThead) * N + tanThead * NW;
                   Gp2 = (1- tanThead) * S + tanThead * SE;
                   break;
            case 3:
                   Gp1 = (1- tanThead) * W + tanThead *NW;
                   Gp2 = (1- tanThead) * E + tanThead *SE;
                   break;
            default:
                break;
            }
            // NMS -非极大值抑制和双阈值检测
            if(grad.at<double>(i, j) >= Gp1  && grad.at<double>(i, j) >= Gp2){
                //双阈值检测
                if(grad.at<double>(i, j) >= highValue){
                    grad.at<double>(i, j) = highValue;
                    result.at<double>(i, j) = 255;
                }
                else if(grad.at<double>(i, j) < lowValue){
                    grad.at<double>(i, j) = 0;
                }
                else{
                     grad.at<double>(i, j) = lowValue;
                }

            }
            else{
                grad.at<double>(i, j) = 0;
            }
        }
    }
    // NMS 和算阈值检测后的梯度图
    cv::Mat tempGradNMS;
    grad.convertTo(tempGradNMS, CV_8UC1);
    imshow("gradNMS", tempGradNMS);

    // 4.抑制孤立低阈值点 3*3. 找到高阈值就255
    for(int i = 1 ; i < img.rows-1; i++ ){
        for( int j = 1; j<img.cols-1; j++){
           if(grad.at<double>(i, j) == lowValue){
               // 3*3区域找强梯度
               rect.x = j-1;
               rect.y = i-1;
               rect.width = 3;
               rect.height = 3;
               for(int i1 = 0; i1 < 3; i1++){
                   for(int j1 = 0; j1<3; j1++){
                       if(grad(rect).at<double>(i1,j1) == highValue){
                           result.at<double>(i, j) = 255;
                           std::cout<<result.at<double>(i, j);
                           break;
                       }
                   }
               }
           }
        }
     }
    // 结果
    result.convertTo(result, CV_8UC1);
    imshow("result", result);


}
  • demo
    main.cpp

#include <opencv2/opencv.hpp>
#include <string.h>
#include <iostream>
#include <math.h>
void main(){
    cv::Mat img = cv::imread("../cameraman.tif");
    cv::Mat result;
    cv::Mat grayImage;
    cv::cvtColor(img,grayImage, CV_BGR2GRAY);
    cannyEdgeDetection(grayImage, result, 3, 0.8, 0.5);
    cv::imshow("gray", grayImage);
    cv::waitKey();
}
  • 结果
    这里写图片描述

参考链接

[1]https://www.cnblogs.com/techyan1990/p/7291771.html
[2]https://wenku.baidu.com/view/607820c16137ee06eff9187b.html (阈值确定参考这个)

  • 9
    点赞
  • 48
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Canny算子是一种常用的边缘检测方法,它的基本原理是利用图像中像素点灰度值的变化来检测边缘。其步骤如下: 1. 高斯滤波:对原始图像进行高斯滤波,平滑图像降低噪声干扰。 2. 计算梯度:利用Sobel算子计算图像的梯度值,求出每个像素点的梯度幅值和方向。 3. 非极大值抑制:对梯度图像进行非极大值抑制,保留梯度方向上的局部最大值,抑制其他值,从而使边缘更加细化。 4. 双阈值检测:根据设定的高低阈值,将梯度幅值分成强边缘、弱边缘和非边缘三个部分。只有强边缘和与之相邻的弱边缘才被认为是真正的边缘,其他部分被认为是噪声。 5. 连通分析:根据强边缘和弱边缘的连通性,将它们分成若干条边缘线段。 Canny算子的代码实现如下: ```python import cv2 import numpy as np # 读取图像 img = cv2.imread('image.jpg', 0) # 高斯滤波 img_blur = cv2.GaussianBlur(img, (3, 3), 0) # 计算梯度 sobelx = cv2.Sobel(img_blur, cv2.CV_64F, 1, 0, ksize=3) sobely = cv2.Sobel(img_blur, cv2.CV_64F, 0, 1, ksize=3) grad_mag = np.sqrt(sobelx ** 2 + sobely ** 2) grad_dir = np.arctan2(sobely, sobelx) * 180 / np.pi # 非极大值抑制 grad_mag_max = cv2.dilate(grad_mag, np.ones((3, 3))) grad_mag_nms = np.zeros(grad_mag.shape) grad_mag_nms[(grad_mag == grad_mag_max) & (grad_mag > 5)] = grad_mag[(grad_mag == grad_mag_max) & (grad_mag > 5)] # 双阈值检测 low_threshold = 40 high_threshold = 80 strong_edges = (grad_mag_nms > high_threshold).astype(np.uint8) weak_edges = ((grad_mag_nms >= low_threshold) & (grad_mag_nms <= high_threshold)).astype(np.uint8) # 连通分析 _, strong_edges = cv2.threshold(strong_edges, 0, 255, cv2.THRESH_BINARY) _, labels = cv2.connectedComponents(strong_edges) label_values = np.arange(1, labels.max() + 1) edge_points = [] for label in label_values: edge_points.append(np.column_stack(np.where(labels == label))) edge_lines = [] for edge_point in edge_points: if edge_point.shape[0] < 5: continue edge_line = cv2.fitLine(edge_point, cv2.DIST_L2, 0, 0.01, 0.01) edge_lines.append(edge_line) # 绘制边缘 for line in edge_lines: vx, vy, x0, y0 = line x1 = int((img.shape[0] - y0) * vx / vy + x0) x2 = int(-y0 * vx / vy + x0) cv2.line(img, (x1, img.shape[0]), (x2, 0), (0, 0, 255), 1) cv2.imshow('edge detection', img) cv2.waitKey() cv2.destroyAllWindows() ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值