LoginSignup
76
77

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-06-13

こんばんは。
最近チャリンコを購入して、チャリンコ通勤始めました。道中のお弁当屋とか発見できて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も触りたい
76
77
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
76
77