swicth ( VcImageProc ) case 3:CImg 和 CImgProc —— 图像操作和处理类
一、前言
在上一篇博文中,我们初步介绍了本系列唯一的 DEMO —— “纯粹的线条”,并在文末讨论了实现此 DEMO 所需要走的两步:(1)设计自己的图像处理类(2)了解必要的 MFC 知识。本篇博文就是围绕第一步展开:首先讨论了图像处理中为什么要使用类,进一步->为什么要设计自己的类,最后讨论了核心类 CImg 的设计与实现,以及 CImg 的子类、封装各类算法的 CImgProc 类的设计,为最后的一篇博文(Demo 的实现)打下基础。
二、为什么要有类
对图像处理进行编程,必然要考虑到两个元素:图像数据 和 相关的处理算法。图像数据的存储方式当然是二维字符数组,一般我们会先在内存中开辟出一段区间存放图像数据,然后使用指向此区间首地址的指针来操作这些数据。而图像处理算法,则是针对图像数据的处理,然后把处理后的图像数据(二维字符数组)保存在内存的另一段区间中。
下面是 OpenCV 1.0 中的一段典型的图像处理算法函数:
void cvDilate( const CvArr* src, CvArr* dst, IplConvKernel* element=NULL, int iterations=1 );
其中参数 src 和 dst 自然表示着原图像和目标图像的存储区间首地址指针了。这样做直观自然,但是缺点也不少,用起来比较混乱,各种指针、各种算法子函数到处都是。有了类之后,我们就可以充分利用它的封装性,把图像数据与操作数据的算法函数组织在一起,不仅使程序结构更加紧凑,并且提高了类内部数据的安全性。现在最新版的 OpenCV 2.2 增加了对 C++ 的兼容,这也说明在图像处理中应用面向对象思想是不错的选择了。
三、 为什么要有自己的类
接下来我们说说为什么要有自己的类。C# 提供有 Bitmap,功能自然比较强大(在不考虑它的速度的前提下...)。MFC 提供的 CImage,封装了很多绘图功能和图像操作功能,“但仍然没有提供完整的图像处理能力”。(这句话是参考书作者说的,笔者还没有亲身体会到...所以先打上引号...)
再说了,用封装好的类,感觉总是隔了一层,不了解它的底层实现机制,用起来感觉挺不爽的。这种情况还是自己造造轮子吧~ 下面我们参考《数字图像处理与机器视觉》一书,逐步设计出具备基本功能的图像处理类 CImg。
四、构造 CImg 头文件
在 C++ 中设计的类,需要把它塞进 .h 和 .cpp 文件里去,当然要考虑类的成员分布问题。下面是一种比较好的设计风格:
- 头文件(.h):包括类的定义、类的成员数据、类的成员函数的声明、内联函数的实现
- 源文件(.cpp):类中成员函数的实现
下面直接给出头文件中的所有代码,然后再慢慢解释:
#ifndef _IMG_H_
#define _IMG_H_
// 在计算图像大小时,采用公式:biSizeImage = biWidth' × biHeight。
// 是biWidth',而不是biWidth,这里的biWidth'必须是4的整倍数,表示
// 大于或等于biWidth的,离4最近的整倍数。WIDTHBYTES就是用来计算 biWidth'
#define WIDTHBYTES(bits) (((bits) + 31) / 32 * 4)
class CImg
{
//构造、析构函数
public:
//构造函数
CImg();
//析构函数
virtual ~CImg();
//成员变量
public:
// 图像信息结构体指针
BITMAPINFOHEADER *m_pBMIH;
// 图像数据指针
BYTE **m_lpData;
protected:
int m_nColorTableEntries;
LPVOID m_lpvColorTable;
//成员函数
public:
//判断位图是否有效
BOOL IsValidate();
// 从文件加载位图
BOOL AttachFromFile(char* filePath);
// 将位图保存到文件
BOOL SaveToFile(char* filePath);
// 在DC上绘制位图
BOOL Draw(CDC* pDC);
// 设置像素的值
void SetPixel(int x, int y, COLORREF color);
// 获取像素的值
COLORREF GetPixel(int x, int y);
// 获取灰度值
BYTE GetGray(int x, int y);
// 获取行的像素数
int GetWidthPixel();
// 获取列的像素数
int GetHeightPixel();
// 获取一行的字节数
int GetWidthByte();
private:
//释放图像所占用的空间
void CleanUp();
};
/**************************************************
inline int CImg::GetWidthPixel()
功能:
返回CImg实例中的图像每行的像素数
参数:
无
返回值:
int类型,返回图像每行的像素数
***************************************************/
inline int CImg::GetWidthPixel()
{
return m_pBMIH->biWidth;
}
/**************************************************
inline int CImg::GetHeightPixel()
功能:
返回CImg实例中的图像每列的像素数
参数:
无
返回值:
int类型,返回图像每列的像素数目
***************************************************/
inline int CImg::GetHeightPixel()
{
return m_pBMIH->biHeight;
}
/**************************************************
inline int CImg::GetWidthByte()
功能:
返回CImg实例中的图像每行占用的字节数
限制:
无
参数:
无
返回值:
int类型,返回图像每行占用的字节数
***************************************************/
inline int CImg::GetWidthByte()
{
return WIDTHBYTES((m_pBMIH->biWidth)*m_pBMIH->biBitCount);
}
/**************************************************
inline SetPixel(int x, int y, COLORREF color)
功能:
设置指定像素的颜色
参数:
int x
指定像素的横坐标
int y
指定像素的纵坐标
COLORREF
RGB颜色值
返回值:
无
***************************************************/
inline void CImg::SetPixel(int x, int y, COLORREF color)
{
if(m_pBMIH->biBitCount == 24) //真彩色位图
{
m_lpData[m_pBMIH->biHeight - y - 1][x*3] = GetBValue(color);
m_lpData[m_pBMIH->biHeight - y - 1][x*3 + 1] = GetGValue(color);
m_lpData[m_pBMIH->biHeight - y - 1][x*3 + 2] = GetRValue(color);
}
}
/**************************************************
inline COLORREF CImg::GetPixel(int x, int y)
功能:
返回指定坐标位置像素的颜色值
参数:
int x
指定像素的横坐标
int y
制定像素的纵坐标
返回值:
COLERREF类型,返回用RGB形式表示的指定位置的颜色值
***************************************************/
inline COLORREF CImg::GetPixel(int x, int y)
{
if(m_pBMIH->biBitCount == 24) //真彩色位图
{
COLORREF color = RGB(m_lpData[m_pBMIH->biHeight - y - 1][x*3 + 2],
m_lpData[m_pBMIH->biHeight - y - 1][x*3 + 1],
m_lpData[m_pBMIH->biHeight - y - 1][x*3]);
return color;
}
else
{
throw "not support!";
return 0;
}
}
/**************************************************
inline BYTE CImg::GetGray(int x, int y)
功能:
返回指定坐标位置像素的灰度值
限制:
无
参数:
int x
指定像素的横坐标
int y
制定像素的纵坐标
返回值:
BYTE类型,返回给定像素的灰度值
***************************************************/
inline BYTE CImg::GetGray(int x, int y)
{
COLORREF color = GetPixel(x, y);
BYTE r, g, b, byte;
r = GetRValue(color);
g = GetGValue(color);
b = GetBValue(color);
double dGray = (0.299*r + 0.578*g + 0.114*b);
byte = (int)dGray;
return byte;
}
#endif // _IMG_H_
(1)构造和析构函数
一个类,当然要有构造和析构函数了:
//构造函数
CImg();
//析构函数
virtual ~CImg();
(2)数据成员
第一个数据成员自然是图像数据了,这里具体则是指向二维数组首地址的指针:
// 图像数据指针
BYTE **m_lpData;
然后我们需要一些数据成员来保存图像的一些信息,比如宽度、高度、图像类型等等。这里我们使用了 MFC 提供的结构体 BITMAPINFOHEADER(对于它的全部成员,可以查看 MSDN 详细介绍)。在类中,我们也没有直接定义结构体,而是定义的指向此类结构体的指针:
// 图像信息结构体指针
BITMAPINFOHEADER *m_pBMIH;
至于其他两个私有类型变量,m_nColorTableEntries 表示颜色索引表中的颜色个数,而 m_lpvColorTable 指向了颜色索引表的地址。但 24 位真彩色并不需要颜色索引表,这里保留它们是为了方便以后对非 24 位真彩色图像处理时的扩充。
(3)有效性判断
在对图像数据进行操作之前,首先要判断此图像是否有效:
//判断位图是否有效
BOOL IsValidate();
(4)读取/保存图像
我们在进行图像处理时,一般是针对保存在本地的图片进行的,所以必须要有读取/保存图像的功能:
// 从文件加载位图
BOOL AttachFromFile(char* filePath);
// 将位图保存到文件
BOOL SaveToFile(char* filePath);
P.S. 为简化读取复杂度,在本系列博文中我们仅使用 24 位真彩色 BMP 类型的图片作为源图像。
(5)设置/获取像素的 RGB 值,获取像素的灰度值
图像处理的操作,归根到底还是对每一像素的操作,所以设置/获取某像素的 RGB 值的操作也必不可少:
// 设置像素的值
void SetPixel(int x, int y, COLORREF color);
// 获取像素的值
COLORREF GetPixel(int x, int y);
同时,获取某一像素的灰度值也是比较常用的操作,我们也就集成到 CImg 类中了:
BYTE GetGray(int x, int y);
大家可以发现,以上三个函数的实现都是在 .h 文件中,而不是在 .cpp 文件中,而且函数前面还多了 inline 这个声明。这就是 C++ 中的所谓内联函数了,它的作用和宏定义非常类似,编译器在编译此段时,就是将它在每个调用点 “内联” 地展开。那么一般什么样的函数适合设计为内联函数呢?《Google C++ 风格指南》中做了很恰当的说明:
"当函数体较小的时, 内联该函数可以令目标代码更加高效. 对于函数体比较短, 性能关键的函数, 鼓励使用内联。"
这三个函数的实现代码都非常小,而且会被经常调用(特别是 SetPixel 函数),因此设计成 inline 函数再好不过了!
(6)获取行/列像素值
有时候我们希望获取图像的长与宽(单位:px),于是有了下面的两个简单的函数:
// 获取行的像素数
int GetWidthPixel();
// 获取列的像素数
int GetHeightPixel();
同样,把他们设计成内联函数是个不错的选择。
(7)获取每一行的字节数
// 获取一行的字节数
int GetWidthByte();
读者可能比较奇怪,上面不是已经有了一个 GetWidthPixel 了吗?怎么还来一个?
呵呵,注意这个函数返回值的单位不再是像素(px)的个数,而是字节(byte)的个数。那么你也许还会奇怪,既然是24位真彩色图,那么每个像素当然是 3 个字节了(R、G、B),我们直接用 GetWidthPixel() * 3 不就 OK 了么?怎么还需要另外定一个函数?
注意,在 windows 中有这样一个规则:位图的每一行占用字节数必须是 4 的整数倍,如果不是,则需补齐。这样一说才明白过来吧!原来还缺了 “补齐” 这一步。
那么怎么补齐?我们首先可以得到每一行图像数据的位数 bits = GetWidthPixel() * 3 * 8,然后考虑到每一行字节数必须是4的倍数的话,那么最小的步长应该就是 8 * 4 = 32 (bits) 了。换句话说,我们有一行有 bits 个位,怎么样补全使之成为 32 的整数倍?
在程序的头部提供了用于计算字节数的宏:
#define WIDTHBYTES(bits) (((bits) + 31) / 32 * 4)
(这里笔者不做过多解释,请读者想一想这算法能起作用吗?如果能的话,可不可以把后面的 /32*4 简化成 /8 ?)
于是,函数的实现代码如下:
inline int CImg::GetWidthByte()
{
return WIDTHBYTES((m_pBMIH->biWidth)*m_pBMIH->biBitCount);
}
注:当图像为 24 位真彩色图时,m_BMIH->bitBitCount = 24。
(8)在DC(设备上下文)上绘图
图像处理完了,当然要在某个窗口中显示出来才能看到效果嘛,所以我们需要有个函数来对 MFC 的设备上下文(Device Context)进行图形绘制。对 C# 比较熟悉的同学一定对 Graphic 这个类非常熟悉,要把图像绘制到窗口中,就得对窗口的 Grapgic 对象进行绘制。C# 中的 Graphic 和 MFC 中的 DC,就是差不多的地位了。
// 在DC上绘制位图
BOOL Draw(CDC* pDC);
(9)资源清理
在类的最后我们可以发现一个私有函数 CleanUp:
void CleanUp();
看名字就可以知道它是干啥的了——清理类中所申请的内存资源。现在我们不需要知道它的具体实现,只要知道它的用途就行,等到下面介绍 .cpp 文件时再详细解释。
五、构造 CImg 源文件
将类放在 .h 和 .cpp 两个文件的好处之一,是可以提供不同层次的服务。如果你不想了解底层实现细节,OK,那么只需要看 .h 文件就行,它会提供所有数据成员、成员函数的声明,你就可以轻松地使用这个类了。但如果你还想了解具体的函数实现,那就需要在 .cpp 文件中仔细分析代码了。
下面给出 CImg.cpp 的全部源代码:
#include "stdafx.h"
#include "Img.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/**************************************************
CImg::CImg()
功能:
CImg类的构造函数
参数:
无
返回值:
无
***************************************************/
CImg::CImg()
{
m_pBMIH = NULL;
m_lpData = NULL;
}
/**************************************************
CImg::~CImg()
功能:
CImg类的析构函数
参数:
返回值:
***************************************************/
CImg::~CImg()
{
CleanUp();
}
/**************************************************
void CleanUp()
功能:
释放图像信息结构体的空间、释放图像数据占用的空间
相当于擦除图像数据
参数:
无
返回值:
无
***************************************************/
void CImg::CleanUp()
{
if(m_lpData != NULL)
{
for (int i; i<m_pBMIH->biHeight; i++)
{
delete[] m_lpData[i];
}
delete[] m_lpData;
}
if(m_pBMIH != NULL)
{
delete[] m_pBMIH;
}
}
/**************************************************
BOOL CImg::IsValidate()
功能:
判断位图是否有限
参数:
无
返回值:
BOOL类型,TRUE表示有效,FALSE表示无效
***************************************************/
BOOL CImg::IsValidate()
{
return m_pBMIH != NULL;
}
/**************************************************
BOOL AttachFromFile(char* filePath)
功能:
从文件中读取位图数据
参数:
char* filePath
文件路径
返回值:
BOOL类型,返回是否读取成功
***************************************************/
BOOL CImg::AttachFromFile(char* filePath)
{
// 使用CFile对象简化操作
CFile file;
if(!file.Open(filePath, CFile::modeRead|CFile::shareDenyWrite))
return FALSE;
// 文件数据
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;
file.Close();
return TRUE;
}
/**************************************************
BOOL SaveToFile(char* filePath)
功能:
将位图数据保存到文件中
参数:
char* filePath
文件路径
返回值:
BOOL类型,返回是否保存成功
***************************************************/
BOOL CImg::SaveToFile(char* filePath)
{
if(!IsValidate())
return FALSE;
CFile file;
if(!file.Open(filePath, CFile::modeRead|CFile::modeWrite|CFile::modeCreate))
{
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);
}
file.Close();
return TRUE;
}
/**************************************************
BOOL Draw(CDC* pDC)
功能:
在给定的设备上下文环境中绘制图像
参数:
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;
}
以上函数的实现代码都有详细的注释,就不做全面的解释了,下面仅挑出两点来说明一下:
(1)析构函数
我们并没有使用默认的析构函数,而是在其中加了 CleanUP 这个函数,那么 CleanUp 函数所起到的作用到底是什么呢?我们先来看看它的源代码:
1 void CImg::CleanUp()
2 {
3 if(m_lpData != NULL)
4 {
5 for (int i; i<m_pBMIH->biHeight; i++)
6 {
7 delete[] m_lpData[i];
8 }
9 delete[] m_lpData;
10 }
11
12 if(m_pBMIH != NULL)
13 {
14 delete[] m_pBMIH;
15 }
16 }
3-10行为第一部分:使用 delete[] 释放图像数据所占用的内存空间;12-15行为第二部分:使用 delete[] 释放图像信息结构体所占用的内存空间。既然说要释放,那么之前总应该有申请内存的语句吧,但是在构造函数中,并没有使用关键字 new 啊?
CImg::CImg()
{
m_pBMIH = NULL;
m_lpData = NULL;
}
其实,在创建了空的 CImg 对象后,我们马上会调用 AttachFromFile() 函数,使用特定的本地图像文件对其数据成员进行初始化,观察 AttachFromFile() 函数可以发现这两段代码:
pBMIH = (BITMAPINFOHEADER*)new BYTE[bmfHeader.bfOffBits - sizeof(bmfHeader)];
// 申请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);
}
呵呵,发现了吧,在对图像信息结构体 pBMIH 和 图像数据 lpData 初始化时,都使用了 new 动态申请了内存空间,这样我们在析构的时候,自然要先把这些手动申请的空间还给系统了,以免引起内存泄露。
(2)读取/保存图像文件
大家看到源代码中的读取图像(AttachFromFile)函数 和 保存图像(SaveToFile)函数时,或许会感觉比较复杂,理解它们需要对 BMP 图像文件的存储结构有一定的了解。如果你不太想了解底层原理,可以直接把这两个函数 copy 过去用就行。但如果真的想知道轮子是怎么造出来的,建议去看园子里的 这一篇 博文,讲得非常详细,这里就不再赘述了。
弄懂了 BMP 文件的存储结构,相信再去看 AttachFromFile 和 SaveToFile 这两个函数就会非常轻松了~
六、构造 CImgProc
之所以构造出这个类来,是出于两方面的考虑(1)让 CImg 类封装图像的数据成员和一些最基本的处理函数,而把所有的特定算法的实现都封装到 CImgProc 类中,这样一来使 “数据” 和 “逻辑” 分离,从而使程序的层次结构更加清晰(2)使 CImgProc 公共地继承自 CImg,那么自然就继承了 CImg 中的数据和成员函数了,这样我们在实现算法时,只需使用 CImgProc 就够了,非常方便。
下面给出 CImgProc 类的头文件:
#ifndef _IMGPROC_H
#define _IMGPROC_H
#include "Img.h"
class CImgProc : public CImg
{
//构造与析构函数
public:
//构造函数
CImgProc();
//析构函数
virtual ~CImgProc();
//各种图像算法
public:
// 灰度阈值变换
void Threshold(CImgProc *pTo, BYTE nThres);
};//class CImgProc
#endif//_IMGPROC_H_
头文件中仅额外申明了一个算法子函数,那就是 Demo 中需要使用到的 “灰度阈值变换算法” —— Threshold 了。以后若需要添加其他的算法的话,只要在头文件中声明,然后在源文件中实现就OK~ 下面给出 CImgProc 类的源文件代码:
#include "stdafx.h"
#include "ImgProc.h"
#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[]=__FILE__;
#define new DEBUG_NEW
#endif
// 构造函数
CImgProc::CImgProc()
{
}
//析构函数
CImgProc::~CImgProc()
{
}
/**************************************************
void CImgProcess::Threshold(CImgProc *pTo, BYTE bThre)
功能:
图像的阈值变换
参数:
CImgProc * pTo
输出CImgProc对象的指针
BYTE bThre
设置的基准阈值
返回值:
无
***************************************************/
void CImgProc::Threshold(CImgProc *pTo, BYTE bThre)
{
int i, j;
BYTE bt;
for(j = 0; j<m_pBMIH->biHeight; j ++)
{
for(i=0; i<m_pBMIH->biWidth; i++)
{
bt = GetGray(i, j);
if(bt<bThre)
bt = 0;
else
bt = 255;
pTo->SetPixel(i, j, RGB(bt, bt, bt));
}
}
}
七、结语
本篇博文将面向对象的思想引入 Vc 平台下的图像处理中,逐步构建了 图像类 CImg 及其用于实现各类图像算法的派生子类 CImgProc,需要源文件的童鞋可以 点击此处 下载。我们已经走完了第一步,下一步(也是本系列博文的最后一篇),将会在 MFC 下利用我们设计的类来完成关于 “灰度阈值变换的” Demo。敬请期待系列博文之:
swicth ( VcImageProc ) case 4:Demo 的实现,基于MFC
P.S. 粘贴代码时勾选了折叠代码选项,结果浏览时点击+号不能展开... 不知道是哪出了问题...