1."浮雕"图像
"浮雕"图象效果是指图像的前景前向凸出背景。所谓的"浮雕"概念是指标绘图像上的一个像素和它左上方的那个像素之间差值的一种处理过程,为了使图像保持一定的亮度并呈现灰色,我在处理过程中为这个差值加了一个数值为128的常量。需要读者注意的是,当设置一个像素值的时候,它和它左上方的像素都要被用到,为了避免用到已经设置过的像素,应该从图像的右下方的像素开始处理,下面是实现的源代码:
void CDibView::OnFDImage() //产生"浮雕"效果图函数 { HANDLE data1handle; LPBITMAPINFOHEADER lpBi; CDibDoc *pDoc=GetDocument(); HDIB hdib; unsigned char *hData; unsigned char *data; hdib=pDoc->GetHDIB(); //我是如何打开图像文件并得到图像数据,请感兴趣的朋友参考 //天极网上我的相关文章,那里有详细的论述,这里不再赘述。 BeginWaitCursor(); lpBi=(LPBITMAPINFOHEADER)GlobalLock((HGLOBAL)hdib); hData=(unsigned char*)FindDIBBits((LPSTR)lpBi); pDoc->SetModifiedFlag(TRUE); data1handle=GlobalAlloc(GMEM_SHARE,WIDTHBYTES(lpBi->biWidth*8)*lpBi->biHeight); //声明一个缓冲区用来暂存处理后的图像数据 data=(unsigned char*)GlobalLock((HGLOBAL)data1handle); AfxGetApp()->BeginWaitCursor(); int i,j,buf; for( i=lpBi->biHeight; i>=2; i--) for( j=lpBi->biWidth; j>=2; j--) { //"浮雕"处理 buf=*(hData+(lpBi->biHeight-i)*WIDTHBYTES(lpBi->biWidth*8)+j)-*(hData+(lpBi->biHeight-i+1)*WIDTHBYTES(lpBi->biWidth*8)+j-1)+128; if(buf>255) buf=255; if(buf<0)buf=0; *(data+(lpBi->biHeight-i)*WIDTHBYTES(lpBi->biWidth*8)+j)=(BYTE)buf; } for( j=0; j biHeight; j++) for( i=0; i biWidth; i++) //重新写回原始图像的数据缓冲区 *(hData+i*WIDTHBYTES(lpBi->biWidth*8)+j)=*(data+i*WIDTHBYTES(lpBi->biWidth*8)+j); AfxGetApp()->EndWaitCursor(); GlobalUnlock((HGLOBAL)hdib); GlobalUnlock(data1handle); EndWaitCursor(); Invalidate(TRUE);//显示图像 } |
2."雕刻"图像
上面讲述了通过求一个像素和它左上方像素之间的差值并加上一个常数的方法生成"浮雕"效果的灰度图像,"雕刻"图像与之相反,它是通过取一个像素和它右下方的像素之间的差值并加上一个常数,这里我也取128,经过这样处理,就可以得到"雕刻"图像,这时候图像的前景凹陷进背景之中。同样需要读者注意的是为了避免重复使用处理过的图像像素,处理图像时要从图像的左上方的像素开始处理。实现代码如下:
void CDibView::OnDKImage() { // TODO: Add your command handler code here HANDLE data1handle; LPBITMAPINFOHEADER lpBi; CDibDoc *pDoc=GetDocument(); HDIB hdib; unsigned char *hData; unsigned char *data; hdib=pDoc->GetHDIB(); BeginWaitCursor(); lpBi=(LPBITMAPINFOHEADER)GlobalLock((HGLOBAL)hdib); hData=(unsigned char*)FindDIBBits((LPSTR)lpBi); pDoc->SetModifiedFlag(TRUE); data1handle=GlobalAlloc(GMEM_SHARE,WIDTHBYTES(lpBi->biWidth*8)*lpBi->biHeight); data=(unsigned char*)GlobalLock((HGLOBAL)data1handle); AfxGetApp()->BeginWaitCursor(); int i,j,buf; //图像的"雕刻"处理 for( i=0;i<=lpBi->biHeight-2; i++) for( j=0;j<=lpBi->biWidth-2; j++) { buf=*(hData+(lpBi->biHeight-i)*WIDTHBYTES(lpBi->biWidth*8)+j)-*(hData+(lpBi->biHeight-i-1)*WIDTHBYTES(lpBi->biWidth*8)+j+1)+128; if(buf>255) buf=255; if(buf<0)buf=0; *(data+(lpBi->biHeight-i)*WIDTHBYTES(lpBi->biWidth*8)+j)=(BYTE)buf; } for( j=0; j biHeight; j++) for( i=0; i biWidth; i++) //重新将处理后的图像数据写入原始的图像缓冲区内 *(hData+i*WIDTHBYTES(lpBi->biWidth*8)+j)=*(data+i*WIDTHBYTES(lpBi->biWidth*8)+j); AfxGetApp()->EndWaitCursor(); GlobalUnlock((HGLOBAL)hdib); GlobalUnlock(data1handle); EndWaitCursor(); Invalidate(TRUE); } |
Lena原图 | "雕刻"效果图 | "浮雕"效果图 |
用VC实现小型矢量图形系统的开发
大家学习了VC的MFC的一些基础知识后,如果能用VC开发一个比较实用的软件,对熟悉VC各方面编程和面向对象的软件设计和开发都是很有帮助的。
本文旨在通过对一个作者自己开发的小型矢量图形系统全面讲述而达到让读者了解一个小软件从设计到实现的阶段的解决的问题。同时也从界面和功能上对MFC和Windows系统功能的挖掘,同样,对于学习计算机图形学的读者,也可以看到本文有很多对图形学算法和实现的有益探讨。
一. 功能和界面设计
首先,让大家对一个本软件功能的大概了解。当你着手开发一个软件时,首先要解决的当然是本软件的功能(软件工程常称作用例,具体概念可以参考有关资料,不妨简单理解为用户使用它能完成哪些工作)。由于写这篇文章时,本软件已经具有比较完整的原型。我们可以结合它的界面(图1)来介绍软件设计的过程。
图 1 软件界面 |
可以看到,本软件是实现了一个绘图功能的子集。最初就确定了开发环境为VC6.0,界面采用IE风格。在使用上为了给用户最大的便利,采用了三种工具条(普通文件、打印操作等标准工具,对图形对象属性设置的工具条式对话框,带文字说明的大按钮式可浮动或任意船坞- Dock定位的绘图工具条)。
操作上采用左键点击建立图形对象起始点,移动动态调整图形大小和位置(随手画采用按住左键拖动的方式,再次点击左键确定位置,右键取消操作,双击确定(结束)多步图形对象(如多边形)的绘制。在功能设计方面基本符合一般图形软件的惯例,但出于作者的便利和保护鼠标的考虑,整个功能体现了基本无需按住左键拖动的思想。这也是很容易让人接受的,因为即便习惯拖动的用户拖动时也会产生位置调整,只是释放后还是出于拖动状态,再次点击或双击才最终确定。
功能上选择了画线、框、圆、多边形、立体、文字、曲线、填充以及删除的功能,根据是否填充和光照又增加了几个类别,填充方式根据图形学的概念提供了两种方式(以后介绍)。根据对图形属性取了线宽、线型(很容易实现简单的线型,由于想加入更多的特性,作者先没有具体实现它,以后作者会提到它的实现,读者有兴趣可以试着实现)、边框色、填充色和字体几个属性。当然,这些功能在面向对象的方法中都是可以很方便扩展的(如画椭圆,选取对象,对象的位移和旋转操作,根据填充算法实现同色选取,即Photoshop等软件的魔棒功能等),对于橡皮擦功能可以很简单的实现特定工具或告诉用户如何实现此功能(即用背景色利用已有功能绘图)。
内部实现上,要求单独记录各图形的关键属性(如位置、色彩等,这些是矢量图区别于位图的特点)。由于各对象可以形成对象链表,因此,也要求实现多步撤消(Undo)和重做(Redo)的功能,这往往是用户所十分期待的功能(Window自带的画笔附件程序在这点上就很欠缺)。
二. 对象设计
面向对象的程序设计方法都支持三种基本的活动:识别对象和类,描述对象和类之间的关系,以及通过描述每个类的功能定义对象的行为。
首先介绍一下对象(Object)和类(Class)的区别,类是同类对象数据凸δ艿拿枋龊褪迪郑–++中用Class关键字定义的是类),对象是类的在内存中的具体形态(用类名声明或用new操作生成的是对象变量),一般称对象为类的实例(Instance)。
对于图形对象的对象设计由于它们的较强的相关性,往往在很多面向对象编程书都提到过,故相信读者识别对象和类不会很困难。但是,要充分利用继承和多态的特性来描述对象和类之间的关系,以及通过描述每个类的功能定义还是要具体问题具体分析的。
下面还是以一副图来说明。图2是采用北航软件所的软件分析与测试工具——SafePro生成的本软件的类图局部。
由图2中可以清晰看到,我们的绘图子系统实现部分主要利用了几个从MFC可序列化的基类CObject继承的四个类:MFC已有类CArray,CObList,CDC以及我们自己需要实现的类CGraph。CDC对象封装了我们可以利用Windows系统绘图功能的设备无关的几乎全部绘图功能。CArray类和CObList 类用于实现基于CObject类的对象的数组和链表存储的辅助类。CGraph是抽象类,所有图形对象都由它继承而来。值得注意的是,由于多边形和框都是直线的组合,本软件采用了从CLine继承的方法,可以充分利用它的功能。
现在并不想把所有类的功能定义(以后会逐步介绍大部分)。下面介绍一些关系全局的类的设计。
图 2本软件的图形对象类的设计 |
在图1可以看到,本软件是基于多文档界面(MDI)的。由AppWizard选取多文档界面后,它会帮助我们生成基本的基于文档-视图结构的类。本软件使用DrawGraph为应用程序名,故有以下类:CMainFrame,CChildFrame,CDrawGraphApp,CDrawGraphDoc ,CDrawGraphView。
其中:CDrawGraphApp(以后我用是应用程序类,支持应用程序的建立和基本交互,我们可以不必改它。CChildFrame类是视图文档的容器,除了在显示图标上的定制外,我们也可以不修改它。
CMainFrame,CDrawGraphDoc ,CDrawGraphView用于分别实现主窗口、文档、视图的功能。
1). 主窗口(CMainFrame)主要需要定制图标、工具条的建立、显示和交互。下面是类的定义,阴影部分是自己定制的(非AppWizard自动生成)
class CMainFrame : public CMDIFrameWnd { DECLARE_DYNAMIC(CMainFrame)//支持动态建立 public: CMainFrame(); // Attributes public: // Operations public: // Overrides // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CMainFrame) public: virtual BOOL PreCreateWindow(CREATESTRUCT& cs); //}}AFX_VIRTUAL // Implementation public: int m_Depth;//立体深度 COLORREF m_fillcolor;//填充色 COLORREF m_pencolor;//边框色 LOGFONT m_font;//字体 int m_penstyle;//线型 UINT m_penwidth;//笔宽 void SaveToReg();//记录退出前的窗口状态 void ReadFromReg();//读取退出前的窗口状态 objecttype GetDrawType();//返回当前选中的绘图工具类别 virtual ~CMainFrame(); #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump(CDumpContext& dc) const; #endif protected: // control bar embedded members CStatusBar m_wndStatusBar;//状态栏 CReBar m_wndReBar;//标准栏和属性栏的容器工具条 CDialogBar m_wndDlgBar;//属性栏 CToolBar m_wndToolBar;//标准栏 CToolBar m_wndDrawTool;//绘图工具条 UINT objtype;//选中工具的ID号 // Generated message map functions protected: afx_msg void OnDropDown(NMHDR* pNotifyStruct,LRESULT* result); //{{AFX_MSG(CMainFrame) afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct); afx_msg void OnShowdrawtool();//显隐工具条 afx_msg void OnUpdateShowdrawtool(CCmdUI* pCmdUI); afx_msg void OnFont(); afx_msg void OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized); afx_msg void OnColor(); afx_msg void OnUpdateColor(CCmdUI* pCmdUI); afx_msg void OnFillcolor(); afx_msg void OnUpdateFillcolor(CCmdUI* pCmdUI); //}}AFX_MSG afx_msg void OnSelectTool(UINT ID);//选中工具 afx_msg void OnUpdateButtons(CCmdUI* pCmdUI);//处理按钮按下状态 afx_msg void onchangedpenwidth(); DECLARE_MESSAGE_MAP() }; |
2). 文档(CDrawGraphDoc)用于实现矢量图形对象的建立、存储和读取(即序列化)。
class CDrawGraphDoc : public CDocument { protected: // create from serialization only CDrawGraphDoc(); DECLARE_DYNCREATE(CDrawGraphDoc) // Attributes public: // Operations public: // Overrides // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CDrawGraphDoc) public: virtual BOOL OnNewDocument(); virtual void Serialize(CArchive& ar); virtual BOOL OnOpenDocument(LPCTSTR lpszPathName); virtual void DeleteContents(); //}}AFX_VIRTUAL // Implementation public: CMainFrame* GetMainFrame();//获得对主框架窗口的指针 BOOLEAN m_fillmode;//两种填充方式 void Cancel();//删除当前正在建立的绘图对象 COLORREF m_color; COLORREF m_filledcolor; UINT m_PenWidth; CGraph* NewDrawing(); CObList m_graphoblist;//绘图对象列表 CObList m_redolist;//为redo功能提供的历史记录对象列表 //以后可以添加下面的功能,把图形存储为流行的图形交互格式。 //SaveAsBitmap(); //SaveAsWMF(); //SaveAsJPEG(); //SaveAsGIF(); virtual ~CDrawGraphDoc(); #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump(CDumpContext& dc) const; #endif protected: // Generated message map functions protected: void Refresh();//用于更新视图 void InitDocument(); //{{AFX_MSG(CDrawGraphDoc) afx_msg void OnFillinborder(); afx_msg void OnUpdateFillinborder(CCmdUI* pCmdUI); afx_msg void OnFilloncolor(); afx_msg void OnUpdateFilloncolor(CCmdUI* pCmdUI); afx_msg void OnPenwidth(); afx_msg void OnEditUndo(); afx_msg void OnUpdateEditUndo(CCmdUI* pCmdUI); afx_msg void OnClear(); afx_msg void OnUpdateClear(CCmdUI* pCmdUI); afx_msg void OnEditRedo(); afx_msg void OnUpdateEditRedo(CCmdUI* pCmdUI); //}}AFX_MSG DECLARE_MESSAGE_MAP() }; |
3). 视图(CDrawGraphView)接收用户的对特定图形对象的操作并绘制图形对象。
class CDrawGraphView : public CView { protected: // create from serialization only CDrawGraphView(); DECLARE_DYNCREATE(CDrawGraphView) // Attributes public: CDrawGraphDoc* GetDocument(); // Operations public: // Overrides // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CDrawGraphView) public: virtual void OnDraw(CDC* pDC); // overridden to draw this view virtual BOOL PreCreateWindow(CREATESTRUCT& cs); protected: virtual BOOL OnPreparePrinting(CPrintInfo* pInfo); virtual void OnBeginPrinting(CDC* pDC, CPrintInfo* pInfo); virtual void OnEndPrinting(CDC* pDC, CPrintInfo* pInfo); //}}AFX_VIRTUAL // Implementation public: virtual ~CDrawGraphView(); #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump(CDumpContext& dc) const; #endif protected: // Generated message map functions protected: CPoint m_ptPrev;//前面一次点击的位置 CGraph* m_curGraph;//当前正在绘制的图形对象 state bdrawbegin;//绘制状态 //{{AFX_MSG(CDrawGraphView) afx_msg void OnLButtonDown(UINT nFlags, CPoint point); afx_msg void OnMouseMove(UINT nFlags, CPoint point); afx_msg void OnRButtonDown(UINT nFlags, CPoint point); afx_msg void OnLButtonDblClk(UINT nFlags, CPoint point); afx_msg void OnLButtonUp(UINT nFlags, CPoint point); //}}AFX_MSG DECLARE_MESSAGE_MAP() }; |
视图类在界面上改得少,主要是处理鼠标事件和调用各图形对象的绘制方法,实现上也尽量统一,充分利用图形对象的多态性。
4). 各图形对象的基类CGraph的考虑是关键,所以是需要关注的。
它定义了绘图类别和绘制状态两个枚举类型。当你把它定义好后,可以在stdafx.h加上#include “graph.h”来使得所有文件都能自由引用它,并且获得预编译。
enum state{notstart=0,startstroke,continuedrag,enddraw}; typedef enum {line ,bezier,solid,light,stroke,circle,rectangle,filledrectangle,& polyline,filledcircle,filledpolygon,ellipse,fill,text} objecttype; class CGraph : public CObject {protected: CGraph( ){}; DECLARE_DYNAMIC( CGraph ) // Attributes protected: COLORREF m_color;//所有图形对象都有颜色 public: // Operations public: virtual state SetNext(CPoint pt)=0;//再次点击,由返回值确定是否结束绘制 virtual void SetStart(CPoint pt)=0;//一次点击,产生第一点的位置 virtual void Draw( CDC* pDC )=0;//图形对象绘制自己的方法 inline void SetColor(COLORREF color){m_color=color;};//设置图形对象颜色 virtual void DrawXOR(CDC*pDC,CPoint pt)=0;//在拖动状态,图形对象绘制自己的方法 virtual void Serialize( CArchive& ar );//图形对象序列化的方法 //以后可以扩展以下功能 // virtual void IsHit(CPoint pt);//确定对象是否被点击 // virtual void Highlight();//被点击后突出显示 // virtual CRect GetBoundRect();//获得图形矩阵,可以用线索的方法局部更新视图,免除闪烁和时延等。 // virtual void Move(CPoint shift);//移动 // virtual void Rotate(int Degree);//旋转 // virtual void Scale(int scalecoef);//缩放 // virtual void Copy();//拷贝、粘贴、剪切功能 // virtual void Paste(); // virtual void Cut(); }; |
虽然是个小软件,“开发过程”还是可以和“软件工程”的步骤基本对应的。
本软件的“需求分析”是人们需要一个比Windows画笔功能强大,但十分小巧易用的小而精的基于矢量的(易于编辑)的图形工具。而且,另一方面,这个小软件的开发是一个典型的基于VC的面向对象软件开发的尝试,很有教学意义。
至此,基本完成了“概要设计”。以后将把“详细设计”和“编码”结合起来讲。至于“测试”和“维护”(改错、升级)有兴趣的读者可以自己完成。
VC编程实现数字图像的边缘检测
数字图像的边缘检测是图像分割、目标区域的识别、区域形状提取等图像分析领域十分重要的基础,图像理解和分析的第一步往往就是边缘检测,目前它以成为机器视觉研究领域最活跃的课题之一,在工程应用中占有十分重要的地位。本文向读者简单介绍一下这个技术,并给出了在Visual C++环境下实现的代码。
所谓边缘就是指图像局部亮度变化最显著的部分,它是检测图像局部变化显著变化的最基本的运算。对于数字图像,图像灰度灰度值的显著变化可以用梯度来表示,以边缘检测Sobel算子为例来讲述数字图像处理中边缘检测的实现:
对于数字图像,可以用一阶差分代替一阶微分;
△xf(x,y)=f(x,y)-f(x-1,y);
△yf(x,y)=f(x,y)-f(x,y-1)
求梯度时对于平方和运算及开方运算,可以用两个分量的绝对值之和表示,即:
G[f(x,y)]={[△xf(x,y)] +[△yf(x,y)] } |△xf(x,y)|+|△yf(x,y)|;
Sobel梯度算子是先做成加权平均,再微分,然后求梯度,即:
△xf(x,y)= f(x-1,y+1) + 2f(x,y+1) + f(x+1,y+1)- f(x-1,y-1) - 2f(x,y-1) - f(x+1,y-1);
△yf(x,y)= f(x-1,y-1) + 2f(x-1,y) + f(x-1,y+1)- f(x+1,y-1) - 2f(x+1,y) - f(x+1,y+1);
G[f(x,y)]=|△xf(x,y)|+|△yf(x,y)|;
上述各式中的像素之间的关系见图
f(x-1,y-1) | f(x,y-1) | f(x+1,y-1) |
f(x-1,y) | f(x,y) | f(x+1,y) |
f(x-1,y+1) | f(x,y+1) | f(x+1,y+1) |
我在视图类中定义了响应菜单命令的边缘检测Sobel算子实现灰度图像边缘检测的函数:
void CDibView::OnMENUSobel() //灰度图像数据的获得参见天极网9.10日发表的拙作//VC数字图像处理一文 { HANDLE data1handle; LPBITMAPINFOHEADER lpBi; CDibDoc *pDoc=GetDocument(); HDIB hdib; unsigned char *hData; unsigned char *data; hdib=pDoc->m_hDIB; BeginWaitCursor(); lpBi=(LPBITMAPINFOHEADER)GlobalLock((HGLOBAL)hdib); hData= lpbi +* (LPDWORD)lpbi + 256*sizeof(RGBQUAD); //得到指向位图像素值的指针 pDoc->SetModifiedFlag(TRUE);//设修改标志为"TRUE" data1handle=GlobalAlloc(GMEM_SHARE,WIDTHBYTES(lpBi->biWidth*8)*lpBi->biHeight); //申请存放处理后的像素值的缓冲区 data=(unsigned char*)GlobalLock((HGLOBAL)data1handle); AfxGetApp()->BeginWaitCursor(); int i,j,buf,buf1,buf2; for( j=0; j biHeight; j++)//以下循环求(x,y)位置的灰度值 for( i=0; i biWidth; i++) { if(((i-1)>=0)&&((i+1) biWidth)&&((j-1)>=0)&&((j+1) biHeight)) {//对于图像四周边界处的向素点不处理 buf1=(int)*(hData+(i+1)*WIDTHBYTES(lpBi->biWidth*8)+(j-1)) +2*(int)*(hData+(i+1)*WIDTHBYTES(lpBi->biWidth*8)+(j)) +(int)(int)*(hData+(i+1)*WIDTHBYTES(lpBi->biWidth*8)+(j+1)); buf1=buf1-(int)(int)*(hData+(i-1)*WIDTHBYTES(lpBi->biWidth*8)+(j-1)) -2*(int)(int)*(hData+(i-1)*WIDTHBYTES(lpBi->biWidth*8)+(j)) -(int)(int)*(hData+(i-1)*WIDTHBYTES(lpBi->biWidth*8)+(j+1)); //x方向加权微分 buf2=(int)(int)*(hData+(i-1)*WIDTHBYTES(lpBi->biWidth*8)+(j+1)) +2*(int)(int)*(hData+(i)*WIDTHBYTES(lpBi->biWidth*8)+(j+1)) +(int)(int)*(hData+(i+1)*WIDTHBYTES(lpBi->biWidth*8)+(j+1)); buf2=buf2-(int)(int)*(hData+(i-1)*WIDTHBYTES(lpBi->biWidth*8)+(j-1)) -2*(int)(int)*(hData+(i)*WIDTHBYTES(lpBi->biWidth*8)+(j-1)) -(int)(int)*(hData+(i+1)*WIDTHBYTES(lpBi->biWidth*8)+(j-1)); //y方向加权微分 buf=abs(buf1)+abs(buf2);//求梯度 if(buf>255) buf=255; if(buf<0){buf=0; *(data+i*WIDTHBYTES(lpBi->biWidth*8)+j)=(BYTE)buf; } else *(data+i*lpBi->biWidth+j)=(BYTE)0; } for( j=0; j biHeight; j++) for( i=0; i biWidth; i++) *(hData+i*WIDTHBYTES(lpBi->biWidth*8)+j)=*(data+i*WIDTHBYTES(lpBi->biWidth*8)+j); //处理后的数据写回原缓冲区 AfxGetApp()->EndWaitCursor(); GlobalUnlock((HGLOBAL)hdib); GlobalUnlock(data1handle); GlobalFree(date1handle); EndWaitCursor(); Invalidate(TRUE); } |
上述的数学分析读者可能看起来有些吃力,不过不要紧,对与边缘检测,大家只要知道有若干个检测模板(既边缘检测矩阵)可以直接实现检测功能就行了,现在将常用的检测实现公式列出如下:
Roberts算子:G[i,i]=|f[i,j]-f[i+1,j+1]|+|f[i+1,j]-f[i,j+1]|;
Sobe算子:G[i,i]=|f[i-1,j+1]+2f[i,j+1]+f[i+1,j+1]-f[i-1,j-1]-2f[i,j-1]-f[i+1,j-1]|
+|f[i-1,j-1]+2f[i-1,j]+f[i-1,j+1]-f[i+1,j-1]-2f[i+1,j]-f[i+1,j+1]|;
拉普拉斯算子:G[I,j]=|f[i+1,j]+f[i-1,j]+f(i,j+1)+f[i,j-1]-4f[i,j]|;
其中G[i,j]表示处理后(i,j)点的灰度值,f[i,j]表示处理前该点的灰度值。
笔者开发的该图像处理程序在Windows2000环境下编译通过,下面图2给出了依据图像处理算法得到的图像二值化、高通滤波、Sobel边缘算子的处理结果,读者需要注意的是我在进行Sobel算子进行处理后,又对它进行了二值化处理,这才得到C图。关于如何实现二值化图像,我会后续撰文对相关知识进行介绍。
谈对话框的动画弹出和动画消隐
在Windows应用程序中,对话框是应用最广泛也是比较难控制其风格(外表)的一类窗口。相信用过Windows 的朋友在享受其强大功能的同时,一定也为它所提供的具有立体感的界面而感叹吧。通常情况下,对话框的弹出和消隐都是瞬时的,下面将介绍如何实现对话框的动画弹出和消隐,增强程序的美观性。
请按以下步骤实现:
第一步:生成我们的工程(基于对话框)FlashDlg,所有的选项都取默认值,在对话框上随意添加几个控件。
第二步:在对话框的类头文件中定义如下变量,如下:
CPoint point; int nWidth,nHeight; int dx,dy; int dx1,dy1; |
第三步:在OnInitDialog()中添加如下代码:
BOOL CFlashDlgDlg::OnInitDialog() { CDialog::OnInitDialog(); CRect dlgRect; GetWindowRect(dlgRect); CRect desktopRect; GetDesktopWindow()->GetWindowRect(desktopRect); MoveWindow( (desktopRect.Width() - dlgRect.Width()) / 2, (desktopRect.Height() - dlgRect.Height()) / 2, 0, 0 ); nWidth=dlgRect.Width(); nHeight=dlgRect.Height(); dx=2; dy=4; dx1=2; dy1=2; SetTimer(1,10 , NULL); return TRUE; } |
第四步:添加OnTimer函数,添加如下代码:
void CFlashDlgDlg::OnTimer(UINT nIDEvent) { // TODO: Add your message handler code here and/or call default CRect dlgRect; GetWindowRect(dlgRect); CRect desktopRect; GetDesktopWindow()->GetWindowRect(desktopRect); if(nIDEvent == 1) { MoveWindow( (-dx+desktopRect.Width() - dlgRect.Width()) / 2, (-dy+desktopRect.Height() - dlgRect.Height()) / 2, +dx+dlgRect.Width(), +dy+dlgRect.Height() ); if(dlgRect.Width() >=nWidth) dx=0; // do not over grow if(dlgRect.Height() >=nHeight) dy=0; // do not over grow if((dlgRect.Width() >=nWidth) && (dlgRect.Height() >=nHeight)) KillTimer(1); //Stop the timer } if((dlgRect.Width() >=nWidth) && (dlgRect.Height() >=nHeight)) KillTimer(1); //Stop the timer if(nIDEvent == 2) { MoveWindow((+dx+desktopRect.Width() - dlgRect.Width()) / 2, (+dy+desktopRect.Height() - dlgRect.Height()) / 2, -dx1+dlgRect.Width(), -dy1+dlgRect.Height() ); if(dlgRect.Width() <= 0) dx1=0; // do not over grow if(dlgRect.Height() <= 0 ) dy1=0; // do not over grow if((dlgRect.Width() <= 0 ) && (dlgRect.Height() <=0)) { KillTimer(2); //Stop the timer CDialog::OnOK(); } } CDialog::OnTimer(nIDEvent); } |
好了,对话框的动画出现和消隐实现了,运行程序我们会发现对话框平滑的划出,关闭程序我们会发现对话框平滑的消失。
VC编程实现灰度图像与彩色图像的相互转换
PhotoShop的图像处理功能很强,其中有一个功能是将灰度图像转换为彩色图像,数字图像处理中,也经常要遇到灰度图像与彩色图像相互转换的问题,如何自己解决这个问题,值得大家探讨,现将我解决这类问题的方法陈述如下:
工程应用中经常要遇到需要把彩色图像到灰度图像的变换的问题,采集卡过来的图像为彩色图像,为加快处理速度,要把彩色图像转换为黑白图象,这个问题比较好解决,一般情况下彩色图像每个像素用三个字节表示,每个字节对应着R、G、B分量的亮度(红、绿、蓝),转换后的黑白图像的一个像素用一个字节表示该点的灰度值,它的值在0~255之间,数值越大,该点越白,既越亮,越小则越黑。转换公式为Gray(i,j)=0.11*R(i,j)+0.59*G(i,j)+0.3*B(i,j),其中Gray(i,j)为转换后的黑白图像在(i,j)点处的灰度值,我们可以观察该式,其中绿色所占的比重最大,所以转换时可以直接使用G值作为转换后的灰度。
至于灰度图像转换为彩色图像,技术上称为灰度图像的伪彩色处理,这是一种视觉效果明显而技术又不是很复杂的图像增强技术。灰度图像中,如果相邻像素点的灰度相差不大,但包含了丰富的信息的话,人眼则无法从图像中提取相应的信息,因为人眼分辨灰度的能力很差,一般只有几十个数量级,但是人眼对彩色信号的分辨率却很强,这样将黑白图像转换为彩色图像人眼可以提取更多的信息量。在转换过程中,经常采用的技术是灰度级-彩色变换,意思就是对黑白图像上的每一个像素点,取得该点的灰度值并送入三个通道经过实施不同的变换,产生相应的R、G、B的亮度值,即所求彩色图像对应像素点的彩色值,具体变换公式很多,我采用的是最常用的一种,变换曲线图如下:
上图中,三个图分别代表了三个变换通道,R、G、B指的是变换后对应点的R、G、B分量值,L指的是各个分量的最大值为255,G(x,y)为相应点的灰度值。理论上就这些,下面是我用VC实现的源代码,图一为我的灰度位图,图二为伪彩色处理后的结果图。我这个实现函数中是如何得到灰度位图的数据的就不多讲了,有兴趣的朋友可参考我在天极网上九月十号发表的《VC灰度位图处理》一文,那里应该讲的很清楚了。需要读者注意的是彩色图像中每个象素中的三个字节分别代表的分量,第一个字节为B,第二个为G值、最后一个为R值,这个顺序不要搞错了。代码实现如下:
void CDibView::OnMenuchange() file://图像转换实现函数 { // TODO: Add your command handler code here HANDLE data1handle; LPBITMAPINFOHEADER lpBi; BITMAPINFO *m_pBMI; CDibDoc *pDoc=GetDocument(); HDIB hdib; unsigned char *hData; unsigned char *data; hdib=pDoc->GetHDIB();//得到位图数据的句柄,其中包含图像信息头 BeginWaitCursor(); lpBi=(LPBITMAPINFOHEADER)GlobalLock((HGLOBAL)hdib); hData=(unsigned char*)FindDIBBits((LPSTR)lpBi); m_pBMI=new BITMAPINFO;//生成彩色图像的信息头 m_pBMI->bmiHeader.biBitCount=24; m_pBMI->bmiHeader.biClrImportant=0; m_pBMI->bmiHeader.biClrUsed=0; m_pBMI->bmiHeader.biCompression=BI_RGB; m_pBMI->bmiHeader.biHeight=lpBi->biHeight; m_pBMI->bmiHeader.biWidth=lpBi->biWidth; m_pBMI->bmiHeader.biPlanes=1; m_pBMI->bmiHeader.biSize=sizeof(BITMAPINFOHEADER); m_pBMI->bmiHeader.biXPelsPerMeter=0; m_pBMI->bmiHeader.biYPelsPerMeter=0; m_pBMI->bmiHeader.biSizeImage=WIDTHBYTES(lpBi->biWidth*8)*lpBi->biHeight*3; file://data=hData; int R,G,B,i,j; data1handle=GlobalAlloc(GMEM_SHARE,WIDTHBYTES(lpBi->biWidth*8)*lpBi->biHeight*3); file://生成存储彩色图象数据的缓冲区 data=(unsigned char*)GlobalLock((HGLOBAL)data1handle); for(i=0;i biHeight;i++)//实现灰度到彩色变换 for(j=0;jbiWidth*8);j++) { if(*(hData+i*WIDTHBYTES(lpBi->biWidth*8)+j)<=64) {R=0; G=(int)4*(*(hData+i*WIDTHBYTES(lpBi->biWidth*8)+j)); B=255; } if(*(hData+i*WIDTHBYTES(lpBi->biWidth*8)+j)>64 && *(hData+i*WIDTHBYTES(lpBi->biWidth*8)+j)<=128) {R=0; G=255; B=(int)4*(128-*(hData+i*WIDTHBYTES(lpBi->biWidth*8)+j)); } if(*(hData+i*WIDTHBYTES(lpBi->biWidth*8)+j)>128 && *(hData+i*WIDTHBYTES(lpBi->biWidth*8)+j)<=192) {R=(int)4*(*(hData+i*WIDTHBYTES(lpBi->biWidth*8)+j)-128); G=255; B=0; } if(*(hData+i*WIDTHBYTES(lpBi->biWidth*8)+j)>192 && *(hData+i*WIDTHBYTES(lpBi->biWidth*8)+j)<=255) {R=255; G=(int)4*(255-*(hData+i*WIDTHBYTES(lpBi->biWidth*8)+j)); B=0; } file://将生成的R、G、B分量存入目标缓冲区 *(data+i*WIDTHBYTES(lpBi->biWidth*8)*3+j*3)=B; *(data+i*WIDTHBYTES(lpBi->biWidth*8)*3+j*3+1)=G; *(data+i*WIDTHBYTES(lpBi->biWidth*8)*3+j*3+2)=R; } GlobalUnlock((HGLOBAL)hdib); GlobalUnlock(data1handle); EndWaitCursor(); CClientDC pDC(this); file://显示真彩色图像 StretchDIBits(pDC.GetSafeHdc(),0,0,lpBi->biWidth,lpBi->biHeight,0,0, lpBi->biWidth, lpBi->biHeight,data,m_pBMI,DIB_RGB_COLORS, SRCCOPY); delete m_pBMI; } |
图 一 |
图 二 |
数字图像处理技术博大精深,我真诚的希望和广大朋友探讨
Visual C++6.0开发灰度位图处理
图像处理技术已经渗透到人类生活的各个领域并得到越来越多的应用,图像处理所涉及的图像格式有很多种,如TIF、JEMP、BMP等等,工程应用中经常要处理256级的灰度BMP图像,如通过黑白采集卡采集得到的图像。BMP灰度图像作为Windows环境下主要的图像格式之一,以其格式简单,适应性强而倍受欢迎。在进行图像处理时,操作图像中的像素值就要得到图像阵列;经过处理后的图像的像素值存储起来;显示图像时要正确实现调色板,结合这些问题,文章针对性的给出了操作灰度BMP图像时的部分函数实现代码及注释。
一、 BMP位图操作
BMP位图包括位图文件头结构BITMAPFILEHEADER、位图信息头结构BITMAPINFOHEADER、位图颜色表RGBQUAD和位图像素数据四部分。处理位图时要根据文件的这些结构得到位图文件大小、位图的宽、高、实现调色板、得到位图像素值等等。对于256级灰度图像每个像素用8bit表示颜色的索引值,这里要注意的一点是在BMP位图中,位图的每行像素值要填充到一个四字节边界,即位图每行所占的存储长度为四字节的倍数,不足时将多余位用0填充。
在处理图像应用程序的文档类(CdibDoc.h)中声明如下宏及公有变量:
#define WIDTHBYTES(bits) (((bits) + 31) / 32 * 4)//计算图像每行象素所占的字节数目
HANDLE m_hDIB;//存放位图数据的句柄
CPalette* m_palDIB;//指向调色板Cpalette类的指针
CSize m_sizeDoc; file://初始化视图的尺寸
1、 读取灰度BMP位图
根据BMP位图文件的结构,操作BMP位图文件读入数据,重载了文挡类的OnOpenDocument函数如下:
BOOL CDibDoc::OnOpenDocument(LPCTSTR lpszPathName) { CFile file; CFileException fe; if (!file.Open(lpszPathName, CFile::modeRead | CFile::shareDenyWrite, &fe)) { AfxMessageBox("文件打不开"); return FALSE; }//打开文件 DeleteContents();//删除文挡 BeginWaitCursor(); BITMAPFILEHEADER bmfHeader;//定义位图文件头结构 DWORD dwBitsSize; HANDLE hDIB; LPSTR pDIB; BITMAPINFOHEADER *bmhdr;//指向位图信息头结构的指针 dwBitsSize = file.GetLength();//得到文件长度 if (file.Read((LPSTR)&bmfHeader, sizeof(bmfHeader)) != sizeof(bmfHeader)) return FALSE; if (bmfHeader.bfType != 0x4d42) file://检查是否为BMP文件 return FALSE; hDIB=(HANDLE) ::GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, dwBitsSize); file://申请缓冲区 if (hDIB == 0) { return FALSE; } pDIB = (LPSTR) ::GlobalLock((HGLOBAL)hDIB); file://得到申请的缓冲区的指针 if (file.ReadHuge(pDIB, dwBitsSize - sizeof(BITMAPFILEHEADER)) != dwBitsSize - sizeof(BITMAPFILEHEADER) ) { ::GlobalUnlock((HGLOBAL)hDIB); hDIB=NULL; return FALSE; }//读数据,包括位图信息、位图颜色表、图像像素的灰度值 bmhdr=(BITMAPINFOHEADER*)pDIB;//为指向位图信息头结构的指针付值 ::GlobalUnlock((HGLOBAL)hDIB); if ((*bmhdr).biBitCount!=8) file://验证是否为8bit位图 return FALSE; m_hDIB=hDIB; InitDIBData(); file://自定义函数,根据读入的数据得到位图的宽、高、颜色表 file:// 来得到初始化视的尺寸、生成调色板 EndWaitCursor(); SetPathName(lpszPathName);//设置存储路径 SetModifiedFlag(FALSE); // 设置文件修改标志为FALSE return TRUE; } |
2、 灰度位图数据的存储
为了将图像处理后所得到的像素值保存起来,重载了文档类的OnSaveDocument函数,其具体实现如下:
BOOL CDibDoc::OnSaveDocument(LPCTSTR lpszPathName) { CFile file; CFileException fe; BITMAPFILEHEADER bmfHdr; // 位图文件头结构 LPBITMAPINFOHEADER lpBI; file://指向位图信息结构的指针 DWORD dwDIBSize; if (!file.Open(lpszPathName, CFile::modeCreate | CFile::modeReadWrite | CFile::shareExclusive, &fe)) { AfxMessageBox("文件打不开"); }//打开文件 BOOL bSuccess = FALSE; BeginWaitCursor(); lpBI = (LPBITMAPINFOHEADER) ::GlobalLock((HGLOBAL) m_hDIB); if (lpBI == NULL) return FALSE; dwDIBSize = *(LPDWORD)lpBI + 256*sizeof(RGBQUAD); // Partial Calculation DWORD dwBmBitsSize;//BMP文件信息结构所占的字节数 dwBmBitsSize=WIDTHBYTES((lpBI->biWidth)*((DWORD)lpBI->biBitCount)) *lpBI->biHeight;// 存储时位图所有像素所占的总字节数 dwDIBSize += dwBmBitsSize; lpBI->biSizeImage = dwBmBitsSize; // 位图所有像素所占的总字节数 file://以下五句为文件头结构填充值 bmfHdr.bfType =0x4d42; // 文件为"BMP"类型 bmfHdr.bfSize = dwDIBSize + sizeof(BITMAPFILEHEADER);//文件总长度 bmfHdr.bfReserved1 = 0; bmfHdr.bfReserved2 = 0; bmfHdr.bfOffBits = (DWORD)sizeof(BITMAPFILEHEADER) + lpBI->biSize + 256*sizeof(RGBQUAD); file://位图数据距问件头的偏移量 file.Write((LPSTR)&bmfHdr, sizeof(BITMAPFILEHEADER));//写文件头 file.WriteHuge(lpBI, dwDIBSize); file://将位图信息(信息头结构、颜色表、像素数据)写入文件 ::GlobalUnlock((HGLOBAL) m_hDIB); EndWaitCursor(); SetModifiedFlag(FALSE); // back to unmodified return TRUE; } |
二、 调色板的操作
灰度图像要正确显示,必须实现逻辑调色板和系统调色板 ,通过在主框架类中处理Windows定义的消息WM_QUERYNEWPALETTE 、WM_PALETTECHANGED及视图类中处理自定义消息WM_DOREALIZE(该消息在主框架窗口定义如下:#define WM_REALIZEPAL (WM_USER+100))来实现调色板的操作。
void CMainFrame::OnPaletteChanged(CWnd* pFocusWnd) { file://总实现活动视的调色板 CMDIFrameWnd::OnPaletteChanged(pFocusWnd); CMDIChildWnd* pMDIChildWnd = MDIGetActive(); if (pMDIChildWnd == NULL) return CView* pView = pMDIChildWnd->GetActiveView(); ASSERT(pView != NULL); SendMessageToDescendants(WM_DOREALIZE, (WPARAM)pView->m_hWnd); file://通知所有子窗口系统调色板已改变 } BOOL CMainFrame::OnQueryNewPalette()//提供实现系统调色板的机会 { // 实现活动视的调色板 CMDIChildWnd* pMDIChildWnd = MDIGetActive(); if (pMDIChildWnd == NULL) return FALSE; // no active MDI child frame (no new palette) CView* pView = pMDIChildWnd->GetActiveView(); ASSERT(pView != NULL); file://通知活动视图实现系统调色板 pView->SendMessage(WM_DOREALIZE, (WPARAM)pView->m_hWnd); return TRUE; } LRESULT CDibView::OnDoRealize(WPARAM wParam, LPARAM)//实现系统调色板 { ASSERT(wParam != NULL); CDibDoc* pDoc = GetDocument(); if (pDoc->m_hDIB == NULL) return 0L; // must be a new document CPalette* pPal = pDoc->m_palDIB; file://调色板的颜色表数据在InitDIBData()函数中实现 if (pPal != NULL) { CMainFrame* pAppFrame = (CMainFrame*) AfxGetApp()->m_pMainWnd; ASSERT_KINDOF(CMainFrame, pAppFrame); CClientDC appDC(pAppFrame); CPalette* oldPalette = appDC.SelectPalette(pPal, ((HWND)wParam) != m_hWnd); file://只有活动视才可以设为"FALSE", // 即根据活动视的调色板设为"前景"调色板 if (oldPalette != NULL) { UINT nColorsChanged = appDC.RealizePalette();//实现系统调色板 if (nColorsChanged > 0) pDoc->UpdateAllViews(NULL);//更新视图 appDC.SelectPalette(oldPalette, TRUE); file://将原系统调色板置为逻辑调色板 } else { TRACE0("/tSelectPalette failed in CDibView::OnPaletteChanged/n"); } } |
注:在调用API函数显示位图时,不要忘记设置逻辑调色板,即"背景"调色板,否则位图将无法正确显示。
三、图像的数字化处理
通过以上读文件的操作,已经得到图像数据,由于得到的数据包括多余信息,所以在进行数字图像处理时要进一步删除多余信息,只对位图的像素进行操作,以基于模板的高通滤波为例来讲述数字图像处理的实现 :
void CDibView::OnMENUHighPass() { HANDLE data1handle; LPBITMAPINFOHEADER lpBi; CDibDoc *pDoc=GetDocument(); HDIB hdib; unsigned char *hData; unsigned char *data; hdib=pDoc->GetHDIB(); BeginWaitCursor(); lpBi=(LPBITMAPINFOHEADER)GlobalLock((HGLOBAL)hdib); hData=(unsigned char*)FindDIBBits((LPSTR)lpBi); pDoc->SetModifiedFlag(TRUE); data1handle=GlobalAlloc(GMEM_SHARE,WIDTHBYTES(lpBi->biWidth*8)*lpBi->biHeight); data=(unsigned char*)GlobalLock((HGLOBAL)data1handle); AfxGetApp()->BeginWaitCursor(); int i,j,s,t,ms=1; int sum=0,sumw=0; int mask[3][3]={{-1,-1,-1},{-1,9,-1},{-1,-1,-1}}; for(i=0; i biHeight; i++) for(j=0; j biWidth; j++) { sumw=0; sum=0; for(s=(-ms); s<=ms; s++) for(t=(-ms); t<=ms; t++) if(((i+s)>=0) && ((j+t)>=0) && ((i+s) biHeight) && ((j+t) biWidth)) { sumw += mask[1+s][1+t]; sum+=*(hData+(i+s)*WIDTHBYTES(lpBi->biWidth*8)+(j+t))*mask[1+s][1+t]; } if(sumw==0) sumw=1; sum/=sumw; if(sum>255)sum=255; if(sum<0)sum=0; *(data+i*WIDTHBYTES(lpBi->biWidth*8)+j)=sum; } for( j=0; j biHeight; j++) for( i=0; i biWidth; i++) *(hData+i*WIDTHBYTES(lpBi->biWidth*8)+j)=*(data+i*WIDTHBYTES(lpBi->biWidth*8)+j); AfxGetApp()->EndWaitCursor(); GlobalUnlock((HGLOBAL)hdib); GlobalUnlock(data1handle); EndWaitCursor(); Invalidate(TRUE); } |
四、图像的基本操作处理
1、图像平移
图像平移只是改变图像在屏幕上的位置,图像本身并不发生变化。假设原图像区域左上角坐标为(x0, y0),右下角坐标为(x1, y1),将图像分别沿x和y轴平移dx和dy,则新图像的左上角坐标为(x0+dx, y0+dy),右下角坐标为(x1+dx, y1+dy)。坐标平移变换公式为:
x1 = x + dx
y1 = y + dy
在屏幕上实现图像的移动分为四个步骤:
⑴ 读原图像到缓冲区;
⑵ 擦除视图上原图像;
⑶ 计算平移后的新坐标。
⑷ 利用API函数::StretchDIBits()在新的左上角坐标位置处重新显示原图像。
其中,擦除原图像的方法与图形变换中擦除原图形的方法一致,在实现中仍采用XOR异或方式画图擦除原图像。对于新坐标值的计算还需要考虑边界情况,不要在图像平移后超出允许的屏幕范围。
2、图像颠倒
图像颠倒是指把定义好的图像区域上下翻转地显示在屏幕上。分析图像颠倒的过程,可发现每行的图像信息都保持不变,而只是改变了行的顺序,将第一行与最后的第n行相互交换,第二行与第n - 1行交换……,依此类推,从而实现了图像的颠倒。只需采用按行交换的方式,即可方便地修改缓冲区内容,实现图像的颠倒。基本步骤如下:
(1)将原图像读入缓冲区,并擦除原图像;
(2) 计算图像的高度,即行数height;计算图像宽度width;根据宽度、高度生成新缓冲区;
(3)把第一行与最末行交换,第2行与第n-1行交换……,依此类推,直至全部交换完毕。既原图中的(x、y)点,在新生成的图象中对应为x1=x,y1=height-1-y。把原图中的象素值读入新缓冲区的(x1,y1)点处。
(4)把交换后的图像缓冲区内容重新显示在屏幕上。
3、图像镜像变换
镜像变换是指将指定区域的图像左右翻转地显示在屏幕。分析镜像变换过程可以发现:每行图像信息的处理方式是相同的,而且行顺序不发生变化,只是每一行的像素信息按从左到右的顺序进行了左右颠倒,从而实现了镜像变换。因此,采用按行逐点变换的方式实现图像的镜像。
给出原图中的任意点(x, y)镜像变换后的新坐标(x1, y1)的坐标变换公式:
x1 = width-x-1
y1 = y
根据以上公式,对各个像素点计算新坐标后,把原图中的象素值读入新缓冲区的(x1,y1)点处。
4、图像任意角度的旋转
图像旋转是指把定义的图像绕某一点以逆时针或顺时针方向旋转一定的角度,通常是指绕图像的中心以逆时针方向旋转。
首先根据旋转的角度、图象对角线的长度计算旋转后的图像的最大宽度、高度,根据旋转后图象最大的宽度、高度生成新的缓冲区,假设图像的左上角为(left, top),右下角为(right, bottom),则图像上任意点(x, y)绕其中心(xcenter, ycenter)逆时针旋转angle角度后,新的坐标位置(x1, y1)的计算公式为:
xcenter = (width+1)/2+left;
ycenter = (height+1)/2+top;
x1 = (x-xcenter) cosθ- (y - ycenter) sinθ+xcenter;
y1 = (x-xcenter) sinθ+ (y- ycenter) cosθ+ ycenter;
与图像的镜像变换相类似,把原图中的象素值读入新缓冲区的(x1,y1)点处。注意在新缓冲区中与原图没有对应的象素点的值用白色代替。
五、小结
笔者开发的该图像处理程序在Windows98环境下编译通过,本文主要讲述了8bit灰度图像的处理,读者可以本文的基础上开发自己的针对二值、真彩色格式的图像处理系统。
用VC++实现图像检索技术
一. 理论和方法介绍
a) 采用颜色检索方法的目的:
对多媒体数据的检索,早期的方法是用文本将多媒体数据进行标识,这显然不是基于多媒体信息本身内容的检索,对多媒体数据中包含的信息是一中及大的浪费;
基于内容的检索是多媒体数据库的关键技术,如何实现这块技术,是值得商榷的,而最好的方法是使用无需领域知识的检索方法,因此,基于颜色的方法就是实现的关键;
本文介绍了颜色直方图和颜色对方法在基于内容检索时的实现思路和理论;
其实颜色直方图简单来说,就是统计图像中具有某一特定颜色的象素点数目而形成的各颜色的直方图表示,不同的直方图代表不同图片的特征。
b) 利用颜色直方图进行检索:
该方法也可以应用于视频数据库的查询中,有以下三种方式:
(1)指明颜色组成--该法需要用户对图像中的颜色非常敏感,而且使用起来也不方便,检索的查准率和查全率并不高,因此文章中并未介绍该法的实现思路
(2)指明一幅示例图像--通过与用户确定的图像的颜色直方图的相似性匹配得到查询结果,这是文章介绍的两种方法的根本
(3)指明图像中一个子图--分割图像为各个小块,然后利用选择小块来确定图像中感兴趣的对象的轮廓,通过建立更复杂的颜色关系(如颜色对方法)来查询图像,该方法是文章的重心所在
c) 颜色直方图实现思路的介绍:
两图片是否相似可以采用欧氏距离来描述:
Ed=(G,S)= (Ed越小相似度就越大)
检索后,全图直方图的相似度的定量度量可以用如下公式表示:
Sim(G,S)=
(N为颜色级数,Sim越靠近1两幅图片越相似)
可以对上面2中的公式加改进对某些相对重要的颜色乘上一个权重,就可以做寻找某一前景或组合的查询。
全图的颜色直方图算法过于简单,因此带来很多问题,如:可能会有两幅根本不同的图像具有完全一样的颜色直方图,不反映颜色位置信息,这样导致查准率和查全率都不高,因此问文章提出了一个改进,即将图像进行了分割,形成若干子块,这样就提供了一定程度的位置信息,而且可以对含用户感兴趣的子块加大权重,提高检索的查询智能性和查准查全率,相应的公式有,子块Gij与Sij的相似性度量为:
(P为所选颜色空间的样点数)
再引入子块权重Wij,选取L个最大的Sim值作Simk(Gk,Sk),就有:
(Wk 的选取应根据图像的特点决定,可以使图像中间或用户指定的区域权重大,以反映图像的位置信息)
d) 颜色对实现思路介绍:
主要目的:借助图像中相邻子块之间的颜色直方图的配对建模,实现对图像中的具体对象的查询,支持对象的移位、旋转和部分变形;
颜色对方法特别适合于对边界明显的对象的查询;
实现思路:计算用户输入图像的子块直方图片à用户选定包含查询对象的子块à计算这些子块与周围相邻的子块的颜色对表à将这些颜色对中差值小于某一域值的颜色对删除以消除颜色噪声à选取颜色对表中数值最大的几个颜色对做为图片的代表特征à搜索目标图像的每一子块的颜色对表寻找与这写代表颜色对的匹配à统计单一匹配次数à若有某一比例以上的颜色对匹配到,图像即被检索到。
相似性度量:
(N为所用查询颜色对数目)
qj、gj:颜色对j在查询图像Q和目标图像G中出现的次数
查询时颜色对的匹配应该是不精确的,应该允许的误差为2%以内
二. 具体程序实现
a) 基于子块颜色直方图方法的程序实现:
将图片分成4×4格局,按从左到右、从上到下的顺序,分别计算各子块的颜色直方图,因此需要设定一个三维数组,前两维为子块的坐标,最后一维为颜色级,但现在采样得到的象素点的颜色值是RGB形式的,因此,需要将RGB形式转换为可以用比较合理的有限数表示的颜色级,而人眼对亮度是最为敏感的,因此可以将RGB转换为亮度值Y,公式为:
Y=R×0.299+G×0.587+B×0.114
这样就确定的一个256级的颜色级别,而统计颜色直方图的三维数组就可以定义为:int Color[4][4][256],当采样到某一颜色级时候,将相应的位置加一即可。
根据以上的子块间的相似公式: ,知道某一颜色级对应的数有可能是分母,当两个颜色级的数都为0的时候,显然是不能统计的,因此需要一个数组记录实际统计过的颜色级数,也需要一个数组记录4×4子块的两幅图像的各子块的相似度。
对于用户选定的块其实是代表查询对象的,因此应该加大权重,相对来说就是减小其他块的权重,然后可以将乘过对应权重的块的相似度相加,得到最终的相似度,然后将所有目标图像与用户输入的图像的相似度从大到小排序,选出值最大的几张作为最后的查询结果显示出来返回。
以上是具体实现设想,程序实现如下:
//基于颜色直方图的方法 |
b) 基于颜色对的方法的程序实现
该方法也需要分成4×4子块,计算颜色直方图,具体计算颜色直方图的方法上面已经有过详细的解释。
该方法主要在于对颜色对表示结构的实现,颜色对是某一图片的代表特征,因此在程序中必须有定量表示,现在采取用两个子块颜色直方图的欧氏距离表示,因此计算某一子块的颜色对表就是按八方向计算其与周围的子块之间的欧氏距离,将结果存放于一个double o_dis[8]的数组中,然后将这个数组从大到小排序,排序完成后再将数组中相互之间值的差小于某一域值(取8个颜色对的平均值的2%)的颜色对祛除(按序两两比较再移动数组里的变量实现),最后将结果先填入图像的特征颜色对表(有4×8=32个变量,是一个结构数组,结构记录用户选定子块的坐标和与其相对应的被选中的颜色对值)。
最后,对4个用户选定的子块依次计算完毕,就可以调用SortColorPair()函数,对特征颜色对表做出处理(先从大到小排序,然后祛除差值小于总平均值的2%的特征颜色对)。
在比较的时候,按顺序计算出目标图像的子块颜色对表,和以上的特征颜色对表匹配,如果匹配到,则标记该颜色对(设定另一标记0数组),并且将匹配数变量加一,如果最后匹配到的数目是60%以上,就算目标图像被搜索到。
具体程序实现如下:
//计算子块(x,y)的颜色对表,采取"八方向邻接技术" //计算该图像的最终确定的特征颜色对表 |
以上三个函数实现对某一图像内部的具体计算,而对于基于颜色对方法的外部计算如下:
//计算用户确定的4块位置与其周围位置的颜色对(颜色对现采取用相邻两块的直方图的欧氏距离表示) int pairflag[32]; file://颜色对匹配标志数组,即某一颜色对如果在目标图像中找到,下一次就不能再匹配 |
通过以上的程序,我们就实现了真正的图像内容检索,简单的程序就实现了现代计算机科学在多媒体研究前沿的任务。
Visual C++实现数字图像增强处理
前言
对于一个图像处理系统来说,可以将流程分为三个阶段,在获取原始图像后,首先是图像预处理阶段、第二是特征抽取阶段、第三是识别分析阶段。图像预处理阶段尤为重要,如果这阶段处理不好,后面的工作根本无法展开。
在实际应用中,我们的系统获取的原始图像不是完美的,例如对于系统获取的原始图像,由于噪声、光照等原因,图像的质量不高,所以需要进行预处理,以有利于提取我们感兴趣的信息。图像的预处理包括图像增强、平滑滤波、锐化等内容。图像的预处理既可以在空间域实现,也可以在频域内实现,我们主要介绍在空间域内对图像进行点运算,它是一种既简单又重要的图像处理技术,它能让用户改变图像上像素点的灰度值,这样通过点运算处理将产生一幅新图像。下面我们开始介绍与图像点运算的相关知识。
一、图像的直方图
图像直方图是图像处理中一种十分重要的图像分析工具,它描述了一幅图像的灰度级内容,任何一幅图像的直方图都包含了丰富的信息,它主要用在图象分割,图像灰度变换等处理过程中。从数学上来说图像直方图是图像各灰度值统计特性与图像灰度值的函数,它统计一幅图像中各个灰度级出现的次数或概率;从图形上来说,它是一个二维图,横坐标表示图像中各个像素点的灰度级,纵坐标为各个灰度级上图像各个像素点出现的次数或概率。如果不特别说明,本讲座中的直方图的纵坐标都对应着该灰度级在图像中出现的概率。我们的例子是在一个对话框中显示一个图像的直方图,为实现该目的,我们定义了一个名为"ZFT"的对话框类用来显示图像的直方图,具体实现代码和效果图如下(关于代码实现部分可以参考笔者2001年在天极网上发表的一篇VC实现数字图像处理的文章):
//直方图对话框构造函数; ZFT::ZFT(CWnd* pParent /*=NULL*/) : CDialog(ZFT::IDD, pParent)//ZFT为定义的用来显示直方图的对话框类; { Width=Height=0;//对话框初始化阶段设置图像的宽和高为"0"; } 对话框重画函数; void ZFT::OnPaint() { CRect rect;//矩形区域对象; CWnd *pWnd;//得到图片框的窗口指针; pWnd=GetDlgItem(IDC_Graphic);//得到ZFT对话框内的"Frame"控件的指针; file://(IDC_Graphic为放置在对话框上的一个"Picture"控件,并讲类型设置为"Frame")。 pWnd->GetClientRect(&rect);//得到"Frame"控件窗口的"视"区域; int i; CPaintDC dc(pWnd);//得到"Frame"控件的设备上下文; file://画直方图的x、y轴; dc.MoveTo(0,rect.Height()); dc.LineTo(rect.Width(),rect.Height()); dc.MoveTo(0,rect.Height()); dc.LineTo(0,0); file://画直方图,num[]是"ZFT"的内部数组变量,存放的是图像各个灰度级出现的概率;该数组的各个分量在 显示具体图像的直方图时设置; for(i=0;i<256;i++)//根据图像上的各个灰度级出现的概率,在坐标上对应的画出一根直线,从而各个表示各灰度级出现概率的直线构成了图像的直方图; { dc.MoveTo(i+1,rect.Height()); dc.LineTo (i+1,(rect.Height()-rect.Height()*num[i]*30)); file://此处num分量乘以"30"是为了放大个灰度级上对应的出现概率直线,增强显示效果; } } void ZFT::OnMouseMove(UINT nFlags, CPoint point) {//OnMouseMove函数处理鼠标消息,显示当前鼠标所在直方图上的灰度值等信息; CWnd *pWnd,*pWndText;//定义两个窗口对象; CPoint point1;//定义个一个点对象; point1=point;//存放当前鼠标的位置信息; CRect rect;//矩形对象; CString string ;//字符串对象; pWnd=GetDlgItem(IDC_Graphic);//得到显示直方图的框架窗口对象指针; pWndText=GetDlgItem(IDC_NUM);//得到指向文本框对象(IDC_NUM)窗口的指针; pWnd->GetWindowRect(&rect);//获取pWnd窗口对象窗口区域位置; file://屏幕坐标转换为客户区坐标; ScreenToClient(&rect); file://判断当前鼠标是否指在直方图内; if(rect.PtInRect (point)) { int x=point1.x-rect.left; file://当前鼠标位置减去区域的起始位置恰好为当前鼠标所指位置所表示的灰度级; string.Format("%d",x); file://显示当前位置对应的图像的灰度级; pWndText->SetWindowText((LPCTSTR)string); } CDialog::OnMouseMove(nFlags, point); } void CDibView::OnImagehorgm() file://在程序的"视"类对象内处理显示图像直方图的函数; { CDibDoc *pDoc=GetDocument(); HDIB hdib; hdib=pDoc->GetHDIB(); BITMAPINFOHEADER *lpDIBHdr;//位图信息头结构指针; BYTE *lpDIBBits;//指向位图像素灰度值的指针; lpDIBHdr=( BITMAPINFOHEADER *)GlobalLock(hdib);//得到图像的位图头信息 lpDIBBits=(BYTE*)lpDIBHdr+sizeof(BITMAPINFOHEADER)+256*sizeof(RGBQUAD); file://获取图像像素值 ZFT dialog;//直方图对话框模板对象; int i,j; int wImgWidth=lpDIBHdr->biWidth; int wImgHeight=lpDIBHdr->biHeight; file://a[]数组用来存放各个灰度级出现的概率; float a[256]; for(i=0;i<256;i++)//初始化数组; { a[i]=0; } file://统计各个灰度级出现的次数; for(i=0;i { for(j=0;j { a[*(lpDIBBits+WIDTHBYTES(wImgWidth*8)*i+j)]++; } file://统计各个灰度级出现的概率; for(i=0;i<256;i++) { a[i]=a[i]/(wImgHeight*wImgWidth);//得到每个灰度级的出现概率; memcpy(dialog.num,a,256*sizeof(float)); } } dialog.DoModal();//显示直方图对话框; } return; } |
(a)LENA图像 | (b)直方图 |
上图为LENA的原始图像和其对应的直方图,在(b)图中的135表示当前鼠标在直方图中所指的位置对应的灰度级为135。从该直方图可以看出,LENA图像的灰度主要分布在中高灰度级上,在低灰度级上图像的像素数几乎为零。
二、图像增强
影响系统图像清晰程度的因素很多,例如室外光照度不够均匀就会造成图像灰度过于集中;由CCD(摄像头)获得的图像经过A/D(数/模转换,该功能在图像系统中由数字采集卡来实现)转换、线路传送都会产生噪声污染等等。因此图像质量不可避免的降低了,轻者表现为图像不干净,难于看清细节;重者表现为图像模糊不清,连概貌也看不出来。因此,在对图像进行分析之前,必须要对图像质量进行改善,一般情况下改善的方法有两类:图像增强和图像复原。图像增强不考虑图像质量下降的原因,只将图像中感兴趣的特征有选择的突出,而衰减不需要的特征,它的目的主要是提高图像的可懂度。图像增强的方法分为空域法和频域法两类,空域法主要是对图像中的各个像素点进行操作;而频域法是在图像的某个变换域内,对图像进行操作,修改变换后的系数,例如付立叶变换、DCT变换等的系数,然后再进行反变换得到处理后的图像。图像复原技术与增强技术不同,它需要了解图像质量下降的原因,首先要建立"降质模型",再利用该模型,恢复原始图像。本期讲座我们主要介绍各种增强技术在图象处理系统中的实际应用。
1.灰度变换
简单的说,灰度变换就是指对图像上各个像素点的灰度值x按某个函数T()变换到y。例如为了提高图像的清晰度,需要将图像的灰度级整个范围或其中某一段(A,B)扩展或压缩到(A ,B );需要显示出图像的细节部分等都要求采用灰度变换方法。灰度变换有时又被称为图像的对比度增强或对比度拉伸。假定输入图像中的一个像素的灰度级为Z,经过T(Z)函数变换后输出图像对应的灰度级为Z ,其中要求Z和Z 都要在图像的灰度范围之内。根据T()形式,可以将灰度变换分为线性变换和非线性变换。具体应用中采用何种T(),需要根据变换的要求而定。
对于图像的灰度变换,我们这里介绍一种稍微复杂一点的方法,既直方图均衡化。直方图均衡化是灰度变换的一个重要应用,广泛应用在图像增强处理中,它是以累计分布函数变换为基础的直方图修正法,可以产生一幅灰度级分布具有均匀概率密度的图像,扩展了像素的取值动态范围。若像素点的原灰度为R,变换后的灰度为S,需要注意的是R、S是归一化后的灰度值,其灰度变换函数T()为:
S=T (R); | k=0,1…, ; |
式中, 是第j级灰度值的概率, 是图像中j级灰度的像素总数, 是图像中灰度级的总数目, 是图象中像素的总数。对变换后的S值取最靠近的一个灰度级的值,建立灰度级变换表,将原图像变换为直方图均衡的图像。下面是实现图像直方图均衡化函数的源代码和效果图:
void CDibView::OnZftJh() { CClientDC pDC(this); HDC hDC=pDC.GetSafeHdc();//获取当前设备上下文的句柄; SetStretchBltMode(hDC,COLORONCOLOR); CDibDoc *pDoc=GetDocument(); HDIB hdib; hdib=pDoc->GetHDIB(); BITMAPINFOHEADER *lpDIBHdr;//位图信息头结构指针; BYTE *lpDIBBits;//指向位图像素灰度值的指针; lpDIBHdr=( BITMAPINFOHEADER *)GlobalLock(hdib);//得到图像的位图头信息 lpDIBBits=(BYTE*)lpDIBHdr+sizeof(BITMAPINFOHEADER)+256*sizeof(RGBQUAD); file://获取图像像素值 float p[256],p1[256],num[256]; int i,j,k; for(i=0;i<256;i++)//清空三个数组; { num[i]=0.0f; p[i]=0.0f; p1[i]=0.0f; } file://num[]存放图象各个灰度级出现的次数; int Height=lpDIBHdr->biHeight; int Width=lpDIBHdr->biWidth; for(i=0;i for(j=30;j { num[*(lpDIBBits+WIDTHBYTES(Width*8)*i+j)]++; } file://p[]存放图像各个灰度级的出现概率; for(i=0;i<256;i++) { p[i]=num[i]/(Width*Height); } file://p1[]存放各个灰度级之前的概率和,用于直方图变换; for(i=0;i<256;i++) { for(k=0;k<=i;k++) p1[i]+=p[k]; } file://直方图变换; for(i=0;i for(j=30;j { *(lpDIBBits+WIDTHBYTES(Width*8)*i+j)=(BYTE)(p1[*(lpDIBBits+WIDTHBYTES(Width*8)*i+j)]*255+0.5); } StretchDIBits (hDC,0,0,lpDIBHdr->biWidth,lpDIBHdr->biHeight,0,0, lpDIBHdr->biWidth,lpDIBHdr->biHeight, lpDIBBits,(LPBITMAPINFO)lpDIBHdr, DIB_RGB_COLORS, SRCCOPY);//显示图像; } |
(a)LENA原图 | (b)直方图均衡化后的效果图 |
(c)原始图象的直方图 | (d)均衡化后的直方他图 |
图 二
从上述效果图可以看出,经过直方图均衡化处理后,图像变的清晰了,从直方图来看,处理后的LENA的图像直方图分布更均匀了,在每个灰度级上图像都有像素点。但是直方图均衡化存在着两个缺点:
1)变换后图像的灰度级减少,某些细节消失;
2)某些图像,如直方图有高峰,经处理后对比度不自然的过分增强。
为此M.Kamel和Lian Guan等人从图像相邻像素一般高度相关这一事实出发,将灰度概率分布和空间相关性联系在一起,提出了用二维条件概率密度函数取代一维概率密度函数作为均衡化条件,很好的解决了这个问题,有兴趣的朋友可以参阅一些图像处理书籍和资料。
2.图像平滑
图像平滑主要是为了消除噪声。噪声并不限于人眼所能看的见的失真和变形,有些噪声只有在进行图像处理时才可以发现。图像的常见噪声主要有加性噪声、乘性噪声和量化噪声等。图像中的噪声往往和信号交织在一起,尤其是乘性噪声,如果平滑不当,就会使图像本身的细节如边界轮廓、线条等变的模糊不清,如何既平滑掉噪声有尽量保持图像细节,是图像平滑主要研究的任务。
一般来说,图像的能量主要集中在其低频部分,噪声所在的频段主要在高频段,同时系统中所要提取的汽车边缘信息也主要集中在其高频部分,因此,如何去掉高频干扰又同时保持边缘信息,是我们研究的内容。为了去除噪声,有必要对图像进行平滑,可以采用低通滤波的方法去除高频干扰。图像平滑包括空域法和频域法两大类,在空域法中,图像平滑的常用方法是采用均值滤波或中值滤波,对于均值滤波,它是用一个有奇数点的滑动窗口在图像上滑动,将窗口中心点对应的图像像素点的灰度值用窗口内的各个点的灰度值的平均值代替,如果滑动窗口规定了在取均值过程中窗口各个像素点所占的权重,也就是各个像素点的系数,这时候就称为加权均值滤波;对于中值滤波,对应的像素点的灰度值用窗口内的中间值代替。实现均值或中值滤波时,为了简便编程工作,可以定义一个n*n的模板数组。另外,读者需要注意一点,在用窗口扫描图像过程中,对于图像的四个边缘的像素点,可以不处理;也可以用灰度值为"0"的像素点扩展图像的边缘。下面给出了采用加权均值滤波的图像平滑函数代码和效果图:
void CDibView::OnImagePh() { CClientDC pDC(this); HDC hDC=pDC.GetSafeHdc();//获取当前设备上下文的句柄; SetStretchBltMode(hDC,COLORONCOLOR); HANDLE data1handle; LPBITMAPINFOHEADER lpBi; CDibDoc *pDoc=GetDocument(); HDIB hdib; unsigned char *hData; unsigned char *data; hdib=pDoc->GetHDIB(); BeginWaitCursor(); lpBi=(LPBITMAPINFOHEADER)GlobalLock((HGLOBAL)hdib); hData=(unsigned char*)FindDIBBits((LPSTR)lpBi); pDoc->SetModifiedFlag(TRUE); data1handle=GlobalAlloc(GMEM_SHARE,WIDTHBYTES(lpBi->biWidth*8)*lpBi->biHeight); data=(unsigned char*)GlobalLock((HGLOBAL)data1handle); AfxGetApp()->BeginWaitCursor(); int i,j,s,t,ms=1; int sum=0,sumw=0; int mask[3][3]={{1,1,1},{1,2,1},{1,1,1}};//定义的3x3加权平滑模板; for(i=0; i biHeight; i++) for(j=0; j biWidth; j++) { sumw=0; sum=0; for(s=(-ms); s<=ms; s++) for(t=(-ms); t<=ms; t++) if(((i+s)>=0)&&((j+t)>=0)&&((i+s) biHeight)&&((j+t) biWidth)) { sumw += mask[1+s][1+t]; sum+=*(hData+(i+s)*WIDTHBYTES(lpBi->biWidth*8)+(j+t))*mask[1+s][1+t]; } if(sumw==0) sumw=1; sum/=sumw; if(sum>255)sum=255; if(sum<0)sum=0; *(data+i*WIDTHBYTES(lpBi->biWidth*8)+j)=sum; } for( j=0; j biHeight; j++) for(i=0;i biWidth;i++) *(hData+i*WIDTHBYTES(lpBi->biWidth*8)+j)=*(data+i*WIDTHBYTES(lpBi->biWidth*8)+j); StretchDIBits (hDC,0,0,lpBi->biWidth,lpBi->biHeight,0,0, lpBi->biWidth,lpBi->biHeight, hData,(LPBITMAPINFO)lpBi, DIB_RGB_COLORS, SRCCOPY);//显示图像; } |
(a)LENA原图 | (b)平滑后的效果图 |
图三
中值或均值平滑有时处理图像的效果并不是很好,它虽然去除了一定的噪声,但同时使图像中的边缘变的模糊,这主要和所选取的窗口大小有关,为此下面介绍了一种既能保持边缘清晰又能消除噪声的方法,其算法如图四所示:
(a) | (b) | (c) |
图 四 图像平滑模板
上图的含义是在图像中取5*5的区域,包含点(i,j)的五边形和六边形各四个,3*3的区域一个,计算这九个区域的标准差和灰度的平均值,取标准差最小区域的灰度平均值作为点(i,j)的灰度。由于该算法的实现代码和上述代码大同小异,所以代码部分就不再赘述。
3.图像锐化
图像平滑往往使图像中的边界、轮廓变的模糊,为了减少这类不利效果的影响,这就需要利用图像鋭化技术,使图像的边缘变的清晰。图像銳化处理的目的是为了使图像的边缘、轮廓线以及图像的细节变的清晰,经过平滑的图像变得模糊的根本原因是因为图像受到了平均或积分运算,因此可以对其进行逆运算(如微分运算)就可以使图像变的清晰。从频率域来考虑,图像模糊的实质是因为其高频分量被衰减,因此可以用高通滤波器来使图像清晰。
为了要把图像中间任何方向伸展的的边缘和轮廓线变得清晰,我们希望对图像的某种运算是各向同性的。可以证明偏导平方和的运算是各向同性的,既:
式中( )是图像旋转前的坐标,( )是图像旋转后的坐标。梯度运算就是在这个式子的基础上开方得到的。图像(x,y)点的梯度值:
为了突出物体的边缘,常常采用梯度值的改进算法,将图像各个点的梯度值与某一阈值作比较,如果大于阈值,该像素点的灰度用梯度值表示,否则用一个固定的灰度值表示。
我们在对图像增强的过程中,采用的是一种简单的高频滤波增强方法:
式中f,g分别为锐化前后的图像, 是与扩散效应有关的系数。 表示对图像f进行二次微分的拉普拉斯算子。这表明不模糊的图像可以由模糊的图像减去乘上系数的模糊图像拉普拉斯算子来得到。 可以用下面的模板H={{1,4,1},{4,-20,4},{1,4,1}}来近似。在具体实现时,上述模板H中的各个系数可以改变, 这个系数的选择也很重要,太大了会使图像的轮廓过冲,太小了则图像锐化不明显。实验表明, 选取2-8之间往往可以达到比较满意的效果。下面给出 等于4的情况下的实现代码和效果图:
void CDibView::OnMenuitem32785() { CClientDC pDC(this); HDC hDC=pDC.GetSafeHdc();//获取当前设备上下文的句柄; SetStretchBltMode(hDC,COLORONCOLOR); CDibDoc *pDoc=GetDocument(); HDIB hdib; hdib=pDoc->GetHDIB(); BITMAPINFOHEADER *lpDIBHdr;//位图信息头结构指针; BYTE *lpDIBBits;//指向位图像素灰度值的指针; lpDIBHdr=( BITMAPINFOHEADER *)GlobalLock(hdib);//得到图像的位图头信息 lpDIBBits=(BYTE*)lpDIBHdr+sizeof(BITMAPINFOHEADER)+256*sizeof(RGBQUAD);//获取图像像素值 BYTE* pData1; static int a[3][3]={{1,4,1},{4,-20,4},{1,4,1}};//拉普拉斯算子模板; int m,n,i,j,sum; int Width=lpDIBHdr->biWidth; int Height=lpDIBHdr->biHeight; pData1=(BYTE*)new char[WIDTHBYTES(Width*8)*Height]; file://进行拉普拉斯滤波运算; for(i=1;i for(j=1;j { sum=0; for(m=-1;m<2;m++) for(n=-1;n<2;n++) sum+=*(lpDIBBits+WIDTHBYTES(Width*8)*(i+m)+j+n)*a[1+m][1+n]; if(sum<0) sum=0; if(sum>255) sum=255; *(pData1+WIDTHBYTES(Width*8)*i+j)=sum; } file://原始图像pData减去拉普拉斯滤波处理后的图像pData1 for(i=0;i for(j=0;j { sum=(int)(*(lpDIBBits+WIDTHBYTES(Width*8)*i+j)-4*(*(pData1+WIDTHBYTES(Width*8)*i+j))); if(sum<0) sum=0; if(sum>255) sum=255; *(lpDIBBits+WIDTHBYTES(Width*8)*i+j)=sum; } StretchDIBits (hDC,0,0,lpDIBHdr->biWidth,lpDIBHdr->biHeight,0,0, lpDIBHdr->biWidth,lpDIBHdr->biHeight, lpDIBBits,(LPBITMAPINFO)lpDIBHdr, DIB_RGB_COLORS, SRCCOPY); } |
(a)LENA原图 | (b)拉普拉斯锐化图 |
图 五
本文主要讲解了图像直方图的基本概念和图像点处理运算中的增强、平滑、锐化概念和实现算法,并给处理实现代码和处理效果图和广大读者朋友们交流,希望达到抛砖引玉的作用。
VC++实现对退化图像的恢复
摘要:本文以VC++ 6.0为编程工具,讲述了采取逆滤波和维纳滤波两种图像恢复算法对退化图像的恢复实现过程。
引言
图像恢复技术是图像处理领域一类重要的处理技术,与图像增强等其他基本图像处理技术类似,该技术也是以获取视觉质量得到某种程度改善为目的的,所不同的是图像恢复过程需要根据指定的图像退化模型来完成,根据这个退化模型对在某种情况下退化或恶化了的退化图像进行恢复,以获取到原始的、未经过退化的原始图像。换句话说,图像恢复的处理过程实际是对退化图像品质的提升,并通过图像品质的提升来达到图像在视觉上的改善。本文以VC++作为开发工具,讲述了对退化图像进行逆滤波和维纳滤波处理算法。
逆滤波处理
对图像进行恢复处理通常需要根据一定的图像退化模型来进行,一个简单的通用图像退化模型可将图像的退化过程模型化为一个作用在原始图像f(x,y)上的退化系统H,作用结果与一个加性噪声n(x,y)的联合作用导致产生出了退化图像g(x,y),表现为数学形式为g(x,y)=H[f(x,y)]+n(x,y)。根据上述退化系统H可以从给定的退化图像g(x,y)得到原始图像f(x,y)的一个近似结果。逆滤波处理就是其中一种无约束恢复的图像恢复技术,其恢复过程的数学形式可表示为F (u,v)=G(u,v)/H(u,v) (u,v=0,1,…,M-1),其中F(u,v)和G(u,v)分别为图像f(x,y)和g(x,y)的频域变换,H(u,v)可看作是一个滤波函数。由于图像在退化过程中存在噪声的干扰,因此通常情况下的滤波器往往不是正好的1/H(u,v),而是关于u和v的某个非线形的恢复转移函数M(u,v)。经过以上的分析,图像的退化和恢复过程(模型)大致可用下图来表示:
一种简便的恢复方法是在选取恢复转移函数M(u,v) 时,如果u2+v2≤w2,则取值1/H(u,v),否则为1。这样处理虽然简单,但是恢复后的图像往往存在较明显的振铃现象,通常为了消除振铃现象,以H(u,v)的值作为判据,如不大于d(0
由于恢复过程需要在频域进行,因此需要通过二维傅立叶变换将图像由空域变换到频域。二维的傅立叶变换较一维傅立叶变换要复杂的多,一般采取连续2次运用一维离散快速傅立叶变换的方法来实现,即先沿f(x,y)的每一个x对y求变换再乘以N得到F(x,v),完成第一步变换。然后再将得到的F(x,v)沿f(x,v)的每一个v对x求变换即可得到f(x,y)的最终变换F(u,v),这两步的数学表达式如下:
F(x,v)=N*[(1/N)* f(x,y)exp[-j2πvy/N]] (v=0,1,……,N-1) F(u,v)=(1/N)* F(x,v)exp[-j2πux/N] (u,v=0,1,……,N-1) |
类似也可以得出二维离散傅立叶变换逆变换用一维变换计算的表达式:
F(x,v)= F(u,v)exp[j2πux/N] (x,y=0,1,……,N-1) f(x,y)=(1/N)* F(x,v)exp[j2πvy/N]] (y=0,1,……,N-1) |
在分布进行一维傅立叶变换时,多采用"蝴蝶图"的快速算法(详见信号处理方面资料),其核心算法如下:
int N=(int)pow(2,M); file://N:序列长度(2的整数次幂) ReverseOrder(A,N); file://对空间序列进行倒序 for(int i=1;i<=M;i++){ int b=(int)pow(2,(i-1)); for(int j=0;j<=(b-1);j++) { float p=(float)(pow(2,(M-i))*j*2.0*PI/(float)N); for(int k=j;k<=(N-1);){ float tr=(float)(A[k+b].Re*cos(p)+A[k+b].Im*sin(p)); file://计算复数运算A*U float ti=(float)(A[k+b].Im*cos(p)-A[k+b].Re*sin(p)); A[k+b].Re=A[k].Re-tr; file://复数运算A-tr A[k+b].Im=A[k].Im-ti; A[k].Re+=tr; file://复数运算A+tr A[k].Im+=ti; k+=b*2; } } } |
傅立叶逆变换的同傅立叶变换比较相似,只是在计算exp[j2πvy/N]时同正变换有符号的区别,以及在计算完成后逆变换需要将值除以N,因此不难写出一维傅立叶逆变换的实现代码。在进行二维傅立叶变换将图像由空域变换到频域之前,首先需要通过补0的手段将点数非2的整数次幂的非正方型网格采样构造为一个长宽均为2的整数次幂的正方型网格:
int WM=(int)(log(W)/log(2)+1.0f); file://计算图像宽应为2的多少次幂 int HM=(int)(log(H)/log(2)+1.0f); file://计算图像高应为2的多少次幂 WM=HM=max(WM,HM); file://取二者大值 int WN=(int)pow(2,WM); file://构造网格宽度 int HN=(int)pow(2,HM); file://构造网格高度 for{int i=0;i;for(int j=0;j if(i U[i*WN*3+j].Re=D[i*W*3+j]; file://D为图像序列 U[i*WN*3+j].Im=0.0f; }else file://缺位补0 U[i*WN*3+j].Re=U[i*WN*3+j].Im=0.0f; } } |
预处理完毕后,可对构造网格的每一列分别进行一维快速傅立叶变换,并将结果存放在原位置,结果乘以N,完成第一步的转换,求得F(x,v):
for(i=0;i for(int j=0;j UH[j].Re=U[j*WN*3+i].Re; UH[j].Im=U[j*WN*3+i].Im; } DFT_FFT(UH,HM); file://对UH进行快速离散傅立叶变换 for(j=0;j U[j*WN*3+i].Re=HN*UH[j].Re; file://N=HN U[j*WN*3+i].Im=HN*UH[j].Im; } } |
随即对构造网格的每一行进行傅立叶变换,得到最终的变换结果F(u,v):
for(i=0;i for(int k=0;k<3;k++){ file://对24位位图的R、G、B三分量均各自进行变换 for(int j=0;j UW[j].Re=U[i*WN*3+j*3+k].Re; UW[j].Im=U[i*WN*3+j*3+k].Im; } DFT_FFT(UW,WM); file://对UW序列进行快速离散傅立叶变换 for(j=0;j U[i*WN*3+j*3+k].Re=UW[j].Re; U[i*WN*3+j*3+k].Im=UW[j].Im; } } } |
至于二维傅立叶逆变换则基本上是上述过程的逆过程,在此就不再赘述。根据逆滤波图像恢复的设计方案,先通过前面的二维傅立叶变换将退化图像g(x,y)从空域变换到频域得到G(u,v),然后在频域经过恢复转移函数M(u,v)的恢复处理并经过二维傅立叶逆变换将结果由频域转换回空域,就可得到经过恢复处理的近似原始图像:
…… dsp.DFT_2D_FFT(m_cpBuffer+54,m_nWidth,m_nHeight,U); file://进行二维傅立叶变换 for(int i=0;i for(int j=0;j int k=(int)(j/3); D1=(float)sqrt(i*i+k*k); H=1.0f/(1+(D1/D0)*(D1/D0)); file://H(u,v)=1/(1+(u2+v2)/D02)) if(H>0.45f){ file://阀值 d取0.45 U[i*3*WN+j].Re/=H; file://在频域与M(u,v)相乘 U[i*3*WN+j].Im/=H; }else{ U[i*3*WN+j].Re*=0.6f; file://如未超过阀值则M(u,v)取常数k=0.6 U[i*3*WN+j].Im*=0.6f; } } } dsp.DFT_2D_IFFT(m_cpBuffer+54,m_nWidth,m_nHeight,U); file://进行傅立叶逆变换 |
这里的逆滤波处理算法采用的是经过改进的恢复转移函数M(u,v),因此恢复后的图像不会出现振铃现象。以标准检测图像Lina为处理对象应用以上恢复处理算法,效果如下图所示。其中间图像为未经过改进的简单算法,在胳膊和脸部存在较明显的振铃现象,而采取了改进措施的图像则没有任何振铃现象出现,图像得到了较好的恢复。
维纳滤波处理
维纳(Wiener)滤波是对退化图像进行恢复处理的另一种常用算法,是一种有约束的恢复处理方法,其采用的维纳滤波器是一种最小均方误差滤波器,其数学形式比较复杂:
F(u,v)=[(1/H(u,v))*(|H(u,v)|2)/(|H(u,v)|2+s*[Sn(u,v)/Sf(u,v)])]*G(u,v) |
当s为1时,上式就是普通的维纳滤波;如果s为变量,则为参数维纳滤波,如果没有噪声干扰,即Sn(u,v)=0时,上式实际就是前面的逆滤波。从其数学形式可以看出:维纳滤波比逆滤波在对噪声的处理方面要强一些。以上只是理论上的数学形式,在进行实际处理时,往往不知道噪声函数Sn(u,v)和Sf(u,v)的分布情况,因此在实际应用时多用下式进行近似处理:
F(u,v)=[(1/H(u,v))* (|H(u,v)|2)/(|H(u,v)|2+K)]*G(u,v) |
其中K是一个预先设定的常数。由此可以写出维纳滤波的实现代码:
…… float K=0.05f; file://预先设定常数K dsp.DFT_2D_FFT(m_cpBuffer+54,m_nWidth,m_nHeight,U); file://转换到频域 for(int i=0;i for(int j=0;j int k=(int)(j/3); D1=(float)sqrt(i*i+k*k); float H=1.0f/(1+(D1/D0)*(D1/D0));//H(u,v)= 1/(1+(u2+v2)/D02)) U[i*3*WN+j].Re=(U[i*3*WN+j].Re*H)/(H*H+K); file://维纳滤波 U[i*3*WN+j].Im=(U[i*3*WN+j].Im*H)/(H*H+K); } } dsp.DFT_2D_IFFT(m_cpBuffer+54,m_nWidth,m_nHeight,U);//返回到空域 |
对经过退化的Lina图像应用维纳滤波处理,可得到如右图所示的恢复效果图。由于维纳滤波在进行恢复时对噪声进行了处理,因此其恢复效果要比逆滤波要好,尤其是退化图像的噪声干扰较强时效果更为明显。
小结
本文对比较常用的两种图像恢复算法逆滤波和维纳滤波的实现过程作了较为详细的讲述,通过对图像质量较低的退化图像应用上述算法可以使图像质量得到一定程度的改善,在视觉上可以得到较好的改观。类似的图像恢复算法还有有约束最小平方恢复算法等多种,应视具体情况灵活选择合适的算法以获取最佳的恢复效果。本文所述程序在Windows 98下,由Microsoft Visual C++ 6.0编译通过。
Visual C++实现视频图像处理技术
[前言:] 随着计算机软件、硬件技术的日新月异的发展和普及,人类已经进入一个高速发展的信息化时代,人类大概有80%的信息来自图像,科学研究、技术应用中图像处理技术越来越成为不可缺少的手段。图像处理所涉及的领域有军事应用、医学诊断、工业监控、物体的自动分检识别系统等等,这些系统无不需要计算机提供实时动态,效果逼真的图像。
基于图像采集卡的视频图像处理系统
计算机图像处理系统从系统层次上可分为高、中、低档三个层次,目前一般比较普及的是低档次的系统,该系统由CCD(摄像头)、图像采集卡、计算机三个部分组成,其结构简单,应用方便,效果也比较不错,得到的图像较清晰。目前网上基于VC开发经验的文章不少,可是关于如何在VC开发平台上使用图像采集卡的文章确没发现,笔者针对在科研开发中积累的使用图像采集卡经验,介绍如何自己是如何将采集卡集成到图像开发系统中,希望能够给目前正需要利用图像采集卡开发自己的图像处理系统的朋友有所帮助。
笔者使用的摄像机采用台湾BENTECH INDUSTRIAL 有限公司生产的CV-155L黑白摄像机。该摄像机分辨率为752x582。图象采集卡我们采用北京中科院科技嘉公司开发的基于PCI 总线的CA-MPE 1000 黑白图象采集卡。使用图像采集卡分三步,首先安装采集卡的驱动程序,并将虚拟驱动文件VxD.vxd拷贝到Windows的SYSTEM目录下;这时候就可以进入开发状态了,进入VC开发平台,生成新的项目,由于生产厂家为图像采集卡提供了以mpew32.dll、mpew32.lib命名的库文件,库中提供了初始硬件、采集图像等函数,为使用这些函数,在新项目上连接该动态库;最后一步就是采集图像并显示处理了,这一步要设置系统调色板,因为采集卡提供的是裸图形式,既纯图像数据,没有图像的规格和调色板信息,这些需要开发者自己规定实现,下面是实现的部分代码:
CTestView::CTestView() / CTestView::~CTestView() SetStretchBltMode (hdc, COLORONCOLOR) ; bGrabMark = TRUE; while (bGrabMark == TRUE)
if ((pLogPal=(LOGPALETTE *)malloc(sizeof(LOGPALETTE) + (j*sizeof(PALETTEENTRY)))) == NULL) pLogPal->palVersion=0x300; for (i=0;i pLogPal->palPalEntry[i].peRed = i ;
cxDib = width; cyDib = height; if ( (bmi = (BITMAPINFO *)malloc(sizeof(BITMAPINFOHEADER) + j*sizeof(RGBQUAD))) == NULL ) bmi->bmiHeader.biBitCount = bitCount ; bmi->bmiHeader.biCompression = 0 ; for (i=0;i bmi->bmiColors[i].rgbBlue = i ;
} |
视频"画中画"技术
"画中画"这个概念类似与彩色电视机"画中画",就是在一幅大的图像内显示另外一幅内容不同的小的图像,小图像的尺寸大小一般地说为大图像尺寸的1/4或1/9,显示位置在大图像的右上角。这种技术不仅在电视技术中,在可视电话系统也可以发现这种技术的身影,它们都是依靠硬件来实现的,但是如何在VC开发平台上用编程语言来将该功能添加到自己开发的视频监控软件,为使用者提供更大的信息量呢?也许读者最容易想到的是首先显示大图像,然后再在一个固定位置画第二幅小图像,这种技术技术如果对于静止图像当然没有问题,但是对于视频流,由于每一秒钟需要画25幀,即25幅图像,这样一来计算机需要不停的画不停的擦除,会给用户以闪烁的感觉,如何解决这个问题呢?有的参考书上将大小图像分快显示,这种方法要将待显示的图像数据与显示位置的关系对应起来,容易出错不说,而且麻烦,且速度慢,为此,我对该方法进行了改进,得到了满意的效果。实现的代码如下:
void pictureinpicture( ) CDC MemDc; MemDc.SelectObject(oldmap); } |
VC实现对不同信号波形相似程度的判别
摘要:本文介绍了利用相关对信号波形进行相似程度的判别方法。通过该技术可以对采集到的多种类型的数据信号间的相似度进行判别。本算法由Microsoft Visual C++ 6.0实现。
一、 引言
在工程上我们经常要判断某设备产生的实际波形信号是否能同预先设计的相拟合,但由于实际产生的波形不仅仅是简单的正、余弦波形,而往往是含有较丰富频率分布的不规则波形,而设备元器件本身及外界的电磁干扰又不可避免的引入了干扰噪声,就为我们分析其与预先设计波形的拟合程度的判别增加了困难。另外,实际波形和预先设计波形间往往存在着时序上的差别,相位的改变同样也不利于信号的拟合判别。本文利用高等数学以及信号与系统方面的有关知识提出对该问题的解决方法。
二、 信号相似程度判别的理论依据
在信号与系统这门学科中,相关性是一种在时域中对信号特性进行描述的重要方法。由于其通信的功率谱函数是一对傅立叶变换,在信号分析中往往利用它来分析随机信号的功率谱分布,以致不少人一提到相关性马上会联想到信号功率谱的计算,但相关在对确定信号的分析也是有一定应用。由于相关的概念是为研究随机信号的统计特性而引入的,那么从理论上我们也可以将其应用于两个确定信号(一个我们采集到的信号波形和一个理论波形)相似性的研究上。
要比较两波形的相似程度还要从相关的概念上入手,假定两信号分别为x(t)、y(t),可以选择当倍数a使a*y(t)去逼近x(t)。再此我们可以借用误差能量来度量这对波形的相似程度,具体方法同高等数学上用来判断函数间正交性的方法基本类似:
误差能量用x(t)-a*y(t)的平方在时域上的积分来表示;倍数a的选择必须要保证能使能量误差为最小,通过对函数求导求极值可以得知当a为x(t)*y(t)在时域的积分与y(t)*y(t)在时域的积分比值时可以满足条件,在此条件下的误差能量是可能所有条件下最小的。定义x(t)与y(t)的相关数为Pxy,其平方与1的差值为相对误差能量,即误差能量与x(t)*x(t)在时域积分的比值。其中,xy就可以用来表征两波形的相似程度。解出关于Pxy的方程,其分子为x(t)*y(t)在时域的积分;分为两信号各自的平方在时域积分之积的平方根。从数学上可以证明分子的模小于分母,也即相关数Pxy的模不会大于1。由于对于能量有限的信号而言,能量是确定的,相关系数Pxy的大小只由x(t)*y(t)的积分所决定。如果两完全不相似的波形其幅度取值和出现时刻是相互独立、彼此无关的,x(t)*y(t)=0,其积分结果亦为0,所以当相关系数为0时相似度最差,即不相关。当相关系数为1,则误差能量为0,说明这两信号相似度很好,是线形相关的。因此把相关系数作为两个信号
波形的相似性(或线形相关性)的一种度量完全是有理论依据的、合理的。
三、 算法的设计与实现
我们在对信号进行比较之前,先将理论波形做成一个数据文件,实际设备输出的波形也通过计算机接口采集并将数据存成数据文件。我们编写的程序通过对两个数据文件的相关性比较来得出实际波形同理论波形信号的拟合度。下面就对数据文件的读取、数据相关程度计算等关键代码作简要的绍:
首先,要在计算相关系数前把参加运算的两序列数据从文件读取到内存(堆栈)中去,为了方便读取多种数据格式的文件使程序更加灵活,选用MFC基本类库的CFileDialog类的成员函数来选取数文件,然后再通过CFile类的相关成员函数将其读取到内存中:
…… CFileDialog dlg(TRUE,"dat","*.dat", //TRUE为"打开文件"窗口 OFN_HIDEREADONLY|OFN_OVERWRITEPROMPT, "信号数据文件(*.dat)|*.dat|所有文件(*.*)|*.*||",NULL); if(dlg.DoModal()==IDOK) { CString FileName=""; FileName=dlg.GetPathName(); //取文件所在的完整路径 CFile file; file.Open(FileName,CFile::modeReadWrite); //以读写方式打开文件 buf1=new char [file.GetLength()]; //为指针动态分配堆栈 file.Read(buf1,file.GetLength()); //将数据读取到内存 m_nData1Len=file.GetLength(); //获取文件长度 file.Close(); //关闭文件 } …… |
上述为读取一个信号文件的相关代码,其中buf1是一个char*类型的指针,该指针指向的内存存储有数据文件的数据,m_nData1Len 保存有第一个数据文件的长度。用同样的方法将第二个数据文件也读取到内存中,指向其首地址的指针为buf2,文件长度为m_nData2Len。参与运算的数据序列准备好后就可以进行这两组信号波形的相关系数的计算了,下面是有关的关键部分代码:
…… int N=m_nData1Len>m_nData2Len?m_nData2Len:m_nData1Len; |
由于两序列长度可能不一样,如以较长序列为准,将短序列不足部分补0,根据相关系数的概念,补0部分的x(t)*y(t)的积分为0,没有实际意义,故以较短序列为准可以避免不必要的运算,运算效较高。
…… float A,B,C,Pxy; A=B=C=Pxy=0.0f; …… |
在计算机中将积分近似按离散点取和的方式进行近似的积分:
for(int i=0;i { A+=buf1[i]*buf2[i];//对x(t)*y(t)的积分 B+=buf1[i]*buf1[i];//对x(t)*x(t)的积分 C+=buf2[i]*buf2[i];//对y(t)*y(t)的积分 } Pxy=A/(sqrt(B*C));//计算出相关系数 …… |
最后释放掉申请的内存:
delete[] buf1; delete[] buf2; |
四、 实验效果的检验
下面通过一个实际的例子来检验一下上述程序,我们想要获得的理想的波形如下图Data1所示,Data2所示波形是设备经过噪声抑制和相位纠偏等诸多措施后产生的实际波形,Data3所示波形是在没有任何保护措施下得到的粗糙的波形,显然Data2要比Data3能更好的同设计的理想波形Data1相拟合,但只是停留在定性分析上,究竟相似程度如何,定量的分析靠肉眼显然是无法完成的。先对Data1和Data2波形信号进行相关系数计算,得出其相关系数为0.793931,基本上是线形相关的,即实际的Data2信号设计的理想信号Data1的拟合程度还是可以接受的;然后再对Data1和Data3两波形信号进行相关系数计算,组信号的相关系数为 -0.013341,基本上线形不相关;再对Data2和Data3进行分析,计算结果是0.011665,结论也是基本不相关。通过上述程序对波形信号进行的定量分析同直观上的定性分析是相吻的。通过实际实验的检验证明该程序是可靠、实用的。
小结:本文提出的对波形信号相似程度的判断在电子工程上有着较为广泛的应用,能准确的判断出参加比较的两波形信号的相似程度,为设备的改进、元器件的选型等提供可参考的依据。另外,在判断移动的信号是否具有线形相关性的场合,如对雷达站接收到的两个不同距离的目标的反射信号的分析等都可以用本算法。通过对本文介绍的相关算法的改进还可以对信号的功率谱进行绘制、对波形信号进行更全面
的分析。本程序在Windows 98下,由Microsoft Visual C++ 6.0编译通过。