Canny边缘检测实现(Opencv C++)
Canny边缘检测是Canny在1986年提出来的,目前仍是图像边缘检测算法中最经典、先进的算法之一。canny方法基于如下三个基本目标:
1. 低错误率:所有边缘都应被找到,并且不应有虚假响应。
2. 最优定位:已定位的边缘必须尽可能接近真实边缘 。也就是说,由检测子标记为边缘的一点和真实边缘的中心之间的距离应最小。
3. 单个边缘点响应:对于每个真实的边缘点,检测子应只返回一个点。也就是说,真实边缘周围的局部最大数应该是最小的。这意味着检测子不应识别只存在单个边缘点的多个边缘像素。
canny的工作本质是,从数学上表达了前三个准则,并试图找到这些公式的最优解。
上篇博客介绍了边缘模型以及基本的边缘检测算法,接下来将了解Canny算法的步骤:
1. 使用高斯函数滤波器平滑输入图像
令f(x,y)表示输入图像,G(x,y)表示高斯函数:
我们用G和f卷积后得到平滑后的图像fs(x,y):
Mat gauImg;
GaussianBlur(src, gauImg, Size(13, 13), 2);
2. 计算梯度幅度和角度图像
计算梯度:
分别计算梯度幅度M的方向α:
代码实现:
Mat grad_x, grad_y, grad_xy;
Sobel(gauImg, grad_x, CV_32F, 1, 0, 3);
Sobel(gauImg, grad_y, CV_32F, 0, 1, 3);
Mat directImg, gradXY;
divide(grad_y, grad_x, directImg);
Mat gradX, gradY;
convertScaleAbs(grad_x, gradX);
convertScaleAbs(grad_y, gradY);
gradXY = gradX + gradY;
3. 非极大值抑制
利用非极大值抑制,细化梯度图像在局部极大值附近包含的一些宽脊。这种方法的实质是规定法线(梯度向量)的多个离散方向。例如,在一个3×3区域内,对于一个过该区域中心点的边缘,我们可以定义4个方向:水平,垂直,+45度和-45度,分别为d1,d2,d3,d4。对于任意点(x,y)为中心的3×3区域,我们将非极大值抑制方案表述如下:
1. 寻找最接近α(x,y)的方向dk。
2. 令K表示梯度幅值在(x,y)处的值。若K小于dk方向上的点(x,y)的一个或两个邻点处的幅度值,令g(x,y)=0(抑制);否则,令g(x,y)=K。
对x和y的所有值重复这一过程,产生一幅与梯度幅值图像大小相同的非极大值抑制图像g(x,y)。例如下图的边缘点,方向均是垂直方向,感兴趣的点为p2和p8,如果p5大于等于这两点,则保留该点,不然令该点为0(抑制)。
代码实现:将梯度法线分为4个方向,根据4个方向找相邻感兴趣的两个点的梯度幅值
void NMS(Mat& grad_x, Mat& grad_y,Mat& gradXY, Mat& directImg, Mat& dst) {
dst = gradXY.clone();
for (int i = 1; i < grad_x.rows - 1; i++) {
for (int j = 1; j < grad_x.cols - 1; j++) {
directImg.at<float>(i, j) = atan(directImg.at<float>(i, j));
int P1 = (int)gradXY.at<uchar>(i - 1, j - 1);
int P2 = (int)gradXY.at<uchar>(i - 1, j);
int P3 = (int)gradXY.at<uchar>(i - 1, j + 1);
int P4 = (int)gradXY.at<uchar>(i, j - 1);
int P5 = (int)gradXY.at<uchar>(i, j);
int P6 = (int)gradXY.at<uchar>(i, j + 1);
int P7 = (int)gradXY.at<uchar>(i + 1, j - 1);
int P8 = (int)gradXY.at<uchar>(i + 1, j);
int P9 = (int)gradXY.at<uchar>(i + 1, j + 1);
// 边缘法线水平 —
if (directImg.at<float>(i, j) <= CV_PI / 8 && directImg.at<float>(i, j) >= -CV_PI / 8)
if (P5 < P4 || P5 < P6)
dst.at<uchar>(i, j) = 0;
// 边缘法线45° \
if (directImg.at<float>(i, j) > CV_PI / 8 && directImg.at<float>(i, j) < CV_PI / 8 * 3)
if (P5 < P1 || P5 < P9)
dst.at<uchar>(i, j) = 0;
// 边缘法线垂直 |
if ((directImg.at<float>(i, j) >= CV_PI / 8 * 3 && directImg.at<float>(i, j) <= CV_PI / 2) ||
(directImg.at<float>(i, j) <= -CV_PI / 8 * 3 && directImg.at<float>(i, j) >= -CV_PI / 2))
if (P5 < P2 || P5 < P8)
dst.at<uchar>(i, j) = 0;
// 边缘法线-45° /
if (directImg.at<float>(i, j) < -CV_PI / 8 && directImg.at<float>(i, j) > -CV_PI / 8 * 3)
if (P5 < P3 || P5 < P7)
dst.at<uchar>(i, j) = 0;
}
}
}
通常为了更加精确的计算,在跨梯度方向的两个相邻像素之间使用线性插值来得到要比较的像素梯度。在讨论时分为下面4种情况:
代码实现:线性插值计算感兴趣两点的梯度幅值
void NMS(Mat& grad_x, Mat& grad_y, Mat& gradXY, Mat& dst) {
dst = gradXY.clone();
for (int i = 1; i < grad_x.rows - 1; i++) {
for (int j = 1; j < grad_x.cols - 1; j++) {
float gx = grad_x.at<float>(i, j);
float gy = grad_y.at<float>(i, j);
int g1, g2, g3, g4; float w;
if (abs(gy) > abs(gx)){
g2 = gradXY.at<uchar>(i - 1, j);
g3 = gradXY.at<uchar>(i + 1, j);
w = abs(gx / gy);
if (gx * gy > 0) {
g1= gradXY.at<uchar>(i - 1, j - 1);
g4 = gradXY.at<uchar>(i + 1, j + 1);
}
else {
g1 = gradXY.at<uchar>(i - 1, j + 1);
g4 = gradXY.at<uchar>(i + 1, j - 1);
}
}
else {
g2 = gradXY.at<uchar>(i, j - 1);
g3 = gradXY.at<uchar>(i, j + 1);
w = abs(gy / gx);
if (gx * gy > 0) {
g1 = gradXY.at<uchar>(i - 1, j - 1);
g4 = gradXY.at<uchar>(i + 1, j + 1);
}
else {
g1 = gradXY.at<uchar>(i + 1, j - 1);
g4 = gradXY.at<uchar>(i - 1, j + 1);
}
} float grad1 = g1 * w + g2 * (1 - w);
float grad2 = g4 * w + g3 * (1 - w);
if (gradXY.at<uchar>(i, j) < grad1 || gradXY.at<uchar>(i, j) < grad2)
dst.at<uchar>(i, j) = 0;
}
}
}
4. 使用双阈值处理和连通性分析来检测和连接边缘
双阈值处理是指设置两个阀值,分别为TL和TH。其中大于TH的都被检测为边缘,而低于TL的都被检测为非边缘。对于中间的像素点,如果与确定为边缘的像素点邻接,则判定为边缘;否则为非边缘。具体地说,高阈值得到的图像gH为强边缘,低阈值得到图像gL的弱边缘。强边缘均被认为是有效边缘,并被立即标记。较长的边缘容易被高阈值切断,可利用下面的步骤连接较长的边缘:
(a)在gH中定位下一个未被访问的边缘像素p;
(b)将gL(x,y)中中用8连通连接到p的所有若像素标记为有效边缘像素
(c)如果gH中的所有非零像素已被访问,则跳到步骤(d),否则返回步骤(a)
(d)将gL中未标记为有效边缘像素的所有像素设置为零
在这一过程的末尾,将来自gL(x,y)的所有非零像素附加到gH(x,y),形成canny算子输出的最终图像。
代码实现:利用了膨胀重建完成上面的过程
以gH为标记图,gL为模板,进行膨胀重建,若想了解形态学重建,移步链接
//膨胀重建
void dilateRestruct(Mat& mark, Mat& mould, Size dilate_size, Mat& dst) {
Mat dilateImg_pre = mark.clone();
Mat temp = mark.clone();
Mat cmp = Mat::zeros(mould.size(), CV_8UC1);
Mat element = getStructuringElement(MORPH_RECT, dilate_size);
int n = -1;
while (n != mould.cols * mould.rows) {
morphologyEx(dilateImg_pre, temp, MORPH_DILATE, element);
bitwise_and(temp, mould, temp);
compare(temp, dilateImg_pre, cmp, 0);
n = countNonZero(cmp);
dilateImg_pre = temp.clone();
}
dst = temp;
}
//双阈值
void double_threshold(Mat& src, int T_low, int T_high, Mat& dst) {
Mat g_h, g_l, g;
threshold(src, g_h, 65, 255, THRESH_BINARY);
threshold(src, g_l, 25, 255, THRESH_BINARY);
dilateRestruct(g_h, g_l, Size(3, 3), dst);
}
尽管非极大值抑制后的边缘要比原始梯度边缘细,但仍要粗于1像素。为得到1像素粗的边缘,通常要在步骤4后执行一次边缘细化算法(细化见链接)。
完整代码见链接
参考:
1. 冈萨雷斯《数字图像处理(第四版)》Chapter 10(所有图片可在链接中下载)
Canny边缘检测实现(Opencv C++)的更多相关文章
- OpenCV图像Canny边缘检测
Canny边缘检测 图像的边缘检测的原理是检测出图像中所有灰度值变化较大的点,而且这些点连接起来就构成了若干线条,这些线条就可以称为图像的边缘函数原型: void cvCanny( ...
- OpenCV: Canny边缘检测算法原理及其VC实现详解(转载)
原文地址:http://blog.csdn.net/likezhaobin/article/details/6892176 原文地址:http://blog.csdn.net/likezhaobin/ ...
- openCV(四)---Canny边缘检测
图像的边缘检测的原理是检测出图像中所有灰度值变化较大的点,而且这些点连接起来就构成了若干线条,这些线条就可以称为图像的边缘. 直接上代码,函数简介都在代码注释中 //canny边缘检测 -(void) ...
- 基于opencv下对视频的灰度变换,高斯滤波,canny边缘检测处理,同窗体显示并保存
如题:使用opencv打开摄像头或视频文件,实时显示原始视频,将视频每一帧依次做灰度转换.高斯滤波.canny边缘检测处理(原始视频和这3个中间步骤处理结果分别在一个窗口显示),最后将边缘检测结果保存 ...
- Python+OpenCV图像处理(十三)—— Canny边缘检测
简介: 1.Canny边缘检测算子是John F. Canny于 1986 年开发出来的一个多级边缘检测算法. 2.Canny 的目标是找到一个最优的边缘检测算法,最优边缘检测的含义是: 好的检测- ...
- OpenCV——边缘检测入门、Canny边缘检测
边缘检测的一般步骤: 最优边缘检测的三个评价标准: 低错误率:表示出尽可能多的实际边缘,同时尽可能地减少噪声产生的误报: 高定位性:标识出的边缘要与图像实际边缘尽可能接近: 最小响应:图像中的边缘只能 ...
- OpenCV学习代码记录——canny边缘检测
很久之前学习过一段时间的OpenCV,当时没有做什么笔记,但是代码都还在,这里把它贴出来做个记录. 代码放在码云上,地址在这里https://gitee.com/solym/OpenCVTest/tr ...
- openCV实例:Canny边缘检测
http://blog.sina.com.cn/s/blog_737adf530100z0jk.html 在第一次使用openCV程序成功对图像进行打开后,现在开始试验第二个例程试验:Canny边缘检 ...
- OpenCV学习笔记(11)——Canny边缘检测
了解Canny边缘检测的概念 1.原理 Canny边缘检测是一种非常流行的边缘检测算法,是 John F.Canny在1986年提出的.它是一个有很多步构成的算法 1)噪声去除 使用5*5的高斯滤波器 ...
- Canny边缘检测算法(基于OpenCV的Java实现)
目录 Canny边缘检测算法(基于OpenCV的Java实现) 绪论 Canny边缘检测算法的发展历史 Canny边缘检测算法的处理流程 用高斯滤波器平滑图像 彩色RGB图像转换为灰度图像 一维,二维 ...
随机推荐
- 力扣178(MySQL)-分数排名(中等)
题目: 表: Scores 编写 SQL 查询对分数进行排序.排名按以下规则计算: 分数应按从高到低排列.如果两个分数相等,那么两个分数的排名应该相同.在排名相同的分数后,排名数应该是下一个连续的整数 ...
- MyBatis源码之前言—JDBC编码存在的问题和Mybatis的介绍
MyBatis源码之前言-JDBC编码存在的问题和Mybatis的介绍 为了方便操作,我们在sjdwz_test数据库下建立一张表: CREATE TABLE `t_student` ( `id` b ...
- MaxCompute Tunnel 技术原理及开发实战
简介: MaxCompute(原名ODPS)是一种快速.完全托管的EB级数据仓库解决方案, 致力于批量结构化数据的存储和计算,为用户提供数据仓库的解决方案及分析建模服务.Tunnel是MaxCompu ...
- 基于Delta lake、Hudi格式的湖仓一体方案
简介: Delta Lake 和 Hudi 是流行的开放格式的存储层,为数据湖同时提供流式和批处理的操作,这允许我们在数据湖上直接运行 BI 等应用,让数据分析师可以即时查询新的实时数据,从而对您的 ...
- Flink 在爱奇艺广告业务的实践
简介: 5 月 22 日北京站 Flink Meetup 分享的议题. 本文整理自爱奇艺技术经理韩红根在 5 月 22 日北京站 Flink Meetup 分享的议题<Flink 在爱奇艺广告业 ...
- MAUI 已知问题 PathFigureCollectionConverter 非线程安全
在 MAUI 里,可以使用 PathFigureCollectionConverter 将 Path 字符串转换为 PathFigureCollection 对象,从而实现从 Path 字符串转换为路 ...
- dotnet 读 WPF 源代码笔记 插入触摸设备的初始化获取设备信息
在 WPF 触摸应用中,插入触摸设备,即可在应用里面使用上插入的触摸设备.在 WPF 使用触摸设备的触摸时,需要获取到触摸设备的信息,才能实现触摸 获取触摸设备插入 在 WPF 中,通过 Window ...
- 2018-8-10-WPF-省市县3级联动
title author date CreateTime categories WPF 省市县3级联动 lindexi 2018-08-10 19:16:53 +0800 2018-2-13 17:2 ...
- CMDB开发(二)
一.项目架构:目录规范 # 遵循软件开发架构目录规范 bin 启动文件 src 源文件(核心代码) config 配置文件 lib 公共方法 tests 测试文件 二.采集规范 # bin目录下新建s ...
- 服务端向客户端发送消息Server-Sent Events
今天听说了服务端向客户端发消息的一种方式:Server-Sent Events SSE使用的是HTTP协议,本质上是服务端向客户端发送流式数据. HTTP不支持服务端向客户端发送请求,但是如果客户端向 ...