虎の穴開発室ブログ

虎の穴ラボ株式会社所属のエンジニアが書く技術ブログです

MENU

バーコード を読めるようになりたかったので ~ Shape Detection API を使ってみる~

皆さん、こんにちは。 とらのあなラボのおっくんです。

「バーコードを見れば、なんとなくは読める」となんてことを言う友人がいます。 詳しく聞くと、すべてではないけれど一次元バーコードであれば目で見てなんとなく読めるんだそうです。

言うからには「読めると何かしら便利なんだろうな」と感じたので、技術に頼ってバーコードを読んでみることにしました。 使用する「Shape Detection API」と作成したアプリを紹介します。

最終的に完成したのがこちらです。

f:id:toranoana-lab:20200811205910g:plain

順を追って解説します。

実行環境

開発環境

  • OS:macOS Catalina 10.15.4
  • Chrome 84.0.4147.89

確認用デバイス

  • OS:Android バージョン 10
  • Chrome 83.0.4103.106

Shape Detection API とは

Shape Detection API は、外部のライブラリに依存せずに画像から情報の検出を行う API 群です。 現在 W3C で採択中の API です。

Shape Detection API は、以下の 3 種類の検出機能を提供します

  • 顔(FaceDetector)
  • バーコード(BarcodeDetector)
  • テキスト(TextDetector)

今回は、これらの中からバーコードの検出を行うBarcodeDetectorを使っていきます。

デバッグ環境準備

開発に当たり、開発環境で立ち上げた Web サーバーを Android が参照できるようにする必要があります。

まず、Android を開発者モードにします。

Android Studio - デバイスの開発者向けオプションを設定するに記載があります。

developer.android.com

続いて、Android を開発環境に接続します。

Chrome DevTools - Android 端末のリモート デバッグを行うを参照して、リモートデバッグできる環境を用意します。

developers.google.com

開発環境から、Android で立ち上げた Chrome を参照できるようになったら、準備完了です。

開発中機能の有効化

今回、使用する Shape Detection API は、以前ご紹介した Web NFC のように現在の Chrome で stable な機能ではありません。

有効化する必要があります。 有効化の方法は、Web NFC を試してみました 〜 動作確認とアプリケーション作成 〜 にて紹介していますので、こちらを参照ください。

toranoana-lab.hatenablog.com

参考資料

今回の実装は、W3C Community Group Draft Report - Accelerated Shape Detection in Imagesを参考に進めます。 サンプルコードが豊富なので、全編英語ではあるものの進めやすいと思います。

バーコードを画像として読み取る

Shape Detection API の動作確認の手始めに、カメラは使用せず、画像上のバーコードの読み取りを試みます。

(read.html)

<html>
  <head>
    <script
      src="https://code.jquery.com/jquery-3.5.1.slim.min.js"
      integrity="sha256-4+XzXVhsDmqanXGHaHvgh1gMQKX40OUvDEBTu8JcmNs="
      crossorigin="anonymous"
    ></script>
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css"
    />
  </head>
  <body>
    <section>
      <div class="container">
        <img src="qr.png" width="640px" id="data_source" />
        <div id="message"></div>
      </div>
    </section>
    <section>
      <div class="container">
        <div class="row">
          <table class="table is-bordered is-fullwidth">
            <tbody>
              <tr>
                <td>フォーマット</td>
                <td id="format"></td>
              </tr>
              <tr>
                <td></td>
                <td id="raw_value"></td>
              </tr>
              <tr>
                <td></td>
                <td id="width"></td>
              </tr>
              <tr>
                <td>高さ</td>
                <td id="height"></td>
              </tr>
              <tr>
                <td>左上x座標</td>
                <td id="top_left_x"></td>
              </tr>
              <tr>
                <td>左上y座標</td>
                <td id="top_left_y"></td>
              </tr>
            </tbody>
          </table>
        </div>
      </div>
    </section>
    <script type="text/javascript" src="image_read_app.js"></script>
  </body>
</html>

(image_read_app.js)

const barcodeDetector = new BarcodeDetector();

const image = document.getElementById("data_source");

window.onload = async () => {
  let code = null;
  try {
    code = await barcodeDetector.detect(image);
  } catch {
    $("#message").text("ERROR");
  }

  if (code == null) {
    return;
  }

  console.log(code); //<=取得できたデータを確認

  $("#message").html("SUCCESS");

  for (const barcode of code) {
    $("#format").text(barcode.format);
    $("#raw_value").text(barcode.rawValue);
    $("#width").text(barcode.boundingBox.width);
    $("#height").text(barcode.boundingBox.height);
    $("#top_left_x").text(barcode.boundingBox.x);
    $("#top_left_y").text(barcode.boundingBox.y);
  }
};

上記の用意したページにアクセスします。 実行結果は、次のようになります。

f:id:toranoana-lab:20200811205921p:plain

QR コードに書き込まれていたのは、このブログの URL でした。

image_read_app.js の 17 行目で、コンソールに取得結果を表示しています。

f:id:toranoana-lab:20200811205859p:plain

取得結果には、バーコードの値だけなく詳細な座標情報も含むことがわかります。
サンプルコードでは、これらの中からバーコードのフォーマット、値、幅・高さ、左上の x・y 座標を表示しました。

QR コードのような 2 次元バーコードだけでなく、1 次元バーコードも読み取りすることができます。

f:id:toranoana-lab:20200811205855p:plain

こちらのバーコードは、code39 形式になっています。

Shape Detection API では、以下の形式のバーコードが対応していると記載があります。

  • aztec
  • code_128
  • code_39
  • code_93
  • codabar
  • data_matrix
  • ean_13
  • ean_8
  • itf
  • pdf417
  • qr_code
  • upc_a
  • upc_e

フリーのバーコード作成サイトなどがあるので、探して使うといいでしょう。

カメラから取り込んで、「バーコードを目で読む」を実現してみる

ここからは、カメラを使ってバーコードの情報を読み取っていきます。 バーコードの座標を取得できることも確認できているので、それらを利用して以下の要件で実装します。

  • 読み取ったバーコードの「位置」を画像上で示す
  • 画像上でバーコードの「内容」を示す

これら達成のため、以下の通り実装します。

(barcord_eye_reader.html)

<html>
  <head> </head>
  <body>
    <div id="canvas_area">
      <canvas id="result" width="500" height="500"></canvas>
    </div>
    <div id="result_text"></div>
    <script type="text/javascript" src="barcord_eye_reader_app.js"></script>
  </body>
</html>

(barcord_eye_reader_app.js)

const barcodeDetector = new BarcodeDetector();

// streamを入力するvideoを作成する
const image = document.createElement("video");

// 検出と加工する非表示のcanvasを作成する
const offscreen_canvas = document.createElement("canvas");
const offscreen_context = offscreen_canvas.getContext("2d");

// 最終的に取得した画像を表示するcanvasを取得する
const canvas = document.querySelector("#result");
const context = canvas.getContext("2d");

//カメラと中間処理のキャンバスのサイズを最終的に表示するキャンバスを基準に設定
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

offscreen_canvas.width = canvas.width;
image.videoWidth = canvas.width;
offscreen_canvas.height = canvas.height;
image.videoHeight = canvas.height;

window.onload = async () => {
  //カメラを取得
  const stream = await navigator.mediaDevices.getUserMedia({
    video: {
      facingMode: { exact: "environment" },
    },
  });

  //オブジェクトと関連付ける
  image.srcObject = stream;
  image.play();

  //バーコードの解析処理自体の実行
  analysis();
};

const analysis = async () => {
  //カメラの入力をCanvasに書き込む
  offscreen_context.drawImage(image, 0, 0, canvas.width, canvas.height);

  let code = null;

  try {
    code = await barcodeDetector.detect(offscreen_canvas);
  } catch (e) {
    console.log(e);
  }

  let state = true;

  if (code == null) {
    state = false;
  }
  if (state == true && code.length == 0) {
    state = false;
  }

  //バーコードの値が取れていた場合、赤い線で囲む
  if (state) {
    //バーコードを囲む処理
    offscreen_context.strokeStyle = "rgb(255, 100, 100) ";
    offscreen_context.lineWidth = 10;
    offscreen_context.beginPath(
      code[0].cornerPoints[0].x,
      code[0].cornerPoints[0].y
    );
    offscreen_context.lineTo(
      code[0].cornerPoints[1].x,
      code[0].cornerPoints[1].y
    );
    offscreen_context.lineTo(
      code[0].cornerPoints[2].x,
      code[0].cornerPoints[2].y
    );
    offscreen_context.lineTo(
      code[0].cornerPoints[3].x,
      code[0].cornerPoints[3].y
    );
    offscreen_context.lineTo(
      code[0].cornerPoints[0].x,
      code[0].cornerPoints[0].y
    );
    offscreen_context.closePath();
    offscreen_context.stroke();

    //バーコードから取得した文字列の表示前加工
    //バーコードを囲んだ四角の中に文字列が収まるように、収まる文字数と改行の回数を計算する
    const w = code[0].boundingBox.width - 20;
    const split_chr_count = Math.floor(w / 25);
    const split_loop_count = Math.ceil(
      code[0].rawValue.length / split_chr_count
    );

    let viewertext = [];
    for (let i = 0; i < split_loop_count; i++) {
      if (code[0].rawValue.length > i * split_chr_count) {
        const a = code[0].rawValue.substr(i * split_chr_count, split_chr_count);
        viewertext.push(
          `${code[0].rawValue.substr(i * split_chr_count, split_chr_count)}\n`
        );
      } else {
        const a = code[0].rawValue.substr(
          i * split_chr_count,
          code[0].rawValue.length - 1
        );
        viewertext.push(
          `${code[0].rawValue.substr(
            i * split_chr_count,
            code[0].rawValue.length - 1
          )}`
        );
      }
    }
    offscreen_context.fillStyle = "rgb(255, 100, 100) ";
    offscreen_context.font = "bold 50px Times Roman";
    offscreen_context.textAlign = "start";

    viewertext.forEach((text, index) => {
      offscreen_context.fillText(
        text,
        code[0].cornerPoints[0].x + 10,
        code[0].cornerPoints[0].y + 50 * (index + 1)
      );
    });
  }

  context.drawImage(offscreen_canvas, 0, 0, canvas.width, canvas.height);
  window.requestAnimationFrame(analysis);
};

取得したカメラのストリームから、情報取得と一次加工用の canvas に静止画として切り出します。

動作確認の様子が、冒頭の動画になります。

f:id:toranoana-lab:20200811205910g:plain

座標取得の精度については、2次元バーコードのほうが安定している様子がわかります。

バーコードを目で読めるという友人には、こんなふうに見えているのかもしれませんね。

今回は、Shape Detection API を使用してバーコード読み取りを試してみました。
レジで読んでもらったり、SNSのアカウントをシェアしたり普段は「利用」することが多いバーコードですが、
読み取りそのものや枠での表現方法を考えるとなかなか面白いです。
今回は枠で囲んで、値を表示をしましたがほかにも面白い表現があると感じています。

また、バーコードにも多数の規格があるということを学べたことが収穫でした。
その中でも、pdf417 形式は推しバーコード形式といってもいいでしょう。
なんだか、かっこよさを感じています。

f:id:toranoana-lab:20200811205916p:plain

最後に、
Q.バーコードを目で見て読めると便利か?
A.何かしら機械で読んだほうが活用しやすいと思う。目で見て読める利点はあまり感じられませんでした。

P.S

「虎の穴ラボ」社員による2回目のトークイベントが 8/20(木)19:30〜 オンライン開催します!
前回はリモートワークについてでしたが、今回は「オタク企業で働くエンジニアってどうなの?」というテーマとなっています。
yumenosora.connpass.com

また、虎の穴ラボ主催のオンラインライトニングトークイベントを 8/26(水)19:30〜 開催します!
今回もフリーテーマとなっており、ITに関連する内容であれば、何でも大歓迎ですので、初心者の方も練習の場としてお気軽にご参加ください! connpassにて参加受付中です!
yumenosora.connpass.com

虎の穴ラボでの開発に少しでも興味を持っていただけた方は、採用説明会やカジュアル面談という場でもっと深くお話しすることもできます。ぜひお気軽に申し込みいただければ幸いです。
カジュアル面談では虎の穴ラボのエンジニアが、開発プロセスの内容であったり、「今期何見ました?」といったオタクトークから業務の話まで何でもお応えします。

カジュアル面談や採用情報はこちらをご確認ください。
yumenosora.co.jp

QRコードは(株)デンソーウェーブの登録商標です