最近和小徒弟玩QQ游戏中的“美女找茬”,这个游戏也就是给你两幅差不多的图片,让你找出几个不同的地方(一般是五个)。可惜我老眼昏花比较反应迟钝,总是输,被小徒弟取笑。不禁一时心血来潮,既然作为普通玩家赢不了,何不...!于是我琢磨了一下,不过就是两幅图片比较一下吗,对计算机来说当然很简单。也不需要考虑什么算法。
所以我就做了这样一个小程序,纯属贪图好玩。我首先找到游戏窗口,然后把这个窗口“截屏”下来,在内存里判断两幅图片的不同之处,然后把结果输出到一个半透明窗口上,并且把这个半透明窗口准确的覆盖到左侧图片上。这个半透明窗口的背景是一个白色矩形,两幅图不同的地方用红色填充出来。
判断两幅图片之前,我们做一些准备工作。先用SPY++查询出游戏窗口的窗口类和窗口标题。我在 Photoshop 里精确测量了两幅图在窗口中的起始坐标和长度宽度,很庆幸的一点是在游戏里这些值是固定的!现在为了简单起见,我们先把这些值 hard code 在程序里(如果考虑更周到,我们应该把这些信息存到一个 ini 配置文件里)。
那么截图并且找到不同处的代码如下所示。为了考虑容错性,我还尝试了以 3*3 和 2*2 的像素矩形块为基本单位进行检测,但经过测试后我发现意义不大,实际上仅对单个像素进行检测就足够了,所以下面的代码就是逐个像素检测。
这个函数的效率不是很高,检测不同的时候需要等大概一秒钟的时间(我的电脑配置是 2.67GHz * 2 CPU, 3.24G 内存)。我想如果改进为直接用指针访问位图的数据块会比这种方法的效率好很多,所以它还可以改进,不过目前的处理时间对于我来说也还是可以接受的。
然后我们需要给用户一个接口去调用上面的函数,因此我在通知栏(Tray:系统托盘)放置了一个图标,只要用户用左键单击通知栏图标,就会调用上面的函数,也就是执行一次查找,并把半透明窗口和游戏窗口进行对齐。通过鼠标右键,可以选择显示或者隐藏半透明窗口。
【注意】如果游戏窗口被其他窗口遮挡,或者游戏窗口有屏幕以外,或者游戏窗口上有动态元素(例如游戏的倒计时提示等),请注意这时候获取到的窗口截图是有问题的,那么查找结果也会出现不准确的情况。
如图所示,我在左侧图上叠加了一个半透明窗口,它只是一个普通对话框(上面什么控件也没有),当然我们还需要把这个半透明窗口设置成顶层窗口,还要使其“鼠标穿透”,也就是说它自身不想接收鼠标事件,而是让鼠标“穿透”它传送给其下面的窗口,这是通过设置窗口样式来完成的。在初始化对话框时,我们用下面的代码即可:
【注意】VC6.0提供的PLATFORM SDK(1998年的)并不支持图层窗口相关的API,因此编译时会提示找不到相应函数。要正确编译,解决方法是在 winuser.h 文件中补充相关的定义,并用高版本的Visual Studio(例如VS2003, VS2005 )中的 user32.lib 覆盖VC中的相应文件。
我增加了一个设置对话框如下图所示,用于设置 FindDifference 函数中使用的全局参数 m_Threshold 的值。可以修改这个值的大小然后观察这个值的大小对输出结果中红色区域形状的影响,这个值越小,则输出结果的红色区域越接近为“矩形”(这个形状主要是基于游戏中使用的图片),即对差异的检验越严格。这个值越大,则输出结果的红色区域会产生收缩,使其更接近差异的“实际形状”,即使“容许误差”增加(把灰度变化较小的部分过滤,仅标示灰度差异明显的地方)。当然这个值如果设置的过大,则红色区域会减少到“完全消失”。
这里是源代码的下载链接:(于 2014 年 2 月 17 日 被我撤除)
http://files.cnblogs.com/hoodlum1980/FindIt.rar
最后我要特别提示的是,游戏的本质是娱乐,不要为了追求浮云而失去游戏的本意。
【补充】by hoodlum1980 @ 2011-11-19
其实对于这个工具来说,仅仅是比较两幅位图,找出不同之处。其实就很简单了,我们只要两张图的位图数据块做异或就可以得到差异结果,相当于在 photoshop 中的图层模式设置为“差值”,就可以看到两个图层之间的差异。大致方法如下:
为了加快效率,用 uint32* 类型的指针,分别指向两个位图的数据块。一般我们截图的结果是 bpp = 24,也就是说每 3 个字节为一个像素,但我们依然可以每 4 个字节一组进行异或,异或结果就是结果(同样,可以对异或结果不为0的结果像素用显著颜色标识),把结果图片呈现出来就达到和本文中相同的效果。这种方法会比我之前实现的方式速度快很多。
但如果我们要推算出像素位置,则还需要进行一次换算,假设截图为 24 bpp,则数据排列如下:
| 0 | 1 | 2 |
|B G R B|G R B G|R ...
| 0 | 1 | .....
设 pDest 为结果图片的数据块指针,pSrc1和pSrc2分别是要比较的图片(注意实际上可使用同一个位图,只是水平方向上有不同偏移的定位)
*pDest = *pSrc1 ^ *pSrc2;
设 uint32* p0 为数据块起始点:假设在 p1 处发现不为 0,则距离起始点的距离是:
(p1 - p0) * sizeof(uint), 即 (p1-p0)*4;
然后换成成像素坐标:
stride = (width * bpp + 31)/32 * 4;
y = (height - 1) - (p1-p0)*sizeof(uint) / stride; //如果扫描行为逆序
x = (((p1 - p0) * sizeof(uint)) % stride) / 3;
注意实际上这里涵盖了两个相邻像素(x,y)和(x+1,y)组成的(由于数据块用int32对齐,所以一定在同一扫描行内),可能是 BGR|B, GR|BG, R|BGR 三种情况之一:所以我们还应该分析除以 3 以后的余数。
令余数 k = (((p1-p0)*sizeof(uint)) % stride) % 3;
k = 0: BGR|B (x, y) , (x+1, y)的B
k = 1: GR|BG (x,y)的GR, (x+1,y)的BG
k = 2: R|BGR (x,y)的R, (x+1,y)
如果我们需要更精确的知道到底似乎是那个像素不同导致的,我们还需要对这四个字节扫描一下,然后按照上诉情况分析不同的值位于(x,y)还是(x+1,y)。当然一般来说如果是在差异区域内部实际没必要区分。
【对以上补充的补充说明:】
由于我已经采用更佳方法(参见:http://www.cnblogs.com/hoodlum1980/p/3536444.html),所以以上补充说明已不再有效。-- hoodlum1980, on 2014-2-17。