Blazor WebAssembly + OpenCVSharpでブラウザ上で動作するカスタマバーコードリーダーを作成した(実装解説)

# Blazor WebAssemblyって何？

* .NET 6や.NET 7で提供されているBlazorフレームワークのモデルの一つ
  - BlazorはWebUIに関するフレームワーク
    - SPA(Single Page Application)方式でブラウザ上にWebページの描画を動的に行う
* Blazorで使用可能なモデル
  - Blazor Server: 前者はサーバー側で描画処理
  - Blazor WebAssembly: ブラウザ側で描画処理
    - 今回は、後者を使用

# Blazor WebAssemlbyの凄いところ

通常ブラウザで動的な描画を行う場合は、JavaScriptによるDOM操作やVue.jsやReactなどのJSフレームワークを使用します。
Blazorでは、C#を中心にWebUIの動的な動作を作成することができます。さらに、.NET系の標準ライブラリを使用することができます。たとえ、ブラウザ上で動作するBlazor WebAssemblyであっても、C#系のプログラムや.NET系のライブラリが動きます。

:::note info
知っている人は多いと思いますがWebAssmeblyは、ブラウザ上で動作するJavaScript言語に続く第2言語(?)であり、JavaScriptよりも高速に動作します。Blazor WebAssemblyでは、C#からWebAssemblyのバイナリが生成されるので高速な動作が期待できます[^1]。
:::

# カスタマバーコードって何?

* 郵便物を大量に出す際に、宛名に印字されていると料金の割引ができるバーコード
  * 下の画像がカスタマバーコード
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108558/3c48bf19-093a-ba55-e77c-d6e031d33caa.png)

* カスタマバーコードの仕様は以下のページから

https://www.post.japanpost.jp/zipcode/zipmanual/p10.html

# カスタマバーコードリーダーを作成した理由

* カスタマバーコードを読み取れるスマホアプリがあまりなかったから
  * iOSで動作するアプリはなさそう
* アプリではなく、ブラウザ上で動作するものがなかったから
  * カスタマバーコードを作成するWebサイトはあっても、読み取りができるものは多分ない
* 仕事でカスタマバーコードの検証を行う際に役立つ？
* ***単純にBlazor WebAssemblyを使ってみたかった***
  - ここ重要 

# 実際に動いているサイト

https://postal-cb-reader.sakura.ne.jp/

* この記事では概要のみ記載します。

https://github.com/EmEpsilon/PostalBarcodeReaderWebAssembly/

* ソースコードは上のGitHubリポジトリで公開中

# 仕組みについて

今回、WebUIはBlazor WebAssembly。カスタマーバーコードの読み取りの処理本体は、OpenCVSharp[^2]を使用しています。OpenCVSharpの機能を使用して、画像の加工や画素の参照を行います。
また、作成したWebサイトでは、画像ファイルの読み取りとカメラでの読み取りの2機能を実装しています。
ブラウザ上のみで動作する様にしてあり、アプリケーションサーバーが無くても、WebサーバーのみあればWebサイトを稼働させることができます。

```mermaid
flowchart TD

subgraph front[Webサイト]
  d4[Blazor WebAssemblyで実装]:::description
  page1[ファイル読み取り]
  page2[カメラ読み取り]
  page3[カスタマバーコード読み取り処理]

  page1--"呼び出し"-->page3
  page2--"呼び出し"-->page3
end
front:::front


classDef description stroke-width:0px, color:#000, fill:transparent, font-size:13px
```

## WebAssemblyを使用するにあたっての注意点

:::note warn
実装時に注意が必要なのが、WebAssemblyでは直接DOM操作はできません。
JavaScriptからWebAssemblyを呼び出したり、WebAssemblyからJavaScriptを呼び出すことは可能ですが、WebAssemblyから直接DOM操作は行えないのでJavaScriptを呼び出して操作する必要があります。
:::


ただし、Blazor WebAssemblyのフレームワーク上で用意されている仕組みに乗っかる分にはそこを意識する必要はないです。
基本的には動的な描画はBlazor WebAssemblyが行いますが、イレギュラーな操作(Canvasなど)はJavaScriptによる呼び出しが必要です。

## ファイル取り込み機能

ファイル読み取り機能は、ブラウザから画像ファイルを指定することで、ブラウザ上でカスタマーバーコードの読み取りを行う機能になります。

以下のような流れで処理しています。(実際の実装と差異があるので注意)

```mermaid
flowchart TD

subgraph bw[Blazor WebAssembly]
    FUNC1[ブラウザから画像の指定]
    FUNC2[画像をImageURIに変換]
    FUNC5[画像データをセットする]

    FUNC1--> FUNC2

    FUNC2_1[ブラウザから処理開始のボタンを押下]
    FUNC2_2[セットしている画像データをBMPデータに変換]
    FUNC2_3[カスタマバーコード読み取り処理]
    FUNC2_4[検出結果の描画_画像]
    FUNC2_6[検出結果の描画_リスト]
    
    FUNC2_1 --> FUNC2_2
    FUNC2_2 --> FUNC2_3
    FUNC2_3 --> FUNC2_4
    FUNC2_3 --> FUNC2_6

end

subgraph js[JavaScript]
    FUNC3[JS上で画像読み込み]
    FUNC4[Canvasの機能を利用して画像の大きさを変える]
    FUNC2_5[検出結果の描画_画像をCanvasに出力]

    FUNC3 --> FUNC4

end

    FUNC2 --> FUNC3    
    FUNC4 --> FUNC5    
    FUNC2_4 --> FUNC2_5
   
```

:::note info
画像の大きさを変更するのは、画像が大きいと処理時間が長くなり、JavaScriptのタイムアウトに引っかかる可能性があります。
WebAssembly上で画像サイズを変換する方法はありますが、高速で画像サイズ変換可能な方法は無いと思われるので、Canvasの機能で変換する仕組みとしました。
:::

:::note warn
記事執筆時点のOpenCVSharpのWebAssembly版では、BMPデータしか読み込めません。
カスタマバーコード読み取り処理直前に、画像をBMPデータに変換しています。
:::


## カメラ取り込み機能

カメラ読み取り機能は、ブラウザからカメラを呼び出し、ブラウザ上でカスタマーバーコードの読み取りを行う機能になります。

以下のような流れで処理しています。(実際の実装と差異があるので注意)

```mermaid
flowchart TD

subgraph bw[Blazor WebAssembly]
    FUNC_BWA_1[ページの表示]
    FUNC_BWA_2[一時用Canvasのサイズを変更]
    FUNC_BWA_3[ImageURIをBMPデータに変換]
    FUNC_BWA_4[カスタマバーコード読み取り処理]
    FUNC_BWA_5[検出結果の描画_画像]
    FUNC_BWA_6[検出結果の描画_リスト]

    LOOP_START(ループSTART)
    LOOP_END(ループEND)

    FUNC_BWA_2 --> LOOP_START
    FUNC_BWA_3 --> FUNC_BWA_4
    FUNC_BWA_4 --"未検出"--> LOOP_END
    FUNC_BWA_4 --"検出"--> FUNC_BWA_5
    FUNC_BWA_4 --"検出"--> FUNC_BWA_6
    LOOP_END --> LOOP_START

end

subgraph js[JavaScript]
    FUNC_JS_1[カメラ権限の取得]
    FUNC_JS_2[カメラの起動]
    FUNC_JS_3[カメラ映像のサイズを取得]
    FUNC_JS_4[カメラ映像を一時用Canvasにセット]
    FUNC_JS_5[一時用CanvasからImageURIを取得]
    FUNC_JS_6[検出結果の描画_画像を表示用Canvasに出力]

    FUNC_JS_1 --> FUNC_JS_2
    FUNC_JS_2 --> FUNC_JS_3
    LOOP_START --> FUNC_JS_4
    FUNC_JS_4 --> FUNC_JS_5
    FUNC_JS_5 --> FUNC_BWA_3
    FUNC_BWA_5 --> FUNC_JS_6

end

FUNC_BWA_1 --> FUNC_JS_1
FUNC_JS_3 --> FUNC_BWA_2
   
```

:::note info
画像を取得し、Canvasにセットするのは、WebAssemblyでは直接カメラの画像を取得できないためです。
一旦、Canvasにセットしてから、ImageURIとして取り出します。
:::

## カスタマバーコードの読み取り

Qiitaの記事でバーコード読み取りに関して取り上げている記事があります。バーコードの種類は違いますが大まかな流れは一緒なのでこちらを先に読むと良いです。

https://qiita.com/syunnsyunn74/items/a9f53a43430524280cdb

以下のような流れで検出しています。(実際の実装と差異があるので注意)

```mermaid
flowchart TD
    FUNC1[画像の入力] --> FUNC2[画像の加工]
    FUNC2 --> LOOP_START(ループSTART)
    LOOP_START --> LOOP_FUNC1[画像の回転]
    LOOP_FUNC1 --> LOOP_FUNC2[バーコード検出]
    LOOP_FUNC2 --"バーコードを検出"--> LOOP_FUNC3[検出結果の格納]
    LOOP_FUNC2 --"バーコードの未検出"--> LOOP_FUNC4[一定回数以上回転しているか]
    LOOP_FUNC4 --"一定以上回転していない"--> LOOP_END
    LOOP_FUNC4 --"一定以上回転している(ループを抜ける)"--> FUNC3
    LOOP_END(ループEND) --> LOOP_START
    LOOP_FUNC3 --"ループを抜ける"--> FUNC3[検出結果の出力]
   
```

* 画像の加工
  - 入力画像は、カメラで撮影したものであれば明るさや撮影時の影などがあるので、バーコード検出の邪魔にならないように加工します。

:::note info
バーコード読み取りのために2値化しますが、OpenCVの標準的な2値化関数だとカメラ撮影時の影をうまく処理できないので、Cv2.AdaptiveThresholdを使用することをおすすめします。
:::

* 画像の回転
  - カスタマバーコードの読み取りを行う際に、入力画像に対して横方向に読み取りを行います。
    - カスタマバーコードはその性質上、傾きには弱いですので、傾いているものには角度の補正が必要となります。
  - 画像の回転は繰り返し行い、様々な角度でバーコード検出できるようにしました。
    - 本来は、速度面での影響を考えカスタマバーコードの傾きを算出し、それを基に回転するのが効率的ですが、算出アルゴリズムを思いつかなかったのでこのような方法を取りました。多分、傾きを検知するアルゴリズムはあるはずですが、見つからない&思いつきませんでした。
* バーコード検出
  - 専用のアルゴリズムを用いて検出します(下の項目で説明)。

## バーコード検出

バーコードの読み取りを行う際は、画像の行毎で検出を行います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108558/143f778b-3c45-cd36-982b-fb2db52937da.png)

下の画像の通り、カスタマバーコード上の画素を通った際にそれがカスタマバーコードであることを検出します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108558/1e127094-ab15-35d8-578f-5014c8b64e0d.png)

ちなみに、カスタマバーコードに傾きがある場合は、下の様にバーコードの全てを通らなくなるため検出できなくなります。なので、バーコード検出前に画像に回転し、検出できるようにしています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108558/3dce9269-75fc-57e2-d2a6-03d09d9170f7.png)

バーコード検出の流れは、以下のような感じで行っています。(実際の実装と差異があるので注意)

```mermaid
flowchart TD

    FUNC1[画像の入力] 
    FUNC2[バーコードは検出できず]
    FUNC5[デコード結果を格納]
    FUNC6[バーコード検出完了]

    FUNC1 --> LOOP_START

subgraph LOOP[ループ1]
    LOOP_START(ループSTART)
    LOOP_FUNC1[対象行の左側から縦棒を検出]
    LOOP_FUNC2[バーコードの始まりと見なす]
    LOOP_FUNC3[画像の画素を全て走査したか]
    LOOP_FUNC4[次の行を参照する]
    LOOP_END(ループEND)

    LOOP_START --> LOOP_FUNC1
    LOOP_FUNC1 --"検出できた"--> LOOP_FUNC2
    LOOP_FUNC1 --"検出できなかった"--> LOOP_FUNC3
    LOOP_FUNC3 --"走査していない"--> LOOP_FUNC4
    LOOP_FUNC4 --> LOOP_END
    LOOP_FUNC2 --> LOOP_START2
    LOOP_END --> LOOP_START

    LOOP_FUNC2_5 --"フォーマットに従っていない"--> LOOP_END
    LOOP_FUNC2_3 --"間隔が一定でない"--> LOOP_END
    LOOP_FUNC2_1 --"検出できなかった"--> LOOP_END
    LOOP_FUNC2_7 --"チェックデジットに誤りがある"--> LOOP_END

    subgraph LOOP2[ループ2]
        LOOP_START2(ループSTART2)
        LOOP_FUNC2_1[次の縦棒を検出]
        LOOP_FUNC2_2[一つ前の縦棒との間隔を取得]
        LOOP_FUNC2_3[それまでの縦棒同士の間隔と比較]
        LOOP_FUNC2_4[検出した縦棒の数が67本か]
        LOOP_FUNC2_5[バーコードのフォーマットチェック]
        LOOP_FUNC2_6[バーコードをデコード]
        LOOP_FUNC2_7[デコード結果のチェックデジットをチェック]
        LOOP_END2(ループEND2)
        
        LOOP_START2 --> LOOP_FUNC2_1
        LOOP_FUNC2_1 --"検出できた"--> LOOP_FUNC2_2
        LOOP_FUNC2_2 --> LOOP_FUNC2_3
        LOOP_FUNC2_3 --"間隔が一定である"--> LOOP_FUNC2_4
        LOOP_FUNC2_4 --"67本未満"--> LOOP_END2
        LOOP_FUNC2_4 --"67本"--> LOOP_FUNC2_5
        LOOP_FUNC2_5 --"フォーマットに従っている"--> LOOP_FUNC2_6
        LOOP_FUNC2_6 --> LOOP_FUNC2_7
        LOOP_FUNC2_7 --"チェックデジットが合っている"--> FUNC5
        LOOP_END2 --> LOOP_START2
    end

end

LOOP2:::LOOP2


    LOOP_FUNC3 --"走査した"--> FUNC2
    FUNC5 --> FUNC6

classDef LOOP2 fill:#defe6e

```

細かい説明は行いませんが簡単に流れを説明するこんな感じです。
1. バーコードの縦棒67本の検出
1. バーコードのフォーマットチェック
1. バーコードのデコード
1. チェックデジットのチェック(誤検出チェック)
1. すべて問題が無ければ、デコード結果を格納する

# まとめ

機能・ソースコードとしては比較的コンパクトですが、デスクトップアプリケーションに比べて、WebAssemblyやブラウザ事情など複雑な部分があり一筋縄では実装できませんでした。物ができるまで大変だったので、こうして記事をまとめてみました。
あまり洗練されている実装とは言えませんが、どこかの誰かの役に立てればと思います。

[^1]:もっとも、最近のブラウザはJavaScriptでも十分早いので、特別なケース以外ではJavaScriptでも事足りるとは思いますが。
[^2]: OpenCVSharpは、OpenCVのC#ラッパー。WebAssemblyにも対応している。[GitHub](https://github.com/shimat/opencvsharp)