使用 Python 从头开始​​编写 Canny 边缘检测算法。

news2024/12/23 10:06:35

原始图像(左)和检测到的边缘(右)| 图片由作者提供

一、说明

        在本文中,我将解释有关 Canny 边缘检测的所有内容,以及在不使用一些预先编写的库的情况下对算法进行编码,以便您能够了解真正发生的情况。

二、关于Canny算子和边缘检测

Canny 是一种多级边缘检测算法,可以检测图像中的各种边缘。

但是,等等......为什么我们需要检测图像中的边缘?

        作为人类(我假设你是人类),我们的大脑可以毫无问题地检测任何图像中的边缘,但为了在计算机上自动执行任务,我们必须使用可以完成该任务的程序。

        一些必须检测给定数据中的边缘的实际应用程序的示例,

  • 医学影像
  • 指纹识别
  • 在自动驾驶汽车中
  • 卫星成像
  • ETC,…

在检测边缘时,canny 是唯一的选择吗?

        不是的,方法有很多,分别是

  • 索贝尔边缘检测
  • 普威特边缘检测
  • 拉普拉斯边缘检测
  • ETC,…

        尽管有多种不同的方法,但Canny 边缘检测是一种广泛使用的图像边缘检测技术。

        该算法由John F. Canny 于 1986 年开发,现已成为图像处理的标准技术。

        正如我们之前所说的 Canny 是一种多阶段算法,这意味着它是一种包含许多其他算法的算法,我们讨论的 Sobel 边缘检测就是 Canny 中的一种这样的算法。

三、Canny边缘检测的步骤

  1. 灰度转换
  2. 降噪
  3. 梯度计算
  4. 非极大值抑制
  5. 双阈值和迟滞

3.1. 灰度转换

RGB 彩色图像(左)和灰度图像(右)| 图片由作者提供

要将 HSV、YUV 或 RGB 色阶转换为灰度,我们会这样:

“呵呵,这么简单,取每个通道的平均值就可以了。”

理论上,该公式是100%正确的,但平均法并不能按预期工作。

原因是人脑对 RGB 的反应不同。眼睛对绿光最敏感,对红光不太敏感,对蓝光最不敏感。因此,当我们生成灰度图像时,三种颜色应该具有不同的权重。

这给我们带来了另一种方法,称为加权方法,也 称为光度方法。

实现起来相当简单,

权重是根据它们的波长计算的,因此改进后的公式如下所示,

Grayscale = 0.2989 * R + 0.5870 * G + 0.1140 * B

但我认为在我们的上下文(边缘检测)中,我们也可以使用平均方法,尽管我遵循惯例。

import numpy as np


def to_gray(img: np.ndarray, format: str):
    '''
    Algorithm:
    >>> 0.2989 * R + 0.5870 * G + 0.1140 * B 
    - Returns a gray image
    '''

    r_coef = 0.2989
    g_coef = 0.5870
    b_coef = 0.1140

    if format.lower() == 'bgr':
        b, g, r = img[..., 0], img[..., 1], img[..., 2]
        return r_coef * r + g_coef * g + b_coef * b
    elif format.lower() == 'rgb':
        r, g, b = img[..., 0], img[..., 1], img[..., 2]
        return r_coef * r + g_coef * g + b_coef * b
    else:
        raise Exception('Unsupported value in parameter \'format\'')

3.2 降噪

        图片进行模糊化,为什么要这样做?最根本目的是,营造一个图片信息连续化处理,即将台阶函数变成逐步趋近,避免对微积分运算(尤其高阶运算)造成中断问题。此处理可以极大防止不可微点的存在。

灰度图像(左)和高斯模糊图像(右)| 图片由作者提供

模糊有助于平滑图像并减少像素强度中小的随机变化的影响。

有很多用于模糊的内核,

  • 高斯滤波器
  • 箱式过滤器
  • 均值滤波器
  • 中值滤波器

        这里我们将使用高斯滤波器进行模糊,其实我已经写了一篇完整的文章了,所以我不会详细解释它。

编码:在 Python 中从头开始进行高斯模糊操作。

编码底层概念而不调用一些自定义函数,就这样,您将接触到什么......

        该过程是将图像与高斯核矩阵进行卷积运算,得到对应给定图像的模糊图像。

3.3.梯度计算

因此,灰度转换和模糊是预处理阶段。

在这一步中,我们将使用 Sobel 滤波器,因此这一步实际上是 Sobel 边缘检测方法。

在解释细节之前,我将一次性告诉您完整的故事。

该过程使用Sobel 滤波器,它是导数的离散近似,我们将对上面生成的图像(模糊)执行卷积运算,

因此,我们将得到两个X 和 Y 梯度,梯度是指向图像强度变化率最大方向的向量,

使用这两个梯度,我们生成一个总梯度,如果我们绘制该梯度,我们会得到一个由图像边缘组成的图像,

此外,我们还将把X 和 Y 梯度之间的角度 (theta)存储在变量中以供进一步使用。

Sobel X 和 Y 滤波器 | 图片由作者提供

在开始讨论之前,我想问你什么是边缘,我们如何将图像的特定部分称为边缘?

我们可以说边缘是颜色突然变化的部分。

因此,在阅读其余部分时请记住这一点。

如果我们使用 Sobel 滤波器 X 对图像进行卷积运算,我们将得到另一个矩阵,该矩阵显示 x 方向上的颜色变化。

此外,在图像上对 Sobel 滤波器 Y 进行卷积会产生另一个矩阵,该矩阵显示 y 方向上的颜色变化。

但是,这到底是怎么发生的呢?
其实很简单,看下面的图就知道了。

索贝尔滤波器如何生成 X 和 Y 梯度的图示 | 图片由作者提供

现在,如果我们在真实图像上执行此操作,我们会得到如下所示的结果,

Y&X 的变化 | 图片由作者提供

为了找到总变化,我们利用毕达哥拉斯定理。

Base为 X 梯度,Altitude为 Y 梯度,因此斜边将导致总变化。

斜边的 eqn | 图片由作者提供

θ 的计算公式如下:

eqn 计算 theta | 图片由作者提供

可以使用 numpy 在 python 中计算,如下所示。

theta = np.arctan(Gradient_Y / (Gradient_X + np.finfo(float).eps))
# np.finfo(float).eps is added to tackle the division by zero error

但是,如果您看下图,您会发现,即使我们以这种方式获得了正确的角度,方向性也可能会丢失。

反正切方向性问题的示例 | 图片由作者提供

所以你找到的角度不会在整个圆的范围内。

这样,如果我们计算 theta 就会像下面这样:

使用 atan 检测到的角度矩阵的图片表示 | 图片由作者提供

代替,

使用 atan2 检测到的角度矩阵的图片表示 | 图片由作者提供

为了解决这个问题,数学家在计算时添加了一些条件,因此新方法称为atan2。

根据维基百科,条件如下:

从atan 转换为atan2 的条件| 图片由作者提供

因此,在 atan2 中,我们没有给出 y 和 x 之间的比率,而是给出两者,以便算法可以对其进行调整。

在使用 numpy 的 python 中,您可以按如下方式使用 atan2,

theta = np.arctan2(Gradient_Y, Gradient_X)

最后,您了解了所有细节,并找到了边缘。

这是计算梯度和 theta 的代码(theta 将在接下来的阶段中使用)。

G = np.sqrt((Gradient_X ** 2.0)+(Gradient_Y ** 2.0))
''' or simply do the following '''
G = np.hypot(Gradient_X, Gradient_Y)

G = G / G.max() * 255 # the total gradient; ie, the edge detection result
theta = np.arctan2(Gradient_Y, Gradient_X)

sobel边缘检测结果| 图片由作者提供

现在我要问你一个问题,我们结束了吗?

实际上我们不能这么说,因为根据您计划如何处理结果,结果可能会有所不同。

我们目前遇到的问题是,

  1. Sobel 边缘检测算法还可以找到边缘的厚度。
  2. 它不是二值图像,而是灰度图像。
  3. 而且它还有很多我们可以减少的噪音。

为了解决这些问题,我们将使用非极大值算法抑制厚度,并进行阈值处理以使其更好

3.4.非极大值抑制

非极大值抑制结果| 图片由作者提供

在这个阶段,我们将利用theta

其过程是,循环所有像素点,取当前像素点两个相邻像素点进行比较,判断当前像素点的强度是否大于这两个相邻像素点的强度,如果是则继续,如果不是则设置当前像素强度为 0

这里我们要把重点放在取相邻像素上,我们不能只取当前像素周围的一些随机像素,而是必须根据角度(theta),

这个想法是取几乎垂直于主像素角度方向的像素。

例如,如果 theta 介于 22.5° 和 67.5° 之间,则采用如下所示的像素,其中(i, j) 是当前像素。

用作 (i,j) 像素的相邻像素的像素的图示 | 图片由作者提供

下面是该过程的另一个说明,箭头表示为角度,我们的目标是仅检测灰色阴影像素。

| 作者图片 |

下面是非极大值抑制的代码,

'''Non Max Suppression'''

M, N = G.shape
Z = np.zeros((M,N), dtype=np.int32) # resultant image
angle = theta * 180. / np.pi        # max -> 180, min -> -180
angle[angle < 0] += 180             # max -> 180, min -> 0

for i in range(1,M-1):
    for j in range(1,N-1):
        q = 255
        r = 255
        
        if (0 <= angle[i,j] < 22.5) or (157.5 <= angle[i,j] <= 180):
            r = G[i, j-1]
            q = G[i, j+1]

        elif (22.5 <= angle[i,j] < 67.5):
            r = G[i-1, j+1]
            q = G[i+1, j-1]

        elif (67.5 <= angle[i,j] < 112.5):
            r = G[i-1, j]
            q = G[i+1, j]

        elif (112.5 <= angle[i,j] < 157.5):
            r = G[i+1, j+1]
            q = G[i-1, j-1]

        if (G[i,j] >= q) and (G[i,j] >= r):
            Z[i,j] = G[i,j]
        else:
            Z[i,j] = 0

        尽管如此,我们只是减少了边缘的宽度,我们必须使边框颜色一致并消除一些噪音。

3.5. 双阈值和迟滞

        双阈值用于识别图像中的强边缘和弱边缘。

        梯度幅度高于高阈值的像素被视为强边缘,因此我们为其分配像素值 255。梯度幅度低于低阈值的像素被视为非边缘,因此它们的像素值为 0。

        梯度幅度在低阈值和高阈值之间的像素被视为弱边缘。这些像素只分配像素值255,如果它们连接到强边缘,否则分配像素值0给它,这个过程称为滞后。

        下面是计算双阈值处理的代码。

def threshold(img, lowThresholdRatio=0.05, highThresholdRatio=0.09):
    '''
    Double threshold
    '''
    
    highThreshold = img.max() * highThresholdRatio;
    lowThreshold = highThreshold * lowThresholdRatio;
    
    M, N = img.shape
    res = np.zeros((M,N), dtype=np.int32)
    
    weak = np.int32(25)
    strong = np.int32(255)
    
    strong_i, strong_j = np.where(img >= highThreshold)
    zeros_i, zeros_j = np.where(img < lowThreshold)
    
    weak_i, weak_j = np.where((img <= highThreshold) & (img >= lowThreshold))
    
    res[strong_i, strong_j] = strong
    res[weak_i, weak_j] = weak
    
    return (res, weak, strong)

双阈值结果 | 图片由作者提供

下面是滞后过程的代码。

def hysteresis(img, weak, strong=255):
    M, N = img.shape

    for i in range(1, M-1):
        for j in range(1, N-1):
            if (img[i, j] == weak):
                if (
                    (img[i+1, j-1] == strong) or (img[i+1, j] == strong) or
                    (img[i+1, j+1] == strong) or (img[i, j-1] == strong) or
                    (img[i, j+1] == strong) or (img[i-1, j-1] == strong) or
                    (img[i-1, j] == strong) or (img[i-1, j+1] == strong)
                ):
                    img[i, j] = strong
                else:
                    img[i, j] = 0
    return img

滞后结果(最终图像)| 图片由作者提供

完整的代码可以在我的 GitHub 存储库中找到,

GitHub — rohit-krish/CVFS:从“从头开始”编码计算机视觉相关算法。

您目前无法执行该操作。您使用另一个选项卡或窗口登录。您在另一个选项卡中退出或...

四、结论

希望现在您对 Canny 边缘检测算法有了清晰的了解。

从头开始编码并不是一个坏习惯,

它实际上可以帮助你更好地理解事物,而不仅仅是解释。

但是,当您处理实际项目时,您不必从头开始编码,那么您可以使用 OpenCV 等库来提供帮助。

在使用 OpenCV 的 Python 中,您可以生成如下高斯模糊图像,

import cv2

img = cv2.imread(<img_path>)

# to grayscale
imgGray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# guassian blur
imgBlur = cv2.GaussianBlur(imgGray, (13, 13), 0)

# canny edge detection
imgCanny = cv2.Canny(imgBlur, 50, 50)

五、参考

  • Canny Edge Detector
  • https://towardsdatascience.com/canny-edge-detection-step-by-step-in-python-computer-vision-b49c3a2d8123
  • Image Processing 101 Chapter 1.3: Color Space Conversion | Dynamsoft Blog

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CSS 浮动

目标target✓ 能够说出来为什么需要浮动能够说出来浮动的排列特性能够说出来三种最常见的布局方式能够说出来为什么需要清除浮动,能够至少写出两种清楚浮动的方法能够利用Photoshop实现基本的切图能够利用Photoshop插件实现切图能够完成学成在线的页面布 传统网页布局的三种模…

uniapp项目启动时检查版本,版本过旧提示:更新至最新版本,App简单版实现思路详解

效果&#xff1a; 实现的思路比较简单&#xff0c;后期需要优化的话&#xff0c;会持续更新 uniapp文档上有提到关于版本更新 在文档中搜uni.getUpdateManager()&#xff08;小程序的更新&#xff09;&#xff0c;会提示app的更新需要点击连接&#xff0c;跳转到其他页面查看…

iOS 16.4 之后真机与模拟器无法使用Safari调试H5页面问题

背景 iOS 16.4之后用真机调试H5时候发现&#xff0c;Safari中开发模块下面无法调试页面 解决方案 在WKWebView中设置以下代码解决 if (available(iOS 16.4, *)) {[_webView setInspectable:YES];}然后再次调试就可以了

Tcl语言:SDC约束命令create_generated_clock详解(上)

相关阅读 Tcl语言https://blog.csdn.net/weixin_45791458/category_12488978.html?spm1001.2014.3001.5482 有时候&#xff0c;复杂的设计需要多个时钟来完成相应的操作&#xff0c;当设计中有多个时钟存在时&#xff0c;它们需要相互协作或各司其职。有几种时钟可能由其他时…

Centos7部署Python3环境

一、安装Python3 ###查看是否安装Python3 Centos 7 默认安装了python 2.7.5. 因为一些命令要用它比如yum 它使用的是python2.7.5 使用 python -V 命令查看一下是否安装Python 然后使用命令 which python 查看一下Python可执行文件的位置 Python指向的是Python2.7 安装依赖 y…

漏刻有时百度地图API实战开发(1)华为手机无法使用addEventListener click 的兼容解决方案

现象 漏刻有时项目开发中的调用了百度地图API&#xff0c;在PC端、IOS和安卓机型测试都没有问题。但是使用华为手机部分型号时&#xff0c;前端在监听点击事件的时候是使用 map.addEventListener(click,function(){...})&#xff0c;无法触发。或 原理 通过监听touchstart和…

CentOS7安装部署StarRocks

文章目录 CentOS7安装部署StarRocks一、前言1.简介2.环境 二、正文1.StarRocks基础1&#xff09;架构图2&#xff09;通讯端口 2.部署服务器3.安装基础环境1&#xff09;安装JDK 112&#xff09;修改机器名3&#xff09;安装GCC4&#xff09;关闭交换分区&#xff08;swap&…

基于级联广义积分器(CGI)的谐波信号提取MATLAB仿真

微❤关注“电气仔推送”获得资料&#xff08;专享优惠&#xff09; 此方法可用于信号检测、虚拟阻抗合成、锁相环等方面。 在现有的信号提取方法中&#xff0c;众多学者采用了SOGI法、LPF法以及正交信号发生器等方法。当输入信号中不存在直流分量&#xff0c;只有谐波分量时&…

C盘清理指南(一) 内存小的本质原因

相信大家看到日益爆满的C盘一定会很头疼&#xff0c;博主也不例外。C盘之于电脑&#xff0c;犹如心脏之于人类一般重要&#xff0c;合理地清理C盘是工作中必不可少的技能。由于全篇篇幅较长&#xff0c;所以这部分会分为好几期哦&#xff0c;大家敬请期待&#xff01; 当我们发…

V90 EPOS模型下位置控制(完整SCL源代码)

V90EPOS模式下点动控制详细应用介绍和控制源代码,请查看下面文章链接: V90伺服EPOS模式点动控制(详细介绍+完整SCL代码)-CSDN博客文章浏览阅读29次。V90伺服驱动器采用西门子标准报文111加FB284(SINA_POS)详细的报文组态和功能块请参考下面文章链接:博途1200/1500PLC V90 P…

使用 Gorm 进行事务和错误处理

在 GORM 中管理事务和错误的全面指南&#xff0c;以确保可靠的数据库操作 在数据库管理的世界中&#xff0c;确保数据完整性至关重要。GORM&#xff0c;强大的 Go 对象关系映射库&#xff0c;为开发人员提供了维护数据一致性和优雅处理错误的必要工具。本文将作为您全面的指南…

告别传统笔记,8款笔记软件让读书笔记更精彩!

阅读&#xff0c;它打开了一个全新的世界给我们。对于那些热爱阅读的人们来说&#xff0c;没有什么比把手中的一本书翻到最后一页更满足的了。为了更好地理解书籍的内容&#xff0c;或是以后能快速查阅书中的重点内容&#xff0c;很多人习惯于边阅读边做读书笔记。 如果你正在…

【源码】医学影像PACS实现三维影像后处理等功能

医学影像诊断技术近年来取得了快速发展&#xff0c;包括高性能的影像检查设备的临床应用和数字信息技术的图像显示、存储、传输、处理、识别&#xff0c;这些技术使得计算机辅助检测和诊断成为可能&#xff0c;同时人工智能影像诊断也进入了人们的视野。这些技术进步提高了疾病…

王道考研--》顺序表课后习题C语言代码实现(冲刺)

考研是许多计算机科学专业学生追求高学历、寻求更好就业前景的途径。在考研过程中&#xff0c;数据结构是一个非常重要的科目&#xff0c;而代码实现题更是其中的难点之一。在这篇文章中&#xff0c;我们将探讨如何通过实现数据结构代码问题来提升考研成绩。无论您是否有编程经…

C#中LINQtoSQL只能在.NetFramework下使用,不能在.net 的默认安装下使用

目录 一、在net7.0下无法实现LINQtoSQL 1.VS上建立数据库连接 2.VS上创建LINQtoSQL 二、在.NetFramework4.8下成功实现LINQtoSQL 1.VS上建立数据库连接 2.VS上创建LINQtoSQL 三、结论 四、理由 本文是个人观点&#xff0c;因为我百般努力在.net7.0下无法实现LINQtoSQL的…

vr地铁消防虚拟逃生自救系统降低财产及人员伤害

无论是在公共场所还是在家中&#xff0c;火灾都是一种常见的突发事件。这往往会严重影响到人们的财产和生命安全。因此&#xff0c;如何预防火灾和安全逃生就成为了非常重要的话题。这款VR模拟火灾疏散逃生系统&#xff0c;帮助人们了解火灾逃生的技巧以及正确的应对方法。 以传…

【仙逆】尸阴宗秘密揭露,王林差点被夺舍,修仙恐怖消息曝光

Hello,小伙伴们&#xff0c;我是小郑继续为大家深度解析国漫资讯。 深度爆料&#xff0c;《仙逆》国漫第九话最新剧情&#xff0c;尸阴宗表面上令人敬畏&#xff0c;但背后却隐藏着不为人知的秘密。这个宗门暗地里为受伤或死亡的强大修真者提供夺舍容器&#xff0c;帮助他们获…

基础课24——开放域QA问答

早期的对话机器人通常采用基于规则的开放问答系统。这种系统依赖于专家系统的语义模板&#xff0c;即根据预先定义的模板来匹配和回答问题。这种方法的优点是准确性相对较高&#xff0c;因为它是基于人类专家的知识和经验来设计的。然而&#xff0c;这种系统的可扩展性和灵活性…

Linux程序的地址空间

Linux程序的地址空间 &#x1f4df;作者主页&#xff1a;慢热的陕西人 &#x1f334;专栏链接&#xff1a;Linux &#x1f4e3;欢迎各位大佬&#x1f44d;点赞&#x1f525;关注&#x1f693;收藏&#xff0c;&#x1f349;留言 本博客主要内容深刻理解了什么程序或者进程的地址…

2000-2021年全国各省市城乡平均受教育年限数据(分城镇和农村)

2000-2021年全国各省市城乡平均受教育年限数据&#xff08;分城镇和农村&#xff09; 1、时间&#xff1a;2000-2021年 2、范围&#xff1a;全国及31省 3、来源&#xff1a;人口与就业统计年鉴 4、指标包括&#xff1a;城乡平均受教育年限 、6岁以上总人口 未上过学、…