8bppのWriteableBitmapからユーザ領域にデータをコピーして画像処理する

自分で作った画像処理のルーチンを実施するには、本記事のテクニックが必須になります。

WriteableBitmap が持っている画像データというのは、固く閉じられた金庫の中に画像データが格納されているようなもので、ユーザはその中の画像データを自由自在に扱えません。

固く閉じられた金庫だからこそ、ファイルI/Oが簡単に行えたり、ウインドウへのGUI描画が簡単に行えるという利点を有しているのです。

画像データを自由自在に扱いたいけれど、ファイルI/OやウインドウへのGUI描画などのめんどくさいことはあまり考えたくない、これは相反する要求です。

この要求をイッキにかなえるには、下記の方法しかありません。

自分が確保した領域に WriteBitmap から画像データを取り出して、好みの画像処理を施して、画像データを WriteableBitmap に書き戻してやる方法です。

  • WriteableBitmap から画像データを取り出す、WriteableBitmap.CopyPixels() メソッド.
  • 画像データを WriteableBitmap に書き戻す、WriteableBitmap.WritePixels() メソッド.

この二つのメソッドを活用します。本記事で紹介したコードは、その例を示したものです。おおまかには下記の流れです。

(0) 8bppのビットマップファイルを WriteableBitmap に読み込む。
(1) そのサイズに応じて byte[] 型のユーザ領域を new で確保する。
(2) WriteableBitmap に内包された画像データを確保したユーザ領域にコピーする。
(3) ユーザ領域で濃度反転する。
(4) ユーザ領域の画像データをふたたび WriteableBitmap に書き戻す。
(5) WriteableBitmap を 8bppビットマップファイルとして上書き保存する。

元画像
濃度反転した画像

では、本記事のサンプルソースコードについて詳しく解説します。

ビットマップファイルを開くメソッド(1~39行目)

引数の WriteableBitmap は、割り当て済みのものを ref で渡すことに注意してください。

14行目の BitmapSource にデータを代入した瞬間に、ビットマップのピクセルフォーマットが決定されます。ファイルに入っていたデータ内容が8bppでなければ、そこでメッセージボックスを表示して処理をうちきります。

26行目は、コメントにも書いてありますが using 節で囲まれているので Close() を明示する必要はありません。しかし、個人的には何か居心地が悪いのであえて記述してあります。

ビットマップファイルを保存するメソッド(41~80行目)

このサンプルソースでは WriteableBitmap が 8bpp であることは決定済みなので、ピクセルフォーマットについては意識する必要はありません。

気になるようであれば、メソッド内部でピクセルフォーマットを確認して、8bppでなければ保存しないように改造してみてください。

67行目は、上記にある理由と同じで、あえて Close() を記述してあります。

WriteableBitmapとユーザ領域のデータやりとり(82~159行目)

85~92行目はファイル選択ダイアログを表示するお決まりのコードです。

98行目は WriteableBitmap bmp; というクラス宣言だけだと C# では未割り当てという扱いになるので、null を代入することでそれを回避します。

101行目で WriteableBitmap bmp を ref 扱いでメソッドに渡します。メソッド通過後に bmp に画像データが読み込まれています。

114行目でユーザが自由自在に扱える領域 data を new で確保します。Cでいうと malloc なところです。

118行目は bmp から画像データを取り出すところです。WriteableBitmap.CopyPixels() です。

122行目でユーザ領域 data のすべてのピクセルの濃度を反転しています。

131行目で画像データを bmp に書き戻しています。WriteableBitmap.WritePixels() です。

143行目で濃度反転したデータが格納された WriteableBitmap を使ってファイルを上書き保存します。

下記のコードをビルドするには、これらの名前空間の追加をお忘れなく.
using System.Windows.Media.Imaging;
using System.IO;
using Microsoft.Win32;

// ビットマップファイルを開くメソッド.
private bool file_open_bmp_bpp08( String filepath, ref WriteableBitmap writeable_bitmap )
{

	try
	{
		// ファイルから byte[] に単なるデータとして読み込む.
		byte [] tmp = File.ReadAllBytes( filepath );

		using ( MemoryStream ms = new MemoryStream( tmp ))
		{

			// 単なるデータ列を画像ビットマップ形式に再解釈する.
			BitmapSource bms = BitmapFrame.Create( ms );

			if ( bms.Format != PixelFormats.Indexed8 )
			{
				MessageBox.Show( "This file is not Indexed8." );
				return false; // warning.
			}

			// 書き込み可能ビットマップだとなにかと使いやすい.
			writeable_bitmap = new WriteableBitmap( bms );

			// using 節の内部なのでクローズ明示不要だが一応クローズする.
			ms.Close();

		}

	}
	catch ( Exception excp )
	{
		MessageBox.Show( excp.Message );
		return false;
	}

	return true;

}

// ビットマップファイルを保存するメソッド.
private bool file_save_bmp_bpp08( String filepath, WriteableBitmap writeable_bitmap )
{

	try
	{

		// ファイルストリームの引数を決める.
		FileMode fmd = FileMode.Create;
		FileAccess fas = FileAccess.Write;
		using ( FileStream fs = new FileStream( filepath, fmd, fas ) )
		{

			// ここでBMP保存形式に決定する.
			BmpBitmapEncoder enc = new BmpBitmapEncoder();

			// WriteableBitmap からエンコーダに渡すためのイメージデータを作成する.
			BitmapFrame bmf = BitmapFrame.Create( writeable_bitmap );

			// ファイル形式ごとのエンコーダに渡す.
			enc.Frames.Add( bmf );

			// ファイルストリームでファイル保存する.
			enc.Save( fs );

			// using 節の内部なのでクローズ明示不要だが一応クローズする.
			fs.Close();

		}

	}
	catch ( Exception excp )
	{
		MessageBox.Show( excp.Message );
		return false;
	}

	return true;

}

private void menuDebugExec_Click( object sender, RoutedEventArgs e )
{

	// ファイルを開くダイアログを表示する.
	OpenFileDialog ofd = new OpenFileDialog();
	ofd.Filter = "BMP|*.bmp|ALL|*.*";
	bool? ret = ofd.ShowDialog();
	if ( ret == false )
	{
		return; // warning.
	}

	// ファイルパスを取得する.
	String filepath = ofd.FileName;

	// ファイルを開くメソッドに ref で渡すのでビットマップの未割り当てを回避する.
	WriteableBitmap bmp = null;

	// 8bppBMPファイルのデータを開いてビットマップに格納する.
	bool ret_file_io = file_open_bmp_bpp08( filepath, ref bmp );
	if ( !ret_file_io )
	{
		MessageBox.Show( "Error: file_open_bmp_bpp08();" );
		return; // warning.
	}

	int w = bmp.PixelWidth;
	int h = bmp.PixelHeight;

	// ここからユーザ領域を確保する.
	int stride_bytes = bmp.BackBufferStride;
	int alloc_bytes = stride_bytes * h;
	byte [] data = new byte[ alloc_bytes ];

	// ビットマップに格納されているデータをユーザ領域にコピーする.
	int offset_bytes = 0;
	bmp.CopyPixels( data, stride_bytes, offset_bytes );

	// -------------------------------------------------
	// ユーザー領域の画像データを反転させる,begin.
	for ( int n = 0; n < data.Length; n++ )
	{
		data[n] = (byte)( 255 - data[n] );
	}
	// ユーザー領域の画像データを反転させる,end.
	// -------------------------------------------------

	// ユーザー領域のデータをビットマップに書き戻す.
	Int32Rect rect = new Int32Rect( 0, 0, w, h );
	bmp.WritePixels( rect, data, stride_bytes, offset_bytes ); 

	// メッセージボックスで上書き確認して[はい(Y)]ならば実行する.
	String str = "反転したデータをファイル上書きしてもいいですか?";
	String cap = "上書き確認";
	MessageBoxButton mbb = MessageBoxButton.YesNo;
	MessageBoxImage mbi = MessageBoxImage.Question;
	MessageBoxResult mbr = MessageBox.Show( str, cap, mbb, mbi );
	if ( mbr == MessageBoxResult.Yes )
	{

		// ビットマップに格納されたデータを8bppBMPファイルに書き込む.
		ret_file_io = file_save_bmp_bpp08( filepath, bmp );
		if ( ret_file_io )
		{
			MessageBox.Show( "Success: finish." );
		}
		else
		{
			MessageBox.Show( "Error: file_save_bmp_bpp08();" );
		}

	}
	else
	{
		MessageBox.Show( "Overwrite canceled." );
	}

}

本記事に掲載されたサンプルソースのコピーペーストでうまくいかなかった場合は、下記からプロジェクトをアーカイブしたものをダウンロードできますので、ビルドしてから動作確認してみてください。

8bppのビットマップ(*.bmp)画像ファイルというのは、意外と入手しづらいので、まとめてZIPアーカイブしてあります。