這篇博客是之前那篇在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”這個類別,如下圖所示:
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
^-^ 歡迎交流討論!