Canny 边缘检测算法_建筑物线性特征

Canny 检测算法包含下面几个阶段:

1.图像灰度化
2.高斯模糊处理
3.图像梯度、梯度幅值、梯度方向计算
4.NMS(非极大值抑制)
5.双阈值的边界选取
1、图像灰度化
  当仅提取一张图片的边界,单通道的图片已经足够提供检测出边界的信息。所以我们可以将R、G、B的3通道图片乃至更高维的高光谱遥感图像进行灰度化,这是一种降维操作,它减少了冗余数据从而降低了计算开销。以下是对RGB图片灰度化的方法:

def gray( 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)
    """
    # 读取图片,cv2.imread()接口读图像,读进来直接是BGR 格式数据格式在 0~255,
    #    通道格式为(W,H,C)
    img_bgr = cv2.imread(img_path)
    # BGR 转换成 RGB 格式
    img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
    # 灰度化
    img_gray = np.dot(img_rgb[...,:3], [0.299, 0.587, 0.114])
    
    return img_gray

2、高斯模糊处理
  高斯模糊实际上是对灰度化后的图像去噪。因为噪声也集中于高频信号,很容易被识别为伪边缘。应用高斯模糊去除噪声,降低伪边缘的识别。但是由于图像边缘信息也是高频信号,高斯模糊的半径选择很重要,过大的半径很容易让一些弱边缘检测不到。进行高斯滤波之前,需要先得到一个高斯滤波器(kernel)。如何得到一个高斯滤波器呢?其实就是将高斯函数离散化,将滤波器中对应的横纵坐标索引代入高斯函数,即可得到对应的值。不同尺寸的滤波器,得到的值也不同,下面是二维高斯函数与 (2k+1)x(2k+1) 滤波器的计算公式 :
  在这里插入图片描述
在这里插入图片描述
高斯滤波常用尺寸为 5x5,σ=1.4 的高斯滤波器。下面是 5x5 高斯滤波器的实现代码。

# 去除噪音 - 使用 5x5 的高斯滤波器
def smooth( 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

3、图像梯度、梯度幅值、梯度方向计算
一个图像上处于边界附近位置的像素值变化较大。而处于物体内部位置的像素值大多相近。这样我们可以计算当前像素与其附近像素的像素值的差值判断该像素处于物体内部还是边界。这个差值我们称为图像梯度。梯度幅值、梯度方向由图像梯度计算而来。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

# 计算梯度幅值
def gradients(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

4、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):1)下面两幅图是 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( dx, dy,M):

    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
  1. 双阀值。
    一般的边缘检测算法用一个阀值来滤除噪声或颜色变化引起的小的梯度值,而保留大的梯度值。Canny算法应用双阀值,即一个高阀值和一个低阀值来区分边缘像素。如果边缘像素点梯度值大于高阀值,则被认为是强边缘点。如果边缘梯度值小于高阀值,大于低阀值,则标记为弱边缘点小于低阀值的点则被抑制掉。这一步算法很简单。
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

最后代码:

# -*- coding: utf-8 -*-
"""
Created on Sun Feb 16 18:26:51 2020

@author: User
"""

import  matplotlib.pyplot  as plt
import cv2
import numpy as np
import math
# 灰度化
def gray( 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)
    """
    # 读取图片,cv2.imread()接口读图像,读进来直接是BGR 格式数据格式在 0~255,
    #    通道格式为(W,H,C)
    img_bgr = cv2.imread(img_path)
    # BGR 转换成 RGB 格式
    img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
    # 灰度化
    img_gray = np.dot(img_rgb[...,:3], [0.299, 0.587, 0.114])
    
    return img_gray

# 去除噪音 - 使用 5x5 的高斯滤波器
def smooth( 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


# 计算梯度幅值
def gradients(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



def NMS( dx, dy,M):

    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

def double_threshold(NMS):

    W, H = NMS.shape
    DT = np.zeros([W, H])

    # 定义高低阈值
    TL = 0.1 * np.max(NMS)
    TH = 0.3 * np.max(NMS)
#    TL=10
#    TH=100

    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
if __name__=='__main__':
    
    img_path="C:\\Users\\User\\Pictures\\Saved Pictures\\1.png"
    gray_image=gray(img_path)
    smooth_image=smooth(gray_image)
    gradients_images=gradients(smooth_image)
    dx=(gradients_images[0])
    dy=(gradients_images[1])
    M=(gradients_images[2])
    NMS_image= NMS(dx,dy, M)
    double_thresholdimage=double_threshold( NMS_image)
    plt.imshow(double_thresholdimage)
    plt.show()    

结果图:
在这里插入图片描述

发布了8 篇原创文章 · 获赞 0 · 访问量 301

猜你喜欢

转载自blog.csdn.net/qq_41627642/article/details/104348066