8
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【OpenCV × WebAssembly × Next.js】QRコード検出アプリを開発してみた

Last updated at Posted at 2022-01-14

あけましておめでとうございます。
この記事は、私が年末年始に取り組んだ、QRコード検出アプリ開発の記録です。

はじめに

画像からQRコードを検出するWebアプリ「QR Scanner Online」を開発しました。
demo.gif

特徴

1. 画像のコピー&ペースト、ドラッグ&ドロップに対応
2. 複数の QR コード検出に対応
3. C++, WebAssembly を用いて、QR コード検出処理を高速化

App URL

GitHubリポジトリ

アプリケーション構成

アプリケーション構成.png

実装

QRコード検出処理

C++側

本アプリでは、QRコード検出に、OpenCVのQRCodeDetectorクラスのdetectAndDecodeMulti関数を使用しています。

この関数により、入力画像に含まれる複数のQRコードを同時検出し、それらのデコード結果(URL)を一度に得ることができます。
下記リンク先のOpenCVのソースコードを見ると分かる通り、この関数ではdetectMulti関数とdecodeMulti関数を内部的に呼び出すことで、この機能を実現しています。

QRコード検出処理は、高解像度の画像が入力された場合や、多数のQRコードを検出する必要がある場合に、処理に時間がかかることが想定されます。そこで本アプリでは、検出処理をC++で実装することで、処理の高速化を図っています。

src/models/src/detect.cpp
/**
 * 複数のQRコードを同時検出する
 * @param addr 入力画像のアドレス
 * @param width 入力画像の幅
 * @param height 入力画像の高さ
 * @return QRコードが検出されたかどうか
 */
bool Detect::detect(size_t addr, int width, int height)
{
  auto data = reinterpret_cast<void *>(addr);
  cv::Mat img(height, width, CV_8UC4, data);

  cv::QRCodeDetector detector;

  bool hasCode = detector.detectAndDecodeMulti(img, _decoded_info, _points);

  return hasCode;
}
src/models/src/detect.hpp
//! QRコードの座標
cv::Mat_<cv::Vec2f> _points;

//! QRコードのデコード結果(URL)
std::vector<std::string> _decoded_info;

detectAndDecodeMulti関数の第一引数で入力画像のデータを指定し、第二引数、第三引数にそれぞれ検出されたQRコードのURLおよび座標を代入するための変数を渡しています。

以上のC++ソースコードを、WebAssemblyにコンパイルし、JavaScriptから呼び出します。JavaScriptから呼び出すにあたって、今回はEmscriptenのEmbindを用います。Embindを使うことで、あたかもJavaScriptによって実装された変数・関数・クラスかのように、透過的にC++ソースコードを呼び出すことができます。

Embindでは、以下のようにEMSCRIPTEN_BINDINGSを用いて、JavaScriptから呼び出したいオブジェクトをcppファイルに記述します。以下ではDetectクラスのdetect関数、getPoints関数、getDecodedInfo関数を指定しています。ここで、getPoints関数とgetDecodedInfo関数の戻り値の型はそれぞれcv::Mat_<cv::Vec2f>およびstd::vector<std::string>なのですが、このようなstd::vector<T>型をJavaScriptから呼び出すためには、以下のようにemscripten::register_vectorを記述する必要があります。

src/models/src/detect.cpp
EMSCRIPTEN_BINDINGS(my_module)
{
  emscripten::class_<Detect>("Detect")
      .constructor()
      .function("detect", &Detect::detect)
      .function("getPoints", &Detect::getPoints)
      .function("getDecodedInfo", &Detect::getDecodedInfo);

  emscripten::register_vector<float>("vector<float>");
  emscripten::register_vector<std::string>("vector<string>");
}

WebAssembly

WebAssembly はモダンなウェブブラウザーで実行できる新しいタイプのコードです。ネイティブに近いパフォーマンスで動作するコンパクトなバイナリー形式の低レベルなアセンブリ風言語です。さらに、 C/C++ や Rust のような言語のコンパイル対象となって、それらの言語をウェブ上で実行することができます。 WebAssembly は JavaScript と並行して動作するように設計されているため、両方を連携させることができます。

出典:https://developer.mozilla.org/ja/docs/WebAssembly

このように、C/C++やRustなどの言語をWebAssemblyにコンパイルすることで、ブラウザ上で実行することができるようになります。これにより、処理を高速化したい部分をC/C++やRustで実装するといった工夫をWebアプリ開発においても行えるようになります。

C/C++ソースコードは、Emscriptenというツールを用いることで、WebAssemblyにコンパイルできます。今回は、以下のようにemscripten/emsdkのDockerイメージを用いてコンパイルします。

src/models/Dockerfile
FROM emscripten/emsdk:latest

WORKDIR /app
RUN git clone https://github.com/opencv/opencv.git --depth 1

WORKDIR /app/opencv
RUN python3 ./platforms/js/build_js.py build_wasm --build_wasm --emscripten_dir=/emsdk/upstream/emscripten \
  --config_only --cmake_option="-DCMAKE_INSTALL_PREFIX=/usr/local"

WORKDIR /app/opencv/build_wasm
RUN emmake make -j8
RUN emmake make install

まずOpenCVのリポジトリをcloneします。そしてEmscriptenを用いてOpenCV.jsをビルドし、コンテナにインストールします。ビルド手順は以下のドキュメントページを参考にしました。

次に、インストールしたOpenCVと、自作関数を定義したcppファイルをまとめてWebAssemblyにコンパイルします。

src/models/Dockerfile
WORKDIR /app
COPY CMakeLists.txt .
COPY src ./src

WORKDIR /app/build
RUN emcmake cmake ..
RUN emmake make
src/models/CMakeLists.txt
cmake_minimum_required(VERSION 2.8.12)
project(detector LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --bind -O3 \
  -s ALLOW_MEMORY_GROWTH=1 -s ENVIRONMENT=worker -s SINGLE_FILE=1 \
  -s EXPORT_ES6=1 -s MODULARIZE=1 -s EXPORT_NAME=detector")

include_directories(/usr/local/include/opencv4)

file(GLOB_RECURSE sources_main "src/detect.cpp")
add_executable(${PROJECT_NAME} ${sources_main})

file(GLOB opencv_lib "/usr/local/lib/*.a")
file(GLOB opencv_lib_3rdparty "/usr/local/lib/opencv4/3rdparty/*.a")
target_link_libraries(${PROJECT_NAME} ${opencv_lib} ${opencv_lib_3rdparty})

今回はemcmakeのビルドオプションにSINGLE_FILE=1を指定しているので、WebAssemblyのコードを内部に含む単一のJavaScriptファイルが出力されます。
SINGLE_FILE=0の場合は、WebAssemblyファイル(.wasm)とJavaScriptファイル(.js)が別々に出力されます。

JavaScript側

次に、コンパイルして得られたWebAssemblyを含むJavaScriptファイルを呼び出す側のJavaScriptソースコードを実装します。本アプリでは、上記のC++で実装したQRコード検出処理を、Web Workerを用いてワーカースレッドで実行しています。これにより、メインスレッドの処理をブロックせずに検出処理を実行することができます。

src/libs/worker.js
import detector from "../models/build/detector";

addEventListener("message", ({ data: { data, width, height } }) => {
  detector().then((Module) => {
    const detector = new Module.Detect();

    const buffer = Module._malloc(data.data.length);
    Module.HEAPU8.set(data.data, buffer);

    const hasCode = detector.detect(buffer, width, height);

    const decodedInfo = detector.getDecodedInfo();
    const codeNum = decodedInfo.size();
    const points = detector.getPoints();

    if (codeNum === 0) {
      postMessage({ codeNum: 0 });
    }

    for (var i = 0; i < codeNum; i++) {
      let pointArray = [];

      for (let j = 0; j < 8; j++) {
        pointArray.push(points.get(i * 8 + j));
      }

      const url = decodedInfo.get(i);

      postMessage({ i, codeNum, pointArray, url });
    }
  });
});

1行目で、コンパイルの結果得られたdetector.jsというファイルをimportしています。そして5行目のconst detector = new Module.Detect();で、C++で実装されたDetectクラスを呼び出し、インスタンスを生成しています。インスタンスを生成した後は、detector.detect()のように、通常のJavaScriptで実装されたクラスのメソッドと同じようにして呼び出せます。また、Module._malloc関数やModule.HEAPU8.set関数は、デフォルトで(EMSCRIPTEN_BINDINGSで指定しなくても)呼び出すことができ、メモリの動的確保をC++と同様に行うことができます。

ここでは、detector.detect関数でQRコード検出処理を実行し、detector.getDecodedInfo関数およびdetector.getPoints関数で検出結果を受け取っています。そして最後にpostMessage({ i, codeNum, pointArray, url });で、複数のQRコード検出結果を1つずつメインスレッドに返しています。

以上のワーカースレッドでの処理を、以下のようにメインスレッドから呼び出します。

src/components/Scanner/_components/InputPane/ImageCard.tsx
// ワーカースレッドで検出処理を実行
const worker = new Worker(
  new URL("../../../../libs/worker", import.meta.url)
);

worker.postMessage({ data, width, height });

// 検出結果を1つずつ取得
worker.addEventListener(
  "message",
  ({ data: { i, codeNum, pointArray, url } }) => {
    // ...![検出結果.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282840/a5eaa01a-f3cd-014d-1a1b-d39b59e7ba0d.png)

    console.log(url);

    const color = `hsl(${(i * 360) / codeNum}, 100%, 40%)`;

    dispatch(pushResult({ url: url, color: color }));

    ctx.strokeStyle = color;
    ctx.lineWidth = 20;

    // lineWidth分オフセットして矩形を描画
    const x = pointArray[0] - ctx.lineWidth / 2;
    const y = pointArray[1] - ctx.lineWidth / 2;
    const width = pointArray[2] - pointArray[0] + ctx.lineWidth;
    const height = pointArray[7] - pointArray[1] + ctx.lineWidth;

    ctx.strokeRect(x, y, width, height);

    // 全コード検出完了
    if (i === codeNum - 1) {
      setIsLoading(false);
      dispatch(complete());
    }
  }
);

ワーカースレッドからQRコードの検出結果を1つずつ受け取り、その度に上記のmessageイベントハンドラがトリガーされます。イベントハンドラでは、受け取ったQRコードの座標データpointArrayから、QRコードの周囲に矩形を描画する処理を行っています。矩形の線色は、色相環を検出されたQRコードの数codeNumで等分した点の色をそれぞれ使用しています。

UI

本アプリでは、UIの実装にNext.jsを使用しています。また、状態管理にRedux Toolkit、スタイリングにMUI (Material-UI) およびemotionを使用しています。

トップページ(検出前)

アプリトップページ.png

トップページ(検出後)

検出結果.png

CI/CD

本アプリでは、GitHub Actionsを用いてCI/CD環境を構築しています。GitHub Actionsでは主に以下の4ステップをmainブランチへのpush時に自動実行しています。

  1. Dockerイメージのビルド、ghcr.ioへのプッシュ
  2. Dockerコンテナの立ち上げ、WebAssemblyを含むJavaScriptファイルの入手
  3. Next.jsのビルド
  4. Vercelへのデプロイ

CICD.png

その他

Reactコンポーネントのデバッグに、Storybookを使用しました。Storybookを使うことで、ブラウザ上でコンポーネント1つ1つの見た目や挙動を確認することができます。またブラウザ上でスタイルやstate、propsの値を書き換えながらテストすることも可能です。
storybook.png

おわりに

本記事では、私が先日個人開発したQRコード検出アプリについて、技術的な内容を紹介しました。今回の開発ではWebAssemblyやNext.jsなど初めて使用する技術が多く、非常に学びの多い開発でした。特にWebAssemblyについては、機械学習アプリケーションに代表されるように年々リッチになるWebアプリ開発において、今後ますます重要な技術になり、普及が進むものと思われます。例えば、Google Meetの背景ぼかし機能やバーチャル背景機能は、WebAssemblyを用いて実装されています。

機械学習ライブラリのTensorFlow.jsでは、WebGLバックエンドの他に、WebAssemblyバックエンドもデフォルトでサポートされているため、比較的簡単にWebAssemblyを用いたWebMLアプリケーションを開発できそうです。
(ただし、WebGLとWebAssemblyのどちらが高速かは、MLモデルのサイズやデバイスなどによって異なります)

機会があればこのようなWebMLアプリケーション開発にも取り組んでみたいと思います。
:wave:

8
4
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
8
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?