3
Help us understand the problem. What are the problem?

posted at

updated at

LINE上で動くバーコードリーダーを作った

作ったもの

LIFFアプリにバーコード読み取り機能を付けたかったけど、公式ではサポートしてなかったので自作してみました。ご自身のスマホからお試し頂けます→デモサイト

ライブラリを選定

LINE API Use Caseというサイトに使えそうなプロジェクトがあったのですが、内部でBarcode Scanner SDKというライブラリを使っていて、こちらは有償のようなので別のものを探しました。

GitHubで探してみると QuaggaJS なるものを発見。ドキュメントも充実していて良さげだったのでこちらを使ってみました。

使い方を調べてみる

QuaggaJSは以下の2パターンの方法で使用できるようです。

  1. 画像ファイルからバーコードを読み取る
  2. カメラからライブストリームを取得してスキャンする

カメラから取得する場合は HTTPS のサイト(もしくはlocalhost)でないと利用できないので注意が必要です。これは内部で madiaDevices.getUserMedia というAPI を使っていて、その仕様によるものです。

madiaDevices.getUserMedia ....デバイスのカメラやマイクなどの入力にアクセスしてメディアストリームを取得するWeb APIです。ブラウザはカメラやマイクへのアクセス許可を要求します。 MediaDevices.getUserMedia() - Web API | MDN

読み取りページを作成

HTMLファイル

LIFFを使うためにSDKを追加します。QuaggaJSはCDNが公開されているのでそちらも読み込みます。

scanner.html
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>barcode scanner</title>
    <link rel="stylesheet" href="scanner.css" />
</head>
<body>
    <div>
        <div id="photo-area" class="viewport">
            <input id="result" size="24" value="バーコードをスキャンして下さい"></input>
        </div>
        <p id="log"></p>
    </div>
    <script src="https://static.line-scdn.net/liff/edge/2.1/sdk.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/quagga/0.12.1/quagga.min.js"></script>
    <script src="scanner.js" type="text/javascript"></script>
</body>
</html>

スタイルシート

カメラから受け取った映像を映すキャンバスの位置やサイズを調整します。

scanner.css
#photo-area.viewport {
   width: 100%;
   height: auto;
}
#photo-area.viewport canvas, video {
   width: 100%;
   height: auto;
}
#photo-area.viewport canvas.drawingBuffer, video.drawingBuffer {
   margin-left: -100%;
}

バーコードスキャンのスクリプト

誤検知を防ぐため、スキャン後にごにょごにょ処理しています。JANコードの読み取りに使う場合はチェックデジットの処理も入れた方が良いかもしれません。

scanner.js
// 初期化
Quagga.init({
    inputStream: {
        name: "Live",
        type: "LiveStream",
        target: document.querySelector('#photo-area'),
        constraints: {
            decodeBarCodeRate: 3,
            successTimeout: 500,
            codeRepetition: true,
            tryVertical: false,
            frameRate: 15,
            width: 640,
            height: 480,
            facingMode: "environment"
        },
    },
    decoder: {
        readers: [
            "ean_reader"
        ]
    },
}, function (err) {
    if (err) {
        console.log(err);
        return;
    }
    Quagga.start();

});

// バーコードを検出したときの処理
Quagga.onProcessed(function (result) {
    var drawingCtx = Quagga.canvas.ctx.overlay,
        drawingCanvas = Quagga.canvas.dom.overlay;
    if (result) {
        // 認識したバーコードを囲む
        if (result.boxes) {
            drawingCtx.clearRect(0, 0, parseInt(drawingCanvas.getAttribute("width")), parseInt(drawingCanvas.getAttribute("height")));
            result.boxes.filter(function (box) {
                return box !== result.box;
            }).forEach(function (box) {
                Quagga.ImageDebug.drawPath(box, {
                    x: 0,
                    y: 1
                }, drawingCtx, {
                    color: "white",
                    lineWidth: 2
                });
            });
        }
        // 読み取ったバーコードを囲む
        if (result.box) {
            Quagga.ImageDebug.drawPath(result.box, {
                x: 0,
                y: 1
            }, drawingCtx, {
                color: "#01f965",
                lineWidth: 2
            });
        }
        // 読み取ったバーコードに線を引く
        if (result.codeResult && result.codeResult.code) {
            Quagga.ImageDebug.drawPath(result.line, {
                x: 'x',
                y: 'y'
            }, drawingCtx, {
                color: '#00b900',
                lineWidth: 2
            });
        }
    }
});

let scanResults = [];
let resultBuffer = [];

// 検知後の処理
Quagga.onDetected(function (result) {
    // 1つでもエラー率0.19以上があれば除外
    let isErr = false
    $.each(result.codeResult.decodedCodes, function (id, error) {
        if (error.error != undefined) {
            if (parseFloat(error.error) > 0.19) {
                isErr = true
            }
        }
    })
    if (isErr) return

    // エラー率の中央値が0.1以上なら除外
    const errors = result.codeResult.decodedCodes.filter((_) => _.error !== undefined).map((_) => _.error)
    const median = _getMedian(errors)
    if (median > 0.1) {
        return
    }

    // 3回連続で同じ値だった場合のみ採用
    scanResults.push(result.codeResult.code)
    if (scanResults.length < 3) {
        return
    }
    if (scanResults[0] !== scanResults[1]) {
        scanResults.shift()
        return
    }

    // 複数回目は前回と値が違う時だけ発火
    if (resultBuffer.length > 0) {
        if (resultBuffer.slice(-1)[0] === result.codeResult.code) {
            return
        }
    }

    resultBuffer.push(result.codeResult.code);
    $('#result')[0].value = result.codeResult.code;

    if (confirm(result.codeResult.code)) {
        document.dispatchEvent(event);
    }
});

// 中央値を取得
function _getMedian(arr) {
    arr.sort((a, b) => a - b)
    const half = Math.floor(arr.length / 2)
    if (arr.length % 2 === 1)
        return arr[half]
    return (arr[half - 1] + arr[half]) / 2.0
}

まとめ

LINEアプリ内でサクッとバーコードを読めるようになりました。精度も良く、結構使い道がありそうです。

毎回カメラの許可を求められるところは何とかしたいですね。。永続的に許可する方法をご存知の方がいたら教えてください。

ソースコードはGitHubで公開してますのでよかったら使ってください。
kota-imai/liff-barcode-scanner

宣伝

LINEDCでハンズオンします。ご興味ある方はぜひ!

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
Sign upLogin
3
Help us understand the problem. What are the problem?