《Visual C++数字图像模式识别技术详解(第2版)》一2.2 Visual C++数字图像处理

2.2 Visual C++数字图像处理

本节将在2.1节基础上讲解如何用Visual C++进行数字图像处理应用程序的开发。重点介绍Windows下BMP图像文件格式,以及如何用Visual C++对该数字图像文件进行读取,为后续内容的讲解打下基础。

2.2.1 BMP图像文件

BMP位图文件格式是Windows系统交换图像数据的一种标准图像文件存储格式,在Windows环境下运行的所有图像处理软件都支持这种格式。Windows 3.0以前的BMP位图文件格式与显示设备有关,因此把它称为设备相关位图(Device-dependent Bitmap,DDB)文件格式。Windows 3.0以后的BMP位图文件格式与显示设备无关,因此把这种BMP位图文件格式称为设备无关位图(Device-independet Bitmap,DIB)格式,目的是让Windows能够在任何类型的显示设备上显示BMP位图文件。BMP位图文件默认的文件扩展名是bmp。
BMP位图文件由4个部分组成:位图文件头(Bitmap-file Header)、位图信息头(Bitmap- information Header)、调色板(Palette)和像素数据(Image Data),如图2-15所示。
screenshot

1.位图头文件
Visual C++中用BITMAPFILEHEADER数据结构定义位图头文件,它包含文件类型、文件大小和存放位置等信息,结构如下:

typedef  struct  tagBITMAPFILEHEADER{
    WORD            bfType;                  /*说明文件的类型*/
    DWORD            bfSize;                    /*说明文件的大小,以字节为单位*/
    WORD            bfReserved1;            /*保留,设置为0*/
    WORD            bfReserved2;            /*保留,设置为0*/
    DWORD            bfOffBits;               /*说明从BITMAPFILEHEADER结构开始到实际图像数据阵列字节间的字节偏移量*/

}BITMAPFILEHEADER;

这个结构的长度是固定的,为14个字节,其中WORD为无符号16位二进制数,DWORD为无符号32位二进制整数。
2.位图信息头
Visual C++中用BITMAPINFOHEADER数据结构定义位图信息头,它包含位图的大小、压缩类型和颜色格式等信息,其结构定义如下:

typedef  struct  BITMAPINFOHEADER{
     DWORD            biSize;                  /*BITMAPINFOHEADER结构所需要的字节数*/
     LONG            biWidth;                  /*图像的宽度,以像素为单位*/
     LONG            biHeight;                  /*图像的高度,以像素为单位*/
     WORD            biPlanes;                  /*目标设备位平面数,其值设置为l*/
     WORD            biBitCount;            /*每像素位数,为1、4、8或24*/
     DWORD            biCompression;            /*压缩类型,0为不压缩*/
     DWORD            biSizeImage;            /*压缩图像大小的字节数,非压缩图像为0*/
     LONG            biXPelsPerMeter;      /*水平分辨率*/
     LONG            biYPelsPerMeter;      /*垂直分辨率*/
     DWORD            biClrUsed;                  /*使用的色彩数*/
     DWORD            biClrImportant;      /*重要色彩数,0表示都重要*/
} BITMAPINFOHEADER;

3.调色板
Visual C++中,调色板实际上定义为一个数组,共有biClrUsed个元素,每个元素的类型是一个RGBQUAD结构,其定义如下:

typedef  struct  tagRGBQUAD{
     BYTE          rgbBlue;                  /*指定蓝色分量*/
     BYTE          rgbGreen;                /*指定绿色分量*/
     BYTE          rgbRed;                  /*指定红色分量*/
     BYTE          rgbReserved;            /*保留值*/
}RGBQUAD;

24位真彩色图像不使用调色板,因为位图中的RGB值就代表了每个像素的颜色,所以BITMAPI- NFOHEADER后直接是像素数据。
4.像素数据
紧跟在调色板之后的是图像数据字节阵列,用BYTE数据结构存储。图像的每一扫描行由表示图像的连续像素字节组成,每一行的字节数取决于图像的颜色数和用像素表示的图像宽度。扫描行是由底向上存储的,这就是说,数据存放是从下到上,从左到右。从文件中最先读到的图像数据是位图最下面的左边的第一个像素,然后是左边的第二个像素,而最后读到的图像数据是位图最上面一行的最右边的一个像素。

2.2.2 位图文件读取

本节将实现在多文档应用程序中打开一个位图文件的功能。这里将用到Visual C++提供的另一个强大工具ClassWizard。
【例2-2】 位图文件的读取
本例的目的是讲解基于Visual C++6.0进行图像处理应用程序的开发。这里构造了一个标准图像处理类,并以打开一幅数字图像为例解释了如何基于该类构造图像处理应用程序。
 设计步骤
1.文档类中添加成员函数
[1] 在Visual C++集成开发环境(IDE)的【View】菜单中选择【ClassWizard】,在操作类名(Class name)中选择文档类CDemo1Doc,操作对象(Object IDs)选择CDemo1Doc,对应消息(Messages)选择OnOpenDocument,该消息在单击应用程序中“文件”菜单的“打开”菜单项时产生。该过程如图2-16所示。
[2] 单击添加函数按钮,在类CDemo1Doc加入OnOpenDocument成员函数,该函数在程序中出现OnOpenDocument消息时执行。该过程如图2-16所示。
screenshot

该成员函数默认生成代码如下:
BOOL CDemo1Doc::OnOpenDocument(LPCTSTR lpszPathName)
{

  if (!CDocument::OnOpenDocument(lpszPathName))
        return FALSE;
        // TODO: Add your specialized creation code here
        return TRUE;

}
2.数字图像处理类的创建
由于MFC没有提供合适的数字图像处理类,因此,我们需要定义一个用于数字图像处理的新类。
[1] 在Visual C++集成开发环境(IDE)的【Insert】菜单选择创建新类【New Class】。新类类型【Class type】选择一般类Generic Class。在类名【Name】框中键入ImageDib。
[2] 单击按钮即可生成该类。该过程如图2-17所示。
screenshot

[3] 在ImageDib类的头文件ImageDib.h中编辑该类的结构如下:

      class ImageDib
      {
//成员变量
public:      
            unsigned char * m_pImgData;                   //图像数据指针
            LPRGBQUAD m_lpColorTable;                         //图像颜色表指针
            int m_nBitCount;                                    //每像素占的位数
private:
            LPBYTE m_lpDib;                                    //指向DIB的指针
            HPALETTE m_hPalette;                              //逻辑调色板句柄
            int m_nColorTableLength;                         //颜色表长度(多少个表项)
public:
            int m_imgWidth;                                    //图像的宽,以像素为单位
            int m_imgHeight;                                     //图像的高,以像素为单位
            LPBITMAPINFOHEADER m_lpBmpInfoHead;       //图像信息头指针
//成员函数
public:
            ImageDib();                                          //构造函数
            ~ImageDib();                                                      //析构函数
            BOOL Read(LPCTSTR lpszPathName);                         //DIB读函数
            BOOL Write(LPCTSTR lpszPathName);                         //DIB写函数
            int ComputeColorTabalLength(int nBitCount);              //计算颜色表的长度
            BOOL Draw(CDC* pDC, CPoint origin, CSize size);         //图像绘制
            CSize GetDimensions();                                          //读取图像维数
            void ReplaceDib(CSize size, int nBitCount, LPRGBQUAD lpColorTable,
            unsigned char *pImgData);                                  //用新的数据替换DIB
      private:
            void MakePalette();                                            //创建逻辑调色板
            void Empty();                                             //清理空间
};
类ImageDib的结构定义中,引入颜色表指针用于存储图像中实际用到的颜色,该数据来自于图像文件的调色板数据。
[4]    在ImageDib类的代码文件ImageDib.cpp中编辑该类中函数的定义如下:
      ImageDib::ImageDib()
      {
            m_lpDib=NULL;                               //初始化m_lpDib为空
            m_lpColorTable=NULL;                        //设置颜色表指针为空
            m_pImgData=NULL;                              //设置图像数据指针为空
            m_lpBmpInfoHead=NULL;                        //设置图像信息头指针为空
            m_hPalette = NULL;                              //设置调色板为空
            }
      ImageDib::~ImageDib()
          {
              //释放m_lpDib所指向的内存缓冲区
              if(m_lpDib != NULL) 
              delete [] m_lpDib; 
              //如果有调色板,则释放调色板缓冲区
              if(m_hPalette != NULL)
              ::DeleteObject(m_hPalette);
          }
      BOOL ImageDib::Read(LPCTSTR lpszPathName)
      {
            //读模式打开图像文件
            CFile file;
            if (!file.Open(lpszPathName, CFile::modeRead | CFile::shareDenyWrite))
                  return FALSE;
            BITMAPFILEHEADER bmfh;
            //读取BITMAPFILEHEADER结构到变量bmfh中
            int nCount=file.Read((LPVOID) &bmfh, sizeof(BITMAPFILEHEADER));
            //为m_lpDib分配空间,读取DIB到内存中
            if(m_lpDib!=NULL)      delete []m_lpDib;
            m_lpDib=new BYTE[file.GetLength() -sizeof(BITMAPFILEHEADER)];
            file.Read(m_lpDib, file.GetLength() -sizeof(BITMAPFILEHEADER));
            //m_lpBmpInfoHead位置为m_lpDib起始位置
            m_lpBmpInfoHead = (LPBITMAPINFOHEADER)m_lpDib;
            //为成员变量赋值
            m_imgWidth=m_lpBmpInfoHead->biWidth;
            m_imgHeight=m_lpBmpInfoHead->biHeight;
            m_nBitCount=m_lpBmpInfoHead->biBitCount; 
            //计算颜色表长度
            m_nColorTableLength=      ComputeColorTabalLength(m_lpBmpInfoHead->biBitCount);
            //如果有颜色表,则创建逻辑调色板
            m_hPalette = NULL;
            if(m_nColorTableLength!=0){m_lpColorTable=
            (LPRGBQUAD)(m_lpDib+sizeof(BITMAPINFOHEADER));
                MakePalette();
            }
            //m_pImgData指向DIB的位图数据起始位置
            m_pImgData = (LPBYTE)m_lpDib+sizeof(BITMAPINFOHEADER) +
                  sizeof(RGBQUAD) * m_nColorTableLength;
            return TRUE;
      }
BOOL ImageDib::Write(LPCTSTR lpszPathName)
{
      //写模式打开文件
      CFile file;
      if (!file.Open(lpszPathName, CFile::modeCreate | CFile::modeReadWrite 
            | CFile::shareExclusive))
            return FALSE;
      
      //填写文件头结构
      BITMAPFILEHEADER bmfh;
      bmfh.bfType = 0x4d42;  // 'BM'
      bmfh.bfSize = 0;
      bmfh.bfReserved1 = bmfh.bfReserved2 = 0;
      bmfh.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) +
            sizeof(RGBQUAD) * m_nColorTableLength;      
      try {
            //将文件头结构写进文件
            file.Write((LPVOID) &bmfh, sizeof(BITMAPFILEHEADER));
            
            //将文件信息头结构写进文件
            file.Write(m_lpBmpInfoHead,  sizeof(BITMAPINFOHEADER));
            
            //如果有颜色表,将颜色表写进文件
            if(m_nColorTableLength!=0)
                  file.Write(m_lpColorTable, sizeof(RGBQUAD) * m_nColorTableLength);
            
            //将位图数据写进文件
            int imgBufSize=(m_imgWidth*m_nBitCount/8+3)/4*4*m_imgHeight;
            file.Write(m_pImgData, imgBufSize);
      }
      catch(CException* pe) {
            pe->Delete();
            AfxMessageBox("write error");
            return FALSE;
      }
      
      //函数返回
      return TRUE;
}
            void ImageDib::MakePalette()
      {
            //如果颜色表长度为0,则不创建逻辑调色板
            if(m_nColorTableLength == 0) 
                  return;
            //删除旧的逻辑调色板句柄
            if(m_hPalette != NULL) ::DeleteObject(m_hPalette);
            //申请空间,根据颜色表生成LOGPALETTE结构
            LPLOGPALETTE pLogPal = (LPLOGPALETTE) new char[2 * sizeof(WORD) +
                  m_nColorTableLength * sizeof(PALETTEENTRY)];
            pLogPal->palVersion = 0x300;
            pLogPal->palNumEntries = m_nColorTableLength;
            LPRGBQUAD m_lpDibQuad = (LPRGBQUAD) m_lpColorTable;
            for(int i = 0; i < m_nColorTableLength; i++) {
                  pLogPal->palPalEntry[i].peRed = m_lpDibQuad->rgbRed;
                  pLogPal->palPalEntry[i].peGreen = m_lpDibQuad->rgbGreen;
                  pLogPal->palPalEntry[i].peBlue = m_lpDibQuad->rgbBlue;
                  pLogPal->palPalEntry[i].peFlags = 0;
                  m_lpDibQuad++;
            }      
            //创建逻辑调色板
            m_hPalette = ::CreatePalette(pLogPal);
            //释放空间
            delete pLogPal;
      }
            int ImageDib::ComputeColorTabalLength(int nBitCount)
      {
            int colorTableLength;
            switch(nBitCount) {
            case 1:
                  colorTableLength = 2;
                  break;
            case 4:
                  colorTableLength = 16;
                  break;
            case 8:
                  colorTableLength = 256;
                  break;
            case 16:
            case 24:
            case 32:
                  colorTableLength = 0;
                  break;
            default:
                  ASSERT(FALSE);
            }
            ASSERT((colorTableLength >= 0) && (colorTableLength <= 256)); 
            return colorTableLength;
            }
      BOOL ImageDib::Draw(CDC* pDC, CPoint origin, CSize size)
      {
            HPALETTE hOldPal=NULL;                                //旧的调色板句柄
            if(m_lpDib == NULL) return FALSE;                  //如果DIB为空,则返回0
            if(m_hPalette != NULL) {                              //如果DIB有调色板
                                                                      //将调色板选进设备环境中
                  hOldPal=::SelectPalette(pDC->GetSafeHdc(), m_hPalette, TRUE);
                  pDC->RealizePalette();                  
            }
            pDC->SetStretchBltMode(COLORONCOLOR);      //设置位图伸缩模式
            //将DIB在pDC所指向的设备上进行显示
            ::StretchDIBits(pDC->GetSafeHdc(), origin.x, origin.y, size.cx, size.cy,
                  0, 0, m_lpBmpInfoHead->biWidth, m_lpBmpInfoHead->biHeight,m_pImgData,
                   (LPBITMAPINFO) m_lpBmpInfoHead, DIB_RGB_COLORS, SRCCOPY);
            if(hOldPal!=NULL)                                    //恢复旧的调色板
                  ::SelectPalette(pDC->GetSafeHdc(), hOldPal, TRUE);
            return TRUE;
      }
      CSize ImageDib::GetDimensions()
      {      
            if(m_lpDib == NULL) return CSize(0, 0);
            return CSize(m_imgWidth, m_imgHeight);
            }
void ImageDib::ReplaceDib(CSize size, int nBitCount,  
                                    LPRGBQUAD lpColorTable,unsigned char *pImgData)
{ 
      //释放原DIB所占空间
      Empty();
      //成员变量赋值
      m_imgWidth=size.cx;
      m_imgHeight=size.cy;
      m_nBitCount=nBitCount;
      //计算颜色表的长度
      m_nColorTableLength=ComputeColorTabalLength(nBitCount);
      //将每行像素所占字节数,扩展成4的倍数
      int lineByte=(m_imgWidth*nBitCount/8+3)/4*4;
      //位图数据的大小
      int imgBufSize=m_imgHeight*lineByte;
      //为m_lpDib重新分配空间,以存放新的DIB
      m_lpDib=new BYTE [sizeof(BITMAPINFOHEADER) + 
            sizeof(RGBQUAD) * m_nColorTableLength+imgBufSize];
      //填写位图信息头BITMAPINFOHEADER结构
      m_lpBmpInfoHead = (LPBITMAPINFOHEADER) m_lpDib;
      m_lpBmpInfoHead->biSize = sizeof(BITMAPINFOHEADER);
      m_lpBmpInfoHead->biWidth = m_imgWidth;
      m_lpBmpInfoHead->biHeight = m_imgHeight;
      m_lpBmpInfoHead->biPlanes = 1;
      m_lpBmpInfoHead->biBitCount = m_nBitCount;
      m_lpBmpInfoHead->biCompression = BI_RGB;
      m_lpBmpInfoHead->biSizeImage = 0;
      m_lpBmpInfoHead->biXPelsPerMeter = 0;
      m_lpBmpInfoHead->biYPelsPerMeter = 0;
      m_lpBmpInfoHead->biClrUsed = m_nColorTableLength;
      m_lpBmpInfoHead->biClrImportant = m_nColorTableLength;
      //调色板置为空
      m_hPalette = NULL;
      //如果有颜色表,则将颜色表复制到新生成的DIB,并创建逻辑调色板
      if(m_nColorTableLength!=0){
            m_lpColorTable=(LPRGBQUAD)(m_lpDib+sizeof(BITMAPINFOHEADER));
            memcpy(m_lpColorTable,lpColorTable,sizeof(RGBQUAD) * m_nColorTableLength);
            MakePalette();
      }
      //m_pImgData指向DIB的位图数据起始位置
      m_pImgData = (LPBYTE)m_lpDib+sizeof(BITMAPINFOHEADER)+
            sizeof(RGBQUAD) * m_nColorTableLength;
      //将新位图数据复制到新的DIB中
      memcpy(m_pImgData,pImgData,imgBufSize);
}

3.数字图像显示功能实现
[1] 在CDemo1Doc类的头文件Demo1Doc.h中包含ImageDib类的声明文件ImageDib.h。

  #include "ImageDib.h"

[2] 文档类添加成员函数
在Visual C++集成开发环境(IDE)的【View】菜单中选择【ClassWizard】,在操作类名(Class name)中选择文档类CDemo1Doc,操作对象(object IDs)选择CDemo1Doc,对应消息(Messages)选择OnOpenDocument,该消息在单击应用程序中“文件”菜单的“打开”菜单项时产生。
单击添加函数按钮,在类CDemo1Doc加入OnOpenDocument成员函数,该函数在程序中出现OnOpenDocument消息时执行。该过程如图2-18所示。
screenshot

该成员函数生成代码如下:

BOOL CDemo1Doc::OnOpenDocument(LPCTSTR lpszPathName) 
{
      if (!CDocument::OnOpenDocument(lpszPathName))
            return FALSE;
            // TODO: Add your specialized creation code here
            return TRUE;
}

[3] 文档类添加成员对象
  在Visual C++集成开发环境(IDE)的工作区(Workspace)中选中类CDemoDoc,单击鼠标右键,弹出快捷菜单,如图2-19所示。
screenshot

  在弹出的快捷菜单中选中【ADD Member Variable】选项,弹出【Add Member Variable】(添加类新成员)对话框,如图2-20所示。
screenshot

[4] 在变量类型【Variable Type】中输入ImageDib,在变量名称【Variable Name】中输入m_dib,其他为默认值,单击按钮即可完成在CDemo1Doc类中添加ImageDib类成员变量m_dib。
[5] 对CDemo1Doc类的CDemo1Doc、~CDemo1Doc和OnOpenDocument函数代码重新编写如下:

  CDemo1Doc::CDemo1Doc()
  {
              m_dib = new ImageDib;
  }
  CDemo1Doc::~CDemo1Doc()
  {
              if (m_dib != NULL)
              {
                    delete m_dib;
                    m_dib = 0;
              }
        }
  BOOL CDemo1Doc::OnOpenDocument(LPCTSTR lpszPathName)      
  {
         if (m_dib.Read(lpszPathName) == TRUE) {      
              SetModifiedFlag(FALSE);     // start off with unmodified
              return TRUE;
        }
        else 
              return 0;
  }

[6] OnOpenDocument函数仅是实现将数字图像读入内存,如果在文档所对应的视窗内进行数字图像显示,还需对视窗类CDemo1View的OnDraw函数进行编程。首先在类CDemo1View的头文件Demo1View.h中包含ImageDib类的声明文件ImageDib.h,然后对OnDraw函数进行编程。

  void CDemo1View::OnDraw(CDC* pDC)
  {
        CDemo1Doc* pDoc = GetDocument();                     //获取文档类指针
        ImageDib pDib=pDoc->m_dib;                                //返回m_dib的指针
        CSize sizeFileDib = pDib.GetDimensions();            //获取DIB的尺寸
        pDib.Draw(pDC, CPoint(0, 0), sizeFileDib);             //显示DIB
  }

[7] 运行程序。选择菜单【文件】中的【打开】,输入框中输入随书光盘中的文件名demo.bmp,打开示例图像。运行结果如图2-21所示。
screenshot

2.2.3 图像增强

图像增强是图像模式识别中非常重要的图像预处理过程。图像增强的目的是通过对图像中的信息进行处理,使得有利于模式识别的信息得到增强,不利于模式识别的信息被抑制,扩大图像中不同物体特征之间的差别,为图像的信息提取及其识别奠定良好的基础。图像增强按实现方法不同可分为点增强、空域增强和频域增强。
1.点增强
点增强主要指图像灰度变换和几何变换。
图像的灰度变换也称为点运算、对比度增强或对比度拉伸,它是图像数字化软件和图像显示软件的重要组成部分。
灰度变换是一种既简单又重要的技术,它能让用户改变图像数据占据的灰度范围。一幅输入图像经过灰度变换后将产生一幅新的输出图像,由输入像素点的灰度值决定相应的输出像素点的灰度值。灰度变换不会改变图像内的空间关系。
图像的几何变换是图像处理中的另一种基本变换。它通常包括图像的平移、图像的镜像变换、图像的缩放和图像的旋转。通过图像的几何变换可以实现图像的最基本的坐标变换及缩放功能。
(1)图像灰度变换类
灰度变换可以按照预定的方式改变一幅图像的灰度直方图。除了灰度级的改变是根据某种特定的灰度变换函数进行的之外,灰度变换可以看做是“从像素到像素”的复制操作。如果输入图像为,输出图像为,则灰度变换可表示为:。其中函数被称为灰度变换函数,它描述了输入灰度值和输出灰度值之间的转换关系。一旦灰度变换函数确定,该灰度变换就完全被确定下来了。
本节设计一个图像灰度变换类,其目的是将有关图像的灰度变换操作的所有函数封装到该类中。下面介绍该类的创建。
[1] 打开2.1.5节所创建的工程文件demo1。
[2] 在工作区(Workspace)的类视图(Class View)编辑区中,选中【demo classes】后单击鼠标右键,并选择【New Class】。添加继承自ImageDib类的GrayTrans类进行有关图像的灰度变换操作的算法编程与实现,如图2-22所示。
screenshot

[3] 在文件GrayTrans.h中定义该类,如代码2-3所示。
代码2-3 GrayTrans函数

class GrayTrans:public ImageDib
{ 
public:
      //输出图像每像素位数
      int m_nBitCountOut;
      //输出图像位图数据指针
      unsigned char * m_pImgDataOut;
      //输出图像颜色表
      LPRGBQUAD m_lpColorTableOut;
      //输出图像的宽,以像素为单位
      int m_imgWidthOut;
      //输出图像的高,以像素为单位
      int m_imgHeightOut;
      //输出图像颜色表长度
      int m_nColorTableLengthOut;
public:
      //不带参数的构造函数
      GrayTrans();
      //带参数的构造函数
      GrayTrans(CSize size, int nBitCount, LPRGBQUAD lpColorTable, unsigned char *pImgData);
      //析构函数
      ~GrayTrans();
      //以像素为单位返回输出图像的宽和高
      CSize GetDimensions();
      //二值化
      void BinaryImage(int threshold=128);
      //反转
      void RevImage();
    //窗口变换
      void ThresholdWindow( int bTop, int bBottom);
      //分段线性拉伸
      void LinearStrech(CPoint point1,CPoint point2);
private:
      //单通道数据线性拉伸
      void LinearStrechForSnglChannel(unsigned char *pImgDataIn,
            unsigned char *pImgDataOut,int imgWidth,int imgHeight,
            CPoint point1,CPoint point2);
};

[4] 将GrayTrans.h头文件包含进demoView.cpp文件中。
[5] 利用资源管理器,在菜单条上加入【灰度变换】菜单及其子菜单【二值化】、【直方图】、【直方图均衡】、【反转】、【阈值变换】、【窗口变换】、【分段线性拉伸】,如图2-23所示。
[6] 双击上述菜单项,设置其ID值,如图2-24所示。上述菜单项中,【二值化】ID值定义为id_Binary;【直方图】ID值定义为id_HistogramDraw;【直方图均衡】ID值定义为id_HistogramAver;【反转】ID值定义为id_ImageReverse;【阈值变换】ID值定义为id_ImgThresh;【窗口变换】ID值定义为id_ThresholdWindow;【分段线性拉伸】ID值定义为id_LinearStrecth。
screenshot

各子函数代码的实现可参见本书所附程序。
(2)图像几何变换类
本节设计一个图像几何变换类,其目的是将有关图像的几何变换操作的所有函数封装到该类中。创建该类的具体步骤如下所示。
[1] 打开2.1.5节所创建的工程文件demo1。
[2] 在工作区(Workspace)的类视图(Class View)编辑区中,选中【demo classes】后单击鼠标右键,并选择【New Class】。添加继承自ImageDib类的GeometryTrans类进行有关图像的几何变换操作的算法编程与实现,如图2-25所示。
screenshot

[3] 在文件GeometryTrans.h中定义该类,如代码2-4所示。
代码2-4 GeometryTrans函数

class GeometryTrans : public ImageDib  
{
public:
    //输出图像每像素位数
      int m_nBitCountOut;
      //输出图像位图数据指针
      unsigned char * m_pImgDataOut;
      //输出图像颜色表
      LPRGBQUAD m_lpColorTableOut;
      //输出图像的宽
      int m_imgWidthOut;
      //输出图像的高
      int m_imgHeightOut;
      //输出图像颜色表长度
      int m_nColorTableLengthOut;
public:
      //构造函数
      GeometryTrans();
      //带参数的构造函数
      GeometryTrans(CSize size, int nBitCount, LPRGBQUAD lpColorTable, unsigned char 
*pImgData);
      //析构函数
      ~GeometryTrans();
      //以像素为单位返回输出图像的宽和高
      CSize GetDimensions();
      //平移
      void Move(int offsetX, int offsetY);
      //缩放
      void Zoom(float ratioX, float ratioY);
      //水平镜像
      void MirrorHorTrans();
      //垂直镜像
      void MirrorVerTrans();
      //顺时针旋转90度
      void Clockwise90();
      //逆时针旋转90度
      void Anticlockwise90();
      //旋转180
      void Rotate180();
      //0~360度之间任意角度旋转 
      void Rotate(int angle);//angle旋转角度
};

[4] 将GeometryTrans.h头文件包含进demoView.cpp文件中。
[5] 利用资源管理器,在菜单条上加入【几何变换】菜单及其子菜单【平移】、【水平镜像】、【垂直镜像】、【缩放】、【旋转】、【顺时针旋转90度】、【逆时针旋转90度】、【旋转180度】、【任意角度旋转】,如图2-26所示。
[6] 双击上述菜单项,设置其ID值,如图2-27所示。上述菜单项中,【平移】ID值定义为id_Move;【水平镜像】ID值定义为id_HorizontalMirror;【垂直镜像】ID值定义为id_VerticalMirror;【缩放】ID值定义为id_Zoom;【顺时针旋转90度】ID值定义为id_Clockwise90;【逆时针旋转90度】ID值定义为id_Anticlockwise90;【旋转180度】ID值定义为id_Rotate180;【任意角度旋转】ID值定义为id_FreeRotate。
screenshot

screenshot

各子函数代码的实现可参见随书光盘。
2.空域增强
一幅数字图像包括光谱、空间、时间3类基本信息。对于一幅灰度图像,其光谱信息是以像素的灰度值来体现的,对光谱信息的增强可以通过上一节介绍的各种灰度变换方法来实现,如直方图均衡化和直方图规定化可以通过改变像素的灰度值以达到信息增强的目的。图像间的差值运算可以提供图像的动态信息(即时间信息)。本节将要介绍对图像的空间信息进行增强的方法。
(1)图像空域增强
图像的空间信息可以反映图像中物体的位置、形状、大小等特征,而这些特征可以通过一定的物理模式来描述。例如,物体的边缘轮廓由于灰度值变化剧烈一般出现高频率特征,而一个比较平滑的物体内部由于灰度值比较均一则呈现低频率特征。因此,根据需要可以分别增强图像的高频和低频特征。对图像的高频增强可以突出物体的边缘轮廓,从而起到锐化图像的作用。例如,对于人脸的比对查询,就需要通过高频增强技术来突出五官的轮廓。相应地,对图像的低频部分进行增强可以对图像进行平滑处理,一般用于图像的噪声消除。
图像的空域增强是应用模板卷积方法对每一像素的邻域进行处理完成的,一般可分为线性和非线性两类。无论采用什么样的增强方法,其实现步骤大体相同。具体过程如下:
1)将模板在图像中漫游移动,并将模板中心与每个像素依次重合(边缘像素除外)。
2)将模板中的各个系数与其对应的像素一一相乘,并将所有结果相加(或进行其他四则运算)。
3)将步骤2中的结果赋给图像中对应模板中心位置的像素。
图2-28给出了应用模板进行滤波的示意图。图2-28a是一幅图像的一小部分,共9个像素, pi(i=0,1,…,8)表示像素的灰度值。图2-28b表示一个3×3的模板,ki(i=0,1,…,8)称为模板系数,模板的大小一般取奇数(如3×3,5×5等)。现将模板在图像中漫游,并使k0与图2-28a所示的p0像素重合,即可由下式计算输出图像(增强图像)中与p0相对应的像素的灰度值r。

对每个像素按上式进行计算即可得到增强图像中所有像素的灰度值。
screenshot

(2)图像空域增强类
本节设计一个图像空域增强类,其目的是将有关图像的空域变换操作的所有函数封装到该类中。创建该类的具体步骤如下所示。
[1] 打开2.1.5节所创建的工程文件demo1。
[2] 在工作区(Workspace)的类视图(Class View)编辑区中,选中【demo classes】后单击鼠标右键,并选择【New Class】。添加继承自ImageDib类的ImageEnhance类,进行有关图像的空域增强操作的算法编程与实现,如图2-29所示。
screenshot

[3] 在文件ImageEnhance.h中定义该类,如代码2-5所示。

代码2-5 ImageEnhance函数

class ImageEnhance:public ImageDib  
{
public:
          int m_nBitCountOut;                                    //输出图像每像素位数
      unsigned char * m_pImgDataOut;                        //输出图像位图数据指针
      LPRGBQUAD m_lpColorTableOut;                      //输出图像颜色表
      int m_nColorTableLengthOut;                            //输出图像颜色表长度
public:
      ImageEnhance();                                          //构造函数
      ImageEnhance(CSize size, int nBitCount, LPRGBQUAD lpColorTable, unsigned char 
*pImgData);                              //带参数的构造函数
    ~ImageEnhance();                                          //析构函数
      void NeiAveTemplate(int TempH, int TempW, int TempCX, int TempCY, float 
*fpTempArray, float fCoef);     //采用均值模板进行图像平滑
      //中值滤波
      BYTE FindMedianValue(unsigned char* lpbArray,int iArrayLen);  
      void MedianSmooth(int iFilterH, int iFilterW, int iFilterCX, int iFilterCY);
      //拉普拉斯锐化,转化为模板运算
      void LapTemplate(int TempH, int TempW, int TempCX, int TempCY, float *fpTempArray, 
float fCoef);
      //梯度锐化
      void GradeSharp(int Thresh);
      //选择掩模平滑
      void ChooseMaskSmooth();
};

[4] 将ImageEnhance.h头文件包含进demoView.cpp文件中。
[5] 利用资源管理器,在菜单条上加入【图像空域增强】菜单及其子菜单【邻域平均】、【中值平均】、【掩模平滑】、【梯度锐化】、【拉普拉斯锐化】,如图2-30所示。
screenshot

[6] 双击上述菜单项,设置其ID值,如图2-31所示。上述菜单项中,【高斯噪声】ID值定义为id_GaussNoise;【椒盐噪声】ID值定义为id_PepperSaltNosie;【邻域平均】ID值定义为id_PowerSmooth;【中值平均】ID值定义为id_MedianSmooth;【掩模平滑】ID值定义为id_ChooseMaskSmooth;【梯度锐化】ID值定义为id_GradeSharp;【拉普拉斯锐化】ID值定义为id_LaplaceSharp。
screenshot

各子函数代码的实现可参见随书光盘。
3.频域增强
图像空域增强一般只是对数字图像进行局部增强,而图像频域增强则可以对图像进行全局增强。
频域增强技术是在数字图像的频率域空间对图像进行滤波,因此需要将图像从空间域变换到频率域,一般通过傅里叶变换实现。在频率域空间的滤波与空域滤波一样可以通过卷积实现,因此傅里叶变换和卷积理论是频域滤波技术的基础。
假定函数f (x,y)与线性位不变算子h(x,y)的卷积结果是g(x,y),即:
g(x,y)=h(x,y)*f(x,y)
相应的,由卷积定理可得到下述频域关系:
G(u,v)=H(u,v)×F(u,v)
其中G、H、F分别是函数g、h、f的傅里叶变换,H(u,v)称为传递函数或滤波器函数。在图像增强中,图像函数f (x,y)是已知的,即待增强的图像,因此F(u,v)可由图像的傅里叶变换得到。在实际应用中,首先需要确定的是H(u,v),然后就可以求得G(u,v),对G(u,v)求傅里叶反变换后即可得到增强的图像g(x,y)。g(x,y)可以突出f(x,y)的某一方面的特征,如,利用传递函数H(u,v)突出F(u,v)的高频分量,以增强图像的边缘信息,即高通滤波;反之,如果突出F(u,v)的低频分量,就可以使图像显得比较平滑,即低通滤波。
在介绍具体的滤波器之前,先根据以上的描述给出频域滤波的主要步骤。
1)对原始图像f (x,y)进行傅里叶变换得到F(u,v);
2)将F(u,v)与传递函数H(u,v)进行卷积运算得到G(u,v);
3)将G(u,v)进行傅里叶反变换得到增强图像g(x,y)。
本节设计一个图像频域增强类,其目的是将有关图像的频域变换操作的所有函数封装到该类中。创建该类的具体步骤如下所示。
[1] 打开2.1.5节所创建的工程文件demo1。
[2] 在工作区(Workspace)的类视图(Class View)编辑区中,选中【demo classes】后单击鼠标右键,并选择【New Class】。添加继承自ImageDib类的ImageFreqEnhance类进行有关图像的频域增强操作的算法编程与实现,如图2-32所示。
screenshot

[3] 在文件ImageFreqEnhance.h中定义该类,如代码2-6所示。
代码2-6 ImageFreqEnhance函数

class ImageFreqEnhance:public ImageDib  
{
public:
      //输出图像每像素位数
      int m_nBitCountOut; 
      //输出图像位图数据指针
      unsigned char * m_pImgDataOut; 
      //输出图像颜色表
      LPRGBQUAD m_lpColorTableOut; 
      int m_imgWidthOut; //输出图像的宽
      int m_imgHeightOut;//输出图像的高
      int m_nColorTableLengthOut;//输出图像颜色表长度
public:
      //傅里叶变换类对象
      FourierTrans FFtTrans;
public:
      //构造函数
      ImageFreqEnhance();
      //带参数的构造函数
      ImageFreqEnhance(CSize size, int nBitCount, LPRGBQUAD lpColorTable, 
            unsigned char *pImgData);
      CSize GetDimensions();//以像素为单位返回输出图像的宽和高
      void InputImageData(CSize size, int nBitCount, LPRGBQUAD lpColorTable, 
            unsigned char *pImgData);      //输入原图像数据
      void IdealLowPassFilter(int nWidth, int nHeight, int nRadius);    //理想低通滤波
      void ButterLowPassFilter(int nWidth, int nHeight, int nRadius);    //巴特沃斯低通滤波
      void IdealHighPassFilter(int nWidth, int nHeight, int nRadius);      //理想高通滤波
      void ButterHighPassFilter(int nWidth, int nHeight, int nRadius);    //巴特沃斯高通滤波
      //析构函数
      virtual~ImageFreqEnhance();
};

[4] 将ImageFreqEnhance.h头文件包含进demoView.cpp文件中。
[5] 利用资源管理器,在菜单条上加入【图像频域增强】菜单及其子菜单【快速傅里叶变换】、【快速傅里叶反变换】、【理想低通滤波】、【巴特沃斯低通滤波】、【理想高通滤波】、【巴特沃斯高通滤波】、【小波变换】、【小波反变换】,如图2-33所示。
screenshot

[6] 双击上述菜单项,设置其ID值,如图2-34所示。上述菜单项中,【快速傅里叶变换】ID值定义为id_QuickFFt;【快速傅里叶反变换】ID值定义为id_QuickFFt_Reverse;【理想低通滤波】ID值定义为id_IdealLowPass;【巴特沃斯低通滤波】ID值定义为id_ButterLowPass;【理想高通滤波】ID值定义为id_IdealHighPass;【巴特沃斯高通滤波】ID值定义为id_ButterHighPass;【小波变换】ID值定义为id_HarrWaveletTrans;【小波反变换】ID值定义为id_HarrWavRevTrans。

screenshot

各子函数代码的实现可参见随书光盘。

2.2.4 图像形态学处理

数学形态学是一种应用于图像处理和模式识别领域的新的方法。形态学是生物学的一个分支,常用来处理动物和植物的形状和结构。数学形态学是建立在严格的数学理论基础上的科学。用于描述数学形态学的语言是集合论,利用数学形态学对物体几何结构的分析过程就是主客体相互逼近的过程。利用数学形态学的几个基本概念和运算,将结构元素灵活地组合、分解,应用形态变换序列达到分析的目的。
数学形态学是以集合代数为基础的,用集合的方法定量描述几何结构的科学。数学形态学应用于数字图像处理以后,用形态学来处理图像去描述某些区域的形状,如边界曲线、骨架结构和凸形外壳。另外,还可用形态学技术进行预测和快速处理,如形态过滤、形态细化、形态修饰等。而这些处理都是基于一些基本运算实现的。
1.数学形态学的基本概念
用于描述形态学的语言是集合论。集合代表图像中物体的形状,例如:在二值图像中所有的黑色像素点的集合就是这幅图像的完整描述。在二值图像中,当前集合是指二维整形空间的成员,集合中的每个元素就是一个二维变量,用表示,按规则代表图像中的一个黑色像素点。灰度数字图像可以用三维集合来表示。在这种情况下,集合中每个元素的前两个元素表示像素点的坐标,第三个变量代表离散的灰度值。在更高维的空间集合中可以包括其他的图像属性,如颜色和时间。
形态学运算的质量取决于所选取的结构元素和形态变换。结构元素的选择要根据具体情况来确定,而形态运算的选择必须满足一些基本的约束条件。这些约束条件称为图像定量分析的原则。下面列出了数学形态学的几条定量分析原则。
(1)平移不变性
设待分析的图像为,表示某种图像变换或运算,表示经变换或运算后的新图像。设为一矢量,表示将图像平移一个位移矢量后的结果,那么,平移不变性原则可表示如下:

此式说明,图像先平移然后变换的结果与图像先变换后平移的结果相一致。
(2)尺度变换不变性
设缩放因子是一个正的实常数,表示对图像所做的相似变换,则尺度变换不变性可表示如下:

设图像运算为结构元素对的腐蚀(记为),为结构元素对的腐蚀,则上式具体化为

(3)局部知识原理
如果是一个图形(闭集),则相对于存在另一个闭集,使得对于图形有下式成立:

可以将Z理解为一个“掩模”。在实际中,观察某一个对象时,每次只能观察一个局部,即某一掩模覆盖的部分。该原则要求对每种确定的变换或运算,当掩模选定以后,都能找到一个相应的模板,使得通过所观察到的局部性质,即与整体性质相一致。
(4)半连续原理
在研究一幅图像时,常采用逐步逼近的方法,即对图像的研究往往需要通过一系列图像,,…,,…的研究实现,其中诸个逐步逼近X。半连续原理要求各种图像变换后应满足这样的性质:对真实图像的处理结果应包含在对一系列图像的处理结果内。
(5)形态运算的基本性质
除了一些特殊情况外,数学形态学处理一般都是不可逆的。实际上,对图像进行重构的思想在该情况下是不恰当的。任何形态处理的目的都是通过变换法去除不感兴趣的信息,保留感兴趣的信息。在形态运算中的几个关键性质如下:
 递增性:
 反扩展性:
 幂等性:
其中:表示欧几里得(Euclidean)空间的幂级。
2.数学形态学的表示
集合论是数学形态学的基础,在这里我们首先对集合论的一些基本概念做个介绍。对于形态处理的讨论,我们将从两个最基本的模加处理和模减处理开始,它们是以后大多数形态处理的基础。
(1)集合
具有某种性质的确定的有区别的事物的全体。如果某种事物不存在,称为空集。集合常用大写字母A、B、C…表示,空集用表示。
设E为一自由空间,是由集合空间E所构成的幂集,集合,则集合X和B之间只能有以下3种形式,如图2-35所示。
1)集合B1击中X(表示为),即。
2)集合B2离于X(表示为),即。
3)集合B3含于X(表示为)。
(2)元素
构成集合的每一个事物称之为元素。元素常用小写字母a、b、c…表示。应注意的是,任何事物都不是空集的元素。
(3)平移转换
设A和B是两个二维集合,A和B中的元素分别是:

定义,对集合的平移转换为:

(4)子集
当且仅当集合A的所有元素都属于B时,称A是B的子集。
(5)补集
定义集合A的补集为:

(6)差集
定义集合A和B的差集为:

(7)映像
定义集合A的映像为,定义为:

(8)并集
由集合A和B的所有元素组成的集合称为A和B的并集。
(9)交集
由集合A和B的公共元素组成的集合称为A和B的交集。
3.图像形态学类的设计
本节设计一个图像形态学类,其目的是将有关图像的形态学变换操作的所有函数封装到该类中。下面介绍该类的创建步骤。
[1] 打开2.1.5节所创建的工程文件demo1。
[2] 在工作区(Workspace)的类视图(Class View)编辑区中,选中【demo classes】后单击鼠标右键,并选择【New Class】。添加继承自ImageDib类的Morphology类进行有关图像的形态学变换操作的算法编程与实现,如图2-36所示。

screenshot

[3] 在文件Morphology.h中定义该类,如代码2-7所示。
代码2-7 Morphology函数
//结构元素对专门为击中、击不中变换而定义

struct ElementPair
{
      int hitElement[9];
      int missElement[9];
};
class Morphology:public ImageDib
{
public:
      //输出图像每像素位数
      int m_nBitCountOut;                        
      //输出图像位图数据指针
      unsigned char * m_pImgDataOut;      
      //输出图像颜色表
      LPRGBQUAD m_lpColorTableOut;      
      //输出图像的宽,以像素为单位
      int m_imgWidthOut;                        
      //输出图像的高,以像素为单位
      int m_imgHeightOut;                        
      //输出图像颜色表长度
      int m_nColorTableLengthOut;            
      //结构元素(模板)指针
      int *m_maskBuf;                        
      //结构元素宽
      int m_maskW;                              
      //结构元素高
      int m_maskH;                              
      //定义8个方向的击中、击不中变换结构元素对
      ElementPair m_hitMissTemp[8];      
public:
      Morphology();                  //不带参数的构造函数
      Morphology(CSize size, int nBitCount, LPRGBQUAD lpColorTable, unsigned char
*pImgData);          //带参数的构造函数
      virtual~Morphology();          //析构函数
public:
      CSize GetDimensions();          //返回输出图像的尺寸
      void ImgErosion(unsigned char *imgBufIn,unsigned char *imgBufOut,      int imgWidth,
            int imgHeight,int *TempBuf, int TempW, int TempH);//腐蚀
      void ImgDilation(unsigned char *imgBufIn,unsigned char *imgBufOut,int imgWidth,
            int imgHeight,int *maskBuf, int maskW, int maskH);//膨胀
      void Open();                  //二值开
      void Close();                  //二值闭
      void ImgThinning();          //击中、击不中细化
      void DefineElementPair();    //定义击中、击不中变换的结构元素对
      void HitAndMiss(unsigned char *imgBufIn, unsigned char *imgBufOut,
            int imgWidth,int imgHeight,ElementPair hitMissMask);      //击中、击不中变换
};

[4] 将Morphology.h头文件包含进demoView.cpp文件中。
[5] 利用资源管理器,在菜单条上加入【形态学】菜单及其子菜单【二值腐蚀】、【二值膨胀】、【二值开】、【二值闭】、【击中击不中细化】,如图2-37所示。
[6] 双击上述菜单项,设置其ID值,如图2-38所示。上述菜单项中,【二值腐蚀】ID值定义为id_Erosion;【二值膨胀】ID值定义为id__Dilation;【二值开】ID值定义为id_Open;【二值闭】ID值定义为id_Close;【击中击不中细化】ID值定义为id_Thinning。

screenshot

各子函数代码的实现可参见随书光盘。

2.2.5 图像分割

图像分割是一种重要的图像处理技术,是图像分析和理解的第一步。图像分割在很多领域有着广泛的应用,如工业图像处理、军事图像处理、生物医学图像处理、图像传输、文本图像分析处理和识别、身份鉴定、机器人视觉等。不同类型的图像,有不同的分割方法对其进行分割,而同时,某些分割方法也只适用于某些特殊类型的图像分割。
图像分割就是将图像分成具有不同特性的区域,并提取出感兴趣的区域的过程。早期的图像分割方法可以分成两大类:一是边界法,应用这种方法时一般假设图像分割结果的某个子区域在原来图像中一定会有边缘存在;二是区域法,应用这种方法时一般假设假设图像分割结果的某个子区域一定会有相同的性质,而不同区域的像素则没有共同的性质。
现在,随着计算机处理能力的提高,涌现出了很多其他的方法,如基于模型的图像分割、基于彩色分量、纹理的图像分割以及基于人工智能的图像分割方法等。
如图2-39所示的是图像分割的方法框架。

screenshot

本节设计一个图像分割类,其目的是将有关图像的分割操作的所有函数封装到该类中。下面介绍该类的创建步骤。
[1] 打开2.1.5节所创建的工程文件demo1。
[2] 在工作区(Workspace)的类视图(Class View)编辑区中,选中【demo classes】后单击鼠标右键,并选择【New Class】。添加继承自ImageDib类的ImgSegment类进行有关图像分割操作的算法编程与实现,如图2-40所示。
[3] 在文件ImgSegment.h中定义该类,如代码2-8所示。

代码2-8  ImgSegment函数
class ImgSegment:public ImageDib
{
public:
      //输出图像每像素位数
      int m_nBitCountOut;
      //输出图像位图数据指针
      unsigned char * m_pImgDataOut;      
      //输出图像颜色表
      LPRGBQUAD m_lpColorTableOut;      
    //输出图像的宽
      int m_imgWidthOut;      
      //输出图像的高
      int m_imgHeightOut;
      //输出图像颜色表长度
      int m_nColorTableLengthOut;      
public:
      //不带参数的构造函数
      ImgSegment();      
      //带参数的构造函数
      ImgSegment(CSize size, int nBitCount, LPRGBQUAD lpColorTable, 
            unsigned char *pImgData);
      //析构函数
      virtual~ImgSegment();
public:
      //以像素为单位返回输出图像的尺寸
      CSize GetDimensions();
    //自适应阈值分割
      void AdaptThreshSeg(unsigned char *pImgData);
      //Roberts算子
      void Roberts();      
    //Sobel算子
      void Sobel();      
      //Prewitt算子
      void Prewitt();      
      //Laplacian算子
      void Laplacian();      
public:
      //区域生长
      void RegionGrow(CPoint SeedPos, int thresh);
      //曲线跟踪
      void EdgeTrace();
};

[4] 将ImgSegment.h头文件包含进demoView.cpp文件中。
[5] 利用资源管理器,在菜单条上加入【图像分割】菜单及其子菜单【直方图阈值分割】、【自适应阈值分割】、【Robert算子】、【Sobel算子】、【Prewitt算子】、【Laplacian算子】、【边界跟踪】、【区域增长】,如图2-41所示。

screenshot

[6] 双击上述菜单项,设置其ID值,如图2-42所示。上述菜单项中,【直方图阈值分割】ID值定义为id_HistThreshSeg;【自适应阈值分割】ID值定义为id_AdaptiveThreshold;【Robert算子】ID值定义为id_Robert;【Sobel算子】ID值定义为id_Sobel;【Prewitt算子】ID值定义为id_Prewitt;【Laplacian算子】ID值定义为id_Laplacian;【边界跟踪】ID值定义为id_EdgeTrace;【区域增长】ID值定义为id_RegionGrow。

screenshot

各子函数代码的实现可参见随书光盘。

  • 3
    点赞
  • 0
    评论
  • 9
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

表情包
插入表情
评论将由博主筛选后显示,对所有人可见 | 还能输入1000个字符
©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值