Conner's profile☆ Conner Wang ☆PhotosBlogListsMore Tools Help

Blog


    August 14

    VC++和VC++.NET中与图像处理有关的几个概念、结构和类

      最近一直在看VC++有关图像处理方面的书,终于把以前一直混淆的几个概念、结构和类弄清楚了,特整理如下。如有错误,请大家批评指正,不胜感激。下一步想好好学习学习OpenCV,希望也能总结点东西。

    一、DDB与DIB位图

      一个Windows的位图实际上是一些和显示像素相对应的位阵列,它有两种类型:一种称之为GDI(Graphic Device Interface)位图,另一种是DIB位图(Device-Independent Bitmap)

      GDI位图包含了一种和Windows的GDI模块有关的Windows数据结构,该数据结构是与设备有关的,故此位图又称为DDB位图(Device-Dependent Bitmap)。当用户的程序取得位图数据信息时,其位图显示方式视显示卡而定。由于GDI位图的这种设备依赖性,当位图通过网络传送到另一台PC,很可能就会出现问题。

      DIB比GDI位图有很多编程优势,例如它自带颜色信息,从而使调色板管理更加容易。且任何运行Windows的机器都可以处理DIB,并通常以后缀为.BMP的文件形式被保存在磁盘中或作为资源存在于程序的EXE或DLL文件中。

    二、CBitmap类、BITMAP结构 

      CBitmap类继承自CGdiObject,是封装了图形设备接口(GDI)的位图,提供成员函数来操纵位图。要使用一个CBitmap对象,构造该对象,用初始化成员函数之一把一个位图句柄连接到该对象,然后调用该对象的成员函数。

      CBitmap类主要用于处理DDB位图,封装了与DDB位图操作函数相关的数据结构和操作函数。结构体BITMAP定义了DDB位图的类型、宽度、高度、颜色和像素值,其定义如下:

    typedef struct _tagBITMAP       
    {
      LONG          bmType ;                      // set to 0       
      LONG          bmWidth ;                     // width in pixels       
      LONG          bmHeight ;                   // height in pixels       
      LONG          bmWidthBytes ;                // width of row in bytes       
      WORD         bmPlanes ;                    // number of color planes       
      WORD         bmBitsPixel ;                 // number of bits per pixel       
      LPVOID       bmBits ;                             // pointer to pixel bits       
    }       
    BITMAP, * PBITMAP ;

      而CBitmap的LoadBitmap、CreateCompatibleBitmap、SetBitmapBits、GetBitmap等成员函数则定义了对DDB位图的装载、创建、设定位值和属性查询等操作。

      创建或装入内存的位图必须用CDC::SelectObject函数来将其选入设备上下文中,然后用CDC的BitBlt或StretchBlt函数显示出来,这两个函数的原型如下:

    BOOL BitBlt(int x, int y, int nWith, int nHeight, CDC* pSrcDC, int xSrc, int ySrc, DWORD dwRop);

      该函数把源设备上下文中的位图复制到本身的设备上下文中,两个设备下下文可以是内存设备下下文,也可以是同一个设备上下文。

    三、Image类和Bitmap类

      在VC++.NET中,GDI+的Image类封装了对BMP、GIF、JPEG、PNG、TIFF、WMF(Windows元文件)和EMF(增强WMF)图像文件的调入、显示、格式转换以及简单处理(如缩放、旋转、拉伸等)的功能。Bitmap类(注意不是结构BITMAP)是从Image类继承的一个图像(另一个从Image继承的类是Metafile类),它封装了Windows位图操作的常用功能。例如,Bitmap::SetPixel和Bitmap::GetPixel分别用来对位图进行读写像素操作,从而可以为图像的柔化和锐化处理提供一种可能。这些功能和MFC的新类CImage功能基本一样,如果仅用于图像的读取与显示,用Bitmap类或Image类是不错的选择,如果是做图像处理,则CImage可能更符合MFC程序员的编程习惯。

    四、CImage类

      CImage类是VC++.NET中MFC和ATL共享的新类,它能从外部磁盘中调入一个JPEG、GIF、BMP和PNG格式的图像文件加以显示,而且这些文件格式可以相互转换。

      CImage既能处理DIB位图也能处理非DIB位图,但你可以仅用DIB位图来Create或CImage::Load。你也可以使用Attach把一个非DIB位图连接给CImage对象,但你不能使用下列方法,这些方法仅支持DIB位图:GetBits、GetColorTable、GetMaxColorTable、Entries、GetPitch、GetPixelAddress、IsIndexed、SetColorTable。要确定一个连接的位图是否是一个DIB位图,调用IsDibSection。

      由于CImage在不同的Windows操作系统中其某些性能是不一样的,因此在使用时要特别注意。例如,CImage::PlgBlt和CImage::MaskBlt只能在 Windows NT 4.0 或更高版本中使用,但不能运行在Windows 95/98 应用程序中。CImage::AlphaBlend和CImage::TransparentBlt也只能在 Windows 2000/98或其更高版本中使用。即使在Windows 2000运行程序还必须将stdafx.h文件中的WINVER和_WIN32_WINNT的预定义修改成0x0500才能正常使用。 CImage可以在MFC或ATL中使用。当使用CImage创建一个项目时,必须包含atlimage.h文件。

      CImage封装了DIB(设备无关位图)的功能,因而可以让我们能够处理每个位图像素。

      CImage提供了HBITMAP操作符,因此HBITMAP为参数的地方,都可以用CImage来替代。

    五、传统的VC++中的图像处理方法与VC++.NET中的图像处理方法

      由于DIB图不依赖于具体设备,因此可以用来永久性地保存图象。DIB一般是以*.BMP文件的形式保存在磁盘中的,有时也会保存在*.DIB文件中。运行在不同输出设备下的应用程序可以通过DIB来交换图象。因而在数字图象处理中会经常用到DIB位图。DIB还可以用一种RLE算法来压缩图像数据,但一般来说DIB是不压缩的。

      在VC++.net之前MFC未提供现成的类来封装DIB,这给MFC用户带来很多不便。因为用户要想使用DIB,首先应该了解DIB的结构。

      在内存中,一个完整的DIB由两部分组成:一个BITMAPINFO结构和一个存储像素阵列的数组。BITMAPINFO描述了位图的大小,颜色模式和调色板等各种属性,其定义为 typedef struct tagBITMAPINFO { 
        BITMAPINFOHEADER bmiHeader; 
        RGBQUAD bmiColors[1]; //颜色表
    } BITMAPINFO;

    RGBQUAD结构用来描述颜色,其定义为
    typedef struct tagRGBQUAD {
      BYTE     rgbBlue; //蓝色的强度
      BYTE     rgbGreen; //绿色的强度
      BYTE     rgbRed; //红色的强度
      BYTE     rgbReserved; //保留字节,为0
    } RGBQUAD;

    注意,RGBQUAD结构中的颜色顺序是BGR,而不是平常的RGB。

    BITMAPINFOHEADER结构包含了DIB的各种信息,其定义为
    typedef struct tagBITMAPINFOHEADER{
      DWORD     biSize; //该结构的大小
      LONG     biWidth; //位图的宽度(以像素为单位)
      LONG     biHeight; //位图的高度(以像素为单位)
      WORD     biPlanes; //必须为1
      WORD     biBitCount //每个像素的位数(1、4、8、16、24或32)
      DWORD     biCompression; //压缩方式,一般为0或BI_RGB (未压缩)
      DWORD     biSizeImage; //以字节为单位的图象大小(仅用于压缩位图)
      LONG     biXPelsPerMeter; //以目标设备每米的像素数来说明位图的水平分辨率
      LONG     biYPelsPerMeter; //以目标设备每米的像素数来说明位图的垂直分辨率
      DWORD     biClrUsed; /*颜色表的颜色数,若为0则位图使用由biBitCount指定的最大颜色数*/
      DWORD     biClrImportant; //重要颜色的数目,若该值为0则所有颜色都重要
    } BITMAPINFOHEADER;

      与DDB不同,DIB的字节数组是从图象的最下面一行开始的逐行向上存储的,也即等于把图象倒过来然后在逐行扫描。另外,字节数组中每个扫描行的字节数必需是4的倍数,如果不足要用0补齐。

    DIB可以存储在*.BMP或*.DIB文件中。DIB文件是以BITMAPFILEHEADER结构开头的,该结构的定义为
    typedef struct tagBITMAPFILEHEADER {
      WORD     bfType; //文件类型,必须为“BM”
      DWORD     bfSize; //文件的大小
      WORD     bfReserved1; //为0
      WORD     bfReserved2; //为0
      DWORD     bfOffBits; //存储的像素阵列相对于文件头的偏移量
    } BITMAPFILEHEADER;

      紧随该结构的是一个BITMAPINFOHEADER结构,然后是RGBQUAD结构组成的颜色表(如果有的话),文件最后存储的是DIB的像素阵列。

      DIB的颜色信息储存在自己的颜色表中,程序一般要根据颜色表为DIB创建逻辑调色板。在输出一幅DIB之前,程序应该将其逻辑调色板选入到相关的设备上下文中并实现到系统调色板中,然后再调用相关的GDI函数(如::SetDIBitsToDevice或::StretchDIBits)输出DIB。在输出过程中,GDI函数会把DIB转换成DDB,这项工作主要包括以下两步:将DIB的颜色格式转换成与输出设备相同的颜色格式;将DIB像素的逻辑颜色索引转换成系统调色板索引。 

      看到这么多结构,你是不是已经眼花缭乱了,然而更不幸的还在后面。以上说的DIB是Windows中的DIB,还有一种DIB是OS/2 DIB,它和Windows DIB的主要区别是位图信息结构(信息头和颜色表结构)。它们的图像数据的存储方式是完全一样的。在OS/2 DIB中,与WIndows DIB的BITMAPFILEHEADER、BITMAPINFOHEADER、RGBQUAD相对应的结构分别是BITMAPCOREHEADER、BITMAPCOREINFO、和RGBTRIPLE。它们的定义分别如下:

    typedef struct tagBITMAPCOREHEADER  // bmch       
    {       
      DWORD         bcSize ;              // size of the structure = 12       
      WORD          bcWidth ;             // width of image in pixels       
      WORD          bcHeight ;           // height of image in pixels       
      WORD          bcPlanes ;            // = 1       
      WORD          bcBitCount ;          // bits per pixel (1, 4, 8, or 24)       
    }       
    BITMAPCOREHEADER, * PBITMAPCOREHEADER ;

    typedef struct tagBITMAPCOREINFO  // bmci
    {       
      BITMAPCOREHEADER      bmciHeader ;                  // core-header structure       
      RGBTRIPLE             bmciColors[1] ;               // color table array       
    }       
    BITMAPCOREINFO, * PBITMAPCOREINFO ;

    typedef struct tagRGBTRIPLE  // rgbt       
    {       
      BYTE     rgbtBlue ;       // blue level       
      BYTE     rgbtGreen ;      // green level       
      BYTE     rgbtRed ;        // red level       
    }       
    RGBTRIPLE;

      因此再操作之前你应该先根据BITMAPINFOHEADER的大小来判断是哪一种DIB,然后再进行操作。

      由于MFC未提供一个封装好的易用的DIB类,用户在使用DIB时将面临繁重的Windows API编程任务。不信你看看Microsoft提供的MFC的DibLook例程提就知道了。所以传统的图像处理方法一般都会把这些Win32 SDK中的操作DIB位图的APIs做一个封装,作为一个通用的类来使用,以减少后续算法编写中的编程负担。

      VC++.NET提供了多种常用图像文件格式(如BMP、TIF、GIF、JPEG与PNG等)的输入/输出模块,ATL中的CImage类极大地简化了图像数据的操作,因此VC++.NET中的图像处理应以CImage为基础。
      现在市面上有一些冠名教用VC++.NET做图像处理的书,但是编程方法还是老的一套,没有跳出设备相关位图(DDB)与设备无关位图(DIB)概念的条框,编程方法比较复杂、繁琐、低效。用VC++.NET环境做图像处理,应该以CImage类为基础,完全跳出DDB、DIB概念的条框,才能使得处理方法返朴归真,大为简化。
              作者:http://conner-wang.spaces.live.com转载请注明出处!

    August 13

    基于VC.NET的GDI+编程之CImage[转载]

      我们知道,Visual C++的CBitmap类和静态图片控件的功能是比较弱的,它只能显示出在资源中的图标、位图、光标以及图元文件的内容,而不像VB中的Image控件可以显示出绝大多数的外部图像文件(BMP、GIF、JPEG等)。因此,想要在对话框或其他窗口中显示外部图像文件则只能借助于第三方提供的控件或代码。现在,MFC和ATL共享的新类CImage为图像处理提供了许多相应的方法,这使得Visual C++在图像方面的缺憾一去不复返了。

    CImage类概述

      CImage是MFC和ATL共享的新类,它能从外部磁盘中调入一个JPEG、GIF、BMP和PNG格式的图像文件加以显示,而且这些文件格式可以相互转换。由于CImage在不同的Windows操作系统中其某些性能是不一样的,因此在使用时要特别注意。例如,CImage::PlgBlt和CImage::MaskBlt只能在 Windows NT 4.0 或更高版本中使用,但不能运行在Windows 95/98 应用程序中。CImage::AlphaBlend和CImage::TransparentBlt也只能在 Windows 2000/98或其更高版本中使用。即使在Windows 2000运行程序还必须将stdafx.h文件中的WINVER和_WIN32_WINNT的预定义修改成0x0500才能正常使用。

      CImage封装了DIB(设备无关位图)的功能,因而可以让我们能够处理每个位图像素。它具有下列最酷特性:

      1、AlphaBlend支持像素级的颜色混合,从而实现透明和半透明的效果。

      2、PlgBlt能使一个矩形区域的位图映射到一个平行四边形区域中,而且还可能使用位屏蔽操作。

      3、TransparentBlt在目标区域中产生透明图像,SetTransparentColor用来设置某种颜色是透明色。

      4、MaskBlt在目标区域中产生源位图与屏蔽位图合成的效果。

    使用CImage的一般方法

      使用CImage的一般方法是这样的过程:

      (1) 打开应用程序的stdafx.h文件添加CImage类的包含文件:

    #include <atlimage.h>

      (2) 定义一个CImage类对象,然后调用CImage::Load方法装载一个外部图像文件。

      (3) 调用CImage::Draw方法绘制图像。Draw方法具有如下定义:

    程序代码:

    BOOL Draw( HDC hDestDC, int xDest, int yDest,
    int nDestWidth, int nDestHeight, int xSrc, int ySrc,
    int nSrcWidth, int nSrcHeight );
    BOOL Draw( HDC hDestDC, const RECT& rectDest, const RECT& rectSrc );
    BOOL Draw( HDC hDestDC, int xDest, int yDest );
    BOOL Draw( HDC hDestDC, const POINT& pointDest );
    BOOL Draw( HDC hDestDC, int xDest, int yDest,
    int nDestWidth, int nDestHeight );
    BOOL Draw( HDC hDestDC, const RECT& rectDest );

      其中,hDestDC用来指定绘制的目标设备环境句柄,(xDest, yDest)和pointDest用来指定图像显示的位置,这个位置和源图像的左上角点相对应。nDestWidth和nDestHeight分别指定图像要显示的高度和宽度,xSrc、ySrc、nSrcWidth和nSrcHeight用来指定要显示的源图像的某个部分所在的位置和大小。rectDest和rectSrc分别用来指定目标设备环境上和源图像所要显示的某个部分的位置和大小。

      需要说明的是,Draw方法综合了StretchBlt、TransparentBlt和AlphaBlend函数的功能。默认时,Draw的功能和StretchBlt相同。但当图像含有透明色或Alpha通道时,它的功能又和TransparentBlt、AlphaBlend相同。因此,在一般情况下,我们都应该尽量调用CImage::Draw方法来绘制图像。

      例如,下面的示例Ex_Image是实现这样的功能:当选择"文件"ò"打开"菜单命令后,弹出一个文件打开对话框。当选定一个图像文件后,就会在窗口客户区中显示该图像文件内容。这个示例的具体步骤如下:

      (1) 创建一个默认的单文档程序项目Ex_Image。

      (2) 打开stdafx.h文件中添加CImage类的包含文件atlimage.h。

      (3) 在CEx_ImageView类添加ID_FILE_OPEN的COMMAND事件映射程序,并添加下列代码:

    程序代码:

    void CEx_ImageView::OnFileOpen()
    {
     CString strFilter;
     CSimpleArray<GUID> aguidFileTypes;
     HRESULT hResult;

     // 获取CImage支持的图像文件的过滤字符串
     hResult = m_Image.GetExporterFilterString(strFilter,aguidFileTypes,
    _T( "All Image Files") );
     if (FAILED(hResult)) {
      MessageBox("GetExporterFilter调用失败!");
      return;
     }
     CFileDialog dlg(TRUE, NULL, NULL, OFN_FILEMUSTEXIST, strFilter);
     if(IDOK != dlg.DoModal())
      return;

     m_Image.Destroy();
     // 将外部图像文件装载到CImage对象中
     hResult = m_Image.Load(dlg.GetFileName());
     if (FAILED(hResult)) {
      MessageBox("调用图像文件失败!");
      return;
     }

     // 设置主窗口标题栏内容
     CString str;
     str.LoadString(AFX_IDS_APP_TITLE);
     AfxGetMainWnd()->SetWindowText(str + " - " +dlg.GetFileName());

     Invalidate(); // 强制调用OnDraw
    }

      (4) 定位到CEx_ImageView::OnDraw函数处,添加下列代码:

    程序代码:

    void CEx_ImageView::OnDraw(CDC* pDC)
    {
     CEx_ImageDoc* pDoc = GetDocument();
     ASSERT_VALID(pDoc);
     if (!m_Image.IsNull()) {
      m_Image.Draw(pDC->m_hDC,0,0);
     }
    }

      (5) 打开Ex_ImageView.h文件,添加一个公共的成员数据m_Image:

    程序代码:

    public:
    CImage m_Image;

      (6) 编译并运行。单击"打开"工具按钮,在弹出的对话框中指定一个图像文件后,单击"打开"按钮,其结果如图7.21所示。

    将图片用其它格式保存

      CImage::Save方法能将一个图像文件按另一种格式来保存,它的原型如下:

    程序代码:

    HRESULT Save( LPCTSTR pszFileName, REFGUID guidFileType= GUID_NULL);

      其中,pszFileName用来指定一个文件名,guidFileType用来指定要保存的图像文件格式,当为GUID_NULL时,其文件格式由文件的扩展名来决定,这也是该函数的默认值。它还可以是GUID_BMPFile(BMP文件格式)、GUID_PNGFile(PNG文件格式)、GUID_JPEGFile(JPEG文件格式)和GUID_GIFFile(GIF文件格式)。

      例如,下面的过程是在Ex_Image示例基础上进行的,我们在CEx_ImageView类添加ID_FILE_SAVE_AS的COMMAND事件映射程序,并添加下列代码:

    程序代码:

    void CEx_ImageView::OnFileSaveAs()
    {
     if (m_Image.IsNull()) {
      MessageBox("你还没有打开一个要保存的图像文件!");
      return;
     }

     CString strFilter;
     strFilter = "位图文件|*.bmp|JPEG 图像文件|*.jpg| \
    GIF 图像文件|*.gif|PNG 图像文件|*.png||"
     CFileDialog dlg(FALSE,NULL,NULL,NULL,strFilter);
     if ( IDOK != dlg.DoModal())
      return;

     // 如果用户没有指定文件扩展名,则为其添加一个
     CString strFileName;
     CString strExtension;

     strFileName = dlg.m_ofn.lpstrFile;
     if (dlg.m_ofn.nFileExtension == 0)
     {
      switch (dlg.m_ofn.nFilterIndex)
      {
       case 1:
        strExtension = "bmp" break;
       case 2:
        strExtension = "jpg" break;
       case 3:
        strExtension = "gif" break;
       case 4:
        strExtension = "png" break;
       default:
        break;
      }
      strFileName = strFileName + ’.’ + strExtension;
     }

     // 图像保存
     HRESULT hResult = m_Image.Save(strFileName);
     if (FAILED(hResult))
      MessageBox("保存图像文件失败!");
    }

    柔化和锐化处理

      在图像处理中,我们通常用一些数学手段,对图像进行除去噪声、强调或抽取轮廓特征等图像空间的变换。所谓"图像空间的变换"是借助于一个称之为模板的局部像素域来完成的,不同的模板具有不同的图像效果。

    1. 柔化

      图像的柔化是除去图像中点状噪声的一个有效方法。所谓柔化,是指使图像上任何一个像素与其相邻像素的颜色值的大小不会出现陡突的一种处理方法。设在一个3 x 3的模板中其系数为:

      中间有底纹的表示中心元素,即用那个元素作为处理后的元素。很明显,上述模板(称之为Box模板)是将图像上每个像素用它近旁(包括它本身)的9个像素的平均值取代。这样处理的结果在除噪的同时,也降低图像的对比度,使图像的轮廓模糊。为了避免这一缺陷,我们对各点引入加权系数,将原来的模板改为:

      新的模板可一方面除去点状噪声,同时能较好地保留原图像的对比度,因此该模板得到了广泛的应用。由于这个模板是通过二维高斯(Gauss)函数得到的,故称为高斯模板。

    2. 锐化

      锐化和柔化恰恰相反,它通过增强高频分量减少图像中的模糊,因此又称为高通滤波。锐化处理在增强图像边缘效果的同时增加了图像的噪声。常用的锐化模板是拉普拉斯模板:

      用此模板处理后的图像,轮廓线条将明显得到增强。轮廓线以外的部分将变得较暗,而轮廓线部分将变得比较明亮。

      使用程序对模板进行运算时,要考虑到溢出点的处理。所谓溢出点,指的是大于255或小于0的点。处理时,可令大于255的点取255,而小于0的点取其正值。

    3. 实现代码

      实现柔化和锐化时,我们先调用CImage::GetPixel来依次读取相应的像素,然后用柔化和锐化模板进行处理,最后调用CImage::SetPixel函数将处理后的像素写回到CImage对象中。具体的代码如下:

    程序代码:

    void FilterImage(CImage* image, int nType)
    {
     if (image->IsNull())
      return;
     int smoothGauss[9] = {1,2,1,2,4,2,1,2,1}; // 高斯模板
     int sharpLaplacian[9] = {-1,-1,-1,-1,9,-1,-1,-1,-1}; // 拉普拉斯模板

     int opTemp[9];
     float aver; // 系数
     if ( nType > 1) nType = 0;
     switch( nType ){
      case 0: // 高斯模板
       aver = (float)(1.0/16.0);
       memcpy( opTemp, smoothGauss, 9*sizeof(int));
       break;
      case 1: // 拉普拉斯模板
       aver = 1.0;
       memcpy( opTemp, sharpLaplacian, 9*sizeof(int));
       break;
     }

     int i,j;

     int nWidth = image->GetWidth();
     int nHeight = image->GetHeight();

     for (i = 1; i < nWidth-1; i++){
      for (j = 1; j < nHeight-1; j++){

       int rr = 0, gg = 0, bb = 0;
       int index = 0;

       for (int col = -1; col <= 1; col++){
        for (int row = -1; row <= 1; row++){
         COLORREF clr = image->GetPixel( i+row, j+col);
         rr += GetRValue(clr) * opTemp[index];
         gg += GetGValue(clr) * opTemp[index];
         bb += GetBValue(clr) * opTemp[index];
         index++;
        }
       }
       rr = (int)(rr*aver);
       gg = (int)(gg*aver);
       bb = (int)(bb*aver);

       // 处理溢出点
      if ( rr > 255 ) rr = 255;
      else if ( rr < 0 ) rr = -rr;
      if ( gg > 255 ) gg = 255;
      else if ( gg < 0 ) gg = -gg;
      if ( bb > 255 ) bb = 255;
      else if ( bb < 0 ) bb = -bb;

      // 错位重写以避免前一个像素被新的像素覆盖
      image->SetPixel( i-1, j-1, RGB(rr,gg,bb));
      }
     }
    }
    图7.22是使用上述代码将某个图像处理后的结果。


    变成黑白图片

      由于许多图像文件使用颜色表来发挥显示设备的色彩显示能力,因而将一张彩色图片变成黑色图片时需要调用CImage::IsIndexed来判断是否使用颜色表,若是则修改颜色表,否则直接将像素进行颜色设置。例如下面的代码:

    程序代码:

    void CEx_ImageView::MakeBlackAndwhite(CImage* image)
    {
     if (image->IsNull()) return;

     if (!image->IsIndexed()) {
      // 直接修改像素颜色
      COLORREF pixel;
      int maxY = image->GetHeight(), maxX = image->GetWidth();
      byte r,g,b,avg;
      for (int x=0; x<maxX; x++) {
       for (int y=0; y<maxY; y++) {
        pixel = image->GetPixel(x,y);
        r = GetRValue(pixel);
        g = GetGValue(pixel);
        b = GetBValue(pixel);
        avg = (int)((r + g + b)/3);
        image->SetPixelRGB(x,y,avg,avg,avg);
       }
      }
     } else {
      // 获取并修改颜色表
      int MaxColors = image->GetMaxColorTableEntries();
      RGBQUAD* ColorTable;
      ColorTable = new RGBQUAD[MaxColors];
      image->GetColorTable(0,MaxColors,ColorTable);
      for (int i=0; i<MaxColors; i++)
      {
       int avg = (ColorTable[i].rgbBlue + ColorTable[i].rgbGreen + ColorTable[i].rgbRed)/3;
       ColorTable[i].rgbBlue = avg;
       ColorTable[i].rgbGreen = avg;
       ColorTable[i].rgbRed = avg;
      }
      image->SetColorTable(0,MaxColors,ColorTable);
      delete(ColorTable);
     }
    }

      至此,我们介绍了GDI+和CImage的一般使用方法和技巧。当然,它们本身还有许多更深入的方法,由于篇幅所限,这里不再一一讨论。

    基于VC.NET的GDI+图像处理[转载]

     

      我们知道,在以往的图像处理中,常常要根据不同图像文件的格式及其数据存储结构在不同格式中进行转换。某个图像文件的显示也是依靠对文件数据结构的剖析,然后读取相关图像数据而实现的。现在,GDI+提供了Image和Bitmap类使我们能轻松容易地处理图像。

    概述

      GDI+支持大多数流行的图像文件格式,如BMP、GIF、JPEG、TIFF和PNG等。下面先来介绍这些图像文件,然后再说明Image和Bitmap类支持的特性。

    1.图像文件格式简介

      图像文件是描绘一幅图像的计算机磁盘文件,其文件格式不下数十种。这里仅介绍BMP、GIF、JPEG、TIFF和PNG等图像文件格式。

      BMP文件格式

      BMP图像文件格式是Microsoft为其Windows环境设置的标准图像格式。一个Windows的BMP位图实际上是一些和显示像素相对应的位阵列,它有两种类型:一种称之为GDI位图,另一种是DIB位图(Device-Independent Bitmap)。GDI位图包含了一种和Windows的GDI模块有关的Windows数据结构,该数据结构是与设备有关的,故此位图又称为DDB位图(Device-Dependent Bitmap)。当用户的程序取得位图数据信息时,其位图显示方式视显示卡而定。由于GDI位图的这种设备依赖性,当位图通过网络传送到另一台PC,很可能就会出现问题。

      DIB比GDI位图有很多编程优势,例如它自带颜色信息,从而使调色板管理更加容易。且任何运行Windows的机器都可以处理DIB,并通常以后缀为.BMP的文件形式被保存在磁盘中或作为资源存在于程序的EXE或DLL文件中。

      GIF文件格式

      图形交换格式(GIF--Graphics Interchange Format)最早由CompuServe公司于1987年6月15日制定的标准,主要用于CompuServe网络图形数据的在线传输、存储。GIF提供了足够的信息并很好地组织了这些信息,使得许多不同的输入输出设备能够方便地交换图像,它支持24位彩色,由一个最多256种颜色的调色板实现,图像的大小最多是64K x 64K个像点。GIF的特点是LZW压缩、多图像和交错屏幕绘图。

      JPEG文件格式

      国际标准化组织(ISO)和国际电报电话咨询委员会(CCITT)联合成立的"联合照片专家组"JPEG(Joint Photographic Experts Group)经过五年艰苦细致工作后,于1991年3月提出了ISO CD 10918号建议草案:"多灰度静止图像的数字压缩编码"(通常简称为JPEG标准)。这是一个适用于彩色和单色多灰度或连续色调静止数字图像的压缩标准。它包括无损压缩和基于离散余弦变换和Huffman编码的有损压缩两个部分。前者不会产生失真,但压缩比很小;后一种算法进行图像压缩时,信息虽有损失但压缩比可以很大。例如压缩20~40倍时,人眼基本上看不出失真。

      JPEG图像文件也是一种像素格式的文件格式,但它比BMP等图像文件要复杂许多。所幸的是,GDI+的Image提供了对JPEG文件格式的支持,使得我们不需要对JPEG格式有太多的了解就能处理该格式的图像。

      TIFF文件格式

      TIFF(Tagged Image Format File,标志图像文件格式)最早由Aldus公司于1986年推出的,它能对从单色到24位真彩的任何图像都有很好的支持,而且在不同的平台之间的修改和转换是十分容易的。与其它图像文件格式不同的是,TIFF文件中有一个标记信息区用来定义文件存储的图像数据类型、颜色和压缩方法。

      PNG文件格式

      PNG(Portable Network Graphic,可移植的网络图像)文件格式是由Thomas Boutell、Tom Lane等人提出并设计的,它是为了适应网络数据传输而设计的一种图像文件格式,用于取代格式较为简单、专利限制严格的GIF图像文件格式。而且,这种图像文件格式在某种程度上甚至还可以取代格式比较复杂的TIFF图像文件格式。它的特点主要有:压缩效率通常比GIF要高、提供Alpha通道控制图像的透明度、支持Gamma校正机制用来调整图像的亮度等。

      需要说明的是,PNG文件格式支持三种主要的图像类型:真彩色图像、灰度级图像以及颜色索引数据图像。JPEG只支持前两种图像类型,而GIF虽然可以利用灰度调色板补偿图像的灰度级别,但原则上它仅仅支持第三种图像类型。

    2.Image和Bitmap类概述

      GDI+的Image类封装了对BMP、GIF、JPEG、PNG、TIFF、WMF(Windows元文件)和EMF(增强WMF)图像文件的调入、格式转换以及简单处理的功能。而Bitmap是从Image类继承的一个图像类,它封装了Windows位图操作的常用功能。例如,Bitmap::SetPixel和Bitmap::GetPixel分别用来对位图进行读写像素操作,从而可以为图像的柔化和锐化处理提供一种可能。

    3.DrawImage方法

      DrawImage是GDI+的Graphics类显示图像的核心方法,它的重载函数有许多个。常用的一般重载函数有:

    程序代码:

    Status DrawImage( Image* image, INT x, INT y);
    Status DrawImage( Image* image, const Rect& rect);
    Status DrawImage( Image* image, const Point* destPoints, INT count);
    Status DrawImage( Image* image, INT x, INT y, INT srcx, INT srcy,
    INT srcwidth, INT srcheight, Unit srcUnit);

      其中,(x,y)用来指定图像image显示的位置,这个位置和image图像的左上角点相对应。rect用来指定被图像填充的矩形区域, destPoints和count分别用来指定一个多边形的顶点和顶点个数。若count为3时,则表示该多边形是一个平行四边形,另一个顶点由系统自动给出。此时,destPoints中的数据依次对应于源图像的左上角、右上角和左下角的顶点坐标。srcx、srcy、srcwidth 和srcheight用来指定要显示的源图像的位置和大小,srcUnit用来指定所使用的单位,默认时使用PageUnitPixel,即用像素作为度量单位。

    4.调用和显示图像文件

      在GDI+中调用和显示图像文件是非常容易的,一般先通过Image或Bitmap调入一个图像文件构造一个对象,然后调用Graphics::DrawImage方法在指定位置处显示全部或部分图像。例如下面的代码:

    程序代码:

    void CEx_GDIPlusView::OnDraw(CDC* pDC)
    {
     CEx_GDIPlusDoc* pDoc = GetDocument();
     ASSERT_VALID(pDoc);

     using namespace Gdiplus;
     Graphics graphics( pDC->m_hDC );

     Image image(L"sunflower.jpg");
     graphics.DrawImage(&image, 10,10);

     Rect rect(130, 10, image.GetWidth(), image.GetHeight());
     graphics.DrawImage(&image, rect);
    }
      结果如下图所示,从图中我们可以看出,两次DrawImage的结果是不同的,按理应该相同,这是怎么一回事?原来,DrawImage在不指定显示区域大小时会自动根据设备分辨率进行缩放,从而造成显示结果的不同。


      当然,也可以使用Bitmap类来调入图像文件来构造一个Bitmap对象,其结果也是一样的。例如,上述代码可改为:

    程序代码:

    Bitmap bmp(L"sunflower.jpg");
    graphics.DrawImage(&bmp, 10,10);

    Rect rect(130, 10, bmp.GetWidth(), bmp.GetHeight());
    graphics.DrawImage(&bmp, rect);
      需要说明的是,Image还提供GetThumbnailImage的方法用来获得一个缩略图的指针,调用DrawImage后可将该缩略图显示,这在图像预览时极其有用。例如下面的代码:

    程序代码:

    Graphics graphics( pDC->m_hDC );
    Image image(L"sunflower.jpg");
    Image* pThumbnail = image.GetThumbnailImage(50, 50, NULL, NULL);

    // 显示缩略图
    graphics.DrawImage(pThumbnail, 20, 20);

    // 使用后,不要忘记删除该缩略图指针
    delete pThumbnail;

    5.图像旋转和拉伸

      图像的旋转和拉伸通常是通过在DrawImage中指定destPoints参数来实现,destPoints包含对新的坐标系定义的点的数据。下图说明了坐标系定义的方法。


      从图中可以看出,destPoints中的第一个点是用来定义坐标原点的,第二点用来定义X轴的方法和图像X方向的大小,第三个是用来定义Y轴的方法和图像Y方向的大小。若destPoints定义的新坐标系中两轴方向不垂直,就能达到图像拉伸的效果。

      下面的代码就是图像旋转和拉伸的一个示例,其结果如图7.19所示。

    程序代码:
    Image image(L"sunflower.jpg");
    graphics.DrawImage(&image, 10,10);

    Point points[] = { Point(0, 0), Point(image.GetWidth(), 0),
    Point(0, image.GetHeight())};
    Matrix matrix(1,0,0,1,230,10); // 定义一个单位矩阵,坐标原点在(230,10)
    matrix.Rotate(30); // 顺时针旋转30度

    matrix.Scale(0.63,0.6); // X 和 Y 方向分别乘以0.63和0.6比例因子
    matrix.TransformPoints(points, 3); // 用该矩阵转换points
    graphics.DrawImage(&image, points, 3);

    Point newpoints[] = {Point(450, 10), Point(510, 60), Point(350, 80)};
    graphics.DrawImage(&image, newpoints, 3);

      当然,对于图像旋转还可直接使用Graphics::RotateTransform来进行,例如下面的代码。但这样设置后,以后所有的绘图结果均会旋转,有时可能感觉不方便。

    程序代码:
    Image image(L"sunflower.jpg");
    graphics.TranslateTransform(230,10); // 将原点移动到(230,10)
    graphics.RotateTransform(30); // 顺时针旋转30度
    graphics.DrawImage(&image, 0,0);

    6.调整插补算法的质量

      当图像进行缩放时,需要对图像像素进行插补,不同的插补算法其效果是不一样的。Graphics:: SetInterpolationMode可以让我们根据自己的需要使用不同质量效果的插补算法。当然,质量越高,其渲染时间越长。下面的代码就是使用不同质量效果的插补算法模式,其结果如图7.20所示。

    程序代码:

    Graphics graphics( pDC->m_hDC );
    Image image(L"log.gif");
    UINT width = image.GetWidth();
    UINT height = image.GetHeight();

    // 不进行缩放
    graphics.DrawImage( &image,10,10);

    // 使用低质量的插补算法
    graphics.SetInterpolationMode(InterpolationModeNearestNeighbor);
    graphics.DrawImage( &image,
    Rect(170, 30, (INT)(0.6*width), (INT)(0.6*height)));

    // 使用中等质量的插补算法
    graphics.SetInterpolationMode(InterpolationModeHighQualityBilinear);
    graphics.DrawImage( &image,
    Rect(270, 30, (INT)(0.6*width), (INT)(0.6*height)));

    // 使用高质量的插补算法
    graphics.SetInterpolationMode(InterpolationModeHighQualityBicubic);
    graphics.DrawImage( &image,
    Rect(370, 30, (INT)(0.6*width), (INT)(0.6*height)));

      事实上,Image功能还不止这些,例如还有不同格式文件之间的转换等。但这些功能和MFC的新类CImage功能基本一样,但CImage更符合MFC程序员的编程习惯,因此下一节中我们来重点介绍CImage的使用方法和技巧。