# 數位影像-邊緣偵測、線段偵測 :::spoiler 目錄 [TOC] ::: Canny Edge Detection (邊緣偵測) --- ### 高斯模糊化、轉灰階影像 會在canny中的第一個副函式sobel裡面呼叫,先高斯模糊再轉灰階 ![](https://i.imgur.com/RzC7JVF.png) 將kernel size的大小定義成 3 * 3 ,完成高斯模糊 ```python= def Turn_to_Grayscale(input_image): # 轉灰階 input_image = cv2.cvtColor(input_image, cv2.COLOR_BGR2GRAY) return input_image def GaussianBlur(input_image): # 模糊 / 平滑化 input_image = cv2.GaussianBlur(input_image, (3, 3), 0) return input_image ``` ### 取得每一個像素的梯度值和使用 atan 取得梯度方向 #### 先設三個空的二維陣列 ```python= def Done_SobelFilter(input_image): # 先模糊再灰階-------------------------- input_image = Turn_to_Grayscale(GaussianBlur(input_image)) # 賦值3個與圖片一樣大小的一個全部為零的二維 np.array------------------------------------------------------ sobel_dst = np.zeros(input_image.shape) after_sobel_x = np.zeros(input_image.shape) after_sobel_y = np.zeros(input_image.shape) ``` #### 利用梯度運算子計算梯度向量 * ==梯度運算子==是採用老師講義提供的 matrix_coeffient_x 偵測直線 matrix_coeffient_y 偵測橫線 ![](https://i.imgur.com/AuAmrnx.png =40%x) * 對影像中的每一個 Pixel,做sobel運算 * 依照對應項==相乘再相加==會得出以下結果(after_sobel_x、after_sobel_y) ![](https://i.imgur.com/q37eiD0.png =50%x) ```python= # 梯度運算子,各製作 x、y方向 3*3 的係數矩陣(要各自與輸入的圖片相乘),再取角度---------------------------- matrix_coeffient_x = np.array(([-1, 0, 1], [-2, 0, 2], [-1, 0, 1])) # 偵測直線 matrix_coeffient_y = np.array(([-1, -2, -1], [0, 0, 0], [1, 2, 1])) # 偵測橫線 #矩陣相乘,對應項先相乘再相加,填入正中間那格------------------------------------------------------------- for i in range(1, input_image.shape[0] - 1): for j in range(1, input_image.shape[1] - 1): after_sobel_x[i, j] = np.sum(np.multiply(input_image[i - 1 : i + 2, j - 1 : j + 2], matrix_coeffient_x)) after_sobel_y[i, j] = np.sum(np.multiply(input_image[i - 1 : i + 2, j - 1 : j + 2], matrix_coeffient_y)) ``` * ==取得每一個像素點的梯度值==的計算:從x方向跟y方向合起來可以得到**M(x, y)**(sobel_dst) 這裡用的是先平方(**power**)再相加、開根號(**sqrt**)的方式 ![](https://i.imgur.com/v9jeGDa.png =50%x) ```python= # 先平方相加再開根號(或是可以兩邊各取絕對值再相加),將x軸與y軸合併----------------------------------------- sobel_dst = np.sqrt(np.power(after_sobel_x, 2) + np.power(after_sobel_y, 2)) # 經過上面公式運算過後會有大於255的值出現,所以用比例把資料壓在255以下 sobel_dst = np.multiply(sobel_dst, 255.0 / sobel_dst.max()) ``` * 計算==梯度向量的方向==,可以由gx、gy求得 * 公式中的gx為變數after_sobel_x,gy為變數after_sobel_y * 是以下計算後的角度決定的(利用以下的公式,由tan反函數取得弧度,從x,y的找到theta角) * 這邊用的公式是numpy內建的**arctan**(反正切函數) -> 跟math的atan是做一樣的事。但算出的結果值為弧度 ![](https://i.imgur.com/2kXsdky.png =35%x) * 那==弧度==我們需要==轉換成角度==,就套python內建函式**rad2deg** ```python= theta = np.rad2deg(np.arctan2(after_sobel_y, after_sobel_x)) # 弧度轉角度,獲得theta角 ``` * 但因為有負的跟正的after_sobel_y、after_sobel_x,所以導致算出來的theta角度有負數 -> 找到theta小於0的地方,將==負數變成正數==(把小於0的地方全部都+180) * 轉換成正數是因為要為了方便後面的NMS處理可以有少一點的條件判斷 * 最後再用uint8這個型態回傳 ```python= theta[theta < 0] += 180 # 剛剛已將弧度轉換成的角度 return sobel_dst.astype('uint8'), theta After_Sobel_img, get_theta = Done_SobelFilter(image) ``` ### NMS(non maximum suppression) 去除假的邊緣 * 根據以下的講義圖來看,可以把梯度方向分成四個方向(水平、垂直、+45° 、 −45°) ![](https://i.imgur.com/GPgHYER.png =55%x) * 找到==梯度方向最大的那個梯度值留下==,**如果沒有比兩邊的像素點要大的話就設為0 -> "抑制"的動作** * 因為這邊是用一個新的陣列After_NMS去接他,所以只要把最大的存進去就好了,其餘應該要變成0的就不會再賦值進去 ![](https://i.imgur.com/qTWitiK.png =35%x) * 那就可以利用上面的想法找到該像素向上以及向下梯度方向的像素,並且比較這一些像素的梯度值的大小 ```python= def Non_Maximum_Suppression(input_image, theta): # 找最大的那個值-------------------------------------------------------------------------- After_NMS = np.zeros(input_image.shape) for i in range(1, input_image.shape[0] - 1): for j in range(1, input_image.shape[1] - 1): # 上下,左右,右上到左下,左上到右下 if (0 <= theta[i, j] < 22.5) or (157.5 <= theta[i, j] <= 180): # 垂直 value_to_compare = max(input_image[i, j - 1], input_image[i, j + 1]) elif (22.5 <= theta[i, j] < 67.5): # 左上到右下 value_to_compare = max(input_image[i - 1, j - 1], input_image[i + 1, j + 1]) elif (67.5 <= theta[i, j] < 112.5): # 水平 value_to_compare = max(input_image[i - 1, j], input_image[i + 1, j]) else: # 左下到右上 value_to_compare = max(input_image[i + 1, j - 1], input_image[i - 1, j + 1]) # 如果有比較大 -> 替換,將該點設為梯度方向的最大值 if input_image[i, j] >= value_to_compare: After_NMS[i, j] = input_image[i, j] # 同時乘上一個變數,像向量的概念 After_NMS = np.multiply(After_NMS, 255.0 / After_NMS.max()) return After_NMS After_NMS_img = Non_Maximum_Suppression(After_Sobel_img, get_theta) ``` ### 雙門檻(標記 weak、strong和 none edge),並使用 BFS 進行連通成份連接斷掉的邊界 * 把強度標示出來 * 弱邊(weak edge) -> 低門檻偵測的點 * 強邊(strong和 edge) -> 高門檻偵測的點 * 跟沒有邊(none edge = 0) ```python= def Double_Threshold(input_image, low, high): # 雙門檻值 # 先設定弱邊與強邊-------------------------------------------------------------------------- weak_pixel = 50 strong_pixel = 255 # 建一個可以存結果的 0 陣列---------------------------------------------------------------------- after_double_threshold = np.zeros(input_image.shape) ``` * 將input_image介於低門檻(傳進來副函式的變數low) ~ 高門檻(傳進來副函式的變數high)之間的index值,存回weak_x、weak_y(都是陣列) * 這邊使用的**where**套件,是去尋找符合條件np.where(條件)的地方,然後回傳其index ```python= # 找出 比門檻高 以及 界於低門檻和高門檻 的地方,其他的忽略不計-------------------------------------- weak_x, weak_y = np.where((input_image > low) & (input_image <= high)) strong_x, strong_y = np.where(input_image >= high) ``` * 將找到的點標示成弱邊跟強邊 ```python= # 將對應的門檻填入對應的門檻值----------------------------------------------------------------------- after_double_threshold[strong_x, strong_y] = strong_pixel # 邊緣點 after_double_threshold[weak_x, weak_y] = weak_pixel ``` * 利用==八個連通方向==進行==遍歷==,從左邊開始逆時針一個一個看 ``` (-1,-1)、(0,-1)、(1,-1) (-1, 0)、(0, 0)、(1, 0) (-1, 1)、(0, 1)、(1, 1) ``` ```python= dx = np.array((-1, -1, 0, 1, 1, 1, 0, -1)) dy = np.array((0, 1, 1, 1, 0, -1, -1, -1)) while len(strong_x): # 當還有東西的時候 x = strong_x[0] y = strong_y[0] strong_x = np.delete(strong_x, 0) # 每做一次刪掉一個 strong_y = np.delete(strong_y, 0) for direction in range(len(dx)): new_x = x + dx[direction] # 將新的x填進去 new_y = y + dy[direction] ``` * 如果找到一個new_x、new_y這個點,有在影像的邊界範圍內且是有**與強邊聯通的==弱邊==**,將此連通的弱邊**設成強邊**,然後再依照這個點繼續向下看 * 這邊使用的**append**是用來把new_x與new_y這個值新增進去strong_x與strong_y的陣列裡面的 * 最後會將上面便利過後所找到的所有強邊標記到一個新的二維陣列After_Double_T_img上,最後回傳canny edge的成品就可以按照格式印出了 ```python= if((new_x >= 0 & new_x < input_image.shape[0] & new_y >= 0 & new_y < input_image.shape[1]) and (after_double_threshold[new_x, new_y] == weak_pixel)): after_double_threshold[new_x, new_y] = strong_pixel np.append(strong_x, new_x) np.append(strong_y, new_y) # 如果不是強像素就填0 after_double_threshold[after_double_threshold != strong_pixel] = 0 return after_double_threshold After_Double_T_img = Double_Threshold(After_NMS_img, low, high) ``` Hough Transform (線段偵測) --- ### 將 xy 座標影像轉換至ϴρ 座標(霍夫空間)並繪出 * 一開始先需要轉換方程式,將通用的直線方程式 𝑎𝑥+𝑏𝑦+𝑐=0 轉換成 法線表示法 𝑥 cos𝜃+𝑦 sin𝜃−𝜌=0 * 𝜃 為直線與法向量的夾角 * 𝜌 為直線與原點距離 ![](https://i.imgur.com/1fXvBzn.png =45%x) * 由𝜃、𝜌組成的這個平面取名為accumulator -> 做投票用的 * 針對一個在x,y坐標系上的點,找到通過他的所有直線(因為在 (x, y)坐標系中的==線==對應到(𝜃, 𝜌)坐標系上的==點==),所以可以對應到𝜃, 𝜌坐標系上的點做累加 * 因為要配合霍夫空間圖的座標𝜃方向座標為 -90° 到 90°,所以在一開始先把範圍設定在此 ![](https://i.imgur.com/TUrpriL.png =35%x) * 但因為在cos、sin計算上來看的話我們需要用 0° 到 180°,所以在賦值、轉換角度變弧度的時候的時候就順便的把角度的 90° 加回去, * 角度變換弧度的方式為套件**deg2rad** * 轉換成正弦,為使用的正弦函數**sin** * 轉換成餘弦,為使用的餘弦函數**cos** * (對應下圖程式碼的第三行)再依據==對角距離公式== **D(影像對角距離) = sqrt(長平方 + 寬平方)** ,𝜌軸為負√2𝐷到正√2𝐷,rho_range這個變數為全部𝜌軸的一半 * 這裡的**round**是四捨五入到整數位 ```python= def Hough_Transport(canny_edge_img, delete_rate): # 建立座標圖的x軸與y軸大小範圍---------------------------------------------------------------- thetas_h = np.deg2rad(np.arange(-90.0, 90.0, 1)) # hough space x 座標 rho_range = round(math.sqrt(canny_edge_img.shape[0]**2 + canny_edge_img.shape[1]**2)) # 計算 y 座標(全部為rho)的一半,分為正負兩邊 ``` * 新增一個二維陣列accumulator(用來累加的),大小為兩倍的rho_range乘上theta的範圍(為180),一開始初使化為全部為0 * **zeros**是可以新增一個全部為零的陣列,其大小為後面的參數 ```python= # 創建兩種 hough space 圖,因為會在index[0] 差一個 rho_range ( 在這裡代表 90 度 ) --------- accumulator = np.zeros((2 * rho_range, len(thetas_h)), dtype = np.uint8) # 真正在做事的,但 theta 的範圍是 0 到 180 #accumulator_range = np.zeros((2 * rho_range, len(thetas_h)), dtype = np.uint8) # 顯示的hough space 為波浪形狀,theta 的範圍是 -90 到 90 ``` * 先利用原本canny edge後的結果(只有0與255)的圖,用**where**套件去找出圖片上哪個像素點的值為255(白色) -> 就是邊緣的點把他給抓出來 * 再==隨機減少點的數量==,加快運算的速度 * 計算一下刪除點的比例delete_rate,刪除總共的幾趴 -> 總共要刪除幾個點,比例算起來的小數點也做四捨五入到整數位 * 之後用**np.random.choice**隨機找要刪除的點,先存在一個變數內 * 然後用**delete**去除掉剛剛要刪除的點,之後就完成了隨機刪除的動作了 ```python= # 存邊緣的座標 pixel(y, x)--隨機減少點的數量--------------------------------------------------------------------------------- canny_edge_pixels = np.where(canny_edge_img == 255) # 原本canny結果後的二值圖(有0與255) -> 是255(白色) -> 代表是邊緣pixel delete_number = round(min(len(canny_edge_pixels[0]), len(canny_edge_pixels[1]))*delete_rate) # 計算要刪除點的個數 delete_random_point0 = np.random.choice(canny_edge_pixels[0], delete_number, replace = False) # delete_random_point0 為canny_edge_pixels[0]要刪除的點 delete_random_point1 = np.random.choice(canny_edge_pixels[1], delete_number, replace = False) # delete_random_point1 為canny_edge_pixels[1]要刪除的點 a_array = np.array(np.delete(canny_edge_pixels[0], delete_random_point0.astype('int64'))).astype('int64') # 刪除點 b_array = np.array(np.delete(canny_edge_pixels[1], delete_random_point1.astype('int64'))).astype('int64') ``` * 那之後就可以用**zip**去做互相配對的動作一個a_array配一個b_array,再轉成list型態 * 完成後就可以做累加的動作了 ```python= coordinates_of_canny = list(zip(a_array, b_array)) # zip為相互搭配,a_array[0] 對應 b_array[0], 以此類推 ``` * (下列範例的第4行)利用公式 x*cos𝜃 + y*sin𝜃 = 𝜌 代入變數,對累加器做每次+1的動作,就完成了 ![](https://i.imgur.com/owikNRe.png) ```python= # 用每一個得到的邊緣座標(y, x)去對應極座標的空間-------累積器單元----避免誤差-------------------------------------------------- for coordinate in range(len(coordinates_of_canny)): for theta_run in range(len(thetas_h)): # t為逐個的theta rho = int(round(coordinates_of_canny[coordinate][1] * cos_h[theta_run] + coordinates_of_canny[coordinate][0] * sin_h[theta_run])) # rho = xcos(t)+ysin(t) 原點到線的直線距離 # 做投票的動作-------------------------------------------------------------------------------------------------------- accumulator[rho, theta_run] += 1 return accumulator, accumulator_range, rhos_h, thetas_h # 霍夫轉換-------------------------------------------------------------------------------------------------------- accumulator, accumulator_have_add_rho_range, rhos_range, thetas_range = Hough_Transport(After_Canny_pic1, 0.5) # 呼叫函式 hough_pic = origin_pic1.copy() # 將彩色縮放過的原圖複製一份當作畫線的底圖 ``` ### 顯示霍夫結果 * 一開始先將原圖hough_img與名字name導進來 * ==將投票數過低的過濾掉==,用hough_max_rate的比例去用**where**找出最大的幾趴(投票數要超過多少才可以畫線顯示),所得到的edge_pixels[0]、edge_pixels[1]為index的x、y值,然後再把它用**zip**對應起來 ```python= def Show_About_Hough(accumulator_img, thetas_r, rhos_r, canny_img, hough_img, hough_space, hough_max_rate, name): hough_img = cv2.cvtColor(hough_img, cv2.COLOR_BGR2RGB) # 因為最後要印在plt上 name = Turn_to_Grayscale(name) # 高於這個門檻值的才把他列入最後要繪製線段的行列當中------------------------------------------------------ edge_pixels = np.where(accumulator_img > accumulator_img.max()*hough_max_rate) # 可更改 coordinates = list(zip(edge_pixels[0], edge_pixels[1])) ``` * 之後我們需要把==法線表示法 𝑥cos𝜃+𝑦sin𝜃−𝜌=0 轉換回去直線方程式𝑎𝑥+𝑏𝑦+𝑐=0== * 已知𝜃的值,所以可以回推 a=cos(𝜃)、b=sin(𝜃),𝜌為直線與原點的距離 ![](https://i.imgur.com/9F9rmbP.png =40%x) * coordinates[i][0]為𝜌 * coordinates[i][1]為𝜃 (範圍其實就是0-180) * 套入上面的ab後,x0為 𝜌*cos(𝜃) 、y0為𝜌*sin(𝜃) * 那因為是要求要畫一條直線,所以我們可以從(x0,y0)出發往正向與反向前進,找到(x1,y1)、(x2,y2)... * 再用**cv2.line**給頭尾的座標點去畫出直線 ```python= # Use line equation to draw detected line on an original image for i in range(0, len(coordinates)): a = np.cos(np.deg2rad(coordinates[i][1])) # 角度計算 b = np.sin(np.deg2rad(coordinates[i][1])) x0 = a*coordinates[i][0] # 起始點計算 y0 = b*coordinates[i][0] x1 = int(x0 + 1000*(-b)) # 座標點計算 y1 = int(y0 + 1000*(a)) x2 = int(x0 - 1000*(-b)) y2 = int(y0 - 1000*(a)) # 依照頭尾的x ,y座標會畫出線條------------------------------------------------------------ cv2.line(hough_img,(x1,y1),(x2,y2),(255,0,0),1) ``` * 最後將名字疊圖上去畫好線的原圖上,就完成了霍夫了 ```python= # 將名字貼到圖片左上角 white = np.array([0,0,0]) for i in range(len(name)): for j in range(len(name[i])): if name[i][j] < 200: # 疊圖 canny_img[i][j] = 255 hough_img[i][j] = white # 顯示最後的結果-------------------------------------------------------------------------------------------------- Show_About_Hough(accumulator, thetas_range, rhos_range, After_Canny_pic1, hough_pic, accumulator_have_add_rho_range, 0.9, name) ```