作者:AchillesL
若轉載文章,請標明文章出處
1.序
??上周實現了用程序自動填充數獨(詳情見《記數獨X--Android openCV識別數獨并自動求解填充APP開發過程》),這次想試試能不能用同樣的方法玩“連連看”。實際上是可以的,解決連連看的真正難點在于:圖片分類,即需要識別游戲中共有多少張不同種類的圖片,并匹配相同的圖片。對于求解連連看的算法并不非常難,本文有具體的介紹。
??本文重點涉及到的內容有:感知哈希算法、連連看求解算法、屏幕模擬點擊。
??附LinkGameX運行的效果圖,解一局連連看也就只需幾秒:
2.下載鏈接
??LinkGameX APP:點擊下載 提取碼:ez6v
??本文用到的連連看游戲APP:點擊下載 提取碼:clnh
??LinkGameX GitHub地址:https://github.com/AchillesLzg/jianshu-LinkGameX
3.本文內容
- 實現思路
- 項目介紹
- openCV配置與無Root截屏
- 圖片相似度判斷算法
- 連連看求解算法
- 后記
- 參考文章
4.實現思路
??步驟1:通過截屏,獲取游戲的界面圖像。為了避免遮擋游戲界面,本應用應該做成懸浮窗形式。
??步驟2:得到連連看游戲面板的矩形區域后,計算并截取每個小圖片,通過感知哈希算法生成每個小圖片的特征序列。
??步驟3:遍歷每個小圖片與其他圖片的特征序列,計算兩者的相識度,超過特定閾值可以認為是同一圖片,否則是不同的圖片,并最終將結果存入數組中。
??步驟4:使用連連看求解算法計算點擊序列。
??步驟5:進行屏幕模擬點擊。
5.項目介紹
??項目主要包含文件如下圖:
類名 | 功能 |
---|---|
LinkGameXAccessibility | 該類繼承AccessibilityService,用于實現屏幕模擬點擊 |
LinkGameXAnalyse | 該類主要實現連連看求解 |
LinkGameXPicAnalyse | 該類主要實現圖片識別、匹配相同的圖片 |
LinkGameXService | 該類用于實現懸浮窗,實現應用的工作窗口,實現本應用的主要邏輯 |
LinkGameXUtils | 該類存放了廣播的Action,屏幕大小等常量信息 |
LocInfo | 該類記錄連連看某格子的行列號 |
MainActivity | 該類實現應用的啟動窗口,主要用于申請權限、截圖等操作 |
ScreenShotHelper | 該類為截圖助手類,封裝了獲取截屏圖片的一些方法 |
6.openCV配置與無Root截屏
??我們需要截取其他APP的界面,因此需要用到截屏,得到截屏圖片后,需要使用openCV的部分功能進行圖像處理,因此也需要openCV的配置。此部分內容筆者有文章專門介紹,本文不再贅述。
??在Android Studio中配置openCV項目
??Android 5.0 無Root權限實現截屏
7.圖片相似判斷算法
??判斷兩張圖片內容是否相同,有很多算法可以實現。本項目需要兼顧性能與精度,最終選用了感知哈希算法(Perceptual hash algorithm)。
感知哈希算法(perceptual hash algorithm),它的作用是對每張圖像生成一個“指紋”(fingerprint)字符串,然后比較不同圖像指紋。結果越接近,就說明圖像越相似。
??感知哈希算法實現步驟:
縮小尺寸:將圖像縮小到8*8的尺寸,總共64個像素。這一步的作用是去除圖像的細節,只保留結構/明暗等基本信息,摒棄不同尺寸/比例帶來的圖像差異;
簡化色彩:將縮小的圖像,轉為64級灰度,即所有像素點總共只有64種顏色。
計算平均值:計算所有64個像素的灰度平均值;
比較像素的灰度:將每個像素的灰度,與平均值進行比較,大于或等于平均值記為1,小于平均值記為0;
計算哈希值:將上一步的比較結果,組合在一起,就構成了一個64位的整數,這就是這張圖像的指紋。組合的次序并不重要,只要保證所有圖像都采用同樣次序就行了;
得到指紋以后,就可以對比不同的圖像,看看64位中有多少位是不一樣的。在理論上,這等同于”漢明距離”(Hamming distance,在信息論中,兩個等長字符串之間的漢明距離是兩個字符串對應位置的不同字符的個數)。如果不相同的數據位數不超過5,就說明兩張圖像很相似;如果大于10,就說明這是兩張不同的圖像。
??可以看到,該算法將圖片歸一化到8*8大小,并將彩色轉灰度,因此很大程度去掉了不重要的細節,只保留了圖像的輪廓、結構的信息,以此來判斷兩圖像是否相同。
??事情真的這么簡單?!ヾ(?°?°?)??
??經過筆者的試驗,該算法的準確度可以達到98%左右,但這個精度滿足本項目的要求嗎?然而并沒有!因為測試用的連連看APP共7行12列,84個格子,84×98%≈79,即每局游戲很可能會出現一兩張圖片時匹配錯誤的。如果連圖片100%匹配也無法保證,更不用提求解算法了。
??o(▼皿▼メ;)o!!o(▼皿▼メ;)o??!o(▼皿▼メ;)o!!o(▼皿▼メ;)o!!o(▼皿▼メ;)o?。?/strong>
??難道就這樣做不下去了嗎?也不是的,筆者思考后,至少還有兩個地方可以繼續優化:
- 連連看中格子圖片過小,而且可能會出現比較類似的圖片,前面我們把圖片歸一化到8×8大小64灰度,可能會過多地省略了細節。筆者對感知哈希算法做了一些調整:圖片歸一化到16×16大小,保持255灰度。避免省去過多的細節。
- 目前為止,我們是通過連連看面板的信息,通過計算得到的小格子區域,但可能計算誤差,即使相同圖案的格子,也可能存在位置的偏差。為了修正這個偏差,我們可以通過openCV的輪廓檢測直接把圖像的區域”扣“出來。這個也可以進一步提高感知哈希算法算法的精確度。
??筆者在完成這兩步的優化后,基本可以做到每局游戲的圖片匹配精度為100%。使用openCV進行輪廓檢測,可以參考筆者另一篇文章《記數獨X--Android openCV識別數獨并自動求解填充APP開發過程》中的相關部分。
??【注】本部分代碼主要在LinkGameXPicAnalyse類中實現,在這里不再貼代碼。
8.連連看求解算法
??在編寫連連看求解算法前,我們需要先知道連連看的游戲規則:若兩點間的連接路徑不超過兩個拐點,則該兩點可以被消除。所以,兩點可消除,共有三種情況。
8.1 情況分類
8.1.1 情景一:兩點間路徑沒有拐點
??如上圖,若兩點在同一行或者同一列上,兩點是可直達的。這種情況很好判斷,只需要判斷兩點的路徑上是否滿足所有的格子都被消除即可,即AB兩點是否連通。
8.1.2 情景二:兩點間路徑有一個拐點
??若兩點路徑有一個拐點,此時的A、B點必然不在同一行或者同一列上,并且A→B存在兩條路徑,各有一個拐點。此時,我們只需判斷兩條路徑的拐點是否有其中一個能滿足能同時與AB點連通,若滿足,A與B可消除。
8.1.3 情景三:兩點間的路徑有兩個拐點
??如上圖,A→B(實際情況AB不一定同行或同列)存在一條路徑,路徑上有兩個拐點。這種情況看上去很復雜,其實不然,這個問題我們可以轉換為剛才的情況:我們能不能找到一個拐點C,使得拐點C與拐點B能連通,并只存在一個拐點?明顯地,這個拐點C與A必定同行或同列,我們只需遍歷點A的同行與同列的格子,若找到一個這樣的拐點C,則說明AB點可消。
8.2 代碼實現
??我們稍加留心,就會發現:判斷AB是否滿足情景二(一個拐點),會調用到判斷情景一(直連)的功能;判斷AB是否滿足情景三(兩個拐點),會調用到情景二(一個拐點)的功能。可見,代碼復用程度是很高的。
??實現代碼時,我們用到布爾值二維數組,用于判斷某個格子是否已消除。需要注意的是,判斷兩個拐點時可能會存在兩拐點在矩形區域外的情況,因此我們的布爾值二維數組范圍應該是bool[行+2][列+2]
,而邊界值全部置為true,表示已消除(可連通)。
??關鍵代碼:
(LinkGameXAnalyse.java)
...
//沒有拐點的情況
private boolean canLink1(LocInfo locInfo1, LocInfo locInfo2) {
if (locInfo1.x != locInfo2.x && locInfo1.y != locInfo2.y) return false;
if (locInfo1.x == locInfo2.x) {
int min = Math.min(locInfo1.y, locInfo2.y);
int max = Math.max(locInfo1.y, locInfo2.y);
for (int i = min + 1; i < max; i++) {
if (!isPointDismiss(new LocInfo(locInfo1.x, i))) {
return false;
}
}
} else {
int min = Math.min(locInfo1.x, locInfo2.x);
int max = Math.max(locInfo1.x, locInfo2.x);
for (int i = min + 1; i < max; i++) {
if (!isPointDismiss(new LocInfo(i, locInfo1.y))) {
return false;
}
}
}
return true;
}
//只有一個拐點的情況
private boolean canLink2(LocInfo locInfo1, LocInfo locInfo2) {
LocInfo crossPont1 = new LocInfo(locInfo1.x, locInfo2.y);
if (isPointDismiss(crossPont1)) {
return (canLink1(locInfo1, crossPont1) && canLink1(crossPont1, locInfo2));
}
LocInfo crossPont2 = new LocInfo(locInfo2.x, locInfo1.y);
if (isPointDismiss(crossPont2)) {
return (canLink1(locInfo1, crossPont2) && canLink1(crossPont2, locInfo2));
}
return false;
}
//有兩個拐點的情況
private boolean canLink3(LocInfo locInfo1, LocInfo locInfo2) {
for (int i = 0; i < flagRow; i++) {
LocInfo locInfo = new LocInfo(i, locInfo1.y);
if (!isPointDismiss(locInfo) || !canLink1(locInfo1, locInfo)) continue;
if (canLink2(locInfo, locInfo2)) {
return true;
}
}
for (int i = 0; i < flagCol; i++) {
LocInfo locInfo = new LocInfo(locInfo1.x, i);
if (!isPointDismiss(locInfo) || !canLink1(locInfo1, locInfo)) continue;
if (canLink2(locInfo, locInfo2)) {
return true;
}
}
return false;
}
...
9.屏幕模擬點擊
??實現屏幕的模擬點擊,一般比較可行有兩個方法:一是申請root權限,調用adb指令執行屏幕點擊。二是通過調用AccessibilityService新增了dispatchGesture
方法,發送手勢,當發送的手勢Path是一個點時,表示點擊操作。由于方法一需要root權限,且adb的指令執行速度慢,本項目采用第二個方法。注意首先這個方法是7.0之后加入的,所以最小版本改為24。
??【注】對于AccessibilityService的入門介紹,可見筆者的另一篇文章《記數獨X--Android openCV識別數獨并自動求解填充APP開發過程》的相關部分,在此不再贅述。
??在LinkGameXAccessibility的onCreate方法中,我們需要生產連連看小格子的屏幕坐標,以便后序點擊時使用,關鍵代碼:
(LinkGameXAccessibility.java)
...
public class LinkGameXAccessibility extends AccessibilityService {
private static final String TAG = "LinkGameXAccessibility";
private ArrayList<ArrayList<Point>> mPoints = new ArrayList<>();
@Override
public void onCreate() {
super.onCreate();
initPanelPointList();
}
private void initPanelPointList() {
double width = LinkGameXUtils.RECT_WIDTH * 1.0 / LinkGameXUtils.COL;
double high = LinkGameXUtils.RECT_HEIGH * 1.0 / LinkGameXUtils.ROW;
for (int i = 0; i < LinkGameXUtils.ROW; i++) {
ArrayList<Point> points = new ArrayList<>();
for (int j = 0; j < LinkGameXUtils.COL; j++) {
int x = (int) (LinkGameXUtils.RECT_Y + width * j + width / 2);
int y = (int) (LinkGameXUtils.RECT_X + high * i + high / 2);
points.add(new Point(x, y));
}
mPoints.add(points);
}
}
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
Log.d(TAG, "onAccessibilityEvent: " + event.toString());
}
@Override
public void onInterrupt() {
}
}
...
??通過dispatchGesture完成模擬點擊,關鍵代碼:
(LinkGameXAccessibility.java)
...
public void dispatchGestureView(int startTime, int x, int y) {
Point position = new Point(x, y);
GestureDescription.Builder builder = new GestureDescription.Builder();
Path p = new Path();
p.moveTo(position.x, position.y);
/**
* StrokeDescription參數:
* path:筆畫路徑
* startTime:時間 (以毫秒為單位),從手勢開始到開始筆劃的時間,非負數
* duration:筆劃經過路徑的持續時間(以毫秒為單位),非負數*/
builder.addStroke(new GestureDescription.StrokeDescription(p, startTime, 1));
dispatchGesture(builder.build(), null, null);
}
...
??【注】該部分主要在LinkGameXAccessibility類中實現。
10.后記
??該項目還是有很多地方可以繼續優化的,如:
- 一局連連看有可能無法一次性完成,需要多次截屏,該功能在這里還沒實現。
- 有些連連看的游戲界面不一定是方方正正的矩陣區域,對于異型連連看,還需要判斷哪些區域是背景圖片。
- 可嘗試采用其他圖像識別算法判斷兩圖片是否內容相同。