50

JS写小游戏「跳一跳」外挂之Canvas图像识别

 5 years ago
source link: http://js8.in/2017/08/26/JS写小游戏「跳一跳」外挂之Canvas图像识别/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

17年结尾的时候微信发布新版重点推出了「小游戏」概念,H5的游戏再次火了起来,新版微信开屏的游戏就是「跳一跳」游戏可玩度很高,网上也出现了各种语言版本的外挂,前几天看到一篇用nodejs搭建的外挂,需要手动点击截屏图片来判断当前和下一步的位置然后跳转,于是就起了用Canvas来实现图像的想法,后面有实现了自动跳转,算是齐活了。今天来完整说下图像识别。

代码都放到了: https://github.com/ksky521/wechat-jump-game-hack 欢迎自己去尝试

先来看最终效果视频: https://v.qq.com/x/page/o1331igmskh.html

Canvas图像处理的原理

Canvas可以通过 drawImage 在上面添加图片,然后通过 getImageData 方法获取一个 imageData 对象,此对象包括了 datawidthheight ,其中data为图片width height 4长度的数组,每个像素点表现在数组内为:RGBA四个0~255的值,即Red、Green、Blue和Alpha值。

通过对这个 imageData.data 进行遍历操作,可以利用图像差值比较找出图片内物体的边缘、物体的中心点,也可以根据图像中某个固定颜色范围的物体,进行匹配,从而找到「小人」的位置。

颜色值差值比较函数

先介绍一个函数 tolerenceHelper ,用来比较颜色差值,即传入需要比较的 rgb ,然后跟对比的 rtgtbt 和差值范围的 t 进行对比的函数,在范围内则返回 true

function tolerenceHelper(r, g, b, rt, gt, bt, t) {
    return r > rt - t && r < rt + t 
            && g > gt - t && g < gt + t 
            && b > bt - t && b < bt + t;
}

获取小人当前位置

小人获取位置用的方式是差值比较,首先通过截屏中的紫色小人颜色范围,可以大致拿到小人的颜色值为:

// 小人的颜色值
const playerR = 40;
const playerG = 43;
const playerB = 86;

这个值可以从小人的底部中心取色得到。所以找到小人的底部中心点的方式就是,在一定范围内(即 tolerenceHelper 的t参数,这里取值为16)查找,如果像素点rgb在这个范围内,则加入待选,最后像素点集合中最低点(最大y)的位置就是小人底部中心所在点的y,x为最大和最小宽度的中心位置。为了好理解,我画了图:

2Mby63R.png!web

下面三图是 t=16t=26t=36 分别识别的效果,为了便于分辨,我将匹配到的像素点颜色都设置为了红色(rgb=255,0,0)。

Ef2QBnb.png!webi6R77ze.png!webBNJ7naa.png!web

为了准确,防止相近颜色的干扰 t=16 就够用了,这样小人的底部位置 pos 就得到了:

// x, y
pos[0] = Math.floor((maxX + minX) / 2);
pos[1] = maxY;

优化

很容易一眼就看出来小人不能在图片的顶部和底部,而是在画面的中心区域范围内,所以可以直接从图片高度的 height/4 ~ height*3/4 的范围内查找,这样可以提高不必要的工作量。

完整查找小人点代码如下

function getCurCenter(data, width, height) {
    // 小人的颜色值
    const playerR = 40;
    const playerG = 43;
    const playerB = 86;

    let minX = Infinity;
    let maxX = -1;
    let maxY = -1;
    // 找到小人当前的底部位置
    let pos = [0, 0];

    let startY = Math.floor(height / 4);
    let endY = Math.floor(height * 3 / 4);
    for (let x = 0; x < width; x++) {
        for (let y = startY; y < endY; y++) {
            let i = y * (width * 4) + x * 4;
            let r = data[i];
            let g = data[i + 1];
            let b = data[i + 2];
            if (y > pos[1] && tolerenceHelper(r, g, b, playerR, playerG, playerB, 16)) {
                minX = Math.min(minX, x);
                maxX = Math.max(maxX, x);
                maxY = Math.max(maxY, y);
            }
        }
    }
    pos[0] = Math.floor((maxX + minX) / 2);
    pos[1] = maxY;
    // console.log(`player position (x, y)= (${pos[0]}, ${pos[1]})`);
    return pos;
}

获取的跳转位置

怎样获取小人下一步跳转的位置呢?

按照上面的逻辑,我们还是从图片高度的 height/4 ~ height*3/4 的范围查找,这是我们先取出当前的背景色,然后在高度范围内扫描图片,当出现跟背景色相差很大的第一个点时,这时候就是下一个物体的主颜色值了!如果为四边体之类的,则这个点就是顶点了!

知道这个物体的主体颜色值,我们就可以以这个值为基准继续扫描,在这个颜色值范围的像素点就是物体的顶面,然后根据顶面像素点坐标 minYmaxY 得到中心点的坐标(圆形和正方形都是对称的,所以都可以用这个方法)。

看图理解下:

uqYV3y6.png!web

下图是将背景色涂红,这样就可以看到识别出来的第一个点就是顶点(圆形一样)

a2mINj2.png!webm26JzuE.png!web

优化

height/4~Math.min(height*3/4, 小人中心Y)
break

完整代码

function getNextCenter(data, width, height, y = -1) {
    let startY = Math.floor(height / 4);
    let endY = Math.floor(height * 3 / 4);

    // 去除背景色
    let startX = startY * width * 4;
    let r = data[startX],
        g = data[startX + 1],
        b = data[startX + 2];
    let maxY = -1;
    let apex = [];
    let pos = [0, 0];
    // 保证从当前小人位置底部点往上
    endY = Math.min(endY, y);
    let endX = width;
    let gapCount = 0;
    for (let y = startY; y < endY; y++) {
        let find = 0;
        for (let x = 1; x < endX; x++) {
            let i = y * (width * 4) + x * 4;
            let rt = data[i];
            let gt = data[i + 1];
            let bt = data[i + 2];
            // 不是默认背景颜色
            if (!tolerenceHelper(rt, gt, bt, r, g, b, 30)) {
                if (apex.length === 0) {
                    if (!tolerenceHelper(data[i + 4], data[i + 5], data[i + 6], r, g, b, 30)) {
                        //椭圆形找中心,往后找30个像素点
                        let len = 2;
                        while (len++ !== 30) {
                            i += len * 4;
                            if (tolerenceHelper(data[i + 4], data[i + 5], data[i + 6], r, g, b, 30)) {
                                break;
                            }
                        }
                        x += len;
                    }
                    //找出顶点
                    apex = [rt, gt, bt, x, y];
                    pos[0] = x;
                    // 减少循环范围
                    endX = x;
                    break;
                } else if (tolerenceHelper(rt, gt, bt, apex[0], apex[1], apex[2], 5)) {
                    //存在顶点了,则根据颜色值开始匹配
                    maxY = Math.max(maxY, y);
                    find = x;
                    break;
                }
            }
        }
        if (apex.length !== 0 && !find) {
            gapCount++;
        }
        if (gapCount === 3) {
            break;
        }
    }
    pos[1] = Math.floor((maxY + apex[4]) / 2);
    // console.log(points_top, points_left, points_right);
    console.log(`next position center (x,y)=${pos[0]},${pos[1]}`);
    return pos;
}

最后

在整个测试的过程中,还尝试了其他的方式,比如先将边缘找出再找中心点,各种尝试,想练手的可以直接改下看看。

为了调试方便,我将这部分代码单独一个router,可以直接github clone下来代码访问 localhost:3000/test ,然后边改边尝试边看效果。

本篇文章介绍了怎么识别出来图片中「小人」和下一个跳转的位置,下一篇介绍下怎么让「小人」自动跳转过去,敬请期待。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK