ぼやきごと/2011-08-02/CopyPixels メソッドを用いた WPF BitmapSource から GDI Bitmap への変換 のバックアップ差分(No.1)


  • 追加された行はこの色です。
  • 削除された行はこの色です。
#blog2navi()
*CopyPixels メソッドを用いた WPF BitmapSource から GDI Bitmap への変換 [#m65c9b37]

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

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

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

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

#code(csharp){{
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ビットパレット形式

の各形式の @code{BitmapSource}; について、

-@code{CopyPixels}; メソッドを用いる方法。
-@code{BmpBitmapEncoder}; クラスを用いる方法。
-@code{PngBitmapEncoder}; クラスを用いる方法。

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

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

#pre{{
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
}}

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

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

RIGHT:Category: &#x5b;[[C#>ぼやきごと/カテゴリ/C#]]&#x5d;&#x5b;[[プログラミング>ぼやきごと/カテゴリ/プログラミング]]&#x5d; - 2011-08-02 09:50:51
----
RIGHT:&blog2trackback();
#comment(above)
#blog2navi()