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

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

移動平均フィルタ VS ガウシアンフィルタ

いろいろと検索していたら、2008年の国家試験に以下のような問題があったらしい。


画像が最も平滑化される空間フィルタはどれか。

ただし、数字は重み係数を示す。

(参考)http://www.clg.niigata-u.ac.jp/~lee/jyugyou/img_processing/medical_image_processing_03_press.pdf


 

この問題、

1.ラプラシアンフィルタ

2.3x3の移動平均フィルタ

3.3x3のガウシアンフィルタ

4.5x5の移動平均フィルタ

5.5x5のσが小さめのガウシアンフィルタ

なので、正解は4番だとは思いますが、そもそも平滑化ってなに?

というところに引っかかった。。

 平滑化 = 画像をボヤかす  なら、4番が正解

 平滑化 = ノイズを除去する なら、3番か5番はどうだろう??

 

そもそも「ガウシアンフィルタは中心の画素からの距離に応じてσを小さくすると、平滑化効果が弱まり、σを大きくすると平滑化効果が強くなる」というのが教科書的な説明で、昔の私もそのように書いちゃってますね。。

ガウシアンフィルタの処理アルゴリズムとその効果

 

ただ、ガウシアンフィルタには ローパスフィルタの特性もある というのも大事な特徴だと思います。

 

試しにガウシアンフィルタを画像にかけてみると、

 

オリジナル画像
3×3移動平均フィルタ 3×3ガウシアンフィルタ

 

これだと、差が分かりにくいので、やや意図的なデータではあるのですが、1次元データに対して、1/3, 1/3, 1/3 という移動平均と 1/4, 2/4, 1/4  のガウシアンフィルタとで比べると、

 

オリジナルデータ
移動平均フィルタ
ガウシアンフィルタ

 

となって、ガウシアンフィルタの方がノイズ除去ができています。

上記データはあまりにも意図的なので、いまいち納得できないような部分もあるかと思いますが、連続する3点のデータにおいて、ノイズの成分がどのように加わっているのか?を考えると、理論値からのズレは

+-+

ー+-

++-

ー++

+--

ーー+

のように「+側のノイズが2個、-側のノイズ1個」もしくは「+側のノイズが1個、-側のノイズ2個」となるので、平均すると、+1/3 もしくは -1/3 余ってしまいます。

ガウシアンフィルタでは、少なくとも

+-+

ー+-

のパターンでは、+1/4-2/4+1/4 もしくは ー1/4+2/4ー1/4 となるので、誤差が打ち消しあってくれます。

その他の

++-

ー++

+--

ーー+

パターンでは+1/2 もしくは -1/2 となるので、移動平均よりもダメ!と思ったりもするのですが、この場合は5点以上のガウシアンフィルタを用いると誤差が消えてくれる可能性が高くなります。

 

...と、何が言いたかったかというと、ガウシアンフィルタでは、ローパスフィルタの効果があるため、高周波のノイズには移動平均フィルタよりガウシアンフィルタを用いた方が効果的な場合があります。

私の本職では検査装置などで使われる工業用のカメラを用いる事が多いのですが、工業用のカメラで撮影した画像のノイズと言えば、まずは高周波なノイズなので、ノイズを消したい場合は、移動平均フィルタよりも、まずはガウシアンフィルタを検討するようにしています。

ガウシアンフィルタの方が画像がボケないですし。

 

画像処理アルゴリズムへ戻る