成人无码视频,亚洲精品久久久久av无码,午夜精品久久久久久毛片,亚洲 中文字幕 日韩 无码

資訊專欄INFORMATION COLUMN

記數(shù)獨(dú)X--Android openCV識別數(shù)獨(dú)并自動求解填充APP開發(fā)過程

yvonne / 3732人閱讀

摘要:可以針對筆者常用的數(shù)獨(dú)本文的實現(xiàn)都基于該,實現(xiàn)數(shù)獨(dú)的識別求解并把答案自動填入。專家級別的平均秒完成求解包括圖像數(shù)字提取,識別過程,完成全部操作。步驟四數(shù)獨(dú)求解,生成答案,并生成需要填充的數(shù)字序列。

1 序

??數(shù)獨(dú)是源自18世紀(jì)瑞士的一種數(shù)學(xué)游戲。是一種運(yùn)用紙、筆進(jìn)行演算的邏輯游戲。玩家需要根據(jù)9×9盤面上的已知數(shù)字,推理出所有剩余空格的數(shù)字,并滿足每一行、每一列、每一個粗線宮(3*3)內(nèi)的數(shù)字均含1-9,不重復(fù)。

???最近一段時間常常做數(shù)獨(dú)題(此處插入廣告:推薦一個非常良心的數(shù)獨(dú)APP點(diǎn)擊下載),并思考了一下能不能編寫一個APP,可以自動求解數(shù)獨(dú)、最后將結(jié)果填入該APP中。

..............................................................摸魚的開發(fā)過程,此處省略10^N^行字.............................................................

???最終寫一個APP:數(shù)獨(dú)X。可以針對筆者常用的數(shù)獨(dú)APP(本文的實現(xiàn)都基于該APP),實現(xiàn)數(shù)獨(dú)的識別、求解、并把答案自動填入。專家級別的平均1秒完成求解(包括圖像數(shù)字提取,識別過程),8s完成全部操作。

???本文將簡單介紹相關(guān)功能的實現(xiàn)。數(shù)獨(dú)X的使用效果,如下圖:

2 下載鏈接

???數(shù)獨(dú) APP鏈接:https://pan.baidu.com/s/1b67L...

???數(shù)獨(dú)X APP鏈接:https://pan.baidu.com/s/1xJMT...

???數(shù)獨(dú)X 源代碼鏈接:https://github.com/AchillesLz...

???【注】數(shù)獨(dú)X對手機(jī)要求:Android 7.0 或以上。

3 本文內(nèi)容

實現(xiàn)思路介紹

項目結(jié)構(gòu)介紹

如何創(chuàng)建懸浮窗

如何獲取第三方應(yīng)用中的控件信息

如何無Root實現(xiàn)跨應(yīng)用截屏

如何提取數(shù)獨(dú)九宮格中的數(shù)字

如何實現(xiàn)數(shù)字識別

如何編寫代碼求解數(shù)獨(dú)

如何實現(xiàn)模擬屏幕點(diǎn)擊

后記

參考文章

4 實現(xiàn)思路介紹

???步驟一:我們需要獲得數(shù)獨(dú)APP中的九宮格數(shù)字。由于數(shù)獨(dú)App是第三方應(yīng)用,數(shù)獨(dú)信息當(dāng)然是無法直接獲取的,筆者的思路是打開數(shù)獨(dú)界面后調(diào)用截屏,再通過圖片處理提取九宮格的數(shù)字。同時,為了避免截屏?xí)r遮擋應(yīng)用,數(shù)獨(dú)X的工作窗口應(yīng)該使用懸浮窗形式。

???步驟二:截屏后,我們需要進(jìn)一步截取數(shù)獨(dú)面板圖片,以便數(shù)字提取用。我們可以寫死面板坐標(biāo)、寬高來提取截圖中的面板。在這里,當(dāng)然有更好的方法,就是通過輔助功能AccessibilityService獲得數(shù)獨(dú)應(yīng)用的數(shù)獨(dú)面板坐標(biāo)信息。

???步驟三:在獲得數(shù)獨(dú)面板的圖片后,使用openCV框架提取數(shù)字的輪廓,生成數(shù)字圖片,再調(diào)用TessTwo框架將圖片轉(zhuǎn)為數(shù)字,并生成原始數(shù)獨(dú)二維數(shù)組。

???步驟四:數(shù)獨(dú)求解,生成答案,并生成需要填充的數(shù)字序列。

???步驟五:最后通過輔助功能AccessibilityService類的相關(guān)方法,模擬屏幕點(diǎn)擊,輸入填充數(shù)獨(dú)的數(shù)字。

5 項目結(jié)構(gòu)介紹

???項目主要包含文件如下圖:

???主要作用:

類名 功能
FileStorageHelper 該類封裝了把a(bǔ)sset目錄下復(fù)制到SD卡的相關(guān)方法
LocTextInfo 該類記錄數(shù)獨(dú)某格子的行列號,及對應(yīng)的數(shù)字
MainActivity 該類實現(xiàn)應(yīng)用的啟動窗口,主要用于申請權(quán)限、截圖等操作
ScreenShotHelper 該類為截圖助手類,封裝了獲取截屏圖片的一些方法
SPUtils 該類封裝了SharedPreferences的一些操作
SudokuAccessibility 該類繼承AccessibilityService,實現(xiàn)第三方應(yīng)用的控件獲取、屏幕模擬點(diǎn)擊
SudokuXAnalyse 該類用于數(shù)獨(dú)求解,輸入原始的數(shù)獨(dú)二維數(shù)組,返回求解后的數(shù)獨(dú)二維數(shù)組
SudokuXOrc 該類用于數(shù)獨(dú)識別,輸入數(shù)獨(dú)圖片Bitmap,返回原始的數(shù)獨(dú)二維數(shù)組
SudokuXService 該類用于實現(xiàn)懸浮窗,實現(xiàn)應(yīng)用的工作窗口,實現(xiàn)數(shù)獨(dú)X的主要邏輯
SudokuXUtils 該類存放了廣播的Action,屏幕大小等常量信息
TessTwoHelper 該類封裝了TessBaseApi的相關(guān)方法,實現(xiàn)文字識別

6 如何創(chuàng)建懸浮窗

???Android的界面繪制,都是通過WindowMananger的服務(wù)來實現(xiàn)的。要實現(xiàn)一個能夠在自身應(yīng)用界面外的懸浮窗,我們就要利用WindowManager類。同時,為了讓懸浮窗與Activity脫離,讓應(yīng)用處于后臺時懸浮窗仍然可以正常運(yùn)行,這里使用Service來啟動懸浮窗并做為其背后邏輯支撐。

6.1 申請權(quán)限

???在創(chuàng)建懸浮窗前,必須先申請權(quán)限,代碼十分簡單:

(MainActivity.java)

...
private boolean startOverLay() {
    if (!Settings.canDrawOverlays(MainActivity.this)) {
        Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
        Toast.makeText(this, "需要取得權(quán)限以使用懸浮窗",Toast.LENGTH_SHORT).show();
        startActivity(intent);
        return false;
    }
    return true;
}
...
6.2 在service中創(chuàng)建懸浮窗
(SudokuXService.java)

...
private void initView() {
    //注意Android O版本與其他系統(tǒng)的差異
    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中實現(xiàn)。

7 如何獲得其他APP中的控件信息

???本項目使用Android的輔助服務(wù)AccessibilityService來獲取數(shù)獨(dú)APP的控件信息。

7.1 介紹
?    AccessibilityService設(shè)計初衷在于幫助殘障用戶使用android設(shè)備和應(yīng)用,在后臺運(yùn)行,可以監(jiān)聽用戶界面的一些狀態(tài)轉(zhuǎn)換,例如頁面切換、焦點(diǎn)改變、通知、Toast等,并在觸發(fā)AccessibilityEvents時由系統(tǒng)接收回調(diào)。后來被開發(fā)者另辟蹊徑,用于一些插件開發(fā),比如微信紅包助手,還有一些需要監(jiān)聽第三方應(yīng)用的插件。

? 我們可以把AccessibilityService理解為——『按鍵精靈』。相信很多開發(fā)者都玩過PC上的這款軟件,他的作用,就是將你一次操作的整個記錄,錄制下來,然后就可以根據(jù)這個記錄,重復(fù)的執(zhí)行這些操作,例如:先點(diǎn)擊某個輸入框,再輸入XXXX,再輸入驗證碼,最后點(diǎn)擊某按鈕,這些操作如果需要重復(fù)執(zhí)行,那么顯然是一套機(jī)械的步驟,那么通過按鍵精靈,記錄下這些操作后,直接通過腳本就可以完成這些操作。其實AccessibilityService跟這個是一樣的,我們記錄的,實際上就是我們的操作步驟,或者稱之為『腳本』,那么系統(tǒng)在監(jiān)控整個手機(jī)的各種AccessibilityService事件時,就會根據(jù)我們的邏輯來判斷該使用哪一個腳本。

? 因此,我們完全可以抽象出一個基類AccessibilityService,并抽象出一些腳本的事件,例如,根據(jù)Text查找對應(yīng)的View、點(diǎn)擊某個View、滑動、返回等等。

7.2 配置

???首先,要使用AccessibilityService實際上非常簡單,一般來說,只需要以下三步即可。

7.2.1 繼承系統(tǒng)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() {
    }
}

???強(qiáng)制重新的有兩個方法:onAccessibilityEvent和onInterrupt。重點(diǎn)關(guān)注onAccessibilityEvent方法,在該方法中,我們可以接收所監(jiān)聽的事件。

7.2.2 新建配置文件

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


???里面有一些比較簡單的配置。本項目要輔助的是數(shù)獨(dú)應(yīng)用,在xml的android:packageNames處指定輔助應(yīng)用的包名,即com.easybrain.sudoku.android。當(dāng)沒有指定時,默認(rèn)輔助所有的應(yīng)用,建議大家在使用時,指定需要監(jiān)聽的包名(你可以通過|來進(jìn)行分隔),而不是所有的包名。typeAllMask是設(shè)置響應(yīng)事件的類型,feedbackGeneric是設(shè)置回饋給用戶的方式,有語音播出和振動。

7.2.3 注冊

???最后,在AndroidMainifest中注冊service信息:


    
        
    

    

???完成以上步驟后,一個輔助服務(wù)就可以使用了,AccessibilityService具有很高的系統(tǒng)權(quán)限,所以,系統(tǒng)不會讓App直接設(shè)置是否啟用,需要用戶進(jìn)入設(shè)置-輔助功能中去手動啟用,這樣在一定程度上,保護(hù)了用戶數(shù)據(jù)的安全。

???這里不再贅述AccessibilityService的基本用法,有需要的讀者可參考相關(guān)文章,例如:AccessibilityService從入門到出軌。

7.3 使用

???本節(jié)介紹如何數(shù)獨(dú)APP的控件信息以及代碼編寫。

7.3.1 通過Layout Inspector工具,獲取數(shù)獨(dú)APP的控件信息

???使用AccessibilityService拿到數(shù)獨(dú)APP的控件信息,我們必須先知道對應(yīng)的控件id。這一步,我們可以使用Android Studio的Layout Inspector工具來完成。

???先啟動數(shù)獨(dú)APP,在Android Studio中,點(diǎn)擊Tools->Layout Inspector,選中包名:com.easybrain.sudoku.android,即可以看到一下畫面:

???可見數(shù)獨(dú)面板id為sudoku_board,1-9的數(shù)字按鈕id分別是button_1button_9

7.3.2 相關(guān)代碼

???當(dāng)數(shù)獨(dú)APP窗口發(fā)生變化時,將觸發(fā)SudokuAccessibility中onAccessibilityEvent方法。在此方法中,通過控件id獲取數(shù)獨(dú)面板與1-9數(shù)字按鈕控件的信息,然后計算并將相關(guān)信息使用SharedPreferences保存至本地。

? 關(guān)鍵代碼:

(SudokuAccessibility.java)

public class SudokuAccessibility extends AccessibilityService {
    //記錄1-9數(shù)字按鈕的中心點(diǎn)坐標(biāo)
    private List mTypeNumberPointList = new ArrayList<>(9);
    //記錄數(shù)獨(dú)面板中81個小格子的中心點(diǎn)坐標(biāo)
    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;

        //初始化等待區(qū)數(shù)字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);
            }
        }

        //生成數(shù)獨(dú)面板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格中,第一個格子的中心點(diǎn)
            int x = rect.left + step / 2;
            int y = rect.top + step / 2;

            /*保存數(shù)獨(dú)面板的左上角頂點(diǎn)、高度信息,便于截取數(shù)獨(dú)面板時使用。*/
            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, "數(shù)獨(dú)信息獲取成功!", Toast.LENGTH_SHORT).show();
        }
    }
    //保存數(shù)獨(dú)面板的坐標(biāo)信息,便于截取數(shù)獨(dú)面板圖片時使用
    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類中實現(xiàn)。

8 如何實現(xiàn)無Root權(quán)限截屏

???Android在5.0之后提供了官方的截屏API,現(xiàn)在的手機(jī)Android版本普遍在Android 5.0以上,該方法還是有比較高的適用性。此時,再也不需要通過root權(quán)限調(diào)用adb指令,或者使用輔助服務(wù)模擬截屏按鍵實現(xiàn)截屏了。

???由于節(jié)省文章篇幅,具體的實現(xiàn)讀者可參考筆者的另一篇文章《Android 5.0 無Root權(quán)限實現(xiàn)截屏》。

9 如何提取數(shù)獨(dú)九宮格中的數(shù)字

???要求解數(shù)獨(dú),需要進(jìn)行計算,圖片格式的數(shù)字肯定是不行的,所以必須把圖片上的數(shù)字轉(zhuǎn)換為實實在在的數(shù)字才能進(jìn)行計算。要得到實實在在的數(shù)字,我們需要做的是對圖片上的數(shù)字進(jìn)行提取和識別。

???本小節(jié)主要介紹數(shù)獨(dú)圖片中數(shù)字的提?。传@取數(shù)字圖像區(qū)域),該功能本項目使用openCV實現(xiàn)。

9.1 介紹
OpenCV于1999年由Intel建立,如今由Willow Garage提供支持。OpenCV是一個基于BSD許可(開源)發(fā)行的跨平臺計算機(jī)視覺庫,可以運(yùn)行在Linux、Windows和Mac OS操作系統(tǒng)上。它輕量級而且高效——由一系列 C 函數(shù)和少量 C++ 類構(gòu)成,同時提供了Python、Ruby、MATLAB等語言的接口,實現(xiàn)了圖像處理和計算機(jī)視覺方面的很多通用算法。
9.2 openCV的配置

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

9.3 openCV的使用

???提取圖片內(nèi)容的輪廓,我們可以使用openCV視覺庫Imgproc類中findContours()方法來實現(xiàn)。在對圖片進(jìn)行輪廓識別時,先需要對圖片進(jìn)行灰度化二值化處理,這里先簡單介紹這兩個操作。

9.3.1 灰度化

???我們從findContours的參數(shù)要求中得知,第一個參數(shù)是圖像二值化后的Mat對象。在生成二值化的圖像前,我們需要對圖像進(jìn)行灰度化處理。

灰度化,在RGB模型中,如果R=G=B時,則彩色表示一種灰度顏色,其中R=G=B的值叫灰度值,因此,灰度圖像每個像素只需一個字節(jié)存放灰度值(又稱強(qiáng)度值、亮度值),灰度范圍為0-255。一般有分量法 最大值法平均值法加權(quán)平均法四種方法對彩色圖像進(jìn)行灰度化。

???使用openCV中對圖片灰度化的實現(xiàn)很簡單,只需要一行代碼即可:Imgproc.cvtColor(rgbMat, grayMat, Imgproc.COLOR_RGB2GRAY);

cvtColor方法的定義:

cvtColor(Mat src, Mat dst, int code)
參數(shù)名 含義
Mat src 原Mat對象
Mat dst 目標(biāo)Mat對象
int code 本項目使用的是Imgproc.COLOR_RGB2GRAY,即RGB圖像轉(zhuǎn)灰度圖像
9.3.2 二值化

???接下來要做圖像的二值化,簡單來說,就是把圖片變成只有黑色和白色的像素點(diǎn)。

圖像的二值化,就是將圖像上的像素點(diǎn)的灰度值設(shè)置為0或255,也就是將整個圖像呈現(xiàn)出明顯的只有黑和白的視覺效果。

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

???threshold方法的定義:

threshold(Mat src, Mat dst, double thresh, double maxval, int type)
參數(shù)名 含義
Mat src 原Mat對象
Mat dst 目標(biāo)Mat對象
double thresh 閾值的具體值
double maxval type取THRESH_BINARY 或THRESH_BINARY_INV閾值類型時的最大值
int type THRESH_BINARY:像素值大于閾值時,取Maxval,也就是第四個參數(shù),否則置為0。
THRESH_BINARY_INV:當(dāng)前點(diǎn)值大于閾值時,設(shè)置為0,否則設(shè)置為Maxval。
THRESH_TRUNC: 當(dāng)前點(diǎn)值大于閾值時,設(shè)置為閾值,否則不改變。
THRESH_TOZERO: 當(dāng)前點(diǎn)值大于閾值時,不改變,否則設(shè)置為0。
THRESH_TOZERO_INV: 當(dāng)前點(diǎn)值大于閾值時,設(shè)置為0,否則不改變。

???在本項目中,thresh取值為100typeTHRESH_BINARY,即像素值超過100的都置為255,否則置為0。注意這里的thresh值的選用:可以剛好將九宮格內(nèi)的縱橫線去掉,在做數(shù)字提取的時候?qū)倥袛嘁粚痈篙喞?/p>

9.3.3 輪廓識別

???終于,我們要對圖像進(jìn)行輪廓識別。這一步將使用openCV視覺庫位于Imgproc類中findContours()方法實現(xiàn)。該方法定義如下:

findContours(Mat image, List contours, Mat hierarchy, int mode, int method)
參數(shù)名 含義
Mat image 單通道圖像矩陣,一般是經(jīng)過Canny、拉普拉斯等邊緣檢測算子處理過的二值圖像。
List contours MatOfPoint是保存Point的Mat,繼承自Mat。
contours表示檢測到的輪廓,輪廓是由一系列的點(diǎn)構(gòu)成,存儲在java 的list中,每個list的元素是MatOfPoint。
Mat hierarchy 包含著圖像的拓?fù)湫畔ⅲ泻蚦ontours相同數(shù)量的元素。
對于每個contours[i],對應(yīng)的hierarchy[i][0], hiearchy[i][9], hiearchy[i][10]和 hiearchy[i][11]分別被設(shè)置同一層次的下一個,上一個,第一個孩子和父親的輪廓。 如果contour [i]不存在對應(yīng)的contours,那么相應(yīng)的hierarchy[i] 就被設(shè)置成-1。
int mode contour的估計方式(4種):
RETR_EXTERNAL :只檢測最外圍的輪廓。
RETR_LIST :檢測所有輪廓,不建立等級關(guān)系,彼此獨(dú)立。
RETR_CCOMP :檢測所有輪廓,但所有輪廓都只建立兩個等級關(guān)系 。RETR_TREE :檢測所有輪廓,并且所有輪廓建立一個樹結(jié)構(gòu),層次完整。(本項目使用該參數(shù))
RETR_FLOODFILL :洪水填充法。
int method contour的檢索方式(4種):
CHAIN_APPROX_NONE:保存物體邊界上所有連續(xù)的輪廓點(diǎn)。
CHAIN_APPROX_SIMPLE:壓縮水平方向,垂直方向,對角線方向的元素,只保留該方向的終點(diǎn)坐標(biāo),例如一個矩形輪廓只需4個點(diǎn)來保存輪廓信息。(本項目使用該參數(shù))
CV_CHAIN_APPROX_TC89_L1:使用Teh-Chin 鏈近似算法。
*CV_CHAIN_APPROX_TC89_KCOS:使用Teh-Chin 鏈近似算法。

??由于數(shù)獨(dú)面板的輪廓包括各種的嵌套關(guān)系,此時mode參數(shù)選用RETR_TREE 。另外我們只需要數(shù)字輪廓的矩陣信息即可,所以method參數(shù)選用CHAIN_APPROX_SIMPLE

9.3.4 關(guān)于層次(Hierarchy)的理解

??檢測輪廓的時候,有時候可能會出現(xiàn)其中一個輪廓包含了另外一個輪廓,比如同心圓。這里我們認(rèn)為外側(cè)輪廓為父輪廓,內(nèi)側(cè)被包含的為子輪廓。同一級別的又有前一個輪廓后一個輪廓??偟膩碚f,hierarchy表達(dá)的是不同輪廓之間的聯(lián)系。

??舉一個例子,下圖產(chǎn)生了7個輪廓信息:

??數(shù)組List contours中共有7個輪廓信息,每個輪廓的id則為數(shù)組下標(biāo)i。如id為0的輪廓a是整個圖片的最外層輪廓、黑色邊框共有里外兩個id為1和2的輪廓b和c、數(shù)字1,3各自有一個輪廓f和g、數(shù)字4有兩個輪廓d和e,其中輪廓c是輪廓efg的父輪廓。

??第i個輪廓的前、后、子、父輪廓會保存在hierarchy[i][0], hiearchy[i][13], hiearchy[i][14]和 hiearchy[i][15]中。要找到上圖中的4、3、1三個數(shù)字輪廓,相對于要找到以輪廓c為父輪廓的contour[i]即可。

??我們處理數(shù)獨(dú)面板圖片時,也是一樣的思路,只是數(shù)獨(dú)面板比上圖再多了一層父輪廓。為了理清楚輪廓關(guān)系,我們在調(diào)用findContours方法生成輪廓信息后,用log打印出所有的輪廓信息,先找到9個九宮格的輪廓id,存放在數(shù)組tmp中。再遍歷contours數(shù)組,所有以tmp的元素為父輪廓的輪廓,則是我們最終需要的數(shù)字輪廓。如下圖所示,可以看到父輪廓id為1的都是九宮格的輪廓(紅框所示),以九宮格輪廓為父輪廓的都是數(shù)字輪廓(綠框所示)。

??使用openCV識別數(shù)字的部分已經(jīng)完成,在這就不貼代碼了,有需要的讀者可參考項目中代碼。

??【注】這部分的代碼主要在SudokuXOrc類中實現(xiàn)。

10 如何實現(xiàn)數(shù)字識別

??上一小節(jié),我們已經(jīng)可以獲得數(shù)獨(dú)圖片中的數(shù)字輪廓信息,可以產(chǎn)生數(shù)獨(dú)數(shù)字圖片。在本小節(jié),將介紹如何識別圖像中的文字。本項目使用tess-two ORC引擎實現(xiàn)圖像識別。

10.1 介紹
Tesseract是Ray Smith于1985到1995年間在惠普布里斯托實驗室開發(fā)的一個OCR引擎,曾經(jīng)在1995 UNLV精確度測試中名列前茅。但1996年后基本停止了開發(fā)。2006年,Google邀請Smith加盟,重啟該項目。目前項目的許可證是Apache 2.0。該項目目前支持Windows、Linux和Mac OS等主流平臺。但作為一個引擎,它只提供命令行工具。 現(xiàn)階段的Tesseract由Google負(fù)責(zé)維護(hù),是最好的開源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語言數(shù)據(jù)文件

??數(shù)據(jù)文件 下載鏈接。我們只需要識別數(shù)字,因此下載英文的語言數(shù)據(jù)eng.traineddata就可以了。

10.2.3 配置tessdata語言數(shù)據(jù)文件

??這一步很重要!在手機(jī)的SD卡根目錄創(chuàng)建一個名為tessdata的文件夾(必須是根目錄和tessdata命名),將下載好的語言數(shù)據(jù)文件eng.traineddata放入其中。

??【注】在實際的應(yīng)用,我們不可能要求用戶手動完成這步操作。一般的做法是將eng.traineddata文件存放在android項目的asset目錄中,在應(yīng)用啟動時將其復(fù)制到SD卡中。

10.3 tess-two使用

??本項目將tess-two的使用封裝在TessTwoHelper類中,代碼十分簡單。使用前,需要調(diào)用TessBaseAPI的init方法進(jìn)行初始化,第一個參數(shù)傳入手機(jī)的根目錄,第二個參數(shù)傳入語言數(shù)據(jù)包名字。我們可以根據(jù)識別的文字圖片類型設(shè)置白名單和黑名單,以便提高準(zhǔn)確率。因為識別的是一個多帶帶的文本塊,所以調(diào)用setPageSegMode方法將模式設(shè)為PSM_SINGLE_BLOCK_VERT_TEXT。

??相關(guān)代碼:

(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方法中,使用數(shù)字輪廓坐標(biāo)截取數(shù)字圖片,使用tess-two識別,實測識別準(zhǔn)確率還是相當(dāng)高。

(SudokuXOrc.java)

public class SudokuXOrc {
    ...
    public int[][] getOriginShuDuArray(Bitmap bitmapSource) {
        ...
        //根據(jù)輪廓截取數(shù)字圖片,進(jìn)行文字識別
        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類實現(xiàn)。

11 如何編寫代碼求解數(shù)獨(dú)

??數(shù)獨(dú)求解算法,聽起來感覺很高大上的東西,但筆者認(rèn)為這可能是本文中最簡單的內(nèi)容,畢竟可以利用機(jī)器算力來解決。(づ ̄3 ̄)づ╭?~

??筆者還沒去了解過高效的數(shù)獨(dú)求解算法,在這里用了一個相對容易理解的思路:

??步驟一:按先行后列的順序遍歷二維數(shù)組,找到第一個空白格子,根據(jù)游戲規(guī)則,找到該格子所有可能填入的數(shù)字的序列(下文稱作數(shù)字序列)。如此重復(fù)填充空白格子。

??步驟二:若步驟一中填入數(shù)字有誤,必將導(dǎo)致未來有一空白格子(假設(shè)格子A)找不到任何可以填入的數(shù)字。此時游標(biāo)回退到上一個數(shù)字序列不為空的格子(假設(shè)格子B)中,并將格子B到A的所有填入的數(shù)字清除(置0)。

??步驟三:在格子B中填入數(shù)字序列的下一個數(shù)字。如此重復(fù),直到填滿全部空格。

??筆者實現(xiàn)該算法,用到棧stack和鍵值對Pair。其中棧stack用于按序儲存多余的數(shù)字序列,鍵值對Pair中的key表示某個格子的坐標(biāo),value表示該格子的多余數(shù)字序列。實測該算法的速度還是可以的,筆者使用小米5的手機(jī)測試,解一個專家級數(shù)獨(dú)(包括圖像處理)平均只需1秒。

??關(guān)鍵代碼:

(SudokuXAnalyse.java)

public class SudokuXAnalyse {
    /*數(shù)獨(dú)二維數(shù)組*/
    private int[][] mShuDu = new int[9][18];
    /*二維數(shù)組,標(biāo)記某個格子是否被修改過,初始化全為false,填入數(shù)字后置為true*/
    private boolean[][] mShuDuFlag = new boolean[9][19];

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

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

    /*把坐標(biāo)(beginX,beginY)到(endX,endY)全部被修改過的格子置為0,在回溯時使用*/
    private void clear(int beginX, int beginY, int endX, int endY) {...}
    
    /*數(shù)獨(dú)求解,無解時返回null*/
    public int[][] getAns() throws InterruptedException {
        int i = 0, j = 0;
        boolean needContinue = true;
        /*棧中存放鍵值對,key為某格子的下標(biāo),value為該格子可能填入數(shù)字的序列*/
        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);
                        //當(dāng)某格子沒有可以填入的數(shù)字時,回溯
                        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);

                            //重新更新當(dāng)前下標(biāo)
                            i = tmpI; 
                            j = tmpJ;

                            //填入某格子的下一個可能數(shù)字
                            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;
                            //保存某格子可能填入的其余數(shù)字
                            if (!arrayList.isEmpty()) {
                                String key = i + "" + j;
                                Pair> pair = new Pair<>(key, arrayList);
                                stack.push(pair);
                            }
                        }
                    }
                    j++;
                }
                i++;
                j = 0;
            }
        }
        return mShuDu;
    }
}

??【注】數(shù)獨(dú)APP提供的題目都是有解的,若測試發(fā)現(xiàn)提示無解,極有可能是使用tess-two做圖像轉(zhuǎn)文字時識別錯誤,導(dǎo)致產(chǎn)生的數(shù)獨(dú)無解。一般而言,使用tess-two來識別印刷體數(shù)字的準(zhǔn)確率非常高,若識別出錯,很可能是TessBaseAPI的setPageSegMode方法傳入的模式不正確。

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

12 如何實現(xiàn)模擬屏幕點(diǎn)擊操作

??在求出數(shù)獨(dú)的答案之后,需要實現(xiàn)數(shù)字的填入,人工填入數(shù)字太慢,比較炫酷的是APP自動填入。此時用到模擬屏幕的點(diǎn)擊,可以在幾秒內(nèi)填好數(shù)十個數(shù)字。在Android程序中模擬屏幕的點(diǎn)擊操作,比較可行的有兩種方式:

??1. 獲取root權(quán)限,執(zhí)行adb指令,如adb shell input tap 250 250,表示在點(diǎn)擊坐標(biāo)(250,250)的位置。

??2. 使用AccessibilityService進(jìn)行模擬點(diǎn)擊。

??筆者最初是采用在APP中調(diào)用adb指令的方法,但實測該方法中指令運(yùn)行速度非常慢,因為在數(shù)獨(dú)輸入一個數(shù)字,需要執(zhí)行兩條指令(原因可見備注),完成整個操作最快需要1分鐘左右,跟人工輸入沒任何區(qū)別。這樣當(dāng)然是不行的,因此轉(zhuǎn)向使用AccessibilityService實現(xiàn)模擬點(diǎn)擊。

??使用AccessibilityService時,根據(jù)目標(biāo)控件的id,通過findAccessibilityNodeInfosByViewId方法得到對應(yīng)的AccessibilityNodeInfo對象,再用performAction(AccessibilityNodeInfo.ACTION_CLICK)方法可以完成一次模擬點(diǎn)擊,但筆者在實踐中發(fā)現(xiàn),該方法失效了!!筆者認(rèn)為很可能是該數(shù)獨(dú)APP的按鈕點(diǎn)擊處理采用onTouch而非onClick的方法,進(jìn)而屏蔽了該輔助功能的模擬點(diǎn)擊。

??最后看到一篇文章中提到AccessibilityService新增了dispatchGesture方法,可發(fā)送手勢。首先這個方法是7.0之后加入的,所以最小版本改為24。執(zhí)行的手勢類為GestureDescription,需要一段path路徑來實例化,若path路徑是一個點(diǎn),則模擬點(diǎn)擊事件。

??我們在前面已經(jīng)使用AccessibilityService獲得了數(shù)獨(dú)面板、1-9數(shù)字按鈕的位置信息,只需要進(jìn)一步計算出數(shù)獨(dú)面板每個格子以及1-9數(shù)字按鈕的中心點(diǎn),再使用dispatchGesture方法,則可以完成模擬點(diǎn)擊操作。

??通過dispatchGesture完成模擬點(diǎn)擊,關(guān)鍵代碼:

(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參數(shù):
     * path:筆畫路徑
     * startTime:時間 (以毫秒為單位),從手勢開始到開始筆劃的時間,非負(fù)數(shù)
     * duration:筆劃經(jīng)過路徑的持續(xù)時間(以毫秒為單位),非負(fù)數(shù)*/
    builder.addStroke(new GestureDescription.StrokeDescription(p, startTime, 1));
    dispatchGesture(builder.build(), null, null);
}

??計算數(shù)獨(dú)面板81個小格子以及1-9按鈕的中心坐標(biāo):

(SudokuAccessibility.java)

private void initViewData(AccessibilityEvent event) {
    ...
    //獲取1-9數(shù)字按鈕的中心位置
    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()) {
            //獲取控件的矩形區(qū)域
            Rect rect = new Rect();
            nodeInfos.get(0).getBoundsInScreen(rect);
            Point point = new Point(rect.centerX(), rect.centerY());
            mTypeNumberPointList.add(point);
        }
    }
    //獲取數(shù)獨(dú)面板81個格子的中心位置
    String id = String.format("com.easybrain.sudoku.android:id/sudoku_board");
    List nodeInfos = root.findAccessibilityNodeInfosByViewId(id);
    if (!nodeInfos.isEmpty()) {
        //獲取控件的矩形區(qū)域
        Rect rect = new Rect();
        nodeInfos.get(0).getBoundsInScreen(rect);
        int step = (rect.bottom - rect.top) / 9;
        //計算81格中,第一個格子的中心點(diǎn)
        int x = rect.left + step / 2;
        int y = rect.top + step / 2;
        /*保存數(shù)獨(dú)面板的左上角頂點(diǎn)、高度信息,便于截圖分析數(shù)獨(dú)面板數(shù)字時使用。*/
        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模擬延時點(diǎn)擊,關(guān)鍵代碼:

(SudokuAccessibility.java)
...
private Handler mHandler = new Handler(new Handler.Callback() {
    int i = 0;
    /**
     * 設(shè)置tag可以實現(xiàn)輪流按下數(shù)獨(dú)面板和選擇區(qū)按鈕,
     * 同時配合變量@param fillingFlag,實現(xiàn)避免某些區(qū)域點(diǎn)擊失效的情況。
     * */
    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配置文件中添加允許執(zhí)行手勢:

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

??【注】首先需要注意,把一個數(shù)字填入數(shù)獨(dú)面板的小格子中,需要執(zhí)行兩次點(diǎn)擊操作:第一次點(diǎn)擊1-9的數(shù)字按鈕,選中要填入的數(shù)字,第二次點(diǎn)擊數(shù)獨(dú)面板對應(yīng)的小格子,填入數(shù)字。(該數(shù)獨(dú)APP的默認(rèn)規(guī)則)

??【注】這部分代碼主要在SudokuAccessibility類中實現(xiàn)。

13 后記

??該軟件還有很多有待改進(jìn)的地方,比如:
??1. 直接集成了openCV和tess-two包,沒有做優(yōu)化處理,導(dǎo)致軟件安裝包有100多M。
??2. 只能針對特定的APP完成求解、填入操作,后序可加入用戶框選數(shù)獨(dú)面板,軟件自動識別當(dāng)前應(yīng)用的功能,使能夠填入任何的數(shù)獨(dú)APP。
??本文只做個人筆記和拋磚引玉之用,若有讀者改進(jìn)了上述缺點(diǎn)請告知...

14 參考文章

??OpenCV玩九宮格數(shù)獨(dú)(一)——九宮格圖片中提取數(shù)字

??Android學(xué)習(xí)八---OpenCV JAVA API

??Android7.0 AccessibilityService新增gesturedescription使用詳解

??AccessibilityService從入門到出軌

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://m.hztianpu.com/yun/73402.html

相關(guān)文章

  • 《AI之矛》(1)【數(shù)獨(dú)Agent】

    摘要:而此處針對進(jìn)一步的搜索,有兩個問題需要考慮如何選取搜索起點(diǎn)方格確定哪種搜索策略深度優(yōu)先搜索,廣度優(yōu)先搜索關(guān)于第一個問題,無論選擇哪個方格起始搜索,對于能否解決問題來說并不存在差異。 Github倉庫地址 學(xué)習(xí)是為了尋找解決問題的答案,若脫離了問題只為知曉而進(jìn)行的打call,那么隨時間流逝所沉淀下來的,估計就只有重在參與的虛幻存在感了,自學(xué)的人就更應(yīng)善于發(fā)現(xiàn)可供解決的問題。為了入門AI,...

    CatalpaFlat 評論0 收藏0
  • [Java] 數(shù)獨(dú)生成和求解

    摘要:首先在二維數(shù)組第一行隨機(jī)填充個數(shù)字,然后將這個數(shù)字隨機(jī)分布到整個二維數(shù)組中,然后使用求解數(shù)獨(dú)的算法對此時的數(shù)組進(jìn)行求解,得到一個完整的數(shù)獨(dú),然后按照用戶輸入的提示數(shù)量進(jìn)行隨機(jī)挖洞,得到最終的數(shù)獨(dú)題目。 思路 1.生成數(shù)獨(dú) 數(shù)獨(dú)的生成總體思路是挖洞法。首先在二維數(shù)組第一行隨機(jī)填充1-9 9個數(shù)字,然后將這9個數(shù)字隨機(jī)分布到整個二維數(shù)組中,然后使用求解數(shù)獨(dú)的算法對此時的數(shù)組進(jìn)行求解,得到一...

    skinner 評論0 收藏0
  • 數(shù)獨(dú)求解(javascript實現(xiàn))

    摘要:數(shù)獨(dú)技巧直觀法候選數(shù)法相關(guān)二十格一個數(shù)字只與其所在行列及小九宮格的二十格相關(guān)我的思路精心設(shè)計了有效性判定函數(shù),最多一次遍歷個小單元格就能做出方案的有效性判定。 看《算法的樂趣》,試著用非遞歸窮舉來解數(shù)獨(dú),看效率如何! 數(shù)獨(dú)規(guī)則 數(shù)獨(dú)游戲,經(jīng)典的為9×9=81個單元格組成的九宮格,同時也形成了3×3=9個小九宮格,要求在81個小單元格中填入數(shù)字1~9,并且數(shù)字在每行每列及每個小九宮格中都...

    Berwin 評論0 收藏0
  • 【Python】判斷數(shù)獨(dú)是否合法

    摘要:題目描述請判定一個數(shù)獨(dú)是否有效。解體思路將數(shù)獨(dú)按照行列和塊進(jìn)行預(yù)處理,然后分別判斷是否合法。代碼利用一些技巧直接按塊儲存數(shù)據(jù)判斷一條記錄按某種方式排列的九個數(shù)字是否合法一步搞定感想能不用循環(huán)體盡量不用循環(huán)體,她好,我也好。 背景 在LintCode刷題的時候碰到一個很水的題目,不過要注意很多細(xì)節(jié),不然調(diào)試的時候非常麻煩,利用Python的一些小技巧寫了一個很簡短的解決方案。 題目描述 ...

    tracy 評論0 收藏0

發(fā)表評論

0條評論

閱讀需要支付1元查看
<