2値化するときに使うしきい値の計算を、注目ピクセルの周囲のピクセルの値を使ってする
範囲は1指定なら3x3ピクセル、2なら5x5、3なら7x7とするようにして
計算は平均値にしてみた
範囲1で注目ピクセルの座標が(1,1)のとき
3x3ピクセルの平均値は162.2 = (140+210+210+50+130+200+90+190+240)/9=162.22222
注目ピクセルの130は、平均値162.2未満なので0にする
右に移動して、注目ピクセル(2,1)
平均値は136.6 = (50+130+200+90+190+240+60+30+240)/9=136.66667
注目ピクセルの190は平均値136.6以上なので255にする
範囲を2(周囲5x5)に固定してC#で書いてみると
/// <summary> /// 2値化、注目ピクセルの周囲5x5ピクセルの平均をしきい値にして2値化 /// </summary> /// <param name="source">PixelFormats.Gray8専用</param> /// <returns></returns> private BitmapSource LocalArea5x5Threshold(BitmapSource source) { //Bitmapから配列作成 int w = source.PixelWidth; int h = source.PixelHeight; int stride = w; byte[] pixels = new byte[h * stride]; source.CopyPixels(pixels, stride, 0); //2値化用配列 byte[] result = new byte[pixels.Length]; for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { //注目ピクセルの上下左右2ピクセル(5x5)範囲の値合計 long total = 0; int count = 0; for (int i = -2; i <= 2; i++) { for (int j = -2; j <= 2; j++) { int yy = y + i; int xx = x + j; //画像の外になる座標は無視する if (yy >= 0 && yy < h && xx >= 0 && xx < w) { total += pixels[yy * stride + xx]; count++; } } } //平均値をしきい値にして2値化 double threshold = total / (double)count; int p = y * stride + x; if (pixels[p] < threshold) result[p] = 0; else result[p] = 255; } } return BitmapSource.Create(w, h, 96, 96, PixelFormats.Gray8, null, result, stride); }
//画像の外になる座標は無視する
参照範囲2で注目ピクセルの座標が(1, 0)のときの参照範囲
マイナス座標(画像の外)になるところが出るので、平均値を求めるときの加算対象にしないようにしたけど、画像の外か内なのかの判定が大変
画像処理してみる
右下の消費期限の写りが良くない画像を処理すると
2020/10/27って見えた、でもみやすさで言ったらどちらも、あんまり変わんないかな
範囲を1にして処理
こちらのほうが見やすくなった
範囲5
結構変わる
画像全体で決めるしきい値の場合
大津の2値化だと
Kittlerの2値化だと
手動でしきい値20
西暦と月が見えるところだと、日にち部分が真っ黒
しきい値77
日にちが見えるところだと他が真っ黒
これだと画像全体で決めるしきい値よりも、今回のような注目ピクセルの周りのピクセル(局所範囲)から計算する、しきい値のほうがいいように見えるけど、使いみちが違うんだと思う
今回のだと減色パレットの作成には使えなさそうなんだよねえ、それでも結果は面白い
さっきは範囲が固定だったのを範囲指定をできるようにしたのがこれ
/// <summary> /// 2値化、注目ピクセルの指定範囲の平均をしきい値にして2値化 /// </summary> /// <param name="pixels">PixelFormats.Gray8専用</param> /// <param name="width"></param> /// <param name="height"></param> /// <param name="stride"></param> /// <param name="near">範囲を1以上で指定、1なら上下左右1ピクセルで3x3マスの範囲になる、2なら5x5</param> /// <returns></returns> private BitmapSource LocalThreshold(byte[] pixels, int width, int height, int stride, int near) { //局所範囲のピクセル数 int count; //2値に置き換えた用 byte[] result = new byte[pixels.Length]; //2値化 for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { long total = 0; count = 0; for (int i = -near; i <= near; i++) { int yy = y + i; if (yy >= 0 && yy < height)//y座標有効判定 { for (int j = -near; j <= near; j++) { int xx = x + j; if (xx >= 0 && xx < width)//x座標有効判定 { total += pixels[(y + i) * stride + x + j]; count++; } } } } //局所範囲の平均値をしきい値にして2値化 double threshold = total / (double)count; int p = (y * stride) + x;//注目ピクセルのインデックス if (pixels[p] < threshold) result[p] = 0; else result[p] = 255; } } return MakeBitmapSource(result, width, height, stride); } private BitmapSource MakeBitmapSource(byte[] pixels, int width, int height, int stride) { return BitmapSource.Create(width, height, 96, 96, PixelFormats.Gray8, null, pixels, stride); }
範囲が広くなると計算量がものすごく増える、1指定なら3x3で9ピクセルだったのが、5だと(5*2+1)2=121ピクセル、10だと(10*2+1)2=441!!!!
処理の高速化
範囲2のときでの処理の流れ
注目ピクセルの座標が(2,2)のときの参照範囲
こうなっていて、次のピクセルの(3,2)の参照範囲は
こう
右に1個ずれるだけだから重複しているところが多い
なので
注目ピクセルの座標が(2,2)の直前で
5x5の右端1列を除いた部分を足し算したのをAオレンジとして
5x5の左右の1列をそれぞれ足し算、それぞれB灰色、C水色として
しきい値計算のときにA+CをD赤として、これは5x5の合計値になるので25で割って平均値、これで2値化して
D-BしたのをAに入れて次の注目ピクセルの計算に持ち越す、これを繰り返していく
次の注目ピクセルの座標は(3,2)
持ち越したAオレンジがあるので、新たに足し算するピクセルは右端1列の5個だけでよくなる
処理時間の比較
製作と計測環境
- CPU AMD Ryzen 5 2400G(4コア8スレッド)
- MEM DDR4-2666
- Window 10 Home 64bit
- Visual Studio 2019 Community .NET Core 3.1 WPF C#
1632x1224ピクセルの画像
範囲2(5x5)を指定
約3倍の差、0.122/0.039=3.1282051
範囲5(11x11ピクセル)を指定
約10倍の差、0.555/0.055=10.090909
範囲10(21x21=441ピクセル!)を指定
約16倍の差、1.862/0.113=16.477876
大差だけど思ったほどではないかなあ、それに差分計算では画像の周縁部は無視しているので、範囲に指定した数値のピクセル幅分が真っ黒になる
真っ黒にならないようにして、一部をマルチスレッドにしたのが
/// <summary> /// 指定座標の周縁部の平均値を返す /// </summary> /// <param name="pixels"></param> /// <param name="x">指定座標x</param> /// <param name="y">指定座標y</param> /// <param name="near">周縁部の広さ、1以上を指定、1なら上下左右1マスづつで3x3の範囲、2指定は5x5</param> /// <param name="width">画像の横ピクセル数</param> /// <param name="height">画像の縦ピクセル数</param> /// <param name="stride"></param> /// <returns></returns> private double AroundAverage(byte[] pixels, int x, int y, int near, int width, int height, int stride) { long total = 0; int effectiveNumber = 0; for (int i = -near; i <= near; i++) { int yy = y + i; if (yy >= 0 && yy < height)//y座標有効判定 { for (int j = -near; j <= near; j++) { int xx = x + j; if (xx >= 0 && xx < width)//x座標有効判定 { total += pixels[(y + i) * stride + x + j]; effectiveNumber++; } } } } return total / (double)effectiveNumber; } //指定範囲を2値化する private void SetBinaryArea(int xBegin, int xEnd, int yBegin, int yEnd, byte[] pixels, int near, int width, int height, int stride, byte[] result) { Parallel.For(yBegin, yEnd, y => { for (int x = xBegin; x < xEnd; x++) { //局所範囲の平均値をしきい値にする double threshold = AroundAverage(pixels, x, y, near, width, height, stride); //しきい値で2値化 int p = (y * stride) + x;//注目ピクセルのインデックス if (pixels[p] < threshold) result[p] = 0; else result[p] = 255; } }); } private BitmapSource LocalThreshold差分計算Multi改(byte[] pixels, int width, int height, int stride, int near) { //局所範囲のピクセル数 int localAreaLength = (near * 2 + 1) * (near * 2 + 1); //2値に置き換えた用 byte[] result = new byte[pixels.Length]; //中央部の2値化 Parallel.For(near, height - near, y => { long motikosiTotal = 0; for (int my = -near; my <= near; my++) { for (int mx = 0; mx < near * 2; mx++) { motikosiTotal += pixels[(y + my) * stride + mx]; } } for (int x = near; x < width - near; x++) { long totalNew = 0; long totalOld = 0; for (int yy = -near; yy <= near; yy++) { totalNew += pixels[(y + yy) * stride + x + near]; totalOld += pixels[(y + yy) * stride + x - near]; } long totalAll = motikosiTotal + totalNew; //局所範囲の平均値をしきい値にする double threshold = totalAll / (double)localAreaLength; //しきい値で2値化 int p = (y * stride) + x;//注目ピクセルのインデックス if (pixels[p] < threshold) result[p] = 0; else result[p] = 255; // motikosiTotal = totalAll - totalOld; } }); //画像周縁部の2値化 //左側 SetBinaryArea(0, near, 0, height, pixels, near, width, height, stride, result); //右側 SetBinaryArea(width - near, width, 0, height, pixels, near, width, height, stride, result); //上側 SetBinaryArea(0, width, 0, near, pixels, near, width, height, stride, result); //下側 SetBinaryArea(0, width, height - near, height, pixels, near, width, height, stride, result); return MakeBitmapSource(result, width, height, stride); }
画像周縁部の処理が大変、もっといい方法ないかなあ
高速化はマルチスレッド化よりも差分計算にしたのが大きいねえ
範囲20((20*2+1)2=1681ピクセル)でも
7秒かかっていたのが
0.087秒で処理できるようになった
範囲1結果
範囲2
範囲5
範囲10
範囲20
今回のアプリ
[20200421_局所平均しきい値で2値化.zip]
↑は画像表示なしのときにボタンを押すとエラーになっていたので
20200421_局所平均しきい値で2値化ver1.1.zip
(2020/05/29追記)
関連記事
続きの記事は1ヶ月後
次のWPF記事は2週間後
前回のWPF記事は3日前
前回の画像2値化アプリは5日前
gogowaten.hatenablog.com