博客
关于我
Canny算子边缘检测原理及实现
阅读量:798 次
发布时间:2023-04-15

本文共 6594 字,大约阅读时间需要 21 分钟。

Canny边缘检测算法是图像处理领域中的经典算法之一,由Joseph Faure和Luc Boyer于1986年提出。经过数十年的发展,Canny算法依然是图像边缘检测领域的核心技术之一。与传统的Sobel、Prewitt等算法相比,Canny算法在边缘检测准确性和鲁棒性上具有显著优势。

Canny算法的优势

传统的Sobel、Prewitt等算子在边缘检测中存在以下不足:

  • 梯度方向利用不足:这些算子通常只计算了边缘的梯度幅度,未充分利用边缘的梯度方向信息。
  • 简单的二值处理:检测后的二值图像仅依赖单一的阈值进行处理,难以有效抑制伪边缘。
  • Canny算法针对上述问题进行了深入改进,提出了以下核心技术:

  • 基于梯度方向的非极大值抑制:通过分析3x3邻域内的梯度方向,抑制非极大值,保留局部最强的边缘点。
  • 双阈值滞后处理:采用双阈值策略,既能有效抑制伪边缘,又能准确提取真实边缘。
  • Canny算法的原理

    Canny算法的目标是实现以下三点:

  • 低错误率:确保所有真实边缘都被检测到,同时避免检测伪边缘。
  • 高精度定位:检测到的边缘点尽可能接近真实边缘。
  • 单一边缘点响应:在单个边缘点位置,检测器应只输出一个响应。
  • Canny算法的具体实现步骤如下:

  • 高斯平滑:对输入图像进行高斯滤波,降低噪声对边缘检测的影响。
  • 计算梯度:使用Sobel算子等计算图像的水平和垂直差分,进而得到每点的梯度向量(方向和幅度)。
  • 非极大值抑制:根据梯度方向,将3x3邻域内的梯度值进行比较,保留最大的梯度值,抑制非极大值。
  • 双阈值滞后处理:通过双阈值策略,确定边缘点,并对相邻的边缘点进行连接,形成连通的边缘曲线。
  • 详细步骤解析

  • 高斯平滑

    高斯滤波是一种有效的降噪技术,通过卷积核对图像进行平滑处理,可以减少噪声对边缘检测的干扰。

  • 计算梯度

    使用Sobel算子等计算水平和垂直方向的梯度。Sobel算子是一种多 tapped 核,能够有效计算图像的梯度信息。通过对原始图像进行水平和垂直方向的卷积,可以得到水平和垂直方向的差分矩阵。

  • 梯度幅度和方向

    梯度的幅度可以通过勾股定理计算,即√(dx² + dy²)。方向则通过反正切函数计算,θ = arctan(dy/dx)。

  • 非极大值抑制

    在3x3邻域内,Canny算法将梯度值进行方向分组。具体来说,边缘的方向分为四类:

    • 水平边缘(梯度方向垂直):22.5° ≤ |θ| ≤ 67.5° 或 -157.5° ≤ θ ≤ -112.5°。
    • 垂直边缘(梯度方向水平):67.5° ≤ |θ| ≤ 112.5° 或 -112.5° ≤ θ ≤ -67.5°。
    • 45°边缘(梯度方向正交):112.5° ≤ |θ| ≤ 157.5° 或 -67.5° ≤ θ ≤ -22.5°。
    • 135°边缘(梯度方向正交):-22.5° ≤ θ ≤ 22.5° 或 157.5° ≤ θ ≤ 180°。

    在每个方向上,Canny算法对3x3邻域内的梯度值进行比较,保留最大的梯度值,抑制非极大值,从而细化边缘。

  • 双阈值滞后处理

    通过双阈值策略,确定边缘点。通常采用两个阈值,较大的阈值用于确定明确的边缘点,而较小的阈值用于连接相邻的边缘点,形成连通的边缘曲线。这种双阈值策略能够有效抑制伪边缘,同时保持边缘的连贯性。

  • 代码实现

    #include 
    #include
    #include
    #include
    using namespace std;// 获取Sobel平滑算子的权重int factorial(int n) { int fac = 1; for (int i = 1; i <= n; ++i) { fac *= i; } return fac;}Mat getSobelSmooth(int wsize) { int n = wsize - 1; Mat SobelSmooth = Mat::zeros(Size(wsize, 1), CV_32FC1); for (int k = 0; k <= n; ++k) { float *ptr = SobelSmooth.ptr
    (0); ptr[k] = factorial(n) / (factorial(k) * factorial(n - k)); } return SobelSmooth;}Mat getSobelDiff(int wsize) { Mat SobelDiff = Mat::zeros(Size(wsize, 1), CV_32FC1); Mat SobelSmooth = getSobelSmooth(wsize - 1); for (int k = 0; k < wsize; ++k) { if (k == 0) { SobelDiff.at
    (0, k) = 1; } else if (k == wsize - 1) { SobelDiff.at
    (0, k) = -1; } else { SobelDiff.at
    (0, k) = SobelSmooth.at
    (0, k) - SobelSmooth.at
    (0, k - 1); } } return SobelDiff;}void conv2D(Mat &src, Mat &dst, Mat kernel, int ddepth, Point anchor = Point(-1, -1), int delta = 0, int borderType = BORDER_DEFAULT) { Mat kernelFlip; flip(kernel, kernelFlip, -1); filter2D(src, dst, ddepth, kernelFlip, anchor, delta, borderType);}void sepConv2D_Y_X(Mat &src, Mat &dst, Mat kernel_Y, Mat kernel_X, int ddepth, Point anchor = Point(-1, -1), int delta = 0, int borderType = BORDER_DEFAULT) { Mat dst_kernel_Y; conv2D(src, dst_kernel_Y, kernel_Y, ddepth, anchor, delta, borderType); conv2D(dst_kernel_Y, dst, kernel_X, ddepth, anchor, delta, borderType);}void sepConv2D_X_Y(Mat &src, Mat &dst, Mat kernel_X, Mat kernel_Y, int ddepth, Point anchor = Point(-1, -1), int delta = 0, int borderType = BORDER_DEFAULT) { Mat dst_kernel_X; conv2D(src, dst_kernel_X, kernel_X, ddepth, anchor, delta, borderType); conv2D(dst_kernel_X, dst, kernel_Y, ddepth, anchor, delta, borderType);}void Sobel(Mat &src, Mat &dst_X, Mat &dst_Y, Mat &dst, int wsize, int ddepth, Point anchor = Point(-1, -1), int delta = 0, int borderType = BORDER_DEFAULT) { Mat SobelSmooth = getSobelSmooth(wsize); Mat SobelDiff = getSobelDiff(wsize); sepConv2D_Y_X(src, dst_X, SobelSmooth, SobelDiff, ddepth, anchor, delta, borderType); sepConv2D_X_Y(src, dst_Y, SobelSmooth, SobelDiff.t(), ddepth, anchor, delta, borderType); dst = abs(dst_X) + abs(dst_Y); convertScaleAbs(dst, dst);}bool checkInRange(int r, int c, int rows, int cols) { return r >= 0 && r < rows && c >= 0 && c < cols;}void trace(Mat &edgeMag_noMaxsup, Mat &edge, float TL, int r, int c, int rows, int cols) { if (edge.at
    (r, c) == 0) { edge.at
    (r, c) = 255; for (int i = -1; i <= 1; ++i) { for (int j = -1; j <= 1; ++j) { if (checkInRange(r + i, c + j, rows, cols) && edgeMag_noMaxsup.at
    (r + i, c + j) >= TL) { trace(edgeMag_noMaxsup, edge, TL, r + i, c + j, rows, cols); } } } }}void Edge_Canny(Mat &src, Mat &edge, float TL, float TH, int wsize = 3, bool L2gradient = false) { int rows = src.rows; int cols = src.cols; GaussianBlur(src, src, Size(5, 5), 0.8); Mat dx, dy, sobel_dst; Sobel(src, dx, dy, sobel_dst, wsize, CV_32FC1); Mat edgeMag; if (L2gradient) { magnitude(dx, dy, edgeMag); } else { edgeMag = abs(dx) + abs(dy); } Mat edgeMag_noMaxsup = Mat::zeros(rows, cols, CV_32FC1); for (int r = 1; r < rows - 1; ++r) { for (int c = 1; c < cols - 1; ++c) { float x = dx.at
    (r, c); float y = dy.at
    (r, c); float angle = atan2f(y, x) / PI * 180; float mag = edgeMag.at
    (r, c); if (abs(angle) < 22.5 || abs(angle) > 157.5) { float left = edgeMag.at
    (r, c - 1); float right = edgeMag.at
    (r, c + 1); if (mag >= left && mag >= right) { edgeMag_noMaxsup.at
    (r, c) = mag; } } else if ((angle >= 67.5 && angle <= 112.5) || (angle >= -112.5 && angle <= -67.5)) { float top = edgeMag.at
    (r - 1, c); float down = edgeMag.at
    (r + 1, c); if (mag >= top && mag >= down) { edgeMag_noMaxsup.at
    (r, c) = mag; } } else if ((angle >= 112.5 && angle <= 157.5) || (angle >= -67.5 && angle <= -22.5)) { float right_top = edgeMag.at
    (r - 1, c + 1); float left_down = edgeMag.at
    (r + 1, c - 1); if (mag >= right_top && mag >= left_down) { edgeMag_noMaxsup.at
    (r, c) = mag; } } else if ((angle >= 22.5 && angle <= 67.5) || (angle >= -157.5 && angle <= -112.5)) { float left_top = edgeMag.at
    (r - 1, c - 1); float right_down = edgeMag.at
    (r + 1, c + 1); if (mag >= left_top && mag >= right_down) { edgeMag_noMaxsup.at
    (r, c) = mag; } } } } edge = Mat::zeros(rows, cols, CV_8UC1); for (int r = 1; r < rows - 1; ++r) { for (int c = 1; c < cols - 1; ++c) { float mag = edgeMag_noMaxsup.at
    (r, c); if (mag >= TH) { trace(edgeMag_noMaxsup, edge, TL, r, c, rows, cols); } else if (mag < TL) { edge.at
    (r, c) = 0; } } }}int main() { Mat src = imread("I:\\Learning-and-Practice\\2019Change\\Image process algorithm\\Img\\lena.jpg"); if (src.empty()) { return -1; } if (src.channels() > 1) { cvtColor(src, src, CV_RGB2GRAY); } Mat edge; Edge_Canny(src, edge, 20, 60); namedWindow("src", CV_WINDOW_NORMAL); imshow("src", src); namedWindow("My_canny", CV_WINDOW_NORMAL); imshow("My_canny", edge); namedWindow("Opencv_canny", CV_WINDOW_NORMAL); imshow("Opencv_canny", src); waitKey(0); return 0;}

    效果对比

    Canny算法与OpenCV的Canny函数有以下主要区别:

  • 双阈值处理:OpenCV的Canny函数默认使用L2距离作为梯度计算方式,并提供了多种参数选项。
  • 非极大值抑制:OpenCV的Canny函数支持多种边缘抑制策略。
  • 通过对比,可以看出Canny算法与OpenCV的实现方式有所不同,但核心目标保持一致:实现高精度、高鲁棒性的边缘检测。

    转载地址:http://qerfk.baihongyu.com/

    你可能感兴趣的文章
    MySQL-索引的分类(聚簇索引、二级索引、联合索引)
    查看>>
    Mysql-触发器及创建触发器失败原因
    查看>>
    MySQL-连接
    查看>>
    mysql-递归查询(二)
    查看>>
    MySQL5.1安装
    查看>>
    mysql5.5和5.6版本间的坑
    查看>>
    mysql5.5最简安装教程
    查看>>
    mysql5.6 TIME,DATETIME,TIMESTAMP
    查看>>
    mysql5.6.21重置数据库的root密码
    查看>>
    Mysql5.6主从复制-基于binlog
    查看>>
    MySQL5.6忘记root密码(win平台)
    查看>>
    MySQL5.6的Linux安装shell脚本之二进制安装(一)
    查看>>
    MySQL5.6的zip包安装教程
    查看>>
    mysql5.7 for windows_MySQL 5.7 for Windows 解压缩版配置安装
    查看>>
    Webpack 基本环境搭建
    查看>>
    mysql5.7 安装版 表不能输入汉字解决方案
    查看>>
    MySQL5.7.18主从复制搭建(一主一从)
    查看>>
    MySQL5.7.19-win64安装启动
    查看>>
    mysql5.7.19安装图解_mysql5.7.19 winx64解压缩版安装配置教程
    查看>>
    MySQL5.7.37windows解压版的安装使用
    查看>>