Canny 边缘检测算法-python实现(附代码)

文章目录

  • 1、调用opencv进行canny边缘检测
  • 2、图像灰度化
  • 3、高斯模糊处理
  • 4、图像梯度、梯度幅值、梯度方向计算
  • 5、NMS(非极大值抑制)
  • 6、双阈值的边界选取

摘要 : Canny 边缘检测算法由计算机科学家 John F. Canny 于 1986 年提出的。其不仅提供了算法,还带来了一套边缘检测的理论,分阶段的解释如何实现边缘检测。Canny 检测算法包含下面几个阶段:

  • 图像灰度化
  • 高斯模糊处理
  • 图像梯度、梯度幅值、梯度方向计算
  • NMS(非极大值抑制)
  • 双阈值的边界选取

1、调用opencv进行canny边缘检测

如果你只是想应用canny得到图片的边缘的话,那么就没有必要往下阅读canny的具体原理与实现了。因为python的opencv库中提供了很好的功能函数来实现这一功能。我们以一个示例来说明如何使用opencv进行canny边缘检测:

这是原图与使用opencv进行canny边缘检测的结果图:

在这里插入图片描述

这是代码实现:

import cv2 #导入opencv库
 #读取图片
img = cv2.imread("images/2007\_000032.jpg")
#进行canny边缘检测
edge = cv2.Canny(img,50,150)
#保存结果
cv2.imwrite('test.jpg',edge)

这四行代码的关键在于 cv2.Canny 函数。我们对它的参数进行详细的解读。希望能对你有帮助。OpenCV-Python中Canny函数的原型为:

cv2.Canny(image, threshold1, threshold2[, edges[, apertureSize[, L2gradient]]])

必要参数:

  • 第一个参数是需要处理的原图像,该图像必须为单通道的灰度图;
  • 第二个参数是阈值1;
  • 第三个参数是阈值2。

其中较大的阈值2用于检测图像中明显的边缘,但一般情况下检测的效果不会那么完美,边缘检测出来是断断续续的。所以这时候用较小的第一个阈值用于将这些间断的边缘连接起来。

可选参数中apertureSize就是Sobel算子的大小。而L2gradient参数是一个布尔值,如果为真,则使用更精确的L2范数进行计算(即两个方向的倒数的平方和再开放),否则使用L1范数(直接将两个方向导数的绝对值相加)。

到这其实已经可以将canny边缘检测应用到你的项目中了。当然如果你想了解canny边缘检测的原理的话,请继续往下阅读。

2、图像灰度化

对于一张图片,当我们只关心其边界的时候,单通道的图片已经足够提供检测出边界的信息。所以我们可以将R、G、B的3通道图片乃至更高维的高光谱遥感图像进行灰度化。灰度化实际上是一种降维操作,它减少了冗余数据从而降低了计算开销。以下是对RGB图片灰度化的方法:

# 灰度化
def gray(self, img\_path):
	"""
	计算公式:
	Gray(i,j) = [R(i,j) + G(i,j) + B(i,j)] / 3
	or :
	Gray(i,j) = 0.299 \* R(i,j) + 0.587 \* G(i,j) + 0.114 \* B(i,j)
	"""
	
	# 读取图片
	img = plt.imread(img_path)
	# BGR 转换成 RGB 格式
	img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
	# 灰度化
	img_gray = np.dot(img_rgb[...,:3], [0.299, 0.587, 0.114])
	return img_gray

3、高斯模糊处理

高斯模糊实际上是对灰度化后的图像去噪。从数学的角度来看,图像的高斯模糊过程就是图像与正态分布做卷积。进行高斯滤波之前,需要先得到一个高斯滤波器(kernel)。如何得到一个高斯滤波器呢?其实就是将高斯函数离散化,将滤波器中对应的横纵坐标索引代入高斯函数,即可得到对应的值。不同尺寸的滤波器,得到的值也不同,下面是二维高斯函数与 (2k+1)x(2k+1) 滤波器的计算公式 :

高斯滤波常用尺寸为 5x5,σ=1.4 的高斯滤波器。下面是 5x5 高斯滤波器的实现代码:

# 去除噪音 - 使用 5x5 的高斯滤波器
def smooth(self, img\_gray):
	# 生成高斯滤波器
	"""
	要生成一个 (2k+1)x(2k+1) 的高斯滤波器,滤波器的各个元素计算公式如下:
	H[i, j] = (1/(2\*pi\*sigma\*\*2))\*exp(-1/2\*sigma\*\*2((i-k-1)\*\*2 + (j-k-1)\*\*2))
	"""
	sigma1 = sigma2 = 1.4
	gau_sum = 0
	gaussian = np.zeros([5, 5])
	for i in range(5):
		for j in range(5):
			gaussian[i, j] = math.exp((-1/(2*sigma1*sigma2))*(np.square(i-3)+ np.square(j-3)))/(2*math.pi*sigma1*sigma2)
			gau_sum = gau_sum + gaussian[i, j]
	
	# 归一化处理
	gaussian = gaussian / gau_sum
	
	# 高斯滤波
	W, H = img_gray.shape
	new_gray = np.zeros([W-5, H-5])
	
	for i in range(W-5):
		for j in range(H-5):
			new_gray[i, j] = np.sum(img_gray[i:i+5, j:j+5] * gaussian)
	
	return new_gray

4、图像梯度、梯度幅值、梯度方向计算

这个步骤的重要性不言而喻。直观感受上来讲我们知道一个图像上处于边界附近位置的像素值变化较大。而处于物体内部位置的像素值大多相近。这样我们可以计算当前像素与其附近像素的像素值的差值判断该像素处于物体内部还是边界。这个差值我们称为图像梯度。梯度幅值、梯度方向由图像梯度计算而来。
具体而言,我们用一阶导数来计算梯度:

在这里插入图片描述

对于上式,实际操作时就是用当前像素的下一个像素减去当前像素。此时 Δ x = 1 \Delta x=1 Δx=1;

梯度包含x方向的梯度与y方向的梯度。它们是两个向量。梯度幅值是这两个向量的向量和:
在这里插入图片描述

既然梯度幅值是一个向量,那么我们需要计算它的方向:

[

我们用如下代码实现:

# 计算梯度幅值
def gradients(self, new_gray):
	"""
	:type: image which after smooth
	:rtype:
		dx: gradient in the x direction
		dy: gradient in the y direction
		M: gradient magnitude
		theta: gradient direction
	"""
	W, H = new_gray.shape
	dx = np.zeros([W-1, H-1])
	dy = np.zeros([W-1, H-1])
	M = np.zeros([W-1, H-1])
	theta = np.zeros([W-1, H-1])
	
	for i in range(W-1):
		for j in range(H-1):
		dx[i, j] = new\_gray[i+1, j] - new\_gray[i, j]
		dy[i, j] = new\_gray[i, j+1] - new\_gray[i, j]
		# 图像梯度幅值作为图像强度值
		M[i, j] = np.sqrt(np.square(dx[i, j]) + np.square(dy[i, j]))
		# 计算 θ - artan(dx/dy)
		theta[i, j] = math.atan(dx[i, j] / (dy[i, j] + 0.000000001))
	return dx, dy, M, theta

在计算得到的梯度幅值中我们实际上已经得到了图像的边界(即函数返回值中的M)。如下:
在这里插入图片描述

但是,很容易发现这个边缘存在两个问题:

  • 边缘较粗;
  • 很多边缘断断续续。

针对这两个问题便有了以下两个步骤NMS、双阈值边界选取。

5、NMS(非极大值抑制)

理想情况下,最终得到的边缘应该是很细的。因此,需要执行非极大值抑制以使边缘变细。原理很简单:遍历梯度矩阵上的所有点,并保留边缘方向上具有极大值的像素。就像下面这幅图一样。图中黑色和灰色表示边界。我们通过NMS找出其中的局部最大值(也就是图中的黑色)而把其他位置(也就是图中的灰色)的值取0。

在这里插入图片描述

下面说说 NMS 的细节内容。NMS在八个领域:上,下,左,右,左上,左下,右上,右下上进行(当然,比较的时候不需要将该点与其它八个点比较。只需要将其与其梯度方向上的点比较即可。这个很好理解。因为我们只需要当前值在它所属的边缘上是局部最大值即可,而不需要它在其它边缘上也是局部最大)如下图所示,C 周围的 8 个点就是其附近的八个领域。

在这里插入图片描述

NMS 是要找出局部最大值,因此,需要将当前的像素的梯度,与其他方向进行比较。如下图所示,g1,g2,g3,g4 分别是 C 八个领域中的 4 个点,蓝线是 C 的梯度方向。如果 C 是局部最大值的话,C 点的梯度幅值就要大于梯度方向直线与 g1g2,g4g3 两个交点的梯度幅值,即大于点 dTemp1 和 dTemp2 的梯度幅值。上面提到这种方法无法达到最好的效果,因为 dTemp1 和 dTemp2 不是整像素,而是亚像素。亚像素的意思就是在两个物理像素之间还有像素。那么,亚像素的梯度幅值怎么求?可以使用线性插值的方法,计算 dTemp1 在 g1,g2 之间的权重,就可以得到其梯度幅值。计算公式如下:

weight = |gx| / |gy| or |gy| / |gx|
dTemp1 = weight*g1 + (1-weight)*g2
dTemp2 = weight*g3 + (1-weight)*g4

计算时分两种情况(都是比较当前像素与dtemp1与dtemp2的大小,大于这两个值则保留,小于其中任意一个则将其值取0):

  • 下面两幅图是 y 方向梯度值比较大的情况,即梯度方向靠近 y 轴。所以,g2 和 g4 在 C 的上下位置,此时 weight = |gy| / |gx| 。左边的图是 x,y 方向梯度符号相同的情况,右边是 x,y 方向梯度符号相反的情况。
    在这里插入图片描述

  • 下面两幅图是 x 方向梯度值比较大的情况,即梯度方向靠近 x 轴。所以,g2 和 g4 在 C 的左右位置,此时 weight = |gy| / |gx| 。左边的图是 x,y 方向梯度符号相同的情况,右边是 x,y 方向梯度符号相反的情况。

代码实现如下:

def NMS(self, M, dx, dy):
	d = np.copy(M)
	W, H = M.shape
	NMS = np.copy(d)
	NMS[0, :] = NMS[W-1, :] = NMS[:, 0] = NMS[:, H-1] = 0
	for i in range(1, W-1):
		for j in range(1, H-1):
			# 如果当前梯度为0,该点就不是边缘点
			if M[i, j] == 0:
				NMS[i, j] = 0
			else:
				gradX = dx[i, j] # 当前点 x 方向导数
				gradY = dy[i, j] # 当前点 y 方向导数
				gradTemp = d[i, j] # 当前梯度点
				
				# 如果 y 方向梯度值比较大,说明导数方向趋向于 y 分量
				if np.abs(gradY) > np.abs(gradX):
					weight = np.abs(gradX) / np.abs(gradY) # 权重
					grad2 = d[i-1, j]
					grad4 = d[i+1, j]

					# 如果 x, y 方向导数符号一致
					# 像素点位置关系
					# g1  g2
					#     c
					#     g4  g3

					if gradX * gradY > 0:
						grad1 = d[i-1, j-1]
						grad3 = d[i+1, j+1]

					# 如果 x,y 方向导数符号相反
					# 像素点位置关系
					#     g2  g1
					#     c
					# g3  g4
					
					else:
						grad1 = d[i-1, j+1]
						grad3 = d[i+1, j-1]

				# 如果 x 方向梯度值比较大
				else:
					weight = np.abs(gradY) / np.abs(gradX)
					grad2 = d[i, j-1]
					grad4 = d[i, j+1]
					
					# 如果 x, y 方向导数符号一致
					# 像素点位置关系
					#      g3
					# g2 c g4
					# g1
					if gradX * gradY > 0:
						grad1 = d[i+1, j-1]
						grad3 = d[i-1, j+1]
					
					# 如果 x,y 方向导数符号相反
					# 像素点位置关系
					# g1
					# g2 c g4
					#      g3
					else:
						grad1 = d[i-1, j-1]
						grad3 = d[i+1, j+1]

				# 利用 grad1-grad4 对梯度进行插值
				gradTemp1 = weight \* grad1 + (1 - weight) \* grad2
				gradTemp2 = weight \* grad3 + (1 - weight) \* grad4
				
				# 当前像素的梯度是局部的最大值,可能是边缘点
				if gradTemp >= gradTemp1 and gradTemp >= gradTemp2:
					NMS[i, j] = gradTemp
				else:
					# 不可能是边缘点
					NMS[i, j] = 0

	return NMS

6、双阈值的边界选取

这个阶段决定哪些边缘是真正的边缘,哪些边缘不是真正的边缘。为此,需要设置两个阈值,minVal 和 maxVal。梯度大于 maxVal 的任何边缘肯定是真边缘,而 minVal 以下的边缘肯定是非边缘,因此被丢弃。位于这两个阈值之间的边缘会基于其连通性而分类为边缘或非边缘,如果它们连接到"可靠边缘"像素,则它们被视为边缘的一部分。否则,也会被丢弃。代码如下所示:

def double\_threshold(self, NMS):
	W, H = NMS.shape
	DT = np.zeros([W, H])
	
	# 定义高低阈值
	TL = 0.1 \* np.max(NMS)
	TH = 0.3 \* np.max(NMS)
	
	for i in range(1, W-1):
		for j in range(1, H-1):
			# 双阈值选取
			if (NMS[i, j] < TL):
				DT[i, j] = 0
			elif (NMS[i, j] > TH):
				DT[i, j] = 1
			# 连接
			elif (NMS[i-1, j-1:j+1] < TH).any() or (NMS[i+1, j-1:j+1].any() or (NMS[i, [j-1, j+1]] < TH).any()):
				DT[i, j] = 1	
	return DT

进行完所有的步骤后,结果如下图所示:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/4648.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Python】更精美的俄罗斯方块开发指南(步骤已写)

文章目录前言一、游戏介绍二、源码剖析1.引入库2.读入数据总结前言 最近想找一些Python相关的游戏开发例子&#xff0c;正好在itch.io上闲逛看到这个俄罗斯方块项目&#xff0c;瞬间被惊艳到了。作者是 Mikhail &#xff0c;项目(tetris_for_two)地址是&#xff1a; https://g…

数据库原理及应用(四)——SQL语句(2)SQL基础查询以及常见运算符

一、SELECT语句基础 数据库查询是数据库的核心操作&#xff0c;SELECT 语句用于从数据库中选取数据。 SELECT [ALL/DISTINCT] <列名>,<列名>...FROM <表名或视图名>,<表名或视图名>[WHERE <条件表达式>][GROUP BY <列名1> [HAVING <条…

Jetpack compose:炫酷的按钮点击效果

Jetpack compose&#xff1a;炫酷的按钮点击效果 屏幕的每个组件在与用户交互时都有其给用户反馈的方式。例如&#xff0c;当用户触摸 Toggle 按钮时&#xff0c;它会更改其状态以响应交互。这种交互给用户一种感觉。 在此博客中&#xff0c;我们将实现一些自定义点击效果&…

Air700E开发板|移芯EC618|4G Cat.1模组:概述及PinOut

文章目录基础资料概述主要功能外设分布PinOut&#xff08;管脚定义&#xff09;管脚功能说明固件升级正常开机模式&#xff1a;下载模式&#xff1a;IO 电平选择基础资料 Air700E文档中心 概述 EVB-Air700E 开发板是合宙通信推出的基于 Air700E 模组所开发的&#xff0c;包含…

AI遮天传 NLP-词表示

本文重点在第三部分“词嵌入”及对Word2vec的介绍&#xff0c;前面的知识主要用于小白对词表示和一些定义、名称的理解&#xff0c;和对一些方法不足的思考。一、词表示1.1 词表示的定义词表示是一种将自然语言中的词转换为机器可理解含义的过程其中意思&#xff08;meaning&am…

Docker之安装Docker

安装Docker1. Docker 基本组成2. 安装Docker3. 阿里云镜像加速4. 底层原理1. Docker 基本组成 镜像&#xff08;image&#xff09; docker 镜像就好比是一个模板&#xff0c;可以同通过这个模板来创建容器服务&#xff0c;如&#xff1a;tomcat 镜像 > run > tomcat1 容器…

网络技术这十个术语你知道吗?

你好&#xff0c;这里是网络技术联盟站。 网络技术术语是指在计算机网络领域中所使用的专业术语。由于计算机技术的快速发展&#xff0c;网络技术术语也在不断地更新和发展。本文将对网络技术术语进行一些介绍和解释&#xff0c;以帮助读者更好地了解和掌握网络技术。 一、协…

内网Nexus代理yum、epel 源私有仓库 + 内网设备配置(centos)

一、准备 一些常用的镜像开源站 yum: 阿里开源镜像源&#xff1a;http://mirrors.aliyun.com/centos 网易开源镜像站&#xff1a;http://mirrors.163.com/ Centos社区镜像站&#xff1a;http://mirror.centos.org/centos/ 中科大开源镜像站&#xff1a;http://centos.ustc.edu…

Web3中文|World ID与GPT-4同时上线,OpenAI创始人的另一场探险?

这两天GPT-4的到来&#xff0c;再次成为朋友圈及媒体热议的话题。此次升级、更新让大家看到了AI给科技和社会带来的潜在挑战性甚至革命性。在赞叹AI迅速发展的同时&#xff0c;也让人再次聚焦关注其背后拥有超强开发和创新能力的OpenAI团队。 令人刮目相看的是&#xff0c;由A…

JMeter使用教程

作为一名开发工程师&#xff0c;当我们接到需求的时候&#xff0c;一般就是分析需要&#xff0c;确定思路&#xff0c;编码&#xff0c;自测&#xff0c;然后就可以让测试人员去测试了。在自测这一步&#xff0c;作为开发人员&#xff0c;很多时候就是测一下业务流程是否正确&a…

【JAVA程序设计】(C00125)基于Springboot的人事管理系统

基于Springboot的人事管理系统项目简介项目获取开发环境项目技术运行截图项目简介 基于Springboot框架开发的人事管理系统共分为四个角色&#xff1a;系统管理员、财务专员、人事专员、普通用户 管理员角色包含以下功能&#xff1a; 管理员拥有所有功能&#xff1a;绩效考核&a…

华为OD机试题,用 Java 解【叠放书籍】问题 | 含解题说明

华为Od必看系列 华为OD机试 全流程解析+经验分享,题型分享,防作弊指南华为od机试,独家整理 已参加机试人员的实战技巧华为od 2023 | 什么是华为od,od 薪资待遇,od机试题清单华为OD机试真题大全,用 Python 解华为机试题 | 机试宝典本篇题目:叠放书籍 题目 书籍的长宽都是…

RabbitMQ中死信队列和延迟队列

目录一、死信队列1.过期时间代码实现2.长度限制代码实现3.测试消息拒收4.死信队列小结二、延迟队列1.代码实现1.1 生产者1.2 生产者一、死信队列 死信队列&#xff0c;英文缩写&#xff1a;DLX 。Dead Letter Exchange&#xff08;死信交换机&#xff09;&#xff0c;当消息成…

Makefile第三课:C语言的编译

目录C语言的编译前言1.Fundamental Compiling2.Comiling C2.1 Preprocessing2.2 Generating Assembly Language2.3 Source File to Object File2.4 Single Source to Executable2.5 Multiple Sources to Executable3.Creating a Static Library4.Creating a Shared Library5.总…

贯穿设计模式第二话--开闭职责原则

&#x1f973;&#x1f973;&#x1f973; 茫茫人海千千万万&#xff0c;感谢这一刻你看到了我的文章&#xff0c;感谢观赏&#xff0c;大家好呀&#xff0c;我是最爱吃鱼罐头&#xff0c;大家可以叫鱼罐头呦~&#x1f973;&#x1f973;&#x1f973; 从今天开始&#xff0c;将…

java基础学习——字符流

1.为什么会出现字符流&#xff1a; 由于字节流 操作中文不是特别的方便&#xff0c;所以java就提供字符流 字符流字节流编码表 用字节流复制文本文件时&#xff0c;文本文件也会有中文&#xff0c;但是没有问题&#xff0c;原因是最终底层操作会自动进行字节拼接成中文&#xf…

摄影入门 | 相机的基本原理

一、获取图像——小孔成像实验 小孔成像实验中&#xff0c;点燃蜡烛&#xff0c;会在小孔另一面的白纸上看到一个倒立的烛焰。 此现象可以用来解释物理学原理&#xff1a;光在同种均匀介质中&#xff0c;在不受引力作用干扰的情况下沿直线传播。 这样&#xff0c;我们就用一种…

从零开始搭建游戏服务器 第一节 创建一个简单的服务器架构

目录引言技术选型正文创建基础架构IDEA创建项目添加Netty监听端口编写客户端进行测试总结引言 由于现在java web太卷了&#xff0c;所以各位同行可以考虑换一个赛道&#xff0c;做游戏还是很开心的。 本篇教程给新人用于学习游戏服务器的基本知识&#xff0c;给新人们一些学习…

伪静态技术

网址和纯静态一样&#xff0c;只是&#xff0c;其内部仍旧需要查询数据库&#xff1a;如果有一个页面&#xff0c;希望这个页面利于seo,但是有不适合使用真静态&#xff0c;比如csdn论坛帖子&#xff0c;可以考虑使用伪静态&#xff0c;即: 形式上是一个静态地址&#xff0c;比…

Chapter7.2:MATLAB在频率法中的应用及频率法稳定性分析

该系列博客主要讲述Matlab软件在自动控制方面的应用&#xff0c;如无自动控制理论基础&#xff0c;请先学习自动控制系列博文&#xff0c;该系列博客不再详细讲解自动控制理论知识。 自动控制理论基础相关链接&#xff1a;https://blog.csdn.net/qq_39032096/category_10287468…
最新文章