※アカウント統合により過去記事再掲:元の投稿日 2016年03月03日
バーコード読み取りにZxingを使っている人は多いと思う。
非常に認識率の高いライブラリなのだが、しかし画像読み取りの解像度や明るさによっては、微妙に読めたり読めなかったり、全く読めなかったりと認識率が安定しなくなってしまう。
画像補正でなんとか認識率を上げてみようではないか、と。そういう試みなのである。
まずは均等化してみる
白と黒を定義する
均等化するために必要なのが、対象画像の「黒」と「白」を定義する事だ。
最初は最小の平均RGB値を持つものを黒、最大を白としていたが、これだとなんか暗い値はいいけど明るい値は255周辺しか検出しない……。スキャンした画像なんか、どんな画像でも1ピクセルくらいほぼ真っ白のピクセルが混ざってたりするのか。そういうもんなのかね。
じゃあどうしようか。平均値でいいか。
平均RGB値の最小ピクセルを「黒」
平均RGB値の128以上のピクセルの、平均値を「白」
そんな感じで「黒」と「白」を定義してみたのであった。
「黒」を0に、「白」を255に揃える
こうなると、例えば暗い画像だと「黒」は0だが「白」は211だったり、明るい画像だと「黒」が119で「白」が255と、そこそこそれっぽい数値が出た。
まず暗い方の画像。これは0~211なので、211階調のグレイスケールに変換しちゃう。えいや。
↓
お。綺麗になったじゃん。
明るい方の画像も同じ理屈で均等化しちゃう。
くっきりしてきたぞ。
いいね、いいね。
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くらいでいいかな(適当)
こんなイメージ。
170以上は一括255に、85以下は一括0にして、その間はそれなりに曖昧にしといて、と。
さあどうなるかな。
まず原本。
↓ くっきり化
↓ はっきり化
おお。いい感じ。
明るい方もやってみよう。
↓ くっきり化
↓ はっきり化
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%以上と劇的に改善、誤読率も改善、やったね。
頭のいい人が研究した論文を活用したとか、そういう賢いものではない、いい加減なやり方なのだけれども、取り敢えずこういうちょっぴり潰れたバーコードでも読めるようになったんで大体オッケーって事です。
しかし、もしかしたら
俺が見つけられなかっただけで、こういう読み取り精度向上のアルゴリズム的なモノってさ、ちゃんとしたのがどっかに転がってたりするのかしら?
もしそういうのをご存知の方は教えて下さい。