You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Jetpack Compose数字墨迹识别精度低于XML布局的原因与优化

Jetpack Compose集成MLKit数字墨迹识别精度问题排查与修复

问题描述

在Android应用中使用Jetpack Compose集成MLKit数字墨迹识别功能时,发现识别精度远低于传统XML布局的实现。尽管遵循了相同的识别流程,Compose版本的识别结果始终误差更大、准确性更低,调整参数后也无明显改善。

代码对比

Compose实现代码

MainActivity.kt

val TAG = "Image Recognition"

public class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        lifecycleScope.launch {
            StrokeManager.downloadModel()
        }

        setContent {
            DemoTheme {
                App()
            }
        }
    }
}

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun App() {
    var textState by remember {
        mutableStateOf("")
    }

    val lines = remember {
        mutableStateListOf<Line>()
    }

    Column(
        verticalArrangement = Arrangement.Center,
        modifier = Modifier.fillMaxWidth()
    ) {
        Canvas(
            modifier = Modifier
                .fillMaxWidth()
                .height(300.dp)
                .pointerInteropFilter { event ->
                    // 直接将触摸事件传递给StrokeManager处理
                    StrokeManager.addNewTouchEvent(event)
                    // 根据事件类型更新绘制的线条
                    when (event.actionMasked) {
                        MotionEvent.ACTION_DOWN -> lines.clear()
                        MotionEvent.ACTION_MOVE -> {
                            val historicalX = if (event.historySize > 0) event.getHistoricalX(event.historySize - 1) else event.x
                            val line = Line(
                                start = Offset(historicalX, event.y),
                                end = Offset(event.x, event.y)
                            )
                            lines.add(line)
                        }
                    }
                    true
                }
        ) {
            lines.forEach { line ->
                drawLine(
                    color = line.color,
                    start = line.start,
                    end = line.end,
                    strokeWidth = line.strokeWidth.toPx(),
                    cap = StrokeCap.Round
                )
            }
        }

        Button(
            modifier = Modifier
                .height(50.dp)
                .fillMaxWidth(),
            onClick = {
                StrokeManager.recognize {
                    textState = it
                }
            }
        ) {
            Text("识别字符")
        }

        Spacer(modifier = Modifier.height(10.dp))

        Button(
            modifier = Modifier
                .height(50.dp)
                .fillMaxWidth(),
            onClick = {
                lines.clear()
                StrokeManager.clearInk()
                textState = ""
            }
        ) {
            Text("清空")
        }

        if (textState.isNotEmpty()) {
            Text(textState)
        }
    }
}

data class Line(
    val start: Offset,
    val end: Offset,
    val color: Color = Color.Black,
    val strokeWidth: Dp = 8.dp
)

StrokeManager.kt

object StrokeManager {
    private val TAG = "Image Recognition"

    private val inkBuilder: Ink.Builder = Ink.builder()
    private lateinit var strokeBuilder: Ink.Stroke.Builder
    private lateinit var digitalInkRecognitionModel: DigitalInkRecognitionModel

    fun addNewTouchEvent(event: MotionEvent) {
        val action = event.actionMasked
        val x = event.x
        val y = event.y

        when (action) {
            MotionEvent.ACTION_DOWN -> {
                strokeBuilder = Ink.Stroke.builder()
                strokeBuilder.addPoint(Ink.Point.create(x, y))
            }
            MotionEvent.ACTION_MOVE -> {
                strokeBuilder.addPoint(Ink.Point.create(x, y))
            }
            MotionEvent.ACTION_UP -> {
                strokeBuilder.addPoint(Ink.Point.create(x, y))
                inkBuilder.addStroke(strokeBuilder.build())
            }
            else -> {
                Log.d(TAG, "不相关的触摸事件")
            }
        }
    }

    suspend fun downloadModel() {
        val modelIdentifier = DigitalInkRecognitionModelIdentifier.fromLanguageTag("en-IN")
        digitalInkRecognitionModel = DigitalInkRecognitionModel.builder(modelIdentifier!!).build()

        val remoteModelManager = RemoteModelManager.getInstance()
        remoteModelManager.isModelDownloaded(digitalInkRecognitionModel)
            .addOnSuccessListener { isDownloaded ->
                if (isDownloaded) {
                    Log.i(TAG, "模型已下载")
                } else {
                    Log.i(TAG, "开始下载模型")
                    remoteModelManager.download(digitalInkRecognitionModel, DownloadConditions.Builder().build())
                        .addOnSuccessListener {
                            Log.i(TAG, "模型下载完成")
                        }
                        .addOnFailureListener { e ->
                            Log.e(TAG, "模型下载失败: $e")
                        }
                }
            }
    }

    fun recognize(recognised: (String) -> Unit) {
        if (!this::digitalInkRecognitionModel.isInitialized) {
            Log.e(TAG, "模型未初始化")
            recognised("错误:模型未初始化")
            return
        }

        val recognizer: DigitalInkRecognizer =
            DigitalInkRecognition.getClient(
                DigitalInkRecognizerOptions.builder(digitalInkRecognitionModel).build()
            )

        val ink = inkBuilder.build()
        recognizer.recognize(ink)
            .addOnSuccessListener { result ->
                val textState = result.candidates.joinToString(separator = "\n-> ") { it.text }
                recognised("识别结果:\n-> $textState")
                Log.d(TAG, "识别结果: $textState")
            }
            .addOnFailureListener { e ->
                Log.e(TAG, "识别失败: $e")
                recognised("识别失败: ${e.message}")
            }
    }

    fun clearInk() {
        inkBuilder.clear()
    }
}

XML实现代码

MainActivity.kt

class MainActivity : AppCompatActivity() {
    private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        binding.apply {
            textView.movementMethod = ScrollingMovementMethod()
            StrokeManager.downloadModel()

            buttonRecognize.setOnClickListener {
                textView.visibility = View.VISIBLE
                buttonRecognize.visibility = View.INVISIBLE
                StrokeManager.recognize { result ->
                    textView.text = result
                }
            }

            buttonClear.setOnClickListener {
                drawView.clear()
                StrokeManager.clearInk()
                textView.visibility = View.INVISIBLE
                buttonRecognize.visibility = View.VISIBLE
            }
        }
    }
}

DrawView.kt

class DrawView(context: Context, attributeSet: AttributeSet?) : View(context, attributeSet) {
    private var currentStrokePaint: Paint = Paint()
    private val canvasPaint: Paint = Paint(Paint.DITHER_FLAG)
    private val currentStroke: Path = Path()

    private var drawCanvas: Canvas? = null
    private lateinit var canvasBitmap: Bitmap

    init {
        currentStrokePaint.color = Color.BLACK
        currentStrokePaint.isAntiAlias = true
        currentStrokePaint.strokeWidth = STROKE_WIDTH_DP
        currentStrokePaint.style = Paint.Style.STROKE
        currentStrokePaint.strokeJoin = Paint.Join.ROUND
        currentStrokePaint.strokeCap = Paint.Cap.ROUND
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        val action = event.actionMasked
        val x = event.x
        val y = event.y

        when (action) {
            MotionEvent.ACTION_DOWN -> currentStroke.moveTo(x, y)
            MotionEvent.ACTION_MOVE -> currentStroke.lineTo(x, y)
            MotionEvent.ACTION_UP -> {
                currentStroke.lineTo(x, y)
                drawCanvas?.drawPath(currentStroke, currentStrokePaint)
                currentStroke.reset()
            }
            else -> {}
        }
        StrokeManager.addNewTouchEvent(event)
        invalidate()
        return true
    }

    override fun onDraw(canvas: Canvas) {
        canvas.drawBitmap(canvasBitmap, 0f, 0f, canvasPaint)
        canvas.drawPath(currentStroke, currentStrokePaint)
    }

    fun clear() {
        onSizeChanged(canvasBitmap.width, canvasBitmap.height, canvasBitmap.width, canvasBitmap.height)
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        canvasBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
        drawCanvas = Canvas(canvasBitmap)
        invalidate()
    }

    companion object {
        private const val STROKE_WIDTH_DP = 6.0f
    }
}

布局文件

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.appcompat.widget.AppCompatButton
        android:id="@+id/buttonClear"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_gravity="top|end"
        android:background="@drawable/icon_delete"/>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <com.ibrahimcanerdogan.digitalinkrecognition.view.DrawView
            android:id="@+id/drawView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@color/white"/>

    </FrameLayout>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <androidx.appcompat.widget.AppCompatTextView
            android:id="@+id/textView"
            android:layout_width="match_parent"
            android:layout_height="200dp"
            android:background="@drawable/background_textview"
            android:layout_gravity="bottom"
            android:paddingStart="20dp"
            android:paddingEnd="20dp"
            android:paddingTop="10dp"
            android:textSize="17sp"
            android:textColor="@color/black"
            android:scrollbars="vertical"
            android:visibility="invisible"/>

        <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
            android:id="@+id/buttonRecognize"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center|bottom"
            android:layout_margin="15dp"
            app:icon="@drawable/icon_start"
            android:text="@string/recognize"
            android:textAllCaps="true"/>
    </FrameLayout>
</FrameLayout>

问题根源分析

  1. 触摸事件重复处理:原Compose代码同时使用detectDragGestures和pointerInteropFilter处理触摸事件,导致事件被拆分或重复传递给StrokeManager,无法形成连续的笔画(Stroke),而MLKit的墨迹识别依赖连续的笔画数据来提升精度。

  2. InkBuilder实例不一致:原代码在MainActivity的companion object中维护了一个独立的inkBuilder实例,和StrokeManager内部的实例不同步,导致部分笔画没有被正确添加到识别用的Ink对象中。

  3. 错误的事件分支触发:原StrokeManager的else分支逻辑错误,会在非标准触摸事件时创建新的Stroke并立即添加到Ink中,导致单个笔画被拆分成多个微小Stroke,严重干扰识别精度。

修复方案

  1. 统一触摸事件处理:移除detectDragGestures,仅通过pointerInteropFilter将触摸事件传递给StrokeManager,确保事件流程和XML版本一致。

  2. 统一InkBuilder管理:将InkBuilder的维护完全交给StrokeManager,外部仅调用其提供的方法,避免实例不一致问题。

  3. 修正事件分支逻辑:删除StrokeManager中else分支的错误逻辑,仅处理标准的ACTION_DOWN/ACTION_MOVE/ACTION_UP事件,保证笔画的连续性。

  4. 添加清空方法:在StrokeManager中新增clearInk方法,统一清空InkBuilder,避免残留笔画影响下一次识别。

内容的提问来源于stack exchange,提问作者Shubham

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 09:29:51