Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

【.NET】OpenCVSharpとZXing.Netを使ったバーコード読み込み

More than 1 year has passed since last update.

はじめに

これはOpenCV Advent Calendar 2017の13日目の記事となります。

本来はバーコード読み取り精度を高めた上で記事にする予定だったのですが、別の開発の遅れにより13日に間に合わなかったので、それは後々とします。
【2017/12/23追記】14日以降に幾つか分かったことを追記しました。

お仕事でKEYENCEやDENSOなどの専用バーコードスキャナーを使っています。最近は、アスタリスク社のiPod Touchを使ったバーコードスキャナーの提案も受けます。

今回、マイクロソフト Surface Pro 4 の内蔵カメラを使って、バーコードを読めないかという依頼がありました。Surfaceなら持ち歩けるので、その場で読みたい。バーコードスキャナーを一緒に持ち歩きたくない。

QRコード

QRコード(Quick Responce Code)は1994年にデンソーウェブの開発した、マトリックス型2次元コードの一種です。名前の通り、高速読取を目的に開発されました。

開発環境

マイクロソフト Surface Pro 4 バックカメラ 800万画素

カメラ操作にOpenCVSharp 、バーコード読取りにZXing(ゼブラクロッシング)を使用します。
NuGetから現在バージョンとして、OpenCVSharp v3.3.1.20171117 と ZXing.Net v0.16.2 をインストールしています。

クラス分け

Cameraクラスを別途作成し、Windowsフォーム側と分離するようにしています。
画像処理はMat型を受け渡ししています。バーコード解析処理もCameraクラスに内包しています。
※ソースコードを一部掲載していますが、掲載用に編集しているのでそのままでは動きません。

デリゲート(委譲)

CameraクラスからWindowsフォーム側にアクセスしたい場合、デリゲートを使っています。

// 【Cameraクラス側】
public delegate void DisplayProgress(Mat img, int threshold);
// デコード処理
public string Decode(int min, int max, int add, Mat img, int threshold, DisplayProgress callback)
{
    // (中略)
    // フォーム側処理を呼ぶ
    callback?.Invoke(img2, value);
    // (中略)
}

// 【フォーム側処理】
// しきい値を中央に解析する
string value = await Task.Run(() => _camera.Decode(min, max, 1, img, threshold, AnalyzeDisplay));

public void AnalyzeDisplay(Mat img, int threshold)
{
    this.Invoke(new Action(() =>
    { 
        picCapture.ImageIpl = img;
        txtThreshold.Text = threshold.ToString();
    }));
}

カメラ画像の読取り

カメラ画像の読取りには、BackgroundWorkerクラスを使用しています。キャプチャー画像はPictureBoxIplコントロール(picMain)をCenterImageにして中央にQRコードを表示するようにしています。

// 【Cameraクラス側】
VideoCapture _cap = null;

public Camera()
{
    // カメラ有無 0:バックカメラ 1:フロントカメラ
    _cap = new VideoCapture(0) { AutoFocus = false };
    Enabled = _cap.IsOpened();
}

// 【フォーム側処理】
bool _isRunning = false;

// バッググラウンド処理
private void bgWorker_DoWork(object sender, DoWorkEventArgs e)
{
    BackgroundWorker worker = (BackgroundWorker)sender;

    if (!_camera.Enabled) return;

    while (_isRunning)
    {
        // bgWorker_ProgressChangedイベントで画像取得
        Mat dst = _camera.Read();
        worker.ReportProgress(0, dst);
    }
}

// バッググラウンド進行変更イベント処理
private void bgWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    // カメラ画像表示
    picMain.ImageIpl = (Mat)e.UserState;
}

// キャプチャー開始処理
private void CaputureStart()
{
    _isRunning = true;
    bgWorker.RunWorkerAsync();
    tmrCapture.Enabled = true;
}

// キャプチャー停止処理
private void CaputureStop()
{
    _isRunning = false;
    bgWorker.CancelAsync();
    tmrCapture.Enabled = false;
}

ターゲット枠配置

ユーザーがQRコードを中央の位置に合わせやすいように、赤色十字のターゲット枠を配置します。
キャプチャー画像にターゲット枠を直接描画すると解析上おかしなことになるので、別のPictureBoxにターゲット枠イメージ(png)を用意して、BackColor = Color.Transparent にした上で子コントロール化することでターゲット枠を重ね合せています。PictureBox上のLabelの背景が透明にならない問題の解決法

// ターゲットアイコン用(メイン用)
picMain.Controls.Add(picTargetMain);
picTargetMain.Left -= picMain.Left;
picTargetMain.Top -= picMain.Top;

解析処理

タイマー(1000ms以内)を使って、ぶれ防止を兼ねてキャプチャー画像を一旦保存した上で、キャプチャー画像サイズが大きく、解析部分を減らすために中央に映っているある程度の範囲のみにトリミングしてから解析処理をしていきます。

// キャプチャー画面
const string CAPTURE_PATH = "caputure.png";

// タイマー処理
private void tmrCapture_Tick(object sender, EventArgs e)
{
    if (picMain.Image == null) return;

    // 画面キャプチャー保存
    picMain.ImageIpl.SaveImage(CAPTURE_PATH);

    // 画像読込(中央あたりをトリミング)
    Mat src = new Mat(CAPTURE_PATH);
    Rect rc = new Rect((src.Width - picMain.Width) / 2,
                       (src.Height - picMain.Height) / 2 - Camera.IMAGE_MERGIN_SIZE,
                       picMain.Width,
                       picMain.Height + Camera.IMAGE_MERGIN_SIZE);
    picCapture.ImageIpl = new Mat(new Mat(CAPTURE_PATH), rc);

    // そのまま解析
    Result result = _camera.Analyze(picCapture.ImageIpl);
    if (result == null) return;

    string decode = result.ToString().Trim();
    if (decode != "" && decode != "error")
    {
        // 解析出来たなら入力値へ送信
        Send(decode);
    }

    // 解析可能性がある
    if (decode == "error")
    {
        // 自動解析処理
        AnalyzeAuto(picCapture.ImageIpl, trbThreshold.Value);
    }
}

読み取り精度を高めたい

2cm以上のQRバーコードはなんなく読めたのですが、ユーザーから実際に使用するバーコードが送られてきたところ、ケーブルに付けられた8mmのQRコードでありました。それだとごくまれに読める程度で実用には程遠い状態です。
しかし、専用バーコードスキャナーはレーザーなのでなんなく読めるのです。

ケーブルの紙は少し角度が付いていることもあり、8mmのQRコードを専用プリンタで印刷して平面状態にしたところ、少しは読めるようになったのですが、まだまだ実用には程遠い状態です。

試行錯誤が始まりました。

【2018/07/18追記】
8mmのQRコードはソフトウェアでの対応は難しく(カメラのピントが合わせ難いやQRコードが丸まってると読み取りできない)、結局ハードウェアの小型QRコードリーダーUSB対応(例 cubeQR)で対応することになりました。

試行1 グレースケール化

カメラ画像をグレースケール化します。気休めかも知れません。
【2017/12/23追記】2値化する上でグレースケール化は必須です。

OpenCvSharpを使う その3 (グレースケールに変換)

// カメラ画像の読込み
public Mat Read()
{
    Mat result = null;

    if (_cap.IsOpened())
    {
        Mat frame = new Mat();
        result = new Mat();

        // カメラ画像の読込み
        _cap.Read(frame);
        // グレースケール化
        Cv2.CvtColor(frame, result, ColorConversionCodes.BGRA2GRAY);
    }

    return result;
}

試行2 先鋭化と2値化

先鋭化または2値化をやってみましたが、逆に読めなくなってしまいました。これは研究中です。

【2017/12/23追記】
見た目では読めそうなQRコードを読めるようにするのに色々検討した結果、先鋭化は駄目でしたが2値化ではしきい値を調整すること読めるようになりました。
画像の閾値処理(2 値化)

しきい値をどうやって決めるのかですが、256段階にグレースケール化して使用されている白色が一番多い値(周辺の明るさ)を算出します。
例えば算出値が209と明るめの場合、190~200の範囲で2値化して解析して読めるかを繰り返します。すると194でerrorの値となり、195で解析可能となりました。ちなみに196では解析不可でした。
同様に算出値が90と暗めの場合、70~80の範囲で2値化すると、75で解析可能となりました。

手動(ボタンをクリックして解析)の場合には数秒は待ってもらえるので、2値化する範囲を広げてじっくり解析できます。自動(リアルタイム)では、2値化する範囲を狭めて解析します。その基準となるしきい値はトラックバーを用意してユーザー側である程度調整させるのと、手動で読めた際のしきい値を反映させます。
ここらへんはどうするのか検討中です。

試行3 マクロレンズ

ソフトが駄目ならハードでってことで、100均ショップにスマホ用カメラにクリップで付けるマクロレンズが売っていたので購入しました。

確かに付けることで少しは読めるようになるのですが、SurfaceとQRコードをかなり近づけることになるので、運用上あまりイケていない。あくまでソフト側で対応することにしました。

【2017/12/23追記】
Surfaceは借り物だったので返してしまった。開発PCの内蔵カメラは性能が低いので、マクロレンズを使わないとボケてしまう。さすがにボケると滲んで2値化で調整しても読めないので今は必須です。

試行4 読み取り種類の限定化

TryHarderオプションの有効化と読み取るバーコードの種類をBarcodeFormat.CODE_39, BarcodeFormat.QR_CODEの2種類にしました。

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

// 解析処理
public Result Analyze(Mat img)
{
    BarcodeReader reader = new BarcodeReader
    {
        AutoRotate = true,
        TryInverted = true,
        // Code-39とQRコードに限定
        Options = new ZXing.Common.DecodingOptions
        {
            TryHarder = true,
            PossibleFormats = new[] { BarcodeFormat.QR_CODE }.ToList()
        }
    };

    return reader.Decode(img.ToBitmap());
}

試行5 射影変換

ZXingはファインダパタンなどの座標を取得後に文字列のデコードを開始します。しかし、このデコードに失敗した場合に例え座標が取れていてもResultオブジェクトは生成されません。これは非常に勿体無い。
その為、QRコードの認識位置を取得するようにZXingライブラリを修正する。
座標が取れてデコードが失敗する場合、QRコードの部分を抜き出して、射影変換後に再度デコードさせる。これで少しは良くなりました。

【2017/12/23追記】
2値化で読めるようになったので射影変換は止めました。ある程度、傾いていても誤り訂正で読んでくれたので、必要性に迫られたら再度検討します。

Result情報

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

【2017/12/23追記】
ResultPointは、左下→左上→右上の順の時と、右下→左下→右上の順の時がありました。
ResultPointはファインダパタンの中央点となるので、端までの差を決めてあげればQRコード部分のみの画像が取得できます。※射影変換をやめたので今は使っていません。

// ファインダパタンの中央点から端までの差
public const int IMAGE_MERGIN_SIZE = 22;

/// <summary>
/// ポイント情報からQRコード部分のサイズを取得する
/// </summary>
/// <param name="result">ポイント情報</param>
/// <param name="width">最大幅</param>
/// <param name="height">最大高さ</param>
/// <returns>QRコード部分のサイズ</returns>
public Rect GetPoints(Result result, int width, int height)
{
    // ポイント情報からQRコード部分を切り取る
    int stX = 0;
    int stY = 0;
    int w = 0;
    int h = 0;
    if (result.ResultPoints[0].X < result.ResultPoints[2].X)
    {
        // 左下→左上→右上
        stX = (int)GetMinValue(result.ResultPoints[0].X, result.ResultPoints[1].X) - IMAGE_MERGIN_SIZE;
        stY = (int)GetMinValue(result.ResultPoints[1].Y, result.ResultPoints[2].Y) - IMAGE_MERGIN_SIZE;
        w = (int)result.ResultPoints[2].X + IMAGE_MERGIN_SIZE - stX;
        h = (int)result.ResultPoints[0].Y + IMAGE_MERGIN_SIZE - stY;
    }
    else
    {
        // 右下→左下→右上
        stX = (int)GetMinValue(result.ResultPoints[1].X, result.ResultPoints[2].X) - IMAGE_MERGIN_SIZE;
        stY = (int)(result.ResultPoints[2].Y - IMAGE_MERGIN_SIZE);
        w = (int)result.ResultPoints[0].X + IMAGE_MERGIN_SIZE - stX;
        h = (int)result.ResultPoints[1].Y + IMAGE_MERGIN_SIZE - stY;
    }

    stX = (int)GetMaxValue(stX, 0);
    stY = (int)GetMaxValue(stY, 0);
    w = (int)GetMinValue(w, width);
    h = (int)GetMinValue(h, height);

    return new Rect(stX, stY, w, h);
}

// 引数の小さい値を取得する
private float GetMinValue(float min1, float min2)
{
    return (min1 <= min2 ? min1 : min2);
}

// 引数の大きい値を取得する
private float GetMaxValue(float max1, float max2)
{
    return (min1 >= min2 ? max1 : max2);
}

OpneCVのPointで取得位置の順番

左上、左下、右下、右上の順

ZXing.Netのデバッグ

読めるQRコードと読めないQRコードの画像を比較しても、見た目に違いがないのに読めない。
その原因を探るにはZXing.Netのライブラリをデバッグしていけば何か分かるはず。

ソースコードをダウンロードします。
https://github.com/micjahn/ZXing.Net

今回のアプリケーションは.NET4.6を使用しているので、プロジェクトに.Net4.6用のzxing.net4.6.csprojを追加します。
これで、Decodeメソッドでステップインしていけば、ZXing.Netに入っていけます。

最後に

14日あたりから、QRコード読み取りの研究に再度取り組むことになるので1週間後にまた記事を更新できると思います。

【2017/12/23追記】
14日以降で分かった範囲を追記しました。

参照

OpenCVSharp

OpenCVSharp自体は、日本の方が開発している。

導入と使い方

カメラ

Mat型

OpenCVの画像はMat型変数で表現されます。Matという名前は matrix で行列の略です。その為、行列なので(行、列)の順に指定されるため、一般の画像指定の(x, y)ではない。※x, yの順番が逆なことに注意!!

定数 内容
CV_8UC1 8ビット、符号なし、1チャンネルのデータ
CV_8UC3 8ビット、符号なし、3チャンネルのデータ

※CV_8Uは0から255の整数
※1画素はB, G, Rの順に格納されている。

画像アクセス

MATとBITMAP相互変換

ZXing(ゼブラクロッシング)

ZXing(ゼブラクロッシング)はGoogle が開発して公開している、様々な一次元や二次元のバーコードの生成/操作ができるオープンソースライブラリです。

その他

yaju
静岡県島田市在住ののシニアSE(元Microsoft MVP 2010-2012)がコンピューター、機械学習、Unity、数学について考える。
http://yaju3d.hatenablog.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away