基于Google ML模型开发Android手写文字识别应用

A. 项目描述

在手机上识别手写笔迹是一种常见的情况,用户可以绘制笔画,然后将这些笔画转换为文本。

Android AI应用开发:手写识别_项目源码

B. 开发工具

  • Android Studio Koala
  • Kotlin
  • Gradle 8.7

C. 代码设计

创建绘图View

在用户界面上进行绘图时,我们通常需要实现三种方法来响应不同的用户触控操作。这些方法分别是: touchStart(),当用户首次触摸屏幕时调用; touchMove(),当用户在屏幕上拖动手指或触控笔时调用; 以及touchUp(),当用户将手指或手写笔从屏幕上移开时调用。 这三种方法在视图的onTouchEvent方法中捕获,通过检测到的动作来调用它们,从而形成完整的绘图笔画序列:

override fun onTouchEvent(event: MotionEvent): Boolean {
        motionTouchEventX = event.x
        motionTouchEventY = event.y
        motionTouchEventT = System.currentTimeMillis()

        when (event.action) {
            MotionEvent.ACTION_DOWN -> touchStart()
            MotionEvent.ACTION_MOVE -> touchMove()
            MotionEvent.ACTION_UP -> touchUp()
        }
        return true
    }

当触摸事件开始时,我们需要执行两个操作。首先,启动路径以便在屏幕上绘制笔迹,并将路径移动到当前触摸点。其次,在ML Kit中创建一个新的strokeBuilder,捕获当前点和时间信息,以便接下来可以解析为Ink对象:

private fun touchStart() {
        // 用于在屏幕上绘制
        path.reset()
        path.moveTo(motionTouchEventX, motionTouchEventY)
        // 初始化, 用于获取 将用于 ML Kit 的笔迹
        currentX = motionTouchEventX
        currentY = motionTouchEventY
        strokeBuilder = Ink.Stroke.builder()
        strokeBuilder.addPoint(Ink.Point.create(motionTouchEventX, motionTouchEventY, motionTouchEventT))
    }

当用户手指在屏幕上滑动时,将调用touchMove()函数。这会首先更新路径变量,用于更新屏幕显示;接着更新strokeBuilder,以便当前笔画可以转换为ML Kit识别的Ink对象:

private fun touchMove() {
        val dx = Math.abs(motionTouchEventX - currentX)
        val dy = Math.abs(motionTouchEventY - currentY)
        if (dx >= touchTolerance!! || dy >= touchTolerance) {
            // QuadTo() 从最后一个点开始添加一个二次贝塞尔,接近控制点 (x1,y1),并在 (x2,y2) 结束。
            path.quadTo(currentX, currentY, (motionTouchEventX + currentX) / 2, (motionTouchEventY + currentY) / 2)
            currentX = motionTouchEventX
            currentY = motionTouchEventY
            strokeBuilder.addPoint(Ink.Point.create(motionTouchEventX, motionTouchEventY, motionTouchEventT))
            // 在 extraCanvas 中绘制路径以保存它。
            extraCanvas.drawPath(path, paint)
        }

        invalidate()
    }

最后,当用户移开手指时,将调用touchUp()。在这一时刻,我们需要重新设置路径,以便在下次绘制时从新的起点开始。 对于ML Kit,我们需要在用户移开手指的位置添加最后一个点,然后使用inkBuilder将完成的笔画添加到我们的Ink对象中。 这样就完成了整个绘制过程:从触摸开始,经过移动过程中的绘制,到最后的离开动作。

private fun touchUp() {
        // 重置路径,使其不会被重复绘制。
        strokeBuilder.addPoint(Ink.Point.create(motionTouchEventX, motionTouchEventY, motionTouchEventT))
        inkBuilder.addStroke(strokeBuilder.build())
        path.reset()
    }

从inkBuilder获取所有笔画,实现方法:

fun getInk(): Ink{
        val ink = inkBuilder.build()
        return ink
    }

代码实现在 CustomDrawingSurface 类中。

ML Kit解析笔迹

用户在自定义示图上书写,他们的笔迹将被捕获到Ink对象中。随后,可以利用ML Kit将这个Ink对象解释为文本。

首先,initializeRecognition()函数将创建DigitalInkRecognitionModelIdentifier的实例,并利用它来构建模型的引用:

private fun initializeRecognition(){
        val modelIdentifier: DigitalInkRecognitionModelIdentifier? =
            DigitalInkRecognitionModelIdentifier.fromLanguageTag("zh-Hani-CN")
        // 中文: "zh-Hani-CN" ; 英文: "en-US"
        model = DigitalInkRecognitionModel.builder(modelIdentifier!!).build()
        remoteModelManager.download(model!!, DownloadConditions.Builder().build()).addOnSuccessListener {
            Log.i("InkSample", "Model Downloaded")
            btnClassify.isEnabled = true
        }. addOnFailureListener {  e: Exception ->
            Log.e("InkSample", "Model failed $e")
        }
    }

远程模型管理器下载模型后,APP可以使用它对墨迹笔画进行推理。

通过在ML Kit的DigitalInkRecognition对象上调用getClient方法,创建一个识别器,并将刚刚指定并下载的模型作为构建识别器所需的模型传递进去:

recognizer = DigitalInkRecognition.getClient(DigitalInkRecognizerOptions.builder(model!!).build() )

从之前创建的绘图平面中获取Ink对象,接下来调用识别器上的recognize方法,将Ink对象传递给它。ML Kit会用结果来调用,你可以在成功或失败的侦听器中捕获此信息:

val thisInk = customDrawingSurface.getInk()
			
            recognizer.recognize(thisInk)
                .addOnSuccessListener { result: RecognitionResult ->
                    var outputString = ""
                    txtOutput.text = ""
                    for (candidate in result.candidates){
                        outputString+=candidate.text + "\n\n"
                    }
                    txtOutput.text = outputString
                }
                .addOnFailureListener { e: Exception ->
                    Log.e("DigitalInkTest", "Error during recognition: $e")
                }

成功后,将返回一个包含多个结果候选对象的“result”对象。在这种情况下,只需遍历并输出这些对象,它们已按照与用户的笔画相匹配的可能性预先分类。

D. 项目演示

Android AI应用开发:手写识别_项目源码_02

E. 项目源码

关注公众号『数字森林』,后台发送关键字:手写识别,获取项目源码。