【C#】配列の中身をメモリで確認する方法

C言語の時は、ポインタの中身をメモリで参照する事が多かったのですが、C#をメインで触るようになってからは、メモリの中身を参照することは無くなってしまい、いざ、やってみようとすると、やり方がわかりづらかったので記事にしました。

まず、Visual Studio にメモリのウィンドウを表示するのですが、ウィンドウ表示メニューがステップ実行中にしか表示してくれないため、メモリを見たい場所でブレークポイントを置いて停止させます。

 

この状態で、Visual Studio のメニューの デバッグ→ウィンドウ→メモリ→メモリ1(1) と選択するとメモリウィンドウが表示されます。

 

 

さらにメモリで表示させたい変数をメモリウィンドウへDrag&Dropすることで、配列の中身をメモリで参照することができます。

 

 

と、本当にやりたかったのは、二次元配列の時に、A[x, y]のように、左側の添え字の方からメモリに値が格納されていくのか? それともA[y, x]のように右側の添え字の方から格納されるのか?を確認したかったのですが、確認のため、

var arr2D = new byte[16, 16];

for (int j = 0; j < 16; j++)
{
    for (int i = 0; i < 16; i++)
    {
        arr2D[j, i] = (byte)((j << 4) + i);
    }
}

のようなソースコードで、8ビット(1バイト)中の下位4ビットに i の値を、上位4ビットに j の値を格納するプログラムをステップ実行で確認したところ、メモリへは、以下のように格納されていました。

 

 

ということで、多次元配列の時、配列の値は右側の添え字の方から格納される事がわかります。

 

こっち↓の方が並びは分かりやすいですね。

 

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#へ戻る

【C#】座標が領域内にあるか?調べる方法

マウスをクリックした時など、任意の座標がある領域の範囲内にあるか?どうか?調べたい場合があります。

 

これを調べるには、四角形の領域の場合、Rectangleクラス(名前空間:System.Drawing)のContainsメソッドを用います。

さらに複雑な形状の領域の場合、GraphicsPath(名前空間:System.Drawing.Drawing2D)のIsVisibleメソッドを用います。

 

四角形領域の場合のコード例

bool insideFlg;

var rect = new Rectangle(2, 2, 8, 8);
for (int i = 0; i < 12; i++)
{
    insideFlg = rect.Contains(i, i);
    System.Diagnostics.Debug.WriteLine($"四角の場合:{i}, {insideFlg}");
}

実行結果

四角の場合:0, False
四角の場合:1, False
四角の場合:2, True
四角の場合:3, True
四角の場合:4, True
四角の場合:5, True
四角の場合:6, True
四角の場合:7, True
四角の場合:8, True
四角の場合:9, True
四角の場合:10, False
四角の場合:11, False

丸領域の場合のコード例

bool insideFlg;

var path = new System.Drawing.Drawing2D.GraphicsPath();
path.AddEllipse(2, 2, 8, 8);
for (int i = 0; i < 12; i++)
{
    insideFlg = path.IsVisible(i, i);
    System.Diagnostics.Debug.WriteLine($"丸の場合:{i}, {insideFlg}");
}

実行結果

丸の場合:0, False
丸の場合:1, False
丸の場合:2, False
丸の場合:3, False
丸の場合:4, True
丸の場合:5, True
丸の場合:6, True
丸の場合:7, True
丸の場合:8, True
丸の場合:9, False
丸の場合:10, False
丸の場合:11, False

 

画像処理のための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#】グラフのメモリ間隔の計算

C#でグラフを書く場合はChartを使うと簡単に書く事ができますが、Chartだけでは物足り無い事や、描画速度が遅かったりするので、自前でグラフを書こうとしたときに、Chartでやっているグラフの軸の間隔(下図の例では20)は、どうようにして決めているのか?
いまいち分からなかったので、調べてみました。

 

 

本当は、ソースコードから、どうやっているのか?見たかったのですが、よく分からなかったので、とりあえず、Chartの動きをよ~く見てみました。

 

下図の例は棒グラフで、値を(1, 1),(2, 2),(3, 3),(4, 4),(5, 5)・・・
とデータを追加した時の様子

 

すると、軸の間隔は

 

0.2、0.5、1、2、5、10、20、50、100、200、500・・・

 

という風に変化している事がわかりました。

結局、値そのものは 1、2、5 の繰り返し+指数で表現できそうだったので、グラフの最大値を浮動小数点で表してみると、仮数の部分の数値で1,2,5のいづれかが決まっているようでした。

 

例えば、125の場合は 1.25x10 なので、仮数は1.25で、この時のグラフの間隔は 0.2x10 の 20 となります。

 

仮数 グラフ間隔
1.5未満 0.2x10n
3.5未満 0.5x10n
5以下 1.0x10n
その他 2.0x10n

 

このグラフの間隔を関数にしたのがこちら↓

/// <summary>
/// グラフ間隔の計算
/// </summary>
/// <param name="value">グラフの最大値</param>
/// <returns></returns>
private double GetAxisStep(double value)
{
    // 指数
    double exponent = Math.Pow(10, Math.Floor(Math.Log10(value)));
    // 仮数
    double significand = value / exponent;

    // グラフ間隔
    double axisStep;

    if (significand < 1.5)
    {
        axisStep = 0.2 * exponent;
    }
    else if (significand < 3.5)
    {
        axisStep = 0.5 * exponent;
    }
    else if (significand <= 5.0)
    {
        axisStep = 1.0 * exponent;
    }
    else
    {
        axisStep = 2.0 * exponent;
    }
    return axisStep;
}

もう少し、綺麗な書き方ありそうですが。。

【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#】正規分布に従う乱数の取得

.NETでは乱数のクラスにRamdomクラスがあります、どれも一様に分布する乱数しか取得できません。

 

例えば、NextDoubleメソッドを用いると、0以上、1.0未満の一様に分布した乱数を取得する事ができます。

var rnd = new Random(); 

for (int i = 0; i < 1000; i++) 
{ 
	Console.WriteLine(rnd.NextDouble().ToString()); 
}

上記プログラムで出力した値をExcelでヒストグラムにしてみると、0~1.0の一様??な乱数が取得させていることが分かります。

 

 

正規分布に従う乱数を取得するには、今回はボックス=ミュラー法という手法で乱数をしてみます。

XとYがお互いに独立で、0~1の範囲で一様に分布する乱数のとき

 

$${ Z }_{ 1 }=\sqrt { -2\log { X } } cos(2\pi Y)\\ { Z }_{ 2 }=\sqrt { -2\log { X } } sin(2\pi Y)$$

 

のZ1、Z2はそれぞれ標準偏差1.0、平均値0.0の正規分布に従う乱数となります。

(参考)ボックス=ミュラー法 Wikipedia

https://ja.m.wikipedia.org/wiki/%E3%83%9C%E3%83%83%E3%82%AF%E3%82%B9%EF%BC%9D%E3%83%9F%E3%83%A5%E3%83%A9%E3%83%BC%E6%B3%95

 

これをC#のプログラムにしてみると

var rnd = new Random(); 

double X, Y; 
double Z1, Z2; 

for (int i = 0; i < 1000; i++) 
{ 
	X = rnd.NextDouble(); 
	Y = rnd.NextDouble(); 
	
	Z1 = Math.Sqrt(-2.0 * Math.Log(X)) * Math.Cos(2.0 * Math.PI * Y); 
	Z2 = Math.Sqrt(-2.0 * Math.Log(X)) * Math.Sin(2.0 * Math.PI * Y); 

	Console.WriteLine(Z1.ToString()); 
	Console.WriteLine(Z2.ToString()); 
}

という感じ。

コンソールに吐き出された値をExcelでヒストグラムにしてみると

 

となり、確かに正規分布に従った感じの分布となりました。
標準偏差1.0、平均値0.0ではなく、標準偏差sigma、平均値ave のときの乱数が欲しい場合は

Z1 = sigma * Math.Sqrt(-2.0 * Math.Log(X)) * Math.Cos(2.0 * Math.PI * Y) + ave;
Z2 = sigma * Math.Sqrt(-2.0 * Math.Log(X)) * Math.Sin(2.0 * Math.PI * Y) + ave;

となります。

 

← 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#テクニックへ戻る