# 數位影像-邊緣偵測、線段偵測
:::spoiler 目錄
[TOC]
:::
Canny Edge Detection (邊緣偵測)
---
### 高斯模糊化、轉灰階影像
會在canny中的第一個副函式sobel裡面呼叫,先高斯模糊再轉灰階

將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 偵測橫線

* 對影像中的每一個 Pixel,做sobel運算
* 依照對應項==相乘再相加==會得出以下結果(after_sobel_x、after_sobel_y)

```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**)的方式

```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是做一樣的事。但算出的結果值為弧度

* 那==弧度==我們需要==轉換成角度==,就套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°)

* 找到==梯度方向最大的那個梯度值留下==,**如果沒有比兩邊的像素點要大的話就設為0 -> "抑制"的動作**
* 因為這邊是用一個新的陣列After_NMS去接他,所以只要把最大的存進去就好了,其餘應該要變成0的就不會再賦值進去

* 那就可以利用上面的想法找到該像素向上以及向下梯度方向的像素,並且比較這一些像素的梯度值的大小
```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
* 𝜃 為直線與法向量的夾角
* 𝜌 為直線與原點距離

* 由𝜃、𝜌組成的這個平面取名為accumulator -> 做投票用的
* 針對一個在x,y坐標系上的點,找到通過他的所有直線(因為在 (x, y)坐標系中的==線==對應到(𝜃, 𝜌)坐標系上的==點==),所以可以對應到𝜃, 𝜌坐標系上的點做累加
* 因為要配合霍夫空間圖的座標𝜃方向座標為 -90° 到 90°,所以在一開始先把範圍設定在此

* 但因為在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的動作,就完成了

```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(𝜃),𝜌為直線與原點的距離

* 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)
```