p5.js と jsQR で簡素な QRコードのデコード処理：全体で40行ほど（試行錯誤の過程の情報も記載）

以下の記事の続きのような内容です。

●JavaScript で QRコードのデコード：jsQR で簡素な処理（日本語の扱いでハマった部分についても記載） - Qiita
　https://qiita.com/youtoy/items/d94a5bf835d3f4007c81

上記の記事では JavaScript で書かれた [jsQR](https://github.com/cozmo/jsQR) の公式デモを流用したのですが、この記事では [p5.js](https://p5js.org/) を使って QRコード のデコードを行います。

jsQR と p5.js を組み合わせた話は、検索していたら以下の記事が見つかったのですが、自分の環境で動かなかったので、いろいろ試行錯誤してみたというのがこの記事の内容です。

●ブラウザのみでQRコードをARマーカーにしてみた
　https://zenn.dev/tkyko13/articles/b1dd3060488be6

## 完成版の内容
最初に、今回の内容の最終的な実行結果と、今回の完成版のソースコードを掲載してみます。

### 最終的な実行結果
以下が実行結果です。
QRコードには「test」という文字列を埋め込んでおり、カメラ映像の下側の領域に、その内容が出力できているのが確認できます。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">jsQR ＋ p5.js の組み合わせ、<br>ブラウザの開発者ツールを使って取得できる要素・変数の中身を確認しつつコードを書いていったら、動くようになった！<br><br>認識結果は、Teachable Machine の p5.js 公式サンプルのやり方をまねて、カメラ映像の下に出すようにしてみた！ <a href="https://t.co/eSU3CdcDDK">https://t.co/eSU3CdcDDK</a> <a href="https://t.co/BlftY956oC">pic.twitter.com/BlftY956oC</a></p>&mdash; you (@youtoy) <a href="https://twitter.com/youtoy/status/1421831541924061193?ref_src=twsrc%5Etfw">August 1, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

### 【追記】 ソースコード：修正版
記事を公開した後にソースコードを簡素化できたので、それを修正後のバージョンとして掲載します。

```html
<html>
<head>
  <meta charset="UTF-8">
  <title>jsQR と p5.js</title>
  <script src="https://cdn.jsdelivr.net/npm/p5@1.4.0/lib/p5.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script>
</head>
<body>
  <h1>jsQR と p5.js</h1>
  <script>
    let capture;

    function setup() {
      pixelDensity(1);
      createCanvas(640, 500); // 下にテキスト表示用の領域を作る（ビデオの高さ＋20）
      capture = createCapture(VIDEO);
      capture.size(640, 480);
      capture.hide();
    }

    function draw() {
      background(0);
      image(capture, 0, 0);

      capture.loadPixels();
      const code = jsQR(capture.pixels, capture.width, capture.height, { inversionAttempts: "dontInvert" });
      fill(255);
      textSize(16);
      textAlign(CENTER);
      if (code) {
        text(`QRコードのデータ： ${code.data}`, width / 2, height - 4);
      } else {
        text(`（QRコードが見つかりません）`, width / 2, height - 4);
      }
    }
  </script>
</body>
</html>
```

以下で、ソースコードの内容について補足していきます。


#### ライブラリ
ライブラリは現時点で最新のバージョンのものを、CDN からロードしてます。

```html
  <script src="https://cdn.jsdelivr.net/npm/p5@1.4.0/lib/p5.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script>
```

#### セットアップでのカメラ周りの処理： 修正版
カメラを使う処理は、[p5.js公式の Video Capture のサンプル](https://p5js.org/examples/dom-video-capture.html)をほぼそのまま利用する形になりました。

```javascript
    let capture;

    function setup() {
      pixelDensity(1);
      createCanvas(640, 500); // 下にテキスト表示用の領域を作る（ビデオの高さ＋20）
      capture = createCapture(VIDEO);
      capture.size(640, 480);
      capture.hide();
    }
```

`createCanvas` の指定サイズの高さと `capture.size` の高さが少し違うのは、この後に出てくる結果表示に利用する領域を確保するためです。

#### カメラ画像の表示と jsQR に渡すピクセルデータの処理
`draw()` の中では、ページ内にカメラ画像を表示させつつ、jsQR での処理を行うための画像の受け渡しを行います。

`jsQR()` には「0 から 255 の間の整数データを RGBA の順で収めた一次元配列と幅・高さの値を渡せば良いのですが、「loadPixels()」・「pixels」を用いることで簡単に実行できることが分かりました。

```javascript
    function draw() {
      ...
      image(capture, 0, 0);

      capture.loadPixels();
      const code = jsQR(capture.pixels, capture.width, capture.height, { inversionAttempts: "dontInvert" });
      ...
    }
```

なお、このやり方は、以下のソースコードを探し当てて知りました。
　●p5.js Web Editor | capture pixels
　　https://editor.p5js.org/lmccart/sketches/xmPJXc81u

また、jsQR の処理でオプションとして `{ inversionAttempts: "dontInvert" }` を指定しています。
この指定を行ったのは、以下の公式の説明で「後方互換性のためにデフォルトを attemptBoth にしているものの、パフォーマンスの面から dontInvert を指定するのが良い（将来的には dontInvert をデフォルトにする）」というようなことが書いてあったためでした。また、[1つ前の記事](https://qiita.com/youtoy/items/d94a5bf835d3f4007c81)で参照した[公式のデモ](https://cozmo.github.io/jsQR/)も、こちらを指定していたようでした。
![jsQRのオプション.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/5a754f36-9232-5277-8291-14dadc1ac8dd.jpeg)


#### 結果の表示
結果の表示は、ページ上で行います。
この部分は [Teachable Machine の p5.js版公式サンプル](https://editor.p5js.org/ml5/sketches/ImageModel_TM)のやり方をベースに作りました。

```javascript
    function draw() {
      background(0);
      ...

      fill(255);
      textSize(16);
      textAlign(CENTER);
      if (code) {
        text(`QRコードのデータ： ${code.data}`, width / 2, height - 4);
      } else {
        text(`（QRコードが見つかりません）`, width / 2, height - 4);
      }
    }
```

p5.js で最初に作った Canvas の高さをカメラ画像の高さよりも大きめにしておき、その余分な部分（※ 下側にできるもの）の背景を黒く塗っておいて、白色のテキストを上に表示する、というやり方です。

## まとめ
jsQR と p5.js を組み合わせた JavaScript による QRコードのデコードについて、検索して見つけた記事のやり方が自分の環境でうまくいかなかったため、試行錯誤して動くようなやり方を見つけました。

ソースコードはできるだけシンプルになるようにしてみたので、HTMLの部分を含めて全体で 40行ほどのサイズ感になりました。


## 【追記】 スマホでの動作・長時間の利用について
ここで書いた p5.js版はスマホでの動作確認が十分に行えておらず、また長時間の動作検証もできていないため、それらについて別途対応が必要な事項が出てくるかもしれません。

また、スマホで動かした時に外側のカメラを利用する設定が必要になります。
以下の部分に該当するものですが、まだ今は検証中という状況です。

●capture = createCapture(VIDEO); Can we reference a different Camera on phone? · Issue #1496 · processing/p5.js
　https://github.com/processing/p5.js/issues/1496
![スマホで利用するカメラの話.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/4fee25a4-07b7-42a0-2410-1576fe5ed771.jpeg)


## 【追記】 Teachable Machine での画像分類との同時並行処理
その後、Teachable Machine の画像分類（p5.js版）と同時並行で処理できることが確認できました。
（その内容は、別の記事として書こうと思います）

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">先ほどの <a href="https://twitter.com/hashtag/p5js?src=hash&amp;ref_src=twsrc%5Etfw">#p5js</a> を使ったカメラから映像内の QRコードのデコード（ <a href="https://twitter.com/hashtag/jsQR?src=hash&amp;ref_src=twsrc%5Etfw">#jsQR</a> を利用）は途中段階で、その後やりたかった事があり、その話について。<br><br>やりたかった内容は、上記の QRコードのデコード処理と <a href="https://twitter.com/hashtag/TeachableMachine?src=hash&amp;ref_src=twsrc%5Etfw">#TeachableMachine</a> による画像分類（p5.js版）の同時並行での処理で、無事に動いた！！ <a href="https://t.co/U6EO2J2VHp">pic.twitter.com/U6EO2J2VHp</a></p>&mdash; you (@youtoy) <a href="https://twitter.com/youtoy/status/1421853250840530950?ref_src=twsrc%5Etfw">August 1, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


## 記事公開時点で書いていた内容（古い情報）
**※以下は記事公開時点で掲載していたものですが、上記の簡素化できたバージョンができたので不要になりました（記録として残す意味で、修正前の旧バージョンとして掲載しておきます）**

### ソースコード：修正前の旧バージョン
JavaScript のファイルは分けず、HTMLファイル 1つに詰め込む感じの実装にしました。
また、デバッグで使った処理はコメントアウトして残してます。

```html
<html>

<head>
  <meta charset="UTF-8">
  <title>jsQR と p5.js</title>
  <script src="https://cdn.jsdelivr.net/npm/p5@1.4.0/lib/p5.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script>
</head>

<body>
  <h1>jsQR と p5.js</h1>
  <script>
    let canvas, capture, context;

    function setup() {
      canvas = createCanvas(640, 500);
      capture = createCapture(VIDEO);
      capture.size(640, 480);
      capture.hide();

      console.log(`capture ${capture.width}, ${capture.height}`) // 640, 480
      console.log(`context ${canvas.elt.width}, ${canvas.elt.height}`) // 1280, 1000
      context = canvas.elt.getContext('2d');
      // console.log(context);
    }

    function draw() {
      background(0);
      image(capture, 0, 0);

      const imageData = context.getImageData(0, 0, canvas.elt.height, canvas.elt.height);
      const code = jsQR(imageData.data, imageData.width, imageData.height);
      // console.log(code);
      if (code) {
        fill(255);
        textSize(16);
        textAlign(CENTER);
        text(code.data, width / 2, height - 4);
      }
    }
  </script>
</body>

</html>
```

#### 変数とセットアップ関数のカメラ周りの処理
カメラを使う処理は、まず念のために [p5.js公式の Video Capture のサンプル](https://p5js.org/examples/dom-video-capture.html)を動かしてみた上で、そこから手を加えていくやり方にしました。

やってみると、カメラ利用のまわりは[冒頭で書いていた Zenn の記事](https://zenn.dev/tkyko13/articles/b1dd3060488be6)と似たような実装で問題なく動きました。

```javascript
    let canvas, capture, context;

    function setup() {
      canvas = createCanvas(640, 500);
      capture = createCapture(VIDEO);
      capture.size(640, 480);
      capture.hide();

      ...
    }
```

`createCanvas` の指定サイズの高さと `capture.size` の高さが少し違うのは、この後に出てくる結果表示に利用する領域を確保するためです。

#### 画像描画・取得を行う部分のサイズの確認
ここで、いったん画像描画・取得を行う部分のサイズの確認を行ってみました。
具体的には capture と、p5.js で生成された canvas要素のサイズを確認しました。

p5.js の Canvas生成時の処理で `canvas = createCanvas(640, 500);` と変数 canvas を使った処理を書いていたので、これを使って canvas要素 を取り出します。
`canvas.elt` として width・height の値を取り出しています。

その結果、冒頭で指定した「幅 640, 高さ 500」という値の 2倍の値が取得される形となりました。

```javascript
      console.log(`capture ${capture.width}, ${capture.height}`)
      console.log(`context ${canvas.elt.width}, ${canvas.elt.height}`)
```

念のため、Chrome の開発者ツールでそのあたりを確認してみると、以下のようになっていました。
![Canvas要素のサイズを確認.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/2c4c4012-f8ce-959c-b16b-82373f3301a4.jpeg)

要素の幅・高さは「1280, 1000」となっていて、style属性が「640, 500」となっているようでした。

**【追記】 上記に関わる内容（pixelDensity）**
**要素の幅・高さは「1280, 1000」と倍の値になっていた件は、 `pixelDensity(1);` という指定をすると「640, 500」という値になりました（以下、pixelDensity に関連した記事）。**

●reference | pixels
　https://p5js.org/reference/#/p5/pixels
●p5.jsでsave()した時に2倍の大きさの画像が出力されるのを回避する : だらっと学習帳
　http://blog.livedoor.jp/reona396/archives/55827221.html
●p5.jsでpixel操作をするとなんか結果が小さくなる - Qiita
　https://qiita.com/airtoxin/items/929eb4db382c16d5e18b

#### p5.js の Canvas から画像取得・デコード等の処理
セットアップの部分で、後から画像取得を行えるよう `canvas.elt.getContext('2d')` という形で、[2D レンダリングコンテキストを取得しておきます](https://developer.mozilla.org/ja/docs/Web/API/CanvasRenderingContext2D)。

```javascript
    function setup() {
      ...
      context = canvas.elt.getContext('2d');
    }
```

その後の `draw()` の中で、Canvas ににカメラ画像をはりつけつつ、Canvas上の画像データを取得します。
その部分は、 `getImageData()` を用い、サイズを指定する部分は canvas要素の実サイズ（「canvas.elt.height, canvas.elt.height」で値は「1280, 1000」）を指定しています。 

```javascript
    function draw() {
      ...
      image(capture, 0, 0);

      const imageData = context.getImageData(0, 0, canvas.elt.height, canvas.elt.height);
      const code = jsQR(imageData.data, imageData.width, imageData.height);
      ...
    }
```

あとは、[ImageData オブジェクトの data（0 から 255 の間の整数データを RGBA の順で収めた一次元配列を表す Uint8ClampedArray）](https://developer.mozilla.org/ja/docs/Web/API/Canvas_API/Tutorial/Pixel_manipulation_with_canvas)を取り出して、 `jsQR()` に幅・高さの値とともに渡します。

このあたりは、[前回の記事](https://qiita.com/youtoy/items/d94a5bf835d3f4007c81)で JavaScript で行っているものと同じ流れです。

#### 結果の表示
結果の表示は、ページ上で行います。
この部分は [Teachable Machine の p5.js版公式サンプル](https://editor.p5js.org/ml5/sketches/ImageModel_TM)のやり方をまねています。

```javascript
    function draw() {
      background(0);
      ...

      if (code) {
        fill(255);
        textSize(16);
        textAlign(CENTER);
        text(code.data, width / 2, height - 4);
      }
    }
```

p5.js で最初に作った Canvas の高さをカメラ画像の高さよりも大きめにしておき、その余分な部分（※ 下側にできるもの）の背景を黒く塗っておいて、白色のテキストを上に表示する、というやり方です。