【前言】最近在博客园首页上看到有“大家来找茬”这个游戏(此游戏为找出两个相近图片的不同点)外挂的相关帖子,所以这里我也翻看了我之前(2009年5月)的写的一个简单的辅助程序(采用 VC6 开发的)。我在当时的写法是图快速和简单,代码效率上不是最高的,所以我现在使用更加合理的方法将其改进(采用 VC2005 开发)。因为发表时间临近春节,即将回籍贯老家,所以行文和排版等难免仓促,有待在将来继续改进。
之前我在 2009 年 5 月写的文章为:《快速“美女找茬”(辅助工具)》。
解决方案的主要步骤是相同的,找到游戏窗口,然后将游戏窗口的内容截图,然后把不同点用一个透明图层的方式,对齐覆盖到游戏窗口的相应位置。这里并没有继续去帮助用户做点击,因为这一步需要把图像上的差异点集合划分成五个区域,这需要进一步处理(留待将来考虑)。
相比之前的做法,本次改进内容包括:
(1)增加配置文件记录游戏窗口的布局信息(这些信息为截图后在 Photoshop 中测量得出),这样在游戏布局因为升级等原因发生改变时,程序不需要进行重新编译,普通用户只需要自行修改配置文件即可。
(2)检测差异像素点时,直接对图像像素数据进行分析,这样的效率会更高。(由于当前的 PC 已经普及多核 CPU,所以这一步也采用了多线程技术。)
(3)增加全局热键的配置,因为发出一个命令时,键盘操作的速度比鼠标这种定点设备操作 GUI 的速度更快。
在游戏时,效果如下图所示:
左侧是一个“鼠标穿透”的半透明图层窗口(称之为“提示窗口”),所以用户可以直接在左侧的红色方块内部点击即可。用户可以在程序位于通知栏的图标右键菜单中,选择设置,然后设置全局性热键,提示窗口的不透明度等信息。设置对话框如下图:
接下来主要分析以上改进(主要是 2 和 3 )的实现。
改进(1)比较容易,只需要增加一个配置文件即可,其主要内容如下:
; 连连看游戏窗口信息
[GameWnd]
Class=#32770
Text=大家来找茬 ;图片 1 的位置(客户区坐标)
[Image1]
Left=10
Top=186
Right=390
Bottom=471 ;图片 2 的位置(客户区坐标)
[Image2]
Left=403
Top=186
以上内容主要是用于存储游戏窗口的类名和标题,以及两幅图片在游戏窗口中的客户区坐标,比较直观。很显然,图片2的右下角坐标无需给出,因为两个图片很显然是相同大小的。
对于(2),我首先能想到的是,之前使用了 GetPixel / SetPixel 函数,尽管是对内存 DC 操作,但这样的效率可能仍然是不够高的。所以这一次我改为直接对像素数据块操作。同时,这一次我决定使用对两个图像的相同位置的像素数据直接用异或操作来处理,同时也把对应的指针和数据单位扩展为 4 个字节的 DWORD 类型(而不是单独操作每个像素的 R G B 通道),这样将会加快处理速度。这样做以后,如果我们直接把异或后的结果显示出来,效果如下图(提示窗口已经为完全不透明):
直接显示异或结果,很显然图像相同的部分会变为全黑色(因为相同的数据的异或结果为零),不同的位置会显示出一些比较鲜艳的色彩。这样的显示效果其实并不太好,不太容易辨别,所以我还是把结果变为红色。这里有两个问题,导致我得出最终的解决方案。
(2.1)为了让我对两个数据进行异或时,他们的地址能以 DWORD 对齐。所以我对游戏窗口抓图时,把两个图片分别复制到一个上下排列的内存位图(如下,其尺寸为单个图片宽度 * 二倍图片高度),然后对这个内存位图的像素数据进行异或。如果不这样截图,那么直接对游戏窗口原样图片进行操作,由于图片在游戏窗口中的位置对我们来说是“随机”的,那么就无法保证像素数据能正好对齐到 DWORD(这个问题在 24 bpp 位图时存在,但当采用 32 bpp 内存位图时,任何像素都一定会对齐到 DWORD,这个忧虑已经不存在了,但对于提高复制屏幕图像的效率而言,这样截图依然是应该的,因为在两幅图像以外的部分是我们不感兴趣的,也就没必要全部截取)。(注意这里有一个基本可以肯定的前提条件,系统在分配位图时,其像素数据的起始地址一定是对齐到 DWORD 的。)
图片 1 |
图片 2 |
(2.2)最开始我创建的内存位图为 24 BPP,这样一个像素占据 3 Bytes,而一个 DWORD 占据 4 Bytes,所以如果是 24 BPP,即使找到一个操作结果不为零,要定位到像素或者对此像素设置为特定颜色(例如红色)也会比较麻烦(需要做换算)。所以这里我创建内存位图时把 BPP 改为 32,这样每个 DWORD 就是一个像素,这样就能对图片上的差异像素很容易的设为特定颜色,例如红色就是 0x00FF0000 (这个数字书写出来的顺序是,0x-AA-RR-GG-BB,与 little - endian 的存储顺序相反)。使用 CreateDIBSection 创建一个内存位图,代码比较简单。
(2.3)执行异或时,采用多线程处理。如果是单个线程执行这个计算,那么对于图像数据的输入来说必然是串行的。为了发挥当前的多核 CPU 的优势,所以这里把此异或计算采用多线程执行,把图片从上到下切分为几个高度较小的横条(横条的宽度等于图片宽度,个数等于要创建的线程数),每个线程分配到图片的其中一个横条。由于几个横条部分的数据彼此独立,不会发生数据相关性,所以可以直接并行处理,只需要等所有线程任务完成后(WaitForMultipleObjects),再把内存位图刷新到窗口上即可。以下是线程过程代码(像素数据的起始地址在创建位图时已经被转换为 LPDWORD):
备注:这里的任务切分,为从上到下把图像进行细分,一是遍历像素的方便,二是同时 CPU 对“数据局部性”的偏好,这样可以保持比较理想的 cache 命中率。在这个问题中,还可以考虑能否利用上 Intel CPU 的 MMX 和 SSE2 特性,进一步提高处理的并行度,以提高处理效率。(2014年5月4日 补充 -- hoodlum1980)
DWORD WINAPI ThreadEntry(LPVOID lpParameter)
{
int count, i, lineSize;
LPTASK pTask = (LPTASK)lpParameter; //DWORD Per Line;
lineSize = g_AppData.nStrideLayer / sizeof(DWORD); //Total Count of DWORDS;
count = (pTask->nEnd - pTask->nStart) * lineSize; //[End, Start); p1 - Image1; p2 - Image2
LPDWORD p1 = g_AppData.lpBitsLayer
+ (g_Layout.nImageHeight * - - pTask->nEnd) * lineSize; LPDWORD p2 = g_AppData.lpBitsLayer
+ (g_Layout.nImageHeight - - pTask->nEnd) * lineSize; for(i = ; i < count; i++)
{
*p1 = *p1 ^ *p2; //0xAARRGGBB
if(*p1) *p1 = 0x00FF0000;
++p1;
++p2;
}
return ;
}
采用以上方法之后,执行速度有明显提高,在我的台式机,CPU 为 I7, 16GB 内存,操作系统为 WIN7,之前的程序检测一次的执行速度为 260 ms 左右,改进后的程序的执行速度为 15 ~ 16 ms(而且经常为 0 ms,让我觉得不可思议,这里我采用的只是 GetTickCount 相减得到的粗略估算,没有考虑 CPU 调度的影响,并且这个函数本身就对精确性的要求不是很高),速度上改进前大约为改进后的 16 倍。从用户体验角度看,改进前求解有少许延迟感,改进后速度太快感受不到延迟。由于速度太快,所以我放弃了原本想在检测过程中切换通知栏图标的想法。
最后是两个程序的可执行文件的下载链接(采用 VS2005 , C++ 开发):
http://files.cnblogs.com/hoodlum1980/FindItEx_Bin.zip
之后的工作则是模拟点击功能。根据判断结果把差异像素分解成 5 个集合,然后在这些集合所在位置单击一次。这将会进一步加快游戏完成速度。这一步属于相对简单的图像分析处理任务,留待将来考虑。