画像補正で一次元バーコードの認識率を向上させる

**※アカウント統合により過去記事再掲：元の投稿日 2016年03月03日**

バーコード読み取りにZxingを使っている人は多いと思う。

非常に認識率の高いライブラリなのだが、しかし画像読み取りの解像度や明るさによっては、微妙に読めたり読めなかったり、全く読めなかったりと認識率が安定しなくなってしまう。

画像補正でなんとか認識率を上げてみようではないか、と。そういう試みなのである。

# まずは均等化してみる

## 白と黒を定義する
均等化するために必要なのが、対象画像の「黒」と「白」を定義する事だ。
最初は最小の平均RGB値を持つものを黒、最大を白としていたが、これだとなんか暗い値はいいけど明るい値は255周辺しか検出しない……。スキャンした画像なんか、どんな画像でも1ピクセルくらいほぼ真っ白のピクセルが混ざってたりするのか。そういうもんなのかね。
じゃあどうしようか。平均値でいいか。

**平均RGB値の最小ピクセルを「黒」
平均RGB値の128以上のピクセルの、平均値を「白」**

そんな感じで「黒」と「白」を定義してみたのであった。

## 「黒」を0に、「白」を255に揃える
こうなると、例えば暗い画像だと「黒」は0だが「白」は211だったり、明るい画像だと「黒」が119で「白」が255と、そこそこそれっぽい数値が出た。

まず暗い方の画像。これは0～211なので、211階調のグレイスケールに変換しちゃう。えいや。
![org1.jpg](https://qiita-image-store.s3.amazonaws.com/0/104780/036ff782-7e86-2b6f-6c22-05411f8a06dd.jpeg)
↓
![org1.s.jpg](https://qiita-image-store.s3.amazonaws.com/0/104780/ffbe92c9-002f-9cc1-de05-a0cf850a6e83.jpeg)

お。綺麗になったじゃん。
明るい方の画像も同じ理屈で均等化しちゃう。

![org6.jpg](https://qiita-image-store.s3.amazonaws.com/0/104780/80e44803-9f20-c89f-eef6-94df523d96d9.jpeg)
↓
![org6.s.jpg](https://qiita-image-store.s3.amazonaws.com/0/104780/3979897d-5ab1-6107-24f1-b05085574532.jpeg)

くっきりしてきたぞ。
いいね、いいね。

```csharp:くっきり化
internal static Bitmap _smooth(Bitmap img)
{
    int width = img.Width;
    int height = img.Height;

    // 出力用のビットマップ
    var dstImg = new Bitmap(width, height);

    // ピクセルに直接アクセスしにいく
    var bmpread = img.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
    var bmpwrite = dstImg.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb);

    int blackness = 255;
    int whiteness = -1;

    for (int i = 0; i < (width * height * 3); )
    {
        var r = Marshal.ReadByte(bmpread.Scan0, i++);
        var g = Marshal.ReadByte(bmpread.Scan0, i++);
        var b = Marshal.ReadByte(bmpread.Scan0, i++);

        int ave = (int)new List<int> { r, g, b }.Average();

        // 最も暗い色を抜き出して「黒」を定義
        blackness = Math.Min(blackness, ave);

        // 128以上の値を平均して「白」を定義
        if (ave >= 128)
        {
            if (whiteness == -1)
                whiteness = ave;
            else
                whiteness = (whiteness + ave) / 2;
        }
    }

    var step = 255d / ((double)(whiteness - blackness));

    for (int i = 0, j = 0; i < (width * height * 3); )
    {
        var r = Marshal.ReadByte(bmpread.Scan0, i++);
        var g = Marshal.ReadByte(bmpread.Scan0, i++);
        var b = Marshal.ReadByte(bmpread.Scan0, i++);

        var v = new List<int> { r, g, b }.Average();

        v -= blackness;

        v *= step;

        v = Math.Min(255, Math.Max(0, v));

        Marshal.WriteByte(bmpwrite.Scan0, j++, (byte)v);
        Marshal.WriteByte(bmpwrite.Scan0, j++, (byte)v);
        Marshal.WriteByte(bmpwrite.Scan0, j++, (byte)v);
    }

    dstImg.UnlockBits(bmpwrite);
    img.UnlockBits(bmpread);

    return dstImg;
}    
```

# もっと画像をはっきりさせたい
これでもいいんだけど、まだちょっと曖昧だな。認識率もそれほど劇的には上がってない。
もう一工夫必要かしらね。
もう少し曖昧さを排除して……どんくらいかな。
ま、1/3くらいでいいかな（適当）
![gyutto.jpg](https://qiita-image-store.s3.amazonaws.com/0/104780/eeb55c75-230c-3826-564a-a38cadf8a93b.jpeg)

こんなイメージ。
170以上は一括255に、85以下は一括0にして、その間はそれなりに曖昧にしといて、と。

さあどうなるかな。
まず原本。
![org1.jpg](https://qiita-image-store.s3.amazonaws.com/0/104780/f9ee7cd3-f15d-c352-e893-b0de7c3de52d.jpeg)
↓　くっきり化
![org1.s.jpg](https://qiita-image-store.s3.amazonaws.com/0/104780/c47bc9b2-9cbc-d0ca-e97a-37704d80f6e5.jpeg)
↓　はっきり化
![org1.c.jpg](https://qiita-image-store.s3.amazonaws.com/0/104780/ae1b27ea-b926-2ed0-6642-65718161e968.jpeg)

おお。いい感じ。
明るい方もやってみよう。
![org6.jpg](https://qiita-image-store.s3.amazonaws.com/0/104780/07969e66-0fcd-8fd1-228e-f510eb55030a.jpeg)
↓　くっきり化
![org6.s.jpg](https://qiita-image-store.s3.amazonaws.com/0/104780/0e3be6a9-2ad4-6611-db91-dd978cb42f30.jpeg)
↓　はっきり化
![org6.c.jpg](https://qiita-image-store.s3.amazonaws.com/0/104780/71fbdee2-9195-8b6c-4f2d-53e39de7339c.jpeg)

```csharp:はっきり化
internal static Bitmap _correction(Bitmap img)
{
    int width = img.Width;
    int height = img.Height;

    // 出力用のビットマップ
    var dstImg = new Bitmap(width, height);

    // ピクセルに直接アクセスする
    var bmpread = img.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
    var bmpwrite = dstImg.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb);

    int blackness = 255;
    int whiteness = -1;

    for (int i = 0; i < (width * height * 3); )
    {
        var r = Marshal.ReadByte(bmpread.Scan0, i++);
        var g = Marshal.ReadByte(bmpread.Scan0, i++);
        var b = Marshal.ReadByte(bmpread.Scan0, i++);

        int ave = (int)new List<int>{ r, g, b }.Average();

        // 最も暗い色を抜き出して「黒」を定義
        blackness = Math.Min(blackness, ave);

        // 128以上の値を平均して「白」を定義
        if (ave >= 128)
        {
            if (whiteness == -1)
                whiteness = ave;
            else
                whiteness = (whiteness + ave) / 2;
        }
    }

    // 暗さの調整
    int darkness = 255 - whiteness;
    whiteness -= darkness;

    // 明るさの調整
    blackness += blackness / 2;

    // 黒と白の数値の差を取り閾値を作る
    int adjust = (whiteness - blackness) / 3;

    blackness += adjust;
    whiteness -= adjust;

    int diff = whiteness - blackness;

    for (int i = 0, j = 0; i < (width * height * 3); )
    {
        var r = Marshal.ReadByte(bmpread.Scan0, i++);
        var g = Marshal.ReadByte(bmpread.Scan0, i++);
        var b = Marshal.ReadByte(bmpread.Scan0, i++);

        // 黒の閾値以下ならそのピクセルは黒
        if (r < blackness ||
            g < blackness ||
            b < blackness)
        {
            Marshal.WriteByte(bmpwrite.Scan0, j++, 0);
            Marshal.WriteByte(bmpwrite.Scan0, j++, 0);
            Marshal.WriteByte(bmpwrite.Scan0, j++, 0);
        }
        // 白の閾値以上ならそのピクセルは白
        else if (r > whiteness ||
                    g > whiteness ||
                    b > whiteness)
        {
            Marshal.WriteByte(bmpwrite.Scan0, j++, 255);
            Marshal.WriteByte(bmpwrite.Scan0, j++, 255);
            Marshal.WriteByte(bmpwrite.Scan0, j++, 255);
        }
        else
        {
            double v = new List<int> { r, g, b }.Average();
            v -= diff;
            v *= 3;

            v = Math.Min(255, Math.Max(0, v));

            Marshal.WriteByte(bmpwrite.Scan0, j++, (byte)v);
            Marshal.WriteByte(bmpwrite.Scan0, j++, (byte)v);
            Marshal.WriteByte(bmpwrite.Scan0, j++, (byte)v);
        }
    }

    dstImg.UnlockBits(bmpwrite);
    img.UnlockBits(bmpread);

    return dstImg;
}
```

くっきり化とはっきり化は処理が被るところがあるんで、いいようにリファクタリングして下さい。これとあと「すっきり化」ってのも作ってみたんだけど、あんま意味が無かったんでお蔵入り。

# さて気になる認識率は・・・？
「くっきり化」→「はっきり化」と補正する事で、数千枚の試験画像での認識率が50%程度から90%以上と劇的に改善、誤読率も改善、やったね。
頭のいい人が研究した論文を活用したとか、そういう賢いものではない、いい加減なやり方なのだけれども、取り敢えずこういうちょっぴり潰れたバーコードでも読めるようになったんで大体オッケーって事です。

# しかし、もしかしたら
俺が見つけられなかっただけで、こういう読み取り精度向上のアルゴリズム的なモノってさ、ちゃんとしたのがどっかに転がってたりするのかしら？

もしそういうのをご存知の方は教えて下さい。