Help us understand the problem. What is going on with this article?

サクッとjsでQRリーダー実装

More than 1 year has passed since last update.

こんばんは。
最近チャリンコを購入して、チャリンコ通勤始めました。道中のお弁当屋とか発見できてQOL爆上げ中のmorifujiです。

背景

  • Vuejsでできること色々調べる機会があった
  • いろんなWebAPIが存在していることに気づいた
  • 案件の仕様の中でwebアプリケーション内のみでqr読み取る必要が出てきた

目標

  1. ブラウザで指定のURLを開き
  2. QRリーダーを起動(スマホのカメラ画面にならずに)
  3. QRを撮影すると、瞬時に解析して画面に表示

※スマートフォンでの検証はまだ。確認し次第追記します :bow:

実装後デモ▼
chat_2.gif

shape detection API (不採用)

リアルタイムに物体を認識するためのAPI。
人間の顔(FaceDetection)とバーコード(BarcodeDetection)の二種類がある。
https://wicg.github.io/shape-detection-api/

試してみた。

// 顔認識
if (window.FaceDetector == undefined) {
  console.error('Face Detection not supported on this platform');
}

// バーコード認識
if (window.BarcodeDetector == undefined) {
  console.error('Barcode Detection not supported on this platform');
}

let barcodeDetector = new BarcodeDetector();
// Assuming |theImage| is e.g. a <img> content, or a Blob.
barcodeDetector.detect(theImage)
.then(detectedCodes => {
  for (const barcode of detectedCodes) {
    console.log(' Barcode ${barcode.rawValue}' +
        ' @ (${barcode.boundingBox.x}, ${barcode.boundingBox.y}) with size' +
        ' ${barcode.boundingBox.width}x${barcode.boundingBox.height}');
  }
}).catch(() => {
  console.error("Barcode Detection failed, boo.");
})

結果。どれも未対応。悲しい

ブラウザ 対応有無
firefox X
iOSのsafari X
chrome X

getUserMedia

WebRTCを実現するためのAPIの一種。以下の二種類あるが、前者Navigatorはバグが発見されているので、廃止予定となっている。

試して見た。

js部分▼

let p = navigator.mediaDevices.getUserMedia({ audio: false, video: true });

p.then(function(stream) {
   var video = document.querySelector('video');
   video.src = window.URL.createObjectURL(mediaStream);
   video.onloadedmetadata = function(e) {
      // Do something with the video here.
   };
};

html部分▼

        <div class="container">
          <div class="columns">
            <div class="column">
              <video
                :width="width"
                :height="height"
                autoplay/>
            </div>
           </div>
         </div>

firefoxでできた!!

スクリーンショット 2018-06-13 23.21.13.png

しかし、createObjectURLは廃止予定だから使うな!という警告をconsoleログで受けた。。。
調べてみるとバグがあるらしい。

スクリーンショット 2018-06-13 23.22.46.png

createObjectURLではなく、videoのdomのsrcObjectに直接ぶっ込めばいいとのこと。修正▼

// var video = document.querySelector('video');
// video.src = window.URL.createObjectURL(mediaStream);
document.querySelector("video").srcObject = mediaStream;

canvasにコピー

qrを読み取るライブラリはあまたあるようだが、そのどれも、 canvasからimageを取得している。
なのでまずはwebカメラを表示しているvideoタグの動画を、canvasにコピーしないといけない。

以下、先ほどのコードに追記

 const video = document.querySelector("video");
 const canv = document.querySelector("canvas");
 const context = canv.getContext("2d");

// canvasに描写
 context.drawImage(video, 0, 0, this.width, this.height);

これでボタンを押すたびにvideoタグからcanvasにコピーする(スクショする)ような感じになった。

chat.gif

※上のgifだと、domのサイズ指定してないのでcanvasが引き伸ばされてます

LazarSoft/jsqrcode (不採用)

こちらの記事を参考にQR画像からデータをデコードするライブラリを探した。

start数につられてLazarSoft/jsqrcodeを使おうとしたが、良いサンプルがなかったのと、最終更新が古かったので、やめた、

cozmo/jsQR

最近も開発が進んでいるようなので、このライブラリに決めた。使い方はこんな感じ

const code = jsQR(imageData, width, height);

if (code) {
  console.log("Found QR code", code);
}

めっちゃ簡単。今までのと合わせると、以下のような感じ

      const video = document.querySelector("video");
      let canv = document.querySelector("canvas");

      const context = canv.getContext("2d");
      context.drawImage(video, 0, 0, this.width, this.height);
      const imageData = context.getImageData(0, 0, this.width, this.height);
      const code = jsQR(imageData.data, imageData.width, imageData.height);
      if (code) {
        console.log("Found QR code", code, code.data);
      }

jsQR()の第一引数には、Uint8ClampedArray型が来るはずだが、どうやら canvas.getContext("2d").getImageData(0, 0, this.width, this.height);で取得できるようだ。

定期的に画像を認識

今の状態だと、QRを認識するには毎回ボタンを押す必要があるので、setIntervalで毎時間(0.5秒)ごとにcanvas描写&qr読取できるように修正

      const video = document.querySelector("video");
      let canv = document.querySelector("canvas");

      const context = canv.getContext("2d");
      setInterval(() => {
          context.drawImage(video, 0, 0, this.width, this.height);
          const imageData = context.getImageData(0, 0, this.width, this.height);
          const code = jsQR(imageData.data, imageData.width, imageData.height);
          if (code) {
              console.log("Found QR code", code, code.data);
        }
      }, 500);

負荷を軽減

macがホカホカになっていたので、負荷を軽減したい。
1:videoタグとcanvasタグで同時に描写が起こってるので、canvasタグの方をjs内で仮想DOMとして扱うことにした。修正したもの▼

// let canv = document.querySelector("canvas");
let canv = document.createElement("canvas");
canv.height = this.height;
canv.width = this.width;

2:videoタグのフレームレートを落とす
getUserMedia()の引数を以下のように修正

    // let p = navigator.mediaDevices.getUserMedia({ audio: false, video: true });
    let p = navigator.mediaDevices.getUserMedia({
        audio: false,
        video: {
          width: this.width,
          height: this.height,
          frameRate: { ideal: 5, max: 15 }
        }
    });

まとめ

筆者はvuejsのsfcでwebを作っているので、こんな感じのvueコンポーネントにまとめることができました。

<template>
  <div>
    <section>
      <div class="container">
        <div class="columns">
          <div class="column">
            <b-message
              :active.sync="isReadQr"
              title="読み取り内容">
              {{ json }}
            </b-message>
            {{ isReadQr }}
          </div>
        </div>
      </div>
    </section>
    <section class="hero is-medium has-text-centered">
      <div class="hero-body hero-body-hp-main">
        <div class="container">
          <div class="columns">
            <div class="column">
              <button
                class="button"
                @click="cameraStart">カメラスタート</button>
            </div>
            <div class="column">
              <button
                class="button"
                @click="readImage">読み取りスタート</button>
            </div>
          </div>
          <div class="columns">
            <div class="column">
              <video
                :width="width"
                :height="height"
                autoplay/>
            </div>
            <div class="column">
              <div :style="{ width: width + 'px', height: height+ 'px' }">
                <!-- canvasなしでも仮想DOMを作成して描画 -->
                <!-- <canvas
                  :width="width"
                  :height="height"
                /> -->
              </div>
            </div>
          </div>
        </div>
      </div>
    </section>
  </div>
</template>

<script>
import jsQR from "jsQR";

export default {
  name: "Qr",
  data() {
    return {
      srcObject: "",
      width: 500,
      height: 500,
      json: null
    };
  },
  computed: {
    isReadQr: () => {
      return Boolean(this.json);
    }
  },
  methods: {
    cameraStart() {
      const p = navigator.mediaDevices.getUserMedia({
        audio: false,
        video: {
          width: this.width,
          height: this.height,
          frameRate: { ideal: 5, max: 15 }
        }
      });
      p.then(function(mediaStream) {
        document.querySelector("video").srcObject = mediaStream;
      });
    },
    readImage() {
      const video = document.querySelector("video");
      const canv = document.createElement("canvas");
      canv.height = this.height;
      canv.width = this.width;

      const context = canv.getContext("2d");

      setInterval(() => {
        console.log("search .....");
        context.drawImage(video, 0, 0, this.width, this.height);
        const imageData = context.getImageData(0, 0, this.width, this.height);
        const code = jsQR(imageData.data, imageData.width, imageData.height);
        if (code) {
          console.log("Found QR code", code, code.data);
          this.json = code.data;
        }
      }, 500);
    }
  }
};
</script>

デモ▼
chat_2.gif

行きつけの美容院さんのQRコードを使わさせていただきました。感謝 :bow:

所感

  • もはやwebでなんでもできるなあと思った。こういうところからpwaが流行りそうな理由なのか?
  • QRが遠いと思ったより認識が弱い。
    • 他のライブラリと比較したい(するとはいっていない)
  • アンドロイド・iPhoneでの検証は未検証なのできちんとやりたい。
  • Web Bluetooth APIも触りたい
diggy-mo
クソ雑魚エンジニア
https://blog.morifuji-is.ninja/
atma_inc
Change the common sense with algorithm を達成するためのスタートアップ
https://atma.co.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
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  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
ユーザーは見つかりませんでした