読者です 読者をやめる 読者になる 読者になる

メヘンニミン

たくさんの言語に触れたい.走り書きで見る整理ブログです.

急角度・急斜面に対応するQRデコーダ(ZXing編)

http://33.media.tumblr.com/2f4ef1ea5bf82d48f978e86b6d24582d/tumblr_nmqa3eOq9j1ush50oo1_400.gif
黄色が従来の判定.水色が拡張された判定.斜めからもばっちり!

前回: 画像からQRコードを読み取る(OpenCV + libencodeqr編) - メヘンニミン

さて今回は,ZXing(ゼブラクロッシング)というGoogleさんのオープンソースライブラリを使用します.現代のQRデコーダといえばスマホですよね!認識精度は高めで,以下はAndroidアプリに適用できるライブラリです.

zxing/zxing · GitHub

上記のページを見ると,ZXingは様々な言語で利用できることがわかりますね.

本記事では,Android版とC#版(ZXing .NET)の導入を説明します.(C++は面倒でやめました)

さらに,ZXing .NETOpenCVを織り交ぜて,急射角に対応する拡張QRデコーダの作り方を示します.ARでもあるまいし,QRに急射角の需要あんの?と聞かれそうですが,これは以前の研究で実装したものです.せっかくなので放出します…

QRコードを読み取るAndroidアプリを作る

AndroidアプリでQR実装をしようとする例は多いので,ぐぐると実装例はたくさん出てきますよ.

ここでは,実装がお手軽なAndroid Studioでの事例を紹介します.app\build.gradleのdependenciesの中はこう書く.

dependencies{
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support.appcompat-v7:21.0.3'
    compile 'com.google.zxing:core:3.2.+'
    compile 'com.journeyapps:zxing-android-embedded:2.0.1@aar'
    compile 'com.journeyapps:zxing-android-integration:2.0.1@aar'
}

MainActivity.javaでの書き方は,以下の関数startScanに示しています.この関数をボタンを押して実行するなどの仕組みにしてください.

    // スキャンを開始する
    protected void startScan(){
        IntentIntegrator integrator = new IntentIntegrator(MainActivity.this);
        integrator.setResultDisplayDuration(0);
        integrator.setWide();  // ワイドスキャン
        integrator.setCameraId(0);  // 0番目のカメラを使用する
        integrator.initiateScan(IntentIntegrator.QR_CODE_TYPES); //QRコードの読み込み開始
    }

    // スキャン結果を取得後に実行する
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, data);
        TextView message = (TextView)findViewById(R.id.resultText);
        if (scanResult != null) {
            String result = scanResult.getContents();
            if(result != "") {
                // 取得成功.結果の出力方法について以下に書く
                message.setText(result);
                ( (TextView)findViewById(R.id.formatNameText) ).setText(scanResult.getFormatName());
                 Log.d("qrtest", "error = " + scanResult.getErrorCorrectionLevel());
                // ...
            }
        }
    }

なお,ソースコードの公開を準備中です.

QRコードを読み取るC#アプリを作る

コンソールプロジェクトを新規作成して,出力の種類を「Windowsアプリケーション」に変更します.また,OpenCVSharpの参照の追加を済ませてください.NuGetを使用すると楽ですね.

OpenCvSharpをつかう その17(NuGetで導入) - schima.hatenablog.com

ZXing.Net 0.14.0.0をダウンロードし,Net4.5(ここの数字は,プロジェクト設定の「対象のフレームワーク」に合わせる)フォルダ内のzxing.dllを,自分のプロジェクトのDebugへコピー.参照の追加で,zxing.dllにチェックを入れてください.これで「using ZXing;」が書けます.

プロジェクト名を右クリックして,追加=>クラスを選択.名前を「QRcodeZXing.cs」として,以下をペーストしてください.

using System;
using System.Collections.Generic;
using System.Drawing;
using ZXing;


namespace ZXingTest // ここは適宜合わせるように
{
    class QRcodeZXing
    {
        private readonly IList<ResultPoint> resultPoints;
        private string txtContent = String.Empty;
        private readonly BarcodeReader barcodeReader;

        //コンストラクタ
        public QRcodeZXing()
        {
            //     barcodeReader = new BarcodeReader {AutoRotate = true, TryHarder = true, TryInverted = true};
            barcodeReader = new BarcodeReader { AutoRotate = true, TryInverted = true };
            resultPoints = new List<ResultPoint>();
        }


        /// <summary>
        /// バーコードイメージをデコードする
        /// </summary>
        /// <param name="image">デコードするイメージ</param>
        /// <param name="tryMultipleBarcodes">true を渡すと画像上にある複数のバーコードをデコードする</param>
        /// <returns>IList<string> デコードできたら、その文字列</returns>
        public IList<string> Decode(Bitmap image, bool tryMultipleBarcodes = false)
        {
            IList<string> txtContent = new List<string>();
            resultPoints.Clear();

            Result[] results = null;
            if (tryMultipleBarcodes)
            {
                results = barcodeReader.DecodeMultiple(image);
            }
            else
            {
                Result result = barcodeReader.Decode(image);
                if (result != null)
                {
                    results = new[] { result };
                }
            }

            if (results != null)
            {
                foreach (Result res in results)
                {
                    
                    txtContent.Add(res.Text);
                }
            }
            return txtContent;
        }

        public Result[] DecodeDetail(Bitmap image, bool tryMultipleBarcodes = false, int tryNextBarcode = 0)
        {
            IList<string> txtContent = new List<string>();
            resultPoints.Clear();
            Result[] results = null;
            if (tryMultipleBarcodes)
            {
                results = barcodeReader.DecodeMultiple(image);
            }
            else
            {
                Result result = barcodeReader.Decode(image);
                if (result != null)
                {
                    results = new[] { result };
                }
            }

            
            return results;
        }


        /// <summary>
        /// 文字列からQRコードを作成(暫定版)
        /// </summary>
        /// <param name="value">SQコードにする文字列</param>
        /// <param name="size">SQコードのサイズ</param>
        /// <param name="ERROR_CORRECTION">SQコードエラー強度 1(弱い) - 4(強い) </param>
        /// <returns>出来たSQコード</returns>
        public Bitmap Encode(string value, int size, int errconr = 4)
        {
            /// EncodeHintType.ERROR_CORRECTION
            //L 7%が復元可能
            //M 15%が復元可能
            //Q 25%が復元可能
            //H 30%が復元可能

            Dictionary<EncodeHintType, object> hints = new Dictionary<EncodeHintType, object>();
            hints[EncodeHintType.CHARACTER_SET] = "UTF-8";

            switch (errconr)
            {
                case 1:
                    hints[EncodeHintType.ERROR_CORRECTION] = ZXing.QrCode.Internal.ErrorCorrectionLevel.L;
                    break;

                case 2:
                    hints[EncodeHintType.ERROR_CORRECTION] = ZXing.QrCode.Internal.ErrorCorrectionLevel.M;
                    break;

                case 3:
                    hints[EncodeHintType.ERROR_CORRECTION] = ZXing.QrCode.Internal.ErrorCorrectionLevel.Q;
                    break;

                case 4:
                    hints[EncodeHintType.ERROR_CORRECTION] = ZXing.QrCode.Internal.ErrorCorrectionLevel.H;
                    break;

                default:
                    hints[EncodeHintType.ERROR_CORRECTION] = ZXing.QrCode.Internal.ErrorCorrectionLevel.H;
                    break;
            }
            ZXing.QrCode.QRCodeWriter qrCode = new ZXing.QrCode.QRCodeWriter();
            ZXing.Common.BitMatrix qrCodeData = qrCode.encode(value, ZXing.BarcodeFormat.QR_CODE, size, size, hints);

            Bitmap bitmap = new Bitmap(size, size, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
            for (int x = 0; x < qrCodeData.Width; x++)
            {
                for (int y = 0; y < qrCodeData.Height; y++)
                {
                    if (qrCodeData[x, y] == false)
                    {
                        bitmap.SetPixel(x, y, Color.White);
                    }
                    else
                    {
                        bitmap.SetPixel(x, y, Color.Black);
                    }
                }
            }

            return bitmap;
        }

    }
}

このQRCodeZXingオブジェクトを用いてProgram.csを書きます.以下,「画像のデコード結果をコンソール出力する」コードになります.

using OpenCvSharp;
using OpenCvSharp.Extensions;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ZXing;

namespace ZXingTest
{
    class Program
    {
        static void Main(string[] args)
        {
            // 画像を用意
            IplImage image = null;
            try
            {
                // QRコード画像を指定(カレントディレクトリはDebugなので注意)
                image = Cv.LoadImage(".\\test.png", LoadMode.Color);
            }
            catch (Exception e)
            {
                Console.WriteLine("画像がありません");
            }

            if (image != null)
            {
                // Bitmapに変換
                Bitmap bmp = BitmapConverter.ToBitmap(image);

                // QRcodeZXingオブジェクトの準備
                QRcodeZXing zxing = new QRcodeZXing();
                // ZXingを実行
                Result[] results = zxing.DecodeDetail(bmp, true);
                
                if (results != null)
                {
                    // 取得したQRごとに出力
                    foreach (Result res in results)
                    {
                        string resultText = res.Text;
                        Console.WriteLine(res.BarcodeFormat + ": [" + resultText + "] ");
                    }
                }
                else
                {
                    Console.WriteLine("QRが読み取れませんでした");
                }
            }
        }
    }
}

なかなかシンプルです.ちなみにOpenCVで扱うIplImageと,Bitmapの相互変換は以下のBitmapConverterを使用します.

using OpenCvSharp.Extensions;

// ...

bmp = BitmapConverter.ToBitmap(ipl);
ipl = (OpenCvSharp.IplImage)BitmapConverter.ToIplImage(bmp);

急角度・急斜面に対応するQRデコーダを作る

ここからが本題ですよ!本来は急斜面を考慮しないQRデコーダ,ZXingでも斜め視点すぎるQRには対応しません.普通はそこまでの需要はないはずなんですが,ここから先はマニアック部門ということにしてください.

まず思いつくのは,斜め視点の歪んでいるQRに台形補正(射影変換)をかけて,正方形にすればいいんじゃね!!つまり,QRコードがうまく読み取れない場合はOpenCVで補正した結果を再度デコードするという発想です.
f:id:enlosph:20150913010028j:plain

いいんじゃないかな.以上の変換には,正方形状のQRコードの位置,正確には4つの角の座標が欲しいところです.

QRデコードの仕組み

ところで,QRデコーダってどういう理屈でQRの位置を判定しているのでしょう.ちょっとソースを解読してみましょうか.ZXing .NETのDOWNLOADSのページから,ZXing .Net.Source.0.14.0.0.zipをダウンロード.Base/zxing.vs2012.slnを開いてソリューションの中身をたどっていきます.

OpenCVでQRコード検出器を書く

そういえば,以上の記事にもファインダパタンの話がありました.


QRデコーダでは,黒白黒白黒が1:1:3:1:1の比率である箇所(ファインダパタン.上図の赤枠)をドット走査で発見し,L字型で3つ並んだファインダパタンを基本にしてQRコードの位置を割り出します.さらにQRコードには,そのサイズが大きいほど位置合わせパタン(上図の青枠)が配置され,これをもとに軽い歪みの改善を行っています.これが,QRデコーダが回転角度に強い仕組みです.確かに,QRは逆さまにしても読み取れますよね.

上記のコードで生成したResultオブジェクトには,デコード結果の文字列以外にも,これらの位置座標の情報も含んでいます.具体的には,

  • Result.Text : デコード結果の文字列
  • Result.Points[*] : 0-2はファインダパタン(L字で順に取得する.つまり[1]が折れた箇所),3以降は位置合わせパタン

となります.もちろんZXingでは,ファインダパタンなどの座標を取得後に,文字列のデコードを開始します.しかし,このデコードに失敗した場合,例え座標が取れていてもResultオブジェクトは生成されません.つまりZXingは,なにやらデコードはうまくできないが,位置のわかるQRコードもどきを密かに判定しているのです.

この「デコードはできないくせにファイダパタンは読み取れる」QRコード候補が,例えば「斜め視点すぎて歪んでいるQRコード」などになります.これ,なんだか勿体無いですよね.座標がわかるんだったら,あとはOpenCVにお仕事させるから,せめてそれだけでも出力して!

ZXingライブラリの拡張

そこで本節に移るワケです.ZXingはApache License 2.0ライセンスなので,表記義務などは確認しておいてください.ひとまず改変・再配布はOKのようですね.

まずはライブラリのビルドができるかどうかを確認します.先ほどのzxing.vs2012プロジェクトのプロパティを開きます.で,「署名>アセンブリに署名する」のチェックを外しましょう.
f:id:enlosph:20150913025426j:plain
ビルドを実行すると「直接実行できません」と表示されますが,./Build/Debug/net4.5にzxing.dllとpdbが生成されたら成功です.

次に,ソリューションの中身をチェックします.ファイルがたくさんあるのですが,今回修正が入るのはqrcodeフォルダの中身です.

  1. QRCodeReader.cs : メインの修正箇所.
  2. detector\Decoder.cs : ここ辺りでQRコード候補を確定します
  3. detector\FinderPatternFinder,cs : 前述のファインダパタン探索.知りたい方はここを読む
  4. detector\FinderPatternInfo.cs : ファインダパタンの情報が入るクラスの設計はここ

2.Decoder.csの84-94行目の関数DetectorResultを見てください.ここで取得する変数infoがnullでない時点で,QRコードの候補が見つかっています.つまり,info.BottomLeftなどが出力できます.

1.QRCodeReader.csの91-117行目を見てください.

    if (decoderResult == null)
        return null;

    // If the code was mirrored: swap the bottom-left and the top-right points.
    // ...
    var result = new Result(decoderResult.Text, decoderResult.RawBytes, points, BarcodeFormat.QR_CODE);
    // ...
    
    return result;

ここのdecoderResultがnullというのは,デコード結果の文字列が取得できなかった場合を示します.そこで座標取得をせき止めている木くず(return null;)を取り外します.以下のように修正してください.

    /* ここから追記 */
    Result result = null;
    if (decoderResult == null){
        // return null;
        result = new Result("error", new byte[]{1}, points, BarcodeFormat.QR_CODE);
    }
    else
    {
        // If the code was mirrored: swap the bottom-left and the top-right points.
        // ...
        result = new Result(decoderResult.Text, decoderResult.RawBytes, points, BarcodeFormat.QR_CODE);
        // ...
    }
    /* ここまで追記 */
    return result;

これでうまくは…いかないです.Text情報のないResultオブジェクトを無理やり通すことで起きる弊害があります.この対処も非常に無理やりですが,2点の例外処理を行います.1点目は,pdf417フォルダ内のPDF417Reader.cs,111-128行目.PDF417DetectorResultオブジェクトとそのPointsに関する処理ですが,ここをtry{ ... } catch(NullReferenceException e){ }の中に入れます.2点目は同じフォルダ内の,Detector\\Detector.cs,107-154行目のwhile文.ここはInvalidCastExceptionを例外としてください.

ビルドが成功したら,早速生成したzxing.dllを,自分にプロジェクトに入れて参照してください.

射影変換の実装予告

次の写真を見てください.今回,デコードの成功例を3ルート用意しています

オリジナルのZXingは,Aルートにいくかいくまいか,Textがあるか否かのみを判定します.しかし前節で生成したZXing'(仮)は,Textが読み取れなくてもファインダパターンを取得していれば,とりあえずオブジェクトを生成するような仕組みに変更しました.ここに分岐点があります

ひとまずファインダパタンを取得できていれば,これらの3座標を用いて射影変換を行いましょう.変換結果にZXing'を実行して,うまくいけばBルートの達成です.

一方でCルートは,最初のZxing'処理で何も生成されなかった(TextもPointも見つからなかった)場合にも,射影変換を適用しようという手法です.え?QRコードの座標は?ライブラリの改変は要らなかったの?と思うかもしれませんが,ZXingの力を借りなくても,特徴的なQRコードなんて画像屋さんはすぐに見つけられるでしょ!と思いました.わかんないけど

僕の場合は,画像中央の座標を含む矩形について,縮小・膨張処理やApproxPolyでうまくエッジを抽出し,射影変換用の座標を割り出しています.この辺りの画像処理系は,また長くなるので次回の記事にしようかな…

なお,ソースコードの公開を準備中です.

ほかにOpenCV補正でできること

http://31.media.tumblr.com/619a54bcdb9bf0fb9907e233661ed95c/tumblr_nmqa3eOq9j1ush50oo3_400.gif
円柱上のQRコードを読み取っています

この曲面補正については,逆ワープ処理とかいうスマートな方法でも何でもなく,画素1個1個を動かして無理やり正方形状にした結果で,何故か取得できている事例です.QRデコーダすごいよ….

ある距離,ある角度で固定しないとこの手法は当然のように全然取得できません.したがってソースも公開しませんが,詳しい方はぜひ素敵な素敵な曲面上デコーダを作って欲しい…と画像屋さんに投げるのでした.

おわりに

次回はOpenCV#編ですが,先にGitHubでの公開について本記事で追記します.