談談如何使用 opencv 進行影像識別
1)前言
從 18 年開始,我接觸了叉叉助手 (平臺已經被請喝茶了),透過圖色識別,用來給常玩的遊戲寫掛機指令碼,寫了也有兩三年.也算是我轉行當遊戲測試的理由.
去年 11 月,也是用了這身技術,混進了外包,薪資還不錯,屬於是混日子了,崗位是在發行,接觸到很多遊戲,因為接不了 poco,到手只有 apk,
日積月累,遊戲越來越多,專案組卻還是隻有這點人.為了減輕自己的壓力,就開始了 UI 自動化的不歸路.
2)遊戲 UI 自動化
因為遊戲引擎,是無法透過 appium 等框架去獲取,如果不接入一些 SDK,那麼識別的方法只有影像識別.現在常見的開源框架
- 網易的 Airtest,透過傳統識別進行自動化,還有 airtestIDE 可以簡單快速的編寫 airtest 程式碼
- 騰訊 GameAISDK,透過深度學習進行自動化 (沒用過,好久不維護了)
- 阿里的 SoloPi,主要功能是錄製、群控,有影像匹配輔助
影像相關的常見方法:
- 傳統的識別方法: 特徵點、模板、輪廓
- 特徵點: SIFT, ORB
- 下文會詳細講
- 模板匹配: opencv 的 matchTemplate
- 最簡單的方案,透過講模板在目標影像中平移,找到最符合的目標
- 輪廓: HALCON Shape-based Matching, Canny
- 沒用過,寫不來,halcon 的要花錢
- 特徵點: SIFT, ORB
- 基於深度學習的方法:
- 文字識別: PaddleOCR,tesseract
- paddleOCR 基本上開箱即用,但是對於遊戲內的藝術字,還需要額外的訓練
- 影像分類: paddleClas
- 沒有實際用過,感覺可以用在區分場景,然後去做更加詳細的識別.比如識別彈窗
- 目標檢測: yolo
- 之前很火的 Fps 外掛,基本就是靠這個去識別人體
- 文字識別: PaddleOCR,tesseract
UI 自動化的核心在於查詢元素,並且在什麼位置.那麼重點就會放在影像識別上.
基於深度學習的方案,需要大量的正負樣本和標註工作,因此只能放棄.取而代之的是傳統的識別方案.
在社群裡、qq 的測試群裡就能發現,大多數人對傳統影像識別的印象是:慢,不準.
今年過年前,去張江面試過一家遊戲公司,也是發行公司,聊了一個多小時,聊下來他們的方案是 airtest 一種機型截一個圖去做適配.我大受震撼.
總結下來影像識別的 UI 自動化難點:
- 識別慢
- 識別結果不準確
- 多解析度不相容性
- 遊戲 UI 更新,管理圖片庫的成本
3)怎麼解決
專案就在這裡::https://github.com/hakaboom/image_registration
一開始是參考了 airtest 的 aircv 部分,當時不想有那麼多依賴,就拆出來了.
重構之後,透過對 opencv 一些 api 的封裝,重新組織了構架和演算法
安裝 opencv-python
建議版本可以是 4.5.5
- pypi 上有編譯好的,但是隻能用 cpu 方法:
pip install opencv-python
pip install opencv-contrib-python
- 從原始碼編譯,可以自定義更多的東西,比如增加 cuda 支援
- 先從 opencv 倉庫克隆程式碼
- 剩下的看這裡 https://github.com/hakaboom/py_image_registration/blob/master/doc/cuda_opencv.md
什麼是特徵點
簡單的理解: 用於描述影像特徵的關鍵點
常見的特徵點提取演算法:
- SIFT: 尺度不變特徵變換. opencv 只有 cpu 實現
- SURF: surf 的加速演算法. opencv 有 cpu 和 cuda 實現
- 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/AirtestProject/Airtest/blob/d41737944738e651dd29564c29b88cc4c2e71e2e/airtest/aircv/keypoint_base.py#L133
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.根據透視變換或座標計算,獲取矩形,然後計算置信度
那麼以上步驟會存在什麼問題:
- 在第二步,假設圖片中存在
n
個目標圖片,如果`k<n',就會導致匹配特徵點數量變少 - 在第三步,篩選的方法不太合理,實際 debug 中會發現,一些特徵點即使
distance
數值很高,但從結果上看,還是符合目標的,那麼就意味著單純根據距離去篩選特徵點 的方法是不靠譜的 - 在第四步,獲取完特徵點後,airtest 的方式是,根據透視變換獲取目標的四個頂點座標,計算出最小外接矩形. 那麼如果目標圖片存在旋轉/形變,那麼最後獲取的圖片會裁剪到多餘目標,造成置信度降低
修改後的特徵點匹配
核心在於利用特徵點尺度和旋轉不變性
1.讀取圖片
from baseImage import Image
im_source = Image('tests/image/6.png')
這邊用到了我另外一個庫 https://github.com/hakaboom/base_image
主要的用處對 opencv 的影像資料進行格式和型別的轉換,以及一些介面的包裝
- 使用 place 引數,修改資料格式
- Ndarray: 格式為 numpy.ndarray 格式
- 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.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
對應的queryIdx
和trainIdx
的 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)怎麼最佳化速度
- 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
- matchTemplate
image_registration.matching.template.matchTemplate.CudaMatchTemplate
可以用 opencl 加速的識別方法, 只需要傳影像引數的時候,格式是UMat
,opencv 會自動的呼叫opencl
方法
- surf
- orb
- matchTemplate
這邊只講了特徵點獲取/模板匹配的方法,在其他的影像處理函式中cuda
和opencl
也能有一定的加速,但是不如以上方法明顯
- 從框架設計上進行加速.(可能只限於遊戲應用,傳統 app 用不了)
- 從遊戲上講,我們預先知道一些控制元件,在螢幕中的座標位置.解析度進行轉換時,我們可以透過計算控制元件的位置,裁剪對應位置的影像,透過模板匹配進行快速的識別.
- 舉個例子,下面兩張圖,一個是 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]]
- 建立模板庫,預先載入模板,得到螢幕圖片後,透過一些相似度計算
baseImage.utils.ssim
對場景進行識別與分類,然後去識別相應場景的特徵點.用這樣的方法去減少計算量- 這邊其實有想法去擴充套件到深度學習,比如之前說的影像分類.首先我們建立了一個很大的模板庫,可以拆分出來
介面1
,介面2
,介面3
和一些通用控制元件
- 再透過分類去獲得當前在什麼介面,然後只識別這個介面的控制元件,達到減少計算量的作用
- 這邊其實有想法去擴充套件到深度學習,比如之前說的影像分類.首先我們建立了一個很大的模板庫,可以拆分出來
#6)備註
有其他疑問的話,可以在 testerhome 的遊戲測試 qq 群裡找到我 157875133
#7) 更新
2022/5/15
- 最新的 pr 上,用 numpy 代替了原本的 for 迴圈計算,特徵點篩選的速度快了 10 多倍.
- 之前的基準測試不太嚴謹,現在用了 nvidia Nsight Systems 去拉資料.
這邊遇到了一個問題,不清楚是不是 bug. windows 環境下 opencv 呼叫
cuFFT
庫時,大部分時間都在載入庫上,就是圖中的黃色色塊.甚至在1050ti
上的速度都比3080Ti
快.目前還沒有解決,建議還是在 linux 下部署
下圖是 ubuntu 下的資料. 識別流程在 40~50 毫秒. SM 的佔用也有空餘,可以使用 Mps 等服務,開多程序去多併發.
相關文章
- iOS下使用OpenCV進行影象識別iOSOpenCV
- 使用 Nim 進行基礎影像識別
- 使用 Racket 進行基礎影像識別Racket
- 使用 OCaml 進行基礎影像識別
- 使用 Lua 進行基礎影像識別
- 如何使用機器學習進行影像識別 | 資料標註機器學習
- 走進 JDK 之 談談基本型別JDK型別
- 【雜談】如何對Redis進行原子操作Redis
- 卷積神經網路進行影像識別卷積神經網路
- C#+OpenCV進階(二)_文字識別C#OpenCV
- 從Kubectl Top說起,談談Kubernetes是如何進行資源監控的?
- 從影像融合談起
- C#+OpenCV進階(一)_人體識別C#OpenCV
- 使用Tesseract進行圖片文字識別
- [譯] 使用 WFST 進行語音識別
- 1995年的資深工程師,和你談談如何進階工程師
- 樹莓派利用OpenCV的影像跟蹤、人臉識別等樹莓派OpenCV
- 影像識別
- 淺談如何搭建知識體系
- OpenCV使用ParallelLoopBody進行平行計算OpenCVParallelOOP
- 如何使用Mask RCNN模型進行影像實體分割?CNN模型
- opencv 影像的型別轉換、影像的縮放OpenCV型別
- opencv 人臉識別OpenCV
- 走進 JDK 之談談字串拼接JDK字串
- ONNX Runtime入門示例:在C#中使用ResNet50v2進行影像識別C#
- 談談mysql和redis的區別MySqlRedis
- 談談import和require的區別ImportUI
- 使用 OpenCV-Python 識別答題卡判卷OpenCVPython
- 談談面試知識點準備面試
- 談談Markdown的認識與入門
- 談談stream的執行原理
- orange影像識別
- python影像識別Python
- 談談BUG嚴重級別(severity)管理
- 談談Java基礎資料型別Java資料型別
- 談一談Coders Programmer Developer的區別Developer
- [譯]計算機如何高效識別影像?計算機
- 談談網路協議 – 基礎知識協議