OpenCV機器學習:Android上利用SVM實現手寫體數字識別

這篇博客是之前那篇在win7上用OpenCV的SVM分類器做MNIST手寫數字識別的後續。用MNIST數據集做SVM訓練和測試的細節可以移步那篇博客進行了解。

0.開發環境

這篇文章的思路是將Windows上訓練好的SVM分類模型移植到Android上,並可以實時通過手機觸摸屏進行數字手寫體測試,這樣對算法的理解更直觀,也讓算法有了實用性。後期如果有時間和條件,我可以逐漸將這個識別功能具體化,做一個可以識別任意文字的App。

以下是我的開發環境配置:

  • Android Studio
  • Android SDK 7.1.1 (API25)
  • OpenCV4Android 2.4.10

1.設計思路

考慮到手機的處理器性能,所以這次的實現將不會在手機端進行SVM分類器的訓練。換句話說,我們首先需要現在PC上用OpenCV訓練出一個可用的SVM分類模型,然後在Android上將這個分類模型進行加載,最後再用它進行手寫體的分類測試。

2.Layout

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.bolong_wen.handwritedigitrecognize.MainActivity">

    <TextView
        android:id="@+id/intro"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="25sp"
        android:text="It's a recognition demo on hand written digits, enjoy!" />
    <com.example.bolong_wen.handwritedigitrecognize.HandWriteView
        android:id="@+id/handWriteView"
        android:layout_below="@id/intro"
        android:layout_width="match_parent"
        android:background="@drawable/draw_background"
        android:layout_height="400dp" />
    <Button
        android:id="@+id/btnRecognize"
        android:layout_below="@id/handWriteView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft = "true"
        android:text="Recognize" />
    <Button
        android:id="@+id/btnClear"
        android:layout_below="@id/handWriteView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight = "true"
        android:text="Clear" />
    <TextView
        android:id="@+id/resultShow"
        android:layout_below="@id/btnRecognize"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft = "true"
        android:layout_alignParentBottom="true"
        android:textSize="25sp"
        android:layout_marginBottom = "50dp"
        android:text= "The recognition result is: " />
</RelativeLayout>

在界面設計上,除了兩個交互性的按鈕Button和一些顯示性的靜態文本外,需要特別注意的是通過觸摸屏進行手寫的部分。
這部分顯示是繼承於Android的View,我們將其命名爲HandWriteView。當手指在屏幕上滑動時,會觸發onTouchEvent函數,我們在這個函數中進行座標提取,並把每次滑動的軌跡用很小的線段拼接起來,這樣就達到了手寫體顯示的效果。在進行識別時,將當前View上面的內容通過BitMap取出,然後送入SVM分類器進行識別。

3.核心代碼

3.1 加載SVM分類器

爲了方便每次更新訓練好的SVM模型,我將它放入Android的res目錄下,在Android Studio環境中要注意添加新的res目錄時,請選擇“raw”這個類別,如下圖所示:

![AS添加新的resmulu](https://img-blog.csdn.net/20180508172735779?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dibGdlcnMxMjM0/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70) 首先聲明一個SVM分類器和SVM模型的承載器:
CvSVM mClassifier;
File mSvmModel;

然後我們通過Android的資源目錄將保存好的分類器模型進行載入,我存放的模型名字爲mnist.xml

mClassifier = new CvSVM();

//
try {
    // load cascade file from application resources
    InputStream is = getResources().openRawResource(R.raw.mnist);
    File mnist_modelDir = getDir("mnist_model", Context.MODE_PRIVATE);
    mSvmModel = new File(mnist_modelDir, "mnist.xml");
    FileOutputStream os = new FileOutputStream(mSvmModel);

    byte[] buffer = new byte[4096];
    int bytesRead;
    while ((bytesRead = is.read(buffer)) != -1) {
        os.write(buffer, 0, bytesRead);
    }
    is.close();
    os.close();

    mClassifier.load(mSvmModel.getAbsolutePath());

    mnist_modelDir.delete();

} catch (IOException e) {
    e.printStackTrace();
    Log.e(TAG, "Failed to load cascade. Exception thrown: " + e);
}

在完成這一步並且沒有報錯的情況下,mClassifier已經將整個SVM模型加載完成,可以進行接下來的預測。

3.2 HandWriteView繪製手寫體

先給出這部分的代碼:

package com.example.bolong_wen.handwritedigitrecognize;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
/**
 * Created by bolong_wen on 2018/3/29.
 */

public class HandWriteView extends View{

    public Bitmap returnBitmap(){
        return mBitmap;
    }
    private Paint mPaint;
    private float degrees=0;
    private int mLastX, mLastY, mCurrX, mCurrY;
    private Bitmap mBitmap;

    public HandWriteView(Context context) {
        super(context);
        init();
    }

    public HandWriteView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public HandWriteView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }
    private void init(){
        mPaint = new Paint();
        mPaint.setColor(Color.WHITE);
        mPaint.setStrokeWidth(70);
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setStrokeJoin(Paint.Join.ROUND);
    }
    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);

        int width = getWidth();
        int height = getHeight();
        if (mBitmap == null) {
            mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        }
        canvas.drawBitmap(mBitmap, 0, 0, mPaint);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        mLastX = mCurrX;
        mLastY = mCurrY;
        mCurrX = (int) event.getX();
        mCurrY = (int) event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastX = mCurrX;
                mLastY = mCurrY;
                break;
            default:
                break;
        }
        updateDrawHandWrite();
        return true;
    }

    private void updateDrawHandWrite(){
        int width = getWidth();
        int height = getHeight();

        if (mBitmap == null) {
            mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        }

        Canvas tmpCanvas = new Canvas(mBitmap);
        tmpCanvas.drawLine(mLastX, mLastY, mCurrX, mCurrY, mPaint);
        invalidate();
    }

    public void clearDraw(){
        mBitmap = null;
        invalidate();
    }
}

在這個View類的初始化裏面,我們設置好畫筆的顏色,寬度,同時需要注意的是要設置筆觸風格和連接處的形狀爲“圓形”,以及設置反鋸齒,這樣會使得畫出來的手寫體數字更光滑,細節處更連貫,有利於後期的識別。這段代碼如下所示:

mPaint.setAntiAlias(true);
mPaint.setDither(true);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setStrokeJoin(Paint.Join.ROUND);

當用戶通過手指觸摸在屏幕上移動時會觸發onTouchEvent函數,在該函數裏我們獲取當前的接觸點座標:

mLastX = mCurrX;
mLastY = mCurrY;
mCurrX = (int) event.getX();
mCurrY = (int) event.getY();

同時在原始接觸點Last和當前接觸點Curr之間繪製出直線:

int width = getWidth();
int height = getHeight();

if (mBitmap == null) {
    mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
}

Canvas tmpCanvas = new Canvas(mBitmap);
tmpCanvas.drawLine(mLastX, mLastY, mCurrX, mCurrY, mPaint);
invalidate();

3.3 識別數字

在Android程序的主activity中MainActivity,當按下識別按鈕時,會從HandWriteView返回得到一個Bitmap,它是當前繪製得到的一個截圖(snapshot),然後將這個Bitmap轉換爲OpenCV的Mat格式,同時進行灰度化處理。

Bitmap tmpBitmap = mHandWriteView.returnBitmap();
if(null == tmpBitmap)
    return;
Mat tmpMat = new Mat(tmpBitmap.getHeight(),tmpBitmap.getWidth(),CvType.CV_8UC3);
Mat saveMat = new Mat(tmpBitmap.getHeight(),tmpBitmap.getWidth(),CvType.CV_8UC1);

Utils.bitmapToMat(tmpBitmap,tmpMat);

Imgproc.cvtColor(tmpMat, saveMat, Imgproc.COLOR_RGBA2GRAY);

在前一篇博客中我們的SVM分類器模型是基於MNIST數據集進行訓練得到的,數據集中的每幅圖片的大小是28×28。因此在進行實際測試時,我們也需要將上一步手寫得到的圖片進行resize處理,歸一化到[0,1],並且轉換爲一維向量。

int imgVectorLen = 28 * 28;
Mat dstMat = new Mat(28,28,CvType.CV_8UC1);
Mat tempFloat = new Mat(28,28,CvType.CV_32FC1);

Imgproc.resize(saveMat,dstMat,new Size(28,28));
dstMat.convertTo(tempFloat, CvType.CV_32FC1);

Mat predict_mat = tempFloat.reshape(0,1).clone();
Core.normalize(predict_mat,predict_mat,0.0,1.0,Core.NORM_MINMAX);

其中特別需要注意的是歸一化,MNIST中每幅圖片的數據都是在[0,1]之間,要保持一致才能得到正確的結果。
最後一步,我們調用加載好的SVM模型進行預測,得到識別出的數字:

int response = (int)mClassifier.predict(predict_mat);

4.demo效果

直接給出在手機上運行的識別效果:

這裏寫圖片描述

經過多次測試,發現在8/9兩個數字上的識別率比較低。還需要在後續的開發中進行改進,有一個思路:將誤識別的8/9手寫體圖片保存下來,加入訓練集,重新訓練模型,這樣應該會得到一個更好的分類效果。

項目地址:HandwriteDigitRecognize
^-^ 歡迎交流討論!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章