け日記

最近はPythonでいろいろやってます

OpenCV: 2値化

前回・前々回と引き続き、OpenCVを触っていきます。

2値化

画像はチャネル (グレー画像であれば1チャネル, RGB画像であれば3チャネル) ごとに階調を持っており、一般的に256階調になります。

これを2階調、つまり白・黒に変換する処理のことを2値化と言います。画像中の特定の際立った情報を抽出したいときに用いられ、特に文字認識タスクなどの前処理としてよく登場します。

変換処理は2段階で行います。

  1. 画像をグレースケール画像に変換
  2. 各画素に対して、閾値以上であれば白 (=255) 、閾値未満であれば黒 (=0) に変換する

このしきい値をどうやって決めるかによって、大きく2パターンに分類されます。 - 画像中で唯一の閾値を用いる (大局的閾値) - 画像中の画素ごとに異なる閾値を用いる (適応的閾値)

まず、今回使う画像を読み込み、グレースケールに変換します。

import numpy as np
import matplotlib.pyplot as plt
import cv2

# サンプル画像
input_file_path = "./yagi.jpg"

# 画像のロード
img = cv2.imread(input_file_path)
img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)  # グレースケールで読み込み
print(img.shape)  #  (400, 400)

f:id:ohke:20190817155636p:plain

大局的閾値による2値化

大局的閾値の場合、cv2.thresholdを使います。

ここでは閾値を127 (2番目の引数) に設定してます。

  • 3番目の引数 (maxval) は、変換後の最大値
  • 4番目の引数 (type) は、変換処理の種類でいくつか用意されてます
    • cv2.THREASH_BINARY_INVとすると白黒反転します
_, output = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)

全体的に暗めの写真なので、127で切ると少し暗い画像になります。

f:id:ohke:20190817160251p:plain

画素値の非ストルグラムをみますと、多くの画素は127より小さいことがわかります。
このように全体的に暗い (明るい) などで、画素値の偏りが大きい場合があり、固定の閾値ではうまく欲しい情報を抽出できなくなります。画素値の分布をプロットしてみるとよくわかります。

f:id:ohke:20190817161248p:plain

画素値の分布を考慮して閾値を決めるアルゴリズムの1つに、大津の2値化があります。
ある閾値で2つのクラスに分類したときに、クラス内の分散が最小となる閾値を選択する方法です。画素値の分布に双峰性があると仮定してます。

cv2.thresholdの3番目の引数で、通常のcv2.THRESH_BINARYcv2.THRESH_OTSUを加えることで、大津のアルゴリズムで閾値を設定してくれます。

1番目の返り値が設定された閾値で、87となってます。上の分布を見ると、たしかに2つの山のちょうど谷間で設定できてそうです。

th, output = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
print(th)  # 87.0

出力された画像を見ると、明るくなって目などは縁取られてます。

f:id:ohke:20190817161810p:plain

適応的閾値による2値化

同一の画像内でも、光の当たり具合などによってうまく欲しい情報を取り出せないことがあります。
大津の2値化画像を見ると、ヤギのおでこのあたりの輪郭に比べると、顎部分の輪郭はそれほど明確ではないです。地面の色が明るく、明暗差が小さいためです。

適応的閾値では、変換対象画素の周辺 (正方形領域) の画素値の分布によって閾値を決めます。これによって同一画像内 (より正確には全ての画素) で異なる閾値によって2値化します。

OpenCVではcv2.adaptiveThresholdによって提供されています。

  • 2番目の引数 (adaptiveMethod) で、閾値の計算方法を指定しています
    • cv2.ADAPTIVE_THRESH_MEAN_C: 周辺画素の平均値を閾値にする
    • cv2.ADAPTIVE_THRESH_GAUSSIAN_C: ガウス窓で重み付けされた値で平均値を計算されます
      • 近い画素ほど重要な情報であるため
  • 4番目の引数 (blockSize) で、閾値の計算で用いる周辺領域のサイズを指定
    • blockSize × blockSizeが周辺領域となる
  • 5番目の引数 (C) は閾値を下げる定数となっています (0以上の値が一般的)
output = cv2.adaptiveThreshold(
    img, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 101, 2
)

顎の部分に着目していただいても分かる通り、全体的に明暗差が強調され、輪郭や毛並みがはっきり現れるようになっています。

f:id:ohke:20190817164710p:plain

周辺領域が狭すぎると、全体的にギザギザした画像になります。101x101 -> 11x11で狭めたのが下の画像です。

output = cv2.adaptiveThreshold(
    img, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY,11, 0
)

f:id:ohke:20190817164741p:plain

引数Cを設定した例です。閾値が-5されますので、全体的に明るくなってます。

output = cv2.adaptiveThreshold(
    img, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 101, 5
)

f:id:ohke:20190817165206p:plain

cv2.ADAPTIVE_THRESH_GAUSSIAN_Cを試した例です。遠くの画素の影響を受けづらくなりあますので、周辺領域を狭くしたときに近いギザギザも見られます。

output = cv2.adaptiveThreshold(
    img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 101, 0
)

f:id:ohke:20190817165354p:plain

まとめ

OpenCVでいろいろな2値化を試しました。