OpenCV实现微信跳一跳
最近微信上的跳一跳可是非常火,因为自己又对计算机视觉有感兴趣,所有就想写一个程序,实现自动跳一跳。在前段时间因为要考试,所以没有怎么写程序,考试结束了,这几天重新搞了搞,到1.22晚上终于把算法改进地差不多了,写下这篇博客记录一下。
完整的源码在我的github上:https://github.com/myzcl/WeChat-Jump
后续更新都会在Github上。
github上有一个项目已经很成熟了,大家可以去看一看
https://github.com/wangshub/wechat_jump_game
微信现在已经加大反作弊的力度了
adb工具使用
adb配置
adb工具主要是为了与Android手机进行通讯,实现获取屏幕截图和按压屏幕等操作。adb工具包在网上搜一下就可以下载到。如果不想下载的话,在这有网盘链接:
链接:https://pan.baidu.com/s/1mjRVRZy 密码:cz7w
adb工具环境变量的配置。将adb工具报的路径添加到环境变量中,如我的路径是D:\Program Files\ADB\platform-tools-latest-windows\platform-tools,如图:
就将这个目录添加到环境变量中,如图:
adb命令使用详解,在网上找到有个讲的非常好的文章,分享一下:http://blog.csdn.net/u010375364/article/details/52344120
遇到的问题:
在运行指令时,可能会遇到这种情况,出现这种情况的原因是,端口被手机助手给占用了。详情见http://blog.csdn.net/liguilicsdn/article/details/50902194
解决的方法就是打开任务管理器,结束相关进程,如图:
C++调用Python
因为最近在学习python,所以就用Python调用adb命令,再通过C++调用Python,Python的按抓个方法再此就不赘述了,主要讲一下调用Python遇到的问题。
遇到的问题
1.配置问题
配置步骤和VS下配置OpenCV一样,如图:
2.x64配置
因为Python我用的是x64版本,所以在VS中也得选择成x64版本,不然会报错:
3.向Python传参数
C++调用Python一般的方法大家可自行百度,需要注意的是:
一定要设定Python文件的目录,例如我的是: PyRun_SimpleString("sys.path.append('E:/vs2013test/3.py-test/py-test')");
否则会找不到Python文件。
传递参数可参考以下代码:
//按压屏幕
void press(int x1, int y1, int x2, int y2, int dist)
{
Py_Initialize(); //初始化Python
// 检查初始化是否成功
if (!Py_IsInitialized())
cout << "python init failed!" << endl;
PyRun_SimpleString("import os");
PyRun_SimpleString("import sys");
PyRun_SimpleString("sys.path.append('E:/vs2013test/3.py-test/py-test')"); //设定Python路径
PyObject *pModule = NULL;
PyObject *pFunc = NULL;
PyObject *pArg = NULL;
PyObject *pResult = NULL;
pModule = PyImport_ImportModule("py_called"); //找到Python文件
if (!pModule)
cout << "pModule erro!" << endl;
//pFunc = PyObject_GetAttrString(pModule, "add_func"); //找到add_func函数
//pArg = Py_BuildValue("(i,i)", 10, 25); //传入参数 两个整形的参数
//pResult = PyEval_CallObject(pFunc, pArg); //执行函数
pFunc = PyObject_GetAttrString(pModule, "press_value");
pArg = Py_BuildValue("(i, i, i, i, i)", x1, y1, x2, y2, dist);
PyEval_CallObject(pFunc, pArg);
Py_Finalize(); //关闭Python
}
OpenCV算法
检测的思路是这样的:
1.0:因为棋子的形状,大小不发生变化,所以采用的就是简单暴力的模板匹配。关于下一个目标点,原先采用的是Canny算法,找到目标的轮廓,然后通过预估,确定目标点。但是实验了几次以后,感觉效果不是很好,当背景和目标的颜色相近是,就会检测不到边缘,导致目标点不准确。最后采用的是图像的特征进行识别。用这个方法,全自动跳只能跳一百多分。
2.0:通过分析可知,目标的周围都是背景,并且目标一直处于背景最上部,所以可以每一行每一行进行检测,设定阈值,当检测到到从背景到目标,记录这个位置,在此基础上,再检测到由目标到背景,再记录这个位置,最后,两个位置进行求均值。经过实验,这个方法的准确性非常高。
具体代码:
main.cpp
#include <math.h>
#include <iostream>
#include <Python.h>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
//二值化
void mythreshold(Mat &img, uchar T)
{
int n1 = img.rows;
int nc = img.cols * img.channels();
if (img.isContinuous())//判断图像是否连续
{
nc = nc * n1;
n1 = 1;
}
for (int i = 0; i < n1; i++)
{
uchar *p = img.ptr<uchar>(i);
for (int j = 0; j < nc; j++)
{
if (p[j] < T)
p[j] = 0;
else p[j] = 255;
}
}
}
//获取截图
void get_screen()
{
Py_Initialize(); //初始化Python
// 检查初始化是否成功
if (!Py_IsInitialized())
cout << "python init failed!" << endl;
PyRun_SimpleString("import os");
PyRun_SimpleString("os.system('adb shell screencap -p //sdcard//src.png')");
int py_test = PyRun_SimpleString("os.system('adb pull //sdcard//src.png')");
if (py_test != -1)
cout << "获取截图成功" << endl;
else cout << "获取截图失败" << endl;
Py_Finalize(); //关闭Python
}
//按压屏幕
void press(int x1, int y1, int x2, int y2, int dist)
{
Py_Initialize(); //初始化Python
// 检查初始化是否成功
if (!Py_IsInitialized())
cout << "python init failed!" << endl;
PyRun_SimpleString("import os");
PyRun_SimpleString("import sys");
PyRun_SimpleString("sys.path.append('E:/vs2013test/3.py-test/py-test')"); //设定Python路径
PyObject *pModule = NULL;
PyObject *pFunc = NULL;
PyObject *pArg = NULL;
PyObject *pResult = NULL;
pModule = PyImport_ImportModule("py_called"); //找到Python文件
if (!pModule)
cout << "pModule erro!" << endl;
//pFunc = PyObject_GetAttrString(pModule, "add_func"); //找到add_func函数
//pArg = Py_BuildValue("(i,i)", 10, 25); //传入参数 两个整形的参数
//pResult = PyEval_CallObject(pFunc, pArg); //执行函数
pFunc = PyObject_GetAttrString(pModule, "press_value");
pArg = Py_BuildValue("(i, i, i, i, i)", x1, y1, x2, y2, dist);
PyEval_CallObject(pFunc, pArg);
Py_Finalize(); //关闭Python
}
//获取棋子位置
void loca_start(Mat img_src, Mat img_model, Point &point)
{
/************************************模板匹配****************************************/
//创建输出结果的矩阵
int result_cols = img_src.cols - img_model.cols + 1;
int result_rows = img_src.rows - img_model.rows + 1;
Mat result(Size(result_cols, result_rows), CV_8UC1, Scalar(0));
//进行匹配和标准化
int match_method = 3;//选择 匹配的方式
/* CV_TM_SQDIFF = 0, CV_TM_SQDIFF_NORMED = 1, CV_TM_CCORR = 2, CV_TM_CCORR_NORMED = 3, CV_TM_CCOEFF = 4, CV_TM_CCOEFF_NORMED = 5 */
matchTemplate(img_src, img_model, result, match_method);
normalize(result, result, 0, 1, NORM_MINMAX, -1, Mat());
// 通过函数 minMaxLoc 定位最匹配的位置
double minVal; double maxVal; Point minLoc; Point maxLoc;
minMaxLoc(result, &minVal, &maxVal, &minLoc, &maxLoc, Mat());
// 对于方法 SQDIFF 和 SQDIFF_NORMED, 越小的数值代表更高的匹配结果. 而对于其他方法, 数值越大匹配越好
Point matchLoc;
if (match_method == CV_TM_SQDIFF || match_method == CV_TM_SQDIFF_NORMED)
{
matchLoc = minLoc;
//cout << "匹配相似度为:" << minVal * 100 << endl;
}
else
{
matchLoc = maxLoc;
//cout << "匹配相似度为:" << maxVal * 100 << endl;
}
//画出棋子位置
//rectangle(img_src, matchLoc, Point(matchLoc.x + img_model.cols, matchLoc.y + img_model.rows), Scalar(0, 255, 0), 2);
//画出中心位置
Point point_cir = Point(matchLoc.x + (img_model.cols >> 1), matchLoc.y + img_model.rows - 8);
//circle(img_src, point_cir, 5, Scalar(0, 0, 255), -1);
point = point_cir;
//imshow("result", result);
/************************************模板匹配****************************************/
}
//获取目标位置
void loca_next(Mat img_gray, Point point_start, Point &point_next)
{
Rect area_0(0, 0, (img_gray.cols >> 3) * 4, (img_gray.rows >> 2));
if (point_start.x <= (img_gray.cols >> 1)) //棋子在左边
{
//area_0.x = (img_gray.cols >> 3) * 3;
area_0.x = img_gray.cols >> 1;
area_0.y = (img_gray.cols >> 1) + 50;
}
else
{
area_0.x = 10;
area_0.y = (img_gray.cols >> 1) + 50;
area_0.width = img_gray.cols >> 1;
}
////画出下一个目标大体位置
//rectangle(img_src, area_0, Scalar(0, 255, 0), 2);
Mat img_scan = img_gray(area_0).clone();
Point p_node1 = Point(0, 0);
Point p_node2 = Point(0, 0);
Point p_node = Point(0, 0);
bool flag1 = false;
for (int i = 5; i < img_scan.rows; i++)
{
bool flag = false;
Vec3b *p = img_scan.ptr<Vec3b>(i);
for (int j = 0; j < img_scan.cols - 1; j++)
{
int val_l = p[j + 1][0] + p[j + 1][1] + p[j + 1][2] - p[j][0] - p[j][1] - p[j][2];
int val_r = p[j][0] + p[j][1] + p[j][2] - p[j + 1][0] - p[j + 1][1] - p[j + 1][2];
if (val_l > 10 && flag1 == false)
{
//cout << "1:" << val_l << endl;
p_node1.x = j;
p_node1.y = i;
flag1 = true;
}
if (val_r > 10 && flag1 == true)
{
//cout << "2:" << val_r << endl;
p_node2.x = j;
p_node2.y = i;
flag = true;
break;
}
}
if (flag == true)
break;
}
if (p_node1.x != 0 && p_node2.y != 0)
{
//cout << "already" << endl;
p_node.x = (p_node1.x + p_node2.x) >> 1;
p_node.y = (p_node1.y + p_node2.y) >> 1;
circle(img_scan, p_node1, 10, Scalar(255, 0, 0), -1);
circle(img_scan, p_node2, 10, Scalar(255, 0, 0), -1);
circle(img_scan, p_node, 10, Scalar(0), 2);
p_node.x += area_0.x;
p_node.y += area_0.y;
point_next = p_node;
}
imshow("img_scan", img_scan);
}
//计算比例
void dist(Point start, Point next, float &dist_val)
{
float xx = abs((float)next.x - (float)start.x);
float distance = xx / cos(3.1415926 / 6);
//float yy = abs((float)start.y - (float)next.y);
//float distance = xx / sin(3.1415926 / 6);
//int distance = sqrt(pow(next.x - start.x, 2) + pow(start.y - next.y, 2));
dist_val = (float)distance * 2.8419 + 8.4488; //dist_val = (float)distance * 2.8619 + 1.4488;
cout << "距离:" << distance << " 按压时间:" << dist_val << endl;
}
Point point_mouse = Point(0, 0);
void on_mouse(int event, int x, int y, int flags, void *ustc)//event鼠标事件代号,x,y鼠标坐标,flags拖拽和键盘操作的代号
{
if (event == CV_EVENT_LBUTTONDOWN)//左键按下,读取初始坐标
{
point_mouse.x = x;
point_mouse.y = y;
//cout << " 鼠标值已更新" << point_mouse << endl;
}
}
int main()
{
//通过鼠标确定下一位置(半自动)
namedWindow("img_src");
setMouseCallback("img_src", on_mouse, 0);//调用回调函数
int count = 0;
while (1)
{
//产生一定范围内的随机数,模拟手指进行按压,否则得分会被清除
srand((int)time(NULL));
int myrnd_x = (rand() % (800 - 700 + 1)) + 700 - 1;
int myrnd_y = (rand() % (1500 - 1400 + 1)) + 1400 - 1;
//得到手机截图
get_screen();
Mat img_src = imread("E:\\vs2013test\\3.py-test\\py-test\\src.png");
resize(img_src, img_src, Size(img_src.cols >> 1, img_src.rows >> 1)); //540, 960
//载入棋子模板
Mat img_model = imread("E:\\vs2013test\\3.py-test\\py-test\\img_model.jpg");
resize(img_model, img_model, Size(img_model.cols >> 1, img_model.rows >> 1));
//模板匹配得到棋子位置
Point point_start;
loca_start(img_src, img_model, point_start);
circle(img_src, point_start, 5, Scalar(0, 0, 255), -1);
Mat img_gray;
img_src.copyTo(img_gray);
//cvtColor(img_src, img_gray, COLOR_BGR2GRAY);
//边缘检测得到目标位置
Point point_next;
loca_next(img_gray, point_start, point_next);
circle(img_src, point_next, 5, Scalar(0, 255, 0), -1);//画出下一个目标点
/*通过鼠标获取位置*/
//Point point_next;
//point_next = point_mouse;
//circle(img_src, point_next, 5, Scalar(0, 255, 0), -1);//画出下一个目标点
imshow("img_src", img_src);
waitKey(10);
//起始点和目标点都存在,则发送指令
if (point_start.x != 0 && point_start.y != 0 /*&& point_next.x != 0*/)
{
cout << "start: " << point_start << " next: " << point_next << endl;
float dist_val = 0;
dist(point_start, point_next, dist_val);
//发送指令按压屏幕
press(myrnd_x, myrnd_y, myrnd_x, myrnd_y, (int)dist_val);
count++;
cout << "跳的次数:" << count << endl;
cout << endl;
////鼠标位置清零
//point_mouse.x = 0;
//point_mouse.y = 0;
char key = waitKey(1000);//一定的延时
}
//waitKey(0);
//if (key == 27)
// break;
}
//system("pause");
return 0;
}
下图就是用程序跳一跳的成绩截图:
总结与反思
1.没做检测游戏结束的处理。今天早上试了一下,用的也是模板匹配,效果不好,就放弃了。
2.还没能准确检测到目标的准确位置,现在的检测方法只是检测到了差不多准确的x方向的位置,对y方向还没有什么好的办法。
如果大家有什么更好的算法,欢迎一起来讨论。
QQ: 1570553025
github: https://github.com/myzcl
CSDN: http://blog.csdn.net/qq_36327203
扫描二维码即可关注公众号: