# 人工智慧深度學習概論與應用 HW1 重點整理 [ToC] ## 7 image point processing 1. Invert: $255-x$ 2. Darken: $x-128$ 3. Lighten: $x+128$ 4. Lower Contrast: $x/2$ 5. Raise Contrast: $x*2$ 6. Non-linear Lower Contrast: $(x/255)^{1/3}*255$ 7. Non-linear Raise Contrast: $(x/255)^{2/3}*255$ ### skill - np.Array 常數運算 ``` python # 加法 np.array([0, 1, 2, 3]) + 10 > array([10, 11, 12, 13]) # 減法 np.array([10, 11, 12, 13]) - 10 > array([0, 1, 2, 3]) # 乘法 np.array([0, 2, 4, 6]) * 2 > array([ 0, 4, 8, 12]) # 除法 (注意此處會轉換為 float64 型別) np.array([0, 2, 4, 6]) / 2 > array([0., 1., 2., 3.]) # 指數 np.array([1, 2, 3, 4]) ** 2 > array([ 1, 4, 9, 16]) ``` ### skill - np.Array 型別轉換 由作業範例可以知道,圖片讀取預設型別為 `uint8`,即 8-bits 的無號整數,下方舉例: 8-bits 無號整數 1111 1111 為 255 8-bits 有號整數 1111 1111 為 -1 因此若今天將一值為 255 的 pixel 上加 1,目標應為得到 256,可能再收斂回 255 但根據無號數的運算,加一後變成 (1) 0000 0000,即為 0,會造成與目標極大的差距 因此可以以下方法進行轉換: ```python # 圖片讀取預設為 uint8 A = np.array([1, 2, 3, 4], dtype='uint8') A > array([1, 2, 3, 4], dtype=uint8) # 運算時擴大整數容許範圍進行超過 0~255 運算 A = A.astype(dtype='int32') A > array([1, 2, 3, 4], dtype=int32) # 或轉換為浮點數 A = A.astype(dtype='float32') A > array([1., 2., 3., 4.], dtype=float32) # 記得輸出圖片(plt.imshow)時要轉換回 uint8 A = A.astype(dtype='uint8') A > array([1, 2, 3, 4], dtype=uint8) ``` ### skill - np.Array 範圍擷取 由上方可知,在最後要把數值轉換為 `uint8` 型別,且數值範圍為 0~255,經上方計算後由於我們讓數值大小範圍擴大,可能出現範圍外的數值,這裡最簡單的方式就是 1. 小於 0 -> 寫 0 2. 大於 255 -> 寫 255 因此可以以下方式實作: ```python A = np.array([256, 255, 0, -1], dtype='int32') A > array([256, 255, 0, -1], dtype='int32') # np.clip(src, lower_bound, upper_bound) A = np.clip(A, 0, 255) A > array([255, 255, 0, 0], dtype='int32') ``` ### skill - np.Array 圖片顯示 * 單一圖片顯示 ```python plt.imshow(image) plt.show() ``` * 多圖片顯示 ```python fig = plt.figure(figsize=(height, width)) axes = fig.subplots(2, 2) axes[0][0].imshow(img1) axes[0][1].imshow(img2) axes[1][0].imshow(img3) axes[1][1].imshow(img4) plt.show() ``` * 其他常用設定 ```python # 灰階 plt.imshow(image, cmap='gray') # 設定標題 plt.title(title) axes[i][j].set_title(title) # 關閉座標軸 axes[i][j].set_axis_off() # 儲存圖片 fig.savefig('pic.png') ``` :::info 可依需求在官方 document 找到更多可用調整函式 ::: ## Gaussian Filter 1. 取得相對應大小的 Gaussian Kernel,$x$ 及 $y$ 的值以中心點為 $(0, 0)$ * Sigma: ![](https://i.imgur.com/VULM9Wo.png) * Kernel: ![](https://i.imgur.com/swndawH.png) * 可得到如下 kernel ![](https://i.imgur.com/3ZUT9fb.png) 2. 以 kernel 對整張圖做卷積,即沿 $x$ 及 $y$ 每次位移一 pixel 求內積和成為新的 $(r,g,b)$ 值,當所有像素點皆重新運算後,則為完成 Guassian Blur 後的新圖。 3. Gaussian Blur 的概念可想像為將一個 pixel 周圍 $ksize*ksize$ 的像素點皆賦予一定權重(越靠近中央越重要,權重越大),並藉由加權求和使每個像素點新值皆提高和周遭像素的關聯,降低梯度,以達到模糊的效果。 4. 作業中 Convolution 的部分一個 if/else,第一個 case 是針對 border 的額外處理,第二個則是正常一個 kernel 可完整覆蓋對應圖塊的情況。 ### skill - np.array 初始宣告 ```python # 以 list 型別創建 np.array([0, 0, 0, 0]) # 給定 size, 以 0 做初始值 np.zeros((h, w)) # 2-dimension np.zeros((h, w, c)) # 3-dimension # 給定 size, 以 1 做初始值 np.ones((h, w)) # 2-dimension np.ones((h, w, c)) # 3-dimension ``` ### skill - python 整數整除語法 ```python # 普通除法 print(9 / 2) > 4.5 # 整除除法 print(9 // 2) > 4 ``` ### skill - BGR/RGB 通道轉換 ```python # 先 split 再 merge b,g,r = cv2.split(image) image = cv2.merge([r,g,b]) # 直接使用內建語法交換 image = image[:,:,::-1] ``` ## Downsampling Downsampling 是為了把大圖片資訊縮小,減少資訊量以利計算。 最基本的 downsampling 方式即是將 $size*size$ 的數個 pixel 以其中一個作為代表像素點,並濃縮為 $1*1$ 大小,以下為範例圖片: | 1 | 2 | 3 | 4 | 5 | 6 | | --- | --- | --- | --- | --- | --- | | 7 | 8 | 9 | 10 | 11 | 12 | | 13 | 14 | 15 | 16 | 17 | 18 | | 19 | 20 | 21 | 22 | 23 | 24 | | 25 | 26 | 27 | 28 | 29 | 30 | | 31 | 32 | 33 | 34 | 35 | 36 | 若以每 $2*2$ 大小取左上像素點為新像素點,即可 downsampling 為 $3*3$ 圖片如下: | 1 | 3 | 5 | | --- | --- | --- | | 13 | 15 | 17 | | 25 | 27 | 29 | ### skill - round / floor / ceil ```python from math import floor, ceil # 四捨五入 round(4.5) > 5 # 無條件捨去 floor(4.5) > 4 # 無條件進入 ceil(4.3) > 5 ``` ## Upsampling Upsampling 和 downsampling 相反,當一圖片尺寸過小時,可藉此手法將其放大,本次作業中又以兩種方式實作。 1. Nearest Neighbor Interpolation 此方式較為簡單,即是上方 downsampling 的反向,若一 $3*3$ 區塊被濃縮為 $1*1$ 的一值,則將其放大為 $3*3$ 並全部都給定該值,以下為範例圖片: | 1 | 3 | 5 | | --- | --- | --- | | 13 | 15 | 17 | | 25 | 27 | 29 | 將上圖以兩倍重新放大為 $6*6$ 如下: | 1 | 1 | 3 | 3 | 5 | 5 | | --- | --- | --- | --- | --- | --- | | 1 | 1 | 3 | 3 | 5 | 5 | | 13 | 13 | 15 | 15 | 17 | 17 | | 13 | 13 | 15 | 15 | 17 | 17 | | 25 | 25 | 27 | 27 | 29 | 29 | | 25 | 25 | 27 | 27 | 29 | 29 | 2. Bicubic Interpolation 原理詳見[論文](https://ieeexplore.ieee.org/document/1163711),助教補充 ppt 中有提供,以下直接敘述實作概念。 ![](https://i.imgur.com/pfKHJIl.png) ![](https://i.imgur.com/NU1VPqk.png) * 由於計算時需要使用以放大後各座標於原圖對應座標為中心的 $4*4$ 圖塊(中心點外圍 $5*5$ 的座標點),因此須先將原圖向外做 2 個 pixel 的 padding * 接著建立一個三層的 nested loop,分別為 RGB 三個通道、大圖的 X 方向整數座標、大圖的 Y 方向整數座標 * 計算當前大圖的 X, Y 座標分別對應至原圖的座標 ```python x = X / scale + 2 y = Y / scale + 2 ``` * 以 $(-1,-1),(0, 0),(1,1),(2,2)$ 由原圖座標對應的 pixel 起點進行偏移,並計算該點與對應原圖座標的距離,以下用範例解釋: | 0 | 0 | 0 | 0 | 0 | 0 | 0 | | --- | --- | --- | --- | --- | --- | --- | | 0 | 0 | 0 | 0 | 0 | 0 | 0 | | 0 | 0 | **1** | 3 | 5 | 0 | 0 | | 0 | 0 | 13 | 15 | 17 | 0 | 0 | | 0 | 0 | 25 | 27 | 29 | 0 | 0 | | 0 | 0 | 0 | 0 | 0 | 0 | 0 | | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 上圖為經過 padding 後的原圖,可以看到值為 1 的該 pixel 左上座標為 $(2, 2)$,假設欲放大八倍則會對應至新圖的 $(0,0)$,即 $(0/8+2, 0/8+2)$。同理,值為 3 的 pixel 左上座標於舊圖中為 $(2, 3)$,即 $(0/8+2, 8/8+2)$,此處可看出,在八倍大的新圖中,八個像素長等同於舊圖中的一個像素,換句話說,此法便是以原本的一個像素距離分為八段,並分別計算異值變為八個漸進像素,以降低像素間的梯度大小。 如下圖為新圖的最左上角,圖中的藍點在新圖中座標為 $(0,1)$,對應回原圖則為 $(2.125,2)$,而圖中整個紅框範圍皆會對應到原圖值為 1 的像素,因此除右側及下方邊線上座標外,其餘座標點的位移起始點皆會由原圖的像素座標 $(2,2)$ 開始。 ![](https://i.imgur.com/jeJGXXb.jpg) 以上可解釋如何從新圖中的座標換算至舊圖,接著重點來了,上方所述的偏移起點會是該對應原圖座標中的像素起點(左上),如新圖中的 $(0,0)$ 至 $(7,7)$ 座標會對應至原圖中 $(2,2)$ 至 $(2.875,2.875)$ 座標,並且都位於值為 1 的像素上,因此位移起點皆為原圖的 $(2,2)$ 座標。承上,可得四個目標點 $(1,1)$、$(2,2)$、$(3,3)$、$(4,4)$,接著再依序計算目標點與欲計算點圓點座標$X$、$Y$方向距離,可得 $[x1, x2, x3, x4]$ 及 $[y1, y2, y3, y4]^T$,再套用上方 u(s) 的公式並將 $a$ 帶 -0.5,即可求得矩陣乘式中的前後兩矩陣。 ```python x1 = abs(x - (floor(x) - 1)) x2 = abs(x - (floor(x) + 0)) x3 = abs(x - (floor(x) + 1)) x4 = abs(x - (floor(x) + 2)) . . . ``` * 最後 f 矩陣則為 $(x-1,y-1)$ 至 $(x+2,y+2)$ 的方形圖塊,將三者做矩陣乘法運算後最終會得一值,將其收斂至 0~255 後即為該計算像素點的新值。 ### [矩陣相乘參考資料](https://blog.csdn.net/zenghaitao0128/article/details/78715140) ## Canny Edge Detector 共拆分為五個步驟: 1. Gaussian Filter: 使圖相模糊化,降低像素梯度以求邊緣偵測最佳化 2. Sobel Operator: 計算梯度及方向,分離水平及垂直邊緣後再予以整合 3. Non-maximum Suppression: 取梯度方向上最大值像素,將輪廓精簡化 4. Threshoding Edges: 用像素值將現有輪廓分為 strong、weak 兩種,並刪除過小、不重要輪廓 5. Hysteresis: 找出與 strong edge 相連的所有輪廓,並刪除其他輪廓 ## Gaussian Filter (1-2) :::info 請直接複製 HW1-1 的 code 過來,並作適當更改即可 ::: :::danger 須注意此處圖片為灰階,不同於彩色圖片為三通道,此圖片陣列一個像素只以一個值代替。 ::: ## Sobel Operator ![](https://i.imgur.com/9gegwuQ.png) 索伯運算子用於計算一 $3*3$ 圖塊中梯度變化,若卷積結果值越大,該圖塊中梯度則較明顯且有 edge 存在,且由於其值較大,在結果的圖片中 edge 的部分會呈現相對高亮白色。 * 根據 sample code 定義的 `iterate_regions` 函式,可知道已有處理好的 $3*3$ 圖塊 `img_region` 陣列,可參照 [矩陣相乘參考資料](https://blog.csdn.net/zenghaitao0128/article/details/78715140) 直接使用 `*` 進行卷積並求和 ```python (Sx * img_region).sum() (Sy * img_region).sum() ``` * 求方向可參考本連結:[arctan() vs arctan2()](https://geo.libretexts.org/Courses/University_of_California_Davis/UCD_GEL_56_-_Introduction_to_Geophysics/Geophysics_is_everywhere_in_geology.../zz%3A_Back_Matter/Arctan_vs_Arctan2) ## Non-maximum Supression 在 Sobel Operator 運算中,除了計算出各像素點上梯度量,同時也求得該梯度方向。而在一條輪廓線的垂直切面上可能有數個像素點具有相同梯度方向,但卻有不同輪廓強度。因此 non-maximum supression 便是根據各個像素點的梯度方向,找出強度最強的像素點保留,使得最後的輪廓線更細更精準。 * 在 sobel 的部分中,已對 angle 取絕對值並收斂在 0~$pi$ 之間,即 0~180度,因此四個 case 條件可使用各像素的徑度分出所屬方向 * 確認方向後,該方向會有對應的兩側像素點,此時若當前像素點非三者最大值,則可將其設為 0 省略,否則保留 ## Threshoing Edges 此時我們已經有精簡的輪廓線,接著便是以兩個閥值判定各像素點是 strong 還是 weak edge,條件如下: 1. pixel >= maximum: Strong edge,值設為 maximum 2. minimum <= pixel < maximum: Weak edge,值設為 minumum 3. pixel < minimum: 捨棄,設為 0 ## Hysteresis 基本概念用兩句話概括: 1. 在 threshoing 中判定為 strong edge 的繼續保持為 strong edge 2. 有和 strong edge 相連的 weak edge 列為 strong edge,否則捨棄 * 實作上可先宣告一個和圖片大小相同的 `visited` 矩陣,初始為 0,訪問過後設為正值。以及另一個容器用於存取待訪問的像素點,以下簡稱為「待辦」 * 先使用兩層 nested loop 去跑所有像素點,若遇到已訪問或非 strong edge的則跳過 (continue) * 若遇到屬於 strong edge 的像素點,則檢查其四周是否存在 weak & strong edge 像素點,若有則將其加入待辦 * 用一個 while 迴圈持續執行至待辦清單為空,並且待辦中所有像素點都設為 strong edge 且處理周圍像素點方式等同 strong edge * 待辦為空後回到第二點的迴圈中繼續找 strong edge 直到圖片處理完畢