🌟

BitmapからBitmapSourceへ変換するベストプラクティス

2023/12/03に公開

はじめに

WPFで画像処理関連の開発をしているとBitmapとBitmapSourceの相互変換が頻繁に発生します。

ここではBitmapからBitmapSourceへの変換のベストプラクティスを整理しました。

ベストプラクティス

BitmapからBitmapDataを作って、直接メモリー内容を流し込むのがベストです。

var bitmap = (Bitmap)Image.FromFile("foo.jpg");
var bitmapData = bitmap.LockBits(
    new Rectangle(0, 0, bitmap.Width, bitmap.Height),
    ImageLockMode.ReadOnly, 
    bitmap.PixelFormat);
try
{
    var bitmapSource = BitmapSource.Create(
        bitmapData.Width, 
        bitmapData.Height,
        bitmap.HorizontalResolution, 
        bitmap.VerticalResolution,
        PixelFormats.Bgr24, 
        null,
        bitmapData.Scan0, 
        bitmapData.Stride * bitmapData.Height, 
        bitmapData.Stride);

    return bitmapSource;
}
finally
{
    bitmap.UnlockBits(bitmapData);
}

UnlockBitsを忘れずに。

検証

実現にはいくつかの手段があるのですが、今回はつぎの4つで評価しました。

  1. BitmapDataを利用する(ベストプラクティス)
  2. StreamにJPEGを保存してBitmapSourceをロードする
  3. StreamにBMPを保存してBitmapSourceをロードする
  4. CreateBitmapSourceFromHBitmapを利用する

これらのベンチマーク結果が以下の通りです。

処理速度もメモリーの使用量も圧倒的にBitmapDataです。

2と3は、JPEGを利用するとメモリーの使用量を抑えられますが、JPEGは不可逆圧縮なので厳密な再現性はなく、BMPだと厳密だけどメモリーの使用量が増えるという対比になっています。

これまでCreateBitmapSourceFromHBitmapを利用することが多かったのですが、今後はBitmapData経由を利用しようと思います。CreateBitmapSourceFromHBitmapだと、解像度(DPI)が保存されない点でも注意が必要です。

おまけ

前述の検証だとPixelFormats.Bgr24しか対応していないので、二値のTIFFを読み取ったBitmapからの変換も追加対応してみた。

        using var bitmap = (Bitmap)Image.FromStream(new MemoryStream(Data));
        var bitmapData = bitmap.LockBits(
            new Rectangle(0, 0, bitmap.Width, bitmap.Height),
            ImageLockMode.ReadOnly, bitmap.PixelFormat);
        try
        {
            var bitmapSource = BitmapSource.Create(
                bitmapData.Width,
                bitmapData.Height,
                bitmap.HorizontalResolution,
                bitmap.VerticalResolution,
                bitmap.PixelFormat == PixelFormat.Format1bppIndexed
                    ? PixelFormats.Indexed1
                    : PixelFormats.Bgr24,
                bitmap.PixelFormat == PixelFormat.Format1bppIndexed
                    ? new BitmapPalette(new List<System.Windows.Media.Color> { Colors.Black, Colors.White })
                    : null,
                bitmapData.Scan0, bitmapData.Stride * bitmapData.Height, bitmapData.Stride);

            return bitmapSource;
        }
        finally
        {
            bitmap.UnlockBits(bitmapData);
        }

PixcelFormatすべてに対応するのは大変だなぁ。きれいにマップしきれるかも分からないし。

サンプルコード

SaveJpegStream

using var bitmapStream = new MemoryStream();
_bitmap.Save(bitmapStream, ImageFormat.Jpeg);
bitmapStream.Position = 0;

var bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.StreamSource = bitmapStream;
bitmapImage.EndInit();
bitmapImage.Freeze();

return bitmapImage;

SaveBmpStream

using var bitmapStream = new MemoryStream();
_bitmap.Save(bitmapStream, ImageFormat.Bmp);
bitmapStream.Position = 0;

var bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.StreamSource = bitmapStream;
bitmapImage.EndInit();
bitmapImage.Freeze();

return bitmapImage;

CreateBitmapSourceFromHBitmap

[System.Runtime.InteropServices.DllImport("gdi32.dll")]
public static extern bool DeleteObject(System.IntPtr hObject);

public BitmapSource CreateBitmapSourceFromHBitmap()
{
    var handle = _bitmap.GetHbitmap();
    try
    {
        var bitmapSource = Imaging.CreateBitmapSourceFromHBitmap(handle, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
        bitmapSource.Freeze();
        return bitmapSource;
    }
    finally { DeleteObject(handle); }
}

Discussion