在玩 3D 列印的過程中,我使用 OpenSCAD 來建模,有陣子在玩類似點陣圖的建模,例如基於圖片建立以下的模型:
OpenSCAD 本身沒有辦法取得讀取的圖片資料,因此我借助 OpenCV,將讀取圖片並轉為黑與白兩個值,這個動作稱為 Image Thresholding,中文常翻譯為圖片二值化,基本原理是先將圖片轉為灰階,然後指定一個門檻值,灰階度超過門檻的像素設為 255,低於門檻的像素設為 0。例如:
import cv2
import numpy as np
threshold = 127
img = cv2.imread('caterpillar.jpg', cv2.IMREAD_GRAYSCALE)
binary = np.where(img > threshold, 255, 0).astype('float')
cv2.imshow('caterpillar', binary)
cv2.waitKey(0)
cv2.destroyAllWindows()
這可以建立以下的圖片:
實際上,OpenCV 內建了 threshold
函式,用來實現二值化的需求,只不過 threshold
函式不單只做二值化,threshold
函式的第一個參數接受灰階圖片資料,第二個參數是門檻值,第三個參數是超過門檻時要設定的最大值,第四個參數是門檻的行為類型:
cv2.THRESH_BINARY
:過門檻設為最大值,否則設為 0。cv2.THRESH_BINARY_INV
:過門檻設為 0,否則設為最大值。cv2.THRESH_TRUNC
:過門檻設為門檻值,否則保持不變。cv2.THRESH_TOZERO
:過門檻設為 0,否則保持不變。cv2.THRESH_TOZERO_INV
:過門檻保持不變,否則設為 0。
threshold
函式傳回兩個值,第一個是設定的門檻,第二個是處理後的圖片資料,例如來寫個程式:
import cv2
import numpy as np
threshold = 127
img = cv2.imread('caterpillar.jpg', cv2.IMREAD_GRAYSCALE)
types = [
cv2.THRESH_BINARY,
cv2.THRESH_BINARY_INV,
cv2.THRESH_TRUNC,
cv2.THRESH_TOZERO,
cv2.THRESH_TOZERO_INV
]
titles = ['BINARY', 'BINARY_INV', 'TRUNC', 'TOZERO', 'TOZERO_INV']
cv2.imshow('caterpillar', img)
for i in range(len(types)):
_, thresh = cv2.threshold(img, threshold, 255, types[i])
cv2.imshow(titles[i], thresh)
cv2.waitKey(0)
cv2.destroyAllWindows()
示範一下以上幾個設定的效果:
方才談到,threshold
函式傳回值會有兩個,第一個是門檻值,第二個才是處理後的圖片資料,為什麼要特別傳回門檻值?這不是自行設定的嗎?OpenCV 提供 Otsu's method,該方法為大津展之(Ōtsu Nobuyuki)提出,因此中文常翻譯為大津法、大津演算或大津二值化,它可以自動計算門檻,若使用大津二值化,透過 threshold
函式的傳回值就可以知道計算出來的門檻為何。
大津二值化的原理,是逐步增加灰階度,將圖片大於與小於灰階度的像素畫分為兩群,看看哪個灰階度下,基於兩群像素的灰階度平均差的平方會是最大,接著使用該灰階度來作為門檻,想要結合大津二值化,只要增加 cv2.THRESH_OTSU
,而 threshold
函式的門檻參數只要設為 0 就可以了。例如:
import cv2
import numpy as np
threshold = 127
img = cv2.imread('caterpillar.jpg', cv2.IMREAD_GRAYSCALE)
types = [
cv2.THRESH_BINARY,
cv2.THRESH_BINARY_INV,
cv2.THRESH_TRUNC,
cv2.THRESH_TOZERO,
cv2.THRESH_TOZERO_INV
]
titles = ['BINARY', 'BINARY_INV', 'TRUNC', 'TOZERO', 'TOZERO_INV']
cv2.imshow('caterpillar', img)
for i in range(len(types)):
t, thresh = cv2.threshold(img, 0, 255, types[i] + cv2.THRESH_OTSU)
print(t) # 顯示門檻值
cv2.imshow(titles[i], thresh)
cv2.waitKey(0)
cv2.destroyAllWindows()
就上例來說,顯示的圖片會如下:
如果來源圖片光線分佈不平均,有的地方較明,有的地方較亮,使用 threshold
函式的話,效果可能並不理想,因為它會整個圖片使用單一門檻,例如,若故意將來源圖片做點光暈處理,然後進行二值化的話:
如果不想要二值化後,受到光線不平均的影響,可以選擇像素鄰近區塊,取區塊的灰階值平均作為門檻進行二值化,區塊若選擇適當,就有可能得到想要的結果。
對於這個需求,OpenCV 提供了 cv2.adaptiveThreshold
函式,第一個參數接受灰階圖片,第二個參數是最大值,第三個接受區塊處理方法:
cv2.ADAPTIVE_THRESH_MEAN_C
:取區塊的灰階值平均後,減去指定常數作為門檻。cv2.ADAPTIVE_THRESH_GAUSSIAN_C
:取區塊的灰階值計算高斯加權(Gaussian Weighted Sum)(也就是會做高斯模糊),再減去指定常數作為門檻。
第四個參數是二值化類型,cv2.adaptiveThreshold
函式只接受 cv2.THRESH_BINARY
與 cv2.THRESH_BINARY_INV
,第四個參數是鄰近區塊大小,第五個參數是要減去的常數,來看看 cv2.ADAPTIVE_THRESH_MEAN_C
的效果:
import cv2
import numpy as np
img = cv2.imread('caterpillar.jpg', cv2.IMREAD_GRAYSCALE)
types = [
cv2.THRESH_BINARY,
cv2.THRESH_BINARY_INV
]
titles = ['BINARY', 'BINARY_INV']
cv2.imshow('caterpillar', img)
for i in range(len(types)):
thresh = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_MEAN_C, types[i], 11, 2)
cv2.imshow(titles[i], thresh)
cv2.waitKey(0)
cv2.destroyAllWindows()