OpenCV學(xué)習(xí)筆記 | 邊緣檢測(cè)Canny算法復(fù)現(xiàn) | Python實(shí)現(xiàn)

摘要
????????OpenCV中的邊緣檢測(cè)是指在圖像中檢測(cè)出明顯的邊緣輪廓線,可以通過計(jì)算圖像中每個(gè)像素的梯度來實(shí)現(xiàn)。Canny算法是一種常用的邊緣檢測(cè)算法,它主要通過連續(xù)的操作來尋找邊緣,包括對(duì)圖像去噪、計(jì)算圖像梯度、非極大值抑制和雙閾值處理等步驟。

一、圖片加載及添加椒鹽噪聲
? ? ? ? 為方便算法實(shí)現(xiàn),本文僅對(duì)灰度圖像進(jìn)行測(cè)試。首先導(dǎo)入必要的庫(kù)后對(duì)圖片進(jìn)行加載,轉(zhuǎn)化成灰度圖像后重置圖片大小,以便最后圖像輸出。
import CV2 as cv
import numpy as np
import matplotlib.pyplot as plt
from scipy.interpolate import splprep, splev
from scipy import signal
?
image = cv.imread('D:\pythonProject2\canny_image.jpg')
image = cv.cvtColor(image, cv.COLOR_BGR2GRAY)
image = cv.resize(image, (928, 723))
? ? ? ? 為對(duì)比不同濾波算法對(duì)圖片去噪處理的能力,首先添加像素比例為0.03的椒鹽噪聲。椒鹽噪聲是數(shù)字圖像處理中常見的一種噪聲類型,它通常是由于傳感器故障、傳輸過程中的干擾以及存儲(chǔ)介質(zhì)的損壞等因素引起的。它的特點(diǎn)是將圖像中的某些像素點(diǎn)替換為黑色或白色,從而形成亮點(diǎn)或暗點(diǎn),看起來就像粒子的分布一樣,因此也稱為“椒鹽粒子噪聲”。原始圖片和添加了椒鹽噪聲的圖片如圖1和圖2所示。

添加椒鹽噪聲的函數(shù)如下:
def add_salt_and_pepper_noise(image, ratio): # 椒鹽噪聲
? ? noisy = np.copy(image)
?
? ? num_salt = int(ratio * np.size(image)) # 計(jì)算需要添加的椒鹽像素個(gè)數(shù):
? ? coords_row = np.random.randint(0, image.shape[0] - 1, size=num_salt)
? ? coords_col = np.random.randint(0, image.shape[0] - 1, size=num_salt)
? ? # 首先生成0到image.shape[0] - 1的隨機(jī)整數(shù),一共生成num_salt個(gè)隨機(jī)數(shù),即num_salt個(gè)椒鹽噪聲位置
? ? # 設(shè)置最大值為image.shape[0] - 1是因?yàn)樗饕龔?開始,不能包含image.shape[0]
? ? coords = np.vstack((coords_row,coords_col))
? ? # 用vstack將椒鹽噪聲的行坐標(biāo)列坐標(biāo)組合成一個(gè)二維數(shù)組
? ? noisy[tuple(coords)] = 0 # 在原始圖像上將coords的位置像素點(diǎn)變?yōu)楹谏?/span>
? ? # 用tuple進(jìn)行操作,是因?yàn)閠uple是一種不可改變的數(shù)據(jù)類型,能保證添加椒鹽噪聲后原始圖像不會(huì)改變
?
? ? return noisy

?二、中值濾波和高斯濾波去噪
????????中值濾波是一種常見的數(shù)字圖像處理技術(shù),用于去除數(shù)字圖像中的椒鹽噪聲等,中值濾波不僅能去除椒鹽噪聲,而且能夠保留圖像的邊緣信息。
? ? ? ? 首先設(shè)置一個(gè)3×3的核,我們將遍歷整個(gè)圖片的像素,將核中心的像素值替換為周圍領(lǐng)域八個(gè)像素的中值,需要注意的是得注意核的半徑,避免出現(xiàn)遍歷到圖片大小以外的位置。再一個(gè)是像素的數(shù)值都是處于0到255之間,因此在初始化時(shí)定義了np.uint8的數(shù)據(jù)類型,可以有效地降低圖像數(shù)據(jù)的存儲(chǔ)空間,節(jié)省內(nèi)存,并提高計(jì)算速度。中值濾波的實(shí)現(xiàn)代碼如下,效果如圖3所示。
def median_filter(image, kernel_size): # 中值濾波
? ? row, col = image.shape # 圖像的行高和列寬
? ? kernel_radius = kernel_size // 2 # 計(jì)算 kernel 的半徑
? ? median_image = np.zeros((row, col), np.uint8) # 初始化輸出圖像
? ? # 遍歷每個(gè)像素點(diǎn),進(jìn)行中值濾波
? ? for y in range(kernel_radius, row - kernel_radius): # range左閉右開
? ? ? ? for x in range(kernel_radius, col - kernel_radius):
? ? ? ? ? ? kernel = image[y - kernel_radius:y + kernel_radius + 1, x - kernel_radius:x + kernel_radius + 1]
? ? ? ? ? ? # 獲取 kernel 區(qū)域內(nèi)的像素值
? ? ? ? ? ? median_image[y, x] = np.median(kernel) # 計(jì)算中位數(shù)并更新輸出圖像,注意坐標(biāo)y,x
? ? return median_image

三、計(jì)算每個(gè)像素點(diǎn)的梯度強(qiáng)度和方向
????????計(jì)算圖像梯度強(qiáng)度通常使用Sobel算子,其算法基于微積分中的梯度概念,通過計(jì)算像素點(diǎn)周圍像素值變化的差異來確定圖像的邊緣。假設(shè)Gx和Gy分別表示原始圖像在?x?和?y?方向上的梯度,則可以使用以下Sobel算子計(jì)算梯度強(qiáng)度:

????當(dāng)G數(shù)值較大時(shí),表示該點(diǎn)有較大的梯度強(qiáng)度,可以認(rèn)為是圖像的邊緣。其中Gx和Gy的矩陣大小如下所示:


?????而對(duì)于梯度方向的計(jì)算,如果梯度方向在y軸上,即夾角為90°,其他情況只需代入公式計(jì)算即可,公式如下:

????實(shí)現(xiàn)代碼如下,利用Sobel算子計(jì)算出梯度強(qiáng)度輸出的邊緣檢測(cè)圖片如圖4所示。
def Sobel(image): # 利用Sobel算子計(jì)算每個(gè)像素點(diǎn)的梯度和梯度方向
? ? # 默認(rèn)窗口大小為3*3
? ? Gx = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]])
? ? Gy = np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]])
? ? row, col = image.shape
? ? gradients = np.zeros([row - 2, col - 2])
? ? direction = np.zeros([row - 2, col - 2])
? ? for i in range(row - 2): # range左閉右開,從0到row-3
? ? ? ? for j in range(col - 2): # range左閉右開,從0到col-3
? ? ? ? ? ? dx = np.sum(image[i:i+3, j:j+3] * Gx) # 進(jìn)行卷積運(yùn)算
? ? ? ? ? ? dy = np.sum(image[i:i+3, j:j+3] * Gy)
? ? ? ? ? ? gradients[i, j] = np.sqrt(dx ** 2 + dy ** 2) # 計(jì)算每一點(diǎn)的梯度
? ? ? ? ? ? if dx == 0:
? ? ? ? ? ? ? ? direction[i, j] = np.pi / 2 # 若梯度在y軸上,那么方向夾角為90°,由于dx在分母上,因此需要單獨(dú)討論
? ? ? ? ? ? else:
? ? ? ? ? ? ? ? direction[i, j] = np.arctan(dy / dx) # 其余情況方向角的計(jì)算代入公式即可
? ? gradients = np.uint8(gradients) # 由于像素的值都在0到255之間,因此需要將數(shù)值存儲(chǔ)在8位的整型數(shù)組中
? ? return gradients, direction

?四、非極大值抑制算法減少非邊緣
????????非極大值抑制算法(Non-Maximum Suppression,NMS)是一種常見的邊緣檢測(cè)后處理方法,它的目的是減少由離散化邊緣檢測(cè)算法產(chǎn)生的假陽性結(jié)果(即對(duì)圖像中非邊緣位置賦值為邊緣),從而得到更加準(zhǔn)確的圖像邊緣。
? ? ? ? 首先先將Sobel算子所計(jì)算出來的梯度方向離散成水平方向(0°、180°)和對(duì)角線方向(45°、135°)以及垂直方向(90°、270°)三種類別。
對(duì)于水平方向,我們檢查當(dāng)前像素點(diǎn)的左右兩個(gè)像素點(diǎn)是否比當(dāng)前像素點(diǎn)的梯度幅值小,如果是,說明當(dāng)前像素點(diǎn)不是一條邊緣上的點(diǎn)之一,舍棄這個(gè)點(diǎn);否則,將該像素點(diǎn)保留下來。
對(duì)于垂直方向,我們檢查當(dāng)前像素點(diǎn)的上下兩個(gè)像素點(diǎn)是否比當(dāng)前像素點(diǎn)的梯度幅值小,如果是,說明當(dāng)前像素點(diǎn)不是一條邊緣上的點(diǎn)之一,舍棄這個(gè)點(diǎn);否則,將該像素點(diǎn)保留下來。
對(duì)于對(duì)角線方向,我們檢查當(dāng)前像素點(diǎn)的兩個(gè)對(duì)角線方向的像素點(diǎn)是否比當(dāng)前像素點(diǎn)的梯度幅值小,如果是,說明當(dāng)前像素點(diǎn)不是一條邊緣上的點(diǎn)之一,舍棄這個(gè)點(diǎn);否則,將該像素點(diǎn)保留下來。
? ? ? ? 對(duì)于離散類別的角度范圍如下表所示,為方便討論,所有負(fù)數(shù)角都將加Π
,轉(zhuǎn)化成正數(shù)角考慮。實(shí)現(xiàn)代碼如下,使用非極大值抑制算法檢測(cè)的邊緣圖像如圖5所示。
def non_maximum_suppression(magnitude, orientation): # 非極大值抑制算法
? ? # magnitude:梯度強(qiáng)度矩陣 orientation:梯度方向矩陣
? ? row, col = magnitude.shape? # 獲取尺寸信息
? ? out_edges = np.zeros_like(magnitude)? # 初始化輸出矩陣
? ? # 每個(gè)像素和周圍像素點(diǎn)進(jìn)行比較
? ? for i in range(1, row - 1): # 該算法是要比較中間像素點(diǎn)和周圍八個(gè)像素點(diǎn)的數(shù)值大小,因此需要去除首行和尾行
? ? ? ? for j in range(1, col - 1): # 去除首列和尾列
? ? ? ? ? ? angle = orientation[i, j] # 對(duì)每個(gè)像素點(diǎn)的梯度方向進(jìn)行比較
? ? ? ? ? ? if angle < 0: # 將負(fù)值角度翻轉(zhuǎn)到x軸上方討論
? ? ? ? ? ? ? ? angle += np.pi
? ? ? ? ? ? if (angle <= np.pi/8 or angle >= 7*np.pi/8) :? # 離散成0度和180度的梯度方向
? ? ? ? ? ? ? ? if magnitude[i, j] > magnitude[i, j - 1] and magnitude[i, j] > magnitude[i, j + 1]: # 只檢查左右兩個(gè)像素點(diǎn)的數(shù)值
? ? ? ? ? ? ? ? ? ? out_edges[i, j] = magnitude[i, j]
? ? ? ? ? ? elif (angle > np.pi/8 and angle <= 3*np.pi/8) or (angle >= 5*np.pi/8 and angle < 7*np.pi/8): # 離散成45度和135的梯度方向
? ? ? ? ? ? ? ? if magnitude[i, j] > magnitude[i - 1, j - 1] and magnitude[i, j] > magnitude[i + 1, j + 1]: # 只檢測(cè)對(duì)角像素點(diǎn)的數(shù)值
? ? ? ? ? ? ? ? ? ? out_edges[i, j] = magnitude[i, j]
? ? ? ? ? ? elif (angle > 3*np.pi/8 and angle < 5*np.pi/8) : # 離散成90度或270度的梯度方向
? ? ? ? ? ? ? ? if magnitude[i, j] > magnitude[i - 1, j] and magnitude[i, j] > magnitude[i + 1, j]: # 只檢測(cè)上下兩個(gè)像素點(diǎn)的數(shù)值
? ? ? ? ? ? ? ? ? ? out_edges[i, j] = magnitude[i, j]
? ? return out_edges

五、雙閾值法確定強(qiáng)邊緣
????????雙閾值法是Canny邊緣檢測(cè)算法中的一種方法,用于確定哪些邊緣是真正的邊緣(強(qiáng)邊緣),哪些邊緣是無關(guān)緊要的噪聲(弱邊緣)。
????????在這個(gè)算法中,我們可以通過設(shè)定低于低閾值(threshold_low)的梯度幅值的像素被認(rèn)為是非邊緣像素,低閾值(threshold_low)和高閾值(threshold_high)之間的像素被認(rèn)為是弱邊緣像素,而高于高閾值(threshold_high)的梯度幅值被認(rèn)為是強(qiáng)邊緣像素。因此該函數(shù)只需檢查弱邊緣像素是否與強(qiáng)邊緣像素相連接。
????????對(duì)于一般的圖像,可以以該閾值設(shè)置為參考去對(duì)比調(diào)整:
threshold_low?取圖像梯度幅值的平均值減一倍標(biāo)準(zhǔn)差
threshold_high 取圖像梯度幅值的平均值加一倍標(biāo)準(zhǔn)差
? ? ? ?通常情況下,較小的最小閾值會(huì)產(chǎn)生更多的弱邊緣,因此它可能會(huì)導(dǎo)致更多的總邊緣數(shù)量,但同時(shí)也可能會(huì)增加邊緣的錯(cuò)誤檢測(cè)率。較高的最大閾值可以消除由于噪聲引起的邊緣,從而可以提高邊緣檢測(cè)的精度,并減少誤判率。但是,較高的最大閾值也可能會(huì)將實(shí)際的邊緣誤判為非邊緣,從而導(dǎo)致錯(cuò)過一些邊緣。
? ? ? ??threshold_low?取圖像梯度幅值的平均值減一倍標(biāo)準(zhǔn)差,threshold_high 取圖像梯度幅值的平均值加一倍標(biāo)準(zhǔn)差,即threshold_low取-19.59,threshold_high取62.92的效果,如圖6所示。

threshold_low取-5,threshold_high取75的效果,如圖7所示。

threshold_low取-5,threshold_high取100的效果,如圖8所示。

??threshold_low取-5,threshold_high取85的效果,如圖9所示。

????需要注意的是,在實(shí)現(xiàn)算法過程中應(yīng)該對(duì)圖像進(jìn)行拷貝,否則將會(huì)修改本地圖片。代碼如下。
def double_threshold_discrete(out_edges,threshold_low,threshold_high): # 雙閾值法進(jìn)行邊緣連接
? ? out_pic = out_edges.copy()
? ? row, col = np.shape(out_pic)
? ? thresholded_mag = np.zeros((row, col))
? ? # 將滿足高閾值的像素點(diǎn)設(shè)為強(qiáng)邊緣像素點(diǎn)
? ? strong_i, strong_j = np.where(out_pic >= threshold_high)
? ? thresholded_mag[strong_i, strong_j] = 1
? ? # 將滿足低閾值的像素點(diǎn)設(shè)為弱邊緣像素點(diǎn)
? ? weak_i, weak_j = np.where((out_pic <= threshold_high) & (out_edges >= threshold_low))
? ? # 使用連通域方法連接弱邊緣像素點(diǎn),得到最終的邊緣圖像
? ? edge_map = np.zeros((row, col))
? ? for k in range(len(weak_i)):
? ? ? ? # 如果與確定為邊緣的像素點(diǎn)鄰接,則判定為邊緣;否則為非邊緣
? ? ? ? i = weak_i[k]
? ? ? ? j = weak_j[k]
? ? ? ? if np.any(thresholded_mag[i-1:i+2, j-1:j+2]):
? ? ? ? ? ? edge_map[i, j] = 1
? ? return edge_map.astype(np.uint8)

?????曲線擬合實(shí)現(xiàn)函數(shù)如下,擬合后的效果如圖11所示。由于是擬合的結(jié)果,會(huì)避免不了地出現(xiàn)重影模糊的情況,不推薦使用。
def double_threshold_spline(out_edges, threshold_low, threshold_high, s=500):
? ? # 使用雙閾值法進(jìn)行邊緣連接,得到離散的邊緣點(diǎn)
? ? edge_map = double_threshold_discrete(out_edges, threshold_low, threshold_high)
? ? edge_points = np.argwhere(edge_map == 1)
? ? # 使用樣條插值進(jìn)行邊緣曲線擬合
? ? tck, u = splprep(edge_points.T, s=s, per=1)
? ? # 確定邊緣曲線上的樣本點(diǎn)
? ? x, y = splev(u, tck)
? ? # 將曲線上的像素點(diǎn)在圖像上標(biāo)出
? ? out_pic = out_edges.copy()
? ? for i in range(len(x)):
? ? ? ? out_pic[int(x[i]), int(y[i])] = 255
? ? return out_pic

六 、完整代碼
import CV2 as cv
import numpy as np
from scipy.interpolate import splprep, splev
def cv_show(name,image): # 圖像顯示
? ? cv.imshow('name',image)
? ? cv.waitKey(0)
def add_salt_and_pepper_noise(image, ratio): # 椒鹽噪聲
? ? noisy = np.copy(image)
? ? num_salt = int(ratio * np.size(image)) # 計(jì)算需要添加的椒鹽像素個(gè)數(shù):0.05*291600
? ? coords_row = np.random.randint(0, image.shape[0] - 1, size=num_salt)
? ? coords_col = np.random.randint(0, image.shape[0] - 1, size=num_salt)
? ? # 一共生成num_salt個(gè)隨機(jī)數(shù),即num_salt個(gè)椒鹽噪聲位置
? ? coords = np.vstack((coords_row,coords_col))
? ? # 用vstack將椒鹽噪聲的行坐標(biāo)列坐標(biāo)組合成一個(gè)二維數(shù)組
? ? noisy[tuple(coords)] = 0 # 在原始圖像上將coords的位置像素點(diǎn)變?yōu)楹谏?/span>
? ? # 用tuple進(jìn)行操作,是因?yàn)閠uple是一種不可改變的數(shù)據(jù)類型,能保證添加椒鹽噪聲后原始圖像不會(huì)改變
? ? return noisy
def median_filter(image, kernel_size): # 中值濾波
? ? row, col = image.shape # 圖像的行高和列寬
? ? kernel_radius = kernel_size // 2 # 計(jì)算 kernel 的半徑
? ? median_image = np.zeros((row, col), np.uint8) # 初始化輸出圖像
? ? # 遍歷每個(gè)像素點(diǎn),進(jìn)行中值濾波
? ? for y in range(kernel_radius, row - kernel_radius): # range左閉右開
? ? ? ? for x in range(kernel_radius, col - kernel_radius):
? ? ? ? ? ? kernel = image[y - kernel_radius:y + kernel_radius + 1, x - kernel_radius:x + kernel_radius + 1]
? ? ? ? ? ? # 獲取 kernel 區(qū)域內(nèi)的像素值
? ? ? ? ? ? median_image[y, x] = np.median(kernel) # 計(jì)算中位數(shù)并更新輸出圖像,注意坐標(biāo)y,x
? ? return median_image
def Sobel(image): # 利用Sobel算子計(jì)算每個(gè)像素點(diǎn)的梯度和梯度方向
? ? # 默認(rèn)窗口大小為3*3
? ? Gx = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]])
? ? Gy = np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]])
? ? row, col = image.shape
? ? gradients = np.zeros([row - 2, col - 2])
? ? direction = np.zeros([row - 2, col - 2])
? ? for i in range(row - 2): # range左閉右開,從0到row-3
? ? ? ? for j in range(col - 2): # range左閉右開,從0到col-3
? ? ? ? ? ? dx = np.sum(image[i:i+3, j:j+3] * Gx) # 進(jìn)行卷積運(yùn)算
? ? ? ? ? ? dy = np.sum(image[i:i+3, j:j+3] * Gy)
? ? ? ? ? ? gradients[i, j] = np.sqrt(dx ** 2 + dy ** 2) # 計(jì)算每一點(diǎn)的梯度
? ? ? ? ? ? if dx == 0:
? ? ? ? ? ? ? ? direction[i, j] = np.pi / 2 # 若梯度在y軸上,那么方向夾角為90°,由于dx在分母上,因此需要單獨(dú)討論
? ? ? ? ? ? else:
? ? ? ? ? ? ? ? direction[i, j] = np.arctan(dy / dx) # 其余情況方向角的計(jì)算代入公式即可
? ? gradients = np.uint8(gradients) # 由于像素的值都在0到255之間,因此需要將數(shù)值存儲(chǔ)在8位的整型數(shù)組中
? ? return gradients, direction
def non_maximum_suppression(magnitude, orientation): # 非極大值抑制算法
? ? # magnitude:梯度強(qiáng)度矩陣 orientation:梯度方向矩陣
? ? row, col = magnitude.shape? # 獲取尺寸信息
? ? out_edges = np.zeros_like(magnitude)? # 初始化輸出矩陣
? ? # 每個(gè)像素和周圍像素點(diǎn)進(jìn)行比較
? ? for i in range(1, row - 1): # 該算法是要比較中間像素點(diǎn)和周圍八個(gè)像素點(diǎn)的數(shù)值大小,因此需要去除首行和尾行
? ? ? ? for j in range(1, col - 1): # 去除首列和尾列
? ? ? ? ? ? angle = orientation[i, j] # 對(duì)每個(gè)像素點(diǎn)的梯度方向進(jìn)行比較
? ? ? ? ? ? if angle < 0: # 將負(fù)值角度翻轉(zhuǎn)到x軸上方討論
? ? ? ? ? ? ? ? angle += np.pi
? ? ? ? ? ? if (angle <= np.pi/8 or angle >= 7*np.pi/8) :? # 離散成0度和180度的梯度方向
? ? ? ? ? ? ? ? if magnitude[i, j] > magnitude[i, j - 1] and magnitude[i, j] > magnitude[i, j + 1]: # 只檢查左右兩個(gè)像素點(diǎn)的數(shù)值
? ? ? ? ? ? ? ? ? ? out_edges[i, j] = magnitude[i, j]
? ? ? ? ? ? elif (angle > np.pi/8 and angle <= 3*np.pi/8) or (angle >= 5*np.pi/8 and angle < 7*np.pi/8): # 離散成45度和135的梯度方向
? ? ? ? ? ? ? ? if magnitude[i, j] > magnitude[i - 1, j - 1] and magnitude[i, j] > magnitude[i + 1, j + 1]: # 只檢測(cè)對(duì)角像素點(diǎn)的數(shù)值
? ? ? ? ? ? ? ? ? ? out_edges[i, j] = magnitude[i, j]
? ? ? ? ? ? elif (angle > 3*np.pi/8 and angle < 5*np.pi/8) : # 離散成90度或270度的梯度方向
? ? ? ? ? ? ? ? if magnitude[i, j] > magnitude[i - 1, j] and magnitude[i, j] > magnitude[i + 1, j]: # 只檢測(cè)上下兩個(gè)像素點(diǎn)的數(shù)值
? ? ? ? ? ? ? ? ? ? out_edges[i, j] = magnitude[i, j]
? ? return out_edges
def double_threshold_discrete(out_edges,threshold_low,threshold_high): # 雙閾值法進(jìn)行邊緣連接
? ? out_pic = out_edges.copy()
? ? row, col = np.shape(out_pic)
? ? thresholded_mag = np.zeros((row, col))
? ? # 將滿足高閾值的像素點(diǎn)設(shè)為強(qiáng)邊緣像素點(diǎn)
? ? strong_i, strong_j = np.where(out_pic >= threshold_high)
? ? thresholded_mag[strong_i, strong_j] = 1
? ? # 將滿足低閾值的像素點(diǎn)設(shè)為弱邊緣像素點(diǎn)
? ? weak_i, weak_j = np.where((out_pic <= threshold_high) & (out_edges >= threshold_low))
? ? # 使用連通域方法連接弱邊緣像素點(diǎn),得到最終的邊緣圖像
? ? edge_map = np.zeros((row, col))
? ? for k in range(len(weak_i)):
? ? ? ? # 如果與確定為邊緣的像素點(diǎn)鄰接,則判定為邊緣;否則為非邊緣
? ? ? ? i = weak_i[k]
? ? ? ? j = weak_j[k]
? ? ? ? if np.any(thresholded_mag[i-1:i+2, j-1:j+2]):
? ? ? ? ? ? edge_map[i, j] = 1
? ? return edge_map.astype(np.uint8)
def double_threshold_spline(out_edges, threshold_low, threshold_high, s=500):
? ? # 使用雙閾值法進(jìn)行邊緣連接,得到離散的邊緣點(diǎn)
? ? edge_map = double_threshold_discrete(out_edges, threshold_low, threshold_high)
? ? edge_points = np.argwhere(edge_map == 1)
? ? # 使用樣條插值進(jìn)行邊緣曲線擬合
? ? tck, u = splprep(edge_points.T, s=s, per=1)
? ? # 確定邊緣曲線上的樣本點(diǎn)
? ? x, y = splev(u, tck)
? ? # 將曲線上的像素點(diǎn)在圖像上標(biāo)出
? ? out_pic = out_edges.copy()
? ? for i in range(len(x)):
? ? ? ? out_pic[int(x[i]), int(y[i])] = 255
? ? return out_pic
image = cv.imread('D:\pythonProject2\canny_image.jpg')
image = cv.cvtColor(image, cv.COLOR_BGR2GRAY)
image = cv.resize(image, (928, 723))
# 輸入圖片以及需要添加噪聲點(diǎn)的比例
noisy_image = add_salt_and_pepper_noise(image,0.03)
# 中值濾波去噪
median_image = median_filter(noisy_image,3)
# 梯度強(qiáng)度和梯度方向
gradients,? direction = Sobel(median_image)
# 輸出初始邊緣檢測(cè)的圖像
# 統(tǒng)一化梯度幅值到 [0,255] 范圍內(nèi)
gradients_image = cv.normalize(gradients, dst=None, alpha=0, beta=255, norm_type=cv.NORM_MINMAX, dtype=cv.CV_8UC1)
# 非極大值抑制算法
nms = non_maximum_suppression(gradients,direction)
# 離散邊緣檢測(cè)圖像
threshold_low = np.mean(gradients) - np.std(gradients)
threshold_high = np.mean(gradients) + np.std(gradients)
# print(threshold_low)
# print(threshold_high)
Output_Image_Discrete = double_threshold_discrete(nms,-5,85)
Output_Image_Discrete = Output_Image_Discrete * 255
# 使用樣條插值擬合出邊緣曲線
Output_Image_continuous = double_threshold_spline(nms,10,85)
cv.imshow('initial_image',image)
cv.imshow('noisy_image',noisy_image)
cv.imshow('median_image',median_image)
cv.imshow('gradients_image',gradients_image)
cv.imshow('nms_image',nms)
cv.imshow('Output_Image_Discrete',Output_Image_Discrete)
cv.imshow('Output_Image_continuous',Output_Image_continuous)
cv.waitKey(0)
cv.destroyAllWindows()