影像分割是圖像處理的基本技巧,本文將說明二值化閥值方法的基本原理,以及幾種進階的閥值定義方法,包括Otsu’s method、多重閥值、自適應閥值等數種算法,讓讀者在設計圖像分割算法的流程時,可以提升流程對新進圖片的適應程度。
目錄
- 為什麼需要自動化閥值?
- 二值化閥值篩選的基礎原理
- 自動定閥值的範例
- 用變異數決定二值化閥值的方法
- 多重閥值篩選的方法
- 結論
1. 為什麼需要自動化閥值?
圖像分割的二值化是一個常見的應用,但許多人對二值化的處理,是用土法煉鋼的方法,先人工選擇不同閥值進行測試,再挑選結果表現最好的當作閥值。
這樣的做法可以應付大部分的情況,但當我們想要大量使用在不同情境時,就需要減少人工決策的部分。並且,我們會希望設計的圖像處理流程對不同圖像有良好的適應性,而人工選擇的閥值會產生以下問題:
- 以人工進行閥值篩選時,不同人會出現不同的標準
- 多張圖像的閥值:通常會找一個最佳閥值用在所有圖像,但當多張圖像彼此的灰度值差異很大時,設定單一閥值就會導致分割效果不好。例如:多個鏡頭拍攝時,不同鏡頭間可能會有色調差異。
下面將說明如何將閥值篩選自動化的方法。
2. 二值化閥值篩選的基礎原理
透過繪製圖片像素的分布圖,找到灰度值不連續的位置作為閥值
圖 1(a) 是一張帶有漸層的圓形,而中間圓形的顏色較周遭顏色來的深,我們該怎樣把中間的圓提取出來?首先,我們可以將灰度值繪製成直方圖,看出哪些顏色在整張圖形占的比重較大,像圖 1(b) 的 0 到 60 附近有一個高峰,會對應到 圖1(a) 中圓內深色的部分;而90到220,則是 圖1(a) 圓外淺色的部分。所以只要將圖像分割的閥值,定在60至90之間,就可以完美的將中間的圓切開來,如 圖1(c) 是以75為閥值切割的結果。
### 執行環境 colab ###
import urllib.request
import numpy as np
from google.colab.patches import cv2_imshow
import matplotlib.pyplot as plt
###檔案讀取
req = urllib.request.urlopen('https://imgur.com/AX3EH43.jpg')
arr = np.asarray(bytearray(req.read()), dtype=np.uint8)
###彩色圖
img = cv2.imdecode(arr, -1)
cv2_imshow(img)
### 灰階圖:圖1a
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
cv2_imshow(img)
### 畫圖直方圖:圖1b
plt.figure(figsize=(10, 10))
plt.hist(img.ravel(),256,[0,256])
plt.show()
### 繪製 mask:圖1c
ret, dst = cv2.threshold(img, 75, 255, cv2.THRESH_BINARY)
cv2_imshow(255-dst)
上述的方法是手動觀察、找到最佳切割點的方法,但許多時候圖片的分布很難靠著觀察找到合適的切割點。舉例而言,圖2(a) 是好吃好吃流沙包,圖2(b) 是他的分布圖,從圖上來看可能會切在80、160、甚至想切在200也不能說錯,但我們需要每次判斷時標準都是一樣的;另外,當手上的圖像不是一張,而是百張、千張時,很難人工逐張判讀。此時自動化閥值的決策方法就非常重要,接著先示範一個簡單決定閥值的方法。
3. 自動定閥值的範例
以灰度值中位數附近的最低點為閥值
我們嘗試將閥值篩選過程自動化。圖1(a) 目標是要提取圖片中間的深色圓型,可以預期圖像的直方圖會是一個雙峰分布,而我們只要找到雙峰間的低點,就可以完成切割。這裡示範自動化設定的方法:
Step1:繪製圖像灰度值的直方圖
Step2:找到分布圖的中位數,如下圖紅線
Step3:透過中位數設定一個尋找閥值的區域,如下圖藍線間的區域
如下圖中位數在120附近,此時我們可以設定 120 ± 50 的範圍,做為搜尋的區間。
Step4:選擇區域內直方圖數值最小的位置,作為閥值
所以在 120 ± 50 的範圍內,我們找尋灰度值最小的數值,就可以自動找到90為切割位置。
自動化的目的,是希望閥值能自動適應不同的圖像,圖3. 是多張圖型的切割狀況,第一至第四列依序為原始圖像、灰階圖像、分布圖、以及切割結果。
4. 用變異數決定二值化閥值的方法
用某個閥值將圖像分成兩群時,使兩群間的變異最大,那就是最佳閥值!
上面的案例是用中位數找峰低點,這裡來講解要如何利用兩群資料的變異來進行分割。先說明組內變異的方法,概念是這樣的,如果我們切開兩群資料時,此時它們各自灰度值的變異數,相較其他切割閥值的變異數小,表示分割的兩群資料各自是很集中的,此時就會有好的分割效果。
- 先找一個閥值T,用這個T將像素切成兩群,如圖4. 紅線。
- 計算左邊黃色像素的變異數,得到σ₁
- 計算左邊黃色像素的變異數,得到σ₂
- 將 σ₁ 與 σ₂ 相加,得到σₜ
如果你有三個閥值T₁、T₂、T₃,你就可以得到三個變異數,σₜ₁、σₜ₂、σₜ₃,這時如果 σₜ₂ 是最小的,那閥值就是 T₂ 。而實際上,,算法會將所有可能的切割點位都掃過一遍,找到一個最佳的分割點 T。這個算法是讓組內變異 (within-group variation, wg)最小。
但除此之外,如果讓切開的兩群,彼此間的離散程度很大,也可以達到相同的目的。而這是使組間變異 (between-groups variance, bg )最大,也是Otsu所使用的算法:
剛剛我們嘗試分割 圖 3. 最右邊圓形時,無法得到好的結果,這裡分別使用組內變異、及組間變異閥值算法。組內變異的結果如 圖5(b);Otsu的結果如圖6(b),可以看到,兩種分割方法均能有效切割。
5. 多重閥值篩選的方法
很多情況,我們不只是想要進行二元切割,而是有多重分割的需求,這時要怎麼找到最佳閥值?以組內變異舉例,2組的閥值是將2組各別的變異數相加;而3組,其實就是3組變異數相加;而10組,就是10組相加。圖7. 為 2、3、4組的分割情形。
### 多元閥值分割
from skimage import data
from skimage.filters import threshold_multiotsu
req = urllib.request.urlopen('https://imgur.com/AX3EH43.jpg')
req = urllib.request.urlopen('https://i.imgur.com/vb8OTNd.jpg')
arr = np.asarray(bytearray(req.read()), dtype=np.uint8)
img = cv2.imdecode(arr, -1)
plt.imshow(img[...,[2, 1, 0]])
plt.show()
plt.hist(img_gray4.ravel(),256,[0,256])
plt.show()
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
### 你可以修改 threshold_multiotsu 的第二個參數,來決定要分割的群數
thresholds = threshold_multiotsu(img_gray, 2)
print(thresholds)
plt.figure(figsize=(5, 10))
plt.subplot(2, 1, 1)
plt.hist(img_gray.ravel(),256,[0,256])
plt.axvline(thresholds, color='r') # vertical
plt.subplot(2, 1, 2)
regions = np.digitize(img_gray, bins=thresholds)
plt.imshow(regions, cmap='gray'); plt.axis('off')
plt.show()
6. 結論
閥值篩選的自動化是將圖像處理系統化的一個重要元素,本文簡要的說明二值化閥值的基本原理,但上述範例都是在圖像完美的情況之下。但如果今天的圖像帶有雜訊、或是圖片有光源的差異,使用Otsu會發生什麼事,又要怎樣處理?下一篇文章,會說明一些常見汙染的圖像處理,並且說明進階的二值化處理方法。