利用VC实现图像的特殊显示效果
经常看电视的朋友们不知注意到没有,最近的电视连续剧在每集片头或片尾部分都有显示一些特殊效果的图像,比如前一阵子中央一套放的《长征》、目前中央八套正在播放的《康熙王朝》,这些特效称为"图像的浮雕效果"和"图像的雕刻效果",经过这些特效处理后的图像增强了观众们的视觉效果,它们看上去仿佛是使用3D技术作的,这也是为什么这种技术那么流行的原因吧。其实,我们完全可以用一些简单的数字图像处理算法来实现这些看似复杂高深的显示效果。我们手头上的一些关于利用VC开发数字图像资料大都是讲解如何控制图像的每一行或每一列像素的显示时间或顺序来实现一些简单的图像显示效果,涉及到图像算法的文章很少,本文针对上述的这两种图像特效处理技术并加以引伸,讲解了一些实现图像特效算法,以一个标准的Lena灰度图像为原图,给出了处理后的效果图,同时给出了VC开发平台上的部分实现源代码。

  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; jbiHeight; j++)
   for( i=0; ibiWidth; 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; jbiHeight; j++)
 for( i=0; ibiWidth; 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. 基于文档-视图结构的类

  在图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; jbiHeight; j++)//以下循环求(x,y)位置的灰度值
  for( i=0; ibiWidth; 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; jbiHeight; j++)
     for( i=0; ibiWidth; 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;ibiHeight;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; ibiHeight; i++)
  for(j=0; jbiWidth; 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; jbiHeight; j++)
      for( i=0; ibiWidth; 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子块的两幅图像的各子块的相似度。

  对于用户选定的块其实是代表查询对象的,因此应该加大权重,相对来说就是减小其他块的权重,然后可以将乘过对应权重的块的相似度相加,得到最终的相似度,然后将所有目标图像与用户输入的图像的相似度从大到小排序,选出值最大的几张作为最后的查询结果显示出来返回。

  以上是具体实现设想,程序实现如下:

//基于颜色直方图的方法
pDC->TextOut(10,168,"检索结果:");
CBmpProc *pDestBmp;
CString comp_pic_path;
double fsim[15]; file://15张待比较的目标图片与用户输入图片的相似度存放的数组
int psim[15]; file://与fsim想对应的图片编号数组,以便显示
for(int comp_pic=1;comp_pic<=15;comp_pic++){
comp_pic_path.Format("image%d.bmp",comp_pic);
bmp.LoadFromFile(comp_pic_path); // 从库中读入位图
pDestBmp = (CBmpProc*)new(CBmpProc); // 用new分配类目标
pDestBmp->LoadFromObject(bmp, &CRect(0,0,128,128));
// 从bmp中的指定区域读入图像,以便图片匹配的进行
pDestBmp->CalculateColor(*pDC); file://计算目标图片的颜色直方图
int x1,x2,y1,y2,x3,x4,y3,y4;
x1=obj_set.m_x1;x2=obj_set.m_x2;x3=obj_set.m_x3;x4=obj_set.m_x4;
y1=obj_set.m_y1;y2=obj_set.m_y2;y3=obj_set.m_y3;y4=obj_set.m_y4;
file://用户输入的对象所在子块(既用户选定的4个子块)的坐标
double sim[4][4]; file://子块之间的相似度数组
int ccount[4][4]; file://有过统计的颜色数目记录数组
for(int i=0;i<4;i++)
for(int j=0;j<4;j++){
sim[i][j]=0;
ccount[i][j]=0;
}
file://以下两个for按公式计算两幅图像的各对应子块之间的相似度
for(i=0;i<4;i++)
for(int j=0;j<4;j++)
for(int k=0;k<256;k++){
if((pDestBmp->Color[i][j][k]>=pBmp->Color[i][j][k])&&pDestBmp->Color[i][j][k]!=0){
sim[i][j]+=(1-((fabs(pDestBmp->Color[i][j][k]-pBmp->Color[i][j][k]))/(pDestBmp->Color[i][j][k])));
ccount[i][j]++;
}
if((pDestBmp->Color[i][j][k]Color[i][j][k])&&pBmp->Color[i][j][k]!=0){
sim[i][j]+=(1-((fabs(pDestBmp->Colori][j][k]-pBmp->Color[i][j][k]))/(pBmp->Color[i][j][k])));
 ccount[i][j]++;
  }
 }
for(i=0;i<4;i++)
for(int j=0;j<4;j++){
sim[i][j]=sim[i][j]/ccount[i][j];
}
file://计算两图像最终的相似度结果
double final_sim=0;
for(i=0;i<4;i++)
for(int j=0;j<4;j++){
file://对用户指定的块设置权重为1
if((i==x1&&j==y1)||(i==x2&&j==y2)||(i==x3&&j==y3)||(i==x4&&j==y4))
final_sim+=sim[i][j];
else
file://其他块降低权重为0.7,提高对对象匹配的精确度
final_sim+=(sim[i][j]*0.7);
}
file://将15幅被比较图像与用户输入源图像的最后计算出来的相似度结果记录在数组中
fsim[comp_pic-1]=final_sim;
delete (CBmpProc*)pDestBmp;
}
int count=15;double tempf;int tempp;
for(int l=0;l<15;l++){
psim[l]=l+1; file://设定编号数组
}
file://将15个相似度从大到小排列,并且改变次序的时候编号数组和跟着改变
for(int i=count;i>0;i--){
for(int j=0;jif(fsim[j]tempf=fsim[j];
tempp=psim[j];
fsim[j]=fsim[j+1];
psim[j]=psim[j+1];
fsim[j+1]=tempf;
psim[j+1]=tempp;
}
}
int disp=0;
int space=-128;
file://将相似度最大的的两张图片显示出来
for(int disp_pic=1;disp_pic<=2;disp_pic++){
comp_pic_path.Format("image%d.bmp",psim[disp_pic]);
bmp.LoadFromFile(comp_pic_path); // 从库中读入位图
pDestBmp = (CBmpProc*)new(CBmpProc); // 用new分配类目标
pDestBmp->LoadFromObject(bmp, &CRect(0,0,128,128)); // 从bmp中的指定区域读入图像
disp++;
space+=128;
pDC->Rectangle(10+space-1,190-1,138+space+1,318+1);
pDestBmp->Draw(*pDC, &CRect(10+space,190,138+space,318));
// 将pBmp中的图像绘入DC的指定区域
space+=6;
}
delete (CBmpProc*)pBmp; // 删除类目标,delete会自动调用类的析构函数。
AfxMessageBox("检索完成");
}


b) 基于颜色对的方法的程序实现

  该方法也需要分成4×4子块,计算颜色直方图,具体计算颜色直方图的方法上面已经有过详细的解释。
该方法主要在于对颜色对表示结构的实现,颜色对是某一图片的代表特征,因此在程序中必须有定量表示,现在采取用两个子块颜色直方图的欧氏距离表示,因此计算某一子块的颜色对表就是按八方向计算其与周围的子块之间的欧氏距离,将结果存放于一个double o_dis[8]的数组中,然后将这个数组从大到小排序,排序完成后再将数组中相互之间值的差小于某一域值(取8个颜色对的平均值的2%)的颜色对祛除(按序两两比较再移动数组里的变量实现),最后将结果先填入图像的特征颜色对表(有4×8=32个变量,是一个结构数组,结构记录用户选定子块的坐标和与其相对应的被选中的颜色对值)。

  最后,对4个用户选定的子块依次计算完毕,就可以调用SortColorPair()函数,对特征颜色对表做出处理(先从大到小排序,然后祛除差值小于总平均值的2%的特征颜色对)。

  在比较的时候,按顺序计算出目标图像的子块颜色对表,和以上的特征颜色对表匹配,如果匹配到,则标记该颜色对(设定另一标记0数组),并且将匹配数变量加一,如果最后匹配到的数目是60%以上,就算目标图像被搜索到。

  具体程序实现如下:

//计算子块(x,y)的颜色对表,采取"八方向邻接技术"
int CBmpProc::CalculateColorPair(int x, int y)
{
file://颜色对采取欧氏距离来描述
double o_dis[8];
for(int k=0;k<8;k++){
o_dis[k]=0;
}
file://计算(x,y)与周围所有子块的颜色直方图的欧氏距离
file://---------------------------------------------
for(int i=0;i<256;i++){
if((x-1)>=0&&(y-1)>=0)
o_dis[0]=o_dis[0]+(Color[x-1][y-1][i]-Color[x][y][i])*(Color[x-1][y-1][i]-Color[x][y][i]);
else
o_dis[0]=-1;
if((y-1)>=0)
o_dis[1]=o_dis[1]+(Color[x][y-1][i]-Color[x][y][i])*(Color[x][y-1][i]-Color[x][y][i]);
else
o_dis[1]=-1;
if((x+1)<=3&&(y-1)>=0)
o_dis[2]=o_dis[2]+(Color[x+1][y-1][i]-Color[x][y][i])*(Color[x+1][y-1][i]-Color[x][y][i]);
else
o_dis[2]=-1;
if((x-1)>=0)
o_dis[3]=o_dis[3]+(Color[x-1][y][i]-Color[x][y][i])*(Color[x-1][y][i]-Color[x][y][i]);
else
o_dis[3]=-1;
if((x+1)<=3)
o_dis[4]=o_dis[4]+(Color[x+1][y][i]-Color[x][y][i])*(Color[x+1][y][i]-Color[x][y][i]);
else
o_dis[4]=-1;
if((x-1)>=0&&(y+1)<=3)
o_dis[5]=o_dis[5]+(Color[x-1][y+1][i]-Color[x][y][i])*(Color[x-1][y+1][i]-Color[x][y][i]);
else
o_dis[5]=-1;
if((y+1)<=3)
o_dis[6]=o_dis[6]+(Color[x][y+1][i]-Color[x][y][i])*(Color[x][y+1][i]-Color[x][y][i]);
else
o_dis[6]=-1;
if((x+1)<=3&&(y+1)<=3)
o_dis[7]=o_dis[7]+(Color[x+1][y+1][i]-Color[x][y][i])*(Color[x+1][y+1][i]-Color[x][y][i]);
else
o_dis[7]=-1;
}
for(int j=0;j<8;j++){
if(o_dis[j]>=0)
o_dis[j]=sqrt(o_dis[j]);
}
file://------------------------------------------------
file://欧氏距离计算结束
int flag=0;
int num=0;
for(int pairnum=0;pairnum<32;pairnum++){
if(pair[pairnum].x!=-1){
num++;
}
}//因为在计算子块的颜色对表的时候已经写了特征颜色对数组,因此要先统计一下特征颜色对数组里已经//有多少有数值了,以便下次的写入可以接在后面,而不至于覆盖了前面的数值
file://计算颜色对差值小于某个"域值"的这个域值
double ave=0;
for(int e=0;e<8;e++){
ave+=o_dis[e];
}
ave=ave/8;ave=ave*0.02; file://采取与子块周围颜色对的平均值的2%计为域值
file://对该子块的颜色对表进行从大到小的排序,采取冒泡排序
int count=8; double temp;
for(i=count;i>0;i--){
for(int j=0;jif(o_dis[j]temp=o_dis[j];
o_dis[j]=o_dis[j+1];
o_dis[j+1]=temp;
}
}
file://消除那些颜色对差值小于某个"域值"的颜色对,以消除那些没有意义的小对象
for(k=0;kif(fabs(o_dis[k]-o_dis[k+1])for(int l=k+1;lo_dis[l]=o_dis[l+1];
}
count--;
k--;
o_dis[count]=-1;
}
}
file://将该字块计算得到的颜色对表填入该图像的特征颜色对表
for(int scan=0;scan<8;scan++){
if(o_dis[scan]>0){
pair[num].x=x;
pair[num].y=y;
pair[num].o_dis=o_dis[scan];
num++;
}
}
return 1;
}

//计算该图像的最终确定的特征颜色对表
BOOL CBmpProc::SortColorPair()
{
file://32个数据项中有count个有实际数值
for(int count=0;count<32;count++){
if(pair[count].x==-1)
break;
}
struct color_pair temp;
file://对颜色对表从大到小排列序(冒泡排序法)
for(int i=count;i>0;i--){
for(int j=0;jif(pair[j].o_distemp=pair[j];
pair[j]=pair[j+1];
pair[j+1]=temp;
}
}
file://计算域值以消除差值小于这个值的颜色对
double ave=0;
for(int e=0;eave+=pair[e].o_dis;
}
ave=ave/count;
ave=ave*0.02;
file://消除差值小于域值的颜色对
for(int k=0;kif(fabs(pair[k].o_dis-pair[k+1].o_dis)for(int l=k+1;lpair[l]=pair[l+1];
}
count--;
k--;
}
}
file://置特征颜色对数目变量
pair_count=count;
return true;
}
将计算颜色直方图的代码表达如下:
file://以下函数计算颜色直方图
BOOL CBmpProc::CalculateColor(CDC &dc)
{
if (!IsValid())
return FALSE;
ASSERT(m_pInfo);
ASSERT(m_pInfo->bmiHeader.biSize == sizeof(BITMAPINFOHEADER));
// 复制源图
CDC compDC;
// 创建与当前显示设备兼容的内存设备描述表
compDC.CreateCompatibleDC(&dc);
compDC.SelectObject(this);
COLORREF clr; file://定义一个COLORREF结构,因为提取的象素点的颜色是以RGB形式表示的
int pix_color;
int red,green,blue;
int x,y;
for(int fd=0;fd<4;fd++)
for(int sd=0;sd<4;sd++)
for(int td=0;td<256;td++){
Color[fd][sd][td]=0;
}
file://计算颜色直方图
for(int i=0;i<4;i++)
for(int j=0;j<4;j++)
for(int k=0;k<32;k++)
for(int l=0;l<32;l++){
x=j*32+l;
y=i*32+k;
clr=compDC.GetPixel(x,y);
red=GetRValue(clr);
green=GetGValue(clr);
blue=GetBValue(clr);
file://因为RGB颜色共256^3种,不可能都保存到数组中,因此要先进行一定的提取工作,因为人对亮度的感
file://觉是最明显的,所以可以先将RGB颜色值转成亮度值,这个公式即转换公司,刚好亮度数值是256级的,//就可以统计颜色直方图了
pix_color=red*0.299+green*0.587+blue*0.114;
Color[i][j][pix_color]++;
file://对该象素点的颜色直方图数组中的相信位置加一,是直方图的物理实现
}
return true;
}

  以上三个函数实现对某一图像内部的具体计算,而对于基于颜色对方法的外部计算如下:

//计算用户确定的4块位置与其周围位置的颜色对(颜色对现采取用相邻两块的直方图的欧氏距离表示)
pBmp->CalculateColorPair(obj_set.m_x1,obj_set.m_y1);
pBmp->CalculateColorPair(obj_set.m_x2,obj_set.m_y2);
pBmp->CalculateColorPair(obj_set.m_x3,obj_set.m_y3);
pBmp->CalculateColorPair(obj_set.m_x4,obj_set.m_y4);
file://其实在以上的4部计算中,已经形成了初步的颜色对表,在此只不过是将表中的数据从大到小排列出来//并且祛除差值小于某一域值的颜色对
file://计算颜色对结束,形成颜色对表
pBmp->SortColorPair();
file://颜色对表计算出来,表中的数据既是用户输入的该图像的代表特征
pDC->TextOut(10,168,"检索结果:");
CBmpProc *pDestBmp;
CString comp_pic_path;
int disp=0;
int space=-128;
file://读取带比较的图像(在此初定15幅--现定义这15幅图像即图片数据库)
for(int comp_pic=1;comp_pic<=15;comp_pic++){
comp_pic_path.Format("image%d.bmp",comp_pic);
bmp.LoadFromFile(comp_pic_path); // 从库中读入位图
pDestBmp = (CBmpProc*)new(CBmpProc); // 用new分配类目标
pDestBmp->LoadFromObject(bmp, &CRect(0,0,128,128)); // 从bmp中的指定区域读入图像
file://计算当前被比较的图像的颜色直方图
pDestBmp->CalculateColor(*pDC);
int match=0; file://颜色对匹配数目
double ave=0; file://确定匹配时候不能使用精确匹配,所以需要一个差值小于某一域值时的域值
for(int s=0;spair_count;s++){
ave+=pBmp->pair[s].o_dis;
}
ave=ave/pBmp->pair_count; file://这个域值的基数即是用户输入的图片的颜色对表中颜色对的平均值
ave=ave*0.02; file://确定误差小于2%的颜色对均属于这个域值

int pairflag[32]; file://颜色对匹配标志数组,即某一颜色对如果在目标图像中找到,下一次就不能再匹配
for(int t=0;t<32;t++){
pairflag[t]=-1;
}
for(int i=0;i<4;i++){
for(int j=0;j<4;j++){
file://按顺序计算目标图像中一子块与其周围子块的颜色对,然后在用户输入的图像的颜色对表中查询计算出//来的颜色对
pDestBmp->CalculateColorPair(i,j);
for(int scan=0;scan<8;scan++){
if(pDestBmp->pair[scan].x==-1)
break;
}
for(int comp=0;compfor(int count=0;countpair_count;count++){
if((fabs(pBmp->pair[count].o_dis-pDestBmp->pair[comp].o_dis))file://差值小于某域值,则匹配到
pairflag[count]=0; file://置颜色对匹配标志位
match++; file://匹配数加一
break;
}
}
}
file://重新置目标图像的颜色对表为空,因为现在的实现方式是在计算某一子块的颜色对时已经写过了颜色对//表,为保证颜色对表的真确性,必须在查询下一子块的时候重新置颜色对表为空
for(int re=0;repDestBmp->pair[re].x=-1;
}
}
file://如果有60%以上的特征颜色对匹配到,就说明该图像已经被检索到
if(match>=(pBmp->pair_count*0.60)){
file://以下是对检索到的图像的界面上的排版显示
disp++;
space+=128;
file://画图像边框
pDC->Rectangle(10+space-1,190-1,138+space+1,318+1);
pDestBmp->Draw(*pDC, &CRect(10+space,190,138+space,318)); // 将pBmp中的图像绘入DC的指定区域
space+=6;
}
delete (CBmpProc*)pDestBmp; // 删除类目标,delete会自动调用CBmpProc类的析构函数。
}
delete (CBmpProc*)pBmp; // 删除类目标,delete会自动调用类的CBmpProc析构函数。
AfxMessageBox("检索完成");

  通过以上的程序,我们就实现了真正的图像内容检索,简单的程序就实现了现代计算机科学在多媒体研究前沿的任务。

 
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; ibiHeight; i++)
 for(j=0; jbiWidth; 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; jbiHeight; j++)
 for(i=0;ibiWidth;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之间往往可以达到比较满意的效果。下面给出