Home / ぼやきごと / 2011-08-02
2011-08-02

CopyPixels メソッドを用いた WPF BitmapSource から GDI Bitmap への変換

グダグダと書く前にまず答えから。
次の Convert メソッドで System.Windows.Media.Imaging.BitmapSource クラスのオブジェクトを System.Drawing.Bitmap クラスのオブジェクトに変換できます。

すべて開くすべて閉じる
  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
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 
 
 
 
 
 
 
 
 
-
|
-
-
|
|
|
|
!
-
-
!
|
-
|
|
|
|
|
|
!
|
-
!
|
|
|
|
|
-
!
|
|
|
|
|
-
|
|
|
|
|
!
|
-
|
|
|
!
|
-
|
-
|
!
!
|
|
!
!
!
using System;
using System.Runtime.InteropServices;
using System.Drawing;
using System.Windows.Media;
using System.Windows.Media.Imaging;
 
using Imaging = System.Drawing.Imaging;
 
namespace BitmapSourceSample
{
    class Sample
    {
        /// <summary>
        /// BitmapSource をARGB形式の Bitmap に変換する。
        /// </summary>
        /// <param name="src">BitmapSource 。</param>
        /// <returns>Bitmap 。</returns>
        public static Bitmap Convert(BitmapSource src)
        {
            // フォーマットが異なるならば変換
            BitmapSource s = src;
            if (s.Format != PixelFormats.Bgra32)
            {
                s = new FormatConvertedBitmap(
                    s,
                    PixelFormats.Bgra32,
                    null,
                    0);
                s.Freeze();
            }
 
            // ピクセルデータをコピー
            int width = (int)s.Width;
            int height = (int)s.Height;
            int stride = width * 4;
            byte[] datas = new byte[stride * height];
            s.CopyPixels(datas, stride, 0);
 
            // Bitmap へピクセルデータ書き出し
            Bitmap dest = new Bitmap(
                width,
                height,
                Imaging::PixelFormat.Format32bppArgb);
            Imaging::BitmapData destBits = null;
            try
            {
                destBits = dest.LockBits(
                    new Rectangle(0, 0, width, height),
                    Imaging::ImageLockMode.WriteOnly,
                    Imaging::PixelFormat.Format32bppArgb);
                Marshal.Copy(datas, 0, destBits.Scan0, datas.Length);
            }
            catch
            {
                dest.Dispose();
                dest = null;
                throw;
            }
            finally
            {
                if (dest != null && destBits != null)
                {
                    dest.UnlockBits(destBits);
                }
            }
 
            return dest;
        }
    }
}

この例では、必ずARGB形式となるように必要に応じてソースのフォーマットを変換しています。
任意のフォーマットで変換したい場合はソースのフォーマットに応じた処理を行う必要があります。

GoogleでWPFの BitmapSource からGDIの Bitmap への変換について検索すると、 BmpBitmapEncoder を用いる方法が多く紹介されています(2011年8月現在)。
この方法は、一旦エンコーダによってBMP形式へ落とし込むことにより、ソースのフォーマットを気にすることなく変換できるのが利点といえます。

しかしこの方法は言ってしまえば一度BMPファイルにしてから読み込み直すのと変わらないため、速度面でのコストが気になります。
また、BMP形式では不透明度を扱えない為、不透明度を保持したい場合は PngBitmapEncoder 等を用いることになり、更に処理時間が掛かることになります。

「フォーマットは固定でも構わないのでより高速に変換したい」という要求がある場合、今回紹介した CopyPixels メソッドを用いる方法に軍配が上がります。
どれほど速度に差が出るのか、次のようなベンチマークプログラムで実験してみました。
なお、上述の Sample クラスが定義済みであるものとします。

すべて開くすべて閉じる
  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
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
 
 
 
 
 
 
 
 
 
 
-
|
-
-
|
|
|
|
|
|
|
!
|
-
-
!
|
|
-
!
|
-
|
|
|
-
-
|
!
!
!
|
|
!
|
-
|
|
|
|
!
-
-
!
|
|
|
|
|
|
|
-
!
|
-
|
!
|
-
!
|
|
|
|
-
!
|
|
|
!
|
-
|
|
|
|
|
|
|
!
|
|
|
-
-
!
|
|
-
|
!
|
|
-
!
|
|
|
|
-
!
-
|
!
!
|
-
|
|
|
!
-
-
!
|
-
!
|
|
|
|
|
|
|
|
|
|
|
!
|
-
|
|
!
-
|
|
|
!
!
!
using System;
using System.IO;
using System.Diagnostics;
using System.Drawing;
using System.Globalization;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
 
namespace BitmapSourceSample
{
    class Program
    {
        /// <summary>
        /// BitmapSource を Bitmap に変換する。
        /// </summary>
        /// <typeparam name="TEncoder">
        /// 中間形式の生成に用いる BitmapEncoder 型。
        /// </typeparam>
        /// <param name="src">BitmapSource 。</param>
        /// <returns>Bitmap 。</returns>
        static Bitmap ConvertBy<TEncoder>(BitmapSource src)
            where TEncoder : BitmapEncoder, new()
        {
            // エンコーダを作成してフレーム追加
            var encoder = new TEncoder();
            encoder.Frames.Add(BitmapFrame.Create(src));
 
            // ストリームを介して Bitmap に変換
            Bitmap dest = null;
            using (var s = new MemoryStream())
            {
                encoder.Save(s);
                s.Seek(0, SeekOrigin.Begin);
                using (var temp = new Bitmap(s))
                {
                    // ストリームを閉じた後の Save メソッド呼び出し等で
                    // GDI+例外が発生しないように、別の Bitmap へコピー
                    dest = new Bitmap(temp);
                }
            }
 
            return dest;
        }
 
        /// <summary>
        /// 変換ソースとなる BitmapSource を作成する。
        /// </summary>
        /// <param name="format">フォーマット。</param>
        /// <returns>BitmapSource 。</returns>
        static BitmapSource MakeSource(PixelFormat format)
        {
            // 文字列作成
            var ft = new FormattedText(
                "あいうえお\nABCDEFG 12345",
                CultureInfo.CurrentCulture,
                FlowDirection.LeftToRight,
                new Typeface("MS ゴシック"),
                48,
                new LinearGradientBrush(Colors.Blue, Colors.Red, 0));
 
            // DrawingVisual へ文字列描画
            DrawingVisual dv = new DrawingVisual();
            using (var dc = dv.RenderOpen())
            {
                dc.DrawText(ft, new System.Windows.Point());
            }
 
            // RenderTargetBitmap へ描画
            var bmp =
                new RenderTargetBitmap(640, 480, 96, 96, PixelFormats.Pbgra32);
            bmp.Render(dv);
            bmp.Freeze();
 
            // 目的のフォーマットへ変換
            var dest = new FormatConvertedBitmap(bmp, format, null, 0);
            dest.Freeze();
 
            return dest;
        }
 
        /// <summary>
        /// ベンチマーク処理を行う。
        /// </summary>
        /// <param name="name">
        /// ベンチマーク項目名。保存する画像ファイルの名前にも使われる。
        /// </param>
        /// <param name="loopCount">ループ回数。</param>
        /// <param name="func">ベンチマーク処理デリゲート。</param>
        static void DoBenchmark(
            string name,
            int loopCount,
            Func<Bitmap> func)
        {
            // 処理実施
            var sw = new Stopwatch();
            sw.Start();
            for (int i = 0; i < loopCount; ++i)
            {
                using (var temp = func()) { }
            }
            sw.Stop();
 
            // 結果出力
            Console.WriteLine(
                "{0,-25} : {1,12:F6}",
                name,
                sw.ElapsedTicks * 1000 / (decimal)Stopwatch.Frequency);
 
            // 一応、PNGで保存してみる
            using (var bmp = func())
            {
                bmp.Save(name + ".png");
            }
        }
 
        /// <summary>
        /// 全ベンチマーク処理を行う。
        /// </summary>
        /// <param name="format">ソース画像のフォーマット。</param>
        static void DoBenchmarkAll(PixelFormat format)
        {
            // ソース画像作成
            var src = MakeSource(format);
 
            // ベンチマーク処理
            DoBenchmark(
                format.ToString() + "-CopyPixels",
                100,
                () => Sample.Convert(src));
            DoBenchmark(
                format.ToString() + "-BmpBitmapEncoder",
                100,
                () => ConvertBy<BmpBitmapEncoder>(src));
            DoBenchmark(
                format.ToString() + "-PngBitmapEncoder",
                100,
                () => ConvertBy<PngBitmapEncoder>(src));
        }
 
        /// <summary>
        /// メインエントリポイント。
        /// </summary>
        static void Main()
        {
            DoBenchmarkAll(PixelFormats.Bgra32);
            DoBenchmarkAll(PixelFormats.Bgr24);
            DoBenchmarkAll(PixelFormats.Indexed8);
        }
    }
}

このプログラムは、

  • 32ビットBGRA形式
  • 24ビットBGR形式
  • 8ビットパレット形式

の各形式の BitmapSource について、

  • CopyPixels メソッドを用いる方法。
  • BmpBitmapEncoder クラスを用いる方法。
  • PngBitmapEncoder クラスを用いる方法。

の各方法による Bitmap への変換処理を各100回実行し、その処理時間を出力するものです。
また、ついでにその際生成される Bitmap をPNG画像ファイルとして保存しています。

このプログラムを私の環境(Windows7 64bit)で実行した結果は次の通りです。
数値の単位はミリ秒です。

Bgra32-CopyPixels         :   126.398125
Bgra32-BmpBitmapEncoder   :   989.100697
Bgra32-PngBitmapEncoder   :  1797.793503
Bgr24-CopyPixels          :   227.051113
Bgr24-BmpBitmapEncoder    :   986.993156
Bgr24-PngBitmapEncoder    :  1504.336953
Indexed8-CopyPixels       :   689.379903
Indexed8-BmpBitmapEncoder :  1367.630629
Indexed8-PngBitmapEncoder :  1188.354228

ご覧の通り、元となる BitmapSource のフォーマットによって比率に差はあるものの、すべてにおいて CopyPixels メソッドを用いる方法が最も高速になりました。
特にフォーマット変換不要な32ビットBGRA形式においては、 BmpBitmapEncoder を用いる場合の約8倍、 PngBitmapEncoder を用いる場合の約15倍高速になりました。

今まで BmpBitmapEncoder クラスを用いていた方は、検討の余地があるのではないでしょうか。

Category: [C#][WPF・XAML][プログラミング] - 2011-08-02 09:50:51