C#における「ビットマップ形式の画像データを相互変換」まとめ

# 概要

　「ビットマップ形式」……いわゆるラスターグラフィックスは、最も典型的な画像の表現方法でしょう。
　それをプログラム上で表現するため、C#では `System.Drawing.Bitmap` など様々な型が用意されています。

　……そう、 **様々な型** です。暗黙の型変換でよしなにしてくれない場合、メソッドを用いた **変換が必要** なのです！
　というわけで、変換方法についてまとめてみました。元ネタは次のQiita記事です。

　　[WPFの画像相互コンバーター。System.Drawing.BitmapからSystem.Windows.Controls.Imageへの変換。](https://qiita.com/KaoruHeart/items/dc130d5fc00629c1b6ea)
　　[WPFの画像相互コンバーター。BitmapImageからBitmapSourceへの変換。](https://qiita.com/KaoruHeart/items/60de874a05a8c749ff24)

※以下の記事では、メソッドの戻り値と同じ型を使用する場合のみ `var` キーワードを使用しています。
　なぜならそうしないと、型の変化が分かりにくいからです。

# 継承関係について整理

　まず、様々な型について、その継承関係を振り返ってみましょう。
　ただし以下の表では、継承元の名前を「 `S.D.Image` 」などと略しています。

|型名|継承元|説明|
|----|----|----|
|System.Drawing.Image||WinForms用の画像データで、System.Drawingの参照が必要|
|System.Drawing.Imaging.Metafile|S.D.Image|*.emfなどのベクターグラフィックス|
|System.Drawing.Bitmap|S.D.Image|*.bmpなどのラスターグラフィックス|
|System.Windows.Media.ImageSource||WPF用の画像データで、PresentationCoreの参照が必要|
|System.Windows.Media.DrawingImage|ImageSource|ベクターグラフィックス|
|System.Windows.Media.Imaging.BitmapSource|ImageSource|ラスターグラフィックス|
|System.Windows.Media.Imaging.BitmapImage|BitmapSource|ファイルやリソースからの画像の読み込みを想定|
|System.Windows.Media.Imaging.WriteableBitmap|BitmapSource|プログラム中で動的に画素の読み書きができる|
|System.Windows.Media.Imaging.RenderTargetBitmap|BitmapSource|任意のVisualオブジェクトをレンダリングしてビットマップ化するために利用|
|System.Windows.Interop.InteropBitmap|BitmapSource|(※1)|
|System.Windows.Media.Imaging.BitmapFrame|BitmapSource|エンコーダーやデコーダーで扱われる画像形式|
|System.Windows.Controls.Image||WPFにおける<Image>コントロールで、PresentationFrameworkの参照が必要(※2)|

※1……例えばクリップボードからビットマップ形式の画像データを取り出した際、
　WinFormsでは `System.Drawing.Bitmap` で返るが、
　WPFでは `System.Windows.Interop.InteropBitmap` で返る。
　これにどういう意味があるのかよく分かりませんので誰か教えてください＞＜
※2……SourceプロパティにSystem.Windows.Media.ImageSourceを指定する

　このことから、例えば `BitmapImage` を `System.Windows.Controls.Image` にするには

```csharp
// BitmapImage型の画像
System.Windows.Media.Imaging.BitmapImage bitmapImage;
// 
var image = new System.Windows.Controls.Image();
image.Source = bitmapImage;
```

で十分ですし、 `BitmapImage` を `BitmapSource` にするには

```csharp
// BitmapImage型の画像
System.Windows.Media.Imaging.BitmapImage bitmapImage;
// BitmapImage→BitmapSource(型変換を明示する必要はない)
System.Windows.Media.Imaging.BitmapSource bitmapSource = bitmapImage;
// BitmapSource→BitmapImage(型変換を明示する必要がある)
System.Windows.Media.Imaging.BitmapImage bitmapImage2 = (System.Windows.Media.Imaging.BitmapImage)bitmapSource;
```

でOKです。なお、継承元が同じな `BitmapImage` や `InteropBitmap` は直接キャストすることができず、必ず `BitmapSource` など上位の型に変換してからキャストすることが必要となります。

　……上記考察から、 `System.Drawing.Bitmap` **と** `System.Windows.Media.Imaging.BitmapSource` **との間の変換が重要** だと分かります。

# BitmapとBitmapSourceとの相互変換

## Bitmap→BitmapSource

　一番無難なのは、 `System.IO.MemoryStream` を経由することでしょう。

```csharp
// 変換元の画像
System.Drawing.Bitmap bitmap;
// MemoryStreamを利用した変換処理
using (var ms = new System.IO.MemoryStream()) {
	// MemoryStreamに書き出す
	bitmap.Save(ms, System.Drawing.Imaging.ImageFormat.Bmp);
	// MemoryStreamをシーク
	ms.Seek(0, System.IO.SeekOrigin.Begin);
	// MemoryStreamからBitmapFrameを作成
	// (BitmapFrameはBitmapSourceを継承しているのでそのまま渡せばOK)
	System.Windows.Media.Imaging.BitmapSource bitmapSource =
		System.Windows.Media.Imaging.BitmapFrame.Create(
			ms,
			System.Windows.Media.Imaging.BitmapCreateOptions.None,
			System.Windows.Media.Imaging.BitmapCacheOption.OnLoad
		);
}
```

　また、 `BitmapImage` は `System.IO.Stream` をプロパティから受け取れますので、そちらから攻める手もあるでしょう。

```csharp
// 変換元の画像
System.Drawing.Bitmap bitmap;
// MemoryStreamを利用した変換処理
using (var ms = new System.IO.MemoryStream()) {
	bitmap.Save(ms, System.Drawing.Imaging.ImageFormat.Bmp);
	// MemoryStreamのポジションを設定？
	ms.Position = 0;
	// BitmapImageを初期化
	var bitmapImage = new System.Windows.Media.Imaging.BitmapImage();
	// MemoryStreamを書き込むために準備する
	bitmapImage.BeginInit();
	bitmapImage.CacheOption = System.Windows.Media.Imaging.BitmapCacheOption.OnLoad;
	bitmapImage.CreateOptions = System.Windows.Media.Imaging.BitmapCreateOptions.None;
	// MemoryStreamを書き込む
	bitmapImage.StreamSource = ms;
	//
	bitmapImage.EndInit();
	// ここでFreezeしておくといいらしい(参考資料参照)
	bitmapImage.Freeze();
	// BitmapImageはBitmapSourceを継承しているのでそのまま渡せばOK
	System.Windows.Media.Imaging.BitmapSource bitmapSource = bitmapImage;
}
```

　なお、Unmanagedなメソッドを使っていい場合は、次のようなコードになります。
　別の方の記事になりますが、 **MemoryStreamよりずっと高速** だそうです。

　　[.NETで画像処理を試してみる OpenCVSharp編 第5回 - A certain engineer "COMPLEX"](https://taktak.jp/2017/01/18/1753)

```csharp
[System.Runtime.InteropServices.DllImport("gdi32.dll")]
public static extern bool DeleteObject(System.IntPtr hObject);
(中略)
// 変換元の画像
System.Drawing.Bitmap bitmap;
// HBitmapに変換
var hBitmap = bitmap.GetHbitmap();
// HBitmapからBitmapSourceを作成
try {
	var bitmapSource = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(
		hBitmap,
		System.IntPtr.Zero,
		System.Windows.Int32Rect.Empty,
		System.Windows.Media.Imaging.BitmapSizeOptions.FromEmptyOptions()
	);
}
finally {
	DeleteObject(hBitmap);
}
```

## BitmapSource→Bitmap

　色々手段がありますが、Bitmap→BitmapSourceよりこっちの方が面倒くさいように見えます。

```csharp
// LockBitsとUnlockBitsを使う方法
// フォーマット決め打ちなので、他フォーマットだと別途変換が必要。
// 詳しくは参考資料における「ルーチェ's Homepage」または「nuits.jp blog」のコードを参照すること

// 元画像
System.Windows.Media.Imaging.BitmapSource bitmapSource;
// 処理
var bitmap = new System.Drawing.Bitmap(
	bitmapSource.PixelWidth,
	bitmapSource.PixelHeight,
	System.Drawing.Imaging.PixelFormat.Format32bppPArgb
);
var bitmapData = bitmap.LockBits(
	new System.Drawing.Rectangle(System.Drawing.Point.Empty, bitmap.Size),
	System.Drawing.Imaging.ImageLockMode.WriteOnly,
	System.Drawing.Imaging.PixelFormat.Format32bppPArgb
);
bitmapSource.CopyPixels(
	System.Windows.Int32Rect.Empty,
	bitmapData.Scan0,
	bitmapData.Height * bitmapData.Stride,
	bitmapData.Stride
);
bitmap.UnlockBits(bitmapData);
```

```csharp
// Encoderを使う方法
// かなり楽だがその分遅い。
// SampleEncoderがSystem.Windows.Media.Imaging.BmpBitmapEncoderなら不透明部分を扱えず、
// System.Windows.Media.Imaging.PngBitmapEncoderなら高性能な分余計に遅くなる

// 元画像
System.Windows.Media.Imaging.BitmapSource bitmapSource;
// 処理
var encoder = new SampleEncoder();
encoder.Frames.Add(System.Windows.Media.Imaging.BitmapFrame.Create(bitmapSource));
System.Drawing.Bitmap bitmap = null;
using (var ms = new System.IO.MemoryStream()){
	encoder.Save(ms);
	ms.Seek(0, System.IO.SeekOrigin.Begin);
	using (var temp = new System.Drawing.Bitmap(ms)){
		// このおまじないの意味は参考資料を参照
		bitmap = new System.Drawing.Bitmap(temp);
	}
}
```

# 参考資料
- [WPFの画像相互コンバーター。System.Drawing.BitmapからSystem.Windows.Controls.Imageへの変換。](https://qiita.com/KaoruHeart/items/dc130d5fc00629c1b6ea)
- [WPFの画像相互コンバーター。BitmapImageからBitmapSourceへの変換。](https://qiita.com/KaoruHeart/items/60de874a05a8c749ff24)
- [System.Drawing.BitmapをWPF用に変換](http://axion.sakura.ne.jp/blog/index.php?UID=1333166737)
- [Convert to BitmapImage](https://kaliko.com/blog/convert-to-bitmapimage/)
- [俺が遭遇したWPFイメージコントロールのメモリーリークと回避法(？)の１つ - C#でプログラミングあれこれ](http://d.hatena.ne.jp/fkmt5/20130729/1375090831)
- [.NETで画像処理を試してみる OpenCVSharp編 第5回 - A certain engineer "COMPLEX"](https://taktak.jp/2017/01/18/1753)
- [Bitmap <-> BitmapSource converter](https://gist.github.com/nashby/916300)
- [ぼやきごと/2011-08-02/CopyPixels メソッドを用いた WPF BitmapSource から GDI Bitmap への変換 - ルーチェ's Homepage](https://www.ruche-home.net/boyaki/2011-08-02/CopyPixe)
- [System.Drawing.BitmapとImageSourceを相互変換する - nuits.jp blog](http://www.nuits.jp/entry/2016/10/17/181232)