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

1 序

  數獨是源自18世紀瑞士的一種數學遊戲。是一種運用紙、筆進行演算的邏輯遊戲。玩家須要根據9×9盤面上的已知數字,推理出全部剩餘空格的數字,並知足每一行、每一列、每個粗線宮(3*3)內的數字均含1-9,不重複。html

數獨

​  最近一段時間經常作數獨題(此處插入廣告:推薦一個很是良心的數獨APP點擊下載),並思考了一下能不能編寫一個APP,能夠自動求解數獨、最後將結果填入該APP中。java

..............................................................摸魚的開發過程,此處省略10^N^行字.............................................................node

​  最終寫一個APP:數獨X。能夠針對筆者經常使用的數獨APP(本文的實現都基於該APP),實現數獨的識別、求解、並把答案自動填入。專家級別的平均1秒完成求解(包括圖像數字提取,識別過程),8s完成所有操做。android

​  本文將簡單介紹相關功能的實現。數獨X的使用效果,以下圖:git

圖片描述

2 下載連接

​  數獨 APP連接:https://pan.baidu.com/s/1b67L...github

​  數獨X APP連接:https://pan.baidu.com/s/1xJMT...算法

​  數獨X 源代碼連接:https://github.com/AchillesLz...shell

​  【注】數獨X對手機要求:Android 7.0 或以上數組

3 本文內容

  • 實現思路介紹
  • 項目結構介紹
  • 如何建立懸浮窗
  • 如何獲取第三方應用中的控件信息
  • 如何無Root實現跨應用截屏
  • 如何提取數獨九宮格中的數字
  • 如何實現數字識別
  • 如何編寫代碼求解數獨
  • 如何實現模擬屏幕點擊
  • 後記
  • 參考文章

4 實現思路介紹

​  步驟一:咱們須要得到數獨APP中的九宮格數字。因爲數獨App是第三方應用,數獨信息固然是沒法直接獲取的,筆者的思路是打開數獨界面後調用截屏,再經過圖片處理提取九宮格的數字。同時,爲了不截屏時遮擋應用,數獨X的工做窗口應該使用懸浮窗形式。安全

​  步驟二:截屏後,咱們須要進一步截取數獨面板圖片,以便數字提取用。咱們能夠寫死麪板座標、寬高來提取截圖中的面板。在這裏,固然有更好的方法,就是經過輔助功能AccessibilityService得到數獨應用的數獨面板座標信息。

​  步驟三:在得到數獨面板的圖片後,使用openCV框架提取數字的輪廓,生成數字圖片,再調用TessTwo框架將圖片轉爲數字,並生成原始數獨二維數組。

​  步驟四:數獨求解,生成答案,並生成須要填充的數字序列。

​  步驟五:最後經過輔助功能AccessibilityService類的相關方法,模擬屏幕點擊,輸入填充數獨的數字。

流程圖

5 項目結構介紹

​  項目主要包含文件以下圖:

圖片描述

​  主要做用:

類名 功能
FileStorageHelper 該類封裝了把asset目錄下複製到SD卡的相關方法
LocTextInfo 該類記錄數獨某格子的行列號,及對應的數字
MainActivity 該類實現應用的啓動窗口,主要用於申請權限、截圖等操做
ScreenShotHelper 該類爲截圖助手類,封裝了獲取截屏圖片的一些方法
SPUtils 該類封裝了SharedPreferences的一些操做
SudokuAccessibility 該類繼承AccessibilityService,實現第三方應用的控件獲取、屏幕模擬點擊
SudokuXAnalyse 該類用於數獨求解,輸入原始的數獨二維數組,返回求解後的數獨二維數組
SudokuXOrc 該類用於數獨識別,輸入數獨圖片Bitmap,返回原始的數獨二維數組
SudokuXService 該類用於實現懸浮窗,實現應用的工做窗口,實現數獨X的主要邏輯
SudokuXUtils 該類存放了廣播的Action,屏幕大小等常量信息
TessTwoHelper 該類封裝了TessBaseApi的相關方法,實現文字識別

時序圖

6 如何建立懸浮窗

​  Android的界面繪製,都是經過WindowMananger的服務來實現的。要實現一個可以在自身應用界面外的懸浮窗,咱們就要利用WindowManager類。同時,爲了讓懸浮窗與Activity脫離,讓應用處於後臺時懸浮窗仍然能夠正常運行,這裏使用Service來啓動懸浮窗並作爲其背後邏輯支撐。

6.1 申請權限

​  在建立懸浮窗前,必須先申請權限,代碼十分簡單:

(MainActivity.java)

...
private boolean startOverLay() {
    if (!Settings.canDrawOverlays(MainActivity.this)) {
        Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
        Toast.makeText(this, "須要取得權限以使用懸浮窗",Toast.LENGTH_SHORT).show();
        startActivity(intent);
        return false;
    }
    return true;
}
...

6.2 在service中建立懸浮窗

(SudokuXService.java)

...
private void initView() {
    //注意Android O版本與其餘系統的差別
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        mParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
    } else {
        mParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
    }
    mParams.format = PixelFormat.RGBA_8888;
    mParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
    mParams.gravity = Gravity.START | Gravity.TOP;
    mParams.x = SudokuXUtils.getScreenWidth();
    mParams.y = SudokuXUtils.getScreenHeight();
    mParams.width = SudokuXUtils.SMALL_SIZE_WIDTH;
    mParams.height = SudokuXUtils.SMALL_SIZE_HIGH;
    LinearLayout linearLayout = (LinearLayout) LayoutInflater.from(getApplication()).inflate(R.layout.layout, null);
    mBtn = linearLayout.findViewById(R.id.btn);
    
    //添加懸浮窗佈局到WindowManager中
    mWindowManager.addView(linearLayout, mParams);
    ...
}
...

​  最後在首頁啓動SudokuXService便可,講述Android懸浮窗的文章不少,讀者可自行查閱,在此再也不贅述。

​  【注】這部分的代碼主要在SudokuXService.java中實現。

7 如何得到其餘APP中的控件信息

​  本項目使用Android的輔助服務AccessibilityService來獲取數獨APP的控件信息。

7.1 介紹

​ AccessibilityService設計初衷在於幫助殘障用戶使用android設備和應用,在後臺運行,能夠監聽用戶界面的一些狀態轉換,例如頁面切換、焦點改變、通知、Toast等,並在觸發AccessibilityEvents時由系統接收回調。後來被開發者另闢蹊徑,用於一些插件開發,好比微信紅包助手,還有一些須要監聽第三方應用的插件。

​ 咱們能夠把AccessibilityService理解爲——『按鍵精靈』。相信不少開發者都玩過PC上的這款軟件,他的做用,就是將你一次操做的整個記錄,錄製下來,而後就能夠根據這個記錄,重複的執行這些操做,例如:先點擊某個輸入框,再輸入XXXX,再輸入驗證碼,最後點擊某按鈕,這些操做若是須要重複執行,那麼顯然是一套機械的步驟,那麼經過按鍵精靈,記錄下這些操做後,直接經過腳本就能夠完成這些操做。其實AccessibilityService跟這個是同樣的,咱們記錄的,實際上就是咱們的操做步驟,或者稱之爲『腳本』,那麼系統在監控整個手機的各類AccessibilityService事件時,就會根據咱們的邏輯來判斷該使用哪個腳本。

​ 所以,咱們徹底能夠抽象出一個基類AccessibilityService,並抽象出一些腳本的事件,例如,根據Text查找對應的View、點擊某個View、滑動、返回等等。

7.2 配置

​  首先,要使用AccessibilityService實際上很是簡單,通常來講,只須要如下三步便可。

7.2.1 繼承系統AccessibilityService

public class SudokuAccessibility extends AccessibilityService {

    private static final String TAG = "lzg";

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        Log.d(TAG, "onAccessibilityEvent: " + event.toString());
    }

    @Override
    public void onInterrupt() {
    }
}

​  強制從新的有兩個方法:onAccessibilityEvent和onInterrupt。重點關注onAccessibilityEvent方法,在該方法中,咱們能夠接收所監聽的事件。

7.2.2 新建配置文件

​  在資源目錄res下新建xml文件夾,新建accessibility.xml文件,寫入:

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFeedbackType="feedbackSpoken"
    android:canRetrieveWindowContent="true"
    android:canPerformGestures="true"
    android:packageNames = "com.easybrain.sudoku.android"
    android:notificationTimeout="1000"/>

​  裏面有一些比較簡單的配置。本項目要輔助的是數獨應用,在xml的android:packageNames處指定輔助應用的包名,即com.easybrain.sudoku.android。當沒有指定時,默認輔助全部的應用,建議你們在使用時,指定須要監聽的包名(你能夠經過|來進行分隔),而不是全部的包名。typeAllMask是設置響應事件的類型,feedbackGeneric是設置回饋給用戶的方式,有語音播出和振動。

7.2.3 註冊

​  最後,在AndroidMainifest中註冊service信息:

<service
    android:name="com.example.sudokux.SudokuAccessibility"
    android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
    <intent-filter>
        <action android:name="android.accessibilityservice.AccessibilityService" />
    </intent-filter>

    <meta-data
        android:name="android.accessibilityservice"
        android:resource="@xml/accessibility" />
</service>

​  完成以上步驟後,一個輔助服務就可使用了,AccessibilityService具備很高的系統權限,因此,系統不會讓App直接設置是否啓用,須要用戶進入設置-輔助功能中去手動啓用,這樣在必定程度上,保護了用戶數據的安全。

​  這裏再也不贅述AccessibilityService的基本用法,有須要的讀者可參考相關文章,例如:AccessibilityService從入門到出軌

7.3 使用

​  本節介紹如何數獨APP的控件信息以及代碼編寫。

7.3.1 經過Layout Inspector工具,獲取數獨APP的控件信息

​  使用AccessibilityService拿到數獨APP的控件信息,咱們必須先知道對應的控件id。這一步,咱們可使用Android Studio的Layout Inspector工具來完成。

​  先啓動數獨APP,在Android Studio中,點擊Tools->Layout Inspector,選中包名:com.easybrain.sudoku.android,便可以看到一下畫面:

圖片描述

​  可見數獨面板id爲sudoku_board,1-9的數字按鈕id分別是button_1button_9

7.3.2 相關代碼

​  當數獨APP窗口發生變化時,將觸發SudokuAccessibility中onAccessibilityEvent方法。在此方法中,經過控件id獲取數獨面板與1-9數字按鈕控件的信息,而後計算並將相關信息使用SharedPreferences保存至本地。

​ 關鍵代碼:

(SudokuAccessibility.java)

public class SudokuAccessibility extends AccessibilityService {
    //記錄1-9數字按鈕的中心點座標
    private List<Point> mTypeNumberPointList = new ArrayList<>(9);
    //記錄數獨面板中81個小格子的中心點座標
    private List<List<Point>> mShuDuPanelPointList = new ArrayList<>(9);
    ...
    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        Log.d(TAG, "onAccessibilityEvent: " + event.toString());

        if (!mInitDataFlag) {
            initViewData(event);
        }
    }

    private void initViewData(AccessibilityEvent event) {
        AccessibilityNodeInfo root = getRootInActiveWindow();
        if (root == null) return;

        //初始化等待區數字1-9的中心位置
        for (int i = 0; i < 9; i++) {
            String id = String.format("com.easybrain.sudoku.android:id/button_%d", i + 1);
            List<AccessibilityNodeInfo> nodeInfos = root.findAccessibilityNodeInfosByViewId(id);
            if (!nodeInfos.isEmpty()) {
                Rect rect = new Rect();
                nodeInfos.get(0).getBoundsInScreen(rect);
                Point point = new Point(rect.centerX(), rect.centerY());
                mTypeNumberPointList.add(point);
            }
        }

        //生成數獨面板81個格子的中心位置
        String id = String.format("com.easybrain.sudoku.android:id/sudoku_board");
        List<AccessibilityNodeInfo> nodeInfos = root.findAccessibilityNodeInfosByViewId(id);
        if (!nodeInfos.isEmpty()) {
            Rect rect = new Rect();
            nodeInfos.get(0).getBoundsInScreen(rect);

            int step = (rect.bottom - rect.top) / 9;
            //計算81格中,第一個格子的中心點
            int x = rect.left + step / 2;
            int y = rect.top + step / 2;

            /*保存數獨面板的左上角頂點、高度信息,便於截取數獨面板時使用。*/
            saveSudokuBroadInfo(rect);

            for (int i = 0; i < 9; i++) {
                List<Point> points = new ArrayList<>(9);
                for (int j = 0; j < 9; j++) {
                    Point point = new Point(x + step * j, y + step * i);
                    points.add(point);
                }
                mShuDuPanelPointList.add(points);
            }
        }

        if (mShuDuPanelPointList.size() == 9 && mTypeNumberPointList.size() == 9) {
            mInitDataFlag = true;
            Toast.makeText(this, "數獨信息獲取成功!", Toast.LENGTH_SHORT).show();
        }
    }
    //保存數獨面板的座標信息,便於截取數獨面板圖片時使用
    private void saveSudokuBroadInfo(Rect rect) {
        SPUtils.put(SudokuAccessibility.this, SudokuXUtils.SP_RECT_LEFT, rect.left - 5);
        SPUtils.put(SudokuAccessibility.this, SudokuXUtils.SP_RECT_TOP, rect.top - 5);
        SPUtils.put(SudokuAccessibility.this, SudokuXUtils.SP_RECT_HEIGH, rect.bottom - rect.top + 10);
    }
    ...
}

​  【注】這部分代碼主要在SudokuAccessibility類中實現。

8 如何實現無Root權限截屏

​  Android在5.0以後提供了官方的截屏API,如今的手機Android版本廣泛在Android 5.0以上,該方法仍是有比較高的適用性。此時,不再須要經過root權限調用adb指令,或者使用輔助服務模擬截屏按鍵實現截屏了。

​  因爲節省文章篇幅,具體的實現讀者可參考筆者的另外一篇文章《Android 5.0 無Root權限實現截屏》

9 如何提取數獨九宮格中的數字

​  要求解數獨,須要進行計算,圖片格式的數字確定是不行的,因此必須把圖片上的數字轉換爲實實在在的數字才能進行計算。要獲得實實在在的數字,咱們須要作的是對圖片上的數字進行提取和識別。

​  本小節主要介紹數獨圖片中數字的提取(即獲取數字圖像區域),該功能本項目使用openCV實現。

9.1 介紹

OpenCV於1999年由 Intel創建,現在由Willow Garage提供支持。OpenCV是一個基於BSD許可(開源)發行的跨平臺計算機視覺庫,能夠運行在 LinuxWindowsMac OS操做系統上。它輕量級並且高效——由一系列 C 函數和少許 C++ 類構成,同時提供了Python、Ruby、MATLAB等語言的接口,實現了 圖像處理和計算機視覺方面的不少通用算法。

9.2 openCV的配置

​  在Android中配置openCV其實也很是簡單,可見筆者的另外一篇文章《在Android Studio中配置openCV項目》,在此再也不贅述。

9.3 openCV的使用

​  提取圖片內容的輪廓,咱們可使用openCV視覺庫Imgproc類中findContours()方法來實現。在對圖片進行輪廓識別時,先須要對圖片進行灰度化二值化處理,這裏先簡單介紹這兩個操做。

9.3.1 灰度化

​  咱們從findContours的參數要求中得知,第一個參數是圖像二值化後的Mat對象。在生成二值化的圖像前,咱們須要對圖像進行灰度化處理。

灰度化,在 RGB模型中,若是R=G=B時,則彩色表示一種灰度顏色,其中R=G=B的值叫 灰度值,所以,灰度圖像每一個像素只需一個字節存放灰度值(又稱強度值、亮度值),灰度範圍爲0-255。通常有份量法 最大值法平均值法加權平均法四種方法對彩色圖像進行灰度化。

​  使用openCV中對圖片灰度化的實現很簡單,只須要一行代碼便可:Imgproc.cvtColor(rgbMat, grayMat, Imgproc.COLOR_RGB2GRAY);

圖片描述

cvtColor方法的定義:

cvtColor(Mat src, Mat dst, int code)
參數名 含義
Mat src 原Mat對象
Mat dst 目標Mat對象
int code 本項目使用的是Imgproc.COLOR_RGB2GRAY,即RGB圖像轉灰度圖像

9.3.2 二值化

​  接下來要作圖像的二值化,簡單來講,就是把圖片變成只有黑色和白色的像素點。

圖像的二值化,就是將圖像上的像素點的 灰度值設置爲0或255,也就是將整個圖像呈現出明顯的只有黑和白的視覺效果。

​  一樣地,圖像二值化的實現也只需一行代碼:Imgproc.threshold(grayMat, binaryMat, 100, 255, Imgproc.THRESH_BINARY);

​  threshold方法的定義:

threshold(Mat src, Mat dst, double thresh, double maxval, int type)
參數名 含義
Mat src 原Mat對象
Mat dst 目標Mat對象
double thresh 閾值的具體值
double maxval type取THRESH_BINARY 或THRESH_BINARY_INV閾值類型時的最大值
int type THRESH_BINARY:像素值大於閾值時,取Maxval,也就是第四個參數,不然置爲0。
THRESH_BINARY_INV:當前點值大於閾值時,設置爲0,不然設置爲Maxval。
THRESH_TRUNC: 當前點值大於閾值時,設置爲閾值,不然不改變。
THRESH_TOZERO: 當前點值大於閾值時,不改變,不然設置爲0。
THRESH_TOZERO_INV: 當前點值大於閾值時,設置爲0,不然不改變。

​  在本項目中,thresh取值爲100typeTHRESH_BINARY,即像素值超過100的都置爲255,不然置爲0。注意這裏的thresh值的選用:能夠恰好將九宮格內的縱橫線去掉,在作數字提取的時候將會少判斷一層父輪廓。

圖片描述

9.3.3 輪廓識別

​  終於,咱們要對圖像進行輪廓識別。這一步將使用openCV視覺庫位於Imgproc類中findContours()方法實現。該方法定義以下:

findContours(Mat image, List<MatOfPoint> contours, Mat hierarchy, int mode, int method)
參數名 含義
Mat image 單通道圖像矩陣,通常是通過Canny、拉普拉斯等邊緣檢測算子處理過的二值圖像。
List<MatOfPoint> contours MatOfPoint是保存Point的Mat,繼承自Mat。
contours表示檢測到的輪廓,輪廓是由一系列的點構成,存儲在java 的list中,每一個list的元素是MatOfPoint。
Mat hierarchy 包含着圖像的拓撲信息,有和contours相同數量的元素。
對於每一個contours[i],對應的hierarchy[i][0], hiearchy[i][9], hiearchy[i][10]和 hiearchy[i][11]分別被設置同一層次的下一個,上一個,第一個孩子和父親的輪廓。 若是contour [i]不存在對應的contours,那麼相應的hierarchy[i] 就被設置成-1。
int mode contour的估計方式(4種):
RETR_EXTERNAL :只檢測最外圍的輪廓。
RETR_LIST :檢測全部輪廓,不創建等級關係,彼此獨立。
RETR_CCOMP :檢測全部輪廓,但全部輪廓都只創建兩個等級關係 。RETR_TREE :檢測全部輪廓,而且全部輪廓創建一個樹結構,層次完整。(本項目使用該參數)
RETR_FLOODFILL :洪水填充法。
int method contour的檢索方式(4種):
CHAIN_APPROX_NONE:保存物體邊界上全部連續的輪廓點。
CHAIN_APPROX_SIMPLE:壓縮水平方向,垂直方向,對角線方向的元素,只保留該方向的終點座標,例如一個矩形輪廓只需4個點來保存輪廓信息。(本項目使用該參數)
CV_CHAIN_APPROX_TC89_L1:使用Teh-Chin 鏈近似算法。
*CV_CHAIN_APPROX_TC89_KCOS:使用Teh-Chin 鏈近似算法。

  因爲數獨面板的輪廓包括各類的嵌套關係,此時mode參數選用RETR_TREE 。另外咱們只須要數字輪廓的矩陣信息便可,因此method參數選用CHAIN_APPROX_SIMPLE

9.3.4 關於層次(Hierarchy)的理解

  檢測輪廓的時候,有時候可能會出現其中一個輪廓包含了另一個輪廓,好比同心圓。這裏咱們認爲外側輪廓爲父輪廓,內側被包含的爲子輪廓。同一級別的又有前一個輪廓後一個輪廓。總的來講,hierarchy表達的是不一樣輪廓之間的聯繫。

  舉一個例子,下圖產生了7個輪廓信息:
圖片描述

  數組List<MatOfPoint> contours中共有7個輪廓信息,每一個輪廓的id則爲數組下標i。如id爲0的輪廓a是整個圖片的最外層輪廓、黑色邊框共有裏外兩個id爲1和2的輪廓b和c、數字1,3各自有一個輪廓f和g、數字4有兩個輪廓d和e,其中輪廓c是輪廓efg的父輪廓。

  第i個輪廓的前、後、子、父輪廓會保存在hierarchy[i][0], hiearchy[i][13], hiearchy[i][14]和 hiearchy[i][15]中。要找到上圖中的四、三、1三個數字輪廓,相對於要找到以輪廓c爲父輪廓的contour[i]便可。

  咱們處理數獨面板圖片時,也是同樣的思路,只是數獨面板比上圖再多了一層父輪廓。爲了理清楚輪廓關係,咱們在調用findContours方法生成輪廓信息後,用log打印出全部的輪廓信息,先找到9個九宮格的輪廓id,存放在數組tmp中。再遍歷contours數組,全部以tmp的元素爲父輪廓的輪廓,則是咱們最終須要的數字輪廓。以下圖所示,能夠看到父輪廓id爲1的都是九宮格的輪廓(紅框所示),以九宮格輪廓爲父輪廓的都是數字輪廓(綠框所示)。

​ 最後,咱們獲得的輪廓信息能夠經過Imgproc類的rectangle(Mat img, Point pt1, Point pt2, Scalar color)方法將輪廓繪製到圖像中,以便調試。

圖片描述

  使用openCV識別數字的部分已經完成,在這就不貼代碼了,有須要的讀者可參考項目中代碼。

  【注】這部分的代碼主要在SudokuXOrc類中實現。

10 如何實現數字識別

  上一小節,咱們已經能夠得到數獨圖片中的數字輪廓信息,能夠產生數獨數字圖片。在本小節,將介紹如何識別圖像中的文字。本項目使用tess-two ORC引擎實現圖像識別。

10.1 介紹

Tesseract是Ray Smith於1985到1995年間在惠普布里斯托實驗室開發的一個OCR引擎,曾經在1995 UNLV精確度測試中名列前茅。但1996年後基本中止了開發。2006年,Google邀請Smith加盟,重啓該項目。目前項目的許可證是Apache 2.0。該項目目前支持Windows、Linux和Mac OS等主流平臺。但做爲一個引擎,它只提供命令行工具。 現階段的Tesseract由Google負責維護,是最好的開源OCR Engine之一,而且支持中文。

tess-two是Tesseract在Android平臺上的移植。

10.2 tess-two的配置

  tess-two在Android Studio中的配置很是簡單,只須要如下三步便可。

10.2.1 在Android Studio中的引入依賴

dependencies {
    implementation 'com.rmtheis:tess-two:9.0.0'
}

10.2.2 下載tessdata語言數據文件

  數據文件 下載連接。咱們只須要識別數字,所以下載英文的語言數據eng.traineddata就能夠了。

圖片描述

10.2.3 配置tessdata語言數據文件

  這一步很重要!在手機的SD卡根目錄建立一個名爲tessdata的文件夾(必須是根目錄和tessdata命名),將下載好的語言數據文件eng.traineddata放入其中。

  【注】在實際的應用,咱們不可能要求用戶手動完成這步操做。通常的作法是將eng.traineddata文件存放在android項目的asset目錄中,在應用啓動時將其複製到SD卡中。

10.3 tess-two使用

  本項目將tess-two的使用封裝在TessTwoHelper類中,代碼十分簡單。使用前,須要調用TessBaseAPI的init方法進行初始化,第一個參數傳入手機的根目錄,第二個參數傳入語言數據包名字。咱們能夠根據識別的文字圖片類型設置白名單和黑名單,以便提升準確率。由於識別的是一個單獨的文本塊,因此調用setPageSegMode方法將模式設爲PSM_SINGLE_BLOCK_VERT_TEXT

  相關代碼:

(TessTwoHelper.java)

public class TessTwoHelper {

    public static final String DATA_DIR_PATH = "/storage/emulated/0/tessdata";
    public static final String DATA_NAME = "eng.traineddata";
    private TessBaseAPI tessBaseAPI = new TessBaseAPI();

    public void init() {
        tessBaseAPI.init("/storage/emulated/0/", "eng");
        tessBaseAPI.setDebug(true);
        tessBaseAPI.setVariable(TessBaseAPI.VAR_CHAR_WHITELIST, "123456789");
        tessBaseAPI.setVariable(TessBaseAPI.VAR_CHAR_BLACKLIST, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0!@#$%^&*()_+=-[]}{;:'\"\\|~`,./<>?");
        tessBaseAPI.setPageSegMode(TessBaseAPI.PageSegMode.PSM_SINGLE_BLOCK_VERT_TEXT);
    }

    public String getText(Bitmap bitmap) {
        tessBaseAPI.setImage(bitmap);
        return tessBaseAPI.getUTF8Text();
    }
}

  在SudokuXOrc類的getOriginShuDuArray方法中,使用數字輪廓座標截取數字圖片,使用tess-two識別,實測識別準確率仍是至關高。

(SudokuXOrc.java)

public class SudokuXOrc {
    ...
    public int[][] getOriginShuDuArray(Bitmap bitmapSource) {
        ...
        //根據輪廓截取數字圖片,進行文字識別
        Bitmap tmpBitmap = Bitmap.createBitmap(bitmapSource, rect.x, rect.y, rect.width, rect.height);
        int number = mTessTwoHelper.getText(tmpBitmap).charAt(0) - '0';
        saveBitmap(tmpBitmap, "bitmap" + rect.x + "" + rect.y + "tag:" + number);
        ...
    }
    ...
}

  【注】這部分代碼主要在TessTwoHelper類實現。

11 如何編寫代碼求解數獨

  數獨求解算法,聽起來感受很高大上的東西,但筆者認爲這多是本文中最簡單的內容,畢竟能夠利用機器算力來解決。(づ ̄3 ̄)づ╭❤~

  筆者還沒去了解太高效的數獨求解算法,在這裏用了一個相對容易理解的思路:

  步驟一:按先行後列的順序遍歷二維數組,找到第一個空白格子,根據遊戲規則,找到該格子全部可能填入的數字的序列(下文稱做數字序列)。如此重複填充空白格子。

  步驟二:若步驟一中填入數字有誤,必將致使將來有一空白格子(假設格子A)找不到任何能夠填入的數字。此時遊標回退到上一個數字序列不爲空的格子(假設格子B)中,並將格子B到A的全部填入的數字清除(置0)。

  步驟三:在格子B中填入數字序列的下一個數字。如此重複,直到填滿所有空格。

  筆者實現該算法,用到棧stack和鍵值對Pair<key,value>。其中棧stack用於按序儲存多餘的數字序列,鍵值對Pair<key,value>中的key表示某個格子的座標,value表示該格子的多餘數字序列。實測該算法的速度仍是能夠的,筆者使用小米5的手機測試,解一個專家級數獨(包括圖像處理)平均只需1秒。

  關鍵代碼:

(SudokuXAnalyse.java)

public class SudokuXAnalyse {
    /*數獨二維數組*/
    private int[][] mShuDu = new int[9][18];
    /*二維數組,標記某個格子是否被修改過,初始化全爲false,填入數字後置爲true*/
    private boolean[][] mShuDuFlag = new boolean[9][19];

    public SudokuXAnalyse(int[][] shuDu) {...}

    /*獲得某個格子可能填入的數字序列*/
    private  ArrayList<Integer> getPendingQueue(int x, int y)  {...}

    /*把座標(beginX,beginY)到(endX,endY)所有被修改過的格子置爲0,在回溯時使用*/
    private void clear(int beginX, int beginY, int endX, int endY) {...}
    
    /*數獨求解,無解時返回null*/
    public int[][] getAns() throws InterruptedException {
        int i = 0, j = 0;
        boolean needContinue = true;
        /*棧中存放鍵值對,key爲某格子的下標,value爲該格子可能填入數字的序列*/
        Stack<Pair<String, ArrayList<Integer>>> stack = new Stack<>();

        while (needContinue) {
            needContinue = false;
            while (i < 9) {
                while (j < 9) {
                    if (mShuDu[i][j] == 0) {
                        needContinue = true;
                        ArrayList<Integer> arrayList = getPendingQueue(i, j);
                        //當某格子沒有能夠填入的數字時,回溯
                        if (arrayList.size() == 0) {
                            //棧空,無解
                            if (stack.size() == 0) {
                                return null;
                            }
                            int tmpI = stack.peek().first.charAt(0) - '0';
                            int tmpJ = stack.peek().first.charAt(1) - '0';

                            clear(tmpI, tmpJ, i, j);

                            //從新更新當前下標
                            i = tmpI; 
                            j = tmpJ;

                            //填入某格子的下一個可能數字
                            mShuDu[i][j] = stack.peek().second.remove(0);

                            if (stack.peek().second.size() == 0) {
                                stack.pop();
                            }
                        } else {
                            mShuDu[i][j] = arrayList.remove(0);
                            mShuDuFlag[i][j] = true;
                            //保存某格子可能填入的其他數字
                            if (!arrayList.isEmpty()) {
                                String key = i + "" + j;
                                Pair<String, ArrayList<Integer>> pair = new Pair<>(key, arrayList);
                                stack.push(pair);
                            }
                        }
                    }
                    j++;
                }
                i++;
                j = 0;
            }
        }
        return mShuDu;
    }
}

  【注】數獨APP提供的題目都是有解的,若測試發現提示無解,極有多是使用tess-two作圖像轉文字時識別錯誤,致使產生的數獨無解。通常而言,使用tess-two來識別印刷體數字的準確率很是高,若識別出錯,極可能是TessBaseAPI的setPageSegMode方法傳入的模式不正確。

  【注】這部分的代碼主要在類SudokuXAnalyse中。

12 如何實現模擬屏幕點擊操做

  在求出數獨的答案以後,須要實現數字的填入,人工填入數字太慢,比較炫酷的是APP自動填入。此時用到模擬屏幕的點擊,能夠在幾秒內填好數十個數字。在Android程序中模擬屏幕的點擊操做,比較可行的有兩種方式:

  1. 獲取root權限,執行adb指令,如adb shell input tap 250 250,表示在點擊座標(250,250)的位置。

  2. 使用AccessibilityService進行模擬點擊。

  筆者最初是採用在APP中調用adb指令的方法,但實測該方法中指令運行速度很是慢,由於在數獨輸入一個數字,須要執行兩條指令(緣由可見備註),完成整個操做最快須要1分鐘左右,跟人工輸入沒任何區別。這樣固然是不行的,所以轉向使用AccessibilityService實現模擬點擊。

  使用AccessibilityService時,根據目標控件的id,經過findAccessibilityNodeInfosByViewId方法獲得對應的AccessibilityNodeInfo對象,再用performAction(AccessibilityNodeInfo.ACTION_CLICK)方法能夠完成一次模擬點擊,但筆者在實踐中發現,該方法失效了!!筆者認爲極可能是該數獨APP的按鈕點擊處理採用onTouch而非onClick的方法,進而屏蔽了該輔助功能的模擬點擊。

  最後看到一篇文章中提到AccessibilityService新增了dispatchGesture方法,可發送手勢。首先這個方法是7.0以後加入的,因此最小版本改成24。執行的手勢類爲GestureDescription,須要一段path路徑來實例化,若path路徑是一個點,則模擬點擊事件。

  咱們在前面已經使用AccessibilityService得到了數獨面板、1-9數字按鈕的位置信息,只須要進一步計算出數獨面板每一個格子以及1-9數字按鈕的中心點,再使用dispatchGesture方法,則能夠完成模擬點擊操做。

  經過dispatchGesture完成模擬點擊,關鍵代碼:

(SudokuAccessibility.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);
}

  計算數獨面板81個小格子以及1-9按鈕的中心座標:

(SudokuAccessibility.java)

private void initViewData(AccessibilityEvent event) {
    ...
    //獲取1-9數字按鈕的中心位置
    for (int i = 0; i < 9; i++) {
        String id = String.format("com.easybrain.sudoku.android:id/button_%d", i + 1);
        List<AccessibilityNodeInfo> nodeInfos = root.findAccessibilityNodeInfosByViewId(id);
        if (!nodeInfos.isEmpty()) {
            //獲取控件的矩形區域
            Rect rect = new Rect();
            nodeInfos.get(0).getBoundsInScreen(rect);
            Point point = new Point(rect.centerX(), rect.centerY());
            mTypeNumberPointList.add(point);
        }
    }
    //獲取數獨面板81個格子的中心位置
    String id = String.format("com.easybrain.sudoku.android:id/sudoku_board");
    List<AccessibilityNodeInfo> nodeInfos = root.findAccessibilityNodeInfosByViewId(id);
    if (!nodeInfos.isEmpty()) {
        //獲取控件的矩形區域
        Rect rect = new Rect();
        nodeInfos.get(0).getBoundsInScreen(rect);
        int step = (rect.bottom - rect.top) / 9;
        //計算81格中,第一個格子的中心點
        int x = rect.left + step / 2;
        int y = rect.top + step / 2;
        /*保存數獨面板的左上角頂點、高度信息,便於截圖分析數獨面板數字時使用。*/
        saveSudokuBroadInfo(rect);
        for (int i = 0; i < 9; i++) {
            List<Point> points = new ArrayList<>(9);
            for (int j = 0; j < 9; j++) {
                Point point = new Point(x + step * j, y + step * i);
                points.add(point);
            }
            mShuDuPanelPointList.add(points);
        }
    }
    ...
}

  ​ 經過Handler模擬延時點擊,關鍵代碼:

(SudokuAccessibility.java)
...
private Handler mHandler = new Handler(new Handler.Callback() {
    int i = 0;
    /**
     * 設置tag能夠實現輪流按下數獨面板和選擇區按鈕,
     * 同時配合變量@param fillingFlag,實現避免某些區域點擊失效的狀況。
     * */
    boolean tag = true;
    @Override
    public boolean handleMessage(Message msg) {
        if (i < mLocTextInfos.size()) {
            LocTextInfo locTextInfo = mLocTextInfos.get(i);
            if (tag == true) {
                Point numberPoint = mShuDuPanelPointList.get(locTextInfo.locX).get(locTextInfo.locY);
                dispatchGestureView(0, numberPoint.x, numberPoint.y);
            } else {
                Point typeNumberPoint = mTypeNumberPointList.get(locTextInfo.number - 1);
                dispatchGestureView(0, typeNumberPoint.x, typeNumberPoint.y);
                i++;
            }
            tag = !tag;
            mHandler.sendEmptyMessageDelayed(0, 25);
        } else {
            i = 0;
            tag = true;
            mHandler.removeCallbacksAndMessages(null);
            mLocalBroadcastManager.sendBroadcast(new Intent(SudokuXUtils.ACTION_FILLING_COMPLETE));
        }
        return false;
    }
});
...

​ 最後須要在xml配置文件中添加容許執行手勢:

(accessibility.xml)
...
android:canPerformGestures="true"
...

  【注】首先須要注意,把一個數字填入數獨面板的小格子中,須要執行兩次點擊操做:第一次點擊1-9的數字按鈕,選中要填入的數字,第二次點擊數獨面板對應的小格子,填入數字。(該數獨APP的默認規則)

  【注】這部分代碼主要在SudokuAccessibility類中實現。

13 後記

  該軟件還有不少有待改進的地方,好比:
  1. 直接集成了openCV和tess-two包,沒有作優化處理,致使軟件安裝包有100多M。
  2. 只能針對特定的APP完成求解、填入操做,後序可加入用戶框選數獨面板,軟件自動識別當前應用的功能,使可以填入任何的數獨APP。
  本文只作我的筆記和拋磚引玉之用,如有讀者改進了上述缺點請告知...

14 參考文章

  OpenCV玩九宮格數獨(一)——九宮格圖片中提取數字

  Android學習八---OpenCV JAVA API

  Android7.0 AccessibilityService新增gesturedescription使用詳解

  AccessibilityService從入門到出軌

相關文章
相關標籤/搜索