Python利用Canny算法检测硬币边缘

目录
  • 一、问题背景
  • 二、Canny 算法
    • (一)、高斯平滑
    • (二)Sobel算子计算梯度
    • (三)非极大化抑制
    • (四)滞后边缘跟踪

一、问题背景

纸面上有一枚一元钱的银币,你能在 CannyHough 的帮助下找到它的坐标方程吗?

确定一个圆的坐标方程,首先我们要检测到其边缘,然后求出其在纸面上的相对位置以及半径大小。

在这篇文章中我们使用 Canny 算法来检测出纸面上银币的边缘。

二、Canny 算法

Canny 可以用于拿到图像中物体的边缘,其步骤如下

  • 进行高斯平滑
  • 计算图像梯度(记录其强度、方向)
  • 进行非极大化抑制
  • 进行滞后边缘跟踪

进行上面的四步之后,我们拿到的纸面上硬币边缘提取效果图如下

(一)、高斯平滑

class GaussianSmoothingNet(nn.Module):
    def __init__(self) -> None:
        super(GaussianSmoothingNet, self).__init__()

        filter_size = 5
        # shape为(1, 5), 方差为 1.0 的高斯滤波核
        generated_filters = gaussian(filter_size,std=1.0).reshape([1,filter_size]) 

        # GFH(V): gaussian filter of horizontal(vertical) 水平(竖直)方向的高斯滤波核
        self.GFH = nn.Conv2d(1, 1, kernel_size=(1,filter_size), padding=(0,filter_size//2))
        self.GFV = nn.Conv2d(1, 1, kernel_size=(filter_size,1), padding=(filter_size//2,0))

        # 设置 w 的值为 高斯平滑核, b 的值为 0.0
        init_parameter(self.GFH, generated_filters, np.array([0.0]))
        init_parameter(self.GFV, generated_filters.T, np.array([0.0])) 

    def forward(self, img):
        img_r = img[:,0:1]  # 取出RGB三个通道的数据
        img_g = img[:,1:2]
        img_b = img[:,2:3]

        # 对图片的三个通道进行水平、垂直滤波
        blurred_img_r = self.GFV(self.GFH(img_r))
        blurred_img_g = self.GFV(self.GFH(img_g))
        blurred_img_b = self.GFV(self.GFH(img_b))

        # 合并成一张图
        blurred_img = torch.stack([blurred_img_r, blurred_img_g, blurred_img_b], dim=1)
        blurred_img = torch.stack([torch.squeeze(blurred_img)])

        return blurred_img

进行高斯平滑(模糊)之后的图片较原图更为模糊如下图右侧银币所示

完整代码见:gaussian_smoothing

(二)Sobel算子计算梯度

PAI = 3.1415926

class SobelFilterNet(nn.Module):
    def __init__(self) -> None:
        super(SobelFilterNet, self).__init__()
        sobel_filter = np.array([[-1, 0, 1],
                                 [-2, 0, 2],
                                 [-1, 0, 1]])
        self.SFH = nn.Conv2d(1, 1, kernel_size=sobel_filter.shape, padding=sobel_filter.shape[0]//2)
        self.SFV = nn.Conv2d(1, 1, kernel_size=sobel_filter.shape, padding=sobel_filter.shape[0]//2)

        init_parameter(self.SFH, sobel_filter, np.array([0.0]))
        init_parameter(self.SFV, sobel_filter.T, np.array([0.0]))

    def forward(self, img):
        img_r = img[:,0:1]
        img_g = img[:,1:2]
        img_b = img[:,2:3]

        # # SFH(V): sobel filter of horizontal(vertical) 水平(竖直)方向的Sobel滤波
        grad_r_x = self.SFH(img_r)  # 通道 R 的 x 方向梯度
        grad_r_y = self.SFV(img_r)
        grad_g_x = self.SFH(img_g)
        grad_g_y = self.SFV(img_g)
        grad_b_x = self.SFH(img_b)
        grad_b_y = self.SFV(img_b)

        # 计算强度(magnitude) 和 方向(orientation)
        magnitude_r = torch.sqrt(grad_r_x**2 + grad_r_y**2) # Gr^2 = Grx^2 + Gry^2
        magnitude_g = torch.sqrt(grad_g_x**2 + grad_g_y**2)
        magnitude_b = torch.sqrt(grad_b_x**2 + grad_b_y**2)

        grad_magnitude = magnitude_r + magnitude_g + magnitude_b

        grad_y = grad_r_y + grad_g_y + grad_b_y
        grad_x = grad_r_x + grad_g_x + grad_b_x

        # tanθ = grad_y / grad_x 转化为角度 (方向角)
        grad_orientation = (torch.atan2(grad_y, grad_x) * (180.0 / PAI))
        grad_orientation =  torch.round(grad_orientation / 45.0) * 45.0  # 转化为 45 的倍数

        return grad_magnitude, grad_orientation

将梯度强度当作图片进行输出,得到右下图最右侧图片,可知硬币的边缘区域梯度值较大(越大越亮)

完整代码见:sobel_filter

(三)非极大化抑制

非极大化抑制(NMS)的过程为:

  • 将梯度强度矩阵grad_magnitude的每一点都作为中心像素点,与其同向或者反向的两个相邻点(共有8个)的梯度强度进行比较。
  • 若中心点的梯度小于这两个方向上的梯度,则点中心的的梯度值设为0

进过上面的两个步骤,可以用一个像素的宽度替代了梯度屋脊效应,同时保留了屋脊的梯度强度(最大的梯度)。

class NonMaxSupression(nn.Module):
    def __init__(self) -> None:
        super(NonMaxSupression, self).__init__()

        all_orient_magnitude = np.stack([filter_0, filter_45, filter_90, filter_135, filter_180, filter_225, filter_270, filter_315])

        '''
        directional_filter功能见下面详细说明
        '''
        self.directional_filter = nn.Conv2d(1, 8, kernel_size=filter_0.shape, padding=filter_0.shape[-1] // 2)

        init_parameter(self.directional_filter, all_filters[:, None, ...], np.zeros(shape=(all_filters.shape[0],)))

    def forward(self, grad_magnitude, grad_orientation):

        all_orient_magnitude = self.directional_filter(grad_magnitude)     # 当前点梯度分别与其其他8个方向邻域点做差(相当于二阶梯度)

        '''
                \ 3|2 /
                 \ | /
            4     \|/    1
        -----------|------------
            5     /|\    8
                 / | \
                / 6|7 \
        注: 各个区域都是45度
        '''

        positive_orient = (grad_orientation / 45) % 8             # 设置正方向的类型,一共有八种不同类型的方向
        negative_orient = ((grad_orientation / 45) + 4) % 8       # +4 = 4 * 45 = 180 即旋转180度(如 1 -(+4)-> 5)

        height = positive_orient.size()[2]                        # 得到图片的宽高
        width = positive_orient.size()[3]
        pixel_count = height * width                                # 计算图片所有的像素点数
        pixel_offset = torch.FloatTensor([range(pixel_count)])

        position = (positive_orient.view(-1).data * pixel_count + pixel_offset).squeeze() # 角度 * 像素数 + 像素所在位置

        # 拿到图像中所有点与其正向邻域点的梯度的梯度(当前点梯度 - 正向邻域点梯度,根据其值与0的大小判断当前点是不是邻域内最大的)
        channel_select_filtered_positive = all_orient_magnitude.view(-1)[position.long()].view(1, height, width)

        position = (negative_orient.view(-1).data * pixel_count + pixel_offset).squeeze()

        # 拿到图像中所有点与其反向邻域点的梯度的梯度
        channel_select_filtered_negative = all_orient_magnitude.view(-1)[position.long()].view(1, height, width)

        # 组合成两个通道
        channel_select_filtered = torch.stack([channel_select_filtered_positive, channel_select_filtered_negative])

        is_max = channel_select_filtered.min(dim=0)[0] > 0.0 # 如果min{当前梯度-正向点梯度, 当前梯度-反向点梯度} > 0,则当前梯度最大
        is_max = torch.unsqueeze(is_max, dim=0)

        thin_edges = grad_magnitude.clone()
        thin_edges[is_max==0] = 0.0

        return thin_edges

directional_filter的用处是什么?

# 输入
tensor([[[[1., 1., 1.],
          [1., 1., 1.],
          [1., 1., 1.]]]])
# 输出
tensor([[[[0., 0., 1.],
          [0., 0., 1.],
          [0., 0., 1.]],

         [[0., 0., 1.],
          [0., 0., 1.],
          [1., 1., 1.]],

         [[0., 0., 0.],
          [0., 0., 0.],
          [1., 1., 1.]],

         [[1., 0., 0.],
          [1., 0., 0.],
          [1., 1., 1.]],

         [[1., 0., 0.],
          [1., 0., 0.],
          [1., 0., 0.]],

         [[1., 1., 1.],
          [1., 0., 0.],
          [1., 0., 0.]],

         [[1., 1., 1.],
          [0., 0., 0.],
          [0., 0., 0.]],

         [[1., 1., 1.],
          [0., 0., 1.],
          [0., 0., 1.]]]], grad_fn=<ThnnConv2DBackward0>)

可知其获取输入的八个方向的梯度值(在当前项目的代码中,为获取当前点梯度与其它8个方向梯度之差)

根据梯度的强度和方向,将方向分成8个类别(即对于每一点有八个可能方向),如上代码中 "米" 型图所示。

下面给出计算当前点正向邻域的相邻点的梯度强度的过程(反向同理)

梯度方向grad_orientation: 0, 1,, 2, 3, 4, 5, 6, 7 (共有8哥方向)

各方向梯度强度all_orient_magnitude: [[..方向0的梯度..], [..方向1的梯度..], ..., [..方向7的梯度..]]

故对于方向为 i 的点,其在梯度强度中的位置为 all_orient_magnitude[i][x, y],将all_orient_magnitude变化为一维向量后,对应的位置为position = current_orient × pixel_count + pixel_offset,我们就可以根据这个位置信息拿到当前点与其正向邻域点梯度强度之差(同理也可以拿到反向的)。

以下为辅助图示:

最后效果如下右侧图所示(左侧为未进行最大化抑制的图)

完整代码见:nonmax_supression

(四)滞后边缘跟踪

我们思考后发现,到目前为止仍有如下几个问题:

  • 如果图像中有噪声,可能会出现边缘无关的点(伪边)
  • 边缘点时阴时明

所以最后我们就需要进行滞后边缘跟踪了,其步骤如下:

  • 设定两个阈值(一高一低),将梯度强度小于低阈值的像素点的梯度强度设为0,得到图像A
  • 将梯度强度小于高阈值的像素点的梯度强度设为0,得到图像B

我们知道由于A的阈值较低,故边缘保留较完整,连续性较好,但是伪边可能也较多,B正好与A相反。

据此我们设想以B为基础,A为补充,通过递归追踪来补全B中边缺失的像素点。

to_bw = lambda image: (image > 0.0).astype(float)

class HysteresisThresholding(nn.Module):
    def __init__(self, low_threshold=1.0, high_threshold=3.0) -> None:
        super(HysteresisThresholding, self).__init__()
        self.low_threshold = low_threshold
        self.high_threshold = high_threshold

    def thresholding(self, low_thresh: torch.Tensor, high_thresh: torch.Tensor):
        died = torch.zeros_like(low_thresh).squeeze()
        low_thresh = low_thresh.squeeze()
        final_image = high_thresh.squeeze().clone()

        height = final_image.shape[0] - 1
        width = final_image.shape[1] - 1

        def connected(x, y, gap = 1):
            right = x + gap
            bottom = y + gap
            left = x - gap
            top = y - gap

            if left < 0 or top < 0 or right >= width or bottom >= height:
                return False

            return final_image[top, left] > 0  or final_image[top, x] > 0 or final_image[top, right] > 0 \
                or final_image[y, left] > 0 or final_image[y, right] > 0 \
                or final_image[bottom, left] > 0 or final_image[bottom, x] > 0 or final_image[bottom, right] > 0

        # 先高再宽
        def trace(x:int, y:int):
            right = x + 1
            bottom = y + 1
            left = x - 1
            top = y - 1
            if left < 0 or top < 0 or right >= width or bottom >= height or died[y, x] or final_image[y, x] > 0:
                return

            pass_high = final_image[y, x] > 0.0
            pass_low = low_thresh[y, x] > 0.0

            died[y, x] = True

            if pass_high:
                died[y, x] = False
            elif pass_low and not pass_high:
                if connected(x, y) or connected(x, y, 2): # 如果其他方向有连接
                    final_image[y, x] = low_thresh[y, x]
                    died[y, x] = False

            # 往回
            if final_image[y, x] > 0.0: # 当前点有连接
                if low_thresh[top, left] > 0: trace(left, top)
                if low_thresh[top, x] > 0: trace(x, top)
                if low_thresh[top, right] > 0: trace(right, top)
                if low_thresh[y, left] > 0: trace(left, y)
                if low_thresh[bottom, left] > 0: trace(left, bottom)

            # 往下
            trace(right, y)
            trace(x, bottom)
            trace(right, bottom)

        for i in range(width):
            for j in range(height):
                trace(i, j)

        final_image = final_image.unsqueeze(dim=0).unsqueeze(dim=0)

        return final_image

    def forward(self, thin_edges, grad_magnitude, grad_orientation):
        low_thresholded: torch.Tensor = thin_edges.clone()
        low_thresholded[thin_edges<self.low_threshold] = 0.0

        high_threshold: torch.Tensor = thin_edges.clone()
        high_threshold[thin_edges<self.high_threshold] = 0.0

        final_thresholded = self.thresholding(low_thresholded, high_threshold)

        return low_thresholded, high_threshold, final_thresholded

如下图为依次为低阈值、高阈值的效果图

如下为滞后边缘跟踪后的效果图

可知其相对上方左侧图,一些伪边被消除了,相对右侧图,细节更加的丰富。

完整代码见:hysteresis_thresholding

以上就是Python利用Canny算法检测硬币边缘的详细内容,更多关于Python Canny检测边缘的资料请关注我们其它相关文章!

(0)

相关推荐

  • python实现canny边缘检测

    canny边缘检测原理 canny边缘检测共有5部分组成,下边我会分别来介绍. 1 高斯模糊(略) 2 计算梯度幅值和方向. 可选用的模板:soble算子.Prewitt算子.Roberts模板等等; 一般采用soble算子,OpenCV也是如此,利用soble水平和垂直算子与输入图像卷积计算dx.dy: 进一步可以得到图像梯度的幅值: 为了简化计算,幅值也可以作如下近似: 角度为: 如下图表示了中心点的梯度向量.方位角以及边缘方向(任一点的边缘与梯度向量正交) : θ = θm = arcta

  • Python实现Canny及Hough算法代码实例解析

    任务说明:编写一个钱币定位系统,其不仅能够检测出输入图像中各个钱币的边缘,同时,还能给出各个钱币的圆心坐标与半径. 效果 代码实现 Canny边缘检测: # Author: Ji Qiu (BUPT) # filename: my_canny.py import cv2 import numpy as np class Canny: def __init__(self, Guassian_kernal_size, img, HT_high_threshold, HT_low_threshold)

  • python Canny边缘检测算法的实现

    图像边缘信息主要集中在高频段,通常说图像锐化或检测边缘,实质就是高频滤波.我们知道微分运算是求信号的变化率,具有加强高频分量的作用.在空域运算中来说,对图像的锐化就是计算微分.对于数字图像的离散信号,微分运算就变成计算差分或梯度.图像处理中有多种边缘检测(梯度)算子,常用的包括普通一阶差分,Robert算子(交叉差分),Sobel算子等等,是基于寻找梯度强度.拉普拉斯算子(二阶差分)是基于过零点检测.通过计算梯度,设置阀值,得到边缘图像. Canny边缘检测算子是一种多级检测算法.1986年由J

  • opencv python Canny边缘提取实现过程解析

    这篇文章主要介绍了opencv python Canny边缘提取实现过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 Canny是边缘提取算法,在1986年提出的是一个很好的边缘检测器Canny算法介绍 非最大信号抑制: 高低阈值连接: example import cv2 as cv import numpy as np # canny运算步骤:5步 # 1. 高斯模糊 - GaussianBlur # 2. 灰度转换 - cvtCol

  • Python利用Canny算法检测硬币边缘

    目录 一.问题背景 二.Canny 算法 (一).高斯平滑 (二)Sobel算子计算梯度 (三)非极大化抑制 (四)滞后边缘跟踪 一.问题背景 纸面上有一枚一元钱的银币,你能在 Canny 和 Hough 的帮助下找到它的坐标方程吗? 确定一个圆的坐标方程,首先我们要检测到其边缘,然后求出其在纸面上的相对位置以及半径大小. 在这篇文章中我们使用 Canny 算法来检测出纸面上银币的边缘. 二.Canny 算法 Canny 可以用于拿到图像中物体的边缘,其步骤如下 进行高斯平滑 计算图像梯度(记录

  • python利用K-Means算法实现对数据的聚类案例详解

    目的是为了检测出采集数据中的异常值.所以很明确,这种情况下的簇为2:正常数据和异常数据两大类 1.安装相应的库 import matplotlib.pyplot as plt # 用于可视化 from sklearn.cluster import KMeans # 用于聚类 import pandas as pd # 用于读取文件 2.实现聚类 2.1 读取数据并可视化 # 读取本地数据文件 df = pd.read_excel("../data/output3.xls", heade

  • Python利用FlashText算法实现替换字符串

    目录 前言 1.准备 2.基本使用 提取关键词 替换关键词 关键词大小写敏感 标记关键词位置 获取目前所有的关键词 批量添加关键词 单一或批量删除关键词 3.高级使用 支持额外信息 支持特殊单词边界 4.结尾 前言 FlashText 算法是由 Vikash Singh 于2017年发表的大规模关键词替换算法,这个算法的时间复杂度仅由文本长度(N)决定,算法时间复杂度为O(N). 而对于正则表达式的替换,算法时间复杂度还需要考虑被替换的关键词数量(M),因此时间复杂度为O(MxN). 简而言之,

  • Python利用 SVM 算法实现识别手写数字

    目录 前言 使用 SVM 进行手写数字识别 参数 C 和 γ 对识别手写数字精确度的影响 完整代码 前言 支持向量机 (Support Vector Machine, SVM) 是一种监督学习技术,它通过根据指定的类对训练数据进行最佳分离,从而在高维空间中构建一个或一组超平面.在博文<OpenCV-Python实战(13)--OpenCV与机器学习的碰撞>中,我们已经学习了如何在 OpenCV 中实现和训练 SVM 算法,同时通过简单的示例了解了如何使用 SVM 算法.在本文中,我们将学习如何

  • Python利用机器学习算法实现垃圾邮件的识别

    开发工具 **Python版本:**3.6.4 相关模块: scikit-learn模块: jieba模块: numpy模块: 以及一些Python自带的模块. 环境搭建 安装Python并添加到环境变量,pip安装需要的相关模块即可. 逐步实现 (1)划分数据集 网上用于垃圾邮件识别的数据集大多是英文邮件,所以为了表示诚意,我花了点时间找了一份中文邮件的数据集.数据集划分如下: 训练数据集: 7063封正常邮件(data/normal文件夹下): 7775封垃圾邮件(data/spam文件夹下

  • Python OpenCV Hough直线检测算法的原理实现

    目录 直线检测原理 OpenCV实现 直线检测原理 核心要点:图像坐标空间.参数空间.极坐标参数空间 -> (极坐标)参数空间表决 给定一个点,我们一般会写成y=ax+b的形式,这是坐标空间的写法:我们也可以写成b=-xa+y的形式,这是参数空间的写法.也就是说,给定一个点,那么经过该点的直线的参数必然满足b=-xa+y这一条件,也就是必然在参数空间中b=-xa+y这条直线上.如果给定两个点,那么这两点确定的唯一的直线的参数,就是参数空间中两条参数直线的交点. 由于上述写法不适合处理水平或垂直的

  • Python OpenCV基于霍夫圈变换算法检测图像中的圆形

    目录 第一章:霍夫变换检测圆 ① 实例演示1 ② 实例演示2 ③ 霍夫变换函数解析 第二章:Python + opencv 完整检测代码 ① 源代码 ② 运行效果图 第一章:霍夫变换检测圆 ① 实例演示1 这个是设定半径范围 0-50 后的效果. ② 实例演示2 这个是设定半径范围 50-70 后的效果,因为原图稍微大一点,半径也大了一些. ③ 霍夫变换函数解析 cv.HoughCircles() 方法 参数分别为:image.method.dp.minDist.param1.param2.mi

  • Python OpenCV Canny边缘检测算法的原理实现详解

    目录 Gaussian smoothing Computing the gradient magnitude and orientation Non-maxima suppression Hysteresis thresholding OpenCV实现 Gaussian smoothing 总的来说,Canny边缘检测可以分为四个步骤: 由于边缘检测对噪声敏感,因此对图像应用高斯平滑以帮助减少噪声.具体做法是,采用一个5*5的高斯平滑滤波器对图像进行滤波处理. Computing the gra

  • Python利用正则表达式实现计算器算法思路解析

    (1)不使用eval()等系统自带的计算方法 (2)实现四则混合运算.括号优先级解析 思路: 1.字符串预处理,将所有空格去除 2.判断是否存在括号运算,若存在进行第3步,若不存在则直接进入第4步 3.利用正则表达式获取最底层括号内的四则运算表达式 4.将四则运算表达式进行预处理:表达式开头有负数时,在表达式前加上一个0 5.利用re.split().re.findall()方法,通过加减符号,对四则运算进行拆分为乘除运算式和数字,并保留对应的位置下标. 6.利用re.split().re.fi

随机推荐