ポケモンスリープに課金したあんみんどうふです。
大量のBitmapを結合するためにGraphic.DrawImageでひたすら描画していたのですが、.NET備え付けの描画関連の処理は遅いものが多い印象が強いです(Bitmap.GetPixel、Bitmap.SetPixel、Bitmap.Clone等)。
もしかしてGraphicsも高速化できたり?と思い、LockBitsを使ったメモリ展開処理でGraphic.DrawImageを再現し、速度を比較してみようと思います。
とりあえずそれっぽく作る
フォームアプリケーションに600×600のPictureBoxを2つ置き、pictureBox1にはGraphics.DrawImage、pictureBox2にはLockBitsで描画した画像をセットしています。
サンプル画像として、いらすとや様から拝借した「ホログラムシールのイラスト」を使用しています。
正方形っぽくていい感じの画像が欲しかったもので。大きさは400×400とちょうどいい。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | using System; using System.Diagnostics; using System.Drawing; using System.Drawing.Imaging; using System.Runtime.InteropServices; using System.Windows.Forms; private void Form1_Load(object sender, EventArgs e) { Stopwatch sw = new Stopwatch(); Bitmap bmp = Properties.Resources.image; // Graphics.DrawImageで描画 sw.Start(); Bitmap drawImage = new Bitmap(pictureBox1.Width, pictureBox1.Height); using (Graphics g = Graphics.FromImage(drawImage)) g.DrawImage(bmp, 0, 0); sw.Stop(); Debug.WriteLine($"DrawImage: {sw.ElapsedMilliseconds}ms"); pictureBox1.Image = Properties.Resources.image; // Bitmapのロック sw.Restart(); BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); // ピクセルデータをbyte配列で取得 byte[] bmpPixels = new byte[bmpData.Stride * bmpData.Height]; Marshal.Copy(bmpData.Scan0, bmpPixels, 0, bmpPixels.Length); bmp.UnlockBits(bmpData); // 表示用のBitmapにセット Bitmap res = new Bitmap(pictureBox1.Width, pictureBox1.Height); BitmapData resData = res.LockBits(new Rectangle(0, 0, res.Width, res.Height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); byte[] resPixels = new byte[resData.Stride * resData.Height]; Marshal.Copy(resData.Scan0, resPixels, 0, resPixels.Length); for (int y = 0; y < bmpData.Height; y++) { for (int x = 0; x < bmpData.Width; x++) { resPixels[y * resData.Stride + x * 4 + 0] = bmpPixels[y * bmpData.Stride + x * 4 + 0]; // B resPixels[y * resData.Stride + x * 4 + 1] = bmpPixels[y * bmpData.Stride + x * 4 + 1]; // G resPixels[y * resData.Stride + x * 4 + 2] = bmpPixels[y * bmpData.Stride + x * 4 + 2]; // R resPixels[y * resData.Stride + x * 4 + 3] = bmpPixels[y * bmpData.Stride + x * 4 + 3]; // A } } Marshal.Copy(resPixels, 0, resData.Scan0, resPixels.Length); res.UnlockBits(resData); sw.Stop(); Debug.WriteLine($"LockBits: {sw.ElapsedMilliseconds}ms"); pictureBox2.Image = res; } |
色も位置も200pxの余白も遜色なく、無事完コピです。
速度は以下の通り。
Graphic.DrawImage | 1ms |
LockBits | 4ms |
なんと意外にもDrawImageの方が早かった…。
関数化して10000回実行
回数を重ねれば変わってくるのでは?と思い検証してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | /// <summary> /// LockBitsでBitmapへ描画する /// </summary> /// <param name="canvas">描画元</param> /// <param name="image">描画する画像</param> /// <param name="pos">座標</param> /// <returns></returns> private Bitmap DrawImageWithLockBits(Bitmap canvas, Bitmap image, Point pos) { // 描画する画像をロック BitmapData imageData = image.LockBits(new Rectangle(0, 0, image.Width, image.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); // ピクセルデータをbyte配列で取得 byte[] imagePixels = new byte[imageData.Stride * imageData.Height]; Marshal.Copy(imageData.Scan0, imagePixels, 0, imagePixels.Length); image.UnlockBits(imageData); // 描画対象へ描画 BitmapData canvasData = canvas.LockBits(new Rectangle(0, 0, canvas.Width, canvas.Height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); byte[] canvasPixels = new byte[canvasData.Stride * canvasData.Height]; Marshal.Copy(canvasData.Scan0, canvasPixels, 0, canvasPixels.Length); int width = canvas.Width; int height = canvas.Height; for (int y = 0; y < imageData.Height; y++) { for (int x = 0; x < imageData.Width; x++) { int pixel = (y + pos.Y) * canvasData.Stride + (x + pos.X) * 4; // 描画元の範囲内、配列の範囲内のみ値の代入を行う if (x + pos.X < width && y + pos.Y < height && pixel < canvasPixels.Length) { canvasPixels[pixel + 0] = imagePixels[y * imageData.Stride + x * 4 + 0]; // B canvasPixels[pixel + 1] = imagePixels[y * imageData.Stride + x * 4 + 1]; // G canvasPixels[pixel + 2] = imagePixels[y * imageData.Stride + x * 4 + 2]; // R canvasPixels[pixel + 3] = imagePixels[y * imageData.Stride + x * 4 + 3]; // A } } } Marshal.Copy(canvasPixels, 0, canvasData.Scan0, canvasPixels.Length); canvas.UnlockBits(canvasData); return canvas; } private Bitmap DrawImageWithLockBits(Bitmap canvas, Bitmap image, int x = 0, int y = 0) { return DrawImageWithLockBits(canvas, image, new Point(x, y)); } |
これをフォームのLoadイベントでDrawImageと共に10000回実行させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | Stopwatch sw = new Stopwatch(); Bitmap bmp = Properties.Resources.image; // DrawImageを使用して描画 Bitmap drawImage = new Bitmap(pictureBox1.Width, pictureBox1.Height); sw.Start(); using (Graphics g = Graphics.FromImage(drawImage)) for (int i = 0; i < 10000; i++) g.DrawImage(bmp, 0, 0); pictureBox1.Image = drawImage; sw.Stop(); Debug.WriteLine($"DrawImage: {sw.ElapsedMilliseconds}ms"); // LockBitsを使用して描画 Bitmap lockBits = new Bitmap(pictureBox2.Width, pictureBox2.Height); sw.Restart(); for (int i = 0; i < 10000; i++) lockBits = DrawImageWithLockBits(lockBits, bmp, 0, 0); pictureBox2.Image = lockBits; sw.Stop(); Debug.WriteLine($"LockBits: {sw.ElapsedMilliseconds}ms"); |
結果は以下の通り。
Graphics.DrawImage | 5056ms |
LockBits | 32815ms |
勝てません!対戦ありがとうございました!以上!