本ページはプロモーションが含まれています

WebAssembly版OpenCVでQRコードデコーダを試作

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で幅を高さに強制的に合わせることでワークアラウンドとしました。
こちらから、上と同じものを試すことができます。