2014.12. 8.
石立 喬

OpenCVとVisual C++による画像処理と認識(3)

----- 二値化のいろいろと、トラックバーの使用 -----

 これからは、次第にOpenCV本来の関数(メソッド)の積極的な利用法を紹介して行く。ここで取り上げる二値化は、画像認識に不可欠な処理で、OpenCVには優れた関数が備わっている。
 しきい値を外部から調節するのに、OpenCV唯一のGUIと言えるトラックバーを使用した。

二値化とは
 原画像を、1(濃度d=0)と0(濃度d=255)の二値しかない画像に変換するもので、物体の識別、文字の読み取りなどに用いられる。原画像はグレイスケール画像とし、ピクセルごとに比較して、原画像の濃度がしきい値より大きいと1に、小さいと0に設定して二値画像を作る(1と0を逆にすることもできる)。
 しきい値の選び方に二種類あり、全画像に同一のしきい値を用いるグローバルしきい値法と、画像の部分に応じてしきい値を変更するローカル(適応型)しきい値法がある。

グローバルしきい値法
 グローバルしきい値法のためにOpenCVに用意された関数に、
  threshold(gray_image, binary_image, thres, maxValue, THRESH_BINARY);
がある。しきい値thresは外部から与え、二値画像の最大濃度値maxValueは255にする。最適なしきい値thresを選ぶには、人が介在したり、自動的にヒストグラムを採って計算したりする必要がある。THRESH_BINARYは二値化の方法を指定するタイプの一つで、THRESH_BINARY_INVを用いると、1と0が逆転する。
 最適なしきい値thresを自動的に求める「大津の方法」があり、これを使うには、THRESH_OTSUをタイプ指定に追加して、
  threshold(gray_image, binary_image, 0, maxValue, THRESH_BINARY | THRESH_OTSU);
とする。thresの値は、外部から設定しても無視され、一般には0を入れておく。

ローカル(適応型)しきい値法
 OpenCVでローカルしきい値法を使うには、
  adaptiveThreshold(gray_image, binary_image, maxValue, CV_ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, blockSize, offset);
がある。
  CV_ADAPTIVE_THRESH_MEAN_Cは、ローカルしきい値を求める方法を指定するタイプの一つで、注目するピクセルの周辺のピクセル値の単純平均を取り、外部から与えられる一定値(offset)を引くことを意味する。他に、CV_ADAPTIVE_GAUSSIAN_Cがあり、こちらは、周辺ピクセルにガウス分布に従った重みを付けて加算し、一定値を引く。
 これらの方法も、完全自動化とは言えない。周辺ピクセルをどの範囲まで取るかを指定するblockSIzeと、しきい値を補正するoffsetは、外部から設定しなければいけない。これには、経験と試行錯誤が必要になる。blockSizeは奇数でないと、ビルドはできても実行時エラーとなる。offsetの値は、周辺ピクセル値の分散が小さい場合には、それに応じて小さい値を用いないと、効果が過大になる。同一濃度の部分が広範囲に存在する場合には、僅かのoffsetで1か0かが決まる。

二値化の方法を比較する
 まずは、色々な二値化法を比較して見る。図1はそのプログラムで、グローバルしきい値法2種類と、ローカルしきい値法の一例を実行している。具体的には、
1)原画像をグレイスケール画像として読み込む。
  imread関数の第2引数にIMREAD_GRAYSCALEを記述する。
2)グローバルしきい値法を用い、実行結果を見ながら、しきい値を選んで二値化する。
  threshold関数を用い、thresを80に設定する。
3)グローバルしきい値法の大津の方法で二値化する。しきい値は与えない。
  threshold関数で、THRESH_OTSUを追加する。
4)ローカルしきい値法を用いる。41 x 41の周辺ピクセルの単純平均を取り、9を引いて、そのピクセルのしきい値とする。
  adaptiveThreshold関数を用い、blockSize = 41, offset = 9とする。


図1 二値化の方法を比較するプログラム


得られた結果

 図2は得られた結果で、この原画像を用いた限りでは、結果を見ながら最適のグローバルしきい値を求めた、目視手動の方法が最も良い。大津の方法は、画面に占める白と黒の比率を同じにしようと努力した結果が現れ、明るい部分は飛び、暗い部分は潰れている。適応型のローカルしきい値法は二番目に優れ、人手を介さない自動処理として有用である。


図2 いろいろな二値化法の比較結果


グローバルしきい値法で、トラックバーを使ってしきい値を与える
 プログラムを書き換えてしきい値を調節するのではなく、OpenCVに用意されているGUIのトラックバーを使ってみる。図3に示すプログラムは、下記から成る。
1)トラックバーを設置するウィンドウを作成する。
 トラックバーは、ウィンドウの上部に設置するものなので、あらかじめウィンドウを用意し、名前を付けておかなくてはならない。
たとえば、
  namedWindow("二値化画像(目視手動)");
のようにする。これが、ウィンドウのタイトルにもなる。
2)トラックバーの値を格納する変数を用意する
 トラックバーの指し示す値で、外部から設定したり、値を取り出したりに使うために、変数value(名称は一例)を用意し、
  int value = 0;
のように宣言しておく。この変数は、実際には使用しないでも済むが、形式的に必要である。宣言と同時に初期値を設定できるが、後述(【注意事項】参照)の理由で、トラックバーの初期値以外に仮設定しておく。
3)トラックバーを用意する
 トラックバーTrackbarを用意するには、
  createTrackbar("トラックバーの名称", "設置するウインドウの名称", &value, 255, onTrackbarChanged);
を用いる。valueは、さきに宣言したものである。
 次の255は、トラックバーの取りうる最大値である。最小値は、常に0と決まっている。onTrackbarChanged(名称は一例)は、トラックバーのスライダー位置が変化すると呼び出されるコールバック関数(メソッド)の名称である。
4)トラックバーの初期値を設定する
 これには、
  setTrackbarPos("トラックバーの名称", "設置するウインドウの名称",128);
を用いる。128は、初期設定する値で、0〜255の中間値として選んだ。
5)トラックバー変更時に呼び出されるonTrackbarChanged(一例、任意に命名できる)コールバック関数(メソッド)を作成する
 ここでは、しきい値thresの設定を使う場合なので、
  void onTrackbarChanged(int thres, void* )
  {
    threshold(image1, image2, thres, 255, THRESH_BINARY);
    imshow("設置するウィンドウの名称", image2);
  }
とする。
 引数の並びは、必ず(int, void*)にする必要があり、具体的な変数名は省略してグローバル変数を使用することができる。この例では、グローバル変数を嫌って、このメソッド内のみで使用するローカル変数thresを引数並びで宣言して用いた。

【注意事項】
 トラックバーの初期設定に、
  int value = 128;
  createTrackbar("トラックバーの名称", "設置するウインドウの名称", &value, 255, onTrackbarChanged);
とすると、トラックバーは128に設定されるが、onTrackbarChangedが呼び出されないので、プログラム実行の最初には、画像が描画されない。トラックバーのスライダーを動かしてはじめて、画像が描画される。
 念入りな方法で、
  int value = 128;
  createTrackbar("トラックバーの名称", "設置するウインドウの名称", &value, 255, onTrackbarChanged);
  setTrackbarPos("トラックバーの名称", "設置するウインドウの名称",128);
を用いても、すでにvalue = 128に設定されているトラックバーを、さらに128に設定するので、設定が変わらず、やはりonTrackbarChandedが呼び出されない。
 結局、int valueで初期値を設定してはいけないことになる。
 単に、int value; としても、プログラムは正常にビルドされ、実行もできるが、一瞬、デフォルトでトラックバーの範囲外に設定されるので、何らかの支障がないか、気になる。


図3 トラックバーを使ったグローバルしきい値法のプログラム


 図4および図5は、プログラムの実行結果である。図4は、プログラム起動直後の画面で、しきい値の初期設定値128が大きすぎることが分かる。図5はトラックバーを用いて、画面を見ながら最適値を選んだ結果である。しきい値80が良かった。


図4 図3のプログラムを実行した直後の結果



図5 二値化画像を見ながら、トラックバーでしきい値を80に設定した結果


ローカル(適応型)しきい値法で、ブロックサイズの効果を比較する
 ローカルしきい値法の関数、
  adaptiveThreshold(gray_image, binary_image, maxValue, CV_ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, blockSize, offset);
で、blockSizeを変えるとどのように結果が変わるかを確認した。プログラムについては、特に説明の要は無いものと思われる。
 結果を図7に示す。図7で、ウィンドウの名称に11, 9などとあるのは、blocklSize = 11でoffset = 9の意味である。blockSizeが小さいと、狭い範囲での部分的な白黒比が一定になる(offsetによって白を多くしてあるが)ので、線画風になり、全体の画像としてのメリハリがなくなる。
 CV_ADAPTIVE_GAUSSIAN_Cタイプを使うと、周辺ピクセルにガウス分布に従った重みを掛けて加算するので、ローカル性が出やすい。一般的には、MEANタイプの方が高速で使いやすい。


図6 ブロックサイズを変えて二値化した結果


認識を目的とした画像で比較する
 車のナンバープレートや製品の製造番号を読み取るには、文字の二値化が欠かせない。光源にむらがあったり、日照の条件が悪い場合などの画像サンプルを用いて二値化を試みた結果を、図7と図8に示す。使用プログラムは省略する。
 各図で、左上は原画像をグレイスケール化したもの、右上はグローバルしきい値法で大津の方法を用いたもの、左下はローカルしきい値法のMEANを用いたもの、右下は同じくGAUSSIANを用いたものである。いずれの場合も、グローバルしきい値法を用いたのが良く、その中では、MEANの方が良いかな、と思われる。


図7 書籍の一部を二値化した結果



図8 電柱の表示を二値化した結果


結 論
 二値化の目的が何であるかによって、選ぶべき二値化手法が変わる。特徴抽出が目的であれば、グローバルしきい値法が、優れている。原画像の濃淡を残したい自然画像のような場合には、グローバルしきい値法で、眼で見ながらしきい値を調節するのが良い。原画像により忠実に二値化したい場合には、ディザ法を使うと良い。
 処理速度で考えると、単純なグローバルしきい値法が最も速く、ローカルしきい値法では、THRESH_MEAN_Cを使うのが速い。
 クルマのナンバープレートの読み取りのような、屋外での応用には、ローカルしきい値法もやむを得ないが、工場内での製品検査などでは、光源を均一化し、固定しきい値でのグローバルしきい値法を使うのが高速化できるだろう。