python-OpenCV之邊緣檢測
簡述
邊緣指畫素值急劇變化的位置。對於識別物體而言,邊緣起著非常重要的作用。邊緣檢測的目的是在不損害影象內容的情況下製作一個線圖。其方式依然是以卷積為核心操作。
知識點
1.有時需要將原圖片分別與若干個卷積核進行卷積,這時需要將各個卷積結果進行最終整合,整合的方式主要有以下四種方式
- 取對應位置絕對值的和
- 取對應位置平方和的開方
- 取對應位置絕對值的最大值
- 插值法:
2.因為畫素值的範圍為0~255,所以圖片陣列最後的資料型別應該為unit8
Roberts邊緣檢測
Roberts運算元是邊緣檢測中最簡單的運算元,利用差分定義生成。
檢測流程
1.分別用45°方向差分的卷積運算元和135°方向差分的卷積運算元對影象進行卷積
2.將上述兩個卷積結果進行整合
3.對最後結果進行整理(規範畫素值)
說明:
1.scipy庫中的convolve2d函式可進行二維陣列的卷積,其語法為 scipy.signal.convolve2d(in1, in2, mode='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()
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()
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()
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()
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(可選) |
邊界擴充型別 |