OpenCVの閾値処理をマスター!threshold
関数の全種類と使い方を徹底解説
はじめに
画像処理の世界へようこそ!OpenCVは、画像処理やコンピュータビジョンにおける強力なオープンソースライブラリです。その中でも、最も基本的かつ重要な処理の一つが「閾値処理(Thresholding)」です。
閾値処理とは何か?なぜ重要なのか?
閾値処理とは、非常にシンプルなアイデアに基づいています。画像の各ピクセルの値(輝度値)を、あらかじめ決めた「閾値(threshold)」と比較し、その結果に応じてピクセル値を2つの値(多くは黒か白)に置き換える処理です。この処理によって、グレースケールやカラーの画像を、白と黒だけの「二値画像」に変換することができます。
なぜこの単純な処理が重要なのでしょうか?
- 情報の単純化と強調: 画像から背景や不要な情報を取り除き、注目したいオブジェクトの形状や輪郭を際立たせることができます。これにより、後続の処理(物体検出、輪郭抽出、文字認識など)が格段に容易になります。
- ノイズの除去: 特定の輝度値を持つ微小なノイズを、背景色やオブジェクト色に統一することで、クリーンな画像を得ることができます。
- 計算コストの削減: 多階調の画像(例えば256段階のグレースケール)を2値(白と黒)に変換することで、扱うデータ量が大幅に減り、処理速度が向上します。
このように、閾値処理は画像解析の「前処理」として不可欠なステップであり、多くの高度な技術の土台となっています。
この記事で学べること
この記事では、OpenCVの閾値処理をマスターすることを目標に、以下の内容を網羅的に、そして実践的に解説していきます。
cv2.threshold
関数の基本: 閾値処理の基本的な考え方と、OpenCVでの関数の使い方を学びます。- 5つの基本閾値処理タイプ:
BINARY
,BINARY_INV
,TRUNC
,TOZERO
,TOZERO_INV
の全種類を、その理論、サンプルコード、結果の比較を通じて徹底的に理解します。 - 適応的閾値処理: 照明が不均一な画像にも対応できる強力な手法
cv2.adaptiveThreshold
を学びます。 - 自動閾値決定: 閾値を自分で設定する手間を省く「大津の二値化」と「Triangle法」という賢いアルゴリズムを紹介します。
- 実践的な応用例: ノイズ除去との組み合わせや、OCR(光学文字認識)の前処理など、実際のユースケースを見ていきます。
この記事を読み終える頃には、あなたは様々な状況に応じて最適な閾値処理を選択し、自在に操れるようになっているはずです。それでは、OpenCVの閾値処理の奥深い世界を探求していきましょう!
1. 閾値処理の基本
まずは、最も基本的なグローバル閾値処理と、それを実現するOpenCVの cv2.threshold
関数について学びましょう。
グローバル閾値処理とは?
グローバル閾値処理は、画像全体に対して単一の閾値を適用する手法です。画像内のすべてのピクセルが、同じ基準で評価されます。
その原理は非常にシンプルです。
- あるピクセルの輝度値
p
を取り出します。 - あらかじめ設定した閾値
T
と比較します。 - もし
p > T
なら、そのピクセルを白(または指定された最大値)にします。 - もし
p <= T
なら、そのピクセルを黒(0)にします。
この処理を画像の全ピクセルに対して行うことで、画像は白と黒の2色に分けられます。このシンプルさゆえに、高速に動作し、多くの場面で有効です。
OpenCV cv2.threshold
関数の基本構文
OpenCVでは、cv2.threshold
関数を使ってグローバル閾値処理を簡単に行うことができます。まずはその基本構文を見てみましょう。
python
retval, dst = cv2.threshold(src, thresh, maxval, type)
それぞれの引数と戻り値について詳しく見ていきます。
引数
src
: 入力画像です。通常、8ビットのシングルチャンネル画像(グレースケール画像)を指定します。カラー画像を指定することも可能ですが、その場合は各チャンネル(B, G, R)に対して独立に閾値処理が適用されます。意図した結果を得るためには、多くの場合、事前にグレースケール化することが推奨されます。thresh
: 閾値です。この値がピクセルの運命を分ける基準となります。0から255の間の値を指定します。maxval
: 最大値です。閾値処理の条件を満たしたピクセルに割り当てられる値です。二値化の場合、通常は白を表す255
を指定します。type
: 閾値処理の種類を指定するフラグです。これが最も重要な引数で、どのようなルールでピクセル値を変換するかを決定します。OpenCVには複数のタイプが用意されており、後ほど詳しく解説します。
戻り値
retval
: 使用された閾値です。これは、後述する「大津の二値化」や「Triangle法」のように閾値を自動で決定する手法を使った場合に特に重要になります。アルゴリズムによって計算された閾値がこの変数に格納されます。手動で閾値を指定した場合は、引数thresh
と同じ値が返されます。dst
: 出力画像です。閾値処理が適用された後の画像(NumPy配列)が格納されます。
基本的な使い方が分かったところで、次は type
引数で指定できる様々な閾値処理の種類を見ていきましょう。
2. threshold
関数の5つの基本タイプを徹底解説
cv2.threshold
関数の type
引数を変えることで、様々な方法で閾値処理を行うことができます。ここでは、最も基本的な5つのタイプを、それぞれの原理、コード例、そして結果を比較しながら詳しく解説します。
比較のために、以下のようなグラデーション画像を入力として使用します。この画像を使うと、各処理タイプの違いが視覚的に分かりやすくなります。
“`python
import cv2
import numpy as np
import matplotlib.pyplot as plt
サンプル用のグラデーション画像を生成
gradient_img = np.arange(256, dtype=np.uint8).reshape(1, 256)
gradient_img = np.repeat(gradient_img, 100, axis=0)
閾値と最大値を設定
thresh_value = 127
max_value = 255
プロット用の準備
fig, axes = plt.subplots(6, 1, figsize=(8, 12))
fig.tight_layout(pad=3.0)
元画像を表示
axes[0].imshow(gradient_img, cmap=’gray’)
axes[0].set_title(‘Original Gradient Image’)
axes[0].axis(‘off’)
この後、各タイプの結果をaxes[1]からaxes[5]にプロットしていきます。
“`
1. cv2.THRESH_BINARY
これは最も一般的で直感的な二値化処理です。
- 原理: ピクセルの値が閾値
thresh
より大きい場合はmaxval
に、そうでない場合は0
になります。 - 数式:
$$
dst(x, y) = \begin{cases}
maxval & \text{if } src(x, y) > thresh \
0 & \text{otherwise}
\end{cases}
$$ - ユースケース: 背景から前景オブジェクトをはっきりと分離したい場合に最適です。
サンプルコード
“`python
THRESH_BINARY
ret, thresh_binary = cv2.threshold(gradient_img, thresh_value, max_value, cv2.THRESH_BINARY)
axes[1].imshow(thresh_binary, cmap=’gray’)
axes[1].set_title(f’THRESH_BINARY (thresh={thresh_value})’)
axes[1].axis(‘off’)
“`
結果と考察
グラデーション画像に適用すると、127を境に画像が真っ黒と真っ白の2つの領域にくっきりと分かれます。これが最も基本的な二値化です。
2. cv2.THRESH_BINARY_INV
THRESH_BINARY
の白黒を反転させたバージョンです。INV
は “Inverted”(反転)を意味します。
- 原理: ピクセルの値が閾値
thresh
より大きい場合は0
に、そうでない場合はmaxval
になります。 - 数式:
$$
dst(x, y) = \begin{cases}
0 & \text{if } src(x, y) > thresh \
maxval & \text{otherwise}
\end{cases}
$$ - ユースケース: 暗い背景に明るいオブジェクトがある場合、オブジェクトを白で抽出したいときなどに使います。
THRESH_BINARY
の結果を単純に反転させたい場合に便利です。
サンプルコード
“`python
THRESH_BINARY_INV
ret, thresh_binary_inv = cv2.threshold(gradient_img, thresh_value, max_value, cv2.THRESH_BINARY_INV)
axes[2].imshow(thresh_binary_inv, cmap=’gray’)
axes[2].set_title(f’THRESH_BINARY_INV (thresh={thresh_value})’)
axes[2].axis(‘off’)
“`
結果と考察
THRESH_BINARY
とは逆に、127を境に真っ白と真っ黒の領域が入れ替わります。
3. cv2.THRESH_TRUNC
TRUNC
は “Truncate”(切り捨て)を意味します。この処理は二値化ではなく、上限値を設定するものです。
- 原理: ピクセルの値が閾値
thresh
より大きい場合は、その値をthresh
に置き換えます(切り捨てます)。閾値以下のピクセルは元の値のままです。 - 数式:
$$
dst(x, y) = \begin{cases}
thresh & \text{if } src(x, y) > thresh \
src(x, y) & \text{otherwise}
\end{cases}
$$ - ユースケース: 画像内の明るすぎる部分(白飛びなど)の輝度を抑えつつ、暗い部分の階調は保持したい場合などに使います。
サンプルコード
“`python
THRESH_TRUNC
ret, thresh_trunc = cv2.threshold(gradient_img, thresh_value, max_value, cv2.THRESH_TRUNC)
注意: TRUNCではmaxvalは使用されません
axes[3].imshow(thresh_trunc, cmap=’gray’)
axes[3].set_title(f’THRESH_TRUNC (thresh={thresh_value})’)
axes[3].axis(‘off’)
“`
結果と考察
グラデーションを見ると、0から127までは元のグラデーションが維持され、127を超えた部分はすべて輝度値127の灰色で塗りつぶされます。maxval
の値は結果に影響しない点に注意してください。
4. cv2.THRESH_TOZERO
“To Zero” の名の通り、閾値を下回るピクセルを0(黒)にする処理です。
- 原理: ピクセルの値が閾値
thresh
より大きい場合は元の値のまま、そうでない場合は0
になります。 - 数式:
$$
dst(x, y) = \begin{cases}
src(x, y) & \text{if } src(x, y) > thresh \
0 & \text{otherwise}
\end{cases}
$$ - ユースケース: 画像内の暗い領域(ノイズなど)を完全に除去しつつ、明るい領域の階調情報は保持したい場合に有効です。
サンプルコード
“`python
THRESH_TOZERO
ret, thresh_tozero = cv2.threshold(gradient_img, thresh_value, max_value, cv2.THRESH_TOZERO)
注意: TOZEROではmaxvalは使用されません
axes[4].imshow(thresh_tozero, cmap=’gray’)
axes[4].set_title(f’THRESH_TOZERO (thresh={thresh_value})’)
axes[4].axis(‘off’)
“`
結果と考察
0から127までの領域は真っ黒になり、127を超える領域は元のグラデーションがそのまま残ります。THRESH_TRUNC
と同様に、maxval
は使用されません。
5. cv2.THRESH_TOZERO_INV
THRESH_TOZERO
の逆バージョンです。
- 原理: ピクセルの値が閾値
thresh
より大きい場合は0
に、そうでない場合は元の値のままです。 - 数式:
$$
dst(x, y) = \begin{cases}
0 & \text{if } src(x, y) > thresh \
src(x, y) & \text{otherwise}
\end{cases}
$$ - ユースケース: 明るすぎる領域を無視したい場合に利用できます。
サンプルコード
“`python
THRESH_TOZERO_INV
ret, thresh_tozero_inv = cv2.threshold(gradient_img, thresh_value, max_value, cv2.THRESH_TOZERO_INV)
注意: TOZERO_INVではmaxvalは使用されません
axes[5].imshow(thresh_tozero_inv, cmap=’gray’)
axes[5].set_title(f’THRESH_TOZERO_INV (thresh={thresh_value})’)
axes[5].axis(‘off’)
全てのプロットを表示
plt.show()
“`
結果と考察
THRESH_TOZERO
とは逆に、0から127までの領域は元のグラデーションが残り、127を超える領域が真っ黒になります。
これらの5つの基本タイプを理解することで、threshold
関数の基本的な振る舞いをマスターできます。しかし、実世界の画像は常に理想的とは限りません。次に、より複雑な状況に対応するための「適応的閾値処理」を見ていきましょう。
3. 適応的閾値処理: adaptiveThreshold
関数
グローバル閾値処理の限界
これまで見てきたグローバル閾値処理は、画像全体で照明条件が均一な場合には非常にうまく機能します。しかし、下のような画像ではどうでしょうか?
- 写真の片側に影が落ちている
- 紙の文書をスキャンした際に、中心が明るく、端が暗くなっている
- 光源がオブジェクトの近くにあり、照明にムラがある
このような画像に単一の閾値(グローバル閾値)を適用すると、問題が生じます。明るい領域では適切に二値化できても、暗い領域ではオブジェクトが背景に埋もれてしまったり、逆に暗い領域に閾値を合わせると、明るい領域が真っ白になってしまったりします。
この問題を解決するのが「適応的閾値処理(Adaptive Thresholding)」です。
適応的閾値処理とは?
適応的閾値処理のアイデアは、「閾値は画像全体で一つである必要はない」というものです。ピクセルごとに、その周辺の小さな領域の特性を分析し、局所的な(ローカルな)閾値を動的に決定します。
これにより、画像のある部分が明るく、別の部分が暗くても、それぞれの場所に応じた最適な閾値が適用されるため、照明の変化に非常に強い二値化が可能になります。
OpenCV cv2.adaptiveThreshold
関数の使い方
OpenCVでは、cv2.adaptiveThreshold
関数でこの処理を実装できます。
python
dst = cv2.adaptiveThreshold(src, maxValue, adaptiveMethod, thresholdType, blockSize, C)
引数が少し増えましたが、一つずつ見ていきましょう。
src
: 入力画像(8ビットグレースケール画像)。maxValue
: 閾値条件を満たしたピクセルに割り当てる最大値(通常は255
)。adaptiveMethod
: 適応的閾値を計算する方法を指定します。2つの選択肢があります。cv2.ADAPTIVE_THRESH_MEAN_C
: 近傍領域の平均値を閾値として使用します。cv2.ADAPTIVE_THRESH_GAUSSIAN_C
: 近傍領域のガウシアン加重平均を閾値として使用します。中心に近いピクセルほど大きな重みを持つため、より自然な結果になりやすいです。
thresholdType
: 閾値処理のタイプ。cv2.THRESH_BINARY
またはcv2.THRESH_BINARY_INV
のどちらかしか指定できません。blockSize
: 閾値を計算するための近傍領域のサイズ。3, 5, 7, ...
といった奇数を指定する必要があります。このサイズが大きいほど、より広い範囲のピクセルが閾値計算に考慮されます。C
: 定数。adaptiveMethod
で計算された平均値または加重平均からこの値を引きます。この値が最終的な閾値となります (閾値 = 平均値 - C
)。微調整のためのパラメータで、正の値にも負の値にもできます。
ADAPTIVE_THRESH_MEAN_C
vs ADAPTIVE_THRESH_GAUSSIAN_C
照明にムラのある文書画像の例で、グローバル閾値処理と2種類の適応的閾値処理を比較してみましょう。
“`python
import cv2
import numpy as np
import matplotlib.pyplot as plt
照明ムラのある画像を想定(擬似的に生成)
img = cv2.imread(‘path/to/your/document_image.png’, cv2.IMREAD_GRAYSCALE)
もし適切な画像がなければ、以下のコードで擬似的な画像を生成できます
base = cv2.imread(‘path/to/some/text_image.png’, cv2.IMREAD_GRAYSCALE)
rows, cols = base.shape
gradient = np.linspace(0.8, 1.2, cols)
img = np.uint8(base * gradient)
img = cv2.GaussianBlur(img, (25, 25), 0)
1. グローバル閾値処理
ret, global_thresh = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)
2. 適応的閾値処理 (Mean)
adaptive_mean = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_MEAN_C,
cv2.THRESH_BINARY, 11, 2)
3. 適応的閾値処理 (Gaussian)
adaptive_gaussian = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 11, 2)
結果の表示
titles = [‘Original Image’, ‘Global Thresholding (v=127)’,
‘Adaptive Mean Thresholding’, ‘Adaptive Gaussian Thresholding’]
images = [img, global_thresh, adaptive_mean, adaptive_gaussian]
plt.figure(figsize=(12, 8))
for i in range(4):
plt.subplot(2, 2, i+1)
plt.imshow(images[i], ‘gray’)
plt.title(titles[i])
plt.xticks([]), plt.yticks([])
plt.show()
“`
結果の比較と考察
- グローバル閾値処理: おそらく、画像の明るい部分か暗い部分のどちらかで文字がうまく抽出できず、途切れたり潰れたりするでしょう。
ADAPTIVE_THRESH_MEAN_C
: グローバル閾値処理よりはるかに改善され、画像全体で文字が抽出されるようになります。しかし、ブロックの境界で不自然なアーティファクトが見られることがあります。ADAPTIVE_THRESH_GAUSSIAN_C
: 最もクリーンな結果が得られることが多いです。ガウシアン加重により、閾値の計算がより滑らかになり、MEAN_C
で見られたようなアーティファクトが軽減されます。
パラメータ(blockSize
, C
)の調整
adaptiveThreshold
を使いこなす鍵は、blockSize
と C
の調整にあります。
blockSize
: この値は、抽出したいオブジェクトのサイズと関連があります。- 小さすぎると、ノイズを拾いやすくなったり、文字の一部分(細い線など)が途切れたりすることがあります。
- 大きすぎると、局所的な変化に対応できなくなり、結果がグローバル閾値処理に近づいていきます。
- 一般的に、抽出したい文字や線の太さより少し大きめの奇数を設定するのが良いスタート地点です。
C
: この定数は、閾値の微調整に使います。- 正の大きい値にすると、閾値が下がるため、より多くのピクセルが黒(背景)と判断され、結果的にオブジェクトが細くなります。
- 負の値にすると、閾値が上がるため、より多くのピクセルが白(前景)と判断され、オブジェクトが太くなります。
- まずは
2
や3
といった小さな正の値から始め、結果を見ながら調整するのが一般的です。
適応的閾値処理は非常に強力ですが、最適なパラメータを見つけるには試行錯誤が必要です。
4. 自動閾値決定法: 大津の二値化とTriangle法
これまで見てきた方法では、thresh
や C
といった値を手動で設定する必要がありました。しかし、処理する画像が毎回変わる場合、その都度最適な閾値を探すのは大変な作業です。
この問題を解決するために、画像の特性から自動的に最適な閾値を見つけ出すアルゴリズムが考案されました。OpenCVでは、代表的な2つの手法を簡単に利用できます。
大津の二値化 (Otsu’s Binarization)
大津の二値化は、最も有名で広く使われている自動閾値決定アルゴリズムです。
-
原理: このアルゴリズムの根底にあるのは、「良い閾値とは、前景と背景の2つのクラスを最もきれいに分離できるものである」という考え方です。具体的には、画像の輝度ヒストグラム(各輝度値を持つピクセルがどれだけあるかを示すグラフ)を分析し、「クラス内分散の合計を最小化する(あるいはクラス間分散を最大化する)」閾値を自動的に探し出します。
- クラス内分散: 前景グループ内の輝度値のばらつきと、背景グループ内の輝度値のばらつき。
- クラス間分散: 前景グループの平均輝度値と、背景グループの平均輝度値の差。
このアルゴリズムは、ヒストグラムが双峰性(bimodal)、つまり前景と背景に対応する2つの明確なピークを持つ場合に最も効果を発揮します。
-
使い方: 使い方は驚くほど簡単です。
cv2.threshold
関数のtype
引数に、cv2.THRESH_OTSU
を追加するだけです。“`python
閾値として0を渡すが、この値は無視される
ret_otsu, dst_otsu = cv2.threshold(src, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
“`thresh
引数(ここでは0
)は無視され、大津のアルゴリズムによって計算された最適な閾値が、戻り値ret_otsu
に格納されます。cv2.THRESH_BINARY
の部分をcv2.THRESH_BINARY_INV
などに変えることも可能です。
サンプルコードと考察
“`python
import cv2
import matplotlib.pyplot as plt
img = cv2.imread(‘path/to/your/image.png’, cv2.IMREAD_GRAYSCALE)
大津の二値化
閾値は自動で計算されるため、thresh引数は0でOK
ret_otsu, otsu_thresh = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
print(f”Otsu’s threshold: {ret_otsu}”)
比較のために手動で設定したグローバル閾値
ret_global, global_thresh = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)
結果の表示
plt.figure(figsize=(15, 5))
plt.subplot(1, 3, 1), plt.imshow(img, ‘gray’), plt.title(‘Original Image’), plt.axis(‘off’)
plt.subplot(1, 3, 2), plt.imshow(global_thresh, ‘gray’), plt.title(‘Global Thresholding (v=127)’), plt.axis(‘off’)
plt.subplot(1, 3, 3), plt.imshow(otsu_thresh, ‘gray’), plt.title(f”Otsu’s Thresholding (v={ret_otsu})”), plt.axis(‘off’)
plt.show()
“`
このコードを実行すると、ret_otsu
に画像に最適化された閾値が表示され、多くの場合、手動で設定した閾値よりも良い結果が得られます。
注意点: 大津の二値化は万能ではありません。照明が極端に不均一な場合や、ヒストグラムが双峰性でない(例えば、背景が大部分を占め、オブジェクトが非常に小さい)場合には、うまく機能しないことがあります。
Triangle法
Triangle法は、大津の二値化が苦手とするような、ヒストグラムが単峰性(unimodal)の画像に有効なもう一つの自動閾値決定アルゴリズムです。
-
原理:
- 画像の輝度ヒストグラムを作成します。
- ヒストグラムで最も高いピーク(最も多くのピクセルが持つ輝度値)を見つけます。
- ヒストグラムの始点(または終点、画像の明るさに応じて決まる)と、そのピークを結ぶ直線を引きます。
- ヒストグラム上の各点から、この直線までの距離を計算します。
- 直線からの距離が最大となる点の輝度値を、閾値として採用します。
幾何学的に三角形のような形を利用して閾値を決めるため、この名前がついています。特に、広大な背景の中に小さなオブジェクトがポツンとあるような画像の分離に適しています。
-
使い方: 大津の二値化と同様に、
type
引数にcv2.THRESH_TRIANGLE
を追加します。python
ret_tri, dst_tri = cv2.threshold(src, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_TRIANGLE)
大津の二値化との使い分け
- 大津の二値化: 前景と背景のピクセル数が同程度で、輝度ヒストグラムに2つの山が見えるような画像に最適。
- Triangle法: 画像の大部分を背景が占め、ヒストグラムに大きなピークが1つだけあるような画像に有効。
どちらの手法も cv2.threshold
にフラグを追加するだけで簡単に試せるので、自分の扱っている画像で両方を試し、より良い結果を与える方を選択するのが良いでしょう。
5. 実践的な応用例
理論を学んだところで、これらの技術を組み合わせて、より実践的な問題を解決する方法を見ていきましょう。
例1: ノイズ除去と組み合わせた閾値処理
実世界の画像には、センサーノイズなどが含まれていることがよくあります。このようなノイズが存在する画像に直接閾値処理を適用すると、ノイズが白や黒の点として残り、きれいな二値画像が得られません。
この問題は、閾値処理の前に平滑化フィルタを適用することで大幅に改善できます。特にガウシアンブラー (cv2.GaussianBlur
) は効果的です。
“`python
import cv2
import matplotlib.pyplot as plt
img = cv2.imread(‘path/to/noisy_image.png’, cv2.IMREAD_GRAYSCALE)
ノイズを低減するためにガウシアンブラーを適用
(5, 5)はカーネルサイズ。大きいほどブラーが強くなる
img_blurred = cv2.GaussianBlur(img, (5, 5), 0)
ブラーをかけた画像に対して大津の二値化を適用
ret, otsu_thresh = cv2.threshold(img_blurred, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
比較のため、ブラーなしの画像にも適用
ret_noblur, otsu_noblur = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
結果表示
plt.figure(figsize=(15, 5))
plt.subplot(1, 3, 1), plt.imshow(img, ‘gray’), plt.title(‘Original Noisy Image’), plt.axis(‘off’)
plt.subplot(1, 3, 2), plt.imshow(otsu_noblur, ‘gray’), plt.title(‘Otsu on Original’), plt.axis(‘off’)
plt.subplot(1, 3, 3), plt.imshow(otsu_thresh, ‘gray’), plt.title(‘Otsu on Blurred Image’), plt.axis(‘off’)
plt.show()
“`
ブラーをかけることで、個々のノイズピクセルの影響が薄まり、大津のアルゴリズムがより安定した閾値を計算できるようになります。結果として、ノイズの少ないクリーンな二値画像が得られるはずです。これは非常に一般的なテクニックです。
例2: OCR(光学文字認識)の前処理
スキャンした文書からテキストを抽出するOCRでは、前処理としての二値化が極めて重要です。文書画像は照明ムラの影響を受けやすいため、adaptiveThreshold
が威力を発揮します。
“`python
import cv2
import matplotlib.pyplot as plt
照明ムラのある文書画像を読み込む
doc_img = cv2.imread(‘path/to/document_image.png’, cv2.IMREAD_GRAYSCALE)
適応的閾値処理(ガウシアン)を適用
blockSize=15, C=4 などのパラメータは画像によって調整が必要
ocr_ready_img = cv2.adaptiveThreshold(doc_img, 255,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 15, 4)
plt.figure(figsize=(10, 5))
plt.subplot(1, 2, 1), plt.imshow(doc_img, ‘gray’), plt.title(‘Original Document’), plt.axis(‘off’)
plt.subplot(1, 2, 2), plt.imshow(ocr_ready_img, ‘gray’), plt.title(‘Adaptive Thresholding for OCR’), plt.axis(‘off’)
plt.show()
“`
この処理により、背景の明るさのムラに関係なく、文字がくっきりと黒で抽出されます。このクリーンな二値画像をTesseractなどのOCRエンジンに渡すことで、認識精度が大幅に向上します。
例3: 特定の色のオブジェクトを抽出する
threshold
は輝度値に対する処理ですが、「閾値処理」という考え方は色に対しても応用できます。特定の色を持つオブジェクトだけを抽出したい場合、HSV色空間と cv2.inRange
関数を使うのが定石です。
cv2.inRange
は、指定した色の範囲(下限と上限)内にあるピクセルを白、それ以外を黒にするマスク画像を生成する関数で、まさに色に対する閾値処理と言えます。
“`python
import cv2
import numpy as np
画像を読み込み、BGRからHSV色空間に変換
img_bgr = cv2.imread(‘path/to/your/color_image.png’)
img_hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV)
抽出したい色のHSV範囲を定義 (例: 青色)
OpenCVのH(色相)は0-179, S(彩度)は0-255, V(明度)は0-255
lower_blue = np.array([100, 150, 50])
upper_blue = np.array([140, 255, 255])
inRange関数でマスクを作成
mask = cv2.inRange(img_hsv, lower_blue, upper_blue)
bitwise_andで元の画像からマスクされた部分だけを抽出
result = cv2.bitwise_and(img_bgr, img_bgr, mask=mask)
結果表示 (OpenCVウィンドウで表示)
cv2.imshow(‘Original Image’, img_bgr)
cv2.imshow(‘Mask’, mask)
cv2.imshow(‘Result’, result)
cv2.waitKey(0)
cv2.destroyAllWindows()
“`
この手法は、特定の色のボールを追跡したり、果物の熟度を判定したりと、幅広い応用が可能です。
まとめ
この記事では、OpenCVにおける閾値処理の基礎から応用までを網羅的に解説しました。最後に、学んだ内容のキーポイントを振り返りましょう。
- グローバル閾値処理 (
cv2.threshold
): 画像全体に単一の閾値を適用する最も基本的な手法です。5つのタイプ (BINARY
,BINARY_INV
,TRUNC
,TOZERO
,TOZERO_INV
) があり、目的に応じて使い分けることができます。 - 適応的閾値処理 (
cv2.adaptiveThreshold
): 照明が不均一な画像に非常に有効です。ピクセルの近傍領域から局所的な閾値を計算することで、照明の変化に頑健な二値化を実現します。blockSize
とC
のパラメータ調整が鍵となります。 - 自動閾値決定法 (
THRESH_OTSU
,THRESH_TRIANGLE
): 閾値を手動で設定する手間を省き、画像のヒストグラムから最適な閾値を自動で計算します。大津の二値化は双峰性ヒストグラム、Triangle法は単峰性ヒストグラムを持つ画像で特に有効です。 - 実践的な組み合わせ: 閾値処理の前にガウシアンブラーでノイズを除去したり、HSV色空間と
inRange
を組み合わせて特定色を抽出したりすることで、より高度なタスクに対応できます。
閾値処理は、それ自体がゴールであることは少なく、多くの場合、より複雑な画像解析タスク(輪郭抽出、形態素変換、物体認識など)のための重要な前処理ステップです。クリーンで意味のある二値画像を生成する能力は、コンピュータビジョンのプロジェクトを成功に導くための必須スキルと言えるでしょう。
今回学んだ知識を土台として、ぜひ様々な画像にこれらのテクニックを試し、パラメータを調整し、その効果を自分の目で確かめてみてください。試行錯誤を繰り返すことで、あなたはOpenCVの閾値処理を真にマスターできるはずです。 Happy coding