これまでに幾つかの画像リサイズフィルタを紹介してきました。
あともう一つ紹介したいところですが、その前に実装したコードにいくらか問題点があるのでそれらを解決したいと思います。
距離計算をちゃんとする
拙作の手抜きプログラムではソースの値を1ピクセル単位でしかサンプルしていません。
それもゼロスタートです。つまり1ピクセルの端の座標(0,0)が距離計算に使われています。
しかし、距離計算は厳密にはピクセルの中心(0.5,0.5)からとってやる必要があります。
また、縮小がちゃんとできないのもこれに起因します。
縮小時は 縮小率 * n の距離にあるピクセルの値に重みを付けて凝縮しなければなりませんが、
現状だと最大で n * 2 の範囲までしかとれません。ついでに言えば縮小率に関係なく n * 2 を取ってしまうのでダメダメです。
その辺りの厳密な計算方法は下記のサイトを参照ください。
距離は半径で測るもの?
現在はX軸とY軸で距離を別々に計算して、重みを掛けあわせています。
ですが、ところによっては厳密な直線距離を取るべきという説があります。
実際にそれをやるには重い計算になりますが、X軸とY軸は直角に交わっているので三平方の定理を使って斜辺の長さを求めます。
定理によると直角三角形の斜辺の長さを2乗したものは、斜辺以外の2辺をそれぞれ2乗して足し合わせたのと等しいです。
つまり斜辺の長さdはX軸の距離dxとY軸の距離dyを用いて以下のコードで求められます。
d = Math.Sqrt(Math.Pow(dx, 2) + Math.Pow(dy, 2));
しかし、持論としてはピクセルって四角いので下手に円で距離計算すると逆に見栄え良くないんじゃないかと思っています。
自分の目で見る限りはX軸、Y軸を別々に求めるほうが綺麗に見えるので、そのままでいきたいと思います。
折り返しの処理
参照するピクセルが元画像の端を越えた場合、現状では一番端のピクセルを採取しています。
ですが、参照距離が広がるほどその一番端のピクセルの度合いが強くなり、縁の近辺に違和感がでる可能性があります。
これをなるべく抑える方法の一つとして鏡面処理が考えられます。
つまりは、端からオーバーしてる距離分だけ端から逆方向に参照するということです。
これを実装する関数はこんな感じ。
static int mirror(int x, int max)
{
if (x >= max)
{
x = max * 2 - x - 1;
}
else if (x < 0)
{
x = -x;
}
return x;
}
他にも予め特殊な計算で導き出した値で縁から先を延長しておく方法があるそうですが、
難しそうな上、コストも掛かるとのことでそこまではやりません。
改良型プログラムのコード
今回は久々に頭をフル回転させてまともなコードを書いてみました。
これで拡大と縮小のいずれにも対応可能です。
static Bitmap resize(Bitmap bmp, int width, int height, int n)
{
int srcw = bmp.Width;
int srch = bmp.Height;
double scalex = (double)width / srcw;
double scaley = (double)height / srch;
double stepx = scalex > 1.0 ? 1.0 : scalex;
double stepy = scaley > 1.0 ? 1.0 : scaley;
int tapx = (int)Math.Ceiling(n / stepx) * 2;
int tapy = (int)Math.Ceiling(n / stepy) * 2;
double ox = (1.0 / scalex - 1.0) / 2.0;
double oy = (1.0 / scaley - 1.0) / 2.0;
var dst = new Bitmap(width, height, PixelFormat.Format32bppRgb);
var bmpData = bmp.LockBits(new Rectangle(Point.Empty, bmp.Size), ImageLockMode.ReadOnly, PixelFormat.Format32bppRgb);
var dstData = dst.LockBits(new Rectangle(Point.Empty, dst.Size), ImageLockMode.WriteOnly, PixelFormat.Format32bppRgb);
unsafe
{
var bmpScan0 = (byte*)bmpData.Scan0;
var dstScan0 = (byte*)dstData.Scan0;
Parallel.For(0, height, dy =>
{
double srcoy = dy / scaley + oy;
for (int dx = 0; dx < width; dx++)
{
double srcox = dx / scalex + ox;
var rgb = new RGBTripleDouble();
double weightTotal = .0;
double disty = -(tapy / 2 - 1 + Math.Abs(srcoy) % 1) * stepy;
for (int iy = 0; iy < tapy; iy++, disty += stepy)
{
double wy = lanczosWeight(disty, n);
int sy = mirror((int)srcoy- tapy / 2 + iy + 1, srch);
double distx = -(tapx / 2 - 1 + Math.Abs(srcox) % 1) * stepx;
for (int ix = 0; ix < tapx; ix++, distx += stepx)
{
double wx = lanczosWeight(distx, n);
int sx = mirror((int)srcox - tapx / 2 + ix + 1, srcw);
double wxy = wx * wy;
weightTotal += wxy;
int spos = 4 * sx + bmpData.Stride * sy;
rgb.B += bmpScan0[spos + 0] * wxy;
rgb.G += bmpScan0[spos + 1] * wxy;
rgb.R += bmpScan0[spos + 2] * wxy;
}
}
int dpos = 4 * dx + dstData.Stride * dy;
dstScan0[dpos + 0] = trimByte(rgb.B / weightTotal);
dstScan0[dpos + 1] = trimByte(rgb.G / weightTotal);
dstScan0[dpos + 2] = trimByte(rgb.R / weightTotal);
}
});
}
bmp.UnlockBits(bmpData);
dst.UnlockBits(dstData);
return dst;
}
説明用コードなのでマルチスレッド化以外の高速化は施していません。
変数名などを見なおして可読性もマシになったかと思います。
ちなみに小数部だけを取り出したいときは% 1
とするだけでいけます。これ便利。
なお、これ一本で重み付け関数はn値と併せてBilinear、Bicubic、Spline、その他に差し替え可能です。
出力結果
今回はサンプル画像として文字も入ってて丁度いいCrystalDiskInfoでお馴染みの水晶雫ちゃんの応援バナーを題材にします。
比較画像は別タブでフルサイズ表示推奨します。
NearestNeighborは別で作成した参考用です。
拡大(2.37倍)
Bicubicは流石、一番使われてだけあって程よくシャープです。
ですが、曲線部分などはSpline36やLanczos3に滑らかさで劣りますね。
やはりLanczos3が一番シャープに出るようです。
Sincはなんかゴーストみたいなノイズがでてるので実用的ではありませんね。
縮小(0.78倍)
※縮小したものを見やすいようにNearestNeighborで各辺2倍拡大してます。
Bilinearの実装が強引すぎたのか変なことになってます。
他はどれもどっこいって感じですね。
しかし、Sincは拡大と同じくノイズ出ちゃってます。
おわりに
縮小向けの高速アルゴリズムである面積平均法をご紹介したいと思います。
あとそろそろアルファチャンネルも対応もしようかな~。
コメント