背景:最近在調研AirTest這個測試工具,據說最近很火,同事說很多平台都在推,但是同事使用AirTest的IDE生成了代碼,然后發現在不同分辨率下,window大小不同的情況下,操作一些小的控件,並不是很穩定,后來我們就調研了一下AirTest的圖像識別。
1. 如何使用AirTest IDE
1.1 環境搭建:
如果你只想使用AirTest IDE,這就很簡單了,你只要下載AirTest IDE, 就可以直接使用了,IDE里面已經把需要的包都已經包含在內了,無需別的操作
解壓后的IDE 文件夾:
執行里面的AirtestIDE.exe 就可以啟動AirTest IDE了。
1.2 使用 AirTest IDE
關於如何使用AirTest IDE, 官網已經非常詳細,操作也比較簡單,相信大家去簡單看一下就會知道如何使用,就不多敘述了。
這里是一些官網的鏈接,大家可以參考一下:
2. 查看AirTest 的測試代碼
2.1 使用AirTest IDE編寫測試代碼
假設大家都已經會使用AirTest IDE了,現在展示一下最簡單的代碼,touch一下:
2.2 代碼解釋
大家看到代碼其實非常簡單,第四行是從airtest的包中導入你需要的內容,第六行是進行一個touch操作。
在IDE中展示出來的是touch傳入一個圖片,后面我們對touch的整個流程進行分析,看看怎么能提高點擊的可靠性。
3. touch方法剖析
3.1 touch代碼解析
在AirTest IDE中,你看見的傳入一張圖片,如果你用別的IDE來打開,你看見的是如下的代碼:
1 # -*- encoding=utf8 -*- 2 __author__ = "Test" 3 4 from airtest.core.api import * 5 6 touch(Template(r"tpl1583817736399.png", record_pos=(2.282, 4.579), resolution=(500, 424)))
如上所示, 真正的代碼中touch傳入的是一個 Template的對象,后面我們來一步一步的詳細的分析一下
3.2 touch 方法源碼解析
第一步,來看一下touch的源碼

1 @logwrap 2 def touch(v, times=1, **kwargs): 3 """ 4 Perform the touch action on the device screen 5 6 :param v: target to touch, either a Template instance or absolute coordinates (x, y) 7 :param times: how many touches to be performed 8 :param kwargs: platform specific `kwargs`, please refer to corresponding docs 9 :return: finial position to be clicked 10 :platforms: Android, Windows, iOS 11 """ 12 if isinstance(v, Template): 13 pos = loop_find(v, timeout=ST.FIND_TIMEOUT) 14 else: 15 try_log_screen() 16 pos = v 17 for _ in range(times): 18 G.DEVICE.touch(pos, **kwargs) 19 time.sleep(0.05) 20 delay_after_operation() 21 return pos
大家看到源碼中有幾個參數,下面也有比較清楚的解釋,v是一個目標對象,一個template對象或者是一個絕對坐標,times是你點擊多少次,后面的kwargs是傳給對應的device.touch的參數
好了,我們在往下看,如果傳的是坐標,那就直接點擊,如果不是坐標,那么來了,loop_find, 這應該是識圖了,后面的點擊是根據這個識圖的返回值來操作了,那我們就來看一下這個方法。

1 if isinstance(v, Template): 2 pos = loop_find(v, timeout=ST.FIND_TIMEOUT)
touch在拿到需要的坐標之后,使用G.DEVICE.touch(pos, **kwargs) 來調用對應的平台驅動來實現點擊事件,在這里就不深入討論了,后面探討一下識圖問題。
4. 識圖解析
4.1 loop_find源碼解析

1 @logwrap 2 def loop_find(query, timeout=ST.FIND_TIMEOUT, threshold=None, interval=0.5, intervalfunc=None): 3 """ 4 Search for image template in the screen until timeout 5 6 Args: 7 query: image template to be found in screenshot 8 timeout: time interval how long to look for the image template 9 threshold: default is None 10 interval: sleep interval before next attempt to find the image template 11 intervalfunc: function that is executed after unsuccessful attempt to find the image template 12 13 Raises: 14 TargetNotFoundError: when image template is not found in screenshot 15 16 Returns: 17 TargetNotFoundError if image template not found, otherwise returns the position where the image template has 18 been found in screenshot 19 20 """ 21 G.LOGGING.info("Try finding:\n%s", query) 22 start_time = time.time() 23 while True: 24 screen = G.DEVICE.snapshot(filename=None, quality=ST.SNAPSHOT_QUALITY) 25 26 if screen is None: 27 G.LOGGING.warning("Screen is None, may be locked") 28 else: 29 if threshold: 30 query.threshold = threshold 31 match_pos = query.match_in(screen) 32 if match_pos: 33 try_log_screen(screen) 34 return match_pos 35 36 if intervalfunc is not None: 37 intervalfunc() 38 39 # ???raise,??????????: 40 if (time.time() - start_time) > timeout: 41 try_log_screen(screen) 42 raise TargetNotFoundError('Picture %s not found in screen' % query) 43 else: 44 time.sleep(interval)
參數解析:
- query:你想要查找的圖片的template
- timeout: 你查找的圖片的超時時間
- threshold:你查圖的閾值,就是打分了,你想找的圖片在整個的圖像里面的打分,超過就算識別到,低於就是沒識別到,你可以降低這個閾值,但是也有可能導致找錯,按照你的需求來,不給的話會有一個默認值
- interval:查圖有時並不是一次,會等待一段時間后繼續查詢
- intervalfunc:查圖失敗之后做的一些操作,當前不討論, 在touch函數中並不涉及此參數
流程解析:
loop_find所做的操作流程是這樣的:
- 先拿到當前操作界面的整個截圖
- 拿你傳入的截圖template來進行匹配
- 拿匹配值和當前的期望閾值進行比價
- 比較成功則返回預期坐標,失敗則等待后繼續匹配
- 若匹配超時,最終返回異常
由上述流程來看,其核心操作其實就是在用你的template進行匹配,其代碼如下所示

1 if threshold: 2 query.threshold = threshold 3 match_pos = query.match_in(screen)
大家看到,圖像匹配其實在match_in方法之中,我們再來看下這個方法。
4.2 match_in源碼解析
match_in這個方法是從哪來的呢,從參數來看,是query的方法,query是什么,是template,那就看下這個方法的源碼:

1 def match_in(self, screen): 2 match_result = self._cv_match(screen) 3 G.LOGGING.debug("match result: %s", match_result) 4 if not match_result: 5 return None 6 focus_pos = TargetPos().getXY(match_result, self.target_pos) 7 return focus_pos
源碼中最主要的是match_result, 后面都是對該變量的處理,那圖像匹配的方法就在_cv_match這個方法里面了,下面再看下_cv_match的源碼。
4.3 _cv_match源碼解析

1 @logwrap 2 def _cv_match(self, screen): 3 # in case image file not exist in current directory: 4 image = self._imread() 5 image = self._resize_image(image, screen, ST.RESIZE_METHOD) 6 ret = None 7 for method in ST.CVSTRATEGY: 8 # get function definition and execute: 9 func = MATCHING_METHODS.get(method, None) 10 if func is None: 11 raise InvalidMatchingMethodError("Undefined method in CVSTRATEGY: '%s', try 'kaze'/'brisk'/'akaze'/'orb'/'surf'/'sift'/'brief' instead." % method) 12 else: 13 ret = self._try_match(func, image, screen, threshold=self.threshold, rgb=self.rgb) 14 if ret: 15 break 16 return ret
根據源碼,我們可以看到,整個匹配的核心流程就是先resize image然后根據ST.CVSTRATEGY里面所包含的策略方法進行輪訓匹配, 最后返回匹配結果。
ST.CVSTRATEGY里面的方法都是在open cv的基礎上進行封裝的,是不變的模式,沒有辦法調整,那么我們能調整的是什么,是image resize 這個行為,圖片到底是怎么resize的,我們來進一步看一下
4.4 _resize_image源碼解析

1 def _resize_image(self, image, screen, resize_method): 2 """模板匹配中,將輸入的截圖適配成 等待模板匹配的截圖.""" 3 # 未記錄錄制分辨率,跳過 4 if not self.resolution: 5 return image 6 screen_resolution = aircv.get_resolution(screen) 7 # 如果分辨率一致,則不需要進行im_search的適配: 8 if tuple(self.resolution) == tuple(screen_resolution) or resize_method is None: 9 return image 10 if isinstance(resize_method, types.MethodType): 11 resize_method = resize_method.__func__ 12 # 分辨率不一致則進行適配,默認使用cocos_min_strategy: 13 h, w = image.shape[:2] 14 w_re, h_re = resize_method(w, h, self.resolution, screen_resolution) 15 # 確保w_re和h_re > 0, 至少有1個像素: 16 w_re, h_re = max(1, w_re), max(1, h_re) 17 # 調試代碼: 輸出調試信息. 18 G.LOGGING.debug("resize: (%s, %s)->(%s, %s), resolution: %s=>%s" % ( 19 w, h, w_re, h_re, self.resolution, screen_resolution)) 20 # 進行圖片縮放: 21 image = cv2.resize(image, (w_re, h_re)) 22 return image
通過源碼,其實注釋已經很清楚了,如果你沒有記錄了截圖時的分辨率,那么就會用原圖的原始比例在window的截圖上匹配,如果記錄了分辨率,會根據你的分辨率和當前window截圖的分辨率根據策略來進行圖片縮放。
那這個就簡單了,核心就是說怎么才能讓你的截圖在不同的分辨率下更接近當前的window截圖中的圖片,后面我們來分析一下。
5. 優化圖片縮放
5.1 圖片縮放策略
當你測試的window分辨率變化時,圖片大體上會有幾種變化,如果還有別的模式,歡迎補充。
- 圖片不隨window的變化而變化
- 圖片隨window的變化而同比例變化
- 圖片有不同的尺寸,在window特定的尺寸范圍內變化
- 圖片隨window特定尺寸變化,且尺寸有縮放 (有種window,在不同尺寸下,整體布局也不一樣,這種操作就比較復雜,我就描述了。)
5.2 測試前提條件
現在我們來模擬一下測試場景。
我打開了Microsoft Store的一個game 頁面,如下圖,在圖片最大時,在圖中截取了一塊圖片,后面開始匹配。
此圖片在不同的window大小情況下會呈現不同的大小,上述第三種情況。
測試代碼如下所示:
1 from vendor.airtest.core.api import Template, touch, auto_setup, connect_device, loop_find 2 3 auto_setup(__file__) 4 connect_device("Windows:///?title_re=.*Microsoft Store.*") 5 6 pos = loop_find(Template("./data/h.png", record_pos=(1.269, 0.013), resolution=(820, 679)))
我先連接了Microsoft Store的window,然后去查找我截圖的部分,看下匹配結果。(使用loop_find來替換touch,結果更清晰。)
5.3 測試策略優化
上面談到了圖片的幾種變化,我們就來根據圖片的變化來優化我們的代碼,來看一下效果如何。
- 場景一: window放大,圖片尺寸不變
我這次放大了window的尺寸,然后看一下匹配的結果是多少。
1 threshold=0.7, result={'confidence': 0.45194211602211, 'result': (129, 499), 'rectangle': ((105, 477), (105, 521), (154, 521), (154, 477))}
如上所示,匹配結果只有0.45,遠小於0.7的期望值,查詢失敗了。
我放大了size,但是圖片不變,其中的resize執行之后就匹配不到了,這不太科學,那我們這次改變測試代碼,不傳入分辨率。
1 pos = loop_find(Template("./data/h.png", record_pos=(1.269, 0.013)))
結果如下所示:
1 threshold=0.7, result={'confidence': 1.0, 'result': (129, 500), 'rectangle': ((110, 483), (110, 517), (148, 517), (148, 483))}
嗯,1.0完成,也就是百分百匹配到,也就是說對於window尺寸來說,不變化的圖像,就刪除分辨率屬性,保持原有尺寸,不進行縮放來匹配,這樣的效果會比較好。
- 場景二:圖片尺寸隨着window的變化而變化
由於Microsoft Store上面沒有隨window比例變化的圖片,我就把圖片截圖然后放在相冊里來測試,因為相冊不是完全隨比例變化的,所有查詢精度會變低,但是不影響結果,如下所示:
1 threshold=0.7, result={'confidence': 0.96263587474823, 'result': (98, 227), 'rectangle': ((85, 217), (85, 238), (111, 238), (111, 217))}
這說明如果是隨比例縮放的圖片,就保持AirTest IDE中的代碼,保持分辨率參數,就可以很好的查找圖片了。
- 場景三:圖片隨window變化而伴隨幾種特定尺寸
這種就比較麻煩了,第一不是不變的,第二不是隨比例變化的。
我推薦的策略是:
第一,測試過程中先將你的window resize到固定的尺寸大小,這樣測試也會相對穩定
第二,如果window並不能固定,那我推薦把圖片變成一個變量,根據不同的分辨率,先初始化一套符合當前的圖片組,然后進行測試
具體哪套更合適你,或者有更好的方式,可以留言,大家共同探討一下。
以上就是關於AirTest識圖准確性的一點個人看法,歡迎探討。