また前回の投稿から間が空いてしまいました。
さてさて、前回のアンセーフコードとマルチスレッドを用いた劇的な高速化に引き続き、今回は『ループの書き方を変える』というコロンブスの卵的な手法を用いたさらなる高速化について紹介します。
まずはソースコード
ではいきなりですが、前回のものに新たな手法を導入したコードを御覧ください。
struct RGBTripleDouble
{
public double R;
public double G;
public double B;
public RGBTripleDouble(double red, double green, double blue)
{
this.R = red;
this.G = green;
this.B = blue;
}
}
static Bitmap resizeBicubic(Bitmap bmp, int width, int height)
{
Bitmap dst;
const double a = -1.0d;
int sw = bmp.Width, sh = bmp.Height;
double wf = (double)sw / width, hf = (double)sh / height;
Func<double, byte> trimByte = x => (byte)Math.Min(Math.Max(0, Math.Round(x)), 255);
// バイキュービック重み付け関数
Func<double, double> weightFunc = d =>
d <= 1.0 ? ((a + 2.0) * d * d * d) - ((a + 3.0) * d * d) + 1 :
d <= 2.0 ? (a * d * d * d) - (5.0 * a * d * d) + (8.0 * a * d) - (4.0 * a) : .0;
unsafe
{
var xBuf = new RGBTripleDouble[sh * width];
var bmpData = bmp.LockBits(new Rectangle(Point.Empty, bmp.Size), ImageLockMode.ReadOnly, PixelFormat.Format32bppRgb);
var bmpScan0 = (byte*)bmpData.Scan0;
//横方向だけ
Parallel.For(0, sh, iy =>
{
for (int ix = 0; ix < width; ix++)
{
var wfx = wf * ix;
var x = (int)wfx;
var rgb = new RGBTripleDouble(.0, .0, .0);
for (int jx = x - 1; jx <= x + 2; jx++)
{
var w = weightFunc(Math.Abs(wfx - jx));
if (w == 0) continue;
var sx = (jx < 0 || jx >= sw) ? x : jx;
var sPos = 4 * sx + bmpData.Stride * iy;
rgb.B += bmpScan0[sPos + 0] * w;
rgb.G += bmpScan0[sPos + 1] * w;
rgb.R += bmpScan0[sPos + 2] * w;
}
xBuf[iy * width + ix] = rgb;
}
});
bmp.UnlockBits(bmpData);
Bitmap _dst = new Bitmap(width, height, PixelFormat.Format32bppRgb);
var dstData = _dst.LockBits(new Rectangle(Point.Empty, _dst.Size), ImageLockMode.WriteOnly, PixelFormat.Format32bppRgb);
var dstScan0 = (byte*)dstData.Scan0;
//次に縦方向も
Parallel.For(0, height, iy =>
{
for (var ix = 0; ix < width; ix++)
{
var wfy = hf * iy;
var y = (int)wfy;
var rgb = new RGBTripleDouble(.0, .0, .0);
for (int jy = y - 1; jy <= y + 2; jy++)
{
var w = weightFunc(Math.Abs(wfy - jy));
if (w == 0) continue;
var sy = (jy < 0 || jy >= sh) ? y : jy;
rgb.B += xBuf[sy * width + ix].B * w;
rgb.G += xBuf[sy * width + ix].G * w;
rgb.R += xBuf[sy * width + ix].R * w;
}
var dPos = 4 * ix + dstData.Stride * iy;
dstScan0[dPos + 0] = trimByte(rgb.B);
dstScan0[dPos + 1] = trimByte(rgb.G);
dstScan0[dPos + 2] = trimByte(rgb.R);
}
});
_dst.UnlockBits(dstData);
dst = _dst;
}
return dst;
}
このコードを前回と同条件の下で動かしてみると、大体85ms前後をキープできるようになりました。
ElapsedTime: 85.38
これはなかなかの改善だと思いませんか?
どこが変わった?
コードを見て気づくとは思いますが、以前よりも長くなっています。
しかしなぜ速くなったのか?
それは横方向と縦方向の処理を別々のループで処理するようにしたことにあります。
見て分かるようにParallel.For
のループが二つになっています。ここがミソなんですよね。
- 最初のループでは横方向の拡大だけを行い、結果を
xBuf
という配列にRGB値を格納します。(この時点では中間結果なので小数で保存しておきます) - 次のループでは縦方向の拡大を行って、先のループの計算結果に掛けあわせて最終的なピクセル値を計算します。
という至って簡単な事です。それがなぜ速くなったのか…
もう「そらそうよ」って声が聞こえてきそうですが、バイキュービックは3×3ピクセル分という範囲のデータを使って1ピクセルのRGB値を計算します。
その為、ループ中で何度も同じピクセルが参照され、同じ計算結果が幾度も利用されることになります。
ですから今回は横方向の計算結果を配列に保存して、縦方向を計算する時にそこからデータを取り出す形にしたことで、横方向において同じ計算をしなくて良くなったことが高速化の要因となります。
以前も似たようなことをした気がしますが。。。原理が分かればなんてことない実に簡単なことだということが分かると思います。
短くすれば速くなるってものでもない、こういった一工夫で大きくパフォーマンスが変わるのはアルゴリズムの面白いところですよね!
コメント