这篇博客是之前那篇在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
version="1.0" encoding="utf-8"?>
"/apk/res/android"
xmlns:tools="/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">
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!" />
id="@+id/handWriteView"
android:layout_below="@id/intro"
android:layout_width="match_parent"
android:background="@drawable/draw_background"
android:layout_height="400dp" />
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" />
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" />
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: " />
在界面设计上,除了两个交互性的按钮Button和一些显示性的静态文本外,需要特别注意的是通过触摸屏进行手写的部分。
这部分显示是继承于Android的View,我们将其命名为HandWriteView。当手指在屏幕上滑动时,会触发onTouchEvent函数,我们在这个函数中进行坐标提取,并把每次滑动的轨迹用很小的线段拼接起来,这样就达到了手写体显示的效果。在进行识别时,将当前View上面的内容通过BitMap取出,然后送入SVM分类器进行识别。
3.核心代码
3.1 加载SVM分类器
为了方便每次更新训练好的SVM模型,我将它放入Android的res目录下,在Android Studio环境中要注意添加新的res目录时,请选择“raw”这个类别,如下图所示:
![AS添加新的resmulu](https://img-/0508172735779?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 /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
^-^ 欢迎交流讨论!