
こんな悩みを解決します。
記事の内容
- OpenCVで用意される二値化処理の解説とPython実装方法
- それぞれの二値化処理のメリット、デメリット解説
記事の信頼性
この記事を書いている私は、ソフトウェア(画像処理)エンジニアとして自動車メーカーでC言語やPythonを使って製品開発をしています。
画像処理を専門としていますので、ノウハウを共有できたらと思います。
本記事を読むことで、OpenCVで用意されている閾値処理を理解でき、用途に合わせて適切な二値化ができますよ。
OpenCVで用意される二値化処理の解説とPython実装方法
OpenCVで用意されている二値化処理は次の3つです。
それぞれのアルゴリズムの内容と、使い方を解説します。
二値化処理
- 単純な閾値処理
- 適応的な閾値処理
- 大津の二値化
単純な閾値処理
単純な閾値処理について、使い方を解説します。
アルゴリズムは簡単で、自分で決めた閾値より大きければある値を割り当てて、閾値より小さければ別の値を割り当てるだけです。
次の図は、適当な閾値を使って二値化した画像です。
道路の二輪車マークだけを抽出したかったのですが、うまくいきません。
狙いどおりに二値化するためには、トライアンドエラーを繰り返して閾値を自分で決めることが必要です。
使い方
単純な閾値処理は、cv2.threshold 関数を使います。
返り値と引数は以下のとおり。
返り値 | 内容 |
第1返り値 | 二値化に使用した閾値 |
第2返り値 | 二値化画像 |
引数 | 内容 |
第1引数 | 二値化したいグレースケール画像 |
第2引数 | 自分で決めた閾値 |
第3引数 | 二値化によって閾値以上の画素に対して割り当てられる最大値 |
第4引数 | 二値化処理のタイプを指定(次に記載) |
二値化処理のタイプは、5つあります。
閾値より大きいときに割り当てる値は何か、閾値より小さいときはどうするか、次のように処理を分けることができます。
二値化処理のタイプ | 閾値より大きい | 閾値より小さい |
cv2.THRESH_BINARY | 最大値(第3引数) | 0 |
cv2.THRESH_BINARY_INV | 0 | 最大値(第3引数) |
cv2.THRESH_TRUNC | 閾値(第2引数) | 値は入力のまま |
cv2.THRESH_TOZERO | 値は入力のまま | 0 |
cv2.THRESH_TOZERO_INV | 0 | 値は入力のまま |
サンプルコード
sample code import cv2 import numpy as np from matplotlib import pyplot as plt # 入力画像の読み込み img = cv2.imread("threshold_origin.png", cv2.IMREAD_GRAYSCALE) cv2.imshow('org_img', img) # 単純な閾値処理 ret, th = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY) print("th:", ret) cv2.imshow('th_img', th) cv2.waitKey(0) cv2.destroyAllWindows()
適応的な閾値処理
適応的な閾値処理は、局所的な領域ごとに異なる閾値を使って二値化します。
OpenCVでは、閾値の決め方が2種類用意されています。
一つは、局所領域の中央値を閾値とするやり方。もう一つは、ガウシアン分布に基づいた重み付け平均値を閾値とするやり方。
この閾値から一定の値(自分で指定する値)を引くことで、二値化で使う閾値とします。
次の図は、適応的な閾値処理によって二値化した画像です。
影と音符の輝度分布が重なるので、単純に1つの閾値で二値化すると、影と音符は分離できません。
適応的な閾値処理では局所的に閾値を求めるので、領域ごとに異なる光源環境になっている場合に効果的です。
使い方
適応的なな閾値処理は、cv2.adaptiveThreshold 関数を使います。
返り値と引数は以下のとおり。
返り値 | 内容 |
第1返り値 | 二値化画像 |
引数 | 内容 |
第1引数 | 二値化したいグレースケール画像 |
第2引数 | 二値化によって閾値以上の画素に対して割り当てられる最大値 |
第3引数 | 閾値計算のタイプを指定(次に記載) |
第4引数 | 二値化処理のタイプを指定(次に記載) |
第5引数 | 閾値を計算する局所領域のサイズ(奇数) |
第6引数 | 計算した閾値から引く値 |
閾値計算のタイプは、2つあります。
局所領域の中でどのように閾値計算するのか決めることができます。
閾値計算のタイプ | 内容 |
cv2.ADAPTIVE_THRESH_MEAN_C | 局所領域の中央値を閾値にする |
cv2.ADAPTIVE_THRESH_GAUSSIAN_C | 局所領域の重み付け(ガウシアン分布の)平均値を閾値にする |
二値化処理のタイプは、2つあります。
閾値より大きいときに割り当てる値は何か、閾値より小さいときはどうするか、次のように処理を分けることができます。
二値化処理のタイプ | 閾値より大きい | 閾値より小さい |
cv2.THRESH_BINARY | 最大値(第3引数) | 0 |
cv2.THRESH_BINARY_INV | 0 | 最大値(第3引数) |
サンプルコード
# sample code import cv2 import numpy as np from matplotlib import pyplot as plt # 入力画像の読み込み img = cv2.imread("threshold_origin.png", cv2.IMREAD_GRAYSCALE) cv2.imshow('org_img', img) # 適応的な閾値処理 th = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 7, 10) cv2.imshow('th_img', th) cv2.waitKey(0) cv2.destroyAllWindows()
大津の二値化
大津の二値化とは、簡単に言うと、画像のヒストグラムに基づいて機械的に閾値を決めてくれるアルゴリズムです。
画像のヒストグラムを描いたときに、明るいクラスと暗いクラスができるだけ離れるように閾値は決められます。
次の図は、大津の二値化を適用した画像です。
道路と二輪車マークをきれいに分離できてますよね?
大津の二値化は、このように、明るいクラスと暗いクラスを持つ画像(2つの山を持つヒストグラム)に対して非常に効果的です。
原理
先ほど書きましたが、大津の二値化は2つの山が存在するヒストグラムを持つ画像を想定しています。
では、どのように閾値を引くのが最適なのでしょうか?
大津の二値化では、次の観点が成立するような値を最適な閾値としています。
- 2つの山ができるだけ離れていること。 = クラス間分散が大きいこと。
- それぞれの山(データ)がまとまっていること。 = クラス内分散が小さいこと。
それぞれ、どういうことかと言うと、
次の図のように、ある閾値に対してヒストグラムをクラスAとクラスBに分けます。
クラス間分散は、クラス内の平均値が、画像全体の平均値からどれだけばらついているかを表現した分散値。
クラス内分散は、クラス内の画素値がクラス内の平均値からどれだけばらついているかを表現した分散値。
これを、
分離度(クラス間分散をクラス内分散で割った値)として表し、分離度が最大になるような閾値を探していくのが、大津の二値化のアルゴリズムです。
数式など詳細を知りたい場合は、文献がたくさんありますので、探してみてください。アルゴリズムを実装するわけではないので、最低限これくらいの知識を入れておけば問題なしです。
使い方
大津の二値化は、cv2.threshold 関数を使います。
返り値と引数は以下のとおり。
返り値 | 内容 |
第1返り値 | 二値化に使用した閾値 |
第2返り値 | 二値化画像 |
引数 | 内容 |
第1引数 | 二値化したいグレースケール画像 |
第2引数 | 閾値(アルゴリズムで計算されるので、"0"にしましょう) |
第3引数 | 二値化によって閾値以上の画素に対して割り当てられる最大値 |
第4引数 | "cv2.THRESH_OTSU"を追加することで、大津の二値化を使える |
サンプルコード
# sample code import cv2 import numpy as np from matplotlib import pyplot as plt # 入力画像の読み込み img = cv2.imread("threshold_origin.png", cv2.IMREAD_GRAYSCALE) cv2.imshow('org_img', img) # 大津の二値化 ret, th = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) print("th:", ret) cv2.imshow('th_img', th) cv2.waitKey(0) cv2.destroyAllWindows()
それぞれの二値化処理のメリット、デメリット解説
単純な閾値処理、適応的な閾値処理、大津の二値化の3つのアルゴリズムを紹介しましたが、どれをどんなときに使えばよいのかわかりづらいですよね。
それぞれの、メリットとデメリットを解説しますので、用途に合わせて使ってみてください。
単純な閾値処理
メリット
- 閾値を自分で決めるため、二値化をコントロールしやすい。(閾値は事前に決まるので、想定外の画像が入ってきて閾値が吹っ飛ぶとかはない)
デメリット
- 画像全体で1つの閾値しか設定できないので、領域ごとに異なる光源環境になっている場合(影など)は、期待通りに二値化できない ⇒ "適応的な閾値処理"がおすすめ。
- トライアンドエラーで閾値を決めるので、調整に時間がかかる ⇒ 特定の条件下のみだが、閾値が機械的に求まる"大津の二値化"がおすすめ。
適応的な閾値処理
メリット
- 局所的な領域ごとに異なる閾値を使うため、領域ごとに異なる光源環境になっている場合(影など)でも、期待通りの二値化ができる。
デメリット
- 画素ごとに閾値を持つため、二値化をコントロールしにくい。(想定外の画像が入ってきて閾値が予期せぬ値になることがある) ⇒ 安全を取るなら、"単純な閾値処理"がおすすめ。
- 制御するパラメータが増える(閾値計算のタイプ、二値化処理のタイプ、局所領域のサイズ、閾値から引く値)ため、調整がやや大変。 ⇒ 特定の条件下のみだが、閾値が機械的に求まる"大津の二値化"がおすすめ。
大津の二値化
メリット
- 閾値を決めるために、トライアンドエラーを繰り返す手間がかからない。
- 2つの山が存在するヒストグラムを持つ画像に対しては、機械的に最適な閾値が決まる。
デメリット
- 2つの山が存在しないヒストグラムを持つ画像に対しては、良い効果が得られない。 ⇒ "単純な閾値処理"か"適応的な閾値処理"がおすすめ。
- 閾値が機械的に決まってしまうため、二値化をコントロールしにくい。(4つの山を持つヒストグラムとか、想定外の画像が入ってきて閾値が予期せぬ値になることがある) ⇒ 安全を取るなら、"単純な閾値処理"がおすすめ。
以上で解説は終わりです。
これからPythonを使って画像処理を始めたい方、さらに知識を深めていきたい方は、動画コンテンツで学ぶことをおすすめします。
私もよく利用しますので、さいごに紹介だけして終わります。
画像処理の基礎:フィルタリング,パターン認識から撮像過程モデルまで
入門としてはこのあたりがおすすめです。
参考までに。