南宁鼠标价格社区

如何用 Selenium + OpenCV 来玩 H5 小游戏

只看楼主 收藏 回复
  • - -
楼主

Round 1

前些天,有个无聊的哥们,给我发了这个小游戏,要跟我比眼力,说他的最好成绩是 22 次:

http://h5.gaoshouyou.com/game/44.html

(在众多凤姐中找到唯一的蔡依林,总共时长为 1 分钟,看能找到多少次)

这只是第 4 关,到后面十几关的时候会出现丧心病狂的 80 个凤姐和 1 个蔡依林。

嗯,这种瞎狗眼的小游戏能难得倒我吗?

先正常开始玩游戏,再在游戏页面上右键点击『检查』,然后就可以发现:

很明显可以看出 2.png 只有 1 个,其他都是 1.png,这就好办了,我们用 Selenium 来写个自动化脚本。

思路很简单:循环找到 id 为 box 的 div,然后找 box 里面所有的 span;然后再对这些 span 做一个循环,找到 style 属性里有 "2.png" 的 span,直接点击,如下:

搞定。来跑一下:

额,一开始连图都没加载完就点击了...秀下最后的成绩:

看到我的 96 次,该哥们已吐血 ...

Round 2

第二天这无聊哥们又来找我比手速:

http://h5.gaoshouyou.com/game/34.html

这个游戏非常简单,就是点击一个个逐渐出现的黑色块。

打开开发者工具后,发现这是个典型的 HTML5 canvas:

这就没那么简单了,因为我们没有 webelement 来操作,只能通过计算机视觉找到黑块才能执行点击。我们分解下步骤:

  • 将 canvas 转换成图片;

  • 找出图片中的黑块;

  • 执行点击。

首先我们来解决下如何将 canvas 转换成图片的问题。

稍微接触过 HTML5 canvas 的同学可能都知道,在 Javascript 里,canvas 对象有个 toDataURL() 函数可以将当前 canvas 的内容转成 base64 格式。而 Selenium webdriver 提供了 execute_script 方法可以在页面上直接执行 js 脚本。

get_base64_script = '''var canvas = document.getElementById("linkScreen"); return canvas.toDataURL().substring(22);'''
img_base64 = driver.execute_script(get_base64_script)

这样我们就拿到了 canvas 的 base64 格式图片,然后我们将 base64 解码然后用 numpy 和 cv2 来转换成 numpy array 格式的图片:

img_decode = base64.b64decode(img_base64)
img_array = np.fromstring(img_decode, np.uint8)
img = cv2.imdecode(img_array, cv2.COLOR_BGR2GRAY)

效果如下:

接着使用 cv2 进行图片处理,先使用 threshold 函数将图片转成黑白,再使用 morphologyEx 函数尽可能去掉一些噪点:

img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
kernel = np.ones((10,10), np.uint8)
ret, thresh = cv2.threshold(img_gray, 60, 255, 0)
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel)

到这里本来是想用 cv2 的轮廓检测函数 (findContours 以及 boundingRect 函数) 找出长方形,比如这是我在另外一个小游戏里测试的结果,能准确地找到方块,如下图绿色框中的轮廓:

但是后来发现我想复杂了。

其实整个 canvas 分成了 4 行 4 列,在游戏的经典模式里,每点击完一次黑块,画面会依次下降然后定格,所以我们需要点击的必然在都是在第 3 行,我们只要检查第 3 行就可以了。

这里我就偷懒了,取出第 3 行每个格子中间的 40 x 40 个像素,判断是否都为黑色 (即 0) 即可,使用 numpy.where 函数非常高效:

def analy(img):
    x = None
    centers = [(x, 300) for x in range(40, 320, 80)]
    for center in centers:
        x, y = center
        block = img[280:320,x-20:x+20]
        rows, _ = np.where(block==0)
        if rows.shape[0] == 1600:
            return x

可以看到模拟点击的点 (绿色圆圈) 是正确的:

剩下的问题就是用 Selenium 如何点击 canvas 的问题了。

Selenium 里有 ActionChains 类,可以记录一系列的鼠标、键盘动作,如:

ActionChains(driver).move_to_element_with_offset(canvas, 120, 300).click().perform()

上面的代码就是移动到 canvas,然后在 canvas 的 (x=120, y=300) 位置处点击。这刚好是我们所需要的!

不过这里有个坑,使用 Chrome 的 webdriver 大多数情况下没有反应,或者是点击的位置不对,折腾了好久始终解决不了。

后来索性换了 Firefox,却能稳定点击。看了下 Selenium 源码:

发现 Firefox 和 Chrome 的 webdriver.w3c 是不同的,Firefox 是 True,而 Chrome 为 False,应该就是这个原因了。

好了,将所有代码串起来,执行,最终效果如下:

可以看到鼠标在左上方一直没有动的。

不过呢,程序这么点虽然不会出错,但是速度上来说,还是比人类手速略慢的,其实还有改进的地方,比如一次检测三行。不过我就懒得搞了。

最后无聊哥们以 76:70 扳回一城。

Round 3

http://www.4399.com/flash/197802_1.htm

这是个用 Unity 来实现的 H5 游戏:

游戏内容是控制小兔子在水面上跳到各种物品上,鼠标左键跳一格,而右键是跳两格。

首先尝试使用上文中的 canvas 截取图片的方式,但是只能得到一个全黑的图片,原因不明。

最后只好将整个浏览器进行截图:

def get_screenshot():
    global driver
    img_base64 = driver.get_screenshot_as_base64()
    img_decode = base64.b64decode(img_base64)
    img_array = np.fromstring(img_decode, np.uint8)
    img = cv2.imdecode(img_array, cv2.COLOR_BGR2GRAY)
    return img

接下来我们来分析下游戏画面:

画面上要跳的格子是固定的:

第一个格子:img[800:1000,1380:1580,:]
第二个格子:img[800:1000,1580:1780,:]
第三个格子:img[800:1000,1780:1980,:]

原则上我们只需要检测第一个格子是什么就行了。

接下来我们需要分辨出来游戏中出现的六种物品:

场景中随着游戏的不断进行,背景的亮度也会有所变化,比如变亮或者变暗等,所以我们采用上面的阈值函数是不靠谱的,需要用到自适应阈值函数 (Adaptive Thresholding),如下:

因为转成阈值图片后,每种物品的形状不一样,所以黑色像素点的数目肯定是不一样的,我们可以通过这点来判断物品:

def count_adaptive_pixels(img):
    img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    img_blur = cv2.medianBlur(img_gray, 5)
    thresh = cv2.adaptiveThreshold(img_gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2)
    rows, _ = np.where(thresh != 255)
    return rows.shape[0]

接下来我们定义一个没有重叠的可信区间,只要 count_adaptive_pixels() 函数返回的值在某个区间内,就是某个物品了:

ITEMS = {
    'empty': (0, 300),
    'stick': (2800, 3200),
    'rotten_stick': (3500, 3800),
    'rock': (4100, 4400),
    'turtle': (4700, 5300),
    'crocodile': (5800, 6300)
}

最后是 Selenium 模拟鼠标点击的动作了,上面我们知道 ActionChains 可以实现点击 (click),而右键则是 context_click

def jump(ty):
    global driver
    global canvas
    if ty == 1:
        ActionChains(driver).click(canvas).perform()
    else:
        ActionChains(driver).context_click(canvas).perform()
        
def take_action(item):
    jump(2) if item in ['empty', 'rotten_stick'] else jump(1)

操作函数非常简单:

k = 0
while k < 100:
    try:
        img = get_screenshot()
        item = predict_item_in_image(img)
        print(item)
        take_action(item)
    except:
        pass
    k += 1
    time.sleep(0.5)

最后效果:

这里最后的 time.sleep(0.5) 是为了等小兔子跳完,画面固定之后才开始截图判断,否则比较容易误判。但是这样会有更严重的问题,比如跳到乌龟或者鳄鱼头上时,需要及时跳走,不然就像上面那样落水失败。

小兔子卒...

Final Round

最终回,彩蛋时间。

http://h5.gaoshouyou.com/game/64.html

直男的噩梦!

各位是不是准备好 OpenCV 和 Selenium 跃跃欲试了,不过可以告诉大家一个有点让人失望的消息,这小游戏把答案都写在 js 里面了:


以上就是本文的所有内容。

scripts

脚本已上传到 gist:

https://gist.github.com/jackhuntcn/e588ef2022f46f889aa5a6313a3fdec0 https://gist.github.com/jackhuntcn/fcf9d8eb2e5fa0b21b99970bbaceac1d https://gist.github.com/jackhuntcn/9f6091fc258787a488c0d2cdb25a534f

为了行文方便,我虚构了一个无聊哥们。没错,这无聊哥们就是我本人 ...




不定期更新 入门级及不靠谱的 数据抓取、数据分析、深度学习以及其他有趣脚本的原创文章,欢迎长按下面的二维码关注?



举报 | 1楼 回复

友情链接