1. 程式人生 > >python-OpenCV之邊緣檢測

python-OpenCV之邊緣檢測

簡述

邊緣指畫素值急劇變化的位置。對於識別物體而言,邊緣起著非常重要的作用。邊緣檢測的目的是在不損害影象內容的情況下製作一個線圖。其方式依然是以卷積為核心操作。

知識點

1.有時需要將原圖片分別與若干個卷積核進行卷積,這時需要將各個卷積結果進行最終整合,整合的方式主要有以下四種方式

  • 取對應位置絕對值的和
  • 取對應位置平方和的開方
  • 取對應位置絕對值的最大值
  • 插值法:

2.因為畫素值的範圍為0~255,所以圖片陣列最後的資料型別應該為unit8

Roberts邊緣檢測

Roberts運算元是邊緣檢測中最簡單的運算元,利用差分定義生成。

檢測流程

1.分別用45°方向差分的卷積運算元和135°方向差分的卷積運算元對影象進行卷積

2.將上述兩個卷積結果進行整合

3.對最後結果進行整理(規範畫素值)

說明:

1.scipy庫中的convolve2d函式可進行二維陣列的卷積,其語法為  scipy.signal.convolve2d(in1in2mode='full'boundary='fill'fillvalue=0)

程式碼示例

import cv2 as cv
import numpy as np
from scipy import signal


# 定義roberts函式
def roberts(I, _boundary='full', _fillvalue=0):
    # 獲得原圖片的尺寸
    H1, W1 = I.shape[0:2]
    # 定義運算元尺寸
    H2, W2 = 2, 2

    # 進行45°方向卷積
    # 定義45°方向卷積核
    R1 = np.array([[1, 0], [0, -1]], np.float32)
    # 錨點位置
    kr1, kc1 = 0, 0
    # 進行卷積
    IconR1 = signal.convolve2d(I, R1, mode='full', boundary=_boundary, fillvalue=_fillvalue)
    # 擷取得到same卷積
    IconR1 = IconR1[H2 - kr1 - 1:H1 + H2 - kr1 - 1, W2 - kc1 - 1:W1 + W2 - kc1 - 1]

    # 進行135°方向卷積
    R2 = np.array([[0, 1], [-1, 0]], np.float32)
    kr2, kc2 = 0, 1
    IconR2 = signal.convolve2d(I, R2, mode='full', boundary=_boundary, fillvalue=_fillvalue)
    IconR2 = IconR2[H2 - kr2 - 1:H1 + H2 - kr2 - 1, W2 - kc2 - 1:W1 + W2 - kc2 - 1]

    return (IconR1, IconR2)


if __name__ == "__main__":
    # 讀取圖片
    image = cv.imread('test2.jpg', flags=0)
    cv.imshow('original_Image', image)

    # 進行roberts邊緣檢測
    IconR1, IconR2 = roberts(image, 'symm')

    # 取圖片陣列各值的絕對值
    IconR1 = np.abs(IconR1)
    # RGB影象的深度應為8位
    edge_45 = IconR1.astype(np.uint8)
    cv.namedWindow('edge_45', cv.WINDOW_NORMAL)
    cv.imshow('edge_45', edge_45)

    IconR2 = np.abs(IconR2)
    edge_135 = IconR2.astype(np.uint8)
    cv.namedWindow('edge_135', cv.WINDOW_NORMAL)
    cv.imshow('edge_135', edge_135)

    # 將45°方向卷積結果和135°方向卷積結果平方後求和在開方求得
    edge = np.sqrt(np.power(IconR1, 2.0) + np.power(IconR2, 2.0))
    edge = np.round(edge)
    # 因為是兩個圖片的‘疊加‘,所以存在大於255的風險,將大於255的畫素值都取255
    edge[edge > 255] = 255
    edge = edge.astype(np.uint8)
    cv.namedWindow('edge', cv.WINDOW_NORMAL)
    cv.imshow('edge', edge)

    cv.waitKey()
    cv.destroyAllWindows()
原圖
45°方向卷積核卷積結果
135°方向卷積核卷積結果
整合後效果

prewitt邊緣檢測

prewitt運算元可以看出是均值平滑運算元和Roberts運算元卷積後的結果,因此他兼具平滑和檢測功能。

檢測流程

1.對影象的豎直/水平方向進行平滑

2.對影象的水平/豎直方向進行差分

3.將水平和豎直方向上的差分結果進行整合

4.對最後結果進行整理(規範畫素值)

程式碼示例

import cv2 as cv
import numpy as np
from scipy import signal


def prewitt(I, _boundary='symm'):
    # 先對豎直方向進行平滑
    ones_y = np.array([[1], [1], [1]], np.float32)
    i_conv_pre_x = signal.convolve2d(I, ones_y, mode='same', boundary=_boundary)
    # 再對水平方向進行差分
    diff_x = np.array([[1, 0, -1]], np.float32)
    i_conv_pre_x = signal.convolve2d(i_conv_pre_x, diff_x, mode='same', boundary=_boundary)

    # 對水平方向進行平滑
    ones_x = np.array([[1, 1, 1]], np.float32)
    i_conv_pre_y = signal.convolve2d(I, ones_x, mode='same', boundary=_boundary)
    # 對豎直方向進行差分
    diff_y = np.array([[1], [0], [-1]], np.float32)
    i_conv_pre_y = signal.convolve2d(i_conv_pre_y, diff_y, mode='same', boundary=_boundary)

    return (i_conv_pre_x, i_conv_pre_y)


if __name__ == "__main__":
    # 讀取圖片,注意,要讀入灰度圖
    image = cv.imread('test.jpg', flags=0)
    # 顯示原圖片
    cv.namedWindow('dfs', cv.WINDOW_NORMAL)
    cv.imshow('dfs', image)
    
    # 呼叫已寫好的函式進行卷積
    i_conv_pre_x, i_conv_pre_y = prewitt(image)

    # 對影象陣列的數值取絕對值
    abs_i_conv_pre_x = np.abs(i_conv_pre_x)
    abs_i_conv_pre_y = np.abs(i_conv_pre_y)

    # 重新複製一份結果,後面合成最終結果時會用到
    edge_x = abs_i_conv_pre_x.copy()
    edge_y = abs_i_conv_pre_y.copy()

    # 將超出255的賦值為255
    edge_x[edge_x > 255] = 255
    edge_y[edge_y > 255] = 255

    # 因為色素的數值範圍為0~255,所以應該設定為unit8資料型別
    edge_y = edge_y.astype(np.uint8)
    edge_x = edge_x.astype(np.uint8)

    cv.namedWindow('edge_x', cv.WINDOW_NORMAL)
    cv.imshow("edge_x", edge_x)

    cv.namedWindow('edge_y', cv.WINDOW_NORMAL)
    cv.imshow('edge_y', edge_y)

    # 將兩個結果合併
    # 有多種合併方法,這裡用的時插值法
    edge = abs_i_conv_pre_x * 0.5 + abs_i_conv_pre_y * 0.5
    edge[edge > 255] = 255
    edge = edge.astype(np.uint8)
    
    cv.namedWindow('edge', cv.WINDOW_NORMAL)
    cv.imshow('edge', edge)
    cv.waitKey()
    cv.destroyAllWindows()
原圖​​​​​
x方向上進行差值
y​​​​​方向上進行差值
整合後效果

Sobel邊緣檢測

Sobel運算元跟Prewitt運算元類似,也自帶平滑效果,只不過它的平滑不是非歸一均值平滑而是非歸一高斯平滑。

檢測流程

1.對影象的豎直/水平方向進行平滑

2.對影象的水平/豎直方向進行差分

3.將水平和豎直方向上的差分結果進行整合

4.對最後結果進行整理(規範畫素值)

程式碼示例

import math
import cv2 as cv
import numpy as np
from scipy import signal


# 理論上sobel演算法採用高斯平滑的運算元,應該比prewitt對“明顯”的邊緣更加敏感
# 返回n階非歸一化的高斯平滑運算元
def pascalSmooth(n):
    # 這裡的陣列必須為二維,因為後面的convoluted函式要求的輸入必須為二維陣列
    pascalSmooth = np.zeros([1, n], np.float32)
    for i in range(n):
        # math.factorial(x)函式返回x的階乘
        pascalSmooth[0][i] = math.factorial(n - 1) / (math.factorial(i) * math.factorial(n - 1 - i))
    return pascalSmooth


# 由高斯平滑運算元得到差分運算元,並返回差分運算元
def pascalDiff(n):
    pascalDiff = np.zeros([1, n], np.float32)
    pascalSmooth_previous = pascalSmooth(n - 1)
    for i in range(n):
        if i == 0:
            pascalDiff[0][i] = pascalSmooth_previous[0][i]
        elif i == n - 1:
            pascalDiff[0][i] = -pascalSmooth_previous[0][i - 1]
        else:
            pascalDiff[0][i] = pascalSmooth_previous[0][i] - pascalSmooth_previous[0][i - 1]
    return pascalDiff


# 得到sobel運算元
def getSobelKernel(n):
    pascalSmoothKernel = pascalSmooth(n)
    pascalDiffKernel = pascalDiff(n)
    # np.transpose的功能是將矩陣轉置
    sobelKernal_x = signal.convolve2d(pascalSmoothKernel.transpose(), pascalDiffKernel, mode='full')
    sobelKernal_y = signal.convolve2d(pascalSmoothKernel, pascalDiffKernel.transpose(), mode='full')

    return (sobelKernal_x, sobelKernal_y)


# sobel邊緣檢測核心函式
def sobel(image, n):
    # rows, cols = image.shape
    pascalSmoothKernel = pascalSmooth(n)
    pascalDiffKernel = pascalDiff(n)

    image_sobel_x = signal.convolve2d(image, pascalSmoothKernel.transpose(), mode='same')
    image_sobel_x = signal.convolve2d(image_sobel_x, pascalDiffKernel, mode='same')

    image_sobel_y = signal.convolve2d(image, pascalSmoothKernel, mode='same')
    image_sobel_y = signal.convolve2d(image_sobel_y, pascalDiffKernel.transpose(), mode='same')

    return (image_sobel_x, image_sobel_y)


if __name__ == "__main__":
    image = cv.imread('test4.jpg', flags=0)
    cv.namedWindow('original', cv.WINDOW_NORMAL)
    cv.imshow('original', image)
    # 進行sobel卷積
    sobel_x, sobel_y = sobel(image, 7)
    # 歸一化處理
    image_sobel_x = sobel_x / np.max(sobel_x)
    image_sobel_y = sobel_y / np.max(sobel_y)
    image_sobel_x = np.power(image_sobel_x, 1)
    image_sobel_y = np.power(image_sobel_y, 1)
    # 數值化
    image_sobel_x = image_sobel_x * 255
    image_sobel_y = image_sobel_y * 255
    image_sobel_x = image_sobel_x.astype(np.uint8)
    image_sobel_y = image_sobel_y.astype(np.uint8)
    # 顯示不同方向的sobel卷積結果
    cv.namedWindow('sobel_x', cv.WINDOW_NORMAL)
    cv.namedWindow('sobel_y', cv.WINDOW_NORMAL)
    cv.imshow('sobel_x', image_sobel_x)
    cv.imshow('sobel_y', image_sobel_y)

    # 對兩個方向的處理結果進行整合
    edge = np.sqrt(np.power(sobel_x, 2.0), np.power(sobel_y, 2.0))
    edge = edge / np.max(edge)
    edge *= 255
    edge = edge.astype(np.uint8)
    cv.namedWindow('sobel', cv.WINDOW_NORMAL)
    cv.imshow('sobel', edge)
    cv.waitKey()
    cv.destroyAllWindows()
原圖
x方向差分的sobel運算元處理結果
y方向差分的sobel運算元處理結果
兩個方向整合後的最終結果

OpenCV API

函式語法:Sobel( src, ddepth, dx, dy, ksize, scale, borderType )

引數

解釋

src

輸入矩陣

ddepth

輸出矩陣的資料型別

dx

當dx ≠ 0時,src與差分方向為水平方向上的Sobel核卷積

dy

當dx = 0,dy ≠ 0時,src與差分方向為垂直方向上的Sobel核卷積

ksize

Sobel核的尺寸,值為1,3,5,7

Scale(可選)

比例係數

Delta(可選)

平移係數

borderType(可選)

邊界擴充型別

Canny邊緣檢測

基於卷積運算的邊緣檢測演算法,如Sobel和Prewitt等有以下兩個缺點:

1.沒有充分利用邊緣的梯度

2.最後輸出的邊緣二值圖只是簡單地利用閾值進行處理,容易損失資訊。

Canny演算法基於這兩點做了改進,提出了:

1.基於邊緣梯度方向的非極大值抑制

2.雙閾值的滯後閾值處理

檢測流程

1.高斯平滑去噪

2.計算梯度方向

3.非極大值抑制

4.雙閾值的滯後閾值處理

示例程式碼

import math

import Sobel
import cv2 as cv
import numpy as np


# 非極大值抑制函式,返回“極大值”矩陣
def non_maximum_suppression_default(dx, dy):
    # dx和dy為經過水平差分運算元和垂直差分運算元卷積後的數值矩陣(sobel或者prewitt)
    edgeMag = np.sqrt(np.power(dx, 2.0), np.power(dy, 2.0))
    rows, cols = edgeMag.shape

    # 梯度矩陣(邊緣強度)
    # gradientDirection = np.zeros(edgeMag.shape)
    # 非極大值抑制後的矩陣
    edgeMag_nonMaxSup = np.zeros(edgeMag.shape)

    for r in range(1, rows - 1):
        for c in range(1, cols - 1):
            angle = math.atan2(dy[r][c], dx[r][c]) / math.pi * 180
            # gradientDirection[r][c] = angle
            # 判斷差值方向
            # 左右方向
            if (abs(angle) < 22.5 or abs(angle) > 157.5):
                if (edgeMag[r][c] > edgeMag[r][c - 1] and edgeMag[r][c] > edgeMag[r][c + 1]):
                    edgeMag_nonMaxSup[r][c] = edgeMag[r][c]
            # 左上/右下方向
            if (angle >= 22.5 and angle < 67.5 or (-angle > 112.5 and -angle <= 157.5)):
                if (edgeMag[r][c] > edgeMag[r - 1][c - 1] and edgeMag[r][c] > edgeMag[r + 1][c + 1]):
                    edgeMag_nonMaxSup[r][c] = edgeMag[r][c]
            # 上下方向
            if (abs(angle) >= 67.5 and abs(angle) <= 112.5):
                if (edgeMag[r][c] > edgeMag[r - 1][c] and edgeMag[r][c] > edgeMag[r + 1][c]):
                    edgeMag_nonMaxSup[r][c] = edgeMag[r][c]
            # 右上/左下方向
            if ((angle > 112.5 and angle <= 157.5) or (-angle >= 22.5 and -angle < 67.5)):
                if (edgeMag[r][c] > edgeMag[r - 1][c + 1] and edgeMag[r][c] > edgeMag[r + 1][c - 1]):
                    edgeMag_nonMaxSup[r][c] = edgeMag[r][c]

    return edgeMag_nonMaxSup


# 判斷一個點的座標是否在影象內
def checkInRange(r, c, rows, cols):
    if r >= 0 and r < rows and c >= 0 and c < cols:
        return True
    else:
        return False


# 在已有高於閾值點的情況下確定邊緣點
def trace(edgeMag_nonMaxSup, edge, lowerThresh, r, c, rows, cols):
    if edge[r][c] == 0:
        # 將符合“邊緣”要求的畫素值都設定為255
        edge[r][c] = 255
        for i in range(-1, 2):
            for j in range(-1, 2):
                # 大於低閾值便視為邊緣
                if checkInRange(r + i, c + j, rows, cols) and edgeMag_nonMaxSup[r + i][c + j] >= lowerThresh:
                    # 這裡用遞迴遍歷周圍的畫素點
                    trace(edgeMag_nonMaxSup, edge, lowerThresh, r + i, c + j, rows, cols)


# 滯後閾值處理
def hyteresisThreshold(edge_nonMaxSup, lowerThresh, upperThresh):
    rows, cols = edge_nonMaxSup.shape
    # 新建立一個用於儲存邊緣的陣列
    edge = np.zeros(edge_nonMaxSup.shape, np.uint8)
    for r in range(1, rows - 1):
        for c in range(1, cols - 1):
            # 如果大於高閾值,就確定與其相連的邊緣點
            if edge_nonMaxSup[r][c] >= upperThresh:
                trace(edge_nonMaxSup, edge, lowerThresh, r, c, rows, cols)
            # 如果低於低閾值,就直接捨棄
            if edge_nonMaxSup[r][c] < lowerThresh:
                edge[r][c] = 0
    return edge


if __name__ == "__main__":
    image = cv.imread('test2.jpg', flags=0)
    cv.imshow('original', image)

    image_sobel_x, image_sobel_y = Sobel.sobel(image, 3)
    edge = np.sqrt(np.power(image_sobel_x, 2.0), np.power(image_sobel_y, 2.0))
    edge[edge > 255] = 255
    edge = edge.astype(np.uint8)
    cv.namedWindow('sobel', cv.WINDOW_NORMAL)
    cv.imshow('sobel', edge)

    # 抑制非極大值
    edgeMag_nonMaxSup = non_maximum_suppression_default(image_sobel_x, image_sobel_y)

    edgeMag_nonMaxSup[edgeMag_nonMaxSup > 255] = 255
    edgeMag_nonMaxSup = edgeMag_nonMaxSup.astype(np.uint8)
    cv.namedWindow('edgeMag_nonMaxSup', cv.WINDOW_NORMAL)
    cv.imshow('edgeMag_nonMaxSup', edgeMag_nonMaxSup)

    lowerThresh = 40
    upperThresh = 150
    edge = hyteresisThreshold(edgeMag_nonMaxSup, lowerThresh, upperThresh)
    cv.namedWindow('canny', cv.WINDOW_NORMAL)
    cv.imshow('canny', edge)

    cv.waitKey()
    cv.destroyAllWindows()
原圖
sobel卷積後的圖片​​​​​
抑制非極大值後的圖片
canny邊緣檢測最終效果

OpenCV API

函式語法:Canny(image, edges, threshold1, threshole2, apertureSize, L2gradient )

引數

解釋

image

輸入影象

threshold1

低閾值

threshold2

高閾值

Edges(可選)

輸出邊緣強度影象

apertureSize(可選)

Sobel核的視窗大小,預設3×3

L2gradient(可選)

計算總的邊緣強度時使用的方式,值為true時代表使用的是平方和開方的方式,值為false代表使用的是絕對值和的方式。

Laplacian邊緣檢測

import cv2 as cv
import numpy as np
from scipy import signal


# 核心步驟,拉普拉斯卷積核與影象矩陣進行卷積
def laplacian(image, _boundary='full', _fillvalue=0):
    laplacianKernel = np.array([[0, -1, 0], [-1, 4, -1], [0, -1, 0]], np.float32)
    i_conv_lap = signal.convolve2d(image, laplacianKernel, mode='same', boundary=_boundary, fillvalue=_fillvalue)

    return i_conv_lap


if __name__ == "__main__":
    image = cv.imread('test5.jpg', flags=0)
    cv.imshow('original', image)
    image = np.array(image)
    # 對影象進行高斯平滑
    image = cv.GaussianBlur(image, (3, 3), 0)
    # 呼叫拉普拉斯核卷積函式
    threshEdge = laplacian(image, 'symm')

    # 進行閾值處理
    threshEdge[threshEdge > 0] = 255
    threshEdge[threshEdge < 0] = 0
    threshEdge = threshEdge.astype(np.uint8)
    cv.imshow('threshEdge', threshEdge)

    cv.waitKey()
    cv.destroyAllWindows()

OpenCV API

函式語法:placian(src, dst, ddepth, ksize, scale, delta, borderType )

引數

解釋

src

輸入矩陣

dst

輸出矩陣

ddepth

輸出矩陣的資料型別(位深)

Ksize(可選)

Laplacian核的型別

Scale(可選)

比例係數

delta(可選)

平移係數

borderType(可選)

邊界擴充型別