【C#】24bitと32bitカラー画像の表示速度の比較

カラー画像の24bitカラーと32bitカラーの画像はR,G,Bの成分はそれぞれ8bitで表現され、32bitの時に、残りの8bit部分で画像の透過率として使われる場合もありますが、画像処理的には、あまり使われる事はありません。

そのため、カラー画像と言えばメモリ容量の少ない24bitカラー画像を扱う事が、個人的には多かったのですが、GPU処理やSIMD処理などを考慮すると、メモリアクセス的に有利な32bitカラーの方が都合が良い場合もあります。

 

そこで今回は、24bitカラーと32bitカラーとで、表示速度に違いがあるか?調べてみました。

 

評価に用いたプログラムのイメージはこちら↓

 

プログラムは以下のような物で、PictureBoxのImageに指定するBitmapクラスオブジェクトのPixelFormatが

  • Format24bppRgb
  • Format32bppRgb
  • Format32bppArgb
  • Format32bppPArgb

の違いによって違うか?

また、表示する画像データが24bitのカラー画像と32bitのカラー画像で違いが出るか?を検証してみました。

 

 

表示に用いた画像サイズは1024×1024画素で24bitと32bitのカラー画像となります。

この画像を1000回表示した時の表示時間から1秒あたりに表示できた画像枚数(フレームレート[fps])を算出し、この値を5回計測し平均したものが以下の結果となります。

 

24bitカラー画像 32bitカラー画像
Format24bppRgb 139.7 142.3
Format32bppRgb 140.3 150.1
Format32bppArgb 123.6 126.6
Format32bppPArgb 182.0 187.8

 

これを見ると、32bitカラー画像の方が少し表示速度が速く、Graphicsオブジェクトに用いたPixelFormatは Format32bppPArgb が少し突出して速い事が分かりました。

 

この Format32bppPArgb とは何か?調べてみると

 

1 ピクセルあたり 32 ビットの形式であることを指定します。つまり、アルファ、赤、緑、および青のコンポーネントに、それぞれ 8 ビットを使用します。 アルファ コンポーネントに応じて、赤、緑、および青のコンポーネントが事前乗算されます。

 

とのこと。

いまいち理解できませんが、事前乗算している事で速いのか??

 

という事で、FromImageメソッドで使うGraphicsオブジェクトは Format32bppPArgb を使うと良さそうです。

 

少し前(32bitOSがメインだった時代)では、搭載できるメモリサイズは2GBだし、実質的にプログラムが使用できるのは1GB程度しか無かったため、メモリサイズはできるだけ節約したかったため、カラー画像と言えば24bitと思っていたのですが、今では64bitOSが当たり前で、メモリも4~8GBぐらいは普通に搭載されているので、32bitカラーデータもありですね。

 

画像処理のためのC#テクニックへ戻る

画像処理100本ノックをC#でやってみた

画像処理100本ノック

https://github.com/yoyoyo-yo/Gasyori100knock

https://qiita.com/yoyoyo_/items/2ef53f47f87dcf5d1e14

 

というページがあるのをご存じでしょうか?

最近の画像処理と言えば、OpenCVをPythonでやってみた!

という情報にあふれているのですが、この画像処理100本ノックでは、画像処理の処理部分はOpenCVに頼らずにベタにPythonやC++でプログラムしています。

 

実際、今どきは画像処理のプログラムをベタに組んだところで、車輪の再開発になるだけで意味があるのか??という意見もあろうかと思いますが、1度はやっておくと画像処理の理解が深まるので、おススメです。

 

そこで私も画像処理100本ノックの問題をC#で作ってみました。

ソースコードはGitHubに置いてあります。

https://github.com/ImagingSolution/ImagingDotNet

 

表向きは画像処理100本ノックに沿ってプログラムしていますが、目指すところはC#だけで、そこそこ処理が速くて、OpenCVのPython版のように簡単に使えるC#の画像処理ライブラリ(ImagingDotNet)を作るのが目標です。

本気で使うとなると、エラー処理は甘いし、処理もすごく速い訳ではないので、あくまでも、C#で作る画像処理プログラムのお勉強用です。

 

Q.1.チャンネル入れ替え

var src = new Bitmap("Lena.bmp");
var dst = ImagingDotNet.CvtColor(src, ImagingDotNet.COLOR_BGR2RGB);
入力 出力

C# >> CvtColor.cs cvt_COLOR_BGR2RGB部分

 

Q.2.グレースケール化

var src = new Bitmap("Lena.bmp");
var dst = ImagingDotNet.CvtColor(src, ImagingDotNet.COLOR_BGR2GRAY);
入力 出力

C# >> CvtColor.cs cvt_COLOR_BGR2GRAY部分

 

Q.3.二値化

(参考)https://imagingsolution.net/imaging/binarization/

var src = new Bitmap("Lena.bmp");
var dst = (Bitmap)src.Clone();
ImagingDotNet.Threshold(src, dst, 128, 255, ImagingDotNet.THRESH_BINARY);
入力 出力

C# >> Threshold.cs

 

Q.4.大津の二値化

(参考)https://imagingsolution.net/imaging/discriminant-analysis-method/

var src = new Bitmap("Lena.bmp");
var gray = ImagingDotNet.CvtColor(src, ImagingDotNet.COLOR_BGR2GRAY);
var dst = (Bitmap)src.Clone();
ImagingDotNet.Threshold(gray, dst, 128, 255, ImagingDotNet.THRESH_OTSU);
入力 出力

C# >> Threshold.cs

 

 

4問やったところで、力尽きました…

100問分、問題を考えてプログラムを作成した100本ノックの著者は、ほんとスゴイ!!

 

画像処理のためのC#テクニック へ戻る

【C#】画像の輝度値の取得設定速度の比較

C#で画像の輝度値を取得/設定を行う場合は、GetPixel、SetPixelメソッドを使うと遅いのでLockBits~UnlockBitsでポインタをむき出して輝度値の取得/設定を行うのが定番となっていますが、自分自身でGetPixel、SetPixelメソッドを使った事が無かったので、いくつかの方法で処理時間を比較してみました。

 

【評価環境】

OS Windows10 64bit ver.1809
CPU Intel Core i7-7700K 4.2GHz
メモリ 32GB
.NET Framework 4.5.2
プラットフォーム Any CPU(32ビットを優先)
評価画像 jpeg 6000×4000画素 24bitカラー

です。

 

評価プログラムとしては、カラー画像の輝度値をR,G,Bそれぞれ取得し、値を明暗反転して輝度値を入れなおすプログラムを作成しています。

 

処理結果は、以下のようにポジ→ネガ→ポジ→ネガと繰り返すようになっています。

 

処理時間は処理を5回行い平均した時間となります。

 

 

GetPixel、SetPixelを使った方法

平均処理時間は27439msec

本当に遅かった。。

しかもGetPixel、SetPixelではモノクロ画像(Format8bppIndexed)に対応していないので、モノクロ画像も多く扱う画像処理には不向きとなります。

 

LockBits~UnlockBitsでポインタ(Scan0)を取得し、データを配列にコピーしてから処理を行い、結果を元にコピーしなおす方法

平均処理時間は101msec

さすがにGetPixel、SetPixelよりはぜんぜん速いです。

 

MarshalクラスのReadByte、WriteByteメソッドでポインタ(Scan0)に直接、値を読み書きする方法

平均処理時間は273.6msec

この方法に少し期待していたのですが、配列にコピーした方が速かった。。

 

unsafeを使ったポインタ(Scan0)を直接読み書きする方法

平均処理時間は25.4msec

やっぱりunsafeはできれば使いたくないのですが、ここまで速いと使うのもあり??

ポインタを使うなら、C言語のライブラリにしておきたい気もしますが、C#だけで完結できるのもちょっと惹かれます。

 

ポインタで読み書きし、Parallel.Forをつかって処理を並列化する方法

平均処理時間は11.4msec

もう、unsafeは使うしかないでしょ!というレベルですが、やっぱり抵抗がある。。

 

処理を呼び出す側のプログラム

 

まとめ

それぞれの処理時間をまとめると以下の通りでした。

 

方法 処理時間(msec)
GetPixel、SetPixel 27439
LockBits、UnlockBitsで配列を介して処理 101
MarshalクラスのReadByte、WriteByte 273.6
unsafeのポインタで参照 25.4
unsafeのポインタの並列処理 11.4

 

結局GetPixel、SetPixelは論外でした。

あとは好みというか、ポリシーというか、意見の分かれるところだと思いますが、ポインタで処理するのは、やはり魅力です。

 

(2019.11.8追記)

.NET Framework4.5.232ビットを優先で評価を行っていたので、この部分を変えると処理速度に差が出るか?確認しました。

 

.NET Framework 4.7.2  32ビットを優先

方法 処理時間(msec)
GetPixel、SetPixel 27293.4
LockBits、UnlockBitsで配列を介して処理 101.8
MarshalクラスのReadByte、WriteByte 290.8
unsafeのポインタで参照 24.4
unsafeのポインタの並列処理 9.8

 

.NET Framework 4.7.2  32ビットを優先なし

方法 処理時間(msec)
GetPixel、SetPixel 21281.2
LockBits、UnlockBitsで配列を介して処理 85.4
MarshalクラスのReadByte、WriteByte 213.6
unsafeのポインタで参照 27.2
unsafeのポインタの並列処理 9.2

 

となりました。

ということで、.NET Frameworkの4.5.2と4.7.2の差はほとんどありませんでしたが、「32ビットを優先」のあるなしでは優先しない(64bitで動作)方が少し速い結果となりました。

 

関連記事

【C#】Bitmap画像データのメモリ構造

 

画像処理のためのC#テクニックへ戻る

【C#】アフィン変換を用いて画像ビューアを作ろう!

C#にはアフィン変換に用いる行列用の

Matrixクラス(名前空間:System.Drawing.Drawing2D)

というクラスがあり、これを駆使すると、以下のようなプログラムを比較的簡単に作成することができます。

 

 

プログラムの実行ファイルはこちらより→ AffineImage101.zip

ソースコードはGitHubで公開しています。

https://github.com/ImagingSolution/AffineImage

 

プログラムの機能としては

  • 画像ファイルのDrag&Dropで画像をピクチャボックス全体に表示
  • マウスのドラッグで表示画像の移動
  • マウスホイールの回転でマウスポインタを中心に画像の拡大縮小
  • マウスホイールの回転 + Shiftキー でマウスポインタを中心に画像の回転
  • マウスのダブルクリックで画像をピクチャボックス全体に表示
  • ステータスバーにマウスポインタのピクチャボックス座標と、画像上の座標を表示

となります。

 

ここで用いていたMatrixクラスの主なメソッドは

Translate 平行移動
Scale 拡大縮小
RotateAt 指定した点を中心に回転
Reset リセット、単位行列の代入
Invert 逆行列を求める
TransformPoints 点の配列をアフィン変換する

となります。

ただ、これだけだと少し物足りないので、以下のメソッドを拡張メソッドとして追加しています。

ScaleAt 指定した点を中心に拡大縮小
ZoomFit 画像をピクチャボックスの大きさに合わせる

 

以下、ソースコードの解説です。

 

アフィン変換行列に基づいて画像の表示

DrawImageメソッド(名前空間:System.Drawing)には、描画する画像の領域と、描画先の座標を画像の左上、右上、左下の順でPointFの配列で指定することで、画像を任意場所や大きさで表示することが可能となります。

この描画先の座標をアフィン変換行列で求めて画像を表示しています。


public static void DrawImage(this Graphics g, Bitmap bmp, System.Drawing.Drawing2D.Matrix mat)
{
    if ((g == null) || (bmp == null))return;

    // 画像の画素の外側の領域
    var srcRect = new RectangleF(
            -0.5f,
            -0.5f,
            bmp.Width,
            bmp.Height
        );

    // 画像の左上、右上、左下の座標
    var points = new PointF[] {
            new PointF(srcRect.Left, srcRect.Top),  // 左上
            new PointF(srcRect.Right, srcRect.Top), // 右上
            new PointF(srcRect.Left, srcRect.Bottom)// 左下
    };

    // アフィン変換で描画先の座標に変換する
    mat.TransformPoints(points);

    // 描画
    g.DrawImage(
        bmp,
        points,
        srcRect,
        GraphicsUnit.Pixel
        );
}

 

ただし、画像の左上の座標は(-0.5, -0.5)となるのでご注意下さい。

(参考)

【C#】画像の座標系

 

 

指定した点を中心に拡大縮小

アフィン変換で行う拡大縮小の変換行列は座標の原点を中心に拡大縮小するため、単に拡大縮小のアフィン変換行列を掛けただけでは、意図しない拡大縮小となります。

そこで、アフィン変換を組み合わせて、

中心座標を原点へ移動→拡大縮小→原点から元の位置へ移動

の変換を行うと、指定して点中心の拡大縮小となります。

 


public static void ScaleAt(this System.Drawing.Drawing2D.Matrix mat, float scale, PointF center)
{
    // 原点へ移動
    mat.Translate(-center.X, -center.Y, System.Drawing.Drawing2D.MatrixOrder.Append);

    // 拡大縮小
    mat.Scale(scale, scale, System.Drawing.Drawing2D.MatrixOrder.Append);

    // 元へ戻す
    mat.Translate(center.X, center.Y, System.Drawing.Drawing2D.MatrixOrder.Append);

}

(参考)

任意点周りの回転移動(アフィン変換)

 

画像をピクチャボックスに合わせて表示する

ここでのアフィン変換の考え方は、まず、画像の左上の座標をピクチャボックスの原点に合わせるため、(+0.5, +0.5)だけ平行移動(Translate)します。

↓ (+0.5, +0.5)だけ平行移動

 

次に、画像の縦横比とピクチャボックスの縦横比を比較して、画像が縦長の場合、画像の縦の長さとピクチャボックスの縦の長さが揃うように画像を拡大縮小(Scale)します。

画像が横長の場合は、横の長さが合うように画像を拡大縮小(Scale)します。

最後に、画像がピクチャボックスの中心に来るように平行移動(Translate)します。


public static void ZoomFit(this System.Drawing.Drawing2D.Matrix mat, PictureBox pic, Bitmap bmp)
{
    if (bmp == null) return;

    // アフィン変換行列の初期化(単位行列へ)
    mat.Reset();

    // 0.5画素分移動
    mat.Translate(0.5f, 0.5f, System.Drawing.Drawing2D.MatrixOrder.Append);

    int srcWidth = bmp.Width;
    int srcHeight = bmp.Height;
    int dstWidth = pic.Width;
    int dstHeight = pic.Height;

    float scale;

    // 縦に合わせるか?横に合わせるか?
    if (srcHeight * dstWidth > dstHeight * srcWidth)
    {
        // ピクチャボックスの縦方法に画像表示を合わせる場合
        scale = dstHeight / (float)srcHeight;
        mat.Scale(scale, scale, System.Drawing.Drawing2D.MatrixOrder.Append);
        // 中央へ平行移動
        mat.Translate((dstWidth - srcWidth * scale) / 2f, 0f, System.Drawing.Drawing2D.MatrixOrder.Append);
    }
    else
    {
        // ピクチャボックスの横方法に画像表示を合わせる場合
        scale = dstWidth / (float)srcWidth;
        mat.Scale(scale, scale, System.Drawing.Drawing2D.MatrixOrder.Append);
        // 中央へ平行移動
        mat.Translate(0f, (dstHeight - srcHeight * scale) / 2f, System.Drawing.Drawing2D.MatrixOrder.Append);
    }
}

 

ピクチャボックスの座標から、元の画像の座標を求める

これまで説明してきたアフィン変換の行列は、画像の座標をピクチャボックスの座標へ変換するためのアフィン変換行列を求めています。

それでは、その逆のピクチャボックスの座標を元の画像の座標へ変換するには、アフィン変換行列の逆行列を求めて座標変換することで、元の画像を求める事ができます。

この処理をサンプルプログラムでは、MouseMoveイベントで行っています。

その一部抜粋です。


private void picImage_MouseMove(object sender, MouseEventArgs e)
{
    ////////////////////////////
    // マウスポインタの座標と画像の座標を表示する

    // マウスポインタの座標
    lblMousePointer.Text = $"Mouse {e.Location}";

    // アフィン変換行列(画像座標→ピクチャボックス座標)の逆行列(ピクチャボックス座標→画像座標)を求める
    // Invertで元の行列が上書きされるため、Cloneを取ってから逆行列
    var invert = _matAffine.Clone();
    invert.Invert();

    var pf = new PointF[] { e.Location };

    // ピクチャボックス座標を画像座標に変換する
    invert.TransformPoints(pf);

    // 画像の座標
    lblImagePointer.Text = $"Image {pf[0]}";

}

(参考)

【C#】アフィン変換の相互座標変換

【C#.NET】マイクロソフト仕様のアフィン変換

 

以上、如何だったでしょうか?

「アフィン変換」と言うと、「えっ?行列の計算??」「なんか難しそう」と言われて何かと敬遠されがちなのですが、アフィン変換に少し慣れると、計算も比較的スッキリとするかと思います。

私からすると、ここでやっている計算を幾何学的に求めようとすると、そっちの方がはるかに大変。。

 

画像をピクチャボックスに合わせて表示するで紹介しているようにアフィン変換行列を一発で求めようとするのではなく、変換前の状態から変換後の状態へと変換するまでの手順を考え、その手順を行列で表して掛けていく部分がポイントとなります。

アフィン変換の行列の計算部分では、画像そのものを拡大縮小/平行移動している訳ではなく、たかだか3x3の行列の積を計算しているだけなので、計算量としても少なくて済みます。

 

画像処理のためのC#テクニックへ戻る

C#から使える画像処理ライブラリ

画像処理のプログラムでは、当然ながら画像の表示や、操作するボタンなどが欲しくなるので、GUIのプログラム作成が簡単なC#が割とよく用いられています。

しかし、画像処理そのものをC#でやるには処理速度に不満もあるので、GUIはC#、画像処理部はC言語で処理が書かれた画像処理ライブラリを用いる事となります。

 

そこで、C#から使える画像処理ライブラリにはどんなものがあるのか?紹介したいと思います。

 

個人で使うなら

ほぼ、OpenCvSharpで決まりだと思います。

USBカメラも使えるし、バージョンアップもされているし、作者は日本人なので。。

OpenCVをラップしているので、画像処理時間も普通にプログラムを組むよりは圧倒的に速いかと思います。

 

インストール方法については、GitHubに少し書かれていますが、NuGetからインストールするのが基本のよう。

詳細は「OpenCvSharp インストール」で検索してみて、新しめの記事を参照して頂ければわかると思います。

 

マニュアル

https://shimat.github.io/opencvsharp_docs/index.html

GitHub

https://github.com/shimat/opencvsharp

 

会社で使うなら

「会社で使う」というか、使用するカメラが工業用のカメラを使うなら、以下の2つの画像処理ライブラリが、よく使われているように感じます。

 

●Cognex Vision Pro

●MVTec HALCON

 

両方とも数十万円するので、個人で買えるようなレベルではありませんが、OpenCVと何か違うか?というと、私が思うにはテンプレートマッチングの性能と工業用のカメラに対応している部分だと思います。

 

テンプレートマッチングについては、OpenCVのテンプレートマッチングでは回転やスケール変動には対応していませんが、これらのライブラリは対応しています。

さらには、袋の表面に印刷された画像のように、画像が歪んだ状態でもマッチングしてくれる機能もあります。OpenCVにも特徴点ベースのマッチングはありますが、やっぱりこっちの方が良いかと思います。

 

工業用のカメラを使うと何がいいか?という部分についてですが、カメラの画素数やフレームレートなどもそうですが、一番大きいのは同期撮影が出来る点だと思います。

例えば、ベルトコンベア上を流れる製品を撮影するような場面では、被写体は毎回画像の中央で撮影したくなりますが、カメラのフレーム単位で撮影タイミングを制御できないと、毎回撮影位置がばらついてしまいます。

 

OpenCVからは工業用のカメラを制御することは、ほとんどの場合、出来ないのですが、カメラ専用のSDK(もしくは画像入力ボードのSDK)を用いて画像を撮影し、撮影したデータをOpenCVに渡して画像処理を行う事も可能なので、やっぱり、一番差が大きいのはテンプレートマッチングの性能でしょうかね。

 

ちなみに、マシンビジョン業界でPythonが使われる事はほとんどありません。(聞いた事がありません。)

Pythonが出来ない私。。

 

画像処理のためのC#へ戻る

【C#】領域(Rectangle)全体を大きくする、小さくする

Rectangle構造体であらわされた領域全体を左右方向、上下方向に大きく/小さくするには

Inflateメソッドを用います。

 

コード例

private void Form1_Paint(object sender, PaintEventArgs e)
{
    // 元の領域1
    var rectSrc1 = new Rectangle(20, 20, 50, 30);
    e.Graphics.DrawRectangle(Pens.Black, rectSrc1);

    // 領域を大きくする
    // Rectangleを左右それぞれ3画素、上下それぞれ6画素大きくする
    rectSrc1.Inflate(3, 6);
    e.Graphics.DrawRectangle(Pens.Red, rectSrc1);

    // 元の領域2
    var rectSrc2 = new Rectangle(120, 20, 50, 30);
    e.Graphics.DrawRectangle(Pens.Black, rectSrc2);

    // 領域を小さくする
    // Rectangleを左右それぞれ6画素、上下それぞれ3画素小さくする
    rectSrc2.Inflate(-6, -3);
    e.Graphics.DrawRectangle(Pens.Red, rectSrc2);
}

 

実行結果

 

画像処理のためのC#へ戻る

【C#】RectangleRectangleFの相互変換

あまりやる事は無いのですが、Rectangle(名前空間:System.Drawing)とRectangleFの相互変換について調べてみました。

RectangleからRectangleFへ変換

これに関しては、型は変わるものの、値そのものは変わらないので、ほぼ、代入するようなノリで以下のようにすると変換ができます。

var rect = new Rectangle(20, 10, 60, 40);
RectangleF rectF = rect;

RectangleFからRectangleへ変換

こちらは値を小数から整数へ切り詰める必要があるので、値を切り捨てや四捨五入する必要が出てきますがRectangle構造体には値を切り捨てる(intでキャストする)Truncateメソッド、切り上げを行うCeilingメソッド、四捨五入を行うRoundメソッドが用意されています。

ここでいうですが、RectangleF構造体のX, Y, Width, Heightプロパティに関して切り捨て、切り上げ、四捨五入が行われます。

 

プログラム例

var rectF = new RectangleF(0.5f, -1.5f, 10.5f, 5.5f);

Rectangle rect;
rect = Rectangle.Round(rectF);      // 四捨五入
rect = Rectangle.Truncate(rectF);   // 切り捨て(intでキャスト)
rect = Rectangle.Ceiling(rectF);    // 切り上げ

結果は

X Y Width Height
オリジナル 0.5 -1.5 10.5 5.2
Round 0 -2 10 5
Truncate 0 -1 10 5
Ceiling 1 -1 11 6

となります。

ここで気になるポイントとしては、Round(四捨五入)でX座標の0.5が0になってしまっています。(期待しているのは1)

 

この四捨五入に関しては

【C#】四捨五入

のページでも書いていますが、座標に関する値の四捨五入は、私は

int x, y;
y = (int)Math.Floor(x + 0.5);

のように書くようにしています。

 

以上のことから、RectangleFからRectangleへ変換するには

var rectF = new RectangleF(0.5f, -1.5f, 10.5f, 5.5f);
var rect = new Rectangle(
        (int)Math.Floor(rectF.X + 0.5),
        (int)Math.Floor(rectF.Y + 0.5),
        (int)Math.Floor(rectF.Width + 0.5),
        (int)Math.Floor(rectF.Height + 0.5)
    );

のように書くようにしています。

このときの結果は

X Y Width Height
オリジナル 0.5 -1.5 10.5 5.2
変換後 1 -1 11 5

となります。

 

せっかくRectangle構造体にRoundメソッドが用意されているのに、いまいち使えず、ベタに書いた方が良いようです。。

 

【参考ページ】

●.NET Frameworkのソースコード(Rectangle構造体)

https://referencesource.microsoft.com/#System.Drawing/commonui/System/Drawing/Rectangle.cs

●Microsoft Docs Rectangle構造体

https://docs.microsoft.com/ja-jp/dotnet/api/system.drawing.rectangle

 

画像処理のためのC#へ戻る

画像の拡大

例えば、下図のように2x2画素の画像を4x4の画像に拡大する場合、アフィン変換を使えばいいんでしょ!と、安易に考えていると、思わぬ落とし穴があったりもします。

大事なポイントとして、

●画像の座標の原点は左上の画素の中心が原点(0.0、0.0)となる。(例外もあります)

●アフィン変換の拡大縮小は原点を基準として拡大縮小される。

 

となります。

これを気にせず、ただ、アフィン変換で画像を2倍に拡大すると、左上の画素の中心を基点に画像が拡大されます。

これで、一見良さそうにも感じるのですが、拡大後の画像において、画素の中心が原点であることから、4x4画素の領域は下図の四角で示した領域であり、画像全体が左上に0.5画素ズレた状態になっていまいます。

 

 

アフィン変換で画像を拡大する時の変換前と変換後の状態は、以下のようになるのが正解です。

 

この変換をアフィン変換で実現するには以下のように行います。

 

変換前の状態

 

①画像全体を右下に(+0.5,+0.5)画素移動
$$\begin{pmatrix} 1 & 0 & 0.5 \\ 0 & 1 & 0.5 \\ 0 & 0 & 1 \end{pmatrix}$$
 

②画像を2倍に拡大

$$\begin{pmatrix} 2 & 0 & 0 \\ 0 & 2 & 0 \\ 0 & 0 & 1 \end{pmatrix}$$
 

③画像全体を左上に(-0.5,-0.5)画素移動

$$\begin{pmatrix} 1 & 0 & -0.5 \\ 0 & 1 & -0.5 \\ 0 & 0 & 1 \end{pmatrix}$$

 

となります。

この一連の変換をアフィン変換行列であらわすと

$$\begin{pmatrix} { x }^{ ‘ } \\ { y }^{ ‘ } \\ 1 \end{pmatrix}=\begin{pmatrix} 1 & 0 & -0.5 \\ 0 & 1 & -0.5 \\ 0 & 0 & 1 \end{pmatrix}\begin{pmatrix} 2 & 0 & 0 \\ 0 & 2 & 0 \\ 0 & 0 & 1 \end{pmatrix}\begin{pmatrix} 1 & 0 & 0.5 \\ 0 & 1 & 0.5 \\ 0 & 0 & 1 \end{pmatrix}\begin{pmatrix} x \\ y \\ 1 \end{pmatrix}\\ \\ $$

$$\begin{pmatrix} { x }^{ ‘ } \\ { y }^{ ‘ } \\ 1 \end{pmatrix}=\begin{pmatrix} 2 & 0 & 0.5 \\ 0 & 2 & 0.5 \\ 0 & 0 & 1 \end{pmatrix}\begin{pmatrix} x \\ y \\ 1 \end{pmatrix}\\ \\ $$

 

となり、単に拡大のアフィン変換行列だけを掛ければOKでは無いことが分かります。

 

ちなみに、C#で2x2画素の画像をPictureBoxのSizeModeプロパティをZoomにして、ImageプロパティにBitmapを設定すると、このようになります。

 

なんとなく、画像が左上にズレているようで、なんか怪しい!!

 

【関連記事】

【C#】画像の座標系

アフィン変換(平行移動、拡大縮小、回転、スキュー行列)

画素の補間(Nearest neighbor,Bilinear,Bicubic)の計算方法

画像の回転

 

画像処理のためのC#へ戻る

Intel RealSense D435を使って腕の血管撮影

腕の血管は近赤外線のカメラで撮影すると見えるそうなのですが、近赤外線のカメラはかなり高価な物が多くてなかなか手が出せません。。

 

血管を見る専用の機材もあるのですが、100万円ぐらいからと、こちらもかなり高額です。

(参考)静脈が見える!「血管可視化装置」の使い勝手は?

https://tech.nikkeibp.co.jp/dm/atcl/feature/15/030200065/071800017/?ST=health

 

しかし、よくよく考えたら、私も以前購入したIntel RealSenseのD435を持っているので、これで血管が撮影できないか?試してみました。

 

ここの資料

CIGS イメージセンサを用いた静脈血管の可視化に関する研究

https://www.ce.nihon-u.ac.jp/researchcenter/biomedical_engineering/pdf/mission2/murayama/h27_2-2.pdf

 

によると、血管をコントラスト良く撮影するには830nmぐらいが良いとのことで、
D435は850nmなので、かなりイケてそう!!

 

ということで、やってみました。

 

やり方はとても簡単。

プロジェクタの前にテープを貼って、近赤外カメラで撮影するだけ。

 

まず、D435のプロジェクタはドットパターンを投影するので、このパターンを消すためにプロジェクタの前にテープを貼りました。

 

使用したのは、光を良く拡散してくれそうな、メンディングテープです。

 

このテープをプロジェクタの部分に貼り付けます。

 

そしてビューアソフトで撮影するだけ。

 

実際に撮影してみた結果がこちら↓

 

血管は見えてはいますが、少しぼんやりとしていたので、画像処理でコントラストを調整したのがこちら↓

 

そこそこ良く見えているとは思いますが、この血管は、点滴や注射を打つときに参考になるかな??

【C#,WPF】多ビット画像の取り扱い

一般的なカメラであれば、モノクロの画像であれば8bit(256諧調)、カラー画像であれば24bit(R,G,Bそれぞれ8bit)の画像が一般的なのですが、マシンビジョン用のカメラでは、1画素あたり10~14bitぐらいまでの諧調を持つカメラがあるのですが、この画像データをプログラム的に取り扱うには、通常のWindows Forms用のC#(Bitmapクラス)では出来ませんでした。。

かなり昔の評価になりますが、評価した結果はこちら↓です

Format16bppGrayScaleは使えるのか?実験してみた

http://imagingsolution.blog.fc2.com/blog-entry-65.html

 

この時に、「WPFなら出来ますよ!」というコメントを頂いていたのを、今更ながらに評価してみました。

 

WPFではSystem.Windows.Media.PixelFormatsクラスにGray16(16ビットのモノクログレースケール)、Bgr101010(R,G,B各10ビットのカラー画像)、Rgb48(R,G,B各16ビットのカラー画像)があり、個人的な使いそうな、この3つについて評価しました。

 

参考までに、Bgr101010のフォーマットでは32ビットの整数型(uint)に下位ビットからB,G,Rの順で輝度値が格納されています。

 

Rgb48のフォーマットは16ビットの整数型(ushort)にカラーデータがR,G,B,R,G,B・・・の順で格納されています。

一般的な24bitのカラー画像はB,G,R,B,G,R・・・の順なので、ここが少しはまりました。

評価方法

配列にカラーグラデーションのデータを格納し、これを画像ファイルに保存し、ファイルを読み込んで元の画像データに復元できるか?という内容で行いました。

画像データを復元したいので、ファイル形式は非圧縮、可逆圧縮であろうbmp,tiff,pngの3つのファイルで行いました。

(評価に用いた画像)

 

評価プログラム

評価に用いたプログラムはこんな感じ↓です。

ボタンをクリックすると、ボタンに示している画像ファイルを作成し、メニューのFile→Openで開くとファイルフォーマットとビット数が表示されるようになっていますが、基本的にデバッグ実行をしながら、各種パラメータをウォッチで見ながら確認を行いました。

 

評価に用いたプログラムはGitHubに置いておきましたので、もしよかったら評価してみてください。

https://github.com/ImagingSolution/ImageFileArrayConverter

 

結果

各ボタンをクリックして、エクスプローラでファイルのプロパティを確認したのが、以下の通りです。

 

保存 読み込み
bmp tif png bmp tif png
Gray16 ×

64bit

Bgr101010 ×

64bit

× ×
Rgb48 ×

64bit

表中の横棒(―)はファイル保存の時点でダメなので、ファイル読み込みの評価は行っておりません。

 

Bgr101010は全滅

Gray16はtifとpngファイルはOK

Rgb48もtifとpngファイルはOK

 

という結果になりました。

ただし、Gray16やRgb48では16ビット全体を輝度データとして用いる事は少なく、16ビット中の下位10ビット、12ビットなどに値を入れている事が多いのですが、この16ビット中の何ビットが有効か?を設定するのがSystem.Windows.Media.PixelFormat構造体のMasksプロパティっぽいのですが、このMasksプロパティは読み取り専用で設定が出来ない!

そのため、データとして、16ビット全体が有効(65536諧調もっている)であればいいのですが、そうでない場合、結局、どうしたらよいのか?分かりませんでした。。

 

ちなみにGray16、Bgr101010に関しては、有効ビットを設定するのにWin32APIのビットフィールドという機能を用いれば設定が可能です。

(参考)多ビット(10Bit、12Bit)画像データの表示、フォーマット

https://imagingsolution.net/imaging/imaging-programing/10bit-image-format/

 

※まだ、WPFを理解しきれていない部分が多いので、間違い等ありましたら、コメント頂けると幸いです。

【C#】画像の輝度値の取得

C#で画像ファイルの輝度値を取得するにはLockBits~UnlockBitsでポインタを取得して配列へコピーするなどするのが、C#では定番となっています。

例えば、下記のようなサンプルコード

var bmp = new Bitmap("01.png"); 

// Bitmapをロック 
var bmpData = bmp.LockBits( 
	new Rectangle(0, 0, bmp.Width, bmp.Height), 
	System.Drawing.Imaging.ImageLockMode.ReadWrite, 
	bmp.PixelFormat 
	); 

// メモリの幅のバイト数を取得 
var stride = Math.Abs(bmpData.Stride); 

// 画像データ格納用配列 
var data = new byte[stride * bmpData.Height]; 

// Bitmapデータを配列へコピー 
System.Runtime.InteropServices.Marshal.Copy( 
	bmpData.Scan0, 
	data, 
	0, 
	stride * bmpData.Height 
	);

// アンロック 
bmp.UnlockBits(bmpData);

このプログラムで、ほとんどの場合、問題ない事が多いのですが、モノクロ8Bitのpng画像ファイルを開くと、なぜか?32bitカラーの画像として認識してしまいます。

(評価に用いた画像) Deep Learningでは定番のMNISTの手書き文字に似せて作成した8bitモノクロ画像

(デバッグ画面)もとの画像は8bitモノクロ画像ですが、PixelFormatがFormat32bppArgbになっています。

 

8bitの画像が32bitとして認識してしまうという事は、無駄に画像データが4倍大きくなってしまうので、画像処理的には処理速度的にも不都合が起こります。

 

そこで、私は基本WindowsFormsを使っているのですが、クラスだけWPFのクラスを使う事で問題を解決します。

 

WPFのクラスを用いるにはプロジェクトの参照から

下記の2つを追加します。

PresentationCore
WindowsBase

 

これでWPFのクラスが使えるようになります。
そこで、最初に書いたBitmapクラスを用いたプログラムと同等のことをWPFのクラスを用いて作成すると

byte[] data; 

using (var fs = new System.IO.FileStream("01.png", System.IO.FileMode.Open, System.IO.FileAccess.Read)) 
{ 
	var bitmapFrame = System.Windows.Media.Imaging.BitmapFrame.Create( 
		fs, 
		System.Windows.Media.Imaging.BitmapCreateOptions.PreservePixelFormat, 
		System.Windows.Media.Imaging.BitmapCacheOption.Default 
		); 

	int stride = ((bitmapFrame.PixelWidth * bitmapFrame.Format.BitsPerPixel + 31) / 32) * 4; 

	// 画像データ格納用配列 
	data = new byte[stride * bitmapFrame.PixelHeight]; 
	
	// 輝度データを配列へコピー 
	bitmapFrame.CopyPixels(data, stride, 0); 
}

となります。

これでFormatもGray8となり、正しいサイズで画像データを取得できるようになります。

 

画像処理のためのC#テクニックへ戻る

関連記事

【C#】画像の輝度値の取得設定速度の比較

【C#】アフィン変換の相互座標変換

アフィン変換を用いて画像を拡大、縮小、回転などを行ってピクチャボックスへが画像の表示を行うと、逆にピクチャボックス上の座標から、元の画像の座標を知りたくなる場合がありますが、画像の表示をアフィン変換行列を用いて表示すると、意外と簡単に求まります。

 

 

画像上の座標を(x  y)、コントロール上の座標を(x’  y’)、アフィン変換の行列をMatとすると

となります。 マイクロソフト仕様の表示です。

上記の式の右側からアフィン変換の行列の逆行列を掛けると

 

 

となり、元の座標(元画像上の座標)を求める事ができます。

 

これをC#でプログラムすると、

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;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        // アフィン変換行列
        System.Drawing.Drawing2D.Matrix _matAffine;

        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            // アフィン変換行列
            _matAffine = new System.Drawing.Drawing2D.Matrix();

            // 30°回転
            _matAffine.Rotate(30, System.Drawing.Drawing2D.MatrixOrder.Append);
            // 20倍
            _matAffine.Scale(20.0f, 20.0f, System.Drawing.Drawing2D.MatrixOrder.Append);
            // 平行移動
            _matAffine.Translate(-4000f, -2300f, System.Drawing.Drawing2D.MatrixOrder.Append);

            pictureBox1.Image = new Bitmap(pictureBox1.Width, pictureBox1.Height);

            using (var bmp = new Bitmap(@"Mandrill.bmp"))
            using (var g = Graphics.FromImage(pictureBox1.Image))
            {
                g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;

                DrawImageLocal(g, _matAffine, bmp);
            }
        }

        private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
        {
            // マウスポインタの座標(コントロールの座標)
            lblSrc.Text = String.Format("(x', y') = ({0}, {1})", e.X, e.Y);

            using (var matInvert = _matAffine.Clone())
            {
                // アフィン変換行列の逆行列を求める
                matInvert.Invert();

                // コントロールの座標
                var points = new PointF[]
                    {
                        new PointF(e.X, e.Y)
                    };

                // 元の座標(画像上の座標)を求める
                matInvert.TransformPoints(points);

                lblDst.Text = String.Format("(x, y) = ({0:#.#}, {1:0.#})", points[0].X, points[0].Y);
            }
        }

        /// <summary>
        /// アフィン変換行列に基づき画像を表示する
        /// </summary>
        /// <param name="g">グラフィックスオブジェクト</param>
        /// <param name="mat">アフィン変換行列</param>
        /// <param name="bmp">表示するBitmapオブジェクト</param>
        private void DrawImageLocal(
            Graphics g,
            System.Drawing.Drawing2D.Matrix mat,
            Bitmap bmp)
        {
            if (bmp == null) return;

                // 描画元の領域
                var srcRect = new RectangleF(-0.5f, -0.5f, bmp.Width, bmp.Height);

                // 描画先の座標(描画元に合わせる、左上、右上、左下の順)
                var points = new PointF[]
                {
                    new PointF(srcRect.Left, srcRect.Top),
                    new PointF(srcRect.Right, srcRect.Top),
                    new PointF(srcRect.Left, srcRect.Bottom),
                };
                // 描画先の座標をアフィン変換で求める(変換後の座標は上書きされる)
                mat.TransformPoints(points);

                // 描画
                g.DrawImage(
                    bmp,
                    points,
                    srcRect,
                    GraphicsUnit.Pixel
                );
        }
    }
}

(実行結果)

 

行列に慣れていないと、アフィン変換って、なんか面倒くさい!!

って言われる事もあるのですが、上記のような 画像座標 ⇔ コントロール座標 の相互変換を行う場合は、アフィン変換を用いると、いかに簡単に求まるか?を実感して頂けたでしょうか?

 

より詳細なサンプルプログラムはこちらのページ↓で公開しています。

【C#】アフィン変換を用いて画像ビューアを作ろう!

 

画像処理のためのC#テクニックへ戻る

【C#】Bitmapの解像度(DPI)の取得設定

画像ファイルのプロパティをエクスプローラーで表示すると、下図のように水平方向の解像度、垂直方向の解像度が表示されますが、ほとんどの場合、Windowsでは96dpiと表示される場合が多いのですが、この解像度の値をC#で取得、設定(変更)してみたいと思います。

 

 

解像度を設定するにはBitmapクラスのSetResolutionメソッドを用いて、以下のように設定します。

var bmp = new Bitmap("Mandrill.bmp");
bmp.SetResolution(400, 400);

解像度を取得するにはGetResolutionではなくて、HorizontalResolutionプロパティVerticalResolutionプロパティが用意されています。

 

(コード例)

Console.WriteLine(String.Format("水平分解能 = {0}, 垂直分解能 = {1}", bmp.HorizontalResolution, bmp.VerticalResolution));

(実行結果)

 

試しに下記のようなコードで、Bitmap,Jpeg,Png,Tiffファイルで解像度の設定が有効になっているか?確認してみましたが、どれも設定されていました。

var bmp = new Bitmap("Mandrill.bmp");
bmp.Save("Mandrill400.bmp", System.Drawing.Imaging.ImageFormat.Bmp);
bmp.Save("Mandrill400.jpg", System.Drawing.Imaging.ImageFormat.Jpeg);
bmp.Save("Mandrill400.png", System.Drawing.Imaging.ImageFormat.Png);
bmp.Save("Mandrill400.tif", System.Drawing.Imaging.ImageFormat.Tiff);

ただし、Bitmapファイル(*.bmp)だけはファイルのプロパティで解像度が表示されなかったので、エクスプローラーで表示項目を増やして解像度を確認しました。

 

 

画像処理のためのC#テクニックへ戻る

【C#】Bitmapクラスへのポインタ渡し

画像処理のプログラムを作成するときは、GUIはC#、画像処理はC、C++で書かれたライブラリで作成するというのが、私の定番となっているのですが、C言語で処理された画像データをC#へ渡す場合は、画像データのコピーや画像ファイルを介せずとも、画像データのポインタをC#側へ渡す事で画像データを扱うことが可能となります。

 

これを可能にするのには、Bitmapクラスのコンストラクタに

public Bitmap(
    int width,
    int height,
    int stride,
    PixelFormat format,
    IntPtr scan0
)

というのがあるので、scan0の部分にC言語で扱っている画像データのポインタを渡す事で、C#のビットマップオブジェクトとして扱うことが可能となります。

ただし、画像データのメモリ構造はトップダウンで画像1行あたりのサイズが4バイト単位となります。

(参考)

【C#】Bitmap画像データのメモリ構造

 

scan0に渡されたポインタはBitmapクラス内ではメモリを参照するだけ(Bitmapクラス内でメモリを確保していない)なので、BitmapオブジェクトをDisposeするまでscan0で示されたメモリは解放しないように注意が必要です。

逆にBitmapオブジェクトをDisposeしてもメモリは解放されないので、メモリの解放も忘れずに。

 

メモリが参照されているだけかどうか?を評価するのに、下記のようなプログラムを作成してみました。

// 元のBitmap
var bmp1 = new Bitmap(@"C:\Temp\Lenna.bmp");

// ロックしてポインタ参照できるようにする
var bmpData1 = bmp1.LockBits(new Rectangle(0, 0, bmp1.Width, bmp1.Height),
System.Drawing.Imaging.ImageLockMode.ReadWrite, bmp1.PixelFormat);

// ポインタを渡してBitmapクラスを作成
var bmp2 = new Bitmap(
    bmp1.Width,
    bmp1.Height,
    bmpData1.Stride,
    bmp1.PixelFormat,
    bmpData1.Scan0 // bmp1の画像データのポインタ
);

// アンロック
bmp1.UnlockBits(bmpData1);

// ロックしてポインタ参照できるようにする
var bmpData2 = bmp2.LockBits(new Rectangle(0, 0, bmp2.Width, bmp2.Height),
System.Drawing.Imaging.ImageLockMode.ReadWrite, bmp2.PixelFormat);

var data2 = new byte[Math.Abs(bmpData2.Stride) * bmp2.Height];

// bmpData2の輝度値を配列へコピーする
System.Runtime.InteropServices.Marshal.Copy(
    bmpData2.Scan0,
    data2,
    0,
    data2.Length
);

// 画像の輝度値を+50する
for (int i = 0; i < data2.Length; i++)
{
    data2[i] = (byte)Math.Min(data2[i] + 50, 255);
}

// 元のポインタへコピーして、データを戻す
System.Runtime.InteropServices.Marshal.Copy(
    data2,
    0,
    bmpData2.Scan0,
    data2.Length
);

// アンロック
bmp2.UnlockBits(bmpData2);
//
bmp2.Dispose();

// bmp1を保存(※bmp1は直接輝度値の変更はしていない)
bmp1.Save(@"C:\Temp\Lenna1.bmp", System.Drawing.Imaging.ImageFormat.Bmp);
// 解放
bmp1.Dispose();

上記のプログラムではbmp1でBitmapオブジェクトを作成(画像のメモリを確保)し、bmp1の画像データのポインタをbmp2へ渡しています。

この時点でbmp1とbmp2の画像データのメモリは同じをメモリを参照しているはずなので、bmp2の画像の輝度値を変更すると、bmp1の画像も変更されているか?を確認しています。

 

元の画像(bmp1の画像)

 

bmp2を処理した後のbmp1の画像

 

以上のようになり、思惑通りでした!

 

画像処理のためのC#テクニックへ戻る

【C#】Bitmap画像データのメモリ構造

Bitmap画像の輝度値を参照するには、こちらのページ↓

【C#】Bitmap画像の輝度値の参照設定

 

で、Bitmap画像のメモリの値を参照するにはBitmapオブジェクトをLockBitsでロックし、BitmapDataScan0プロパティを参照すれば、良いと書きましたが、このScan0で示されたメモリの構造のお話です。

 

C言語で画像の表示を行った事がある人には、WindowsのDIB(Device Independent Bitmaps)の画像データの構造とほぼ同じで、データの並びがトップダウンになります。

で済むのですが、私自身もC言語で画像の表示をすることは少なくなり、これで通じる人は少なくなった気がします。。

 

簡単に画像データのメモリ構造を説明すると

 

●画像の左上から順にメモリに配置されている(トップダウン)

●画像1行あたりのメモリは4の倍数のバイトになるように調整されている

●カラーの場合、B,G,R,B,G,R,・・・もしくは B,G,R,A,B,G,R,A,・・・の順で並んでいる

 

より具体的に3x3画素の画像を例にとって説明します。

 

上図のように3x3の画像であってもメモリ上は下図のように一列にデータが並んでおり、BitmapDataのSacn0がメモリの先頭アドレスを差しています。

 

上図は8ビットのモノクロ画像の場合ですが、1行あたりのメモリサイズが4の倍数のバイトになるように余計にメモリが追加(上図のグレーの部分)されいます。

 

この1行あたりのメモリのサイズはBitmapDataStrideプロパティで取得できますが、自分で計算すると

 

stride = (width * bitCount + 31) / 32 * 4;

 

という計算で求めることができ、幅が3画素、8ビットの場合は

 

24ビットカラーの場合は、1画素あたり3バイト、メモリを使用し、画像の左上からB,G,R,B,G,R,・・・の順番でメモリに画像の輝度値が格納され

 

のようになっています。

同様に32ビットの場合は、1画素あたり4バイト、メモリを使用し、画像の左上からB,G,R,A,B,G,R,A,・・・の順番でメモリに画像の輝度値が格納され

 

となり、32ビットの場合は1画素あたり4バイトなので、画像の幅の画素数に関係なく余計なメモリが追加されることはありません。

 

画像を表示して斜めに表示される場合は、このStrideあたり(メモリの確保や輝度値の参照方法)がおかしい場合が多いので、確認してみて下さい。

 

参考までに、ビットマップファイル(*.bmp)の画像データ部分は、幅方向については、上記の通りですが、画像の左下から上へ(ボトムアップ)メモリに格納されています。

 

画像処理のためのC#テクニックへ戻る

【C#】Bitmap画像の輝度値の参照設定

画像の輝度値(画素値)を取得/設定するのに、.NETでは SetPixel と GetPixel というメソッドが用意されていますが、処理が遅かったり、モノクロの8ビット画像でSetPixelを実行しようとすると

 

型 ‘System.InvalidOperationException’ のハンドルされていない例外が System.Drawing.dll で発生しました

追加情報:SetPixel は、インデックス付きピクセル形式のイメージに対してサポートされていません。

 

というエラーメッセージが出て、実質的に使い物になりません。

そこで、LockBits~UnlockBitsというメソッドを使い、ビットマップのポインタ(Scan0)から輝度値の値を参照/設定するのが定番となっています。

(unsafeコードではScan0をポインタにキャストして使う事もできます。)

 

以下に3x3の移動平均フィルタ処理を行った例を示します。

var bmp = new Bitmap(@"C:\Temp\Mandrill.bmp");

// Bitmapをロック
System.Drawing.Imaging.BitmapData bmpData =
    bmp.LockBits(
        new Rectangle(0, 0, bmp.Width, bmp.Height),
        System.Drawing.Imaging.ImageLockMode.ReadWrite,
        bmp.PixelFormat
    );

// メモリの幅のバイト数を取得
var stride = Math.Abs(bmpData.Stride);
// チャンネル数を取得(8, 24, 32bitのBitmapのみを想定)
var channel = Bitmap.GetPixelFormatSize(bmp.PixelFormat) / 8;

// 元の画像データ用配列
var srcData = new byte[stride * bmpData.Height];
// 処理後の画像データ用配列
var dstData = new byte[stride * bmpData.Height];

// Bitmapデータを配列へコピー
System.Runtime.InteropServices.Marshal.Copy(
    bmpData.Scan0,
    srcData,
    0,
    srcData.Length
    );

int index;

// 移動平均処理
for (int j = 1; j < bmpData.Height - 1; j++)
{
    for (int i = channel; i < (bmpData.Width - 1) * channel; i++)
    {
        index = i + j * stride;
        dstData[index]
            = (byte)((
                srcData[index - channel - stride] + srcData[index - stride] + srcData[index + channel - stride]
              + srcData[index - channel ] + srcData[index ] + srcData[index + channel ]
              + srcData[index - channel + stride] + srcData[index + stride] + srcData[index + channel + stride]
              ) / 9);
    }
}

// 配列をBitmapデータへコピー
System.Runtime.InteropServices.Marshal.Copy(
    dstData,
    0,
    bmpData.Scan0,
    dstData.Length
);

// アンロック
bmp.UnlockBits(bmpData);

bmp.Save(@"C:\Temp\Mandrill_Smooth.bmp", System.Drawing.Imaging.ImageFormat.Bmp);

(処理結果)

処理前画像 処理後画像

 

上記のコードはBitmapのフォーマットが8ビット(1チャンネル、Format8bppIndexed)、24ビット(3チャンネル、Format24bppRgb)、32ビット(4チャンネル、Format32bppArgb)のみを対象としていますが、OpenCVとかで使われるチャンネルという概念を持ち込む事で、上記のコードだけでモノクロとカラーの両対応となっています。

 

Scan0で示されたメモリに画像データがどのように格納されているか?についでは下記ページを合わせて参照下さい。

【C#】Bitmap画像データのメモリ構造

 

画像処理のためのC#テクニックへ戻る