記LinkGameX--Android openCV識別連連看并模擬點擊APP開發過程

作者: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)字符串,然后比較不同圖像指紋。結果越接近,就說明圖像越相似。

??感知哈希算法實現步驟:

  1. 縮小尺寸:將圖像縮小到8*8的尺寸,總共64個像素。這一步的作用是去除圖像的細節,只保留結構/明暗等基本信息,摒棄不同尺寸/比例帶來的圖像差異;

  2. 簡化色彩:將縮小的圖像,轉為64級灰度,即所有像素點總共只有64種顏色。

  3. 計算平均值:計算所有64個像素的灰度平均值;

  4. 比較像素的灰度:將每個像素的灰度,與平均值進行比較,大于或等于平均值記為1,小于平均值記為0;

  5. 計算哈希值:將上一步的比較結果,組合在一起,就構成了一個64位的整數,這就是這張圖像的指紋。組合的次序并不重要,只要保證所有圖像都采用同樣次序就行了;

  6. 得到指紋以后,就可以對比不同的圖像,看看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>

??難道就這樣做不下去了嗎?也不是的,筆者思考后,至少還有兩個地方可以繼續優化:

  1. 連連看中格子圖片過小,而且可能會出現比較類似的圖片,前面我們把圖片歸一化到8×8大小64灰度,可能會過多地省略了細節。筆者對感知哈希算法做了一些調整:圖片歸一化到16×16大小,保持255灰度。避免省去過多的細節。
  2. 目前為止,我們是通過連連看面板的信息,通過計算得到的小格子區域,但可能計算誤差,即使相同圖案的格子,也可能存在位置的偏差。為了修正這個偏差,我們可以通過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.后記

??該項目還是有很多地方可以繼續優化的,如:

  • 一局連連看有可能無法一次性完成,需要多次截屏,該功能在這里還沒實現。
  • 有些連連看的游戲界面不一定是方方正正的矩陣區域,對于異型連連看,還需要判斷哪些區域是背景圖片。
  • 可嘗試采用其他圖像識別算法判斷兩圖片是否內容相同。

11.參考文章

??圖像相似度計算之哈希值方法OpenCV實現

??連連看所屬算法分析與實現

??記數獨X--Android openCV識別數獨并自動求解填充APP開發過程

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,443評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,530評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,407評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,981評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,759評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,204評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,263評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,415評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,955評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,650評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,892評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,675評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,967評論 2 374

推薦閱讀更多精彩內容