談談如何使用 opencv 進行影像識別

TesterHome發表於2022-04-30
原文由hakaboom發表於TesterHome社群,點選原文連結可與作者直接交流。

1)前言

從18年開始,我接觸了叉叉助手(平臺已經被請喝茶了),通過圖色識別,用來給常玩的遊戲寫掛機指令碼,寫了也有兩三年.也算是我轉行當遊戲測試的理由.
去年11月,也是用了這身技術,混進了外包,薪資還不錯,屬於是混日子了,崗位是在發行,接觸到很多遊戲,因為接不了poco,到手只有apk,
日積月累,遊戲越來越多,專案組卻還是隻有這點人.為了減輕自己的壓力,就開始了UI自動化的不歸路.

2)遊戲UI自動化

因為遊戲引擎,是無法通過appium等框架去獲取,如果不接入一些SDK,那麼識別的方法只有影像識別.現在常見的開源框架

  1. 網易的Airtest,通過傳統識別進行自動化,還有airtestIDE可以簡單快速的編寫airtest程式碼
  2. 騰訊GameAISDK,通過深度學習進行自動化(沒用過,好久不維護了)
  3. 阿里的SoloPi,主要功能是錄製、群控,有影像匹配輔助

影像相關的常見方法:

  1. 傳統的識別方法: 特徵點、模板、輪廓

    • 特徵點: SIFT, ORB

      • 下文會詳細講
    • 模板匹配: opencv的matchTemplate

      • 最簡單的方案,通過講模板在目標影像中平移,找到最符合的目標
    • 輪廓: HALCON Shape-based Matching, Canny

      • 沒用過,寫不來,halcon的要花錢
  2. 基於深度學習的方法:

    • 文字識別: PaddleOCR,tesseract

      • paddleOCR基本上開箱即用,但是對於遊戲內的藝術字,還需要額外的訓練
    • 影像分類: paddleClas

      • 沒有實際用過,感覺可以用在區分場景,然後去做更加詳細的識別.比如識別彈窗
    • 目標檢測: yolo

      • 之前很火的Fps外掛,基本就是靠這個去識別人體

UI自動化的核心在於查詢元素,並且在什麼位置.那麼重點就會放在影像識別上.
基於深度學習的方案,需要大量的正負樣本和標註工作,因此只能放棄.取而代之的是傳統的識別方案.
在社群裡、qq的測試群裡就能發現,大多數人對傳統影像識別的印象是:慢,不準.
今年過年前,去張江面試過一家遊戲公司,也是發行公司,聊了一個多小時,聊下來他們的方案是airtest一種機型截一個圖去做適配.我大受震撼.
總結下來影像識別的UI自動化難點:

  1. 識別慢
  2. 識別結果不準確
  3. 多解析度不相容性
  4. 遊戲UI更新,管理圖片庫的成本

3)怎麼解決

那麼我做了什麼,專案就在這裡:https://github.com/hakaboom/p...
目前也是在重構,重構完成後可能起個好名字:https://github.com/hakaboom/i...

一開始是參考了airtest的aircv部分,當時不想有那麼多依賴,就拆出來了.
重構之後,通過對opencv一些api的封裝,重新組織了構架和演算法.目前效果感覺不錯,也已經給airtest提了pr,後續也會推進合併.

安裝opencv-python

建議版本可以是4.5.5

  1. pypi上有編譯好的,但是隻能用cpu方法:

    • pip install opencv-python
    • pip install opencv-contrib-python
  2. 從原始碼編譯,可以自定義更多的東西,比如增加cuda支援

什麼是特徵點

簡單的理解: 用於描述影像特徵的關鍵點

常見的特徵點提取演算法:

  1. SIFT: 尺度不變特徵變換. opencv只有cpu實現
  2. SURF: surf的加速演算法. opencv有cpu和cuda實現
  3. ORB: 使用FAST特徵檢測和BRIEF特徵描述子. opencv有cpu和cuda實現

他們的好處是什麼: 尺度和旋轉不變性,說白了就是相容不同解析度、旋轉、尺度的變換
速度排序: ORB(cuda)>SURF(cuda)>ORB>SURF>SIFT
效果排序(效果不止是特徵點的數量,更重要的是特徵點的質量): SIFT>ORB>SURF

例子

  • 6.png(2532x1170)iphone12pro上的截圖
  • 4.png(1922x1118 實際遊戲渲染是1920x1080,多出來的是windows邊框)崩三桌面端的截圖, 裁剪了右上角的藍色加號區域當模板
import cv2
import time
from baseImage import Image, Rect
from image_registration.matching import SIFT

match = SIFT()
im_source = Image('tests/image/6.png')
im_search = Image('tests/image/4.png').crop(Rect(1498,68,50,56))

start = time.time()
result = match.find_all_results(im_source, im_search)
print(time.time() - start)
print(result)
img = im_source.clone()
for _ in result:
    img.rectangle(rect=_['rect'], color=(0, 0, 255), thickness=3)
img.imshow('ret')
cv2.waitKey(0)

結果可以得到三個加號的位置

[
    {'rect': <Rect [Point(1972.0, 33.0), Size[56.0, 58.0]], 'confidence': 0.9045119285583496}, 
    {'rect': <Rect [Point(2331.0, 29.0), Size[52.0, 66.0]], 'confidence': 0.9046278297901154}, 
    {'rect': <Rect [Point(1617.0, 30.0), Size[51.0, 64.0]], 'confidence': 0.9304171204566956}
]

怎麼進行匹配

Airtest的aircv做了什麼

https://github.com/AirtestPro...
1.獲取特徵點
2.匹配特徵點

def match_keypoints(self, des_sch, des_src):
    """Match descriptors (特徵值匹配)."""
    # 匹配兩個圖片中的特徵點集,k=2表示每個特徵點取出2個最匹配的對應點:
    return self.matcher.knnMatch(des_sch, des_src, k=2)

我們可以看到,這邊k=2代表,一個模板上的特徵點,去匹配兩個目標影像的特徵點
3.篩選特徵點

good = []
for m, n in matches:
   if m.distance < self.FILTER_RATIO * n.distance:
       good.append(m)

通過計算兩個描述符之間的距離差,來篩選結果

4.根據透視變換或座標計算,獲取矩形,然後計算置信度

那麼以上步驟會存在什麼問題

  1. 在第二步,假設圖片中存在n個目標圖片,那麼還是k=2的話,就會導致特徵點數量變少
  2. 在第三步,篩選的方法不太合理,實際debug中會發現,一些特徵點即使distance數值很高,但從結果上看,還是符合目標的,那麼就意味著單純根據距離去篩選特徵點
    的方法是不靠譜的
  3. 在第四步,獲取完特徵點後,airtest的方式是,根據透視變換獲取目標的四個頂點座標,計算出最小外接矩形.
    那麼如果目標圖片存在旋轉/形變,那麼最後獲取的圖片會裁剪到多餘目標,造成置信度降低

既然airtest存在這些問題,那麼我做了什麼改動,我把步驟一個個拆分

我的特徵點匹配

1.讀取圖片

from baseImage import Image
im_source = Image('tests/image/6.png')

這邊用到了我另外一個庫 https://github.com/hakaboom/b...
主要的用處對opencv的影像資料進行格式和型別的轉換,以及一些介面的包裝

  • 使用place引數,修改資料格式

    • Ndarray: 格式為numpy.ndarray格式
    • Mat: 和numpy基本一致
    • Umat: python的繫結不多,沒有ndarray靈活,可以用於opencl加速
    • GpuMat: opencv的cuda格式,需要注意視訊記憶體消耗
from baseImage import Image
from baseImage.constant import Place
    
Image(data='tests/image/0.png', place=Place.Ndarray)  # 使用numpy
Image(data='tests/image/0.png', place=Place.Mat)  # 使用Mat
Image(data='tests/image/0.png', place=Place.UMat)  # 使用Umat
Image(data='tests/image/0.png', place=Place.GpuMat)  # 使用cuda

2.建立特徵點檢測類
這邊會有一些引數,除了threshold(過濾閾值)、rgb(是否通過rgb通道檢測)以為,還有可以加入特徵點提取器的一些配置,一般預設就好,具體可以查opencv文件

from image_registration.matching import SIFT

match = SIFT(threshold=0.8, rgb=True, nfeatures=50000)

3.識別

from image_registration.matching import SIFT
from baseImage import Image, Rect


im_source = Image('tests/image/6.png')
im_search = Image('tests/image/4.png').crop(Rect(1498,68,50,56))

match = SIFT(threshold=0.8, rgb=True, nfeatures=50000)
result = match.find_all_results(im_source, im_search)

4.解析下find_all_results裡做了什麼,可以在image_registration.matching.keypoint.base裡找到基類

  • 第一步: 建立特徵點提取器BaseKeypoint.create_matcher
    例:image_registration.matching.keypoint.sift
def create_detector(self, **kwargs) -> cv2.SIFT:
    nfeatures = kwargs.get('nfeatures', 0)
    nOctaveLayers = kwargs.get('nOctaveLayers', 3)
    contrastThreshold = kwargs.get('contrastThreshold', 0.04)
    edgeThreshold = kwargs.get('edgeThreshold', 10)
    sigma = kwargs.get('sigma', 1.6)
    
    detector = cv2.SIFT_create(nfeatures=nfeatures, nOctaveLayers=nOctaveLayers, contrastThreshold=contrastThreshold,
                                edgeThreshold=edgeThreshold, sigma=sigma)
    return detector
  • 第二步: 建立特徵點匹配器BaseKeypoint.create_detector用於匹配模板和目標圖片的特徵點
    有兩種匹配器,

    • BFMatcher: 暴力匹配, 總是嘗試所有可能的匹配
    • FlannBasedMatcher: 演算法更快,但是也能找到最近鄰的匹配
  • 第三步: 提取特徵點BaseKeypoint.get_keypoint_and_descriptor
    用第一步建立的提取器去獲取特徵點.ORB這種,還需要額外的去增加描述器.具體就看程式碼實現吧.
  • 第四步: 匹配特徵點
    用第二步建立的匹配器,獲取特徵點集
  • 第五步: 篩選特徵點BaseKeypoint.filter_good_point

    • cv2.DMatch opencv的匹配關鍵點描述符類

      • distance: 兩個描述符之間的距離(歐氏距離等),越小表明匹配度越高
      • imgIdx: 訓練影像索引
      • queryIdx: 查詢描述符索引(對應模板影像)
      • trainIdx: 訓練描述符索引(對應目標影像)
    • cv2.Keypoint opencv的特徵點類

      • angle: 特徵點的旋轉方向(0~360)
      • class_id: 特徵點的聚類ID
      • octave:特徵點在影像金字塔的層級
      • pt: 特徵點的座標(x,y)
      • response: 特徵點的響應強度
      • size: 特徵點的直徑大小
        知道了這兩種類之後,我們就可以通過第四步獲取的特徵點集進行篩選
    • 步驟1: 根據queryIdx的索引對列表進行重組,主要目的是,讓一個模板的特徵點只可以對應一個目標的特徵點
    • 步驟2: 根據distance的升序,對特徵點集進行排序,提取出第一個點,也就是當前點集中,distance數值最小的點,為待匹配點A
    • 步驟3. 獲取點待匹配點A對應的queryIdxtrainIdx的keypoint(query_keypoint,train_keypoint,通過兩個特徵點的angle可以計算出,特徵點的旋轉方向
    • 步驟4. 計算train_keypoint與其他特徵點的夾角,根據旋轉不變性,我們可以根據模板上query_keypoint的夾角,
      去篩選train_keypoint的夾角
    • 步驟5. 計算以query_keypoint為原點,其他特徵點的旋轉角,還是根據旋轉不變性,我們可以再去篩選以train_keypoint原點,其他特徵的的旋轉角
    • 最後,我們就可以獲取到,所有匹配的點、圖片旋轉角度、基準點(待匹配點A)

5.篩選完點集後,就可以進行匹配了,這邊會有幾種情況BaseKeypoint.extract_good_points

  • 沒有特徵點,其實肯定會有一個特徵點
  • 有1組特徵點BaseKeypoint._handle_one_good_points

    - 根據兩個特徵點的```size```大小,獲取尺度的變換
    - 根據步驟4中返回的旋轉角度,獲取變換後的矩形頂點
    - 通過透視變換,獲取目標影像區域,與目標影像進行模板匹配,計算置信度
  • 有2組特徵點BaseKeypoint._handle_two_good_points

    - 計算兩組特徵點的兩點之間距離,獲取尺度的變換
    - 根據步驟4中返回的旋轉角度,獲取變換後的矩形頂點
    - 通過透視變換,獲取目標影像區域,與目標影像進行模板匹配,計算置信度
  • 有3組特徵點BaseKeypoint._handle_three_good_points

    - 根據三個特徵點組成的三角形面積,獲取尺度的變換
    - 根據步驟4中返回的旋轉角度,獲取變換後的矩形頂點
    - 通過透視變換,獲取目標影像區域,與目標影像進行模板匹配,計算置信度
  • 有大於等於4組特徵點BaseKeypoint._handle_many_good_points

    - 使用單矩陣對映```BaseKeypoint._find_homography```,獲取變換後的矩形頂點
    - 通過透視變換,獲取目標影像區域,與目標影像進行模板匹配,計算置信度
    

6.刪除特徵點
匹配完成後,如果識別成功,則刪除目標區域的特徵點,然後進入下一次迴圈

4)基準測試

裝置環境:

  • i7-9700k 3.6GHz
  • NvidiaRTX 3080Ti
  • cuda版本11.3
  • opencv版本:4.5.5-dev(從原始碼編譯)

測試內容: 迴圈50次,獲取目標圖片和模板圖片的特徵點.

注:沒有進行特徵點的篩選, 特徵點方法沒有進行模板匹配計算置信度,因此實際速度會比測試的速度要慢

從圖中可以看出cuda方法的速度最快,同時cpu的佔用也小,原因是這部分算力給到了cuda

因為沒有用程式碼獲取cuda使用率,這邊在工作管理員看的,只能說個大概數

  • cuda_orb: cuda佔用在35%~40%左右
  • cuda_tpl: cuda佔用在15%~20%左右
  • opencl_surf: cuda佔用在13%左右
  • opencl_akaze: cuda佔用在10%~15%左右

還有其他的演算法,opencv沒有提供cuda或者是opencl的實現,只能用cpu加速


5)怎麼優化速度

  1. airtest慢的一個原因在於,只用了cpu計算.如果能釋放算力到gpu上,速度就會有成倍的增長.

    opencv已經給我們做好了很多介面.我們可以通過cv2.cuda.GpuMat, cv2.UMat呼叫cuda和opencl的演算法.

    通過baseImage可以快速的建立對應格式的影像
from baseImage import Image
from baseImage.constant import Place
      
Image('tests/images/1.png', place=Place.GpuMat) 
Image('tests/images/1.png', place=Place.UMat) 

可以用cuda加速的識別方法, 需要呼叫其他的類函式,且圖片格式需要是cv2.cuda.GpuMat

  • surf: 沒寫,下次再補
  • orb: 對應函式image_registration.matching.keypoint.orb.CUDA_ORB
  • matchTemplateimage_registration.matching.template.matchTemplate.CudaMatchTemplate

可以用opencl加速的識別方法, 只需要傳影像引數的時候,格式是UMat,opencv會自動的呼叫opencl方法

  • surf
  • orb
  • matchTemplate

這邊只講了特徵點獲取/模板匹配的方法,在其他的影像處理函式中cudaopencl也能有一定的加速,但是不如以上方法明顯

  1. 從框架設計上進行加速.(可能只限於遊戲應用,傳統app用不了)
  2. 從遊戲上講,我們預先知道一些控制元件,在螢幕中的座標位置.解析度進行轉換時,我們可以通過計算控制元件的位置,裁剪對應位置的影像,通過模板匹配進行快速的識別.

    • 舉個例子,下面兩張圖,一個是1280x720下的截圖,一個是2532x1170下的截圖
    • 1280x720下郵件控制元件的座標範圍是Rect(372,69,537,583)
    • 通過下面的計算方式,我們可以得出2532x1170下,範圍是Rect(828,110,874,949),通過裁剪軟體取得的範圍是Rect(830,112,874,948)
    • 具體的原理是利用了,引擎的縮放和錨點原理,反向求出座標範圍.去適應一些黑邊,劉海的情況.
    • 求出範圍後,裁剪範圍的圖片,和模板去做匹配,就可以快速的識別一些固定位置的控制元件
from baseImage import Rect
from baseImage.coordinate import Anchor, screen_display_type, scale_mode_type


anchor = Anchor(
    dev=screen_display_type(width=1280, height=720),
    cur=screen_display_type(width=2532, height=1170, top=0, bottom=0, left=84, right=84),
    orientation=1, mainPoint_scale_mode=scale_mode_type(), appurtenant_scale_mode=scale_mode_type()
)

rect = Rect(371, 68, 538, 584)
point = anchor.point(rect.x, rect.y, anchor_mode='Middle')
size = anchor.size(rect.width, rect.height)
print(Rect.create_by_point_size(point, size))
# <Rect [Point(828.9, 110.5), Size[874.2, 949.0]]


  1. 建立模板庫,預先載入模板,得到螢幕圖片後,通過一些相似度計算baseImage.utils.ssim對場景進行識別與分類,然後去識別相應場景的特徵點.用這樣的方法去減少計算量

    • 這邊其實有想法去擴充套件到深度學習,比如之前說的影像分類.首先我們建立了一個很大的模板庫,可以拆分出來介面1, 介面2,介面3和一些通用控制元件
    • 再通過分類去獲得當前在什麼介面,然後只識別這個介面的控制元件,達到減少計算量的作用

6)備註

有其他疑問的話,可以在testerhome的遊戲測試qq群裡找到我581529846

原文由hakaboom發表於TesterHome社群,點選原文連結可與作者直接交流。


今日份的知識已攝入~
想了解更多前沿測試開發技術:歡迎關注「第十屆MTSC大會·上海」>>>
1個主會場+12大專場,大咖雲集精英齊聚
12個專場包括:
知乎、OpenHarmony、開源、遊戲、酷家樂、音視訊、客戶端、服務端、數字經濟、效能提升、質量保障、智慧化測試

相關文章