8bitsデータをビットマップに書き込む

白黒グレースケールの画像データは、おもに 0..255 の 8bits 深さデータとして扱われます。C# では 8bits データは byte[] で定義されます。

byte[] データを .NET Framework において画像を扱うクラスの Bitmap にかきこめれば、画像データをウインドウに表示したり、ファイルとして保存したり、なんでもやれるといっても過言ではありません。

下記のコードは、あらかじめ準備しておいた byte[] データを、Bitmap の中に書き込んで、Bitmap のメソッドを使って、BMP形式で保存します。

下記コードのコンパイルするためには、VisualStudio のビルドオプションで「アンセーフコードの許可」を実施してください。ビルドオプションの設定方法は下記の記事をごらんください。

アンセーフunsafeコードを許可する

データのコピーやポインタ操作などアンセーフ unsafe なコードのビルドを実施する方法を紹介します。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

// この using 追加を忘れずに. Don't forget to add this sentence.
using System.Drawing.Imaging;
using System.Diagnostics;

namespace aaa
{

	public partial class Form1 : Form
	{

		const int INI_W = 512;
		const int INI_H = 256;

		byte [] Data000;
		Bitmap Bmp000;

		public Form1()
		{
			InitializeComponent();
		}

		private void menuDebugExec000_Click( object sender, EventArgs e )
		{

			// ビットマップに書き込みたい8ビットデータの領域を確保する.
			int w = INI_W;
			int h = INI_H;
			int numpix = w * h;
			Data000 = new byte[ numpix ];

			// 上が黒くて、下が白いグラデーションの画像データを格納する.
			for ( int j = 0; j < h; j++ )
			{

				int adrs = w * j;
				byte level = (byte)(j);

				for ( int i = 0; i < w; i++ )
				{
					Data000[ adrs ] = level;
					adrs++;
				}

			}

			// 8ビットインデックスパレットのビットマップを生成する.
			PixelFormat pixfmt = PixelFormat.Format8bppIndexed;
			Bmp000 = new Bitmap( w, h, pixfmt );

			// いったんデフォルトのパレットを盗む.
			ColorPalette pal = Bmp000.Palette;

			// 盗んだパレットの中身をいじる.
			for ( int k = 0; k < 256; k++ )
			{
				pal.Entries[k] = Color.FromArgb( k, k, k );
			}

			// いじったパレットを戻す.
			Bmp000.Palette = pal;

			// グラデーションデータをビットマップにコピーする.
			unsafe
			{

				BitmapData bmdt = new BitmapData();

				// ビットマップをロックする.
				Rectangle rct = new Rectangle( 0, 0, w, h );
				ImageLockMode mode = ImageLockMode.ReadWrite;
				bmdt = Bmp000.LockBits( rct, mode, pixfmt );

				// ワンスキャンぶんのバイト数を計算する.
				// ビットマップの幅ピクセル数が4の倍数でなかったときは、
				// one_scan_bytes_dst と one_scan_bytes_src は、
				// 違う値になる可能性が高いので別々に扱う.
				int one_scan_bytes_src = w * sizeof( byte);
				int one_scan_bytes_dst = bmdt.Stride;

				// ここで byte[] データを Bitmap に書き込む.
				fixed( byte* src = Data000 )
				{

					byte* pt_scan_src = src;
					byte* pt_scan_dst = (byte*)( bmdt.Scan0 );

					for ( int j = 0; j < h; j++ )
					{

						byte* pt_src = pt_scan_src;
						byte* pt_dst = pt_scan_dst;

						for ( int i = 0; i < w; i++ )
						{
							*pt_dst = *pt_src;
							pt_src++;
							pt_dst++;
						}

						pt_scan_src += one_scan_bytes_src;
						pt_scan_dst += one_scan_bytes_dst;

					}

				}

				// ビットマップをアンロックする.
				Bmp000.UnlockBits( bmdt );

			}

			// ビットマップファイルを保存する.
			String fp_bmp = "c:/tmp/bpp08.bmp";
			ImageFormat imgfmt = ImageFormat.Bmp;
			Bmp000.Save( fp_bmp, imgfmt );

			// ビットマップは生成したら Dispose しなければならない.
			Bmp000.Dispose();
			Bmp000 = null;

			// 「フォト」や「ペイントブラシ」などで保存した画像ファイルを開く
			Process.Start( fp_bmp );

		}

	}
}

Color.FromArgb( k, k, k ); のところを FromArgb( k, 0, 0 ); とすれば赤のグラデーションの画像になりますし、FromArgb( 0, 0, k ) とすれば青のグラデーションの画像になります。

このコードの中で重要な箇所は、「いったんデフォルトのパレットを盗む」...「盗んだパレットの中身をいじる」...「いじったパレットを戻す」というところです。この「盗んで、いじって、戻す」という作業は忘れがちです。これをやらないと、おかしな色の画像がでてしまい、いつまでたってもうまく動かないと悩み続けることになります。

ためしに60行目から70行目までコメントアウトしてみれば、わたしの伝えたいことがわかると思います。

画像データは、Bitmap を8ビットインデックスパレットで作成したので、そのフォーマットのまま保存されています。

昔から Windows のプログラムをしている皆様にはおなじみですが、いちおう下記にビットマップファイルサイズの計算方法を示しておきます。

  • BITMAPFILEHEADER が 14バイト
  • BITMAPINFOHEADER が 40バイト
  • RGBQUADが256要素で 1024バイト
  • 512*256画素で 131072バイト

14 + 40 +1024 + 131072 を全部足すと 132150 バイトになります。出来上がったファイルのプロパティを見て確認してみてください。