以下の記事の続きのような内容です。
●JavaScript で QRコードのデコード:jsQR で簡素な処理(日本語の扱いでハマった部分についても記載) - Qiita
https://qiita.com/youtoy/items/d94a5bf835d3f4007c81
上記の記事では JavaScript で書かれた jsQR の公式デモを流用したのですが、この記事では p5.js を使って QRコード のデコードを行います。
jsQR と p5.js を組み合わせた話は、検索していたら以下の記事が見つかったのですが、自分の環境で動かなかったので、いろいろ試行錯誤してみたというのがこの記事の内容です。
●ブラウザのみでQRコードをARマーカーにしてみた
https://zenn.dev/tkyko13/articles/b1dd3060488be6
完成版の内容
最初に、今回の内容の最終的な実行結果と、今回の完成版のソースコードを掲載してみます。
最終的な実行結果
以下が実行結果です。
QRコードには「test」という文字列を埋め込んでおり、カメラ映像の下側の領域に、その内容が出力できているのが確認できます。
jsQR + p5.js の組み合わせ、
— you (@youtoy) August 1, 2021
ブラウザの開発者ツールを使って取得できる要素・変数の中身を確認しつつコードを書いていったら、動くようになった!
認識結果は、Teachable Machine の p5.js 公式サンプルのやり方をまねて、カメラ映像の下に出すようにしてみた! https://t.co/eSU3CdcDDK pic.twitter.com/BlftY956oC
【追記】 ソースコード:修正版
記事を公開した後にソースコードを簡素化できたので、それを修正後のバージョンとして掲載します。
<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 からロードしてます。
<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 のサンプルをほぼそのまま利用する形になりました。
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」を用いることで簡単に実行できることが分かりました。
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つ前の記事で参照した公式のデモも、こちらを指定していたようでした。
結果の表示
結果の表示は、ページ上で行います。
この部分は Teachable Machine の p5.js版公式サンプルのやり方をベースに作りました。
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
【追記】 Teachable Machine での画像分類との同時並行処理
その後、Teachable Machine の画像分類(p5.js版)と同時並行で処理できることが確認できました。
(その内容は、別の記事として書こうと思います)
先ほどの #p5js を使ったカメラから映像内の QRコードのデコード( #jsQR を利用)は途中段階で、その後やりたかった事があり、その話について。
— you (@youtoy) August 1, 2021
やりたかった内容は、上記の QRコードのデコード処理と #TeachableMachine による画像分類(p5.js版)の同時並行での処理で、無事に動いた!! pic.twitter.com/U6EO2J2VHp
記事公開時点で書いていた内容(古い情報)
※以下は記事公開時点で掲載していたものですが、上記の簡素化できたバージョンができたので不要になりました(記録として残す意味で、修正前の旧バージョンとして掲載しておきます)
ソースコード:修正前の旧バージョン
JavaScript のファイルは分けず、HTMLファイル 1つに詰め込む感じの実装にしました。
また、デバッグで使った処理はコメントアウトして残してます。
<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 のサンプルを動かしてみた上で、そこから手を加えていくやり方にしました。
やってみると、カメラ利用のまわりは冒頭で書いていた Zenn の記事と似たような実装で問題なく動きました。
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倍の値が取得される形となりました。
console.log(`capture ${capture.width}, ${capture.height}`)
console.log(`context ${canvas.elt.width}, ${canvas.elt.height}`)
念のため、Chrome の開発者ツールでそのあたりを確認してみると、以下のようになっていました。
要素の幅・高さは「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 レンダリングコンテキストを取得しておきます。
function setup() {
...
context = canvas.elt.getContext('2d');
}
その後の draw()
の中で、Canvas ににカメラ画像をはりつけつつ、Canvas上の画像データを取得します。
その部分は、 getImageData()
を用い、サイズを指定する部分は canvas要素の実サイズ(「canvas.elt.height, canvas.elt.height」で値は「1280, 1000」)を指定しています。
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)を取り出して、 jsQR()
に幅・高さの値とともに渡します。
このあたりは、前回の記事で JavaScript で行っているものと同じ流れです。
結果の表示
結果の表示は、ページ上で行います。
この部分は Teachable Machine の p5.js版公式サンプルのやり方をまねています。
function draw() {
background(0);
...
if (code) {
fill(255);
textSize(16);
textAlign(CENTER);
text(code.data, width / 2, height - 4);
}
}
p5.js で最初に作った Canvas の高さをカメラ画像の高さよりも大きめにしておき、その余分な部分(※ 下側にできるもの)の背景を黒く塗っておいて、白色のテキストを上に表示する、というやり方です。