游戏测试 (一) 用 python 做 android 游戏自动化测试

xiaocai · 2018年03月27日 · 最后由 Heroman 回复于 2021年11月09日 · 10056 次阅读

游戏自动化测试痛点在于难以定位控件,这里使用图像识别替代控件定位的方式,来完成游戏的自动化测试。
当然也可以混合使用图像识别 + 控件定位的方式满足需求,在这里只分享用 adb+opencv 实现游戏自动化测试过程。

一、测试原理

图像定位测试的核心思路其实是利用 adb 来操作设备,用 opencv 实现图像区域匹配,匹配成功后计算目标位置然后触发 adb。
简单来说就是如下步骤:
截图 -> 图像识别 -> 计算位置 -> 点击位置

二、创建 python 运行环境

virtualenv 提供隔离的 Python 运行环境(如果不需要的话可以忽略此步骤)

1.创建目录:

mkdir venv_gametest
cd venv_gametest

2.创建并运行独立 python 环境

virtualenv --no-site-packages venv
source venv/bin/activate

三、安装依赖环境

1. 安装 android adb

笔者是 mac os 系统,所以直接通过 Homebrew 安装。
也可以通过下载 android SDK 的方式手动安装这里不做说明,下载地址:developer.android.com

brew cask install android-platform-tools

2. 测试 adb

连上手机的数据线或打开模拟器,查看能不能找到设备

(venv) $ demo5_game adb version
Android Debug Bridge version 1.0.32
(venv) $ demo5_game adb devices
List of devices attached
192.168.56.100:5555 device

更多的 adb 操作,可以查看 https://github.com/mzlogin/awesome-adb

3. 安装 opencv

opencv 用来帮助我们完成图像的处理
只需要 numpy、Matplotlib、opencv-python 三个包
详见:install opencv

sudo pip install --upgrade setuptools
sudo pip install numpy Matplotlib
sudo pip install opencv-python

4. 测试 opencv

选一张图片拷贝以下代码,用 opencv 打开测试是否安装成功

#!/usr/bin/env python
# coding=utf-8

import cv2 as cv

img = cv.imread("test.jpg")
cv.namedWindow("Image"
cv.imshow("Image",img)
cv.waitKey(0)
cv2.destroyAllWindows()

运行结果:

四、adb 截图操作

先看如何用 adb 命令进行截图操作,详见:adb 屏幕截图

先截图保存到设备里:

adb shell screencap -p /sdcard/screencap.png

然后将 png 文件导出到电脑:

adb pull /sdcard/screencap.png

同样的使用 python 中commands模块也能实现相同的效果

commands.getstatusoutput('adb shell screencap -p /sdcard/screencap.png')
commands.getstatusoutput('adb pull /sdcard/screencap.png')

将 adb 的操作做个简单的封装,便于后面使用

file: libs/adb.py

#!/usr/bin/env python
# coding=utf-8

"""ADB"""

__author__ = 'xiaocai'

import commands

class adbKit(object):

    def screenshots(self, serialNumber=None):
        self.command('shell screencap -p /sdcard/screencap.png', serialNumber)
        self.command('pull /sdcard/screencap.png', serialNumber)

    def command(self, cmd, serialNumber=None):
        cmdstr = 'adb '
        if serialNumber:
            cmdstr = cmdstr+'-s '+serialNumber
        (status, output) = commands.getstatusoutput(cmdstr+cmd)
        return [status, output]

五、图像匹配

刚刚完成了第一个截图环节,接下来我们开始尝试用 opencv 去匹配目标图像的位置
详见:opencv 文档

1. 选取一张截图

我们利用上面封装好的adbKit对当前设备,进行一次截图操作。

file: test.py

#!/usr/bin/env python
# coding=utf-8

"""demo"""

__author__ = 'xiaocai'

import sys, time, commands
from libs import adb

adbkit = adb.adbKit()
adbkit.screenshots()

运行完之后可以看到根目录下出现了screencap.png图片

.
├── libs
│   ├── __init__.py
│   ├── __init__.pyc
│   ├── adb.py
│   └── adb.pyc
├── screencap.png
└── test.py

接下来我们要实现第一个用例,如下图要识别到游戏中右上角的X按钮

用截图工具将x截取,保持到images/btn_close_full.png(需要注意分辨率)

2. 载入图像

读取截图和要匹配的x图像

import cv2
target_img = cv2.imread("screencap.png")
find_img   = cv2.imread("images/btn_close_full.png")

3. 图像匹配

使用图像模板匹配cv::matchTemplate(),通过返回的 cvMinMaxLoc 计算结果
详见:Template Matching
matchTemplate会将模板图像在源图像中进行滑动匹配(从左往右,从上往下)

参数说明:

image是源图像,templ是模板图像,method是匹配算法

Python: cv2.matchTemplate(image, templ, method[, result]) → result
Parameters: 
image – Image where the search is running. It must be 8-bit or 32-bit floating-point.
templ – Searched template. It must be not greater than the source image and have the same data type.
result – Map of comparison results. It must be single-channel 32-bit floating-point. If image is  W \times H and templ is  w \times h , then result is (W-w+1) \times (H-h+1) .
method – Parameter specifying the comparison method (see below).

代码:

这里我们用cv2.TM_CCOEFF_NORMED
不同的算法对匹配结果会有所差异,具体可参考:Python+OpenCV 学习(7)--- 模板匹配

result = cv2.matchTemplate(target_img, find_img, cv2.TM_CCOEFF_NORMED)
print result

结果:

从打印的结果看到,matchTemplate 会在模板块和输入图像之间寻找匹配最后返回一组匹配结果图像

[[ 0.07075607  0.08976483  0.10165194 ...  0.07626463  0.06611969
   0.04449887]
 [ 0.04982539  0.06268624  0.07642973 ...  0.06295352  0.05140355
   0.02827941]
 [ 0.01129027  0.02193917  0.03671272 ...  0.03954886  0.02578432
   0.00115839]
 ...
 [-0.08573136 -0.07838587 -0.06693702 ... -0.03226329 -0.02669666
  -0.03457748]
 [-0.08250767 -0.07598738 -0.06588773 ... -0.0265673  -0.02382514
  -0.02491279]
 [-0.07762627 -0.07059815 -0.06101505 ... -0.00854323 -0.0053194
  -0.00458878]]

每个匹配图像会有匹配度,比如找出匹配度>0.8 的图像 (如果需要一次匹配多个结果可以使用这个方法)

示例:

#!/usr/bin/env python
# coding=utf-8

"""demo"""

__author__ = 'xiaocai'

import commands, cv2
import numpy as np

from libs import adb

adbkit = adb.adbKit()
adbkit.screenshots()

target_img = cv2.imread("screencap.png")
find_img   = cv2.imread("images/btn_close_full.png")

result = cv2.matchTemplate(target_img, find_img, cv2.TM_CCOEFF_NORMED)
loc    = np.where( result >= 0.5)
for pt in zip(*loc[::-1]):
    print pt

4. minMaxLoc 方法

找到匹配图像之后我们需要使用minMaxLoc函数在给定的矩阵中寻找最大和最小值 (包括它们的位置).

代码:

依次是:最小匹配度,最大匹配度,最小匹配位置,最大匹配位置

result = cv2.matchTemplate(target_img, find_img, cv2.TM_CCOEFF_NORMED)
min_val,max_val,min_loc,max_loc = cv2.minMaxLoc(result)
print min_val,max_val,min_loc,max_loc

结果:

这里的(1019, 74)就是我们模板在源图中的左上角坐标了

-0.381701976061 0.779404222965 (1181, 519) (1019, 74)

六、计算点击位置

上面我们通过matchTemplateminMaxLoc方法已经获取到模板图像在源图中的坐标了,但这个坐标只是左上角的位置,实际点击应该是模板图像的中间位置

首先我们需要先获取模板图像的尺寸

find_img   = cv2.imread("images/btn_close_full.png")
find_height, find_width, find_channel = find_img.shape[::]

根据 max_loc 结果计算出中间位置

pointUpLeft   = max_loc
pointLowRight = (max_loc[0]+find_width, max_loc[1]+find_height)
pointCentre   = (max_loc[0]+(find_width/2), max_loc[1]+(find_height/2))

为了更直观些,我们把坐标在源图中点出来并显示出图片

cv2.circle(target_img, pointUpLeft, 2, (0, 255, 255), -1)
cv2.circle(target_img, pointCentre, 2, (0, 255, 255), -1)
cv2.circle(target_img, pointLowRight, 2, (0, 255, 255), -1)
cv2.namedWindow("Image")
cv2.imshow("Image", target_img)
cv2.waitKey(0)
cv2.destroyAllWindows() 

完整代码:

#!/usr/bin/env python
# coding=utf-8

"""demo"""

__author__ = 'xiaocai'

import commands, cv2
import numpy as np

from libs import adb

# 截图
adbkit = adb.adbKit()
adbkit.screenshots()

# 载入图像
target_img = cv2.imread("screencap.png")
find_img   = cv2.imread("images/btn_close_full.png")
find_height, find_width, find_channel = find_img.shape[::]

# 模板匹配
result = cv2.matchTemplate(target_img, find_img, cv2.TM_CCOEFF_NORMED)
min_val,max_val,min_loc,max_loc = cv2.minMaxLoc(result)

# 计算位置
pointUpLeft   = max_loc
pointLowRight = (max_loc[0]+find_width, max_loc[1]+find_height)
pointCentre   = (max_loc[0]+(find_width/2), max_loc[1]+(find_height/2))

# 画点
cv2.circle(target_img, pointUpLeft, 2, (255, 255, 255), -1)
cv2.circle(target_img, pointCentre, 2, (255, 255, 255), -1)
cv2.circle(target_img, pointLowRight, 2, (255, 255, 255), -1)

# 显示图片
cv2.namedWindow("Image")
cv2.imshow("Image", target_img)
cv2.waitKey(0)
cv2.destroyAllWindows() 

结果:

七、触发点击

离成功只差最后一步了,先看看 adb 是如何模拟按键输入的
详见:模拟按键/输入

命令:

adb shell input tap <x> <y>

python 调用:

class adbKit(object):

    def screenshots(self, serialNumber=None):
        self.command('shell screencap -p /sdcard/screencap.png', serialNumber)
        self.command('pull /sdcard/screencap.png', serialNumber)

    def click(self, point, serialNumber=None):
        return self.command('shell input tap '+str(point[0])+' '+str(point[1]), serialNumber)

    def command(self, cmd, serialNumber=None):
        cmdstr = 'adb '
        if serialNumber:
            cmdstr = cmdstr+'-s '+serialNumber
        (status, output) = commands.getstatusoutput(cmdstr+cmd)
        return [status, output]

测试:

adbkit = adb.adbKit()
adbkit.click(max_loc)

八、总结

回过头来看最后的代码,仅需 30 行即可完成(截图 -> 图像识别 -> 计算位置 -> 点击位置)流程,剩下我们可以思考下如何将整个测试环境串联起来。

完整的编码:

#!/usr/bin/env python
# coding=utf-8

"""demo"""

__author__ = 'xiaocai'

import commands, cv2
import numpy as np

from libs import adb

# 截图
adbkit = adb.adbKit()
adbkit.screenshots()

# 载入图像
target_img = cv2.imread("screencap.png")
find_img   = cv2.imread("images/btn_close_full.png")
find_height, find_width, find_channel = find_img.shape[::]

# 模板匹配
result = cv2.matchTemplate(target_img, find_img, cv2.TM_CCOEFF_NORMED)
min_val,max_val,min_loc,max_loc = cv2.minMaxLoc(result)

# 计算位置
pointUpLeft   = max_loc
pointLowRight = (max_loc[0]+find_width, max_loc[1]+find_height)
pointCentre   = (max_loc[0]+(find_width/2), max_loc[1]+(find_height/2))

# 点击
adbkit.click(pointCentre)

原文地址:http://www.xiaocai.name/2018/02/14/%E7%94%A8python%E5%81%9Aandroid%E6%B8%B8%E6%88%8F%E8%87%AA%E5%8A%A8%E5%8C%96%E6%B5%8B%E8%AF%95(1)/

共收到 20 条回复 时间 点赞
匿名 #1 · 2018年03月27日

很详细很赞! 非常适合初学者上手。得找个游戏来试试

楼主要是遇到输入框咋办,adb 只能对 Android 原生 textFild 等输入吧,游戏一般都是 C2D 或是 U3D 开发,控件不一样😀

zhiqingjiang 回复

可以 adb shell input text 输入

写的挺好的

IGG 的测试同学。喵呜。

opencv-python 有人安装出现问题吗?

王多余 回复

这是缺少 openssl, 试试 pip install pyopenssl、yum install -y openssl openssl-devel

谢谢,学习了~

codeskyblue 回复

大神出现了,膜拜一下~

xiaocai 回复

不客气,一个通用的测试框架配合计算机视觉来做游戏测试是常规方案。

希望老哥能弄一整套的游戏测试自动化框架和体系出来

用图像识别遇到最大的问题:
1、图像匹配度
2、断言
第 1 个问题,我在编写代码的机器上,有时候都会卡主,如果放到其他机型来跑,兼容问题就更多了,需要不停的换图片。
第 2 个问题,主要是一些设计数值的判断,无法获取到图片上的数值,这个不知道大佬有没有办法啊

渐渐 回复

换手机遇到的分辨率问题有两种,一种是图片会在手机屏幕完整展示,这种需要把截图的宽高都要根据截图和设备等比缩放。另一种是宽完整展示,高可以有滚动条,这样不会变形,这种只需要把宽的缩放比用在高上面缩放就可以了。

渐渐 回复

第二个问题是指图片文字识别么,目前免费的 ocr 在不训练的情况下识别率比较低,可以考虑用百度 AI,识别率高,免费次数挺多的,如果有点击需求,还有返回坐标的方法,只是次数少一点

k 回复

谢谢大佬,我去研究一下。

请问下 做没做过滑杆移动人物的实践啊

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册