找茬外挂制作
找茬游戏大家肯定都很熟悉吧,两张类似的图片,找里面的不同。在下眼神不大好,经常瞪图片半天也找不到区别。于是乎决定做个辅助工具来解放一下自己的双眼。
一、使用工具
- Qt:主要是用来做界面的
- OpenCV: 用于图像处理
- C++: 基本实现语言
Qt中OpenCV的配置在【QT】OpenCV配置中讲过了,不会配置的可以看看。
二、实现方案
我要做一个通用的找茬辅助工具,即可以在所有PC找茬游戏中使用。这意味着我们不能通过获取游戏窗口句柄来定位游戏界面。那怎么办呢?灵光一闪,我想到了截图。把有游戏画面的区域截下来,再根据图片的内容作分析,切割出两张画面,进行对比。
实现界面和使用过程中的效果如下图:
可以看到,有四个按钮。
锁定按钮:是用来截取游戏画面位置和找出不同点的。当点击了锁定后,用鼠标像截图一样截取游戏的图片,就像第二张图那样,覆盖住两个待对比的画面。之后软件自动分析图片位置,实现切割查找。结果如第三张图显示的那样。
找茬按钮:大家会觉得,锁定按钮都可以找出不同了为什么还有一个找茬按钮。主要是因为,锁定中截取游戏画面太过耗时了,经常要瞄准半天,要是每张图都这么截取窗口会让人有点烦。好在一般游戏过程中,我们基本不会改变游戏窗口的位置。所以,如果我们之前锁定过了,就不需要再再次截取位置了,只要根据之前的位置来截取就好了。所以找茬按钮就是根据之前的图片截取位置来获取游戏画面,然后分析给出结果的。当然,如果说窗口位置变化了,那么就只好再次用锁定按钮了。
帮助、退出:就是使用说明和退出功能。
2.1 图片截取
图片截取我自己之前也没做过,于是从网上找了一个截图的源码。但具体来源不记得了...截图的大体思路是:首先,截取全屏,用一个Label全屏显示这张截图,再用事件过滤器在这张大截图上截取小块区域。
全屏获取:Qt中有一个QScreen类,里面的grabWindow函数可以实现截取全屏的功能。
void MainWindow::grapWindowScreen() { if (!fullScreenLabel) { fullScreenLabel = new QLabel(); } //获取全屏截图fullScreenPixmap,并将其放入fullScreenLabel QScreen *screen = QGuiApplication::primaryScreen(); fullScreenPixmap = screen->grabWindow(QApplication::desktop()->winId()); fullScreenLabel->setPixmap(fullScreenPixmap); //label全屏显示 fullScreenLabel->showFullScreen(); }
获取区域截图:采用事件过滤器。其中利用了QRubberBand类(橡皮筋窗口)来实现区域选取中的掩膜版的功能。事件过滤器我自己也是第一次用,感觉挺强大的。
//事件过滤器,在全屏的截图上截取想要的部分 bool MainWindow::eventFilter(QObject *o, QEvent *e) { if (o != fullScreenLabel) { return MainWindow::eventFilter(o, e); } QMouseEvent *mouseEvent = static_cast<QMouseEvent*> (e); //true 鼠标左键按下且按键还未弹起 if ((mouseEvent->button() == Qt::LeftButton) && (mouseEvent->type() == QEvent::MouseButtonPress)) { //鼠标左键标志位按下 leftMousePress = true; //获取鼠标点 origin = mouseEvent->pos(); if (!rubberBand) { rubberBand = new QRubberBand(QRubberBand::Rectangle, fullScreenLabel); } rubberBand->setGeometry(QRect(origin,QSize())); rubberBand->show(); return true; } //true 鼠标左键按下并拖动 if ((mouseEvent->type() == QEvent::MouseMove) && (leftMousePress)) { if (rubberBand) { rubberBand->setGeometry(QRect(origin, mouseEvent->pos()).normalized()); } return true; } //鼠标左键松开 if ((mouseEvent->button() == Qt::LeftButton) && (mouseEvent->type() == QEvent::MouseButtonRelease)) { //鼠标标志位弹起 leftMousePress = false; if (rubberBand) { //获取橡皮筋框的终止坐标 termination = mouseEvent->pos(); rect = QRect(origin, termination); rubberBand->hide(); //把橡皮筋窗口隐藏 //根据橡皮筋框截取全屏上的信息,并将其放入shotScreenLabel shotScreenPixmap = fullScreenPixmap.grabWidget(fullScreenLabel, rect.x(), rect.y(), rect.width(), rect.height()); shotScreenPixmap.save("tmp.jpg"); //不会QPixmap转QImage或Mat只好出此下策 fullScreenLabel->hide(); findDifference(); //找不同的处理过程 if(!bNeedLocation) //不需要重新锁定表示结果有效,这时显示结果 { //将shotScreenLabel的用户区大小固定为所截图片大小 shotScreenLabel->setFixedSize(showDifferenceImage.width(), showDifferenceImage.height()); shotScreenLabel->setPixmap(QPixmap::fromImage(showDifferenceImage)); shotScreenLabel->show(); } } return true; } return false; }
2.2 图片定位
我们截取的图片基本上是下面的样子:
区别就是图片是横向排列还是纵向排列的。两张图片在水平或者垂直维度上像素点是对应的。我们先假设图片是两张横向排列的(就像第一张图一样),如果遇到了纵向排列的,我们把图旋转90即可。
图片定位分下面几个步骤:
1. 找到两张图片对应列的横向距离
2. 找到两张图片的左右界限
3. 找到两张图片的上下界限
2.2.1找到两张图片对应列的横向距离
就是如图所示的距离
方法是在图片的左半部分找N条线,我取的是在图片从左边开始的1/10到4/10的范围内以均匀的间隔取8条线。然后从4/10到图片最右边依次扫描每一列像素,找到像素点差异最小的那一列作为相应的匹配位置。两个列坐标相减就是所求的距离。取8条线是为了增加容错的能力。在取位置时,我们用出现次数最多的那个距离。
注意:再判断两个像素是否相同时要考虑噪声。我这里认为三个通道的像素差异加起来超过10的才算是不一样的点。
查找匹配线的代码:
//查找垂直的匹配线 void findFitLineVertical(Mat src, int y, int& fit_y) { fit_y = y; //初始化为y Mat src2; src.copyTo(src2); int different_num = src.size().height; for(int c = src.size().width * 0.4 + 1; c < src.size().width; c++) //遍历垂直方向的线条 { int different_num_tmp = 0; for(int r = 0; r < src.size().height; r++) //遍历线条上的每个点 { int d = 0; for(int k = 0; k < 3; k++) { d += abs(src.at<Vec3b>(r, c)[k] - src.at<Vec3b>(r, y)[k]); } if(d > 10) //很关键,要允许有少量的噪声 否则误差很大 different_num_tmp++; } if(different_num_tmp < different_num) { different_num = different_num_tmp; fit_y = c; //line( src2, Point(c, 0), Point(c, src.size().height - 1), Scalar(0,255,0), 3, 8 ); //imshow("pic2", src2); //cvWaitKey(15); } } return; }
获取相应距离的代码:
const int N = 8; //需要匹配的线条数 int s = src.size().width *0.1; //测试线条的开始列 int e = src.size().width * 0.4; //测试线条的结束列 int gap = (e - s + 1) / (N - 1); //测试线条取样间隔 int dis = 0; //两幅图的横向间隔 int disnum = 0; for(int i = 0; i < N; i++) { int dis_tmp = 0; int y = s + i * gap; //line( src2, Point(y, 0), Point(y, src.size().height - 1), Scalar(255,0,0), 3, 8 ); //imshow("pic1", src2); int fit_y; findFitLineVertical(src, y, fit_y); //下面这段通过典型的找数组中出现超过一半的数字的算法来找两幅图的横向间隔 dis_tmp = fit_y - y; if(disnum == 0) { dis = dis_tmp; disnum++; } else { if(dis_tmp == dis) disnum++; else disnum--; } //printf("%d, %d, %d ", y, fit_y, fit_y - y); //line( src2, Point(fit_y, 0), Point(fit_y, src.size().height - 1), Scalar(0,255,0), 3, 8 ); //imshow("pic1", src2); //cvWaitKey(0); }
2.2.2找到两张图片的左右界限
有了对应点距离后,找左右界限就容易多了。左边界就从图片的最左边开始扫描,看看理论的对应位置上不用的点是否超过了阈值,超过了就表示这不是图片的起始位置,继续扫描下一列,直到找到左边界。右边界同理,就是从右往左找。
2.2.3找到两张图片的上下界限
有了左右界限后,找上下界限也容易了。找上边界时把图片1左右边界范围内的像素和相应行图片2左右边界范围内的像素做比较,相同点超过阈值就表示是对应的边界。下边界同理。
注意:这种找出来的边界可能比实际的图片大一些,不过不影响结果。
2.2.4当图片方向不对时旋转图片
我们第一次找左右边界时,如果没有找到匹配线,这时我们就旋转图片再次找左右边界。如果还没有可能就是图截取的不好了。这时候就需要重新锁定了。
// Load an image Mat srcRGB = imread("tmp.jpg"); Mat srcRotate; //imshow("src", srcRGB); bool bRotate= false; //图片是否经过旋转 //int left1, left2, right1, right2, up, down; bool b1 = findLeftRightBoard(srcRGB, left1, right1, left2, right2); if(!b1) //第一次没有找到匹配线 有可能是因为两张图是上下存放的 我们把图片旋转一下 再试试 { srcRotate = rotateImage(srcRGB); srcRGB.release(); srcRotate.copyTo(srcRGB); bRotate = true; } b1 = findLeftRightBoard(srcRGB, left1, right1, left2, right2); bNeedLocation |= (!b1); if(bNeedLocation) //如果两个方向都失败了 需要重新锁定 { QMessageBox::warning(this, tr("error"), tr("Please locate again! 图片不理想,请重新锁定!")); return; } findUpDownBoard(srcRGB, left1, right1, left2, right2, up, down);
2.2.5图片类型转换
在上述处理过程中需要各种图片类型的转换。抓屏获取的是QPixmap类型,图片处理用的是Mat类型,图片显示用的是QImage类型。
QPixmap转Mat : 在网上找了很久都没找到。只好先把图片保存在当前目录,再用Mat类型读出。
Mat转QImage: 我直接在网上找的代码
QImage const copy_mat_to_qimage(cv::Mat const &mat, QImage::Format format) { QImage image(mat.cols, mat.rows, format); for (int i = 0; i != mat.rows; ++i) { memcpy(image.scanLine(i), mat.ptr(i), image.bytesPerLine() ); } return image; } QImage const mat_to_qimage_cpy(cv::Mat &mat) { if(mat.type() == CV_8UC3) { cv::cvtColor(mat, mat, CV_BGR2RGB); return copy_mat_to_qimage(mat, QImage::Format_RGB888); } if(mat.type() == CV_8U) { return copy_mat_to_qimage(mat, QImage::Format_Indexed8); } return QImage(); }
2.3查找不同
当我们得到两张待对比的图片后,我们通过逐点对比获取不同点。把两张图片像素点差异超过30的取出来,做二值化。然后腐蚀膨胀去噪声。最后通过连通区域提取来把不同的区域取出并画出来。
这部分直接看代码非常直接:
Mat mSub = abs((img1 - img2)) > 30; Mat gray;
cvtColor(mSub, gray, CV_RGB2GRAY); gray = gray > 0; erode(gray, gray, Mat()); dilate(gray, gray, Mat()); erode(gray, gray, Mat()); dilate(gray, gray, Mat()); //连通区域提取相关 vector<vector<Point> > contours; vector<Vec4i> hierarchy; //连通区域提取 findContours(gray, contours, hierarchy, CV_RETR_CCOMP , CV_CHAIN_APPROX_NONE ); for(int i = 0; i < contours.size(); i++) { if(contours[i].size() >= 4) // 点数大于等于 的连通区域才考虑 { Rect r = boundingRect(contours[i]); rectangle(img1, r, Scalar(255,0,255), 2); //画出连通区域 } }//结果转换为QImage if(!bRotate) showDifferenceImage = mat_to_qimage_cpy(img1); else //如果旋转过,需要再转270恢复原来的方向 { img1 = rotateImage(img1); img1 = rotateImage(img1); img1 = rotateImage(img1); showDifferenceImage = mat_to_qimage_cpy(img1); }
2.4界面美化
在做的时候我争取做的漂亮一点。Qt的界面加图片很容易,所以我就找或者制作了一些图片。效果在上面显示过了。
背景图片是在网上找的。
按钮的字是在随便找的字体网站上做的http://www.diyiziti.com/katong。做了几个版本。本来打算做按钮点击时候效果的,后来懒得做了,只用了其中一版。
我自己还设计了个中国风的Logo:
“茬”就表示找茬游戏,Help表示辅助工具,整体的意思就是找茬辅助工具。“茬”字跟上面一样都是网站声成的。毛笔圈是我在百度上找的图片,然后用美图秀秀抠图把毛笔圈扣成透明的,加在“茬”字外面。HELP在美图秀秀上的文字添加上去。
程序左上角的图标可以通过在cpp文件中加入下面语句实现:
setWindowIcon(QIcon(":/pic/chaH.jpg"));
程序的exe图标需要先自己制作一个ico图标文件,可以用iconEditor软件制作。然后把图标放在工程目录下面,再在pro文件里加入
RC_ICONS = chaH2.ico
其中chaH2.ico是自己的图标名称。
三、实现过程中的问题
3.1截图过程中的问题
QT自带的截图似乎噪声很多,也许是我保存成图片再读取的过程中像素发生了变化。之前实验的时候用的是QQ截图,然后做处理,噪声很少,效果也不错。改成QT的截图后效果变差了很多,为了容错不得不增大阈值。结果导致有些图片区别被当做噪声去掉了。
3.2阈值选取问题
多大的阈值合适?这个可能需要进一步分析。就拿下面的图来说,里面有5处不同,那一缕头发,一个手纹的真心跟噪声似的。阈值调小了,这里面的差异倒是出来了,可是换成别的图就是一堆噪点。这里顺带说一句,QQ的大家来找茬,不同时一块一块的,即图片基本都是整块不同,比较好找。而网上其他杂七杂八的,很可能使用photoshop在某个边角抹了抹,很难识别。
3.3图片截取的失败方案:Hough变换
Hough变换是用来检测图片中直线的。我开始想,直接用霍夫变换提取直线,然后找相同大小的区域就可以了。可是实现后我发现霍夫变换的效果很差,那些很明显的直线都不能完整得到。
3.4定位窗口
开始想做针对QQ大家来找茬的外挂。我用按键精灵确定了游戏窗口的名字是“大家来找茬”但是我在QT上通过这个名字却无法获取有效的窗口。为什么呢?
实验时的代码如下,有人帮我看看么?
#include <windows.h> #include "mainwindow.h" #include <QApplication>int main(int argc, char *argv[]) { QApplication a(argc, argv); MainWindow w; w.show(); HWND p = FindWindowA(NULL, "大家来找茬"); qDebug("p = %d ",(int)p); // 获取屏幕鼠标坐标 POINT pt; GetCursorPos(&pt); qDebug("%d %d ",pt.x,pt.y); HWND h = WindowFromPoint(pt); qDebug("%d ",h); //wchar_t text[200]; //GetWindowText(h,text,200); //qDebug("%s ",text); return a.exec(); }
四、应用程序即源码下载
http://yun.baidu.com/share/link?shareid=782947870&uk=2757788903