Image Guid

C#で画像解析を行うために必要な知識です。
C言語(Windows)でも Image Guid を掲載しているので、基礎はこちらを参照して下さい。

前田稔(Maeda Minoru)の超初心者のプログラム入門

Image(画像)概要

  1. コンピュータで扱う画像は、ピクセル(画素)を格子状に並べて表現されます。
    ファイルの形式(jpeg, gif, bmp 等)に関わらず、ピクセル(画素)が格子状に並んでいます。
    ページ先頭の画像は、テスト用に作成した 16*16 ピクセルの画像を拡大したものです。
    C言語(Windows)では「24ビットBMP形式」を使いましたが、C#では「32ビットBMP形式」を使います。
    色の3原色(赤緑青)とアルファ値をそれぞれ8ビット(256階調)の組み合わせでピクセルを表します。
    青色(B)の8ビット 緑色(G)の8ビット 赤色(R)の8ビット Alpha(A)の8ビット
    アルファにはマスクなどを設定するのですが詳細は プログラムの設計 などを参照して下さい。
    C#では BMP 意外にも JPG, GIF, PNG なども読み込むことが出来ますが、Index Color モードに対して GetPixel(), SetPixel() は使えません。
    GIF 画像(256色)を使うときは24ビット BMP 形式に変換して下さい。
  2. 画像の解析は RGB の組み合わせに立ち返って、ピクセル単位に処理するのが基本です。
    C言語では CreateDIBSection() 関数で DIB を作成してピクセルデータの配列にアクセスしているのですが、C#ではこれに相当する関数が見つかりません。
    仕方が無いので GetPixel() 関数, SetPixel() 関数でプログラムしたのですが、実行に時間がかかりすぎます。
    次の段階として「Win32 API」を使ってプログラムすることにします。
  3. C#で Win32 API を使うには、幾つかの注意事項があります。
    1. Win32 API を使うときは Runtime.InteropServices を取り込んで下さい。
      Debug.Write() でデバッグ情報を印字するときは Diagnostics を取り込んで下さい。
      using System.Runtime.InteropServices;
      using System.Diagnostics;
      
      Win32 API に限りませんが、ダイアログボックスからファイルを選択するときは Main() 関数に [STAThread] を設定して下さい。
          [STAThread]
          public static void Main()
      
    2. C#から呼び出す Win32 API の関数を宣言します。
          [DllImport("gdi32.dll")]
          private static extern int BitBlt(IntPtr hDestDC,
              int x, int y, int nWidth, int nHeight,
              IntPtr hSrcDC, int xSrc, int ySrc, int dwRop);
      
          [DllImport("User32.Dll")]
          public static extern IntPtr GetDC(IntPtr hwnd);
      
          [DllImport("User32.Dll")]
          public static extern int ReleaseDC(IntPtr hwnd, IntPtr hdc);
      
          [DllImport("gdi32.dll")]
          public static extern IntPtr CreateCompatibleDC(IntPtr hdc);
      
          [DllImport("gdi32.dll")]
          public static extern int SelectObject(IntPtr hdc, IntPtr hbmp);
      
          [DllImport("gdi32.dll")]
          public static extern IntPtr CreateDIBSection(IntPtr hdc, BITMAPINFO bi, UInt32 iUsage,
              IntPtr ppvBits, IntPtr hSection, UInt32 dwOffset);
      
          [DllImport("gdi32.dll")]
          public static extern int GetDIBits(IntPtr hdc, IntPtr hbmp, UInt32 uStartScan, UInt32 cScanLines,
              IntPtr lpvBits, IntPtr lpbi, UInt32 uUsage);
      
          [DllImport("gdi32.dll")]
          public static extern int DeleteObject(IntPtr hdc);
      
          [DllImport("gdi32.dll")]
          public static extern int DeleteDC(IntPtr hdc);
      
    3. 本来C#ではポインターの使用が禁止されています。
      Win32 API ではポインターを使うのですが、一歩間違えれば他の領域を破壊しかねません。
      配列の場合は特にその危険性が高く、unsafe を設定して覚悟の上で使います。
      unsafe の設定は[プロジェクト]メニューの[プロパティ]から[ビルド]を選択して[unsafe コードブロックの許可]を設定します。
      unsafe の設定画像
    4. そしてポインターを使用するブロックに unsafe を追加します。
      私は Face Object Class 全体に unsafe を設定しています。
      //☆ Face Object Class
      unsafe class Face
      
    5. 配列に Pointer を使うときは、stackalloc で配列を静的に確保したり、fixed でアドレス固定する必要があります。
      fixed の説明は 配列のポインタ 及び ++C++; // 未確認飛行 C を参照して下さい。
  4. HBITMAP に直接アクセス出来ないので Win32 API のデバイス独立ビットマップ(DIB)を使ってピクセルデータに直接アクセスします。
    DIB(Device-Independent Bitmap) は CreateDIBSection() を使って生成します。
    m_Dib が DIB の画像領域で m_DibDC がそのハンドルです。
    *dat がピクセルデータへのポインターです。
    ポインターを通じてピクセルデータを修正すると直ちに画像に反映します。
    BI_RGB, DIB_RGB_COLORS, SRCCOPY は Win32 API を呼び出すときの定数です。
    BMP FILE ではファイルの先頭に格納されているのは最下段のラインで、上下逆に格納されています。
    *dat を通じて DIB のピクセルデータにアクセスする場合も同じで、領域の先頭に格納されているのは最下段のラインです。
        public  Bitmap  m_bmp;      // 入力画像
        public  IntPtr  m_Dib;      // DIB 画像
        public  IntPtr  m_DibDC;    // DIB DC
        public  byte    *dat;       // DIB ピクセルデータ
        public const int BI_RGB = 0;
        public const int DIB_RGB_COLORS = 0;
        public const int SRCCOPY = 0xcc0020;
    
    DIB(BMP FILE) 関係の座標系は左下を起点として、X座標が大きくなるほど右に、Y座標が大きくなるほど上になります。
    GetPixel() 関数, SetPixel() 関数の座標系は、左上を起点とするディスプレイ座標です。
  5. デバイス独立ビットマップ(DIB)を初期化するソースコードです。
    Bitmap 画像のヘッダー情報を定義する BITMAPINFOHEADER, BITMAPINFO 構造体を定義します。
    new BITMAPINFO(); で構造体を割り当てて、DIB を作成するためのパラメータを設定します。
    CreateDIBSection() が DIB を生成する API です。
    fixed(void* ppvBits = &dat) でピクセルデータへのポインターを設定します。
    SelectObject() で生成した m_Dib と m_DibDC を関連付けます。
    DIB の描画には m_DibDC を使います。
    struct BITMAPINFOHEADER
    {
        public UInt32 biSize;
        public Int32  biWidth;
        public Int32  biHeight;
        public UInt16 biPlanes;
        public UInt16 biBitCount;
        public UInt32 biCompression;
        public UInt32 biSizeImage;
        public Int32  biXPelsPerMeter;
        public Int32  biYPelsPerMeter;
        public UInt32 biClrUsed;
        public UInt32 biClrImportant;
    };
    unsafe struct BITMAPINFO
    {
        public BITMAPINFOHEADER bmih;
        public fixed byte bmiColors[1];
    };
    
        //☆ DIB の初期化
        unsafe public void InitDib()
        {
            IntPtr hdc = GetDC(IntPtr.Zero);
            BITMAPINFO bi = new BITMAPINFO();  // BITMAP 構造体
    
            bi.bmih.biSize = (UInt32)sizeof(BITMAPINFOHEADER);
            bi.bmih.biPlanes = (UInt16)1;
            bi.bmih.biCompression = BI_RGB;
            bi.bmih.biBitCount = 32;
            bi.bmih.biWidth = m_bmp.Width;
            bi.bmih.biHeight = m_bmp.Height;
    
            fixed (void* ppvBits = &dat)
            {
                m_Dib = CreateDIBSection(hdc, bi, 0, (IntPtr)ppvBits, (IntPtr)null, 0);
                if (m_Dib == null) MessageBox.Show("CreateDIBSection error");
            }
            m_DibDC = CreateCompatibleDC(hdc);
            SelectObject(m_DibDC, m_Dib);
            ReleaseDC(IntPtr.Zero, hdc);
        }
    
  6. ポインターを通じてピクセルデータにアクセスするソースコードは次のようになります。
    ChkPix(); 関数でイメージ画像の先頭32ピクセルを印字してみました。
    BMP FILE では上下逆に格納されているので、最下段のラインの左端から印字されます。
        // byte *dat; の印字確認
        public void ChkPix()
        {
            for(int i = 0; i < 32; i++)
            {   DebugRGB(dat+i*4);  }
        }
        public void DebugRGB(byte *v)
        {
            Debug.Write("BGRA: " + *v + ", " + *(v+1) + ", " + *(v+2) + ", " + *(v+3) + "\n");
        }
    

ウインドウサイズ

  1. 画像サイズに合わせてウインドウサイズを設定します。
    Height+32; の 32 はタイトルバーの高さです。
    DPI(ドット/インチ)の標準は 96 ですが、DPI が異なる BMP ファイルもあるようです。
    (私のスマホで撮影した画像を BMP に変換すると 72 DPI でした。)
            bmp = new Bitmap(ImgFile);
            Width = bmp.Width;
            Height = bmp.Height+32;
    
  2. 上のソースでは、ドット/インチ(ピクセル/インチ)が標準(96)以外の時は、画像サイズとウインドウサイズが一致しません。
    DPI を考慮したウインドウサイズを設定するソースコードです。
    hdpi は横方向の DPI で、vdpi は縦方向の DPI です。
    Debug.Write で Width, Hight, hdpi, vdpi を印字してみました。
            App = new Face();
            float hdpi = App.bmp.HorizontalResolution;
            float vdpi = App.bmp.VerticalResolution;
            Width = App.bmp.Width*96/(int)hdpi;
            Height = (App.bmp.Height+32)*96/(int)vdpi;
    Debug.Write("Width:" + App.bmp.Width + "  Hight:" + App.bmp.Height + "  H:" + hdpi + "  V:" + vdpi + "\n");
    
  3. このコーナーでは、通常のC#で描画する場合と、Win32 API の BitBlt() で描画する場合があります。
    BitBlt() で描画したときは「ピクセル/インチ」が考慮されないようで、画像サイズとウインドウサイズが一致しない場合があります。
    通常のC#で描画するソースコードです。
        public void View(Graphics g, int x, int y)
        {   if (bmp != null) g.DrawImage(bmp, x, y);
        }
    
    BitBlt() で描画するソースコードです。
        public void ViewDib(Graphics g, int x, int y)
        {   if (m_DibDC == null) return;
            IntPtr hDC = g.GetHdc();
            BitBlt(hDC, x, y, m_bmp.Width, m_bmp.Height, m_DibDC, 0, 0, SRCCOPY);
            g.ReleaseHdc(hDC);
        }
    
  4. 画像をマウスでクリックして、座標を取得するときにも DPI が関係します。
    CRT(ウインドウ画面)の DPI は一般的に 96 で、これに合わせてプログラムしています。
    従って、DPI が 96 以外の画像を使うと座標がずれることがあるので注意して下さい。

関連リンク

C#で画像解析を行うための関連リンクです。
  1. このコーナーのテンプレートのプロジェクトは [Form を作成する] で説明した[空のプロジェクト]を使っています。
    C# の自動生成を使ってプロジェクトを作成すると、訳の解らない沢山のファイルが作成されます。
    それに対して、空のプロジェクトから作成するとファイルも少なくすっきりしたプロジェクトを作成することが出来ます。
    それと何よりも全て自分で作成するので「細部まで理解できる」ことが最大のメリットです。 (^◇^;)
    Form を作成する からは[空のプロジェクトの圧縮ファイル]も提供しています。
  2. とは言え、最も一般的なのは[自動生成を使ったプロジェクト]でしょうか? (^_^;)
    空のプロジェクトから作成した場合も自動生成のプロジェクトも基本的には同じ要領です。
    自動生成のプロジェクトは次のページを参照して下さい。
    自動生成を使って Form を作成
    線を描画する
    画像ファイルを描画する
  3. C言語(Windows) の Image Guid です。
  4. fixed の説明は 配列のポインタ 及び ++C++; // 未確認飛行 C を参照して下さい。
  5. C言語で作成した DLL(HelloMsg) を呼び出す C#⇒DLL(HelloMsg) です。
  6. C言語で作成した DLL(GCM,LCM) を呼び出す C#⇒DLL(GCM,LCM) です。
  7. C言語で作成したポインター引数を持つ DLL(CharMsg) を呼び出す C#⇒DLL(CharMsg) です。

[Next Chapter ↓] Face Image Size

超初心者のプログラム入門(C# Frame Work)