【Android】dm77版ZXingライブラリの高密度バーコードのスキャン精度を向上させる

Improve the scanning accuracy of high-density barcodes using the dm77 version of the ZXing library.
You can translate this page into English.

# はじめに
何年もメンテナンスされているアプリの中ではバーコードスキャンに一世代前の[ZXing(dm77版)](https://github.com/dm77/barcodescanner)を使用していることも多いかと思います。

このdm77版ZXingは、バーコードスキャンに関して若干の問題があり、端末によってはCode-128に代表される高密度バーコードの読み取りが不可能、もしくは認識まで多くの時間（１０秒以上）を要することがあります。

現在はバーコードスキャンにML KitをCamera2と併用して実装する一択ですが、アプリ内の全てのスキャンの実装をML Kitに換装する。という方法はAGP8アップデートを余儀なくされたりと、古いアプリにとっては無視できない問題が多くあります。

また、dm77版ZXingライブラリはすでに開発が停止しているためアップデートを期待することもできません。

なんとか工数をかけずに解決する方法はないでしょうか？

ここでは、ZXingのソースコードに若干手を加えて、バーコード読み取り精度を向上させる方法について解説していきます。なお、<b>この方法は自己責任でお願いいたします</b>。

# dm77版ZXingのバーコード読み取り精度が悪い原因
原因は、ZXingは端末自体が高解像度のカメラを備えていたとしてもなぜか小さなカメラ解像度を選択してしまうことです。このため、オートフォーカス後でも粗いドット画像から高密度バーコードの認識をすることができないという状況が生まれてしまいます。

ですので、ZXingのカメラ解像度の選択ロジックを修正すれば良い。ということになります。
ではZXingのカメラ解像度選択ロジックを見ていきましょう。（便宜上1.9.8のソースコードをベースに解説します）

```java:CameraPreview.java
// (省略)

private Camera.Size getOptimalPreviewSize() {
        // (省略)

        // sizes : 端末がサポートしている全てのカメラ解像度
        List<Camera.Size> sizes = mCameraWrapper.mCamera.getParameters().getSupportedPreviewSizes();

        // w, h : プレビュー表示領域のサイズ
        int w = getWidth();
        int h = getHeight();
        if (DisplayUtils.getScreenOrientation(getContext()) == Configuration.ORIENTATION_PORTRAIT) {
            int portraitWidth = h;
            h = w;
            w = portraitWidth;
        }

        // targetRatio ... プレビュー表示領域のアスペクト比
        double targetRatio = (double) w / h;
        if (sizes == null) return null;

        // optimalSize : 最終的に選択されるカメラ解像度（戻り値）
        Camera.Size optimalSize = null;

        // minDiff : スクリーンの縦サイズ(px)とカメラ解像度の縦サイズ(px)の差
        double minDiff = Double.MAX_VALUE;

        int targetHeight = h;

        // Try to find an size match aspect ratio and size
        for (Camera.Size size : sizes) {
            double ratio = (double) size.width / size.height;
            // mAspectTolerance : カメラのアスペクト比とプレビュー表示領域のアスペクト比の差分
            //                   （＝アスペクト比差分の許容範囲）デフォルト値0.1
            if (Math.abs(ratio - targetRatio) > mAspectTolerance) continue; // ※1
            if (Math.abs(size.height - targetHeight) < minDiff) { // ※2
                optimalSize = size;
                minDiff = Math.abs(size.height - targetHeight);
            }
        }

        // (省略)

        return optimalSize;
    }
// (省略)
```
ソースコードを抜粋し、コメントで少し補強しました。
※1, ※2のループでサポートされている全てのカメラ解像度からどの解像度を使用するかを選別しています。
このコードによると、カメラ解像度の選別条件は以下のようになります。

1. ※1の判定：ブレビュー表示領域のアスペクト比とカメラのアスペクト比の差分が mAspectTolerance 以内に収まること（カメラ解像度が許容範囲内のアスペクト比であること）

2. ※2の判定：かつ、プレビュー表示領域の縦幅(px)に最も近いカメラ解像度の縦幅(px)であること

この2. が低解像度のカメラを選別してしまう原因です。プレビュー表示領域の縦幅(px)に最も近いカメラ解像度の縦幅(px)が最有力候補として選択されてしまっています。このため、高解像度のカメラを備えている端末でも、スクリーンのピクセルサイズに近いカメラ解像度しか選択されないことになります。
このロジックでCode128のような高密度バーコードを読み取るのは難しそうです。

# 高解像度カメラを選択する修正
ではこのコードに少し手を加えて可能な限り高解像度のカメラを選択できるようにしてみます。
```java:CameraPreview.java
// (省略)

private Camera.Size getOptimalPreviewSize() {
        // (省略)

        // 削除
-        // double minDiff = Double.MAX_VALUE;
-        // int targetHeight = h;

        // （追加）resolution : カメラの横幅(px) X 縦幅(px)
+        long resolution = 0L;

        // Try to find an size match aspect ratio and size
        for (Camera.Size size : sizes) {
            double ratio = (double) size.width / size.height;
            if (Math.abs(ratio - targetRatio) > mAspectTolerance) continue;
            
            // 削除
-            //if (Math.abs(size.height - targetHeight) < minDiff) {
-            //    optimalSize = size;
-            //    minDiff = Math.abs(size.height - targetHeight);
-            //}

            // 追加
+            long cameraResolution = (long)size.width * (long)size.height
+            if (resolution < cameraResolution) {
+                optimalSize = size
+                resolution = cameraResolution
+            }
        }
        // (省略)

        return optimalSize;
    }
// (省略)
```
カメラの選別条件を以下のように修正しました。

1. ※1の判定 ... 変更しない

2. ※2の判定：かつ、最も良いカメラ解像度であること

:::note info
ここでは割愛しますが、この修正はこだわればもっと良い選別条件を吟味して実装することもできます。
:::
これで低解像度カメラを選択してしまう問題は解決しました。しかし、ファインダー（ViewFinderView）を併用しているアプリの場合、このままではバーコードをファインダーの中に収めてもバーコードが認識されません。さらに追加で修正が必要となります。

# ファインダーのクリッピング座標の修正

```java:BarcodeScannerView.java
public synchronized Rect getFramingRectInPreview(int previewWidth, int previewHeight) {
        if (mFramingRectInPreview == null) {
            Rect framingRect = mViewFinderView.getFramingRect();
            int viewFinderViewWidth = mViewFinderView.getWidth();
            int viewFinderViewHeight = mViewFinderView.getHeight();
            if (framingRect == null || viewFinderViewWidth == 0 || viewFinderViewHeight == 0) {
                return null;
            }

            Rect rect = new Rect(framingRect);

            // Note: この分岐は、高解像度カメラが選択されているので通過しません
            if(previewWidth < viewFinderViewWidth) {
                rect.left = rect.left * previewWidth / viewFinderViewWidth;
                rect.right = rect.right * previewWidth / viewFinderViewWidth;
            }

            // Note: この分岐は、高解像度カメラが選択されているので通過しません
            if(previewHeight < viewFinderViewHeight) {
                rect.top = rect.top * previewHeight / viewFinderViewHeight;
                rect.bottom = rect.bottom * previewHeight / viewFinderViewHeight;
            }

+            // 追加　ここから
+            double aspectW = (double)previewWidth / (double)viewFinderViewWidth
+            double aspectH = (double)previewHeight / (double)viewFinderViewHeight
+            rect.left = (int)((double)rect.left * aspectW)
+            rect.right = (int)((double)rect.right * aspectW)
+            rect.top = (int)((double)rect.top * aspectH)
+            rect.bottom = (int)((double)rect.bottom * aspectH)
+            // ここまで

            mFramingRectInPreview = rect;
        }
        return mFramingRectInPreview;
    }
```
細部の説明は割愛します。（簡単に説明しますと、カメラ画像をファインダーの領域でクリッピングする座標計算処理を修正しています）

:::note alert
過去の記憶を思い出しながら書いていますので間違いがあったらご容赦を…
:::

# 最後に
ここまでの修正で高密度バーコードをオートフォーカス直後に高速で認識できるようになったと思います。
雑な修正もありますが、本稿では修正の要点のみとさせていただきます。細かいところは調整してみてください。

ZXingをインポートしている場合、ZXing内のクラスをアプリ側に複製して対応可能です。

ML Kit導入までのつなぎとして、本投稿が皆様のお役にたてれば幸いです。