はじめに
二値化の方法として、cv2.threshold
関数を紹介しました。cv2.threshold
関数では大津、トライアングルの2つのアルゴリズムで閾値を自動的に決定する事ができましたが、画像全体に対して一意の閾値を決定するものでした。適応的閾値処理の二値化処理では、局所的な領域ごとに異なる閾値を適用することで、照明条件が不均一な画像に対して優れた二値化結果を提供します。このアルゴリズムは、各ピクセルの閾値をその周囲の領域の平均値や重み付き平均値といった統計的手法を用いて動的に調整します。
特に、画像に明るさのムラがある場合に効果を発揮する二値化プロセスと言えます。
本記事ではOpenCVで提供される適応的閾値処理の二値化処理のcv2.adaptiveThreshold
関数を紹介します。
適応的閾値処理
cv2.adaptiveThreshold
関数のアルゴリズムをOpenCVのソースコードも参考にしながら確認しました。
アルゴリズム
cv2.adaptiveThreshold
関数で採用されているアルゴリズムは、入力画像の各ピクセルに対して、その周囲の一定領域内のピクセルの値に基づいて閾値を算出するものです。閾値の算出方法は、以下の2種類から選択できます。
- cv2.ADAPTIVE_THRESH_MEAN_C:周囲の領域内のピクセル値の平均値を閾値とします。
- cv2.ADAPTIVE_THRESH_GAUSSIAN_C:周囲の領域内のピクセル値の加重平均値を閾値とします。加重はガウス分布に従って計算されます。
具体的なアルゴリズムは、以下のとおりです。
- 入力画像の各ピクセルについて、その周囲の一定領域内のピクセルの値を取得する。
- 閾値の算出方法に応じて、領域内のピクセル値の平均値または加重平均値を計算する。
- 計算した閾値を、入力画像の該当ピクセルに適用する。
確認コード
実際に前述のアルゴリズムに沿ってコードを書いて、cv2.adaptiveThreshold
関数との比較をしました。
下のコードではadaptive_threshold
関数で上記の1〜3の処理を行っています。パラメーターの条件を同様にして、OpenCVのcv2.adaptiveThreshold
関数の結果と比較するサンプルコードとなっています。
閾値の算出方法は、周囲の領域内のピクセル値の平均値を閾値とするcv2.ADAPTIVE_THRESH_MEAN_C
、二値化の方法として画素値が閾値より大きい場合"255"とするcv2.THRESH_BINARY
を用います。
import cv2 import numpy as np import matplotlib.pyplot as plt def adaptive_threshold(src, blockSize, c): """ 適応的閾値処理アルゴリズムの確認関数 (前提条件 : thresholdType=cv2.THRESH_BINARY, adaptiveMethod=cv2.ADAPTIVE_THRESH_MEAN_C) Args: src: 入力画像 blockSize: 近傍領域のサイズ c: 閾値のオフセット """ # 入力画像のサイズを取得する rows, cols = src.shape[:2] # 出力画像を準備 dst = np.zeros((rows, cols), dtype=np.uint8) # boxFilter関数で局所的な領域の平均値を計算 mean = cv2.boxFilter(src, -1, (blockSize, blockSize), borderType=cv2.BORDER_REPLICATE|cv2.BORDER_ISOLATED) # 閾値を算出する threshold = mean - c # 閾値を適用する for x in range(0, cols): for y in range(0, rows): if src[y, x] >= threshold[y, x]: dst[y, x] = 0 else: dst[y, x] = 255 # 白黒反転 dst = cv2.bitwise_not(dst) return dst if __name__ == "__main__": # 入力画像を読み込む image = cv2.imread("image.jpg", cv2.IMREAD_GRAYSCALE) # 適応的閾値処理アルゴリズムの確認関数 dst = adaptive_threshold(image, 7, 20) # OpenCVのadaptiveThreshold関数 dst_cv = cv2.adaptiveThreshold(image, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 7, 20) # 入力画像と二値化画像を表示 plt.rcParams["figure.figsize"] = [12, 3.5] # 表示領域のアスペクト比を設定 title = "cv2.adaptiveThreshold: codevace.com" plt.figure(title) # ウィンドウタイトルを設定 plt.subplots_adjust(left=0.02, right=0.98, bottom=0.02, top=0.98) # 余白を設定 plt.subplot(131) # 1行3列の1番目(左)の領域にプロットを設定 plt.imshow(image, cmap='gray') # 画像をRGBで表示 plt.title('Source Image') # 画像タイトル設定 plt.axis("off") # 軸目盛、軸ラベルを消す plt.subplot(132) # 1行3列の2番目(真ん中)の領域にプロットを設定 plt.imshow(dst, cmap='gray') # 画像をRGBで表示 plt.title('adaptive_threshold') # 画像タイトル設定 plt.axis("off") # 軸目盛、軸ラベルを消す plt.subplot(133) # 1行3列の3番目(右)の領域にプロットを設定 plt.imshow(dst_cv, cmap='gray') # 画像をRGBで表示 plt.title('cv2.adaptiveThreshold') # 画像タイトル設定 plt.axis("off") # 軸目盛、軸ラベルを消す plt.show()
次の入力画像を使用してサンプルコードを動作させます。
入力画像の影を適応的閾値処理の二値化で取り除きます。
下の画像はサンプルコードの出力結果です。
左が入力画像、中央がadaptive_threshold
関数の出力結果、右はOpenCVのcv2.adaptiveThreshold
関数の出力結果です。中央と右の画像を比べると細部で違いはありますが、とてもよく似たものとなりました。
また、入力画像の影もほぼ取り除くことができました。
cv2.adaptiveThreshold
関数の内部プロセスについて、理解ができたのではないでしょうか。
cv2.adaptiveThreshold関数
OpenCVのcv2.adaptiveThreshold
関数の使い方について説明します。
cv2.adaptiveThreshold(入力画像, maxValue, adaptiveMethod, thresholdType, blockSize, C)
引数
名称 | 説明 |
入力画像(必須) | 処理対象の画像(8bitのグレースケール画像)。カラー画像を処理する場合は、事前にグレースケール変換が必要です。 |
maxValue(必須) | 二値化後に与える最大の値 |
adaptiveMethod(必須) | 閾値の計算方法 |
thresholdType(必須) | 二値化の手法 |
blockSize(必須) | 局所的な閾値を計算するための近傍領域のサイズ。奇数である必要があります。 |
C(必須) | 計算された閾値から引かれる定数。値が大きいほど、全体的な閾値が低くなります。 |
adaptiveMethod
(閾値の計算方法)は以下の値が指定できます。AdaptiveThresholdTypesを参照。
type | 説明 |
cv2.ADAPTIVE_THRESH_MEAN_C | 各領域の平均値を計算して閾値を決定します。周囲の画素値の平均が閾値になります。この手法は比較的シンプルで高速です。 |
cv2.ADAPTIVE_THRESH_GAUSSIAN_C | 各領域の画素値にガウス関数で重み付けし、重み付けされた平均値を閾値とします。ガウス関数により、周囲の画素に対して中心の画素への影響がより遠くまで及びます。これにより、より滑らかな結果が得られることがあります。 |
thresholdType
(二値化の手法)は以下の値が指定できます。ThresholdTypesを参照。
cv2.threshold関数と異なり、下記の2つのみが指定可能です。
type | 説明 |
cv2.THRESH_BINARY | 画素の輝度値が閾値を超える場合は maxval 、そうでない場合は 0 に設定。 |
cv2.THRESH_BINARY_INV | 画素の輝度値が閾値を超える場合は 0、そうでない場合は maxval に設定。 |
戻り値
cv2.adaptiveThreshold
関数の戻り値は閾値処理が適用された結果の画像です。
使い方
サンプルコードを紹介します。
入力画像はカラー画像ではなくグレースケール画像を対象としています。
blockSize
とCの値を変えて結果の比較をしました。また、同じ画像でcv2.threshold
関数でどの様な結果になるかも比較しています。
import cv2 import matplotlib.pyplot as plt # 入力画像を読み込む image = cv2.imread("image.jpg", cv2.IMREAD_GRAYSCALE) # OpenCVのadaptiveThreshold関数 maxValue = 255 C = 20 # blockSizeを可変 blockSize_3 = cv2.adaptiveThreshold(image, maxValue, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 3, C) blockSize_7 = cv2.adaptiveThreshold(image, maxValue, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 7, C) blockSize_11 = cv2.adaptiveThreshold(image, maxValue, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 15, C) blockSize = 7 # Cを可変 C_10 = cv2.adaptiveThreshold(image, maxValue, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, blockSize, 1) C_20 = cv2.adaptiveThreshold(image, maxValue, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, blockSize, 20) C_30 = cv2.adaptiveThreshold(image, maxValue, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, blockSize, 40) # cv2.threshold関数との比較 # Type: cv2.THRESH_BINARY, 閾値の指定: 127 retval, thresh_binary = cv2.threshold(image, 54, 255, cv2.THRESH_BINARY) # Type: cv2.THRESH_TRIANGLE, 閾値の指定は不要ですが任意の値として0を設定 retval, thresh_triangle = cv2.threshold(image, 0, 255, cv2.THRESH_TRIANGLE) # 入力画像と二値化画像を表示 plt.rcParams["figure.figsize"] = [12, 9] # 表示領域のアスペクト比を設定 title = "cv2.adaptiveThreshold: codevace.com" plt.figure(title) # ウィンドウタイトルを設定 plt.subplots_adjust(left=0.02, right=0.98, bottom=0.02, top=0.98) # 余白を設定 plt.subplot(331) # 3行3列の1番目(上段左)の領域にプロットを設定 plt.imshow(image, cmap='gray') # 画像をグレースケールで表示 plt.title('Source Image') # 画像タイトル設定 plt.axis("off") # 軸目盛、軸ラベルを消す plt.subplot(332) # 3行3列の2番目(上段中央)の領域にプロットを設定 plt.imshow(thresh_binary, cmap='gray') # 画像をグレースケールで表示 plt.title('cv2.THRESH_BINARY, thresh=54') # 画像タイトル設定 plt.axis("off") # 軸目盛、軸ラベルを消す plt.subplot(333) # 3行3列の3番目(上段右)の領域にプロットを設定 plt.imshow(thresh_triangle, cmap='gray') # 画像をグレースケールで表示 plt.title('cv2.THRESH_TRIANGLE') # 画像タイトル設定 plt.axis("off") # 軸目盛、軸ラベルを消す plt.subplot(334) # 3行3列の4番目(中段左)の領域にプロットを設定 plt.imshow(blockSize_3, cmap='gray') # 画像をグレースケールで表示 plt.title('blockSize=3, C=20') # 画像タイトル設定 plt.axis("off") # 軸目盛、軸ラベルを消す plt.subplot(335) # 3行3列の5番目(中段中央)の領域にプロットを設定 plt.imshow(blockSize_7, cmap='gray') # 画像をグレースケールで表示 plt.title('blockSize=7, C=20') # 画像タイトル設定 plt.axis("off") # 軸目盛、軸ラベルを消す plt.subplot(336) # 3行3列の6番目(中段右)の領域にプロットを設定 plt.imshow(blockSize_11, cmap='gray') # 画像をグレースケールで表示 plt.title('blockSize=11, C=20') # 画像タイトル設定 plt.axis("off") # 軸目盛、軸ラベルを消す plt.subplot(337) # 3行3列の7番目(下段左)の領域にプロットを設定 plt.imshow(C_10, cmap='gray') # 画像をグレースケールで表示 plt.title('blockSize=7, C=10') # 画像タイトル設定 plt.axis("off") # 軸目盛、軸ラベルを消す plt.subplot(338) # 3行3列の8番目(下段中央)の領域にプロットを設定 plt.imshow(C_20, cmap='gray') # 画像をグレースケールで表示 plt.title('blockSize=7, C=20') # 画像タイトル設定 plt.axis("off") # 軸目盛、軸ラベルを消す plt.subplot(339) # 3行3列の9番目(下段右)の領域にプロットを設定 plt.imshow(C_30, cmap='gray') # 画像をグレースケールで表示 plt.title('blockSize=7, C=30') # 画像タイトル設定 plt.axis("off") # 軸目盛、軸ラベルを消す plt.show()
blockSizeが小さいと線の輪郭が抽出され、大きくなるにつれて線全体が二値化されています。また、Cは大きくなるにつれてノイズが除去され、明るなる事がわかります。cv2.threshold
関数では影の部分が残ってしまったり、音符の一部が消えてしまい影だけを除去するのは難しいですね。
局所的に閾値を変えることで、影を除去することが可能である事が確認できました。
おわりに
cv2.adaptiveThreshold
関数は画像ごとに異なるblockSizeとCの値の選定が難しいので、場合によっては使いにくいことがあります。
都度、調整しながら使う場合では、容易に目的が達成できそうです。
blockSizeとCの値の選定方法として、グリッドサーチなどが使えるかもしれませんが、評価関数をよく考える必要がありそうです。
ご質問や取り上げて欲しい内容などがありましたら、コメントをお願いします。
最後までご覧いただきありがとうございました。
参考リンク
OpenCV: adaptiveThreshold
Applies an adaptive threshold to an array.
■(広告)OpenCVの参考書としてどうぞ!■