摘要:可以針對筆者常用的數獨本文的實現都基于該,實現數獨的識別求解并把答案自動填入。專家級別的平均秒完成求解包括圖像數字提取,識別過程,完成全部操作。步驟四數獨求解,生成答案,并生成需要填充的數字序列。
1 序
??數獨是源自18世紀瑞士的一種數學游戲。是一種運用紙、筆進行演算的邏輯游戲。玩家需要根據9×9盤面上的已知數字,推理出所有剩余空格的數字,并滿足每一行、每一列、每一個粗線宮(3*3)內的數字均含1-9,不重復。
???最近一段時間常常做數獨題(此處插入廣告:推薦一個非常良心的數獨APP點擊下載),并思考了一下能不能編寫一個APP,可以自動求解數獨、最后將結果填入該APP中。
..............................................................摸魚的開發過程,此處省略10^N^行字.............................................................
???最終寫一個APP:數獨X。可以針對筆者常用的數獨APP(本文的實現都基于該APP),實現數獨的識別、求解、并把答案自動填入。專家級別的平均1秒完成求解(包括圖像數字提取,識別過程),8s完成全部操作。
???本文將簡單介紹相關功能的實現。數獨X的使用效果,如下圖:
2 下載鏈接???數獨 APP鏈接:https://pan.baidu.com/s/1b67L...
???數獨X APP鏈接:https://pan.baidu.com/s/1xJMT...
???數獨X 源代碼鏈接:https://github.com/AchillesLz...
???【注】數獨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的相關方法,實現文字識別 |
???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時由系統接收回調。后來被開發者另辟蹊徑,用于一些插件開發,比如微信紅包助手,還有一些需要監聽第三方應用的插件。7.2 配置? 我們可以把AccessibilityService理解為——『按鍵精靈』。相信很多開發者都玩過PC上的這款軟件,他的作用,就是將你一次操作的整個記錄,錄制下來,然后就可以根據這個記錄,重復的執行這些操作,例如:先點擊某個輸入框,再輸入XXXX,再輸入驗證碼,最后點擊某按鈕,這些操作如果需要重復執行,那么顯然是一套機械的步驟,那么通過按鍵精靈,記錄下這些操作后,直接通過腳本就可以完成這些操作。其實AccessibilityService跟這個是一樣的,我們記錄的,實際上就是我們的操作步驟,或者稱之為『腳本』,那么系統在監控整個手機的各種AccessibilityService事件時,就會根據我們的邏輯來判斷該使用哪一個腳本。
? 因此,我們完全可以抽象出一個基類AccessibilityService,并抽象出一些腳本的事件,例如,根據Text查找對應的View、點擊某個View、滑動、返回等等。
???首先,要使用AccessibilityService實際上非常簡單,一般來說,只需要以下三步即可。
7.2.1 繼承系統AccessibilityServicepublic 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的android:packageNames處指定輔助應用的包名,即com.easybrain.sudoku.android。當沒有指定時,默認輔助所有的應用,建議大家在使用時,指定需要監聽的包名(你可以通過|來進行分隔),而不是所有的包名。typeAllMask是設置響應事件的類型,feedbackGeneric是設置回饋給用戶的方式,有語音播出和振動。
7.2.3 注冊???最后,在AndroidMainifest中注冊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_1至button_9。
7.3.2 相關代碼???當數獨APP窗口發生變化時,將觸發SudokuAccessibility中onAccessibilityEvent方法。在此方法中,通過控件id獲取數獨面板與1-9數字按鈕控件的信息,然后計算并將相關信息使用SharedPreferences保存至本地。
? 關鍵代碼:
(SudokuAccessibility.java) public class SudokuAccessibility extends AccessibilityService { //記錄1-9數字按鈕的中心點坐標 private ListmTypeNumberPointList = new ArrayList<>(9); //記錄數獨面板中81個小格子的中心點坐標 private List > 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
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 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 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許可(開源)發行的跨平臺計算機視覺庫,可以運行在Linux、Windows和Mac 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圖像轉灰度圖像 |
???接下來要做圖像的二值化,簡單來說,就是把圖片變成只有黑色和白色的像素點。
圖像的二值化,就是將圖像上的像素點的灰度值設置為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取值為100,type為THRESH_BINARY,即像素值超過100的都置為255,否則置為0。注意這里的thresh值的選用:可以剛好將九宮格內的縱橫線去掉,在做數字提取的時候將會少判斷一層父輪廓。
9.3.3 輪廓識別???終于,我們要對圖像進行輪廓識別。這一步將使用openCV視覺庫位于Imgproc類中findContours()方法實現。該方法定義如下:
findContours(Mat image, Listcontours, Mat hierarchy, int mode, int method)
參數名 | 含義 |
---|---|
Mat image | 單通道圖像矩陣,一般是經過Canny、拉普拉斯等邊緣檢測算子處理過的二值圖像。 |
List |
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
??第i個輪廓的前、后、子、父輪廓會保存在hierarchy[i][0], hiearchy[i][13], hiearchy[i][14]和 hiearchy[i][15]中。要找到上圖中的4、3、1三個數字輪廓,相對于要找到以輪廓c為父輪廓的contour[i]即可。
??我們處理數獨面板圖片時,也是一樣的思路,只是數獨面板比上圖再多了一層父輪廓。為了理清楚輪廓關系,我們在調用findContours方法生成輪廓信息后,用log打印出所有的輪廓信息,先找到9個九宮格的輪廓id,存放在數組tmp中。再遍歷contours數組,所有以tmp的元素為父輪廓的輪廓,則是我們最終需要的數字輪廓。如下圖所示,可以看到父輪廓id為1的都是九宮格的輪廓(紅框所示),以九宮格輪廓為父輪廓的都是數字輪廓(綠框所示)。
??使用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之一,并且支持中文。10.2 tess-two的配置tess-two是Tesseract在Android平臺上的移植。
??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
??關鍵代碼:
(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 ArrayListgetPendingQueue(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 >> stack = new Stack<>(); while (needContinue) { needContinue = false; while (i < 9) { while (j < 9) { if (mShuDu[i][j] == 0) { needContinue = true; ArrayList 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 > 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); ListnodeInfos = 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 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 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。
??本文只做個人筆記和拋磚引玉之用,若有讀者改進了上述缺點請告知...
??OpenCV玩九宮格數獨(一)——九宮格圖片中提取數字
??Android學習八---OpenCV JAVA API
??Android7.0 AccessibilityService新增gesturedescription使用詳解
??AccessibilityService從入門到出軌
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/73402.html
摘要:而此處針對進一步的搜索,有兩個問題需要考慮如何選取搜索起點方格確定哪種搜索策略深度優先搜索,廣度優先搜索關于第一個問題,無論選擇哪個方格起始搜索,對于能否解決問題來說并不存在差異。 Github倉庫地址 學習是為了尋找解決問題的答案,若脫離了問題只為知曉而進行的打call,那么隨時間流逝所沉淀下來的,估計就只有重在參與的虛幻存在感了,自學的人就更應善于發現可供解決的問題。為了入門AI,...
摘要:首先在二維數組第一行隨機填充個數字,然后將這個數字隨機分布到整個二維數組中,然后使用求解數獨的算法對此時的數組進行求解,得到一個完整的數獨,然后按照用戶輸入的提示數量進行隨機挖洞,得到最終的數獨題目。 思路 1.生成數獨 數獨的生成總體思路是挖洞法。首先在二維數組第一行隨機填充1-9 9個數字,然后將這9個數字隨機分布到整個二維數組中,然后使用求解數獨的算法對此時的數組進行求解,得到一...
摘要:數獨技巧直觀法候選數法相關二十格一個數字只與其所在行列及小九宮格的二十格相關我的思路精心設計了有效性判定函數,最多一次遍歷個小單元格就能做出方案的有效性判定。 看《算法的樂趣》,試著用非遞歸窮舉來解數獨,看效率如何! 數獨規則 數獨游戲,經典的為9×9=81個單元格組成的九宮格,同時也形成了3×3=9個小九宮格,要求在81個小單元格中填入數字1~9,并且數字在每行每列及每個小九宮格中都...
摘要:題目描述請判定一個數獨是否有效。解體思路將數獨按照行列和塊進行預處理,然后分別判斷是否合法。代碼利用一些技巧直接按塊儲存數據判斷一條記錄按某種方式排列的九個數字是否合法一步搞定感想能不用循環體盡量不用循環體,她好,我也好。 背景 在LintCode刷題的時候碰到一個很水的題目,不過要注意很多細節,不然調試的時候非常麻煩,利用Python的一些小技巧寫了一個很簡短的解決方案。 題目描述 ...
閱讀 3095·2021-09-28 09:42
閱讀 3447·2021-09-22 15:21
閱讀 1121·2021-07-29 13:50
閱讀 3562·2019-08-30 15:56
閱讀 3367·2019-08-30 15:54
閱讀 1196·2019-08-30 13:12
閱讀 1172·2019-08-29 17:03
閱讀 1197·2019-08-29 10:59