画像のエッジを検出

カラー画像から隣り合っているピクセルの濃淡の違いを検出して、エッジ画像を作成します。

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

プログラムの説明

  1. Win32 API を使ってカラー画像からエッジを検出する image.cs のソースコードです。
    次のソースコードは Image Guid にまとめて掲載しているので、こちらを参照して下さい。
    BITMAPINFOHEADER 構造体の定義 BITMAPINFO 構造体の定義 Win32 API の関数宣言InitDib() 関数
    //★ Win32 API で Edge を検出    前田 稔
    using System;
    using System.Drawing;
    using System.Windows.Forms;
    using System.Runtime.InteropServices;
    using System.Diagnostics;
    
    struct BITMAPINFOHEADER
    {
        ・・・ Image Guid を参照 ・・・
    };
    
    public class MyForm : Form
    {   Face    App;
        int     val= 3000;
    
        public MyForm()
        {
            App = new Face();
            Width = App.m_bmp.Width;
            Height = App.m_bmp.Height+32;
            App.InitDib();
            App.CopyBmp(App.m_DibDC);
            App.CopyBmp(App.m_MaskDC);
            Paint += new PaintEventHandler(MyHandler);
            MouseDown += new MouseEventHandler(OnMyMouseDown);
        }
    
        private void MyHandler(object sender, PaintEventArgs e)
        {
            Graphics g; 
            g = e.Graphics;
            if (App.m_bmp == null)  Application.Exit();
            App.ViewMask(g,0,0);
        }
    
        private void OnMyMouseDown(object sender, MouseEventArgs e)
        {
            if (e.Button == MouseButtons.Left)  //マウスの左ボタン
            {   val+= 1000;
                VerMsg("val: ", val);
                App.Edge(val);
            }
            if (e.Button == MouseButtons.Right) //マウスの右ボタン
            {   val = 5000;
                App.CopyBmp(App.m_MaskDC);
            }
            Invalidate();
        }
    
        public void VerMsg(string msg, int v)
        {
            string wstr;
            wstr = msg + v.ToString();
            this.Text = wstr;
        }
    }
    
    //☆ Face Object Class
    unsafe class Face
    {
        public  Bitmap  m_bmp;      // 入力画像
        public  IntPtr  m_Dib;      // DIB 画像
        public  IntPtr  m_DibDC;    // DIB DC
        public  byte    *dat;       // DIB ピクセルデータ
        public  IntPtr  m_Mask;     // Mask 画像
        public  IntPtr  m_MaskDC;   // Mask DC
        public  byte    *msk;       // Mak ピクセルデータ
    
        string  ImgFile = "";
        public  const int BI_RGB = 0;
        public  const int DIB_RGB_COLORS = 0;
        public  const int SRCCOPY = 0xcc0020;
    
        int[,]      vect = new int[9,2]         //エッジ検出テーブル
        { {-1,-1}, {-1,0}, {-1,1}, {0,-1}, {0,0}, {0,1}, {1,-1}, {1,0}, {1,1} };
        int[]       hCoef = new int[9]{ 1, 0, -1,  2, 0, -2,  1, 0, -1 };
        int[]       vCoef = new int[9]{ 1, 2, 1,  0, 0, 0,  -1, -2, -1 };
    
        [DllImport("gdi32.dll")]
    
        ・・・ Image Guid を参照 ・・・
    
        // Constructor
        public Face()
        {
            OpenFileDialog opendlg = new OpenFileDialog();
            opendlg.Filter = "画像ファイル (*.bmp)|*.bmp|すべてのファイル (*.*)|*.*";
            if (opendlg.ShowDialog() == DialogResult.OK)
            {   ImgFile = opendlg.FileName; }
            try
            {   m_bmp = new Bitmap(ImgFile); }
            catch
            {   MessageBox.Show("画像ファイルが読めません!", ImgFile);
                return;
            }
        }
    
        // Destructor
        ~Face()
        {   DeleteDC(m_DibDC);
            DeleteObject(m_Dib);
            DeleteDC(m_MaskDC);
            DeleteObject(m_Mask);
        }
    
        // m_MaskDC の画像を描画する 
        public void ViewMask(Graphics g, int x, int y)
        {
            if (m_MaskDC == null) return;
            IntPtr hDC = g.GetHdc();
            BitBlt(hDC, x, y, m_bmp.Width, m_bmp.Height, m_MaskDC, 0, 0, SRCCOPY);
            g.ReleaseHdc(hDC);
        }
    
        // hDC ← Bitmap m_bmp イメージデータの転送 
        public void CopyBmp(IntPtr hDC)
        {
            IntPtr hBmpSrc = m_bmp.GetHbitmap();
            Graphics gdraw = Graphics.FromImage(m_bmp);
            IntPtr srcHDC = gdraw.GetHdc();
            SelectObject(srcHDC, hBmpSrc);
            BitBlt(hDC, 0, 0, m_bmp.Width, m_bmp.Height, srcHDC, 0, 0, SRCCOPY);
            DeleteObject(hBmpSrc);
            gdraw.ReleaseHdc(srcHDC);
        }
    
        //☆ DIB の初期化
        unsafe public void InitDib()
        {
            ・・・ Image Guid を参照 ・・・
    
            fixed (void* ppvBits2 = &msk)
            {   m_Mask = CreateDIBSection(hdc, bi, 0, (IntPtr)ppvBits2, (IntPtr)null, 0);
                if (m_Mask == null) MessageBox.Show("CreateDIBSection error");
            }
            m_MaskDC = CreateCompatibleDC(hdc);
            SelectObject(m_MaskDC, m_Mask);
            ReleaseDC(IntPtr.Zero, hdc);
        }
     
        // *dat を参照して *msk にエッジ画像を作成
        public void  Edge(int val)
        {
            if (m_DibDC == null)    return;
            for(int y=1; y<m_bmp.Height-1; y++)
                for(int x=1; x<m_bmp.Width-1; x++)
                {
                    if (Sobel(x, y, val))   MASK(x,y,0,0,0);
                    else    MASK(x,y,255,255,255);
            }
        }
    
        // 座標[x,y]周辺のピクセル(*dat)を調べてエッジを検出する
        public bool  Sobel(int x, int y, int val)
        {   int[]   fx = new int[3];    //横3色分
            int[]   fy = new int[3];    //縦3色分
            int     dx, dy;
            byte    *pt;
    
            fx[0]= fx[1]= fx[2]= 0;
            fy[0]= fy[1]= fy[2]= 0;
            for(int i=0; i<9; i++)
            {
                dx = x + vect[i,0];
                dy = y + vect[i,1];
                pt = dat+(dy*m_bmp.Width+dx)*4;
                for(int c=0; c<3; c++)
                {   fx[c] += *(pt+c) * hCoef[i];
                    fy[c] += *(pt+c) * vCoef[i];
                }
            }
            for(int c=0; c<3; c++)
                if (fx[c]*fx[c]+fy[c]*fy[c] > val)  return true;
            return false;
        }
    
        // *msk(y, x) に R,G,B を設定(A はそのまま)
        public void MASK(int x, int y, byte r, byte g, byte b)
        {
            byte *pt;
            pt = msk+(y*m_bmp.Width+x)*4;
            *pt = b;
            *(pt+1)= g;
            *(pt+2)= r;
        }
    }
    
    class image01
    {
        [STAThread]
        public static void Main()
        {
            MyForm mf = new MyForm();
            Application.Run(mf);
        }
    }
    
  2. カラー画像からエッジを検出して白黒2階調の二値画像を作成します。
    カラー画像は RGB をそれぞれ8ビットで記録されています。
    青色(B)の8ビット 緑色(G)の8ビット 赤色(R)の8ビット Alpha(A)の8ビット
  3. エッジの検出は Sobel(ソーベル、またはゾーベル)フィルタと呼ばれる濃淡の違いを検出する手法があります。
    濃淡の違いを調べるピクセルを中心において、8方向の濃さを調べます。
    横方向の濃さを次のウエイトを掛けて演算します。
    1 0 -1
    2 0 -2
    1 0 -1
    同様に縦方向の濃さを次のウエイトを掛けて演算します。
    1 2 1
    0 0 0
    -1 -2 -1
    ウエイトの配列と、方向を決めるベクトルを定義しています。
        int[,]      vect = new int[9,2]         //エッジ検出テーブル
        { {-1,-1}, {-1,0}, {-1,1}, {0,-1}, {0,0}, {0,1}, {1,-1}, {1,0}, {1,1} };
        int[]       hCoef = new int[9]{ 1, 0, -1,  2, 0, -2,  1, 0, -1 };
        int[]       vCoef = new int[9]{ 1, 2, 1,  0, 0, 0,  -1, -2, -1 };
    
  4. 元の画像から隣り合っているピクセルの濃淡の違いを検出するので、二枚の DIB 画像を使います。
    *dat が隣り合っているピクセルの濃淡の違いを調べるポインターで、*msk がエッジ画像を作成するポインターです。
        public  Bitmap  m_bmp;      // 入力画像
        public  IntPtr  m_Dib;      // DIB 画像
        public  IntPtr  m_DibDC;    // DIB DC
        public  byte    *dat;       // DIB ピクセルデータ
        public  IntPtr  m_Mask;     // Mask 画像
        public  IntPtr  m_MaskDC;   // Mask DC
        public  byte    *msk;       // Mak ピクセルデータ
    
        //☆ Dib, Mask の初期化
        unsafe public void InitDib()
        {
            ・・・
    
            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);
    
            fixed (void* ppvBits2 = &msk)
            {   m_Mask = CreateDIBSection(hdc, bi, 0, (IntPtr)ppvBits2, (IntPtr)null, 0);
                if (m_Mask == null) MessageBox.Show("CreateDIBSection error");
            }
            m_MaskDC = CreateCompatibleDC(hdc);
            SelectObject(m_MaskDC, m_Mask);
            ReleaseDC(IntPtr.Zero, hdc);
        }
    
  5. 濃淡の違いを検出して、エッジ画像を作成する Edge() 関数です。
    指定された座標のピクセルと隣接するピクセルの濃淡の違いを Sobel() 関数で調べます。
    両端のピクセルは対象から外します。
        // *dat を参照して *msk にエッジ画像を作成
        public void  Edge(int val)
        {
            if (m_DibDC == null)    return;
            for(int y=1; y<m_bmp.Height-1; y++)
                for(int x=1; x<m_bmp.Width-1; x++)
                {
                    if (Sobel(x, y, val))   MASK(x,y,0,0,0);
                    else    MASK(x,y,255,255,255);
            }
        }
    
  6. 横方向と縦方向の濃淡を調べる Sobel() 関数です。
    三原色を別々に濃淡の違いを調べています。
        // 座標[x,y]周辺のピクセル(*dat)を調べてエッジを検出する
        public bool  Sobel(int x, int y, int val)
        {   int[]   fx = new int[3];    //横3色分
            int[]   fy = new int[3];    //縦3色分
            int     dx, dy;
            byte    *pt;
    
            fx[0]= fx[1]= fx[2]= 0;
            fy[0]= fy[1]= fy[2]= 0;
            for(int i=0; i<9; i++)
            {
                dx = x + vect[i,0];
                dy = y + vect[i,1];
                pt = dat+(dy*m_bmp.Width+dx)*4;
                for(int c=0; c<3; c++)
                {   fx[c] += *(pt+c) * hCoef[i];
                    fy[c] += *(pt+c) * vCoef[i];
                }
            }
            for(int c=0; c<3; c++)
                if (fx[c]*fx[c]+fy[c]*fy[c] > val)  return true;
            return false;
        }
    
  7. MyForm では Face Class の Constructor から BMP File を選択して画像を入力します。
    入力した画像に合わせてウインドウサイズを設定するのですが、BitBlt で描画するので「ピクセル/インチ」は考慮しません。
    App.InitDib(); 関数で DIB を初期化します。
    m_DibDC と m_MaskDC に App.CopyBmp(); で画像を転送します。
    m_DibDC の画像は濃淡の違いを調べるときに参照します。
    MyHandler から描画する画像は m_MaskDC に作成された画像です。
    {   Face    App;
    
        public MyForm()
        {
            App = new Face();
            Width = App.m_bmp.Width;
            Height = App.m_bmp.Height+32;
            App.InitDib();
            App.CopyBmp(App.m_DibDC);
            App.CopyBmp(App.m_MaskDC);
            Paint += new PaintEventHandler(MyHandler);
            MouseDown += new MouseEventHandler(OnMyMouseDown);
        }
    
  8. MyHandler() では App.ViewMask(g,0,0); で m_MaskDC の画像を描画します。
        private void MyHandler(object sender, PaintEventArgs e)
        {
            Graphics g; 
            g = e.Graphics;
            if (App.m_bmp == null)  Application.Exit();
            App.ViewMask(g,0,0);
        }
    
  9. マウスの左クリックで val をアップしながらエッジを検出します。
    val はエッジを検出ための閾値(しきいち)で、値が大きくなるほど黒い部分が少なくなります。
    C言語(Windows)の エッジ検出 に比べて実行に時間がかかるので、タイトルバーの var(閾値)を確認しながらゆっくりクリックして下さい。
    マウスの右クリックで val を最初の値に戻して、元のカラー画像に設定します。
        int     val= 3000;
    
        private void OnMyMouseDown(object sender, MouseEventArgs e)
        {
            if (e.Button == MouseButtons.Left)  //マウスの左ボタン
            {   val+= 1000;
                VerMsg("val: ", val);
                App.Edge(val);
            }
            if (e.Button == MouseButtons.Right) //マウスの右ボタン
            {   val = 5000;
                App.CopyBmp(App.m_MaskDC);
            }
            Invalidate();
        }
    
  10. タイトルバーにメッセージを印字する VerMsg() 関数です。
        public void VerMsg(string msg, int v)
        {
            string wstr;
            wstr = msg + v.ToString();
            this.Text = wstr;
        }
    

[Next Chapter ↓] Mark Rect
[Previous Chapter ↑] W32 Color ⇒ 二値画像

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