Visual C++作为一个功能非常强大的可视化应用程序开发工具,是计算机界公认的最优秀的应用开发工具之一。Microsoft的基本类库MFC使得开发Windows应用程序比以往任何时候都要容易。C++提供的各种函数、指针操作和直接对硬件操作使得图像处理速度较快,专门为VC++设计的OpenGL和DirectX技术可以使开发人员在Windows环境下比较容易地完成图像图形的高性能处理和显示。用VC++编制的应用软件无论在处理速度和图像的显示等方面都表现出较高的效率,因此在编制基于Windows的各类数字图像处理程序时, VC++无疑是程序员进行图像处理的最佳选择。
本文主要介绍了本人在用VC++进行图像基本处理时获得一些技巧与经验,希望与广大爱好图像处理的编程者分享。
一、模板(template)的使用
对于图像处理应用软件来说,图像数据的管理是至关重要的,在"电脑编程与技巧"杂志99年11期, 本人介绍了一个封装的通用图像基类(CImage),可完成BMP格式图像数据的管理和一些基本的图像处理功能。但是该CImage类只是对位图进行处理,即操作的数据类型为无符号8位字符型(BYTE)。但是在进行边缘检测、相关跟踪、快速傅立叶变换等图像处理中经常需要定义一些int或float类型的二维数组来保存处理的中间结果或最终结果。另外如果用OpenGL编程来进行图像处理和显示,为了保证跨平台特性,OpenGL定义了一套数据类型:有无符号8位字符型(BYTE)、16位短整型(short)、无符号16位短整型(unsigned short)、32位字符型(int)、无符号32位字符型(unsigned int)、单精度浮点数(float)等类型。如果对应于每一种数据类型都定义一个类,这样
虽然能解决问题,但并不是一个明智的办法。例如要修改一个变量或函数,那对应于每一种数据类型定义的类多要做同样的修改,毫无疑问,这种方法操作繁琐,容易发生错误,需要做大量的拷贝修改工作,因此是一种低效的方法。
C++引入的模板(template)概念,这一个关键字会告诉编译器下面的定义将操作一个或更多的非特定的类型。只有当对象被定义时,这些类型才必须被指定以使编译器能够替代它们。因此可以用一个非特定的参数来作类型名称,用该非特定的参数来定义一个通用的数据类或一个函数,诸如于MFC类库中的CArrary、CTypedPtrList等类。下面给出了支持不同数据类型的一个图像二维数据模板类和一个快速排序类.
1.定义不同数据类型的数据模板类
数据模板类的定义如下:
//--------不同数据类型图像二维数组模板的定义----------
template <class T>
class ImageTemplate
{
public:
T ** lp_AddRow; // 二维数组的数据指针
unsigned int Width; // 数组的宽度(列数)
unsigned int Height; // 数组的高度(行数)
unsigned int ImageSize; // 数组的大小
public:
ImageTemplate(void); // 构造函数
~ImageTemplate(void); // 析构函数
// 带参数的析构函数
ImageTemplate(unsigned int w, unsigned int h);
// 分配数据内存的函数
void Construct(unsigned int w, unsigned int h);
// 释放数据内存的函数
void DeleteData();
};
// 构造函数
template <class T>
ImageTemplate< T >::ImageTemplate(void)
{
lp_AddRow = NULL;
Width = Height = 0;
}
// 析构函数
template <class T>
ImageTemplate< T >::~ImageTemplate(void)
{
DeleteData();
}
// 带参数的析构函数
template <class T>
ImageTemplate< T >::ImageTemplate(unsigned int w, unsigned int h)
{
lp_AddRow = NULL;
Width = w;
Height = h;
ImageSize = w * h;
Construct(w, h);
}
// 分配数据内存的函数
template <class T>
void ImageTemplate< T >::Construct(unsigned int w, unsigned int h)
{
DeleteData();
if( Height > 0 ) lp_AddRow = new T*[Height];
if( ImageSize > 0 )
{
lp_AddRow[0] = new T[ImageSize];
memset(lp_AddRow[0], 0, ImageSize* sizeof(T) );
for(unsigned int i= 1; i < Height; i++)
lp_AddRow[i] = lp_AddRow[i- 1] + Width;
}
}
// 释放数据内存的函数
template <class T>
void ImageTemplate< T >::DeleteData()
{
if(lp_AddRow != NULL)
{
if( lp_AddRow[0] != NULL ) delete []lp_AddRow[0];
delete []lp_AddRow;
lp_AddRow = NULL;
}
}
// 定义不同数据的类
typedef ImageTemplate< int > IntImage;
typedef ImageTemplate< float > FloatImage;
typedef ImageTemplate< short int > SintImage;
typedef ImageTemplate< unsigned char > BYTEImage;
在程序中使用方法如下:
FloatImage fImage(640, 480);
SintImage sImage(640, 480);
修改或获取数据可以通过公共的数据指针变量 lp_AddRow,如
for(int j= 0; j< 480; j++)
for(int i= 0; i< 640 ;i++)
{
fImage.lp_AddRow[j][i] = i*j/ 100.0f;
iImage.lp_AddRow[j][i] = 20* (i + j);
}
由于图像处理有着快速的要求,因此一般将数据指针作为公共变量直接使用,如果考虑到程序的安全性和稳定性,可以将该数据指针定义为私有变量,通过重载的操作符号[]来进行数据的修改和获取。
2.支持不同数据类型的快速排序的类
在图像处理中,对于提取出来的各种特征量要进行分析和识别,快速数据排序是其中常用的一种算法。虽然可以用函数重载的方法对不同的类型编制该类型数据的排序程序,但是引入的代码较多,因此函数重载的方法是一种低效的方法。本快速排序的类的排序功能由c标准库<stdlib.h>中定义的
qsort( void *base, size_t num, size_t width,
int (__cdecl *compare )(const void *elem1, const void *elem2 ) );
函数来完成,其中参数含义如下:
void *base 数据指针
size_t num 数据个数
size_t width 单个数据的字节宽度
int (__cdecl *compare ) 比较函数
对应于不同类型的数据使用该函数,按一般的方法需要编制对应不同数据类型的比较函数,而且还要进行数据类型强制转换,因此比较麻烦。利用下面定义的类就可以很方便的实现数据的快速排序。
//基于模板的快速排序类
template <class T>
class LQuickSort
{
public:
// 升序排列的比较函数
static int cmpT1(const void *ap, const void *bp);
// 降序排列的比较函数
static int cmpT2(const void *ap, const void *bp);
// 排序的函数
static void rank(T* lp, // 数据的指针
int length, // 数据的个数
BOOL IsAscend = true );//升序
};
// 升序排列的比较函数
template <class T>
int LQuickSort<T>::cmpT1(const void * ap, const void * bp)
{
T a = *(T *)ap;
T b = *(T *)bp;
if( a > b )
return 1;
if( a < b )
return -1;
return 0;
}
// 降序排列的比较函数
template <class T>
int LQuickSort<T>::cmpT2(const void * ap, const void * bp)
{
T a = *(T *)ap;
T b = *(T *)bp;
if( a > b )
return -1;
if( a < b )
return 1;
return 0;
}
//- -----排序的函数----------------------------
// IsAscend = true
// T* lp, 数据的指针
// int length, 数据的个数
// BOOL IsAscend true 升序排列, false 降序排列
template <class T>
void LQuickSort<T>::rank(T* lp, int length, BOOL IsAscend)
{
if( IsAscend )
qsort( (void *)lp, length, sizeof(T), cmpT1 );
else
qsort( (void *)lp, length, sizeof(T), cmpT2 );
}
由于定义的类的成员函数为静态(static)类型,因此也可以不用声明该类的对象而调用排序函数。待排序的数据格式如下:
int ix[10] = {10, -10, 2, ...};
float fx[10] = {10.0, -10.4, 2.3, ...};
具体的排序用法有下面的两种。
用法一:定义类的对象
LQuickSort<int> iSort;
LQuickSort<float> fSort;
iSort.rank(ix, 10,true); // 升序排列
iSort.rank(ix, 10, false); // 降序排列
fSort.rank(fx, 10,true); // 升序排列
fSort.rank(fx, 10, false); // 降序排列
用法二:不定义类的对象
LQuickSort<int>::rank(ix, 10, true); // 升序排列
LQuickSort<int>::rank(ix, 10, false); // 降序排列
LQuickSort<float>::rank(fx, 10, true); // 升序排列
LQuickSort<float>::rank(fx, 10, false);// 降序排列
由于排序函数的最后一个参数缺省为true,因此升序排列也可以写为:
LQuickSort<int>::rank(ix, 10); // 升序排列
用上面定义的类就可以很方便地实现对不同类型的数据快速排序。
使用模板来定义类时,可以看出除了template <class T>以外,该类看上去像一个通常的类,
这里的T时替换参数,它表示一个类型名称。如果使用非内联成员函数定义,则要在函数定义前加入template <class T>用来告诉编译器察看模板声明。在成员函数的定义中,类名称被限制为模板参数类型,如LQuickSort<T>。而且更为特殊的是,即使定义非内联成员函数时,函数的实现代码也要放在类的头文件中,否则会发生连接错误。这似乎违背了通常的头文件定义准则:“不要在分配存储空间前放置任何东西”,这条准则是为了防止连接时的多重定义错误。但模板定义比较特殊,由template <...>处理的任何东西都以为意味着编译器在当时不为它分配存储空间,它一直处于等待状态直到被一个模板实例告知。在编译器和连接器的某一处,有一机制能去掉指定模板的多重定义。所以为了容易使用,几乎总是在头文件中放置全部的模板声明和定义。
可以看出使用了模板后,大大简化了代码,提高了图像处理编程的效率。 二、高精度获得处理所用的时间
对于一种算法,其处理占用的时间越少则表明该算法效率越高,因此获得算法占用的时间是衡量算法的一个指标。
普通的获得处理占用时间的方法有:1.用time()函数或CTime类获得秒量级的时间;2. 用Windows API函数 GetTickCount()可以获得毫秒量级的时间。下面给出一个可以达到微秒量级的时间测量类,主要通过Windows API函数 QueryPerformanceFrequency()和 QueryPerformanceCounter()两个函数来实现。用QueryPerformanceFrequency()函数获得计算机高精度的性能计数的频率,该频率值在多台机器上测量均为 f = 1193180,这样其分辨率为1/f < 1 微秒,只要获得处理前后
QueryPerformanceCounter()返回当前高精度的性能计数值的差值,就可以获得处理占用的时间。具体程序如下:
// 精确获得算法处理时间的类(毫秒量级)
class LTimeCount
{
private:
// 算法处理时间(单位:秒)
double UseTime;
// 计数值
LARGE_INTEGER Time, Frequency, old;
public:
// 计时开始
void Start()
{
QueryPerformanceFrequency(&Frequency);
QueryPerformanceCounter(&old);
UseTime = 0.0;
}
// 计时结束
void End()
{
QueryPerformanceCounter(&Time);
UseTime = (double) ( Time.QuadPart - old.QuadPart) / (double)Frequency.QuadPart;
}
// 获得算法处理时间(单位:秒)
double GetUseTime()
{
return UseTime;
}
};
使用的方法为:
// 定义一个时间测量类的对象
LTimeCount m_CountUseTime;
// 开始计时
m_CountUseTime.Start();
// 下面进行相应的处理
// --图像处理算法--
// 计时结束
m_CountUseTime.End();
// 获得并显示处理占用的时间
CString msg;
msg.Format("处理的时间是: %12.8f 秒\n", m_CountUseTime.GetUseTime() );
AfxMessageBox(msg);
通过实际的测试,该方法获得的时间可以达到微妙量级,同时发现用GetTickCount()函数获得的计数很不准确。以时间间隔为40毫秒,用GetTickCount()函数测量的时间间隔误差在3-5毫秒。
三、图像处理算法程序优化的方法
1. 减少乘除运算,将乘除运算变为加减运算。
如将 a =2*b, 写为 a = b + b。将多项式计算 a = c[0] + c[1] * x + c[2]*x*x + …
+ c[n]*pow(x,n), 写为 for(a = c[n] , i = n; i >= 0; i--) a = a * x + c[i]。
2. 使用指针加快速度
由于指针直接指向操作的内存地址,因此用指针可以加快程序执行的速度。一般在进行图像的卷积运算时,摸板的数据是连续的内存,则可以用 *lpTemp++语句对数据进行操作,该语句的意思是在进行操作后,指针pTemp加1从而指向下一个数据。由于C++语言对"++"进行了优化,因此可以较大幅度的提高速度。
3. 建立查找表或变量
如果在一个循环中要多次用到sin()、cos()、exp()等函数时,这些函数的参数是不变的,则可以通过在外定义查找表或变量的方法来减少计算量。如普通的获得图像数据的方法如下:
Sin1 = sin(Theta);
Cos1 = cos(Theta);
Exp1 = exp(-Theta * Theta);
for(RowAddress = 0, j = 0; j < ImgH; j++)
{
for(i = 0; i< ImgW; i++)
image[RowAddress + i] = i *Sin1 + j *Cos1 + i*j* Exp1;
RowAddress +=imgW;
}
用上面的处理方法可以减少ImgW*(ImgH-1)次乘法运算和3*ImgW*ImgH-3次调用sin()、cos()、exp()
等函数运算。如果进行高斯滤波等运算,则可以建立数据查找表,事先将摸板的数据算好存在查找表中,使用不须计算可以直接使用,可以节省大量的计算时间。如果一次运算需要涉及多行的数据,则可以通过建立指向图像数据行首地址的指针查找表来加快速度,如下:
BYTE **RowAddress = new BYTE*[ImgH];
for(RowAddress[0] = InputImage ,i= 1 ; i< ImgH ; i++)
{
RowAddress[i] = RowAddress[i-1] + ImgW;
}
这样用RowAddress[y][x]便可以对坐标(x,y)的灰度值进行操作。
4. 内存的快速初始化和拷贝
用memset()函数可以对一块连续内存进行初始化,memcpy()函数可以将一块连续内存的数据拷贝到另一块连续内存中。如memset(Sigma, 0, ImgW*ImgH*sizeof(int))语句将Sigma指针指向的一块连续int类型的内存初始化为0。而memcpy(tmpImage, InputImage, Imagesize)语句可以将InputImage指针指向的一块连续内存的数据快速拷贝到tmpImage指针指向的一块连续内存中。
如果拷贝目标或源内存是不连续,由于在一行上面是连续的,因此可以变成数据高度次数行拷贝也可完成。
5.数据指针转换
由于计算机的数据总线的位数一般是32,因此数据类型的占用字节位数为32或32的整数倍,则操作速度将会加快,以用按位取反操作符(~)来对整数型数据取反为例,将数据指针强制由BYTE*类型转换为DWORD*类型,速度比用BYTE*类型指针速度提高4倍。
6.寄存器优化
传统的编译器对寄存器的利用率往往不高,因为传统的编译器对寄存器的分配是以表达式为单位,对于不同的表达式寄存器被重新分配。通常这种方法有利于克服寄存器的溢出,但这种方法在一个循环中会造成一个数据反复从内存中装入寄存器,如下面的语句。
for( i = 0 ; i < m; i++){ c1[i] = a * d1[i]; c2[i] = b * d2[i]; }
变量a和b公用了一个寄存器,因此每次循环时变量a和b都要重新装入。但是这样的方法受到寄存器数量的限制,而且要求程序员对寄存器非常熟悉,因此一般并不常用。实际上许多图像的处理形式都类似于一个模板与图像进行卷积运算。因此针对摸板的大小进行了不同的优化。对小模板处理以Sobel算子为例,一般的程序如下
for(j=1; j<ImgH-1; j++)
{
for(i=1; i<ImgW-1; i++)
{
tempx = tempy =0;
for( y= -1; y<=1; y++)
{
for(x= -1; x<=1; x++)
{
tempx += Image[j+y][I+x]*xMask[y+1][x+1]
tempy += Image[j+y][I+x]*yMask[y+1][x+1]
}
}
nG = int(sqrt(dx*dx+dy*dy) + 0.5);
NewImage[j][i] = (nG>255) ? 255 : nG;
}
}
将内部循环展开,这样首先循环开销如循环控制变量的修改、循环条件的判断被删除;其次,内部循环展开使得原先指令数较少的循环体指令序列变成指令数众多的非循环指令序列。这样许多指令调度和经典优化可以被使用。将摸板的元素都各自分配一个寄存器,如mx0 = xMask[0][0],在循环体中用寄存器变量mx0代替xMask[0][0]可以减少大量的寄存器装载时间。另外如果模板的系数已知则直接用常数计算,如下面的代码,速度比未优化前提高了许多。
for(j=1; j<ImgH-1; j++)
{
for(i=1; i<ImgW-1; i++)
{
dx = RowAddress[j-1][i+1] - RowAddress[j-1][i-1];
dx += (RowAddress[ j ][i+1] + RowAddress[ j ][i+1] -
RowAddress[ j ][i-1] - RowAddress[ j ][i-1]);
dx += RowAddress[j+1][i+1] - RowAddress[j+1][i-1];
dy = RowAddress[j+1][i-1] - RowAddress[j-1][i-1];
dy += (RowAddress[j+1][ i ] + RowAddress[j+1][ i ] -
RowAddress[j-1][ i ] - RowAddress[j-1][ i ]);
dy += RowAddress[j+1][i+1] - RowAddress[j-1][i+1];
nG = int( sqrt(dx* dx+dy* dy) + 0.5 );
Image[i] = (nG>255) ? 255: nG;
}
Image += ImgW;
}
而对于大模板处理,由于寄存器的数量有限,可以将大模板运算变成若干个子模板的计算来完成。
由于循环体语句众多,这样处理增加的绝对计算量与节省的计算量相比可以忽略不计。
四、其它一些杂项
1. 用CRectTracker类获得操作区域
有时仅需要对某一个区域进行处理,如果用常规的方法响应鼠标左键按下,放开和鼠标移动消息,编程则比较复杂,但如果用MFC类库中的CRectTracker类就可以很方便地用鼠标获得要处理的区域。
用法如下:
首先在View类头文件中声明一个对象
CRectTracker RectTracker;
然后在OnDraw()函数中加入下面的语句
RectTracker.Draw(pDC);
最后在响应鼠标左键按下消息处理函数OnLButtonDown()中加入语句
if (RectTracker.TrackRubberBand(this, point))
{
Invalidate();
}
这样便可用RectTracker的成员变量m_rect获得所选的区域。
2. sizeof 操作符的使用
该操作符的用法有 sizeof 一元表达式 或sizeof(类型名),返回的整型值给出操作数在内存空间占用的字节数。如果操作数是一个数组类型,则可以得到该数组数据指针指向的内存空间的大小,如下例。
int hist[100][10];
int size1 = sizeof(hist); //= 4000;
int size2 = sizeof(hist[0]); //= 40;
int size3 = sizeof(hist[0][0]); //= 4;
对于一维数据指针lp, 用sizeof(lp)/sizeof(lp[0])就可以获得数组的大小。
3. 使用“=”操作符时的注意事项
如下例:
int a = 10, b= 5;
int c = a -= b;
由于c++语言是从右到左执行,因此上式执行完后 a = c = 5;
4. 在循环中使用“++”或“--”操作符时的注意事项
在循环语句for()初始化表达式中无论使用++i还是i++(或--i和i--),在循环体中的变量i的值是一样, 应为++i或i++相当于在循环体当次循环的最后一句执行,如下例。
int i, k= 0, l= 0;
for(i= 0; i < 5; i++)
{
TRACE("k= %d l= %d i= %d\n", k++, l++, i);
}
k = l= 0;
for(i= 0; i < 5; ++i)
{
TRACE("k= %d l= %d i= %d\n", k++, ++l, i);
}
结果:
k= 0 l= 0 i= 0
k= 1 l= 1 i= 1
k= 2 l= 2 i= 2
k= 3 l= 3 i= 3
k= 4 l= 4 i= 4
k= 0 l= 1 i= 0
k= 1 l= 2 i= 1
k= 2 l= 3 i= 2
k= 3 l= 4 i= 3
k= 4 l= 5 i= 4
以上是本人使用VC++编制图像处理程序中得到的一点技巧和经验,希望对大家有所帮助。
|