大家来找茬--初识图像处理

今天在一个人的博客http://mindhacks.cn/topics/programming/上看到的文章,引发我自己开始写博客的念头。内容就从最近玩的大家来找茬开始吧。

  最近我的朋友没事迷上了大家来找茬,我想,做个软件搞定它,那就不要烦了。就这样开始了我的图像处理的学习。

  大体分为以下几步:

  1.截图,把正在玩的游戏画面截下来。

  2.对此图进行分析,得到要比较的两个图的位置。(这步比较难)

  3.对选定好的两幅图逐点比较,不同的点突出显示(可以动画什么的),或者相同点都刷一种颜色(这样能立刻看出不同点在哪里),

  4.让鼠标点击5个不同点的位置,对于点完图像上出现的框不予理会。

  目前我自己做了前3个,对于第四点涉及鼠标操作,本人目前不太感兴趣,就没有做完。

  下面详细分析如何做的前3点的。

  1.软件很多,本人只了解VC一点点,就从这个出发了。由于本人没有什么VC基础,所以拷贝了网上的现成的程序。哎~,之前在网上看到一个VC截图全过程的网页,我就是按照那个做的,现在不知道去哪里了,看来还得自己写一遍,以防我这个猪头脑子以后忘掉。
  新建MFC工程,姑且名字为grab33,选择单个文档(为什么选择这个,能不能选择基本对话我不晓得,详情看VC的东西,我这里只是依别人的葫芦画瓢),在MFC AppWizard-Step 6 of 6 里面有个Base class,选择CScrollView(据说是为了在文档里有滚动条),其他都是默认。
  工程生成完了在ResourceView里面找到Menu\IDR_MAINFRAME双击,在右边的图片的菜单里建个按钮还是什么模态的,我不知道怎么说,反正是在文件,编辑,查看,帮助随便点哪个,然后再下面出现的虚线框再点下,给这个虚线框起个名字。比如在查看的下面点的,起名字为抓取全屏:Menu Item Properties:    ID为ID_EDITGRAB 标题为抓取全屏。右击这个 抓取全屏,建立类向导,在class name里面选择CGrab33View,在Messages里面选择COMMAND,双击它,就会跳出一个Add Member Function对话框,上面写的成员函数名为 OnEditgrab 点OK就好了。再点Edit Code,就可以编辑这个函数了。添加代码,使得函数变成如下:
void CGrab33View::OnEditgrab()
{
 // TODO: Add your command handler code here
 // TODO: Add your command handler code here
       // TODO: Add your command handler code here

       //获取全屏幕窗口的设备描述表

       HDC hdcScreen=::GetDC(NULL);

       //产生全屏幕窗口设备描述表的兼容设备描述表

       m_hdcCompatible=CreateCompatibleDC(hdcScreen);

       //产生全屏幕窗口设备描述表的兼容位图

       HBITMAP m_hbmScreen=CreateCompatibleBitmap(hdcScreen,

                     GetDeviceCaps(hdcScreen,HORZRES),GetDeviceCaps(hdcScreen,VERTRES));

       //将兼容位图选入兼容设备描述表

       SelectObject(m_hdcCompatible,m_hbmScreen);

       //将全屏幕窗口位图的象素数据拷贝到兼容设备描述表

       BitBlt(m_hdcCompatible,0,0,GetDeviceCaps(hdcScreen,HORZRES),

                     GetDeviceCaps(hdcScreen,VERTRES),hdcScreen,0,0,SRCCOPY);
  
       //获取当前光标及其位置

       HCURSOR hCursor=GetCursor();

       POINT ptCursor;

       GetCursorPos(&ptCursor);

       //获取光标的图标数据

       ICONINFO IconInfo;

       if (GetIconInfo(hCursor, &IconInfo))

       {

              ptCursor.x -= ((int) IconInfo.xHotspot);

              ptCursor.y -= ((int) IconInfo.yHotspot);

              if (IconInfo.hbmMask != NULL)

                     DeleteObject(IconInfo.hbmMask);

              if (IconInfo.hbmColor != NULL)

                     DeleteObject(IconInfo.hbmColor);

       }

       //在兼容设备描述表上画出该光标

       DrawIconEx(

              m_hdcCompatible,                                                          // handle to device context

              ptCursor.x, ptCursor.y,

              hCursor,                                                          // handle to icon to draw

              0,0,                                                                 // width of the icon

              0,                                                                          // index of frame in animated cursor

              NULL,                                                                   // handle to background brush

              DI_NORMAL | DI_COMPAT                                    // icon-drawing flags

              );

  
       //使窗口无效,调用OnDraw重画窗口

       Invalidate();   
 
}

然后,在此grab33View.cpp里面找到OnDraw函数,使得代码变为如下。
void CGrab33View::OnDraw(CDC* pDC)
{
 CGrab33Doc* pDoc = GetDocument();
 ASSERT_VALID(pDoc);
 // TODO: add draw code for native data here
       //在视图窗口显示全屏幕窗口图像及光标区域

       SelectObject(pDC->m_hDC,m_hbmScreen);

       BitBlt(pDC->m_hDC,0,0,GetSystemMetrics(SM_CXSCREEN),

                     GetSystemMetrics(SM_CXSCREEN),m_hdcCompatible,0,0,SRCCOPY);

}

由于这两个函数用了两个成员变量,需要在ClassView\CGrab33View下添加,所以跳到ClassView\CGrab33View,右击选Add Member Variable,类型为HBITMAP,名字为m_hbmScreen,再添加一个变量 类型为HDC,名字为m_hdcCompatible。
好了,这个完成了,点编译,运行,点查看 \抓取全屏,这个时候就有文档画面上显示抓取全屏的效果了。这里void CGrab33View::OnEditgrab() 里面 一部分是抓取屏幕的,一部分是抓取鼠标的,对我来说抓取鼠标反而不利我分析 大家来找茬图片,所以在我用的时候去掉了这部分。
2. 对截图进行分析,由于本人对VC不懂,所以m_hbmScreen怎么用不了解,后来我找了网上的截图且保存为bmp程序。对CGrab33View::OnEditgrab() 程序进行了调整。

 // TODO: Add your command handler code here
 HDC hdcScreen =::GetDC(NULL);

    int   Width = GetDeviceCaps(hdcScreen,HORZRES);    
    int   Height =GetDeviceCaps(hdcScreen,VERTRES);

 m_hdcCompatible=CreateCompatibleDC(hdcScreen);
 m_hbmScreen=CreateCompatibleBitmap(hdcScreen,   Width,   Height);
 SelectObject(m_hdcCompatible,m_hbmScreen);
 BitBlt(m_hdcCompatible,0, 0, Width, Height, hdcScreen, 0, 0,   SRCCOPY);

 CDC *pDC=CDC::FromHandle(hdcScreen);
      
  CDC   memDC;//内存DC    
  memDC.CreateCompatibleDC(pDC);              
  CBitmap   memBitmap;//建立和屏幕兼容的bitmap    
  memBitmap.CreateCompatibleBitmap(pDC,   Width,   Height);       
  memDC.SelectObject(&memBitmap);//将memBitmap选入内存DC    
  memDC.BitBlt(0, 0, Width, Height, 
   pDC, 0, 0,  
   SRCCOPY);//复制屏幕图像到内存DC    
      
   //以下代码保存memDC中的位图到文件    
  BITMAP   bmp;    
  memBitmap.GetBitmap(&bmp);//获得位图信息  
  
  DWORD bmpBytesSize = bmp.bmWidthBytes   *   bmp.bmHeight; 
  
  BITMAPFILEHEADER   bfh   =   {0};//位图文件头    
  bfh.bfOffBits   =   sizeof(BITMAPFILEHEADER)   +   sizeof(BITMAPINFOHEADER);//到位图数据的偏移量    
  bfh.bfSize   =   bfh.bfOffBits   +  bmpBytesSize ;//文件总的大小    
  bfh.bfType   =   (WORD)0x4d42;    
   
  BITMAPINFOHEADER   bih   =   {0};//位图信息头    
  bih.biBitCount   =   bmp.bmBitsPixel;//每个像素字节大小    
  bih.biCompression   =   BI_RGB;    
  bih.biHeight   =   bmp.bmHeight;//高度    
  bih.biPlanes   =   1;    
  bih.biSize   =   sizeof(BITMAPINFOHEADER);    
  bih.biSizeImage   =   bmpBytesSize;//图像数据大小    
  bih.biWidth   =   bmp.bmWidth;//宽度   
  
  BYTE * p = new byte[bmpBytesSize];//申请内存保存位图数据       
  GetDIBits(memDC.m_hDC, 
   (HBITMAP) memBitmap.m_hObject, 0, Height, 
    p,(LPBITMAPINFO) &bih,  DIB_RGB_COLORS);//获取位图数据    
  
  try 
  { 
   CFile fp("abcd.bmp",CFile::modeCreate | CFile::modeWrite); 
   fp.Write(&bfh, sizeof(BITMAPFILEHEADER));//写入位图文件头    
   fp.Write(&bih, sizeof(BITMAPINFOHEADER));//写入位图信息头    
   fp.Write(p, bmp.bmWidthBytes   *   bmp.bmHeight);//写入位图数据    
   fp.Close(); 
  } 
  catch( CFileException * e ) 
  { 
   // Handle the file exceptions here. 
   e->Delete(); 
  } 
  
  delete   []   p;
  Invalidate();
  以上代码我也解释不了,我只知道,保存的bmp是32位图,图像数据在p这个指针里。比如p[0] p[1] p[2] 分别为第一点的RGB值,p[3]为固定的值0xff,这里的第一点是指图像的左下角,也就是图像方向是最常见的从左到右,从下到上。
  下面就需要对p这个数组(指针)分析如何定位的问题。
  因为在大家来找茬这类游戏里面有两副相同图片,这两副图片一般在局部进行了改动,如果知道是哪两副图,就很容易用逐点比较的办法把不同的点区分开来。但是问题是,在截图里面如何知道这两幅图的位置呢?假如我们有鼠标定点的办法,那这个问题很容易解决。不过如果是这样,那就和图像处理没有什么事情了。在这里我不能采取鼠标定点的办法。我就想,一般情况这两幅图都是矩形图像,那我可不可以找矩形的办法呢?这个方式看起来是可以的。一般情况下,每幅图周围有或者黑或者浅灰的矩形线(条),我只要把这样的矩形线(条)找到就OK了。在大家来找茬里,这样矩形线(条)有这样的特点:矩形线条的左下角开始,横的方向的点颜色和左下角相似,竖方向的点颜色也和左下角相似。右上角也有类似的特点。所以找矩形就围绕这个特点来进行。假使点(x1,y1)为左下角,(x2,y2)为右上角,则(x1,y1~y2),(x1~x2,y1)这些点颜色和(x1,y1)相似,然后再判别(x2,y1~y2),(x1~x2,y2)这些点颜色和(x2,y2)相似。不满足条件则寻找下一个(x1,y1) 及(x2,y2)。具体代码如下:
  判断颜色相似函数:
  //yyy 为原数据,ppp为要和yyy比较的数据
unsigned char panduanxiangsi(unsigned char *ppp,unsigned char *yyy)
{
 unsigned char yscr,yscg,yscb;//颜色差 红 绿 蓝
 unsigned char chazhi=0x60;//颜色差值 允许的范围 这个值自己可以调的
 
             yscr=(*ppp)>(*yyy)?(*ppp-*yyy):(*yyy-*ppp);
 yscg=(*(ppp+1))>(*(yyy+1))?(*(ppp+1))-(*(yyy+1)):(*(yyy+1))-(*(ppp+1));
 yscb=(*(ppp+2))>(*(yyy+2))?(*(ppp+2))-(*(yyy+2)):(*(yyy+2))-(*(ppp+2));
  
 if((yscr>chazhi)||(yscg>chazhi)||(yscb>chazhi))//颜色不接近就返回0
 {
  return 0;
 }
 return 1;
}
  以下是找(x1,y1)(x2,y2)的具体做法,这个是在得到图像数据的那个数组(指针)之后做的。
//QX是横大小的四分之一,因为在玩大家来找茬时,里面要判断的图宽度>四分之一截屏宽度,QY类似
  int QX,QY;
  QX=bih.biHeight/4;
  QY=bih.biWidth/4;

  int x1,y1,x2,y2;
  int xi,yk;
  int xmax,ymax;
  unsigned char flagerr=0;

  for(x1=0;x1<QX;x1++)//左图的x1坐标大概在0~四分之一截图
  {
  for(y1=0;y1<QY;y1++)//左图的y1坐标大概在0~四分之一截图
  {
   //以大家来找茬的实际情况看,每个图周围的那个框是灰白的,
                                        //如果颜色太深肯定不是左下角了,
                                       //当然,如果框是变色的或者大红大绿之类,这段就要删掉了
   if((*(p+(x1*bih.biWidth+y1)*4))<0x20)continue;
   if((*(p+(x1*bih.biWidth+y1)*4+1))<0x20)continue;
   if((*(p+(x1*bih.biWidth+y1)*4+2))<0x20)continue;

   //先判断此(x1,y1)点附近有没有近似点,
                                        //横方向一条,最起码长度为QX都为近似色,否则肯定不是左下角点,
                                        //纵方向一条,长度为QY,理由同QX
   flagerr=0;
   yk=y1;
   for(xi=x1;xi<x1+QX;xi++)
   {
    if(panduanxiangsi(p+(xi*bih.biWidth+yk)*4,p+(x1*bih.biWidth+y1)*4));
    else {flagerr=1;break;}    
   }
   if(flagerr==1)//说明此(x1 y1)不是左下角,则让y1++继续
   {
    continue; 
   }

   xi=x1;
   for(yk=y1;yk<y1+QY;yk++)
   {
    if(panduanxiangsi(p+(xi*bih.biWidth+yk)*4,p+(x1*bih.biWidth+y1)*4));
    else {flagerr=1;break;}    
   }
   if(flagerr==1)//说明此(x1 y1)不是左下角,则让y1为当前不是的点开始
   {
    y1=yk;
    continue;
   }
   //走到这里说明(x1,y1)作为左下角的点基本满足,
                                        //但是具体是不是还要看右上角点怎么样

   //目前这个左下角求出横方向连续近似色最大长度,纵方向最大长度
   yk=y1;
   for(xi=x1+QX;xi<bih.biHeight;xi++)
   {
    if(panduanxiangsi(p+(xi*bih.biWidth+yk)*4,p+(x1*bih.biWidth+y1)*4));
    else {xmax=xi;break;}    
   }

   xi=x1;
   for(yk=y1+QY;yk<bih.biWidth;yk++)
   {
    if(panduanxiangsi(p+(xi*bih.biWidth+yk)*4,p+(x1*bih.biWidth+y1)*4));
    else {ymax=yk;break;}    
   }

                                        //寻找右上角点
   //x2=x1+QX ~ xmax y2=y1+QY ~ymax

   for(x2=x1+QX;x2<xmax;x2++)
   {
    for(y2=y1+QY;y2<ymax;y2++)
    {
     //假如当前点(x2,y2)为近似白色的点
     if(panduanxiangsi(p+(x2*bih.biWidth+y2)*4,p+(x1*bih.biWidth+y1)*4))
     {
      flagerr=0;
      yk=y2;
      for(xi=x1;xi<x2;xi++)
      {
        if(panduanxiangsi(p+(xi*bih.biWidth+yk)*4,p+(x1*bih.biWidth+y1)*4));
        else {flagerr=1;break;}
      }
      if(flagerr==1)continue;


      xi=x2;
      for(yk=y1;yk<y2;yk++)
      {
        if(panduanxiangsi(p+(xi*bih.biWidth+yk)*4,p+(x1*bih.biWidth+y1)*4));
        else {flagerr=1;break;}
      }
      if(flagerr==1)continue;
      

      //走到这里说明有合适的矩形了,下面把这个矩形里面的数据都填上某种颜色
       /*
       for(xi=x1;xi<x2;xi++)
       {
        for(yk=y1;yk<y2;yk++)
        {
         *(p+(xi*bih.biWidth+yk)*4)=0x55;         
        }
       }*/
      //填完颜色后就结束,也就是找一个矩形就可以了
      goto  loopwhiteend;
     }
    }
   }
   
  }
  }
//走到这里说明要不是没有找到合适的矩形,要不就是找到了。
loopwhiteend: ;

是否找到了合适的矩形进行下面判断就可以了
  if(((y2-y1)<2*QY))y2=2*y2+y1;//这说明找到的矩形是(x1,y1)为左图的左下角,(x2,y2)为左图的右上角
             //说明截到的图是两个相似并排图,找到的矩形是(x1,y1)为左图的左下角,(x2,y2)为右图的右上角
 if((y2-y1)>3*QY)
 {
  int y3;
  unsigned long maxcnt=0;
  unsigned long cntdot=0;
  ymax=0;
  //下面精确两副图的左下角,左图为(x1,y1) 右图为(x1,y3)
  for(y3=((y1+y2)/2-20);y3<((y1+y2)/2+20);y3++)
  {
   cntdot=0;
   for(xi=x1;xi<x2;xi++)
   {
    for(yk=0;(yk<(y3-y1))&&(yk<(y2-y3));yk++)
    {
     if(panduanxiangsigao(p+(xi*bih.biWidth+yk+y1)*4,p+(xi*bih.biWidth+yk+y3)*4))
      cntdot++;
    }
   }
   if(cntdot>maxcnt){maxcnt=cntdot;ymax=y3;}
  }
  //以上是两幅图水平移动比对,得到最多的相似点时的y3(ymax)就为右图的左下角

到此就是把流程的第2部完成了,因为这段程序没有完全分割开来,就在这边做个注解

//以下是流程的第3 部
   y3=ymax;
                                        //以下就是对图逐点比较,相同点左图用黑色填充,
                                       //不同点左图白色填充,右图黑色填充
   for(xi=x1;xi<x2;xi++)
   {
    for(yk=0;(yk<(y3-y1))&&(yk<(y2-y3));yk++)
    {
     if(panduanxiangsigao(p+(xi*bih.biWidth+yk+y1)*4,p+(xi*bih.biWidth+yk+y3)*4))
     {
      *(p+(xi*bih.biWidth+yk+y1)*4)=0x00;
      *(p+(xi*bih.biWidth+yk+y1)*4+1)=0x00;
      *(p+(xi*bih.biWidth+yk+y1)*4+2)=0x00;

      *(p+(xi*bih.biWidth+yk+y3)*4)=0x50;
     
     }
      
     else
     {
      *(p+(xi*bih.biWidth+yk+y1)*4)=0xff;
      *(p+(xi*bih.biWidth+yk+y1)*4+1)=0xff;
      *(p+(xi*bih.biWidth+yk+y1)*4+2)=0xff;


      *(p+(xi*bih.biWidth+yk+y3)*4)=0x00;
      *(p+(xi*bih.biWidth+yk+y3)*4+1)=0x00;
      *(p+(xi*bih.biWidth+yk+y3)*4+2)=0x00;


     }
    }
   }
//当处理完图像之后,写入到当前屏幕上或者保存到bmp文件中,因本人VC不精通,所以就写到bmp文件中了
   try 
   { 
    CFile fp("abcdtwo.bmp",CFile::modeCreate | CFile::modeWrite); 
    fp.Write(&bfh, sizeof(BITMAPFILEHEADER));//写入位图文件头    
    fp.Write(&bih, sizeof(BITMAPINFOHEADER));//写入位图信息头    
    fp.Write(p, bmp.bmWidthBytes   *   bmp.bmHeight);//写入位图数据    
    fp.Close(); 
   } 
   catch( CFileException * e ) 
   { 
    // Handle the file exceptions here. 
    e->Delete(); 
   } /**/

 }

 最后谈下效果。

从实践的效果来看,基本还是可以的,5处不同是可以找出,就是有些时候那个矩形找不到,所以造成整个失效。但是这个矩形我也尽力了,有的时候那个矩形颜色会变色,我就很难定位,有的时候由于不小心把大家来找茬的画面移动了,导致部分画面看不到,这样矩形也找不到。对于后者没有什么好说的,但是对于前者,确实我还有还多需要工作的,也就是如果矩形是多变色的,我怎么才能找到呢,是个难题啊,以后慢慢倒腾。

还有,从运行时间上不算理想,我没有做优化,所以在实践中,哈哈,我还是比不过人家天天玩这个游戏的人呢。对于时间这个问题,以后也要好好优化下。

这就是我最近学习图像的心得,以后继续。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值