OpenCvSharpをつかう その15(適応的閾値処理)

適応的閾値処理 (Adaptive Thresholding) を使うサンプルです。

始めからぶっちゃけますと cvAdaptiveThreshold を使え、という話になりますが、本記事はそれだけでは終わりません! OpenCvSharpでは、OpenCV標準では用意されていない適応的閾値処理を、こっそり組み込んでいます。今回、初めてそれを公にしておきます。

OpenCvSharpをつかう 記事一覧

背景

適応的閾値処理は、様々な応用がありますが、特に文書画像処理においては必須の処理です。

例えば以下のような画像(左)を二値化したいとします。このとき、みなさんご存じ 大津の手法 では、(右)のようになってしまいます。
カメラで撮影した場合や古い文書の画像では特に、このような影やシミ等があるのが普通で、明るい部分でうまくいく閾値では高すぎて影部分では全部真っ黒になります。 もし仮に影がもっと大きければ、今度は閾値が暗め方向に引っ張られ、明るい箇所の文字が全部消えてしまうでしょう。

using (IplImage src = new IplImage("kodama.png", LoadMode.GrayScale))
using (IplImage binaryOtsu = src.Clone())
{
    Cv.Threshold(src, binaryOtsu, 0, 255, ThresholdType.Otsu);
    CvWindow.ShowImages(src, binaryOtsu);
}

f:id:Schima:20131018235900p:plain

敗因は、画像全体である一つの閾値を設定したことです。明るい部分では明るめの閾値を、暗い部分では暗めの閾値を設定してあげればよいわけです。それが適応的閾値処理です。


AdaptiveThreshold

解説

OpenCVで用意される適応的閾値処理は、Cv.AdaptiveThreshold から使うことができます。

詳細はリファレンスをご覧ください。ざっくり説明しますと、このメソッドでは入力画像srcの1ピクセルごとに、個別の閾値を計算して決めます。どう計算するかというと、そのピクセルの周囲を見て決めます。周りが明るそうなら高めの閾値に、暗そうなら低めの閾値にするわけです。

以下のコード例ではGaussianCを指定しているので、blockSize x blockSize(ここでは9x9)の近傍領域に対してガウシアンによる重み付けで総和をとります。つまり、近いピクセルほど重視するということです。もう1つのMeanCにした場合は、重み付けなしで単純な相加平均で決めます。
最後の引数(ここでは12)は、こうして求めた値から減算する定数です。減算後の値が、そのピクセルの最終的な閾値です。

using (IplImage src = new IplImage("C:\\kodama.png", LoadMode.GrayScale))
using (IplImage binaryAdaptive = src.Clone())
{
    Cv.AdaptiveThreshold(src, binaryAdaptive, 255, 
        AdaptiveThresholdType.GaussianC, ThresholdType.Binary, 9, 12);
    CvWindow.ShowImages(src,  binaryAdaptive);
}

減算定数の意味

最後の減算定数は何のためにあるのでしょうか。今回の対象は文書画像ですが、これは一般に背景が白で、つまり画像の大部分のピクセルは白で占められます。そうすると、1ピクセルごとに周囲を見ていくときに以下の傾向があります。

  • 文字が有る領域: 周囲の画素値はバラエティ豊か(白地に黒い細い線、で構成されるので)
  • 文字が無い領域: 周囲の画素値はほぼ同じ(周りじゅうが白)

周り中が似たような色のとき、減算定数が有ることで、減算後は対象ピクセルは閾値を上回ることになり、白くなります。これにより、背景領域では多少のノイズ・色の揺らぎに負けずに白で塗りつぶしやすくし、文字領域では黒いエッジを残しやすくなります。賢いですね。

出力画像

結果の画像です。大津の手法(左)と、GaussianCによるAdaptiveThreshold(右)を並べました。ほぼ申し分なく二値化ができていますね。
f:id:Schima:20131019004623p:plain

Sauvolaの手法

はじめに

実際のところ Cv.AdaptiveThreshold でけっこう充分だったりしますが、他にも適応的閾値処理の手法はあまた考案されています。その中でも比較的著名なSauvola[1]の手法を、OpenCvSharpは独自に実装しています。

少なくとも2009年当時、他人の実装は全く見当たらなかったので、以下の原著論文などを読みつつ実装しました。正確性は担保しませんが、たぶん良さげです。

[1] J. Sauvola et. al., “Adaptive document image binarization,” Pattern Recognition 33(2), pp.225–236, 2000.
http://www.mediateam.oulu.fi/publications/pdf/24.pdf

手法について

以下の式で、各ピクセルの閾値を求めています。
T(x,y)=m(x,y) \times \left[ 1+k \times \left( \frac{s(x,y)}{R}-1 \right) \right]
あるピクセル (x, y) の閾値 T(x, y) は、その周囲の平均値 m(x, y) と、標準偏差 s(x, y) によって決まります。kとRは適当に決める定数です。

標準偏差により、周りの画素値のばらつきが計れます。ばらつきが大きい(文字領域)と閾値が上がり、黒くなりやすくなります。

使い方

OpenCvSharp.Extensions.Binarizer クラスにまとめられています。使うには、OpenCvSharp.Extensions.dllを参照に追加してください。(なお、OpenCvSharpExtern.dllのお世話は不要です。)

Binarizer.SauvolaFastがSauvolaの手法による二値化です。
src は入力グレースケール画像、dst はsrcと同じ形式(8ビット1チャネル)です。
blockSizeは、(Cv.AdaptiveThresholdと同様に)3や7などの奇数を指定します。
kとRは上記の式の通りで、良い出力になるまで何度も変えながら試してみましょう。私は以下の値くらいをよく使います。

int blockSize = 7;
double k = 0.15;
double R = 32;
Binarizer.SauvolaFast(src, dst, blockSize , k, R);

出力画像

結果の画像です。Cv.AdaptiveThreshold - GaussianC(左)と、Sauvolaの手法による結果(右)を並べました。大差はありませんが、やや文字が太めになりました。特に「ズ」の文字がかすれ気味だったのが改善しています。これは地味に大きいです。
f:id:Schima:20131019013025p:plain

パラメータ調整次第でいかようにもなるので結論を言いづらいですが、この画像はどちらもそこそこがんばってチューニングした結果ではあります。今までの(あてにならない)経験から言いますと、こんなところがあります。

  • 同じくらいのノイズ除去具合にしたとき、Sauvolaの方が若干くっきりになる。
  • Sauvolaでは、パラメータを濃い目にすると、背景領域に地図の等高線のようなうず模様ができることがある。

その他の手法

Binarizerクラスには他に、Niblackの手法[2]・Bernsenの手法[3]による適応的二値化手法も実装してあります。基本的に精度はSauvolaより劣ると思われますが、興味があればお試しを。

Sauvolaの手法は、Niblackの手法の改良版という位置づけです。参考までに、Niblackの手法での閾値を求める式は以下です。
T(x,y)=m(x,y) + k \times s(x,y)

[2] Wayne Niblack, An Introduction to Digital Image Processing, pp.115–116, 1986.
[3] John Bernsen, "Dynamic thresholding of grey-level images", Proceedings of the 8th International Conference on Pattern Recognition, pp.1251–1255, Oct. 1986.