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アーカイブしてあります。