初心者向けに低レベルでラスターを操作する

この記事の理由は、次の投稿でした: 「bmp画像をマトリックスに変換する、またはその逆をさらに処理する」 。 かつては、さまざまな圧縮および処理アルゴリズムを実装したC#で多くの研究コードを書く必要がありました。 コードが探索的であるという事実は、偶然ではありません。 このコードには固有の要件があります。 一方で、最適化はそれほど重要ではありません-アイデアをテストすることが重要です。 私はこのチェックを何時間も何日も延ばさないようにしたいのですが(さまざまなパラメーターでの起動が進行中の場合、または多数のテストイメージが処理されている場合)。 前述の投稿で使用された、ピクセルの明るさを参照する方法bmp.GetPixel(x、y)は、最初のプロジェクトが始まった場所です。 これは最も簡単な方法ですが、最も低速です。 ここで迷惑をかける価値はありますか? それを測定しましょう。



古典的なビットマップ(System.Drawing.Bitmap)を使用します。 このクラスは、ラスター形式のエンコードの詳細を隠すので便利です-原則として、彼らは興味がありません。 同時に、BMP、GIF、JPEG、PNGなど、すべての一般的な形式がサポートされています。



ちなみに、私は初心者に最初のメリットを提供します。 Bitmapクラスには、画像ファイルを開くことができるコンストラクターがあります。 ただし、この機能には不快な機能があります。このファイルへのアクセスはオープンのままなので、繰り返し呼び出すと実行されます。 この動作を修正するには、このメソッドを使用して、ビットマップにファイルをすぐに「解放」させることができます。



public static Bitmap LoadBitmap(string fileName) { using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read)) return new Bitmap(fs); }
      
      







測定方法



配列に抽出し、ビットマップに戻って古典的な画像処理-Lena( http://en.wikipedia.org/wiki/Lenna )によって測定します。 これは無料の画像であり、多数の画像処理作品(およびこの投稿のタイトル)にも含まれています。 サイズ-512 * 512ピクセル。



テクニックについて少し-このような場合、私は超高精度タイマーを追いかけるのではなく、同じアクションを何度も実行することを好みます。 もちろん、この場合、データとコードは既にプロセッサキャッシュにあります。 ただし、一方で、MSILコードのプロセッサコードへの変換に関連するコードの最初の実行のコストとその他のオーバーヘッドコストを分離します。 これを保証するために、最初に各コードを1回実行します。いわゆる「ウォームアップ」を実行します。



リリースでコードをコンパイルします。 確かにスタジオの下からではありません。 さらに、スタジオを閉鎖することもお勧めします。「無視」という事実が結果に影響する場合がありました。 また、他のアプリケーションを閉じることをお勧めします。



コードを数回実行し、典型的な結果を達成します-予期しないプロセスに影響しないことを確認する必要があります。 ウイルス対策ソフトなどが起動したとしましょう。 これらすべての手段により、安定した再現可能な結果を​​得ることができます。



素朴な方法



この方法は元の記事で使用されていました。 Bitmap.GetPixel(x、y)メソッドを使用することで構成されます。 ビットマップのコンテンツを3次元のバイト配列に変換するこのようなメソッドの完全なコードを提供します。 この場合、最初の次元は色成分(0から2まで)、2番目はy位置、3番目はx位置です。 データを別の方法で整理したい場合、私のプロジェクトでそれが起こりました-問題はないと思います。



 public static byte[, ,] BitmapToByteRgbNaive(Bitmap bmp) { int width = bmp.Width, height = bmp.Height; byte[, ,] res = new byte[3, height, width]; for (int y = 0; y < height; ++y) { for (int x = 0; x < width; ++x) { Color color = bmp.GetPixel(x, y); res[0, y, x] = color.R; res[1, y, x] = color.G; res[2, y, x] = color.B; } } return res; }
      
      







逆変換も同様で、データ転送のみが異なる方向に進みます。 ここでは彼のコードを提供しません。記事の最後にあるリンクでプロジェクトのソースコードを誰でも見ることができます。



I5-2520M 2.5GHzプロセッサーを搭載したラップトップでの画像への100回の変換、およびその逆は、43.90秒かかります。 512 * 512のイメージでは、データ転送に約0.5秒しかかかりません。



ビットマップデータの直接作業



幸いなことに、Bitmapクラスはデータにアクセスするためのより高速な方法を提供します。 これを行うには、BitmapDataクラスによって提供されるリンクとアドレス演算を使用する必要があります。



 public unsafe static byte[, ,] BitmapToByteRgb(Bitmap bmp) { int width = bmp.Width, height = bmp.Height; byte[, ,] res = new byte[3, height, width]; BitmapData bd = bmp.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb); try { byte* curpos; for (int h = 0; h < height; h++) { curpos = ((byte*)bd.Scan0) + h * bd.Stride; for (int w = 0; w < width; w++) { res[2, h, w] = *(curpos++); res[1, h, w] = *(curpos++); res[0, h, w] = *(curpos++); } } } finally { bmp.UnlockBits(bd); } return res; }
      
      







このアプローチでは、100回のコンバージョンあたり0.533秒(82回の加速)が得られます。 これはすでに質問に答えていると思います-より複雑な変換コードを書く価値はありますか? しかし、マネージコード内にとどまることで、プロセスをさらに高速化できますか?



配列とポインター



多次元配列は最速のデータ構造ではありません。 ここでは、インデックスの制限を超えるためのチェックが行われ、要素自体は乗算と加算の演算を使用して計算されます。 ビットマップデータを操作する際、アドレス演算によって既にかなりの高速化が実現しているため、多次元配列にも適用しようとするかもしれません。 直接変換コードは次のとおりです。



 public unsafe static byte[, ,] BitmapToByteRgbQ(Bitmap bmp) { int width = bmp.Width, height = bmp.Height; byte[, ,] res = new byte[3, height, width]; BitmapData bd = bmp.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb); try { byte* curpos; fixed (byte* _res = res) { byte* _r = _res, _g = _res + width*height, _b = _res + 2*width*height; for (int h = 0; h < height; h++) { curpos = ((byte*)bd.Scan0) + h * bd.Stride; for (int w = 0; w < width; w++) { *_b = *(curpos++); ++_b; *_g = *(curpos++); ++_g; *_r = *(curpos++); ++_r; } } } } finally { bmp.UnlockBits(bd); } return res; }
      
      







結果? コンバージョン100回あたり0.162秒。 そのため、彼らはさらに3.3倍加速しました(「ナイーブ」バージョンと比較して270倍)。 アルゴリズムを研究するときに使用したコードです。



なぜ持ち越しますか?



Bitmapからデータを転送する理由はまったく明らかではありません。 おそらく一般的に、すべての変換はそこで実装されていますか? これが可能なオプションの1つであることに同意します。 しかし、実際には、多くのアルゴリズムは浮動小数点データをチェックするのに便利です。 その後、オーバーフロー、中間段階での精度の低下に問題はありません。 double / float配列への変換も同様の方法で実行できます。 逆変換では、バイトに変換するときに検証が必要です。 このようなチェックの簡単なコードを次に示します。



 private static byte Limit(double x) { if (x < 0) return 0; if (x > 255) return 255; return (byte)x; }
      
      







このようなチェックと型変換を追加すると、コードが遅くなります。 ダブル配列のアドレス演算を使用するバージョンは、すでに0.713秒実行しています(100変換あたり)。 しかし、「ナイーブ」オプションを背景に-それはただの稲妻です。



そして、より速く必要な場合は?



より高速が必要な場合は、転送、Cでの処理、Asmを記述し、SIMDコマンドを使用します。 ビットマップラッパーなしで、ラスター形式を直接ダウンロードします。 もちろん、この場合、マネージコードの範囲を超えて、すべての長所と短所があります。 そして、これを行うことは、すでにデバッグされたアルゴリズムにとって意味があります。



この記事の完全なコードは、 rasterconversion.codeplex.com / SourceControl / latestにあります。



更新2013-10-08:

コメンテーターの提案で、彼はコードでMarshal.Copy()を使用して配列にデータを転送するオプションを追加しました。 これは純粋にテスト目的で行われます-この作業方法には制限があります:



したがって、2方向のコピーには0.158秒かかります(100変換あたり)。 ポインターのより柔軟なバージョンと比較して、異なる起動の結果の統計誤差の範囲内で、加速は非常に小さくなっています。



更新2016-04-25:

ユーザーAnt00は、BitmapToByteRgbQメソッドのコードにエラーを示しました。 時間には影響しませんでしたが、シフトは正しく実行されませんでした。 動作中のコードからのフラグメントのコピー&ペーストでエラーが発生しました。 修正しました。 忍耐に感謝します(すでに2.5年前の記事でコードを慎重に調べたのは初めてではありません)。



All Articles