好的图像边缘检测应该满足

  1. 尽可能的标记处所有的边缘;
  2. 标记出的边缘就是实际图像内容的边缘;
  3. 图像中的边缘只标记一次。

Canny边缘检测的步骤

  1. 图像灰度化;
  2. 高斯滤波(不限制)降噪;
  3. 使用简单的算子(如Sobel、Prewitt等)检测图像水平grad_xgrad\_xgrad_x、垂直梯度grad_ygrad\_ygrad_y,并求出梯度grad_x2+grad_y2\sqrt{grad\_x^2 + grad\_y^2}grad_x2+grad_y2​和梯度方向(角度)grad_y/grad_xgrad\_y/grad\_xgrad_y/grad_x;
  4. 对梯度进行NMS;
  5. 使用双阈值法确定强边缘、非边缘、弱边缘,以及弱边缘的二次判定。

C++实现示例

auxiliary.hpp

#pragma once#include <cmath>
#include <numeric>   // std::accumulate
#include <vector>
#include <opencv2/imgproc.hpp>/*  f(x,y) = exp(-(x^2 + y^2) / (2 * sigma^2)) / (2 * pi * sigma ^ 2) */
int GaussianKernel(int kernel_size, std::vector<std::vector<float>> &kernel)
{kernel.clear();kernel.resize(kernel_size);for (auto &it : kernel){it.resize(kernel_size);}std::vector<int> coord_val(kernel_size, -kernel_size / 2);  //  坐标取值范围,一般取0左右对称的相反数for (int i = 1; i < kernel_size; ++i){coord_val[i] = coord_val[i - 1] + 1;}const float kSigma = 0.5f;     // sigma,σ,Standard Deviationfloat val1 = 1 / (2 * M_PI * kSigma * kSigma);float val2 = -1 / (2 * kSigma * kSigma);for (int i = 0; i < kernel_size; ++i){for (int j = 0; j < kernel_size; ++j){kernel[i][j] = val1 * exp(val2 * (pow(coord_val[i], 2) + pow(coord_val[j], 2)));}}float sum = 0.0f;for (int i = 0; i < kernel_size; ++i){sum += std::accumulate(kernel[i].begin(), kernel[i].end(), 0.0);}for (int i = 0; i < kernel_size; ++i){for (int j = 0; j < kernel_size; ++j){kernel[i][j] /= sum;}}return 0;
}int GaussianBlur(const std::vector<std::vector<float>> &kernel, const cv::Mat &src, cv::Mat &formula_gaussian)
{int kernel_size = kernel.size();/* 对原图灰度图补pad */int pad = kernel_size >> 1;cv::Mat formula_pad = cv::Mat(src.rows + 2 * pad, src.cols + 2 * pad, CV_32FC1);memset(formula_pad.data, 0, formula_pad.rows * formula_pad.cols * sizeof(float));cv::Rect rect(pad, pad, src.cols, src.rows);src.copyTo(formula_pad(rect));/* 执行高斯滤波 */formula_gaussian = cv::Mat(src.rows, src.cols, CV_32FC1);memset(formula_gaussian.data, 0, formula_gaussian.rows * formula_gaussian.cols * sizeof(float));for (int i = 0; i < src.rows; ++i){for (int j = 0; j < src.cols; ++j){float tmp = 0.0f;for (int m = 0; m < kernel_size; ++m){for (int n = 0; n < kernel_size; ++n){tmp += kernel[m][n] * formula_pad.at<float>(i + m, j + n);}}formula_gaussian.at<float>(i, j) = tmp;}}return 0;
}int DoSobel(const cv::Mat &formula_gaussian, cv::Mat &sobel_xy, cv::Mat &sobel_angle, float &mean_sobel_xy)
{/* Sobel滤波 */const int kSobelKernel = 3;int pad = kSobelKernel >> 1;   // Sobel的kernel大小是3std::vector<std::vector<int> > kernel_x{ {-1, 0, 1}, {-2, 0, 2}, {-1, 0, 1} };std::vector<std::vector<int> > kernel_y{ {1, 2, 1}, {0, 0, 0}, {-1, -2, -1} };cv::Mat gaussian_pad(formula_gaussian.rows + 2 * pad, formula_gaussian.cols + 2 * pad, CV_32FC1);memset(gaussian_pad.data, 0, gaussian_pad.rows * gaussian_pad.cols * sizeof(float));cv::Rect rect = cv::Rect(pad, pad, formula_gaussian.cols, formula_gaussian.rows);formula_gaussian.copyTo(gaussian_pad(rect));cv::Mat sobel_x(formula_gaussian.rows, formula_gaussian.cols, CV_32FC1);cv::Mat sobel_y(formula_gaussian.rows, formula_gaussian.cols, CV_32FC1);sobel_xy = cv::Mat(formula_gaussian.rows, formula_gaussian.cols, CV_32FC1);sobel_angle = cv::Mat(formula_gaussian.rows, formula_gaussian.cols, CV_32FC1);  // 这里角度用tan值表示const float kEps = 0.000001f;float sum_sobel_xy = 0.0f;mean_sobel_xy = 0.0f;for (int i = 0; i < formula_gaussian.rows; ++i){for (int j = 0; j < formula_gaussian.cols; ++j){float gradient_x = 0.0f;float gradient_y = 0.0f;float gradient_xy = 0.0f;for (int m = 0; m < kSobelKernel; ++m){for (int n = 0; n < kSobelKernel; ++n){gradient_x += kernel_x[m][n] * gaussian_pad.at<float>(i + m, j + n);gradient_y += kernel_y[m][n] * gaussian_pad.at<float>(i + m, j + n);}}gradient_xy = sqrt(pow(gradient_x, 2) + pow(gradient_y, 2));sobel_x.at<float>(i, j) = gradient_x;sobel_y.at<float>(i, j) = gradient_y;sobel_xy.at<float>(i, j) = gradient_xy;sobel_angle.at<float>(i, j) = gradient_y / (fabs(gradient_x) > kEps ? gradient_x : kEps);sum_sobel_xy += gradient_xy;}}mean_sobel_xy = sum_sobel_xy / (sobel_xy.rows * sobel_xy.cols);return 0;
}/* Efficient Non-Maximum Suppression */
// 在梯度图每个像素的八邻域内,求该像素的梯度正反两个方向对应的虚拟像素点的梯度,
// 如果该像素的梯度比两个虚拟像素的梯度大则认为是真边缘,否则是假边缘,舍弃。
// 八邻域eight-neighbor标识说明,en4标识中心点像素,其它8个是其邻域:
//                               en0 en1 en2
//                               en3 en4 en5
//                               en6 en7 en8
int DoNMS(const cv::Mat &sobel_xy, const cv::Mat &sobel_angle, cv::Mat &nms_sobel_xy)
{nms_sobel_xy = cv::Mat(sobel_xy.rows, sobel_xy.cols, CV_32FC1);memset(nms_sobel_xy.data, 0, nms_sobel_xy.rows * nms_sobel_xy.cols * sizeof(float));for (int i = 1; i < nms_sobel_xy.rows - 1; ++i)        // 图像第一行、最后一行不认为是边缘{for (int j = 1; j < nms_sobel_xy.cols - 1; ++j)    // 图像第一列、最后一列不认为是边缘{// 8邻域各像素的梯度float en0 = sobel_xy.at<float>(i - 1, j - 1);float en1 = sobel_xy.at<float>(i - 1, j);float en2 = sobel_xy.at<float>(i - 1, j + 1);float en3 = sobel_xy.at<float>(i, j - 1);float en4 = sobel_xy.at<float>(i, j);          // 当前像素梯度float en5 = sobel_xy.at<float>(i, j + 1);float en6 = sobel_xy.at<float>(i + 1, j - 1);float en7 = sobel_xy.at<float>(i + 1, j);float en8 = sobel_xy.at<float>(i + 1, j + 1);// 插值interpolationfloat grad_inter1 = 0.0f;float grad_inter2 = 0.0f;float ratio = 0.0f;   // 插值时的权重比例,[0.0, 1.0f]float angle = sobel_angle.at<float>(i, j);  // 注意角度使用dy/dx表示的,因此ratio是|angle|或1/|angle|if (angle >= 0)  // 第一、三象限{if (angle >= 1)         // [45°, 90°]使用en1、en2得到插值一,[225°, 270°]使用en6、en7得到插值二{ratio = 1.0f / angle;  // angle越大越靠近Y轴,en1、en7的占比就越大,ratio越小,1-ratio越大;// 当angele=1时是45°或225°方向此时正好en2或en6的占比是1;// 当angle无穷大时是90°或270°方向此时正好en1、en7的占比是1grad_inter1 = (1 - ratio) * en1 + ratio * en2;grad_inter2 = (1 - ratio) * en7 + ratio * en6;}else  // angle∈[0,1)   // [0°, 45°)使用en2、en5得到插值一,[180°, 225°)使用en3、en6得到插值二{ratio = angle;         // angle越小越靠近X轴,en5、en3的占比就越大,ratio越小,1-ratio越大;// 当angle=0时是0°或180°方向此时正好en5、en3的占比是1;// 当angle=1时是45°或225°方向此时正好en2、en6的占比是1grad_inter1 = (1 - ratio) * en5 + ratio * en2;grad_inter2 = (1 - ratio) * en3 + ratio * en6;}}else             // 第二、四象限{if (fabs(angle) >= 1)  // (90°, 135°]使用en0、en1得到插值一,(270°, 315°]使用en7、en8得到插值二{ratio = 1.0f / fabs(angle);  // fabs(angle)越大越靠近Y轴,en1、en7的占比就越大,ratio越小,1-ratio越大;// 当fabs(angle)=1时是135°或315°方向此时正好en0或en8的占比是1;// 当fabs(angle)无穷大时是90°或270°方向此时正好en1、en7的占比是1grad_inter1 = (1 - ratio) * en1 + ratio * en0;grad_inter2 = (1 - ratio) * en7 + ratio * en8;}else  // fabs(angle)∈[0,1)   // (135°, 180°]使用en0、en3得到插值一,(315°, 360°]使用en5、en8得到插值二{ratio = fabs(angle);         // fabs(angle)越小越靠近X轴,en3、en5的占比就越大,ratio越小,1-ratio越大;// 当fabs(angle)=0时是180°或360°方向此时正好en3或en5的占比是1;// 当fabs(angle)=1时是135°或315°方向此时正好en0、en8的占比是1grad_inter1 = (1 - ratio) * en3 + ratio * en0;grad_inter2 = (1 - ratio) * en5 + ratio * en8;}}if (en4 > grad_inter1 && en4 > grad_inter2){nms_sobel_xy.at<float>(i, j) = en4;}}}return 0;
}/* 双阈值检测如果边缘像素梯度值大于高阈值则认为是强边缘;如果边缘像素梯度值小于低阈值则不认为是边缘;如果边缘像素梯度介于高低阈值之间,则视为弱边缘,需要进一步判断若边缘的8邻域是否有强边缘,8邻域内只要有一个强边缘则认为该弱边缘是真实的边缘,否则不认为是边缘 */
int DoBinaryThresh(const cv::Mat &nms_sobel_xy, float mean_sobel_xy, cv::Mat &img_canny)
{cv::Mat binary_thresh_canny = cv::Mat(nms_sobel_xy.rows, nms_sobel_xy.cols, CV_32FC1);memset(binary_thresh_canny.data, 0, binary_thresh_canny.rows * binary_thresh_canny.cols * sizeof(float));float low_thresh = mean_sobel_xy * 0.5f;float high_thresh = low_thresh * 3.0f;for (int i = 1; i < binary_thresh_canny.rows - 1; ++i)      // 图像第一行、最后一行不认为是边缘{for (int j = 1; j < binary_thresh_canny.cols - 1; ++j)  // 图像第一列、最后一列不认为是边缘{float en0 = nms_sobel_xy.at<float>(i - 1, j - 1);float en1 = nms_sobel_xy.at<float>(i - 1, j);float en2 = nms_sobel_xy.at<float>(i - 1, j + 1);float en3 = nms_sobel_xy.at<float>(i, j - 1);float en4 = nms_sobel_xy.at<float>(i, j);              // 当前像素梯度float en5 = nms_sobel_xy.at<float>(i, j + 1);float en6 = nms_sobel_xy.at<float>(i + 1, j - 1);float en7 = nms_sobel_xy.at<float>(i + 1, j);float en8 = nms_sobel_xy.at<float>(i + 1, j + 1);if (en4 >= high_thresh)       // 强边缘{binary_thresh_canny.at<float>(i, j) = 255.0f;}else if (en4 <= low_thresh)  // 不是边缘{binary_thresh_canny.at<float>(i, j) = 0.0f;}else                        // 弱边缘,继续8邻域的判断{if (en0 >= high_thresh || en1 >= high_thresh || en2 >= high_thresh|| en3 >= high_thresh || en5 >= high_thresh|| en6 >= high_thresh || en7 >= high_thresh || en8 >= high_thresh){binary_thresh_canny.at<float>(i, j) = 255.0f;}}}}binary_thresh_canny.convertTo(img_canny, CV_8UC1);return 0;
}

Canny.cpp

#include "auxiliary.hpp"
#include <iostream>
#include <opencv2/highgui.hpp>int main()
{//cv::Mat img = cv::imread("../data/carton1.jpg", 0);cv::Mat img = cv::imread("../data/推导2.jpg", 0);cv::Mat imgf;img.convertTo(imgf, CV_32FC1, 1 / 255.0);/* 计算高斯滤波器 */int kKernelSize = 5;   // 滤波器大小,可自定义,但应当是奇数std::vector<std::vector<float>> kernel;int ret = GaussianKernel(kKernelSize, kernel);if (0 != ret){system("pause");return ret;}/* 高斯滤波 */cv::Mat formula_gaussian;ret = GaussianBlur(kernel, imgf, formula_gaussian);if (0 != ret){system("pause");return ret;}cv::Mat sobel_xy, sobel_angle;float mean_sobel_xy = 0.0f;ret = DoSobel(formula_gaussian, sobel_xy, sobel_angle, mean_sobel_xy);if (0 != ret){system("pause");return ret;}cv::Mat nms_sobel_xy;ret = DoNMS(sobel_xy, sobel_angle, nms_sobel_xy);if (0 != ret){system("pause");return ret;}cv::Mat img_canny;ret = DoBinaryThresh(nms_sobel_xy, mean_sobel_xy, img_canny);if (0 != ret){system("pause");return ret;}cv::imshow("img_canny", img_canny);cv::waitKey(0);return 0;
}

非极大值抑制时双线性插值示意图

图1 非极大值抑制时双线性插值示意图

实验图像及结果

以原图推导2.jpg为示例

图2 推导2.jpg

gaussian.jpg

图3 gaussian.jpg

sobel_x.jpg

图4 sobel_x.jpg

sobel_y.jpg

图5 sobel_y.jpg

sobel_xy.jpg

图6 sobel_xy.jpg

sobel_angle.jpg在NMS时插值计算梯度正反两个方向虚拟像素点梯度时使用。

图7 sobel_angle.jpg

nms_sobel_xy.jpg

图8 nms_sobel_xy.jpg

最终结果img_canny.jpg

图9 最终结果 img_canny.jpg

Reference

Canny算子中的非极大值抑制(Non-Maximum Suppression)分析

Canny的C++实现相关推荐

  1. OpenCV 笔记(03)— 读取视频、通过摄像头采集视频、采集视频 canny 边缘检测

    我们本节学习如何利用 OpenCV 中的 VideoCapture 类,来对视频进行读取显示,以及调用摄像头. VideoCapture 它提供了从摄像机或视频文件捕获视频的 C++ 接口, 作用是从 ...

  2. OpenCV 笔记(02)— 图像显示、保存、腐蚀、模糊、canny 边缘检测(imread、imshow、namedWindow、imwrite)

    OpenCV 提供两种用户界面选项: 基于原生用户界面的基本界面,适用于 Mac OS X 的 cocoa 或 carbon,以及适用于 Linux 或 Windows 用户界面的 GTK ,这些界面 ...

  3. Android OpenCV 边缘检测 Canny 的使用

    先看下实现的效果图 下面看下代码使用 Canny(Mat image, Mat edges, double threshold1, double threshold2, int apertureSiz ...

  4. OpenCV+python:Canny边缘检测算法

    1,边缘处理 图像边缘信息主要集中在高频段,通常说图像锐化或检测边缘,实质就是高频滤波.我们知道微分运算是求信号的变化率,具有加强高频分量的作用. 在空域运算中来说,对图像的锐化就是计算微分.由于数字 ...

  5. python+opencv Canny边缘检测

    Step1:高斯滤波(低通滤波,用高斯滤波器平滑图像) 卷积/卷积核(对应相乘再相加) 原始图片外围加一圈0,为保证新图片与原始图片大小相同 卷积核为3*3,外围补一圈0:卷积核为5*5,外围补两圈0 ...

  6. Python,Opencv cv2.Canny()边缘检测

    Python,Opencv的Canny边缘检测 1. 效果图 2. 源码 参考 这篇博客将介绍Canny边缘检测的概念,并利用cv2.Canny()实现边缘检测: Canny边缘检测是一种流行的边缘检 ...

  7. CS131专题-3:图像梯度、边缘检测(sobel、canny等)

    目录 1 前言 2 图像梯度 2.1 梯度公式的离散形式 2.2 图像的梯度表示 2.3 图像梯度的最简单计算方法 2.4 直接应用梯度找图像边缘的问题以及解决方案 3 边缘检测 3.1 好的边缘检测 ...

  8. OpenCV(十七)边缘检测3 -- Canny算子(最优边缘检测)

    目录 一.基础理论 1.作用及介绍 1.原理 2.过程 3.Canny函数 二.回调函数及总代码 效果 参考资料 ​​​​​​​ 一.基础理论 1.作用及介绍 Canny边缘检测是非常流行的边缘检测算 ...

  9. OpenCV——canny算子

    1. opencv--Canny算子 Canny 的目标是找到一个最优的边缘检测算法,最优边缘检测的含义是: 最好的检测: 算法能够尽可能多地标识出图像中的实际边缘. 最好的定位: 标识出的边缘要尽可 ...

  10. 【OpenCV 】Sobel 导数/Laplace 算子/Canny 边缘检测

    canny边缘检测见OpenCV [七]----边缘提取算子(图像边缘提取)--canny算法的原理及实现 1 Sobel 导数 1.1.1 原因 上面两节我们已经学习了卷积操作.一个最重要的卷积运算 ...

最新文章

  1. jmeter 做ip欺骗遇到的坑
  2. 稀疏矩阵的压缩存储--十字链表(转载)
  3. 浅谈MaxCompute资源规划管理及评估
  4. JSTL (JSP标准标签库)
  5. Git命令集十四——抓取命令
  6. ASM磁盘的添加与删除
  7. sev2008安装mysql_数据库教程
  8. OTO电子商务商业模式探析
  9. 本人有51SAP培训机构全套SAP培训课程教材和视频, 欲转手
  10. vb.net 教程 3-4 窗体编程 公共控件7 DateTimePicker MonthCalendar
  11. TCP Socket通信详细过程
  12. Flask结合flask_sqlalchemy教程
  13. linux java 缓存服务器,linux服务器缓存环境memcached筹建及应用(java)
  14. 过程式计算机语言,Go 语言程序设计——过程式编程(1)
  15. 注册石墨文档无法连接服务器,石墨文档没有访问权限怎么办
  16. 软件模块化定制将造成传统软件消失?
  17. 这朵玫瑰叫Jenny
  18. 2022-2028年中国循环经济产业深度调研及投资前景预测报告
  19. html5通用兄第选择器,css 通用兄弟选择器( ~ )
  20. 再见 Docker,是时候拥抱下一代容器工具 Containerd 了!

热门文章

  1. html多个ul时怎么选择某个li,选中多个ul中的第一个li方法
  2. 「建议收藏」我想进阿里,我该怎么做?
  3. 国际象棋和数学:麦粒、分币和神奇的马
  4. 看Kendo UI文档
  5. kendo ui 动态隐藏列_Kendo UI使用教程:Kendo UI Grid中的动态数据(一)
  6. CodeWar代码学习
  7. 使用ghost硬盘对拷备份系统
  8. spark常用功能:使用Spark计算数列统计值
  9. 【漆学军】MT4进阶到MT5速成之路(4)修改和删除挂单
  10. DWaterEA, 利用MT5历时3年开发的外汇智能交易系统