OpenCV机器学习:Android上利用SVM实现手写体数字识别

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/wblgers1234/article/details/80241774

这篇博客是之前那篇在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
^-^ 欢迎交流讨论!

猜你喜欢

转载自blog.csdn.net/wblgers1234/article/details/80241774