【C#】Bitmapのカラーパレットの設定

8bitのグレースケールのBitmap(PixelFormatがFormat8bppIndexed)を新規で作成する場合、カラーパレットを設定する必要があるのですが、安易に下記のようなコード

// これは設定できない
var bmp = new Bitmap(256, 64, System.Drawing.Imaging.PixelFormat.Format8bppIndexed);
for (int i = 0; i < 256; i++)
{
    bmp.Palette.Entries[i] = Color.FromArgb(i, i, i);
}

のようにするとちゃんと設定できていそうで、実は設定できていません。

 

これはBitmapのPaletteプロパティで値の取得が呼ばれたときに内部でカラーパレットのメモリが確保され、実際にGDI+で描画に用いているカラーパレットをメモリに格納するようにしているためで、上記のコードではコピーしてきたカラーパレットに設定しているだけで、実際に用いているカラーパレットには設定できていません。

 

そこで正しく修正したのがこちら↓

// これなら設定できる
var bmp = new Bitmap(256, 64, System.Drawing.Imaging.PixelFormat.Format8bppIndexed);
var pal = bmp.Palette;
for (int i = 0; i < 256; i++)
{
    pal.Entries[i] = Color.FromArgb(i, i, i);
}
bmp.Palette = pal;

 

ちなみに、画像の輝度値が0~255に変化する、こんな画像↓

を作るのにカラーパレットの設定を失敗すると、こんな感じ↓になっていました。

 

画像処理では8ビットのグレースケールの画像を用いることは多いかと思いますが、カラーパレットの設定は、だた、グレースケールを設定するだけではつまらないので、少し変わった使い方を紹介します。

 

二値化のプレビューとして

指定した範囲の輝度値(val1~val2)に色を付けるメソッド(SetPaletteGrayScal)を作りました。

/// <summary>
/// 8bitグレースケールのカラーパレットを設定
/// </summary>
/// <param name="bmp">カラーパレットを設定するBitmapオブジェクト</param>
/// <param name="val1">val1~val2の範囲を指定した色(col)にする</param>
/// <param name="val2">val1~val2の範囲を指定した色(col)にする</param>
/// <param name="col">指定色</param>
public void SetPaletteGrayScal(Bitmap bmp, int val1 = -1, int val2 = -1, Color? col = null)
{
    var pal = bmp.Palette;

    // グレースケールのパレットの設定
    for (int i = 0; i < 256; i++) { pal.Entries[i] = Color.FromArgb(i, i, i); } if (col == null) col = Color.Red; // 値が設定されているとき if ( (val1 >= 0) &&
        (val1 < 256) && (val2 >= 0) &&
        (val2 < 256)
    )
    {
        if (val1 <= val2)
        {
            // val1~val2の範囲を指定した色に設定する
            for (int i = val1; i <= val2; i++)
            {
                pal.Entries[i] = (Color)col;
            }
        }
        else
        {
            // 0~val2、val1~255の範囲を指定した色に設定する
            for (int i = 0; i <= val2; i++)
            {
                pal.Entries[i] = (Color)col;
            }
            for (int i = val1; i < 256; i++)
            {
                pal.Entries[i] = (Color)col;
            }
        }
    }

    // カラーパレットの設定
    bmp.Palette = pal;
}

 

使い方は、こんな感じ↓

using (var bmp = new Bitmap("Grayscale.bmp"))
{
    // グレースケールの設定(64~128を赤くする)
    SetPaletteGrayScal(bmp, 64, 128);
    bmp.Save("GrayscaleRed.bmp", System.Drawing.Imaging.ImageFormat.Bmp);

    // 元に戻す場合は
    // グレースケールの設定(通常)
    SetPaletteGrayScal(bmp);
}

 

カラーパレット設定前

カラーパレット設定後

 

他の画像で輝度値127~255を赤く表示している例↓

  

 

この方法だと実際に二値化の処理は行っていないので、結果が表示されるまでの時間が殆どかかりません。

二値化のしきい値をどの程度にするか?プレビューする場合などは便利です。

 

疑似カラー画像

黒~白の画像を青~緑~赤と色を付けて表示している、いわゆる疑似カラーのカラーパレットを設定するメソッドを作成しました。

/// <summary>
/// 疑似カラーのパレットを設定
/// </summary>
/// <param name="bmp">カラーパレットを設定するBitmapオブジェクト</param>
public void SetPalettePseudoColor(Bitmap bmp)
{
    var pal = bmp.Palette;

    int r, g, b;

    // 疑似カラー
    for (int i = 0; i < 52; i++)
    {
        r = 0;
        g = (int)((Math.Cos(Math.PI * (i * 900 / 255.0 - 180) / 180.0) + 1) * 110.0);
        b = 255;
        pal.Entries[i] = Color.FromArgb(r, g, b);
    }
    for (int i = 52; i < 102; i++)
    {
        r = 0;
        g = 220;
        b = (int)((Math.Cos(Math.PI * (i * 900 / 255.0 - 180) / 180.0) + 1) * 127.5);
        pal.Entries[i] = Color.FromArgb(r, g, b);
    }
    for (int i = 102; i < 154; i++)
    {
        r = (int)((Math.Cos(Math.PI * (i * 900 / 255.0 - 180) / 180.0) + 1) * 127.5);
        g = 220;
        b = 0;
        pal.Entries[i] = Color.FromArgb(r, g, b);
    }
    for (int i = 154; i < 204; i++)
    {
        r = 255;
        g = (int)((Math.Cos(Math.PI * (i * 900 / 255.0 - 180) / 180.0) + 1) * 110.0);
        b = 0;
        pal.Entries[i] = Color.FromArgb(r, g, b);
    }
    for (int i = 204; i < 256; i++)
    {
        r = 255;
        g = 0;
        b = (int)((Math.Cos(Math.PI * (i * 900 / 255.0 - 180) / 180.0) + 1) * 127.5);
        pal.Entries[i] = Color.FromArgb(r, g, b);
    }
    // カラーパレットの設定
    bmp.Palette = pal;
}

使い方は、こんな感じ↓

using (var bmp = new Bitmap("Grayscale.bmp"))
{
    // 疑似カラーの設定
    SetPalettePseudoColor(bmp);
    bmp.Save("SetColorPalette_PseudoColor.bmp", System.Drawing.Imaging.ImageFormat.Bmp);
}

カラーパレット設定前

カラーパレット設定後

 

他の画像では

  

 

サーモグラフィ―っぽい画像

サーモグラフィ―でよく目にする画像では温度の高い部分が赤く、低い部分が青く表示されている場合が多いかと思いますが、実際にサーモグラフィーのカメラで撮影した画像データは8ビット(それ以上の場合もあるかも?)の場合は、8ビットの画像をカラーの24ビットに変換して色を付けるまでもなく、カラーパレットを設定することで、それっぽい画像になってくれます。

/// <summary>
/// サーモグラフィーっぽいカラーパレットを設定
/// </summary>
/// <param name="bmp">カラーパレットを設定するBitmapオブジェクト</param>
public void SetPaletteThermo(Bitmap bmp)
{
    var pal = bmp.Palette;

    // カラーパレットの設定

    int r, g, b;

    for (int i = 0; i < 256; i++)
    {
        if (i < 32)
        {
            r = 0;
        }
        else if (i < 164)
        {
            r = (int)((Math.Cos(Math.PI * (i * 360 / 255.0 - 225) / 180.0) + 1) * 127.5);
        }
        else
        {
            r = 255;
        }

        if (i < 96)
        {
            g = 0;
        }
        else if (i < 224)
        {
            g = (int)((Math.Cos(Math.PI * (i * 360 / 255.0 + 45) / 180.0) + 1) * 127.5);
        }
        else
        {
            g = 255;
        }

        if (i < 128)
        {
            b = (int)((Math.Cos(Math.PI * (i * 720 / 255.0 - 180) / 180.0) + 1) * 127.5);
        }
        else if (i < 192)
        {
            b = 0;
        }
        else
        {
            b = (int)((Math.Cos(Math.PI * (i * 720 / 255.0) / 180.0) + 1) * 127.5);
        }

        pal.Entries[i] = Color.FromArgb(r, g, b);
    }
    // カラーパレットの設定
    bmp.Palette = pal;
}

使い方は、こんな感じ↓

using (var bmp = new Bitmap("Grayscale.bmp"))
{
    // グレースケールの設定(64~128を赤くする)
    SetPaletteThermo(bmp);
    bmp.Save("SetColorPalette_Thermo.bmp", System.Drawing.Imaging.ImageFormat.Bmp);
}

カラーパレット設定前

カラーパレット設定後

 

他の画像では

  

 

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

【C#】Bitmap画像のビット数の取得

Bitmapクラスのプロパティに Format24bppRgb などを取得できる PixelFormatプロパティ はありますが、1画素あたりのビット数を取得するにはどうすればよいのか??

 

画像処理で使うPixelFormatは Format8bppIndexed(8bit)、Format24bppRgb(24bit)、Format32bppArgb(32bit)ぐらいしかないので、if文とかで拾ってもいいかもしれませんが、Bitmapクラス(Imageクラスでも同じ)にGetPixelFormatSizeメソッドというのがあるので、これを使います。

 

(コード例)

// Bitmapオブジェクトの作成
var bmp = new Bitmap(@"C:\Temp\Lenna.bmp");

// 画像の1画素あたりのビット数の取得(8,24,32など)
var bitCount = Bitmap.GetPixelFormatSize(bmp.PixelFormat);

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

【C#】補間モード(InterpolationMode)の設定

例えば、画像を縦横2倍に拡大すると画像に隙間が生まれてしまうため、この隙間をどのように埋めるか?の手法を補間と呼び、C#ではGraphicsクラスのInterpolationModeプロパティを設定することで、DrawImageメソッドが補間処理を行ってくれます。

 

■■■
■■■
■■■

↓ x2倍に拡大

■□■□■
□□□□□
■□■□■
□□□□□
■□■□■

 

(コード例)

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

using (var bmp = new Bitmap(@"C:\Temp\Parrots.bmp"))
using (var g = Graphics.FromImage(pictureBox1.Image))
{
    g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.Bilinear;
    g.DrawImage(
        bmp,
        new RectangleF(0, 0, 500, 500),
        new RectangleF(170, 100, 50, 50),
        GraphicsUnit.Pixel);
}

デフォルトではInterpolationModeはBilinearに設定されていますが、このInterpolationModeを変えながら実行した結果が以下の通りです。

 

InterpolationMode.NearestNeighbor

 

InterpolationMode.Bilinear

 

InterpolationMode.Bicubic

 

InterpolationMode.Default

 

InterpolationMode.Low

 

InterpolationMode.High

 

InterpolationMode.HighQualityBilinear

 

InterpolationMode.HighQualityBicubic

 

NearestNeighbor、Bilinear、Bicubicの処理アルゴリズムについては、下記ページを参照下さい。

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

 

処理結果の画像を見ると、私の感覚では

NearestNeighbor→Default、Bilinear、Low→HighQualityBilinear→Bicubic→High、HighQualityBicubicの順で良くなる感じでしょうか??

 

どのモードを使ったら良いか?ですが、画像処理を行う場合は1画素1画素が見えるNearestNeighborがおススメです。描画速度も一番速いと思います。

写真のようなビューアのプログラムではHighQualityBicubicあたり?

 

ここでは画像の拡大の時の比較を行っていますが、縮小のとき、Bicubicでは25%以下のとき、Bilinearでは50%以下のとき、適していないとのこと。

(参考)

https://msdn.microsoft.com/ja-jp/library/system.drawing.drawing2d.interpolationmode(v=vs.110).aspx

 

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

【C#】画像(Bitmapクラス)のPictureBoxへの描画

画像(Bitmapオブジェクト)を描画するには、DrawImageメソッドを用いますが、DrawImageメソッドは30個も定義があり、使うと意図しない動きをする定義も含まれています。

DrawImage(Image image, PointF point);
DrawImage(Image image, Rectangle rect);
DrawImage(Image image, PointF[] destPoints);
DrawImage(Image image, Point[] destPoints);
DrawImage(Image image, RectangleF rect);
DrawImage(Image image, Point point);
DrawImage(Image image, float x, float y);
DrawImage(Image image, int x, int y);
DrawImage(Image image, Rectangle destRect, Rectangle srcRect, GraphicsUnit srcUnit);
DrawImage(Image image, RectangleF destRect, RectangleF srcRect, GraphicsUnit srcUnit);
DrawImage(Image image, PointF[] destPoints, RectangleF srcRect, GraphicsUnit srcUnit);
DrawImage(Image image, Point[] destPoints, Rectangle srcRect, GraphicsUnit srcUnit);
DrawImage(Image image, float x, float y, float width, float height);
DrawImage(Image image, Point[] destPoints, Rectangle srcRect, GraphicsUnit srcUnit, ImageAttributes imageAttr);
DrawImage(Image image, int x, int y, int width, int height);
DrawImage(Image image, int x, int y, Rectangle srcRect, GraphicsUnit srcUnit);
DrawImage(Image image, PointF[] destPoints, RectangleF srcRect, GraphicsUnit srcUnit, ImageAttributes imageAttr);
DrawImage(Image image, float x, float y, RectangleF srcRect, GraphicsUnit srcUnit);
DrawImage(Image image, PointF[] destPoints, RectangleF srcRect, GraphicsUnit srcUnit, ImageAttributes imageAttr, DrawImageAbort callback);
DrawImage(Image image, Point[] destPoints, Rectangle srcRect, GraphicsUnit srcUnit, ImageAttributes imageAttr, DrawImageAbort callback);
DrawImage(Image image, Rectangle destRect, float srcX, float srcY, float srcWidth, float srcHeight, GraphicsUnit srcUnit);
DrawImage(Image image, Point[] destPoints, Rectangle srcRect, GraphicsUnit srcUnit, ImageAttributes imageAttr, DrawImageAbort callback, int callbackData);
DrawImage(Image image, Rectangle destRect, int srcX, int srcY, int srcWidth, int srcHeight, GraphicsUnit srcUnit);
DrawImage(Image image, PointF[] destPoints, RectangleF srcRect, GraphicsUnit srcUnit, ImageAttributes imageAttr, DrawImageAbort callback, int callbackData);
DrawImage(Image image, Rectangle destRect, int srcX, int srcY, int srcWidth, int srcHeight, GraphicsUnit srcUnit, ImageAttributes imageAttr);
DrawImage(Image image, Rectangle destRect, float srcX, float srcY, float srcWidth, float srcHeight, GraphicsUnit srcUnit, ImageAttributes imageAttrs);
DrawImage(Image image, Rectangle destRect, float srcX, float srcY, float srcWidth, float srcHeight, GraphicsUnit srcUnit, ImageAttributes imageAttrs, DrawImageAbort callback);
DrawImage(Image image, Rectangle destRect, int srcX, int srcY, int srcWidth, int srcHeight, GraphicsUnit srcUnit, ImageAttributes imageAttr, DrawImageAbort callback);
DrawImage(Image image, Rectangle destRect, float srcX, float srcY, float srcWidth, float srcHeight, GraphicsUnit srcUnit, ImageAttributes imageAttrs, DrawImageAbort callback, IntPtr callbackData);
DrawImage(Image image, Rectangle destRect, int srcX, int srcY, int srcWidth, int srcHeight, GraphicsUnit srcUnit, ImageAttributes imageAttrs, DrawImageAbort callback, IntPtr callbackData);

DrawImageメソッドには、大まかには

●描画先の左上の座標を指定するもの

●描画先の左上の座標、幅、高さを指定するもの

●描画先の左上の座標、描画元の領域を指定するもの

●描画先の左上の座標、幅、高さ、描画元の領域を指定するもの

●上記の座標にint型で指定するもの

●上記の座標にfloat型で指定するもの

●上記にDrawImageAbort を指定するもの

 

の組み合わせとなります。

DrawImageAbortに関しては、使った事がないので、いまいち理解できていないのですが、描画先の左上の座標のみを指定する場合は、画像ファイルのDPI情報(dot per inch, 画像の解像度)に合わせて表示されるので、注意が必要です。(というより使わない方が良いです)

 

例えば、下記のようなコードで72dpiと96dpiのファイルをそれぞれ開くと、

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

using (var bmp = new Bitmap(@"C:\Temp\Lenna.jpg"))
using (var g = Graphics.FromImage(pictureBox1.Image))
{
    g.DrawImage(bmp, 0, 0);
}

(ファイルが72dpiのとき)

(ファイルが96dpiのとき)

 

のように2つの画像とも画素数は同じなのですが、表示される大きさが異なります。

Windowsでは96dpi基準なので、96dpiのファイルだけを表示していると気が付きませんが、macが72dpiのため、たまに表示サイズがおかしくなる場合があるので、意図的にdpiに基づいて表示する場合以外は必ず

  左上の座標、画像の幅、高さ

を指定するメソッドを用いるようにして下さい。

 

(修正したコード例)

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

using (var bmp = new Bitmap(@"C:\Temp\Lenna.jpg"))
using (var g = Graphics.FromImage(pictureBox1.Image))
{
    g.DrawImage(bmp, 0, 0, bmp.Width, bmp.Height);
}

さらに画像を拡大/縮小して表示する場合は、描画先の幅、高さに表示倍率を掛けることで、拡大縮小することが出来ますが、下記のようなコードで拡大表示すると、画像の上と左側が0.5画素分切れて表示されてしまいます。

 

(拡大表示例)

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

using (var bmp = new Bitmap(@"C:\Temp\test.jpg"))  // 6x6画素の画像
using (var g = Graphics.FromImage(pictureBox1.Image))
{
    // 補間モードの設定(各画素が見えるように) 
    g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor; 
    // 50倍で描画  
    g.DrawImage(bmp, 0, 0, bmp.Width * 50, bmp.Height * 50);
}

実行結果

 

このようにならたいためには、こちらのページ↓

【C#】画像の座標系

 

でも書いていますが、PixelOffsetModeを指定するか、元の画像の座標を0.5画素ズラすかのいづれかの方法となります。

ここでは、元の画像の座標を0.5画素ずらす方法の例を示すと、

 

(修正したコード例)

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

using (var bmp = new Bitmap(@"C:\Temp\test.jpg"))  // 6x6画素の画像
using (var g = Graphics.FromImage(pictureBox1.Image))
{
    // 補間モードの設定(各画素が見えるように) 
    g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor; 
    // 描画元を0.5画素ずらして50倍で描画  
  g.DrawImage(
        bmp, 
        new RectangleF(0f, 0f, bmp.Width * 50f, bmp.Height * 50f), 
        new RectangleF(-0.5f, -0.5f, bmp.Width, bmp.Height), 
        GraphicsUnit.Pixel);
}

実行結果

 

さらに面白い使い方として、描画先の座標に画像の左上、右上、左下の座標を指定することで、画像の平行移動、拡大縮小、拡大、せん断までのアフィン変換を実現することもできます。

(コード例)

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

using (var bmp = new Bitmap(@"C:\Temp\Lenna.jpg"))
using (var g = Graphics.FromImage(pictureBox1.Image))
{
    // 補間モードの設定(各画素が見えるように)    
    g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
    // 描画元の領域
    var srcRect = new RectangleF(-0.5f, -0.5f, bmp.Width, bmp.Height);

    // 描画先の座標の初期値(左上、右上、左下の順)
    var points = new PointF[]
    {
        new PointF(0, 0),
        new PointF(bmp.Width, 0),
        new PointF(0, bmp.Height),
    };

    // 描画先の座標をアフィン変換で求める
    var mat = new System.Drawing.Drawing2D.Matrix();
    // 画像の中心を基点に回転
    mat.RotateAt(
        30f,
        //new PointF((bmp.Width - 1) / 2f, (bmp.Height - 1) / 2f),
        new PointF(bmp.Width / 2f, bmp.Height / 2f),
        System.Drawing.Drawing2D.MatrixOrder.Append
        );
    // 拡大
    mat.Scale(1.5f, 1.5f, System.Drawing.Drawing2D.MatrixOrder.Append);
    // 3点のアフィン変換
    mat.TransformPoints(points);

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

実行結果

 

まとめ

●描画先の座標指定では、左上の座標のみの指定は用いないこと
必ず左上の座標、幅、高さを指定すること。

●画像を拡大する場合は、0.5画素分、表示がずれる事を考慮すること

●描画先の座標に画像の左上、右上、左下を指定することで、画像のアフィン変換も実現できる。

 

関連ページ

【C#】画像の座標系

【C#】グローバル変換とローカル変換

 

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

【C#】Bitmapのファイル保存

Bitmapオブジェクトをファイルに保存するにはSaveメソッドを用います。

以下にBitmapファイル(*.bmp)を開き、Jpegファイル(*.jpg)に保存する例を示します。

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

bmp.Save(
    @"C:\Temp\Mandrill.jpg", 
    System.Drawing.Imaging.ImageFormat.Jpeg
    );

コード的には簡単なのですが、注意したいのが、必ずImageFormatを指定することです。

もし指定しないと、上記の例では、Mandrill.jpgファイルが保存されるのですが、ファイルの中身はpngフォーマットで保存されてしまいます。

(参考)

https://msdn.microsoft.com/ja-jp/library/ktx83wah(v=vs.110).aspx

 

一見するとjpegファイルに保存されているように見えるため、分かりづらいのでご注意を!

 

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

【C#】Graphicsオブジェクトの作成

フォームのピクチャボックスなどに画像や線を描画するには、Graphicsオブジェクトに対して描画を行いますが、Graphicsオブジェクトの作成方法はいくつかあり、描画の際の挙動も異なります。

 

Graphicsオブジェクトの作成方法は以下の通り

 

●Imageオブジェクトから作成

●PaintイベントのPaintEventArgsから取得

●CreateGraphicsメソッドによる作成

●FromHwndメソッドによる作成

 

以下にフォームのPictureBoxをDockしたときにPictureBoxに描画するためのGraphicsオブジェクトの作成方法を例にとって説明したいと思います。

 

Imageオブジェクト(Bitmapオブジェクト)から作成

以下の例ではフォームのリサイズイベント内でピクチャボックスと同じサイズのBitmapオブジェクトを作成し、ピクチャボックスのImageプロパティへBitmapオブジェクトを設定し、BitmapオブジェクトからGraphicオブジェクトを作成し、線を描画しています。

private void Form1_Resize(object sender, EventArgs e)
{
    if ((pictureBox1.Width == 0) || (pictureBox1.Height == 0)) return;

    var bmp = pictureBox1.Image as Bitmap;

    if (bmp != null)
    {
        bmp.Dispose();
    }

    bmp = new Bitmap(pictureBox1.Width, pictureBox1.Height,
        System.Drawing.Imaging.PixelFormat.Format24bppRgb);

    pictureBox1.Image = bmp;

    using (var g = Graphics.FromImage(bmp))
    {
        g.Clear(Color.White);

        g.DrawLine(Pens.Red, 0, 0, pictureBox1.Width, pictureBox1.Height);
        pictureBox1.Refresh();
    }
}

実行例

 

ここではピクチャボックスのリサイズイベントではPictureBoxの大きさの取得に失敗する場合があるため、Formのリサイズイベントで処理を行っています。

 

PaintイベントのPaintEventArgsから取得

PictureBoxなどのコントロールのPaintイベントのPaintEventArgsからGraphicsオブジェクトを作成します。

private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
    var g = e.Graphics;

    g.Clear(Color.White);

    g.DrawLine(Pens.Red, 0, 0, pictureBox1.Width, pictureBox1.Height);

}

実は上記コードだけでは問題があり、PictureBoxがリサイズされると、領域が更新された部分のみ線が描画されてしまうため、以下のようになってしまいます。

 

このようにならないようにするには、コントロールのResizeイベントでコントロールのInvalidate()を実行することで、コントロール全体の領域が更新され、正しく描画されるようになります。

private void pictureBox1_Resize(object sender, EventArgs e)
{
    pictureBox1.Invalidate();
}

CreateImageメソッドで作成

var g = pictureBox1.CreateGraphics();

g.Clear(Color.White);

g.DrawLine(Pens.Red, 0, 0, pictureBox1.Width, pictureBox1.Height);

FromHwndメソッドで作成

var g = Graphics.FromHwnd(pictureBox1.Handle);

g.Clear(Color.White);

g.DrawLine(Pens.Red, 0, 0, pictureBox1.Width, pictureBox1.Height);

まとめ

基本的にはImageオブジェクトからFromImageメソッドによりGraphicsオブジェクトを作成する方法がおススメです。

この方法ではGraphicsオブジェクトへ画像や線を描画した内容はBitmapオブジェクトへ反映されるので、使い勝手も良いかと思います。

この方法で描画速度が遅く感じる場合は手動ダブルバッファという方法があるので、こちらのページを参照下さい。

 

【C#】手動ダブルバッファによる高速描画

 

PaintイベントでGraphicsオブジェクトを取得する方法はプログラムが簡単なので、ちょっとした評価用のプログラムなどでは使用することはありますが、描画タイミングなど複雑なプログラムを作成しようとすると、Paintイベント内だけで描画するプログラムを作成するのは難しくなるので、私のブログでPaintイベントでGraphicsオブジェクトを取得しているコードがあったら、「手を抜いているんだな!」と思って下さい。

 

CreateImage、FromHwndメソッドを用いた方法では、なにせ描画が遅い!

線などたくさん描画するようなプログラムでは、線一本一本が描画される様子が見えるぐらいに遅いです。そのため、私は、この方法をほとんど使いません。

 

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

【C#】画像の結合

画像の結合は画像の一部領域の切り出しで行った方法とよく似ています。

処理の手順は

  1. 結合後の大きさと同じ大きさのBitmapオブジェクトの作成
  2. BitmapオブジェクトからGraphicsオブジェクトの作成
  3. 作成したGraphicsオブジェクトへ描画先を指定して描画

 

 

画像を縦方向に結合する例をメソッドにまとめました。

/// <summary>
/// 画像を縦に結合する
/// </summary>
/// <param name="src">結合するBitmapの配列</param>
/// <returns>結合されたBitmapオブジェクト</returns>
public Bitmap ImageCombineV(Bitmap[] src)
{
    // 結合後のサイズを計算
    int dstWidth = 0, dstHeight = 0;
    System.Drawing.Imaging.PixelFormat dstPixelFormat = System.Drawing.Imaging.PixelFormat.Format8bppIndexed;

    for (int i = 0; i < src.Length; i++)
    {
        if (dstWidth < src[i].Width) dstWidth = src[i].Width;
        dstHeight += src[i].Height;

        // 最大のビット数を検索
        if (Bitmap.GetPixelFormatSize(dstPixelFormat)
            < Bitmap.GetPixelFormatSize(src[i].PixelFormat))
        {
            dstPixelFormat = src[i].PixelFormat;
        }
    }

    var dst = new Bitmap(dstWidth, dstHeight, dstPixelFormat);
    var dstRect = new Rectangle();

    using (var g = Graphics.FromImage(dst)) {
        for (int i = 0; i < src.Length; i++)
        {
            dstRect.Width = src[i].Width;
            dstRect.Height = src[i].Height;

            // 描画
            g.DrawImage(src[i], dstRect, 0, 0, src[i].Width, src[i].Height, GraphicsUnit.Pixel);

            // 次の描画先
            dstRect.Y = dstRect.Bottom;
        }
    }
    return dst;
}

使用方法は

var images = new Bitmap[] {
    new Bitmap("Lenna.bmp"),
    new Bitmap("Mandrill.bmp"),
    new Bitmap("Parrots.bmp")
};
            
// 画像の結合
var combinedImage = ImageCombineV(images);

// 結合画像の保存
combinedImage.Save("combinedImage.bmp", System.Drawing.Imaging.ImageFormat.Bmp);

// 解放
foreach (var bmp in images)
{
    bmp.Dispose();
}

のような感じで。

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

【C#】画像の一部領域の切り出し

画像の一部領域を切り出すには以下の手順で行います。

 

  1. 切り出す領域と同じ大きさのBitmapオブジェクトの作成
  2. BitmapオブジェクトからGraphicsオブジェクトの作成
  3. 作成したGraphicsオブジェクトへ元の部分領域を指定して描画

 

 

この一連の処理をメソッドにまとめました。

/// <summary>
/// Bitmapの一部を切り出したBitmapオブジェクトを返す
/// </summary>
/// <param name="srcRect">元のBitmapクラスオブジェクト</param>
/// <param name="roi">切り出す領域</param>
/// <returns>切り出したBitmapオブジェクト</returns>
public Bitmap ImageRoi(Bitmap src, Rectangle roi)
{
    //////////////////////////////////////////////////////////////////////
    // srcRectとroiの重なった領域を取得(画像をはみ出した領域を切り取る)

    // 画像の領域
    var imgRect = new Rectangle(0, 0, src.Width, src.Height);
    // はみ出した部分を切り取る(重なった領域を取得)
    var roiTrim = Rectangle.Intersect(imgRect, roi);
    // 画像の外の領域を指定した場合
    if (roiTrim.IsEmpty == true) return null;

    //////////////////////////////////////////////////////////////////////
    // 画像の切り出し

    // 切り出す大きさと同じサイズのBitmapオブジェクトを作成
    var dst = new Bitmap(roiTrim.Width, roiTrim.Height, src.PixelFormat);
    // BitmapオブジェクトからGraphicsオブジェクトの作成
    var g = Graphics.FromImage(dst);
    // 描画先
    var dstRect = new Rectangle(0, 0, roiTrim.Width, roiTrim.Height);
    // 描画
    g.DrawImage(src, dstRect, roiTrim, GraphicsUnit.Pixel);
    // 解放
    g.Dispose();

    return dst;
}

 

使用方法は

var bmpRoi = ImageRoi(bmpSrc, new Rectangle(150, 80, 100, 100));

のような感じで。

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

【C#】グローバル変換とローカル変換

画像を拡大縮小表示する場合は、.NET Frameworkのアフィン変換の機能を用いて表示するのが比較的簡単なのですが、そのアフィン変換にもグローバル変換ローカル変換というものがあります。

 

詳細はMSDNのページ

https://msdn.microsoft.com/ja-jp/library/c499ats3(v=vs.110).aspx

 

にも書いてありますが、グローバル変換はGraphicsクラスのTransformプロパティにアフィン変換用の行列のMatrixオブジェクト(名前空間:System.Drawing.Drawing2D)を指定すれば、Grapgicsオブジェクトへ描画する画像や線は、そのMatrixオブジェクトに基づいて拡大縮小、移動表示されるので、個々の画像や線などをアフィン変換する必要がなく、簡単といえば簡単なのですが、画像は拡大したいけど、文字は拡大したくない場合などに不都合があります。

 

下図の左側の画像がフローバル変換を用いて描画した結果ですが、画像を拡大すると、文字は大きくなり、線幅も太くなってしまいます。

(ここでは右側の画像のように拡大しても線幅や文字の大きさが変わらないのが目標)

 

それに対して、ローカル変換は画像や線に対して個々にアフィン変換を行い表示を行うのですが、実際にアフィン変換が出来るのはGraphicsPathのみで、画像に対するアフィン変換が用意されていません。

 

そこで、画像に対してもアフィン変換行列であるMatrixオブジェクトに基づいて、画像の拡大縮小を行うメソッドを作成しました。

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
        );
}

ここでポイントとなるのが、描画元の左上の座標が(-0.5,-0.5)となっているのですが、これは一番左上の画素の中心が(0, 0)で、画素の左上の座標は(-0.5, -0.5)となるためで、詳細は以下のページを参照下さい。

 

【C#】画像の座標系

 

これらを使って作成したサンプルプログラムがこちら↓

 

動作イメージ

 

ダウンロード

Global_Local_Transform.zip

 

関連記事

【C#】画像の座標系

【C#】グローバル変換を使ったアフィン変換

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

【C#】GraphicsPathの描画

 

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

【C#】画像ファイルビューア(ソースコード付き)

(2019.09.03追記)本記事の内容を再度、再構成し、下記の記事を作成しました。

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

 

以下は以前、書いた記事


画像ファイルを開いて、マウスホイールの上下で拡大縮小するソフトは、以前にも作り、旧ブログで公開しているのですが、これはGraphicクラスのTransformプロパティにアフィン変換行列を指定することで、Graphicオブジェクトの描画した画像や線は勝手に拡大縮小表示してくれるという、ワールド変換という機能を使っていました。

 

【C#】マウスホイールで画像の拡大縮小

http://imagingsolution.blog107.fc2.com/blog-entry-287.html

 

しかし、この方法では拡大した画像の上に線を引くと、線も太く表示されるので、少し使いにくい部分がありました。

そこで、今回は描画する元の領域と、描画先の左上、右上、左下の3点の座標を指定し、この3点からなる四角形(平行四辺形も含む)に合わせて画像を表示してくれるメソッド

public void DrawImage(
	Image image,
	PointF[] destPoints,
	RectangleF srcRect,
	GraphicsUnit srcUnit
)

を使って画像ファイルのビューアを作成しました。

 

動作イメージ

 

主な機能

  • ドラッグ&ドロップでファイルを開く
  • マウスホイールの上下による画像の拡大縮小表示
  • マウスのボタンを押しながら画像の移動
  • マウス左ボタンのダブルクリックで画像全体表示
  • マウス右ボタンのダブルクリックで画像等倍表示
  • 画像を開いたあと、矢印キーの左右ボタン(←、→)で同一フォルダ内の画像ファイル切替
  • ウィンドウ左下にマウスポインタ位置の画像上の座標および輝度値の表示
  • ウィンドウ右下に画像の幅x高さxビット数を表示

 

ダウンロード

公開日 バージョン ファイル 備考
2020.07.25 Ver.1.1.0 ImageViewer_V110.zip マウスダブルクリックで発生する例外を修正
2018.02.12 Ver.1.0.0 ImageViewer_V100.zip 初版

 

使用方法

プログラムを実行するだけなら、ファイルを解凍し、exeフォルダ内の ImageViewer.exe をダブルクリックすることで、実行できます。

ソースコード(Visual Studio 2015 C#版)はsourceフォルダに格納しています。

 

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

【C#】Bitmapクラスの最大サイズは?

私は仕事でセンサカメラなどを用いる事があるのですが、ラインセンサは横の画素数が8K(8192画素)ぐらいは、割と普通に使われ、1枚の画像として、縦方向の画素数はいくらでも指定できてしまうため、C#プログラムでBitmapクラスはどの程度の大きさ(画素数)まで対応可能なのか?調べてみました。

 

評価環境

OS:Windows10 Pro 64bit

Visual Studio Community 2015

.NET Framework 4.6.2

実装メモリ:32GB

評価プログラム実行時の使用メモリ量:約9GB(残23GB)

 

評価プログラム

private void button1_Click(object sender, EventArgs e)
{
    int width = (int)nudWidth.Value;
    int height = (int)nudHeight.Value;
    // 使用メモリ容量(24bitカラーのとき)
    int size = width * height * 3 / 1024 / 1024;

    try
    {
        using (var bmp = new Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format24bppRgb))
        {
            MessageBox.Show(size.ToString("#,0") + "MB 確保");
        }
    } catch 
    {
        MessageBox.Show(size.ToString("#,0") + "MB エラー");
    }
}

(実行画面)

評価方法

プラットフォームを Any CPU(32bit優先)、Any CPU(32bit優先なし)、x64で切り替えて、エラーが出るまでHeightの値を大きくしてみました。

 

32bit優先のあるなしは、プロジェクトのプロパティから、ビルドを選択して 32ビットを優先 の部分のチェックのあるなしで切り替えます。

 

 

Bitmapクラスは以下のように24ビットカラーで確保したので、メモリ的には

Width x Height x 3 バイト

確保されます。

var bmp = new Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format24bppRgb)

 

結果

プラットフォーム 最大メモリサイズ
Any CPU

(32bit優先)

1GB
Any CPU

(32bit優先なし)

2GB
x64 2GB

 

ということで、Bitmapクラスで確保できる最大のサイズ(メモリ容量)は 2GB のようです。

プロセス的な限界かないか?と思い、下のようにBitmapクラスを2つ確保してみましたが、結果は同じ(bmp1,bmp2とも2GB分、計4GB確保できる)でした。

private void button1_Click(object sender, EventArgs e)
{
    int width = (int)nudWidth.Value;
    int height = (int)nudHeight.Value;
    // 使用メモリ容量(24bitカラーのとき)
    int size = width * height * 3 / 1024 / 1024;

    try
    {
        using (var bmp1 = new Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format24bppRgb))
        using (var bmp2 = new Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format24bppRgb))
        {
            MessageBox.Show(size.ToString("#,0") + "MB 確保");
        }
    } catch 
    {
        MessageBox.Show(size.ToString("#,0") + "MB エラー");
    }
}

もともと、なぜエラーになるか?

と思ったのですが、エラーが出たときの画面にヒントがあるような。。

 

 

-2,047MB エラー

と表示されいます。

これはメモリのサイズを

int size = width * height * 3 / 1024 / 1024;

と計算していますが、エラーが出るときは

width * height * 3

の値がintの最大値を超えてオバーフローしているためだと思いますが、おそらくBitmapクラス内部でも同じ用なコードがあるのでは?ないでしょうかね。

 

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

【C#】画像処理プログラム向け基本プログラム

C#で画像処理を行うための最低限のプログラムを作成しました。

 

 

サンプルプログラムのダウンロードはこちら↓

ImagingBaseSample.zip(Visual Studio 2015 C#)

※Windows7では画像表示が消えてしまうようですので、Windows10でお試し下さい。

 

このプログラムは画像の輝度値へのアクセスを容易にするためのImageDataクラス(自作)と、

ImageDataクラスライブラリ

 

と、Graphicsクラスオブジェクトをコントロールのプロパティで拾えるようにしたGraphicsBoxコントロール(自作)

【.NET】GraphicsBoxコントロール

 

のサンプルとなっています。

 

現状では、むしろ面倒くさいだけのサンプルになっていますが、とりあえずヒストグラムの機能は付けました。

右上の空いたスペースにいろいろ肉付けをしていこうと思います。

 

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