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
扫描二维码即可关注公众号:
这里写图片描述

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐