WriteableBitmap の幅ピクセル数とストライドバイト数の関係について

WPFアプリケーションで画像処理を実施するときは WriteableBitmap を用いるのをお勧めします。例えば処理結果をウインドウに表示したり、結果をファイルに保存したり、データをファイルから読み込んだり、なんでも万能に使えます。

画像ファイル → WriteableBitmap → 自身で確保した byte[] データ領域、のようなデータの流れを実現するには、WriteableBitmap のサイズにしたがって、それに応じたサイズのデータ領域を確保済みにしておかなくてはなりません。

MFC や SDK やそのもっと昔の時代からビットマップ関係のクラスは、画像の幅ピクセル数に応じてストライド(ワンスキャン)バイト数が DWORD (4バイト)アライメントされていました。Windows Forms の Bitmap クラスもそうです。

これは何を意味しているかというと、画像の幅ピクセル数とストライドバイト数は必ずしもわかりやすい比例関係ではないということを意味しています。

本記事では、WPF の WriteableBitmap クラスの画像の幅ピクセル数とストライドバイト数を実際のコードを使って明らかにしていきます。

まず最初に結果を示しておきます。

8bitsデータのストライドバイト数
B8G8R8データのストライドバイト数
B8G8R8A8のストライドバイト数

3個のメッセージボックスの結果は、下記のコードで取得しました。

using System.Windows.Media.Imaging; 名前空間の追加をお忘れなく.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Navigation;
using System.Windows.Shapes;

// これの追加を忘れないように. Don't forget to add this sentence.
using System.Windows.Media.Imaging;

namespace aaa
{

	public partial class MainWindow : Window
	{

		double DPI_X = 96.0;
		double DPI_Y = 96.0;

		WriteableBitmap Bmp_08_256;
		WriteableBitmap Bmp_08_257;
		WriteableBitmap Bmp_08_258;
		WriteableBitmap Bmp_08_259;
		WriteableBitmap Bmp_08_260;
		WriteableBitmap Bmp_08_261;
		WriteableBitmap Bmp_08_262;
		WriteableBitmap Bmp_08_263;
		WriteableBitmap Bmp_08_264;

		WriteableBitmap Bmp_24_256;
		WriteableBitmap Bmp_24_257;
		WriteableBitmap Bmp_24_258;
		WriteableBitmap Bmp_24_259;
		WriteableBitmap Bmp_24_260;
		WriteableBitmap Bmp_24_261;
		WriteableBitmap Bmp_24_262;
		WriteableBitmap Bmp_24_263;
		WriteableBitmap Bmp_24_264;

		WriteableBitmap Bmp_32_256;
		WriteableBitmap Bmp_32_257;
		WriteableBitmap Bmp_32_258;
		WriteableBitmap Bmp_32_259;
		WriteableBitmap Bmp_32_260;
		WriteableBitmap Bmp_32_261;
		WriteableBitmap Bmp_32_262;
		WriteableBitmap Bmp_32_263;
		WriteableBitmap Bmp_32_264;

		List<WriteableBitmap> ListBmp08;
		List<WriteableBitmap> ListBmp24;
		List<WriteableBitmap> ListBmp32;

		public MainWindow()
		{

			// デフォルトからあるコード.
			InitializeComponent();

			// 3種のピクセルフォーマット.
			PixelFormat pixfmt08 = PixelFormats.Indexed8;
			PixelFormat pixfmt24 = PixelFormats.Bgr24;
			PixelFormat pixfmt32 = PixelFormats.Bgra32;

			// bpp08 だけはパレットが必要なためそれの素になるカラーのリストを作成する.
			const int NUM_ELEMENTS_PAL = 256;
			List<Color> list = new List<Color>(0);
			for ( int k = 0; k < NUM_ELEMENTS_PAL; k++ )
			{
				const byte LEVEL_ALPHA = 0xff;
				byte level = (byte)(k);
				Color c = Color.FromArgb( LEVEL_ALPHA, level, level, level );
				list.Add( c );
			}

			// bpp08 用のパレットを生成する.
			BitmapPalette pal = new BitmapPalette( list );

			// このトピックにおいて画像の高さはどうでもよい.
			const int IMG_H = 100;

			// bpp08 のビットマップを生成する.
			Bmp_08_256 = new WriteableBitmap( 256, IMG_H, DPI_X, DPI_Y, pixfmt08, pal );
			Bmp_08_257 = new WriteableBitmap( 257, IMG_H, DPI_X, DPI_Y, pixfmt08, pal );
			Bmp_08_258 = new WriteableBitmap( 258, IMG_H, DPI_X, DPI_Y, pixfmt08, pal );
			Bmp_08_259 = new WriteableBitmap( 259, IMG_H, DPI_X, DPI_Y, pixfmt08, pal );
			Bmp_08_260 = new WriteableBitmap( 260, IMG_H, DPI_X, DPI_Y, pixfmt08, pal );
			Bmp_08_261 = new WriteableBitmap( 261, IMG_H, DPI_X, DPI_Y, pixfmt08, pal );
			Bmp_08_262 = new WriteableBitmap( 262, IMG_H, DPI_X, DPI_Y, pixfmt08, pal );
			Bmp_08_263 = new WriteableBitmap( 263, IMG_H, DPI_X, DPI_Y, pixfmt08, pal );
			Bmp_08_264 = new WriteableBitmap( 264, IMG_H, DPI_X, DPI_Y, pixfmt08, pal );

			// bpp24 のビットマップを生成する.
			Bmp_24_256 = new WriteableBitmap( 256, IMG_H, DPI_X, DPI_Y, pixfmt24, null );
			Bmp_24_257 = new WriteableBitmap( 257, IMG_H, DPI_X, DPI_Y, pixfmt24, null );
			Bmp_24_258 = new WriteableBitmap( 258, IMG_H, DPI_X, DPI_Y, pixfmt24, null );
			Bmp_24_259 = new WriteableBitmap( 259, IMG_H, DPI_X, DPI_Y, pixfmt24, null );
			Bmp_24_260 = new WriteableBitmap( 260, IMG_H, DPI_X, DPI_Y, pixfmt24, null );
			Bmp_24_261 = new WriteableBitmap( 261, IMG_H, DPI_X, DPI_Y, pixfmt24, null );
			Bmp_24_262 = new WriteableBitmap( 262, IMG_H, DPI_X, DPI_Y, pixfmt24, null );
			Bmp_24_263 = new WriteableBitmap( 263, IMG_H, DPI_X, DPI_Y, pixfmt24, null );
			Bmp_24_264 = new WriteableBitmap( 264, IMG_H, DPI_X, DPI_Y, pixfmt24, null );

			// bpp32 のビットマップを生成する.
			Bmp_32_256 = new WriteableBitmap( 256, IMG_H, DPI_X, DPI_Y, pixfmt32, null );
			Bmp_32_257 = new WriteableBitmap( 257, IMG_H, DPI_X, DPI_Y, pixfmt32, null );
			Bmp_32_258 = new WriteableBitmap( 258, IMG_H, DPI_X, DPI_Y, pixfmt32, null );
			Bmp_32_259 = new WriteableBitmap( 259, IMG_H, DPI_X, DPI_Y, pixfmt32, null );
			Bmp_32_260 = new WriteableBitmap( 260, IMG_H, DPI_X, DPI_Y, pixfmt32, null );
			Bmp_32_261 = new WriteableBitmap( 261, IMG_H, DPI_X, DPI_Y, pixfmt32, null );
			Bmp_32_262 = new WriteableBitmap( 262, IMG_H, DPI_X, DPI_Y, pixfmt32, null );
			Bmp_32_263 = new WriteableBitmap( 263, IMG_H, DPI_X, DPI_Y, pixfmt32, null );
			Bmp_32_264 = new WriteableBitmap( 264, IMG_H, DPI_X, DPI_Y, pixfmt32, null );

			// リストで管理する.
			ListBmp08 = new List<WriteableBitmap>(0);
			ListBmp08.Add( Bmp_08_256 );
			ListBmp08.Add( Bmp_08_257 );
			ListBmp08.Add( Bmp_08_258 );
			ListBmp08.Add( Bmp_08_259 );
			ListBmp08.Add( Bmp_08_260 );
			ListBmp08.Add( Bmp_08_261 );
			ListBmp08.Add( Bmp_08_262 );
			ListBmp08.Add( Bmp_08_263 );
			ListBmp08.Add( Bmp_08_264 );

			// リストで管理する.
			ListBmp24 = new List<WriteableBitmap>(0);
			ListBmp24.Add( Bmp_24_256 );
			ListBmp24.Add( Bmp_24_257 );
			ListBmp24.Add( Bmp_24_258 );
			ListBmp24.Add( Bmp_24_259 );
			ListBmp24.Add( Bmp_24_260 );
			ListBmp24.Add( Bmp_24_261 );
			ListBmp24.Add( Bmp_24_262 );
			ListBmp24.Add( Bmp_24_263 );
			ListBmp24.Add( Bmp_24_264 );

			// リストで管理する.
			ListBmp32 = new List<WriteableBitmap>(0);
			ListBmp32.Add( Bmp_32_256 );
			ListBmp32.Add( Bmp_32_257 );
			ListBmp32.Add( Bmp_32_258 );
			ListBmp32.Add( Bmp_32_259 );
			ListBmp32.Add( Bmp_32_260 );
			ListBmp32.Add( Bmp_32_261 );
			ListBmp32.Add( Bmp_32_262 );
			ListBmp32.Add( Bmp_32_263 );
			ListBmp32.Add( Bmp_32_264 );

		}

		protected override void OnRender( DrawingContext dc )
		{

			this.stbLabel000.Content = "000";
			this.stbLabel001.Content = "001";
			this.stbLabel002.Content = "002";
			this.stbLabel003.Content = "003";

		}

		private void menuDebugExec000_Click( object sender, RoutedEventArgs e )
		{

			// 結果表示用のストリングビルダ.
			StringBuilder sb = new StringBuilder();

			// ストライド(ワンスキャン)バイト数を取得する.
			foreach( var bmp in ListBmp08 )
			{

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

				const String FMT = "bpp08 : {0}×{1} is {2} bytes.";
				String str = String.Format( FMT, w, h, stride_bytes );

				// ストリングビルダに追加する.
				sb.AppendLine( str );

			}

			// メッセージボックスで結果表示する.
			String strmsg = sb.ToString().Trim();
			String cap = "Result bpp08";
			MessageBox.Show( strmsg, cap );

			// クリップボードに結果文字列を格納する.
			Clipboard.SetText( strmsg );

		}

		private void menuDebugExec001_Click( object sender, RoutedEventArgs e )
		{

			// 結果表示用のストリングビルダ.
			StringBuilder sb = new StringBuilder();

			// ストライド(ワンスキャン)バイト数を取得する.
			foreach( var bmp in ListBmp24 )
			{

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

				const String FMT = "bpp24 : {0}×{1} is {2} bytes.";
				String str = String.Format( FMT, w, h, stride_bytes );

				// ストリングビルダに追加する.
				sb.AppendLine( str );

			}

			// メッセージボックスで結果表示する.
			String strmsg = sb.ToString().Trim();
			String cap = "Result bpp24";
			MessageBox.Show( strmsg, cap );

			// クリップボードに結果文字列を格納する.
			Clipboard.SetText( strmsg );

		}

		private void menuDebugExec002_Click( object sender, RoutedEventArgs e )
		{

			// 結果表示用のストリングビルダ.
			StringBuilder sb = new StringBuilder();

			// ストライド(ワンスキャン)バイト数を取得する.
			foreach( var bmp in ListBmp32 )
			{

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

				const String FMT = "bpp32 : {0}×{1} is {2} bytes.";
				String str = String.Format( FMT, w, h, stride_bytes );

				// ストリングビルダに追加する.
				sb.AppendLine( str );

			}

			// メッセージボックスで結果表示する.
			String strmsg = sb.ToString().Trim();
			String cap = "Result bpp32";
			MessageBox.Show( strmsg, cap );

			// クリップボードに結果文字列を格納する.
			Clipboard.SetText( strmsg );

		}

		private void menuApplicationQuit_Click( object sender, RoutedEventArgs e )
		{
			this.Close();
		}

	}

}
8bitsデータのストライドバイト数
B8G8R8データのストライドバイト数
B8G8R8A8のストライドバイト数

下記の「4で割れないから」というのは「DWORDアライメントではないから」という意味と同義です。4できれいに整数で割り算が可能か、というところが注目点になります。

1画素グレースケール1バイトの画像のストライド(ワンスキャン)バイト数

bpp08 : 256×100 is 256 bytes.
bpp08 : 257×100 is 260 bytes. ( 4 で割れないから 3バイト追加 )
bpp08 : 258×100 is 260 bytes. ( 4 で割れないから 2バイト追加 )
bpp08 : 259×100 is 260 bytes. ( 4 で割れないから 1バイト追加 )
bpp08 : 260×100 is 260 bytes.
bpp08 : 261×100 is 264 bytes. ( 4 で割れないから 3バイト追加 )
bpp08 : 262×100 is 264 bytes. ( 4 で割れないから 2バイト追加 )
bpp08 : 263×100 is 264 bytes. ( 4 で割れないから 1バイト追加 )
bpp08 : 264×100 is 264 bytes.

1画素 B8G8R8 3バイトの画像のストライド(ワンスキャン)バイト数

bpp24 : 256×100 is 768 bytes.
bpp24 : 257×100 is 772 bytes. ( 257×3 → 771 で 4 で割れないから 1バイト追加)
bpp24 : 258×100 is 776 bytes. ( 258×3 → 774 で 4 で割れないから 2バイト追加)
bpp24 : 259×100 is 780 bytes. ( 259×3 → 777 で 4 で割れないから 3バイト追加)
bpp24 : 260×100 is 780 bytes.
bpp24 : 261×100 is 784 bytes. (261×3 → 783 で 4 で割れないから 1バイト追加)
bpp24 : 262×100 is 788 bytes. (262×3 → 786 で 4 で割れないから 2バイト追加)
bpp24 : 263×100 is 792 bytes. (263×3 → 789 で 4 で割れないから 3バイト追加)
bpp24 : 264×100 is 792 bytes.

1画素 B8G8R8A8 4バイトの画像のストライド(ワンスキャン)バイト数

bpp32 : 256×100 is 1024 bytes. ( 1画素4バイトなので何もしなくていい )
bpp32 : 257×100 is 1028 bytes. ( 1画素4バイトなので何もしなくていい )
bpp32 : 258×100 is 1032 bytes. ( 1画素4バイトなので何もしなくていい )
bpp32 : 259×100 is 1036 bytes. ( 1画素4バイトなので何もしなくていい )
bpp32 : 260×100 is 1040 bytes. ( 1画素4バイトなので何もしなくていい )
bpp32 : 261×100 is 1044 bytes. ( 1画素4バイトなので何もしなくていい )
bpp32 : 262×100 is 1048 bytes. ( 1画素4バイトなので何もしなくていい )
bpp32 : 263×100 is 1052 bytes. ( 1画素4バイトなので何もしなくていい )
bpp32 : 264×100 is 1056 bytes. ( 1画素4バイトなので何もしなくていい )

WriteableBitmap はこのような実装になっています。

ちなみに追加しているバイトのことを「パディングバイト」"padding bytes"と言います。

上記の結果を見れば、ある程度の法則性が見えてくると思います。8bitsグレイ、B8G8R8 どちらにおいてもパディングバイトを考慮しなくていいようにするには、画像の幅方向の画素数を4の倍数にしておけばいいのです。

マウスなどで画像を切り出すときも、画像幅が4の倍数になるように裏でコッソリ調整して切り出してやれば、あとあと面倒がありません。

グラフィック屋さんがアプリユーザの場合は1ピクセル勝負のときもあるので、そうもいかないですけれど。