【部分非原生控件(如按钮的动态贴图、游戏引擎绘制图像、web贴图控件)通过原生的UIAutomaton或Accessibility 等方法是无法到具体的控件位置的,所以此文章旨在探索一种更能针对该难点的问题解决方案】
一、问题场景
手Q厘米秀最近业务上进行了AIO游戏的研发,实现方式比较特殊,在很多交互界面使用到贴图控件,通过绘制库将这些控件以一定的规则放在界面上,常规控件识别方式不能识别到该绘制界面的任何控件(因为整个view都是通过opengl绘制),所以尝试去解决这个问题能使我们自动测试的覆盖面增加很多。
如下面的截图可以看到,该游戏的实现逻辑是从AIO的界面上动态创建了新的view,通过游戏引擎对该view的动态绘制实现。该view下面通过常规的原生控件识别方法无法获取到里面的各个控件,如退出、最小化、开始、静音键等。
Android APP的游戏界面的按钮无法通过原生控件识别
因为贴图在游戏资源包中是分开存储的,所以我们能快速得到这些控件的贴图,如果能通过这些贴图比较当前的屏幕截图的图片,对其属于的位置进行判断则可以解决该问题。
二、核心原理:
opencv的开源库提供了对图像处理的一些实用方法,在自动化测试中尝试用到其中的图像模板匹配去识别一些常规方法不能定位到控件。
- opencv提供的匹配函数:matchTemplate
void matchTemplate(InputArray image, InputArray templ, OutputArray result, int method)
image: 搜索对象图像 It must be 8-bit or 32-bit floating-point.
templ:模板图像,小于image,并且和image有相同的数据类型
result:比较结果 必须是单通单32位浮点数
method:比较算法
- result比较结果的处理方式
1.result中数据的含义
模板匹配函数cvMatchTemplate依次计算模板与待测图片的重叠区域的相似度,并将结果存入映射图像result当中,也就是说result图像中的每一个点的值代表了一次相似度比较结果。
2.result的尺寸大小
模板在待测图像上每次在横向或是纵向上移动一个像素,并作一次比较计算,由此,横向比较W-w+1次,纵向比较H-h+1次,从而得到一个(W-w+1)×(H-h+1)维的结果矩阵,result即是用图像来表示这样的矩阵,因而图像result的大小为(W-w+1)×(H-h+1)。
3.如何result中获得最佳匹配区域
使用函数cvMinMaxLoc(result,&min_val,&max_val,&min_loc,&max_loc,NULL);从result中提取最大值(相似度最高)以及最大值的位置(即在result中该最大值max_val的坐标位置max_loc,即模板滑行时左上角的坐标)
- 其中提供了6种比较算法:
1. 平方差匹配法 method=CV_TM_SQDIFF
这类方法利用平方差来进行匹配,最好匹配为0,而匹配越差,匹配值越大。方法公式如下:
2.归一化平方差匹配法 method=CV_TM_SQDIFF_NORMED
3.相关匹配法 method =CV_TM_SQDIFF_NORRMED
这类方法采用模板和图像间的乘法操作,所以较大的数表示匹配程度较高,0表示最坏的匹配效果,公式如下:
4.归一化相关匹配法 method = TM_CCORR_NORMED
5.系数匹配方法 method = TM_CCOEFF
这类方法将模板对其均值的相对值与图像对其均值的相关值进行匹配,1表示完美匹配,-1表示最坏匹配,0表示没有任何相关性。公式如下:
6.归一化相关系数匹配法 method = TM_CCOEFF_NORMED
三、Android系统中调用opencv的方法
由于实现的测试工具是android代码,所以在这儿重点介绍下android系统下的配置和实现逻辑。
按官方的配置教程,在系统安装opencv 的apk,并导入opencv的sdk到自己的库中:
- 查询系统的cpu架构:adb shell getprop ro.
- 安装指定架构的O到
- Android系统中导入Opencv sdk到Android studio应用项目中:
- 项目根目录创建jniLibs,放入opencv的各个架构的库文件(.so文件),启动应用时读取opencv的库
最开始安装上面的方法配置后,发现调用时还是报库方法无法找到的问题,于是搜索找到一种可解决的方法:
首先在服务或activity创建的时候加入是否加载的判断:
if (!O()) { // Handle initialization error Log.e(TAG, "initialization error"); }在onResume的手动加载opencv库,并增加加载成功的回调:
@Override protected void onResume() { (); O, this, mLoaderCallback); } private BaseLoaderCallback mLoaderCallback = new BaseLoaderCallback(this) { @Override public void onManagerConnected(int status) { switch (status) { case LoaderCallbackIn: { Log.i(TAG, "OpenCV loaded successfully"); } break; default: { (status); } break; } } };上面的步骤完成后,即可在自己的android studio项目中调用opencv的各种方法。
四、使用模板匹配方法进行测试
我们需要拿到当前的屏幕截图和需要匹配的图片进行对比。
当前截图获取的方式暂时采用比较耗时的调用adb命令截图保存到本地(需要root),然后用读取图片为bitmap数据:
private Bitmap getScreenPic() { String capPath = "/sdcard;; //su 命令执行截图命令 ExecU(capPath); //获取位图数据 Bitmap capBmp = Bi(capPath); return capBmp; } public static void getScreenCap(String capPath) { Log.i(TAG, "start capture screen..." + capPath); try { process = Run().exec("su -c /System/bin/screencap -p " + capPath); ().write("exit\n".getBytes()); ().flush(); (); Log.i(TAG, "end capture screen"); close(); } catch (Exception e) { e.printStackTrace(); } }这儿有个需要优化的地方是屏幕截图的图片大小太大,读取会耗时比较严重1080p屏幕大概3s左右。
匹配图片获取可以放在本地sdcard或res或asset中读取为数据流,然后通过bitmapFactory的读取数据流方法转换为bitmap格式数据:
//从res根据name读出match img的数据 AssetManager assetManager = mCon().getAssets(); Bitmap matchImg = null; try { matchImg = Bi("drawable/" + matchImgName + ".png")); } catch (IOException e) { e.printStackTrace(); } Log.i(TAG, "cap and read bitmap cost time: " + () - start));匹配图片过程,这儿的优化点在于需要对读入的图片进行先缩放匹配成功后,再将坐标值等比放大。其中result是结果的数据,需要注意我这儿使用了SQDIFF_NORMED(归一化平方差匹配法)的匹配方法,所以值越小越是匹配最后成功的点,可以尝试下其他方法的准确度,这里我发现这个方法比较准
private Point getMatchPoint(Bitmap capImg, Bitmap matchImg) { //输入图片为空判断 if (null == capImg) { Log.e(TAG, "capImg is not found"); return null; } if (null == matchImg) { Log.e(TAG, "capImg is not found"); return null; } //缩放原图和匹配图,提高运行效率 capImg = zoomImg(capImg, 0.5f); matchImg = zoomImg(matchImg, 0.5f); //转成mat格式的数据 Mat img1 = new Mat(); U(capImg, img1); Mat img2 = new Mat(); U(matchImg, img2); //创建输出结果的矩阵 int result_cols = img1.cols() - img2.cols() + 1; int result_rows = img1.rows() - img2.rows() + 1; Mat result = new Mat(result_rows, result_cols, CvTy); long start = Sy(); //进行匹配和标准化 Img(img1, img2, result, Img); Log.i(TAG, "match cost time: " + () - start)); (result, result, 0, 100, Core.NORM_MINMAX, -1, new Mat()); //通过函数 minMaxLoc 定位最匹配的位置 Core.MinMaxLocResult mmr = Core.minMaxLoc(result); //匹配的数值太大就认为不是该图 Point matchLoc; //对于方法 SQDIFF 和 SQDIFF_NORMED, 越小的数值代表更高的匹配结果. 而对于其他方法, 数值越大匹配越好 matchLoc = mmr.minLoc; //坐标值还原放大 Point point = new Point(); = ma * 2 + ma(); = ma * 2 + ma(); Log.i(TAG, ":" + + "\t:" + ); return point; }最后获取到最小值点即为从匹配图左上角点开始的在截图上面最符合的一点(所以匹配图从左上角的特征点应该取比较特殊代表性的点开始)
PS:还有点没补充的是没对非匹配做阈值,如果用到该方法取最小值匹配点应该设定一个阈值当超过这个值时应该判断为没有匹配成功也就是当前界面没有该图片的控件或判断点
样例实践
针对文章最开始的例子,我们想完成这样一个简单用例:进入游戏后点击退出按钮退出游戏。
为了验证该方法,我们先把退出按钮的图片拿出来放到assets中:
手动截图后,调用匹配方法再将获得的左上角点绘制到原截图中,再通过匹配图的长宽生成矩形的标记区域,生成的图片如下,可以看到在该场景下还是比较准确的
入口是图像化的,也可以截取其中一个关键的特征点进行匹配并搜索定位。
还比如这个例子:下面这个抽屉页点击厘米秀的形象是一个到web页面的入口,我们可以截取腰带的特征来定位
进一步加入到实际测试中,在脚本里面我们在进入游戏后加入该点击的方法,通过文件名来确定是匹配哪个图片(还可以看到注释的地方原来我还是通过偏移的方式去实现这个用例- -):
//点击开始游戏 mNodeO("apollo_aio_game_item_first", 3500, 0); //如果进入新手引导则返回 if ("新手引导")) { mNodeO("返回", 2000); continue; } //通过y偏移点击退出按钮 //mNodeOOffset("ivTitleBtnLeft", 2000, 1, 100); mUiCvO("btn_game_exit", 3000);通过匹配后得到的中心区域点再去执行点击坐标的方法即可完成该用例
/** * 根据匹配图名称点击屏幕中匹配该图的中心坐标 * * @return */ public boolean clickOnImage(String matchImgName, int waitTime) { long start = Sy(); //截图并读出bitmap格式的数据 Bitmap capImg = getScreenPic(); //从res根据name读出match img的数据 AssetManager assetManager = mCon().getAssets(); Bitmap matchImg = null; try { matchImg = Bi("drawable/" + matchImgName + ".png")); } catch (IOException e) { e.printStackTrace(); } Log.i(TAG, "cap and read bitmap cost time: " + () - start)); //匹配图片并返回匹配区域的中心点 Point p = getMatchPoint(capImg, matchImg); if (null == p) { Log.e(TAG, "get point null"); return false; } //点击匹配区域的中心点 return mUiO((float) ), (float) ), waitTime); }五、挑战
该方法可以解决当前的一些非标准控件的场景问题,但是挑战还是有:例如执行的效率;如何更好的管理匹配图片等,抛砖引玉希望还能学习到更好的测试技术来有效的提升我们的功能ui测试效率。