圖片二值化


在玩 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_BINARYcv2.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()

圖片二值化