OpenCVは、C++実装をもとにしたPythonやJavaなどの多言語のバインディングがあるのは知られていますが、WebAssemblyを組み込んだopencv.jsというJavaScript版もあります。JavaScriptと言っても、呼び出しのAPIから先の実際の処理はWasmバイナリが実行するので、ネイティブコードに近いパフォーマンスが期待できます。
WebAssembly版のOpenCV「opencv.js」からQRCodeDetectorクラスを使って、Webブラウザで動くQRコードデコーダアプリを試作してみます。
opencv.jsとWasmのビルド
Wasm版OpenCVの入手
OpenCVのWasm版は、こちらのように有志によるビルド済みのopencv.js opencv.wasmが公開されています。
ところが残念ながら、こちらのWasmモジュールでは、今試したいQRコードのデコーダのクラスがサポートされていないようでした(2025/8月時点ではOpenCVのベースが少し前の4.4.0でした)。
したがってここでは、OpenCVのソースコードからWasmとJavaScriptコードをビルドして作ることにしました。
OpenCVのソースコードは、ここから入手できます。
現時点の安定版は4.12.0でした。
ダウンロードしたらソースツリーを展開してください。4.12.0ならばディレクトリはopencv-4.12.0に展開されます。
Emscriptenのインストール
WasmモジュールおよびグルーコードJavaScriptをビルドするにはコンパイラツールチェーンのEmscriptenが必要です。Emscriptenはここからダウンロードできます。
gitからクローンか、zipのダウンロードで入手してツリーを展開します。
ここでEmscriptenの最新版(2025/8現在4.0.12)を使いたいところですが、OpenCVの4.12.0のビルドで使っているem++のコンパイルオプションがEmscripten4.0.6で廃止になったせいで、最新版ではコンパイルが通らないようです。したがって少し古いバージョンのEmscriptenを使う必要があります。これはOpenCVのアップデートでそのうち解消されるかもしれません。
そのオプションは、-sDEMANGLE_SUPPORTです。OpenCV公式ではEmscripten2.0.10でテストしたと書いてあるので、OpenCVも長いことドキュメントを更新していないようです。
少し古い4.0.0をインストールします。
$ ./emsdk update
$ ./emsdk install 4.0.0
$ ./emsdk activate 4.0.0
コンパイルの環境を設定します。
$ source ./emsdk_env.sh
なおEmscriptenもOpenCVもビルドにPython3.6以上が必要です。
OpenCVのWebAssemblyをビルド
OpenCVソースのディレクトリopencv-4.12.0に入らずにその上の階層で次のコマンドラインを実行し、ビルドします。
$ emcmake python3 ./opencv-4.12.0/platforms/js/build_js.py build_js
build_jsはビルドの作業ディレクトリで、自動的に作成されます。任意のディレクトリ名を指定してください。
ビルドにはしばらく(数分)かかります。完了すると、完成したopencv.jsの場所を示すメッセージが表示されます。
=====
===== Build finished
=====
OpenCV.js location: /home/midori/build_js/bin/opencv.js
完成したWasmのopencv.jsは、WasmバイナリをJavaScriptに取り込んだ一体型Wasmモジュールです。もし、一般的なグルーコードと.wasmファイルに分けて生成したい場合は次のようにします。
$ emcmake python3 ./opencv-4.12.0/platforms/js/build_js.py build_js \
--build_wasm --disable_single_file
この場合、Wasmモジュールバイナリは「./build_js/bin/opencv_js.wasm」に作られます。
QRコードデコーダの試作
WasmモジュールによるJavaScript版OpenCVを使って、QRコード画像をデコーダするローカルWebアプリを試作してみます。
ブラウザに表示している枠内にQRコードの画像ファイルをドロップすると、その上にデコードした文字列が表示されるというものを作ってみます。
そして、これが試作したHTML/JavaScriptです。
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
.flexvert {
width: 70vh;
display: flex;
flex-direction: column;
}
.flexvert img {
height: 70vh;
width: 70vh;
flex: 1 1 auto;
text-align: center;
object-fit: contain;
object-position: 0 0;
box-sizing: border-box;
border: solid 1px #444;
display: block;
}
</style>
<title>OpenCV.js QR Decoder</title>
</head>
<body>
<div class="flexvert">
<h2>OpenCVのQRコードデコーダ</h2>
<textarea id="decoderesult" rows=2></textarea>
<img id="inputimage" src="startmsg.jpg"
alt="ここにQRコード画像をドロップ"></img>
</div>
<script>
const decoded = document.getElementById('decoderesult');
const inputimg = document.getElementById('inputimage');
inputimg .addEventListener('dragover', function(event) {
event.preventDefault();
});
inputimg.addEventListener('drop', async (dropev) => {
dropev.preventDefault();
let file = dropev.dataTransfer.files[0];
if (!file || !file.type.startsWith('image/'))
return;
let reader = new FileReader();
reader.addEventListener("load", async (loadev) => {
if (cv instanceof Promise)
cv = await cv;
inputimg.addEventListener("load", (imgev) => {
let mat = cv.imread(inputimg);
let qr = new cv.QRCodeDetector();
let decodedstr = qr.detectAndDecode(mat);
console.log("decoded=", decodedstr);
decoded.value = decodedstr;
mat.delete();
});
inputimg.src = loadev.target.result;
});
reader.readAsDataURL(file);
});
let Module = {
onRuntimeInitialized() {
console.log("opencv is ready", cv);
decoded.value = '';
}
};
</script>
<script async src="opencv.js"></script>
</body>
</html>
inputimageのimgタグへのdropイベントから、loadイベントを経て画像を読み込みます。imgタグへのドロップした画像の表示完了のloadイベントまで細かく非同期なハンドラで処理していますが、こうしないとOpenCVのimread()とタイミングが合わないようです。ここはsetTimeout()を使って少し遅れてからimread()から始まるようにしてもよいでしょう。
デコーダは、OpenCVのQRCodeDetectorクラスのdetectAndDecode()を呼び出します。imgタグのドロップされた画像の表示完了から次の処理がQRコードのデコード処理です。
let mat = cv.imread(inputimg);
let qr = new cv.QRCodeDetector();
let decodedstr = qr.detectAndDecode(mat);
console.log("decoded=", decodedstr);
decoded.value = decodedstr;
mat.delete();
次のダウンロードリンクから、一式をダウンロードできます(Wasmバイナリ分離でビルドしたものです)。
ZIPを展開したファイルの、
opencv.js、opencv_js.wasm、index.html、starmsg.jpg
をWebサーバの同じディレクトリに配置してください。
注意:Wasmは、エクスプローラなどのファイラからのダブルクリックでは動作しません。ローカルで試す場合はPythonやNodeなどを使ってWebサーバを立ち上げてください。
ページを開いたら、枠の中に何か適当なQRコードの画像をドロップします。
画面の上のテキストエリアに結果が表示されます。
detectAndDecode()が空でない文字列を返せば成功です。失敗時は空文字を返します。
imgタグのオブジェクトをimread()で読み込ませていますが、このときimgタグの形状が横に長いと、ほとんどデコードが失敗するという問題がありました。この例はimgタグを直接imread()に送っていますが、もっと別の適切な画像データの与え方があるのかもしれません。現時点ではその原因はつかめていません。とりあえずこのimgタグが正方形になるように、CSSで幅を高さに強制的に合わせることでワークアラウンドとしました。
こちらから、上と同じものを試すことができます。