数字图像处理与机器视觉:Visual C++与Matlab实现(第2版)
上QQ阅读APP看书,第一时间看更新

2.3 CImg类基础操作

2.3.1 加载和写入图像

CImg类首先提供了加载和写入图像所需要的方法AttachFromFile和SaveToFile,这两个方法分别提供了从文件路径中加载和写入与从文件对象中加载和写入的接口。它们实际上是对MFC库相关功能的进一步封装,定义原型如下。

        // 从文件加载位图
        BOOL AttachFromFile(LPCTSTR lpcPathName);
        BOOL AttachFromFile(CFile &file);

        // 将位图保存到文件
        BOOL SaveToFile(LPCTSTR lpcPathName);
        BOOL SaveToFile(CFile &file);

1.AttachFromFile函数

使用LPCTSTR参数的函数接口只是进一步封装了使用CFile对象参数的函数接口,将LPCTSTR类型的文件路径读入变成CFile,而后调用使用CFile接口的相同方法。这里仅以AttachFromFile函数的LPCTSTR类型参数的实现形式为例进行简要的说明。

      /**************************************************
      BOOL CImg::AttachFromFile(LPCTSTR lpcPathName)
      功能:     打开指定的图像文件并附加到CImg对象上
      限制:      只能处理位图图像
      参数:     LPCTSTR lpcPathName:      欲打开文件的完整路径
      返回值:    BOOL类型:                  TRUE为成功,FALSE为失败
      ***************************************************/
      BOOL CImg::AttachFromFile(LPCTSTR lpcPathName)
      {
            // 使用CFile对象简化操作
            CFile file;
            if(! file.Open(lpcPathName, CFile::modeRead|CFile::shareDenyWrite))
                return FALSE;

            BOOL bSuc = AttachFromFile(file);

            file.Close();
            return bSuc;
      }

上述程序中首先读取文件头,而后提取文件头中的图像信息头部分,并分析信息头中所含有的颜色表和图像的其他相关属性,初始化CImg对象中的相关成员,最后读取图像数据。操作中使用了MFC直接提供的位图信息头BITMAPINFOHEADER对象。

参数为CFile对象的AttachFromFile函数的实现如下。

      /**************************************************
      BOOL CImg::AttachFromFile(CFile &file)
      功能:     打开指定的图像文件并附加到CImg对象上
      参数:     CFile &file:     欲打开的CFile对象
      返回值:    BOOL类型:        TRUE为成功,FALSE为失败
      ***************************************************/
      BOOL CImg::AttachFromFile(CFile &file)
      {
            // 文件数据
            LPBYTE  *lpData;
            // 位图信息头
            BITMAPINFOHEADER *pBMIH;
            // 颜色表指针
            LPVOID lpvColorTable = NULL;
            // 颜色表颜色数目
            int nColorTableEntries;

            BITMAPFILEHEADER bmfHeader;

            // 读取文件头
            if(! file.Read(&bmfHeader, sizeof(bmfHeader)))
            {
                return FALSE;
            }
            // 检查开头两字节是否为BM
            if(bmfHeader.bfType ! = MAKEWORD('B', 'M'))
            {
                return FALSE;
            }

            // 读取信息头
            pBMIH = (BITMAPINFOHEADER*)new BYTE[bmfHeader.bfOffBits - sizeof(bmfHeader)];
            if(! file.Read(pBMIH, bmfHeader.bfOffBits - sizeof(bmfHeader)))
            {
                delete pBMIH;
                return FALSE;
            }

            // 定位到颜色表
            nColorTableEntries =
                (bmfHeader.bfOffBits - sizeof(bmfHeader) - sizeof(BITMAPINFOHEADER))/
      sizeof(RGBQUAD);
            if(nColorTableEntries > 0)
            {
                lpvColorTable = pBMIH + 1;
            }

            pBMIH->biHeight = abs(pBMIH->biHeight);

            // 读取图像数据,WIDTHBYTES宏用于生成每行字节数
            int nWidthBytes = WIDTHBYTES((pBMIH->biWidth)*pBMIH->biBitCount);

            // 申请biHeight个长度为biWidthBytes的数组,用他们来保存位图数据
            lpData = new LPBYTE[(pBMIH->biHeight)];
            for(int i=0; i<(pBMIH->biHeight); i++)
            {
                lpData[i] = new BYTE[nWidthBytes];
                file.Read(lpData[i], nWidthBytes);

            }

            // 更新数据
            CleanUp();

            m_lpData = lpData;
            m_pBMIH = pBMIH;
            m_lpvColorTable = lpvColorTable;
            m_nColorTableEntries = nColorTableEntries;

            return TRUE;
      }

2.SaveToFile函数

与AttachFromFile大致相反,SaveToFile中首先判断位图对象是否有效,而后根据当前颜色表(如果是索引位图)和图像的相关属性信息构造图像信息头和文件信息头,并将文件头和图像数据写入文件。这期间的操作也大量地使用了MFC库提供的位图信息头和文件信息头对象。

参数为CFile对象的SaveToFile函数的实现如下。

      /**************************************************
      BOOL CImg::SaveToFile(CFile &file)
      功能:      把CImg实例中的图像数据保存到指定的图像文件
      参数:      CFile &file:      欲保存到的CFile对象
      返回值:    BOOL类型:        TRUE为成功,FALSE为失败
      ***************************************************/
      BOOL CImg::SaveToFile(CFile &file)
      {
            // 判断是否有效
            if(! IsValidate())
                return FALSE;

            // 构建BITMAPFILEHEADER结构
            BITMAPFILEHEADER bmfHeader = { 0 };
            int nWidthBytes = WIDTHBYTES((m_pBMIH->biWidth)*m_pBMIH->biBitCount);

            bmfHeader.bfType = MAKEWORD('B', 'M');
            bmfHeader.bfOffBits = sizeof(BITMAPFILEHEADER)
                          + sizeof(BITMAPINFOHEADER) + m_nColorTableEntries*4;

            bmfHeader.bfSize = bmfHeader.bfOffBits + m_pBMIH->biHeight * nWidthBytes;

            // 向文件中写入数据
            file.Write(&bmfHeader, sizeof(bmfHeader));
            file.Write(m_pBMIH, sizeof(BITMAPINFOHEADER) + m_nColorTableEntries*4);

            for(int i=0; i<m_pBMIH->biHeight; i++)
            {
                file.Write(m_lpData[i], nWidthBytes);
            }

            return TRUE;
      }

2.3.2 获得图像基本信息

在得到了一个CImg对象后,可以通过下面的方法来获得位图的相关信息(高度、宽度、有效性等),这些方法包括以下几种。

      int CImg::GetHeight(); //获得图像高度
      int CImg::GetWidthPixel(); //获得图像宽度
      int CImg::GetWidthByte(); //获得每行字节数

这3个方法都是内联函数,它们都是通过直接返回CImg对象的相关私有成员属性来得到所需的信息。其中GetHeight()和GetWidthPixel()返回值都是以像素为单位的,是实际的图像大小;而GetWidthByte()返回的是以字节为单位的图像每行宽度(4的整数倍),用于对图像数据进行补齐操作。下面分别给出其实现细节。

1.GetHeight函数

      /**************************************************
      inline int CImg::GetHeight()
      功能:     返回CImg实例中的图像每列的像素数目,即纵向分辨率或高度
      参数:      无
      返回值:    int类型:图像每列的像素数目
      ***************************************************/
      inline int CImg::GetHeight()
      {
        return m_pBMIH->biHeight;
      }

2.GetWidthPixel函数

      /**************************************************
      inline int CImg::GetWidthPixel()
      功能:     返回CImg实例中的图像每行的像素数目,即横向分辨率或宽度
      参数:      无
      返回值:    int类型:图像每行的像素数目
      ***************************************************/
      inline int CImg::GetWidthPixel()
      {
            return m_pBMIH->biWidth;
      }

3.GetWidthByte函数

      /**************************************************
      inline int CImg::GetWidthByte()
      功能:     返回CImg实例中的图像每行占用的字节数
      参数:      无
      返回值:    int类型:图像每行占用的字节数,必须是4的整数倍
      ***************************************************/
      inline int CImg::GetWidthByte()
      {
            return WIDTHBYTES((m_pBMIH->biWidth)*m_pBMIH->biBitCount);
      }

其中宏WIDTHBYTES的定义是如下。

      #define   WIDTHBYTES(bits)    (((bits) + 31) / 32 * 4) //保证每行数据占用的空间是4的整数倍

m_pBMIH->biWidth的值就是以像素为单位表示的图像宽度,而将此值与图像每像素使用的字节长度相乘,理应得出图像每行占用的字节数。但是,对位图图像而言,每行像素占用的字节数和图像中每行所需使用的字节数并不一定相等。前文提到,需要将定义中每行所占用的空间补齐到4的整数倍字节,WIDTHBYTES宏正是为了实现这一功能。

2.3.3 检验有效性

按照程序的健壮性原则,不论是从图像文件读取而后得到CImg对象,还是在新建对象后使用绘图函数构造一幅图像,都需要在真正使用该对象之前检验它的有效性。可以通过检查公有成员m_pBMIH是否有效来达到这一目的。有效性检验函数IsValidate的实现如下。

      /**************************************************
      BOOL CImg::IsValidate ()
      功能:      检验图像的有效性
      参数:      无
      返回值:    TRUE:表示图像有效
                FALSE:表示图像无效
      ***************************************************/
      BOOL  IsValidate() { return m_pBMIH ! = NULL; }

很显然,不存在的图像信息块意味着图像对象是无效的,使用时就会出现错误。此时,用户程序可以选择返回一条错误信息或者重新进行之前的初始化操作。

2.3.4 按像素操作

在得到了图像的高宽信息后,就知道了图像数据的有效坐标范围,从而可以对图像进行逐像素遍历。很多数字图像处理的算法中都需要提取指定位置或区域内的像素值,加以处理后再写入到指定位置。

CImg实例中保存的可能是二值图像、灰度图像,也可能是彩色图像。为此可以再次读取m_pBMIH中的内容来确定图像类型。图像信息头结构中的biBitCount成员保存了存储每个像素使用的比特数,由此可以推测图像的类型。例如,灰度图像的biBitCount=8, RGB图像则为24,二值图像的每个像素只需要一位来保存,因此biBitCount=1。

1.提取指定位置的像素值——GetPixel()函数

像素操作算法的第一步一定是需要提取指定位置像素的数值。CImg类中提取像素值的方法为GetPixel,这个方法可以自动确定CImg类中保存的图像的类型,并根据不同类型返回图像数据矩阵中的对应元素。其中返回类型COLORREF是DWORD的一个别名,用于保存颜色数据。函数的输入参数应当指定像素的x, y坐标,而输出量是(x, y)位置像素的颜色。由于要兼容多种类型的图像,所以使用了兼容性最强的返回类型,即COLORREF,对于灰度图像,将返回一个在R、G、B三个分量上相等的COLORREF数据。

GetPixel函数的原型如下。

      /**************************************************
      inline COLORREF CImg::GetPixel(int x, int y)
      功能:      返回指定坐标位置像素的颜色值
      参数:     int x, int y:指定的像素横、纵坐标值
      返回值:    COLERREF类型:返回用RGB形式表示的指定位置的颜色值
      ***************************************************/
      inline COLORREF CImg::GetPixel(int x, int y)

2.设置指定位置的像素值——SetPixel()函数

在使用GetPixel方法得到像素值,并经算法处理后,算法输出仍然需要写回到图像数据区中。为此,也需要一个按像素绘图的方法。CImg类中实现这一功能的算法是SetPixel,调用时除了应提供(x, y)位置的参数外,还应当包括欲写入指定像素位置的COLORREF型数据。同样,当欲将灰度值gray写入灰度图像时,第3个参数可设置为RGB(gray, gray, gray)。

SetPixel方法的原型如下。

      /**************************************************
      void CImg::SetPixel(int x, int y, COLORREF color)
      功能:      设定指定坐标位置像素的颜色值
      参数:     int x, int y:指定的像素横、纵坐标值
                COLORREF:欲设定的指定位置的颜色值,RGB形式给出
      返回值:    无
      ***************************************************/
      void CImg::SetPixel(int x, int y, COLORREF color)

3.提取指定位置的灰度值——GetGray()函数

由于更多的时候是在处理灰度图像,为方便调用,CImg类还提供了GetGray方法用于直接读入图像中(x, y)坐标位置的灰度值,实现如下。

      /**************************************************
      inline BYTE CImg::GetGray(int x, int y)
      功能:      返回指定坐标位置像素的灰度值
      限制:      无
      参数:     int x, int y:指定的像素横、纵坐标值
      返回值:    BYTE类型:给定像素位置的灰度值
      ***************************************************/
      inline BYTE CImg::GetGray(int x, int y)
      {
            COLORREF ref = GetPixel(x, y);
            BYTE r, g, b, byte;
            // 分别获取三基色亮度
            r = GetRValue(ref);
            g = GetGValue(ref);
            b = GetBValue(ref);

            if(r == g && r == b)
                return r;
            float ff = (0.30*r + 0.59*g + 0.11*b);
            // 灰度化
            byte =  (int)ff;
            return byte;
      }

注意

CImg中的一系列像素存取方法,如GetGray, SetPixel等要求的参数x, y分别为像素位置的横、纵坐标。而横坐标对应于图像的列索引j,纵坐标对应着图像的行索引i,因此在逐行遍历图像的程序段中的调用方式为:SetPixel(j, i, …),具体方法读者可参考稍后介绍的InitPixels方法。

2.3.5 改变图像大小

有时需要改变已经存在的CImg对象的大小。此时,除了要对图像信息头进行相应更新外,还要重新分配一个合适大小的数据存储区,当然在此之前应释放旧的存储区。ImResize方法用于实现这一功能。请注意,ImResize函数在改变图像大小的同时会擦除图像中所有的数据,与第4章几何变换中将要学习的按比例缩放的Scale函数不同!

      /**************************************************
      void CImg::ImResize(int nHeight, int nWidth)
      功能:     用给定的大小重新初始化CImg对象
      限制:     CImg对象必须已经包含有效的图像数据,否则将出错
      参数:     int nHeight:重新初始化成的宽度
                int nWidth:重新初始化成的高度
      返回值:    无
      ***************************************************/
      void CImg::ImResize(int nHeight, int nWidth)
      {
            int i; //循环变量
            //释放图像数据空间
            for(i=0; i<m_pBMIH->biHeight; i++)
            {
                delete[] m_lpData[i];
            }
            delete[] m_lpData;

            //更新信息头中的相应内容
            m_pBMIH->biHeight = nHeight; //更新高度
            m_pBMIH->biWidth = nWidth; //更新宽度

            //重新分配数据空间
            m_lpData = new LPBYTE [nHeight];
            int nWidthBytes = WIDTHBYTES((m_pBMIH->biWidth)*m_pBMIH->biBitCount);
            for(i=0; i<nHeight; i++)
            {
                m_lpData[i] = new BYTE [nWidthBytes];
            }
      }

2.3.6 重载的运算符

和MATLAB不同,C++不是专门为了科学计算或者图像处理设计的语言,因而C++中的运算符在没有重载时是无法接受图像矩阵作为运算参数的。为了后面操作的方便,对一些图像处理中常用的运算符进行了重载。目前已经提供重载的运算符如下。

        void operator = (CImg& gray); //图像赋值
        bool operator = = (CImg& gray); //判断2幅图像是否相同
        CImg operator & (CImg& gray); //图像按位与
        CImg operator | (CImg& gray); //图像按位或
        CImg operator + (CImg gray); //图像相加
        CImg operator - (CImg& gray); //图像减法
        CImg operator ! (); //图像反色

提供了赋值,判断相等,按像素与、或和按像素求和、求差,及图像反色等运算符的重载功能。在这个基础上,就可以方便地使用类似MATLAB中的语法对CImg类的实例进行像素级操作。运算符重载的实现过程是根据图像操作的需要进行的,其实质就是对图像数据矩阵中的元素(与像素一一对应)进行操作。

2.3.7 在屏幕上绘制位图图像

将经过算法处理的图像输出到屏幕上,可以观察处理后的效果。在MFC中,如果需要向屏幕输出图像,就需要将图像绘制到DC平面上。下面的Draw方法可以将CImg图像对象绘制到pDC指定的平面上,实现如下。

      /**************************************************
      BOOL CImg::Draw(CDC* pDC)
      功能:    在给定的设备上下文环境中将CImg对象中存储的图像绘制到屏幕上
      限制:      无
      参数:     CDC * pDC    :指定的设备上下文环境的指针
      返回值:    BOOL类型:TRUE为成功,FALSE为失败
      ***************************************************/
      BOOL CImg::Draw(CDC* pDC)
      {
            if(m_pBMIH == NULL)
                return FALSE;

            for(int i=0; i<m_pBMIH->biHeight; i++)
            {

                ::SetDIBitsToDevice(*pDC, 0, 0, m_pBMIH->biWidth,
                      m_pBMIH->biHeight,  0,  0,  i,  1,  m_lpData[i],  (BITMAPINFO*)m_pBMIH,
      DIB_RGB_COLORS);
            }

            return TRUE;
      }

上述程序中调用了API函数SetDIBitsToDevice,该函数使用DIB位图和颜色数据对与目标设备环境相关的设备上的指定矩形中的像素进行设置。

2.3.8 新建图像

新建图像的实质是新建一个CImg类的实例、初始化图像信息头和图像存储区中的每个像素。

提示

在免费提供的版本中,CImg类的构造函数不接受任何参数,而是仅仅构造一个空的CImg实例,其中的所有成员指针都指向NULL,因此,务必在构造函数初始化之后加入其成员的构造与初始化,以免引用无效内存。一种简便的做法是通过AttachFromFile()函数将空CImg对象和一个读入的图像文件联系在一起。

1.构造空的CImg对象

CImg类的构造函数很简单,其实现的功能仅仅是将图像信息头、图像数据区和颜色索引表的指针指向NULL,从而构造一个空的CImg对象。其实现代码如下。

      CImg::CImg()
      {
            m_pBMIH = NULL;
            m_lpvColorTable = NULL;
            m_lpData = NULL;
      }

2.构造位图信息头

在得到一个空的CImg实例后,首先构造位图信息头,并按要求设置图像的宽度和高度以及像素位数等属性。其实现代码如下。

      m_pBMIH = new BITMAPINFOHEADER;

      // 也可以在这里设置其他的图像信息属性
      m_pBMIH->biBitCount = imgBitCount;
      m_pBMIH->biHeight = imgHeight;
      m_pBMIH->biWidth = imgWidth;

得到了完整的图像信息头之后,图像存储区的大小就确定下来,也就可以据此构造图像数据存储区。

3.分配图像数据存储区

图像存储区是一个BYTE型的数组,可以使用如下的方式手动分配空间。

      m_lpData = new LPBYTE [imgHeight];
      int imgWidthBytes = WIDTHBYTES((m_pBMIH->biWidth)*m_pBMIH->biBitCount);
      int i;
      for(i=0; i<imgHeight; i++)
      {
            m_lpData[i] = new BYTE [imgWidthBytes];
      }

4.初始化图像数据——InitPixels()函数

上一步中得到的图像数据区属于新分配的动态内存,其内容并不确定。为了得到有意义的图像数据,就需要使用InitPixels函数来初始化每个像素为一个特定的值。为安全起见,在真正对像素矩阵执行初始化操作之前,还需要检查这个矩阵的有效性,即判断数据指针是否为NULL。

      /**************************************************
      void CImg::InitPixels(BYTE color)
      功能:      用给定的颜色值初始化图像的所有像素
      限制:      只能使用灰度值提供颜色值,即只能初始化为某种灰色
      参数:     BYTE color   :指定的用来初始化图像的灰度值
      返回值:    无
      ***************************************************/
      void CImg::InitPixels(BYTE color)
      {
            //获得图像高、宽
            int nHeight = GetHeight();
            int nWidth = GetWidthPixel();

            int i, j; //行、列循环变量

            //逐行扫描图像,依次对每个像素设置color灰度
            if(m_lpData ! = NULL)
            {
                for(int i=0; i<GetHeight(); i++)
                {
                      for(int j=0; j<GetWidthPixel(); j++)
                      {
                          SetPixel(j, i, RGB(color, color, color));
                      }//for j
                }//for i
            }
      }

2.3.9 图像类型的判断与转化

很多情况下,图像处理算法只能处理某一类型的图像,因此经常需要在处理之前对图像的类型进行判断,并在必要时进行类型的转换。下面给出3个常用的类型判断和转换函数。

        // 判断是否是二值图像
        BOOL IsBinaryImg();
        // 判断是否是索引图像
        BOOL IsIndexedImg();
        // 索引图像转灰度图像
        bool Index2Gray();

1.判断是否为二值图像——IsBinaryImg()函数

IsBinaryImg()函数通过二重循环对图像进行逐行扫描,如果发现任何一个像素存在0和255之外的灰度值,则返回FALSE,表示不是二值图像;而如果所有像素的灰度都是0或255,则返回TRUE,表示是二值图像。具体实现如下。

      inline BOOL CImg::IsBinaryImg()
      {
            int i, j;
            for(i = 0; i < m_pBMIH->biHeight; i++)
            {
                for(j = 0; j < m_pBMIH->biWidth; j++)
                {
                      if( (GetGray(j, i) ! = 0) && (GetGray(j, i) ! = 255) ) //存在0和255之外的灰度值
                          return FALSE;
                }//for j
            }//for i
            return TRUE;
      }

2.判断是否为索引图像——IsIndexedImg()函数

IsIndexedImg()函数通过检查颜色索引表数据存在并且表条目不为0来判断图像是否为索引图像,具体实现如下。

      inline BOOL CImg::IsIndexedImg()
      {
            if ((m_lpvColorTable ! = NULL)&&(m_nColorTableEntries! =0)) {
                return true;
            }
            else {
                return false;
            }
      }

3.索引图像转灰度图像——Index2Gray()函数

由于大多数图像处理算法都是针对灰度图像的,因此常常需要将彩色图像转化为灰度图像,下面要介绍的Index2Gray()函数可将256色索引图像转化为256级灰度图像,将真彩色RGB图像转化为灰度图的方法将在第9章中介绍。

Index2Gray()函数的具体实现如下。

      // 索引图像转灰度图像
      bool CImg::Index2Gray()
      {
            int i;

            if (! IsIndexedImg()) return false;
            RGBQUAD *table = (RGBQUAD*)m_lpvColorTable;

            m_pBMIH->biBitCount = 8;
            // 更新颜色数据
            for (i=0; i<GetHeight(); i++)
            {
                for (int j=0; j<GetWidthPixel(); j++)
                {
                      RGBQUAD rgb = *(table+GetGray(j, i));
                      BYTE gray = rgb.rgbBlue * 0.114 + rgb.rgbGreen * 0.587 + rgb.rgbRed * 0.299
      + 0.5;
                      SetPixel(j, i, RGB(gray, gray, gray));
                }
            }

            // 更新颜色表
            for (i=0; i<256; i++)
            {
                (table + i)->rgbBlue = i;
                (table + i)->rgbGreen = i;
                (table + i)->rgbRed = i;
                (table + i)->rgbReserved = 0;
            }

            m_nColorTableEntries = 256;
            return true;
      }

打开一幅256色索引图像,通过菜单命令“文件→256色索引图像转为灰度图”可以将它转化为灰度图。