ページ

2015-09-06

ZXing(.Net)のバーコード認識率を上げる方法

ZXing.Netは、1次元/2次元バーコードを認識するためのライブラリで、よく見るJAN-13(EAN-13)コードや、QRコードを認識させることができる。使い方は一見簡単で、NuGetでぽいっと入れて、数行書けば動かすことができる。

しかし、バーコードが含まれる静止画を認識させようとするとデフォルトだと認識率がだいぶ低い。恐らくカメラを動かしながら認識させていくために速度重視なのであろう。この記事では、そんなZXing.Netの認識率を上げる方法について書く。コードは流用できずとも、JavaのZXingも含めた他の言語実装でも同じようなテクニックが利用可能と思う。

ZXing.Netの単純な使い方

BarcodeReaderを作って、Decode/DecodeMultipleメソッドにbitmapを流し込むだけ。コードをざっと見、プラットフォームごと、例えばXamarin用のコードだと受け取る画像オブジェクトが違うように見えるので、使う環境によって見てみるといいと思う。System.Drawingなんてヤダヤダ、BitmapSourceがいいという場合は、zxing.presentationアセンブリにそれを受け取るやつが居るっぽいが、この記事を書いているときに気づいたので試してない。

using (var bitmap = (System.Drawing.Bitmap)System.Drawing.Bitmap.FromFile(path))
{
    var reader = new BarcodeReader();
    var resultArray = reader.DecodeMultiple(bitmap);
    return resultArray == null ? new string[0] : resultArray.Select(x => x.Text).OrderBy(x => x).ToArray();
}

気をつけなきゃいけないのはDecodeMultiple()はnullを返すことがあるということ。なぜ空配列にしなかったのか。…などというのはこの後説明する内容に比べれば些細なのでしれっとnullチェックする。

1. TryHarder他オプションを設定する

デフォルトだと回転すらしてくれないので、ほぼバーコード自体のみの画像でない限り認識した結果を見るほうが珍しいという事態になると思う。回転なんかはさすがにこのTryHarderオプションが面倒を見てくれるので、オプションを設定する。
using (var bitmap = (System.Drawing.Bitmap)System.Drawing.Bitmap.FromFile(path))
{
    var reader = new BarcodeReader();
    reader.Options = new ZXing.Common.DecodingOptions
    {
        TryHarder = true,
        PossibleFormats = new[] { BarcodeFormat.EAN_13 }.ToList()
    };

    var resultArray = reader.DecodeMultiple(bitmap);
    return resultArray == null ? new string[0] : resultArray.Select(x => x.Text).OrderBy(x => x).ToArray();
}
今回のケースではJAN-13だということがはっきりしているので、TryHarderのほかにPossibleFormatsを指示する。

これでデフォルトからするとだいぶ改善する。

2. 事前に2値化する

だがまだ認識してくれないものがだいぶある。輝度の処理がイマイチという話をどこかで読んだので、大津の2値化をしてから投げるだけで改善した。後述の理由により、認識させるものによってはこれでいいケースがかなりあると思う。

例によってOpenCVSharpを使ったコード。
using (var mat = new Mat(path))
using (var matG = mat.CvtColor(ColorConversion.BgrToGray))
using (var matB = matG.Threshold(0, 255, ThresholdType.Otsu))
using (var bitmap = matB.ToBitmap())
{
    var reader = new BarcodeReader();

    reader.Options = new ZXing.Common.DecodingOptions
    {
        TryHarder = true,
        PossibleFormats = new[] { BarcodeFormat.EAN_13 }.ToList()
    };

    var resultArray = reader.DecodeMultiple(bitmap);
    return resultArray == null ? new string[0] : resultArray.Select(x => x.Text).OrderBy(x => x).ToArray();
}
using OpenCvSharp.Extensions; しておくとMatはToBitmap()でSystem.Drawing.Bitmapにできるのがポイント。

AdaptiveThreshold
でもいいかなと思ったが自分の見てるソースだといつもうまくいくパラメータを持ってくるのが逆にしんどかった。カメラで撮っている画像だと頑張ってAdaptiveThresholdのパラメータ合わせた方がいいことがあるかもしれない。

あるいは、バーコードが黒だとはっきりしているのなら、色ありの状態で黒部分を抜き出して処理するという戦略もあると思う。そこまでじゃないかなと思ったのでやっていない。

3. さらに事前に領域分割する

それでも認識してくれない画像があるのだが、その画像もサイズを半分にしたら認識されたりする。ざっと見では追いかけられなかったのでコードは追っていないのだが、画像全体のサイズから一定の比率以下のサイズだと認識されない感じがする。そのため、事前に分割してしまうと、予想通りうまくいった。

具体的には、Erodeで収縮させバーコード領域のあたりを潰し、そこをFindContoursで輪郭検出させる。詳しい処理はZXing.NETが面倒を見てくれるので、ざっくり分かれればよしとしている。処理イメージとしてはこんな感じ。



コードはこんな感じ。


変わったこととしては、サイズ下限でのフィルタ。どう頑張ってもバーコードが入らない小さい領域を食べさせてもしょうがないので。JAN-13は横は縦棒が30本あるので×2で60px, 縦も数字の「8」が最低5px必要なので×2はあるだろうということで10pxとし、回転も考えて長いほう/短いほうでフィルタしてみた。あと、領域ぎりぎりで切り取ると認識しないことがあったので、白枠を付与することにした。

なお、領域分割を行っているのだから、バーコードは分割されると割り切って、画像にバーコードが複数含まれる場合でもDecodeMultiple()でなくDecode()を呼び出すと、速度が向上する。しかし、背景色の関係で領域がくっつくことがまれにあるので、Decode()を狙うならもう少し領域分割をまじめにやったほうがいいかもしれない。

認識率向上の効果

手元の画像200枚(画像サイズ5000x2000程度、各バーコード2つずつ)に対して、1., 2., 3.を実行してみて認識した数がこちら(グラフには個数と書いてしまったが枚数である)。MultipleはDecodeMultiple()を呼んだ版、MultipleなしはDecode()を呼んだ版。


デフォルト状態での認識数はまさかのゼロ。むしろ貼ってあったQRコードを認識してて笑った。

この「事前領域分割(Multiple)」(3.)のほうの残り3枚は、2つ存在するバーコードの他に一つ追加で何かを誤認識してしまったもの。どうせ二つあるうちの一つを先頭の数字(978)でフィルタしようと思っていたので、自分の使い方的には100%使えるという結果なので満足。「事前領域分割(Multipleなし)」のほうも197枚なのだが、「事前領域分割(Multiple)」と同じなわけではなく、誤認識が一つ減っている代わりに、一つの画像で2つのバーコードの領域が合体し、認識が一つ漏れてしまっていた。理由は前述の背景色の関係なのだが、1/200が他にもあってもおかしくないので、自分的にはDecodeMultipleを呼んで使おうと思う。

速度の違い

処理方式の違いによる速度の違いは以下の通り。


認識率を最も重視しているので、方法が複雑になるほど処理速度は下がっている。また領域分割に関しては、ばらつきが大きいが、これはどのように分割されるかによってトータルでの領域サイズが変わるからであろう。

また、DecodeMultipleを使わない(Decodeを使う)版だと領域分割したとしても単にTryHarderにしたDecodeMultipleより速いので、速度重視であれば自前で分割してでもDecodeMultiple()は使わないほうがよさそうである。ここはOpenCVのほうが速いからという話はありそうだが。

今後の展開

自分で使う分には満足行くレベルになったので、もうおしまいにするが、以下のような点を行うと、より改善されるように思う。必要であればお試しされたし。
  • 黒付近の色での領域抜き出しによる、認識率向上/領域サイズ削減による高速化
  • 自前回転
  • PureFormatsオプションの利用
ここまでやってしまったら、JAN-13程度であれば自前で認識してしまうというのも視野に入ってきてしまうと思うので、要求に応じてという感じで。