BitmapData.Strideプロパティ
コメント欄にて情報を頂きましたので、追記しておきます。
1行のバイト数を取得するBitmapData.Strideプロパティという便利なものがあるということで、早速使ってみました。
int width = this.bitmap.Width; int height = this.bitmap.Height; int strideIn = dataRgb.Stride; int strideOut = dataGray.Stride; byte* currentData = datas24bppRgb - 1; for (int y = 0; y < height; ++y) { int widthY = y * strideOut; for (int x = 0; x < strideOut; ++x) { if (x < strideIn / 3) { *(datas8bppGray + widthY + x) = (byte)(((int)*++currentData + (int)*++currentData + (int)*++currentData) / 3); } else { ++currentData; } } }
ビットマップのデータ構造
前エントリに示したコードのバグ
前エントリの最後に書いたバグの話です。
前エントリで示したコードでは、壁紙サイズ等の無難なサイズの画像ではうまく動きますが、半端な画像サイズでは、以下のように、出力結果が不正となります。
実際にどのような画像で不具合が出るのかを知るためには、ビットマップのピクセルデータの構造を知る必要があります。
実際の構造は、
ビットマップデータ
http://ja.wikipedia.org/wiki/Windows_bitmap
このブロックは、イメージを各ピクセルごとに記述する。ピクセルは通常左から右へ、下から上に向かって保存されている。各ピクセルは1バイト以上で記述されている。もし水平方向のバイト数が4の倍数ではないときは、ヌル (0x00)で埋めて4の倍数にする。
となっており、1行あたりのバイト数が4の倍数にならない場合、バグが発現することになります。上記で示した画像のサイズは、222×628となっており、入力画像(24bitカラー)の1行あたりのバイト数は222×3=666バイトとなります。これを4で割ると、144余り2となるため、入力画像が、1行あたり2バイトずつずれてしまうことになります。また、出力画像(8bitグレースケール)の1行あたりのバイト数は222×1=222バイトとなり、これを4で割ると、55余り2となるため、出力画像も、1行あたり2バイトずつずれてしまうことになります。
これを改善するためには、1行あたりのずれを考慮し、ピクセルデータのインデックスにオフセット値を加える必要があります。以下にコードを示します。
// 4バイト境界の考慮(入力) int lineIn = this.bitmap.Width * 3; int lineInNew = (lineIn % 4 != 0) ? ((lineIn / 4 + 1) * 4) : lineIn; int lineInDiff = lineInNew - lineIn; int offsetIn = 0; // 4バイト境界の考慮(出力) int lineOut = this.bitmap.Width; int lineOutNew = (lineOut % 4 != 0) ? ((lineOut / 4 + 1) * 4) : lineOut; int lineOutDiff = lineOutNew - lineOut; int offsetOut = 0; // ポインタを介して、ピクセル毎にグレースケール化 for (int y = 0; y < this.bitmap.Height; y++) { for (int x = 0; x < this.bitmap.Width; x++) { // 4バイト境界の考慮 datas8bppGray[y * this.bitmap.Width + x + offsetOut] = (byte)( ((int)(datas24bppRgb[y * this.bitmap.Width * 3 + x * 3 + offsetIn]) + (int)(datas24bppRgb[y * this.bitmap.Width * 3 + x * 3 + offsetIn + 1]) + (int)(datas24bppRgb[y * this.bitmap.Width * 3 + x * 3 + offsetIn + 2])) / 3); } // 4バイト境界の考慮 offsetIn += lineInDiff; offsetOut += lineOutDiff; }
処理時間の計測
下準備
前エントリで使用したコードを用いて、実際にどの程度の処理時間を要するかを知るための方法を示します。計測結果を表示するために、フォーム上にラベルを作成しておいてください。
System.Diagnostics.Stopwatchクラス
// 処理時間計測開始 Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); // ... // 処理時間計測終了 stopWatch.Stop(); TimeSpan ts = stopWatch.Elapsed; string elapsedTime = (ts.Seconds * 1000 + ts.Milliseconds).ToString() + "ms"; labelTAT.Text = elapsedTime;
System.Environment.TickCountプロパティ
// 処理時間計測開始 int tickBegin = Environment.TickCount; // ... // 処理時間計測終了 int tickEnd = Environment.TickCount; double tick = tickEnd - tickBegin; labelTAT.Text = tick.ToString("#0.0000") + "ms";
QueryPerformanceCounter関数、QueryPerformanceFrequency関数
[DllImport("kernel32.dll")] extern static short QueryPerformanceCounter(ref long x); [DllImport("kernel32.dll")] extern static short QueryPerformanceFrequency(ref long x); // ... private void buttonExecute_Click(object sender, EventArgs e) { // ... // 処理時間計測開始 long cntStart = 0; QueryPerformanceCounter(ref cntStart); // ... // 処理時間計測終了 long cntEnd = 0; QueryPerformanceCounter(ref cntEnd); long frq = 0; QueryPerformanceFrequency(ref frq); double time = ((double)cntEnd - (double)cntStart) / (double)frq; time *= 1000; labelTAT.Text = time.ToString("#0.0000") + "ms";
処理時間の短縮
注意点
本節での処理時間計測は、Releaseビルドのデバッグなしで実行しています。
現状の把握
まずは現状のソースファイルですが、以下のようになっているかと思います。
// 処理時間計測開始 long cntStart = 0; QueryPerformanceCounter(ref cntStart); unsafe { // 出力ビットマップの領域確保 this.grayscale = new Bitmap(this.bitmap.Width, this.bitmap.Height, PixelFormat.Format8bppIndexed); // 入出力ビットマップをシステムメモリにロック BitmapData dataRgb = this.bitmap.LockBits( new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb); BitmapData dataGray = this.grayscale.LockBits( new Rectangle(0, 0, this.grayscale.Width, this.grayscale.Height), ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed); byte* datas24bppRgb = (byte*)dataRgb.Scan0; byte* datas8bppGray = (byte*)dataGray.Scan0; // 4バイト境界の考慮(入力) int lineIn = this.bitmap.Width * 3; int lineInNew = (lineIn % 4 != 0) ? ((lineIn / 4 + 1) * 4) : lineIn; int lineInDiff = lineInNew - lineIn; int offsetIn = 0; // 4バイト境界の考慮(出力) int lineOut = this.bitmap.Width; int lineOutNew = (lineOut % 4 != 0) ? ((lineOut / 4 + 1) * 4) : lineOut; int lineOutDiff = lineOutNew - lineOut; int offsetOut = 0; // ポインタを介して、ピクセル毎にグレースケール化 for (int y = 0; y < this.bitmap.Height; y++) { for (int x = 0; x < this.bitmap.Width; x++) { // 4バイト境界の考慮 datas8bppGray[y * this.bitmap.Width + x + offsetOut] = (byte)( ((int)(datas24bppRgb[y * this.bitmap.Width * 3 + x * 3 + offsetIn]) + (int)(datas24bppRgb[y * this.bitmap.Width * 3 + x * 3 + offsetIn + 1]) + (int)(datas24bppRgb[y * this.bitmap.Width * 3 + x * 3 + offsetIn + 2])) / 3); } // 4バイト境界の考慮 offsetIn += lineInDiff; offsetOut += lineOutDiff; } // グレースケールのパレット情報の設定 ColorPalette palette = this.grayscale.Palette; for (int i = 0; i < palette.Entries.Length; ++i) { Color clr = Color.FromArgb(i, i, i); palette.Entries[i] = clr; } this.grayscale.Palette = palette; // 入出力ビットマップのロックを解除 this.bitmap.UnlockBits(dataRgb); this.grayscale.UnlockBits(dataGray); } // 処理時間計測終了 long cntEnd = 0; QueryPerformanceCounter(ref cntEnd); long frq = 0; QueryPerformanceFrequency(ref frq); double time = ((double)cntEnd - (double)cntStart) / (double)frq; time *= 1000; labelTAT.Text = time.ToString("#0.0000") + "ms";
この状態で、1280×1024の画像をグレースケール化すると、(私の環境では)概ね1156ms程度かかります。計測点を、ネストしているfor文の前後に移動しても、さほど違いが見られなかったため、このネストしているfor文の処理時間が短縮できれば、全体の処理速度も速くなるはずです。
// ポインタを介して、ピクセル毎にグレースケール化 for (int y = 0; y < this.bitmap.Height; y++) { for (int x = 0; x < this.bitmap.Width; x++) { // 4バイト境界の考慮 datas8bppGray[y * this.bitmap.Width + x + offsetOut] = (byte)( ((int)(datas24bppRgb[y * this.bitmap.Width * 3 + x * 3 + offsetIn]) + (int)(datas24bppRgb[y * this.bitmap.Width * 3 + x * 3 + offsetIn + 1]) + (int)(datas24bppRgb[y * this.bitmap.Width * 3 + x * 3 + offsetIn + 2])) / 3); } // 4バイト境界の考慮 offsetIn += lineInDiff; offsetOut += lineOutDiff; }
記述こそ単純ですが、無駄な計算を何度も行っており、非常に遅いです。これから順にチューンアップしていきます。
まず、インデックスの算出を何度も繰り返しているため、その部分に手を入れます。
// ポインタを介して、ピクセル毎にグレースケール化 for (int y = 0; y < this.bitmap.Height; y++) { for (int x = 0; x < this.bitmap.Width; x++) { // 4バイト境界の考慮 int indexIn = y * this.bitmap.Width * 3 + x * 3 + offsetIn; int indexOut = y * this.bitmap.Width + x + offsetOut; datas8bppGray[indexOut] = (byte)( ((int)(datas24bppRgb[indexIn]) + (int)(datas24bppRgb[indexIn + 1]) + (int)(datas24bppRgb[indexIn + 2])) / 3); } // 4バイト境界の考慮 offsetIn += lineInDiff; offsetOut += lineOutDiff; }
これで、400msほど高速化できました。
次に、インデックスのY成分を算出している部分は、Xのループの外に出せますので、修正します。
// ポインタを介して、ピクセル毎にグレースケール化 for (int y = 0; y < this.bitmap.Height; y++) { int indexYOut = y * this.bitmap.Width; int indexYIn = indexYOut * 3; for (int x = 0; x < this.bitmap.Width; x++) { // 4バイト境界の考慮 int indexIn = indexYIn + x * 3 + offsetIn; int indexOut = indexYOut + x + offsetOut; datas8bppGray[indexOut] = (byte)( ((int)(datas24bppRgb[indexIn]) + (int)(datas24bppRgb[indexIn + 1]) + (int)(datas24bppRgb[indexIn + 2])) / 3); } // 4バイト境界の考慮 offsetIn += lineInDiff; offsetOut += lineOutDiff; }
これで、300msほど高速化できました。
インデックス記述ではなく、ポインタの参照外しに修正します。
// ポインタを介して、ピクセル毎にグレースケール化 for (int y = 0; y < this.bitmap.Height; y++) { int indexYOut = y * this.bitmap.Width; int indexYIn = indexYOut * 3; for (int x = 0; x < this.bitmap.Width; x++) { // 4バイト境界の考慮 int indexIn = indexYIn + x * 3 + offsetIn; int indexOut = indexYOut + x + offsetOut; *(datas8bppGray + indexOut) = (byte)( ((int)(*(datas24bppRgb + indexIn)) + (int)(*(datas24bppRgb + indexIn + 1)) + (int)(*(datas24bppRgb + indexIn + 2))) / 3); } // 4バイト境界の考慮 offsetIn += lineInDiff; offsetOut += lineOutDiff; }
若干ではありますが高速化しているようです。
最終的には、以下となりました。
int width = this.bitmap.Width; int height = this.bitmap.Height; byte* currentData = datas24bppRgb - 1; for (int y = 0; y < height; ++y) { int widthY = y * width; for (int x = 0; x < width; ++x) { *(datas8bppGray + widthY + x + offsetOut) = (byte)(((int)*++currentData + (int)*++currentData + (int)*++currentData) / 3); } currentData += lineInDiff; offsetOut += lineOutDiff; }
フルカラービットマップからグレースケールビットマップへの変換
下準備(プロジェクトの作成)
Winfowsフォームアプリケーションを新規作成します。プロジェクト名は、ImageFiltersとしておきます。
以下のようなフォームを作成します。
ボタンを2つ、パネルを1つ、(パネル内に)ピクチャボックスを1つ配置します。
2つのボタンのAnchorプロパティをbottom,leftに設定します。
パネルのAutoScrollプロパティをtrueに、Anchorプロパティをtop,bottom,left,rightに設定します。
ピクチャボックスのSizeModeプロパティをAutoScrollに設定します。
2つのボタンをダブルクリックし、イベントハンドラのコードを生成します。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; namespace ImageFilters { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { } private void button2_Click(object sender, EventArgs e) { } } }
下準備(ビットマップの表示)
次に、ボタン1をクリックすると、ファイル選択ダイアログが開き、選択された画像がパネル内に表示されるようにします。
ここで、button1という名前は不適切なので、NameプロパティをbuttonLoadに変更します。また、Textプロパティを「読み込み」に変更します。
イベントハンドラの名前(button1_Click)も適切な名前に変更する必要があります。イベントハンドラ名の上にマウスカーソルを持っていき、[リファクタ]→[名前の変更]を選択します。
新しい名前にbuttonLoad_Clickを選択し、OKを押します。
変更箇所のプレビューが表示されます。ここでは、メソッド名と同様に、イベントハンドラの登録部分が変更されることを確認します。
Form1というクラス名等も不適切ですので、同様に変更しておくと良いでしょう。
buttonLoadがクリックされた際に行う処理は、以下となります。
-
- ファイルオープンダイアログを開き、画像を選択する
- 選択した画像を元にしてBitmapクラスのオブジェクトを作成する
- 作成したBitmapクラスのオブジェクトをピクチャボックスに設定する
上記を実装したコードを以下に示します。なお、フォームの名前をImageFiltersFormに、ピクチャボックスの名前をpictureBoxに、パネルの名前をpanelに変更しています。さらに、Bitmapクラスのオブジェクトを保持するため、bitmapというメンバ変数を追加しています。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; namespace ImageFilters { public partial class ImageFiltersForm : Form { public ImageFiltersForm() { InitializeComponent(); } private Bitmap bitmap = null; private void buttonLoad_Click(object sender, EventArgs e) { OpenFileDialog dialog = new OpenFileDialog(); if (dialog.ShowDialog() == DialogResult.OK) { if (this.bitmap != null) { this.bitmap.Dispose(); this.bitmap = null; } try { this.bitmap = new Bitmap(dialog.FileName); } catch { MessageBox.Show("画像ファイル読み込みに失敗しました。"); return; } this.pictureBox.Image = this.bitmap; } } private void button2_Click(object sender, EventArgs e) { } } }
グレースケール化
次に、ボタン2をクリックすると、パネル上にグレースケール化された画像が表示されるようにします。
button2についても、(button1と同様に)不適切な名前のため、buttonExecに変更します。イベントハンドラ名も同様に変更します。
buttonExecがクリックされた際に行う処理は、以下となります。ここでは、簡単のため、24bitフルカラー画像から8bitグレースケール画像への変換のみを考慮します。
-
- 出力ビットマップの領域確保
- 入出力ビットマップをシステムメモリにロック
- ポインタを介して、pixcel毎にグレースケール化
- グレースケールのパレット情報の設定
- 入出力ビットマップのロックを解除
以下のような疑似コードを元に、順次実装していきます。System.Drawing.Imagingにあるクラスを使用するため、事前にusing指定しておくと良いかと思います。また、ポインタを使用するunsafeブロックが存在するため、プロジェクトの設定を変更し、unsafeコードを許可する必要があります。
private void buttonExec_Click(object sender, EventArgs e) { if (bitmap == null) { return; } if (bitmap.PixelFormat != PixelFormat.Format24bppRgb) { MessageBox.Show("24ビットの画像ファイルを指定してください。"); return; } this.pictureBox.Image = null; unsafe { // 出力ビットマップの領域確保 // 入出力ビットマップをシステムメモリにロック // ポインタを介して、ピクセル毎にグレースケール化 // グレースケールのパレット情報の設定 // 入出力ビットマップのロックを解除 } this.pictureBox.Image = this.grayscale; }
出力ビットマップを保持するために、事前に、grayscaleというメンバ変数を追加します。
出力ビットマップの領域は、以下のように確保します。別途、パレット情報を与える必要がありますが、それについては後述します。
// 出力ビットマップの領域確保 this.grayscale = new Bitmap(this.bitmap.Width, this.bitmap.Height, PixelFormat.Format8bppIndexed);
次に、ビットマップをシステムメモリ上の特定の位置にロックすることで、ポインタを介したデータ操作を可能にします。入力ビットマップはリードオンリー、出力ビットマップはライトオンリーとしています。
// 入出力ビットマップをシステムメモリにロック BitmapData dataRgb = this.bitmap.LockBits( new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb); BitmapData dataGray = this.grayscale.LockBits( new Rectangle(0, 0, this.grayscale.Width, this.grayscale.Height), ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed); byte* datas24bppRgb = (byte*)dataRgb.Scan0; byte* datas8bppGray = (byte*)dataGray.Scan0;
上記の処理により、入力ビットマップと出力ビットマップのピクセルデータの先頭アドレスが取得できたので、ピクセル毎にグレースケール化を行います。グレースケール化の方法は、RGB値の平均値を使用しています。
// ポインタを介して、pixcel毎にグレースケール化 for (int y = 0; y < this.bitmap.Height; y++) { for (int x = 0; x < this.bitmap.Width; x++) { datas8bppGray[y * this.bitmap.Width + x] = (byte)( ((int)(datas24bppRgb[y * this.bitmap.Width * 3 + x * 3]) + (int)(datas24bppRgb[y * this.bitmap.Width * 3 + x * 3 + 1]) + (int)(datas24bppRgb[y * this.bitmap.Width * 3 + x * 3 + 2])) / 3); } }
8ビットインデックスカラーのビットマップをグレースケールにするために、パレット情報を生成します。具体的には、0x00から0xffまでの値を、順番に、RGB全てに与えています。
// グレースケールのパレット情報の設定 ColorPalette palette = this.grayscale.Palette; for (int i = 0; i < palette.Entries.Length; ++i) { Color clr = Color.FromArgb(i, i, i); palette.Entries[i] = clr; } this.grayscale.Palette = palette;
最後に、入出力ビットマップのロックを解除します。
// 入出力ビットマップのロックを解除 this.bitmap.UnlockBits(dataRgb); this.grayscale.UnlockBits(dataGray);
おわりに
ここまでの手順で作成したグレースケール変換処理には、実は、バグがあります。次回はその辺りの所を書こうと思っています。
再帰テンプレート
今年の仕事始めの日に、「午前中いっぱい(3H)で、何かプログラム書け」というイベントがあったのですが、その時に使ったネタを書いておきます。
数列の計算
単純な数列として、0..nまでの和、nの階乗、フィボナッチ数列などがありますが、例えば、0..nまでの和を求める関数は、以下のように書けると思います。
int sum(const int x) { if (x == 0) { return 0; } return x + sum( x - 1 ); }
実際に数列を使う場合には、以下のようにするかと思います。
int x; ... std::cout << "sum(x) = " << sum(x) << "\n"; ... std::cout << "sum(400) = " << sum(400) << "\n";
ここで、sum(400)は明らかに定数ですので、実行時に毎回算出するのは無駄だと言えます。
しかしながら、sum(400)の計算結果を定数として定義するというのもあまり良いとは思えません。
そこで、テンプレートを使ってコンパイル時に計算させるという方法があります。
/////////////////////////////////////////////////////////////////////////////// // 0..nの合計を求める // n番目を返す template<int x> struct Sum { enum { value = x + Sum<x - 1>::value }; }; // xが0の時は0を返す template<> struct Sum<0> { enum { value = 0 }; };
n番目を返すテンプレートは、enumのvalueを(1回だけ代入できる)変数のように利用して、再帰的に定義されます。そして、そのままでは無限ループになってしまうので、0番目だけは、別途定義して、0を設定します。
実際に使う場合は、以下のようになります。
std::cout << "Sum(400) = " << Sum<400>::value << "\n";
これで、0..nまでの和をコンパイル時に計算し、実行時間を短縮(微々たるものですが)できます。
しかしながら、あまり大きな値を与えてしまうと、コンパイラがテンプレートを処理する際の限界値に達してしまうことがあります。
例えば、自分の環境(VS2008)では、
std::cout << "Sum(489) = " << Sum<489>::value << "\n";
これは計算可能でしたが、
std::cout << "Sum(490) = " << Sum<490>::value << "\n";
これはコンパイラが落ちてしまいました。
最後に、実際にイベントで発表したソースを以下に記しておきます。
# 明らかな間違いやコメントは修正しています。
/////////////////////////////////////////////////////////////////////////////// // 0..nの合計を求める // n番目を返す template<int x> struct Sum { enum { value = x + Sum<x - 1>::value }; }; // 0番目を返す template<> struct Sum<0> { enum { value = 0 }; }; /////////////////////////////////////////////////////////////////////////////// // n!を求める // n番目を返す template<int x> struct Factorial { enum { value = x * Factorial<x - 1>::value }; }; // 0番目を返す template<> struct Factorial<0> { enum { value = 1 }; }; /////////////////////////////////////////////////////////////////////////////// // 0..nのフィボナッチ数列を求める // n番目を返す template<int x> struct FibonacciNumber { enum { value = FibonacciNumber<x - 2>::value + FibonacciNumber<x - 1>::value }; }; // 1番目を返す template<> struct FibonacciNumber<1> { enum { value = 1 }; }; // 0番目を返す template<> struct FibonacciNumber<0> { enum { value = 0 }; }; ///////////////////////////////////////////////////////////////////////////////
↑ヘッダファイル
#include <iostream> #include "FibonacciNumber.hpp" int sum(const int x) { if (x == 0) { return 0; } return x + sum( x - 1 ); } int main(int argc, char* argv[]) { std::cout << "Sum(489) = " << Sum<489>::value << "\n"; // Sum(490)でコンパイラがギブアップ std::cout << "sum(500) = " << sum(500) << "\n"; std::cout << "Fibonacci(46) = " << FibonacciNumber<46>::value << "\n"; // Fibonacci(47)でオーバーフロー std::cout << "Factorial(12) = " << Factorial<12>::value << "\n"; // Factorial(13)でオーバーフロー getchar(); }
↑ソースファイル
Excelでセルの編集状態を認識する
Excelにてセルが編集状態を開始・終了したタイミングをイベントとして認識する方法です。
弊社のHP内の技術情報ページにて掲載しておりますので、リンクを張っておきます。