今天在一个人的博客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处不同是可以找出,就是有些时候那个矩形找不到,所以造成整个失效。但是这个矩形我也尽力了,有的时候那个矩形颜色会变色,我就很难定位,有的时候由于不小心把大家来找茬的画面移动了,导致部分画面看不到,这样矩形也找不到。对于后者没有什么好说的,但是对于前者,确实我还有还多需要工作的,也就是如果矩形是多变色的,我怎么才能找到呢,是个难题啊,以后慢慢倒腾。
还有,从运行时间上不算理想,我没有做优化,所以在实践中,哈哈,我还是比不过人家天天玩这个游戏的人呢。对于时间这个问题,以后也要好好优化下。
这就是我最近学习图像的心得,以后继续。