GLM-OCR在Android端集成指南:移动端文档扫描应用开发

发布时间:2026/7/12 7:11:24

GLM-OCR在Android端集成指南:移动端文档扫描应用开发 GLM-OCR在Android端集成指南移动端文档扫描应用开发如果你正在开发一款需要文字识别功能的Android应用比如文档扫描、名片管理或者翻译工具那么集成一个高效、准确的OCR引擎就是核心任务。今天我们就来聊聊如何把GLM-OCR这个强大的模型搬到你的Android应用里让它能实时“看懂”摄像头捕捉到的文字。整个过程听起来可能有点复杂但别担心我会带你一步步走下来。我们会从怎么把模型“瘦身”到适合手机运行开始讲到怎么用摄像头拍出清晰的图片再到怎么调用模型识别文字最后把识别出来的文字漂亮地展示出来。跟着这篇指南你就能在自己的App里实现一个完整的文档扫描功能。1. 环境准备与项目搭建在开始写代码之前我们需要先把“舞台”搭好。这包括准备好模型文件以及配置好Android项目。1.1 模型准备让GLM-OCR“瘦身”上手机直接用在服务器上的大模型对于手机来说负担太重了。我们需要对它进行转换和优化这个过程通常叫做模型轻量化或移动端部署。首先你需要获取GLM-OCR的原始模型文件通常是.onnx或.pt格式。然后使用专门的工具进行转换。这里推荐使用ONNX Runtime或TensorFlow Lite的转换工具因为它们对移动端支持非常好。一个典型的转换流程以PyTorch模型转TFLite为例可能像这样import torch import torchvision import onnx from onnx_tf.backend import prepare import tensorflow as tf # 1. 加载你的PyTorch模型 model YourGLMOCRModel() model.load_state_dict(torch.load(glm-ocr.pth)) model.eval() # 2. 创建一个示例输入模拟手机图片尺寸例如 320x480 dummy_input torch.randn(1, 3, 480, 320) # 3. 导出为ONNX格式 torch.onnx.export(model, dummy_input, glm-ocr.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch_size}, output: {0: batch_size}}) # 4. 将ONNX模型转换为TensorFlow格式可选步骤如需TFLite # ... (使用onnx-tf或类似工具) # 5. 最终转换为TFLite格式 converter tf.lite.TFLiteConverter.from_saved_model(tf_model_directory) converter.optimizations [tf.lite.Optimize.DEFAULT] # 启用优化 converter.target_spec.supported_types [tf.float16] # 可选使用FP16减少模型大小 tflite_model converter.convert() # 6. 保存最终模型 with open(glm-ocr.tflite, wb) as f: f.write(tflite_model)转换完成后你会得到一个.tflite或.onnx文件。把它放到你Android项目的app/src/main/assets/目录下。这样应用在安装时就会把这个模型文件打包进去。1.2 Android项目配置打开你的Android Studio项目我们需要在app/build.gradle文件里添加一些依赖。这里我们假设使用TFLite来运行模型。android { ... // 确保你使用了足够新的NDK版本模型可能用到一些新算子 ndkVersion 25.1.8937393 aaptOptions { noCompress tflite // 防止AAPT压缩我们的模型文件 } } dependencies { ... // TensorFlow Lite 核心库 implementation org.tensorflow:tensorflow-lite:2.14.0 // 可选如果需要GPU加速 implementation org.tensorflow:tensorflow-lite-gpu:2.14.0 // 可选支持库提供一些工具类 implementation org.tensorflow:tensorflow-lite-support:0.4.4 // 相机X库用于更便捷地处理相机 def camerax_version 1.3.0-rc01 implementation androidx.camera:camera-core:${camerax_version} implementation androidx.camera:camera-camera2:${camerax_version} implementation androidx.camera:camera-lifecycle:${camerax_version} implementation androidx.camera:camera-view:${camerax_version} // 用于图片处理和UI implementation com.github.bumptech.glide:glide:4.15.1 }别忘了在AndroidManifest.xml中添加相机权限uses-permission android:nameandroid.permission.CAMERA / uses-feature android:nameandroid.hardware.camera android:requiredtrue / uses-feature android:nameandroid.hardware.camera.autofocus android:requiredfalse /2. 核心功能实现从拍照到识别环境搭好了现在我们来构建核心功能。整个过程可以分解为三个主要步骤用相机拍照并处理图片、调用模型识别文字、把识别结果显示出来。2.1 相机图像采集与预处理我们使用CameraX来简化相机操作。它的API现代且生命周期感知能省去很多麻烦。预处理的目标是把相机拍到的画面变成模型能“吃下去”的格式。首先创建一个用于预览和拍照的Fragment或Activity布局!-- activity_scan.xml -- androidx.constraintlayout.widget.ConstraintLayout xmlns:androidhttp://schemas.android.com/apk/res/android xmlns:apphttp://schemas.android.com/apk/res-auto android:layout_widthmatch_parent android:layout_heightmatch_parent androidx.camera.view.PreviewView android:idid/viewFinder android:layout_widthmatch_parent android:layout_heightmatch_parent app:layout_constraintBottom_toBottomOfparent app:layout_constraintEnd_toEndOfparent app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toTopOfparent / !-- 可以加一个矩形框提示用户对准文档 -- View android:idid/documentFrame android:layout_width300dp android:layout_height400dp android:backgroundandroid:color/transparent android:backgroundTint#4CAF50 app:layout_constraintBottom_toBottomOfparent app:layout_constraintEnd_toEndOfparent app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toTopOfparent/ Button android:idid/captureButton android:layout_widthwrap_content android:layout_heightwrap_content android:text扫描 app:layout_constraintBottom_toBottomOfparent app:layout_constraintEnd_toEndOfparent app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toTopOfparent android:layout_marginBottom50dp/ /androidx.constraintlayout.widget.ConstraintLayout然后在Activity中设置相机并处理图像// ScanActivity.kt class ScanActivity : AppCompatActivity() { private lateinit var cameraExecutor: ExecutorService private lateinit var imageAnalyzer: ImageAnalysis override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_scan) cameraExecutor Executors.newSingleThreadExecutor() // 请求相机权限代码略需使用ActivityResult API // ... // 设置相机 startCamera() captureButton.setOnClickListener { // 触发图像分析 analyzeCurrentFrame() } } private fun startCamera() { val cameraProviderFuture ProcessCameraProvider.getInstance(this) cameraProviderFuture.addListener({ val cameraProvider: ProcessCameraProvider cameraProviderFuture.get() val preview Preview.Builder().build().also { it.setSurfaceProvider(viewFinder.surfaceProvider) } // 构建图像分析用例用于实时处理帧 imageAnalyzer ImageAnalysis.Builder() .setTargetResolution(Size(1080, 1920)) // 设置分析分辨率 .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .build() .also { it.setAnalyzer(cameraExecutor, ImageAnalysis.Analyzer { imageProxy - // 图像分析逻辑可以在这里实时进行例如边缘检测提示用户对齐 // 为了省电和性能我们只在点击按钮时分析 imageProxy.close() }) } // 选择后置摄像头 val cameraSelector CameraSelector.DEFAULT_BACK_CAMERA try { // 解绑所有用例再重新绑定 cameraProvider.unbindAll() cameraProvider.bindToLifecycle( this, cameraSelector, preview, imageAnalyzer) } catch(exc: Exception) { Log.e(TAG, 相机绑定失败, exc) } }, ContextCompat.getMainExecutor(this)) } private fun analyzeCurrentFrame() { // 这里我们临时设置一个分析器来捕获当前帧 imageAnalyzer.setAnalyzer(cameraExecutor) { imageProxy - // 将ImageProxy转换为Bitmap并进行预处理 val bitmap imageProxy.toBitmap() // 需要实现toBitmap扩展函数 val processedBitmap preprocessImageForOCR(bitmap) // 在后台线程运行OCR runOnUiThread { showLoading(true) } val ocrResult runOcrOnImage(processedBitmap) runOnUiThread { showLoading(false) displayOcrResult(ocrResult) } imageProxy.close() // 分析一次后移除分析器避免持续分析 imageAnalyzer.clearAnalyzer() } } // 关键图像预处理函数 private fun preprocessImageForOCR(srcBitmap: Bitmap): Bitmap { // 1. 裁剪只取中间文档框部分假设documentFrame是屏幕上的矩形框 val cropRect getDocumentFrameRect() // 获取框的屏幕坐标并转换为图片坐标 val croppedBitmap Bitmap.createBitmap( srcBitmap, cropRect.left, cropRect.top, cropRect.width(), cropRect.height() ) // 2. 调整大小缩放到模型输入尺寸例如 320x480 val scaledBitmap Bitmap.createScaledBitmap(croppedBitmap, 320, 480, true) // 3. 增强可以尝试增加对比度、二值化等提升识别率 val enhancedBitmap applyContrastEnhancement(scaledBitmap) // 4. 转换为模型需要的输入格式例如归一化到[0,1]或[-1,1]并转换为RGB数组 return enhancedBitmap } // 其他辅助函数... private fun getDocumentFrameRect(): Rect { ... } private fun applyContrastEnhancement(bitmap: Bitmap): Bitmap { ... } }预处理是提升识别准确率的关键。好的预处理能让模糊、倾斜、光照不均的图片变得“清晰可读”。2.2 调用OCR模型进行识别现在图片准备好了该模型上场了。我们需要加载TFLite模型并喂给它处理好的图片数据。首先创建一个OCR推理类// GLMOCRPredictor.kt class GLMOCRPredictor(context: Context) { private var interpreter: Interpreter? null private val modelInputWidth 320 private val modelInputHeight 480 private val modelInputChannel 3 init { try { // 1. 从assets加载模型文件 val modelFile loadModelFile(context, glm-ocr.tflite) // 2. 创建Interpreter可以添加选项如使用GPU val options Interpreter.Options() // options.setUseNNAPI(true) // 使用NNAPI加速 // 如果需要GPU委托确保设备支持 // val gpuDelegate GpuDelegate() // options.addDelegate(gpuDelegate) interpreter Interpreter(modelFile, options) Log.d(OCR, 模型加载成功) } catch (e: Exception) { Log.e(OCR, 模型加载失败, e) } } private fun loadModelFile(context: Context, filename: String): MappedByteBuffer { val fileDescriptor context.assets.openFd(filename) val inputStream FileInputStream(fileDescriptor.fileDescriptor) val fileChannel inputStream.channel val startOffset fileDescriptor.startOffset val declaredLength fileDescriptor.declaredLength return fileChannel.map(FileChannel.MapMode.READ_ONLY, startOffset, declaredLength) } // 核心识别函数 fun recognize(bitmap: Bitmap): OcrResult { if (interpreter null) { return OcrResult(error 模型未加载) } // 1. 将Bitmap转换为模型输入需要的Float数组 val inputBuffer preprocessBitmapToFloatArray(bitmap) // 2. 准备输出缓冲区根据你的模型输出结构定义 // 假设模型输出两个东西文本框坐标和识别文本 val outputLocations Array(1) { Array(100) { FloatArray(4) } } // 假设最多100个框每个框4个坐标 val outputTexts Array(1) { Array(100) { FloatArray(字符集大小) } } // 假设文本用概率分布表示 val outputsMap HashMapInt, Any() outputsMap[0] outputLocations outputsMap[1] outputTexts // 3. 运行推理 interpreter?.runForMultipleInputsOutputs(arrayOfAny(inputBuffer), outputsMap) // 4. 后处理将模型输出转换为可读的文字和框 val detectedBoxes processOutputBoxes(outputLocations[0]) val recognizedTexts processOutputTexts(outputTexts[0]) // 5. 将文本框和文字配对并映射回原始图片坐标 val finalResults pairBoxesAndTexts(detectedBoxes, recognizedTexts, bitmap) return OcrResult( textBlocks finalResults, success true ) } private fun preprocessBitmapToFloatArray(bitmap: Bitmap): FloatArray { // 确保Bitmap尺寸正确 val resizedBitmap Bitmap.createScaledBitmap(bitmap, modelInputWidth, modelInputHeight, true) val inputBuffer FloatArray(modelInputWidth * modelInputHeight * modelInputChannel) val pixels IntArray(modelInputWidth * modelInputHeight) resizedBitmap.getPixels(pixels, 0, modelInputWidth, 0, 0, modelInputWidth, modelInputHeight) // 将像素值归一化到模型需要的范围例如[0, 255] - [0, 1] for (i in pixels.indices) { val pixel pixels[i] inputBuffer[i * 3] ((pixel shr 16) and 0xFF) / 255.0f // R inputBuffer[i * 3 1] ((pixel shr 8) and 0xFF) / 255.0f // G inputBuffer[i * 3 2] (pixel and 0xFF) / 255.0f // B } return inputBuffer } // 后处理函数简化版实际更复杂 private fun processOutputBoxes(boxes: ArrayFloatArray): ListRectF { ... } private fun processOutputTexts(textProbs: ArrayFloatArray): ListString { ... } private fun pairBoxesAndTexts(boxes: ListRectF, texts: ListString, originalBitmap: Bitmap): ListTextBlock { ... } data class OcrResult( val textBlocks: ListTextBlock emptyList(), val success: Boolean false, val error: String? null ) data class TextBlock( val text: String, val boundingBox: RectF, // 文本框在原图中的位置 val confidence: Float ) }这个类封装了模型加载和推理的全过程。recognize函数是核心它接收一个预处理好的Bitmap运行模型并返回结构化的识别结果。2.3 识别结果的可视化与编辑识别出文字和位置后我们需要把它直观地展示给用户并允许他们进行简单的编辑。我们可以创建一个自定义的OverlayView在图片上绘制识别出的文本框和文字// OcrResultOverlayView.kt class OcrResultOverlayView JvmOverloads constructor( context: Context, attrs: AttributeSet? null, defStyleAttr: Int 0 ) : View(context, attrs, defStyleAttr) { private var originalBitmap: Bitmap? null private var textBlocks: ListTextBlock emptyList() private var selectedBlockIndex: Int -1 private val paint Paint().apply { isAntiAlias true style Paint.Style.STROKE strokeWidth 4f } private val textPaint Paint().apply { isAntiAlias true color Color.WHITE textSize 24f style Paint.Style.FILL } private val selectedPaint Paint().apply { isAntiAlias true style Paint.Style.STROKE strokeWidth 6f color Color.RED } fun setOcrResult(bitmap: Bitmap, blocks: ListTextBlock) { originalBitmap bitmap textBlocks blocks invalidate() // 触发重绘 } fun setSelectedBlock(index: Int) { selectedBlockIndex index invalidate() } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) originalBitmap?.let { bitmap - // 1. 绘制原始图片可能需要缩放以适应View val scaleX width.toFloat() / bitmap.width val scaleY height.toFloat() / bitmap.height val scale scaleX.coerceAtMost(scaleY) val scaledWidth bitmap.width * scale val scaledHeight bitmap.height * scale val left (width - scaledWidth) / 2 val top (height - scaledHeight) / 2 canvas.drawBitmap(bitmap, null, RectF(left, top, left scaledWidth, top scaledHeight), null) // 2. 绘制所有文本框 textBlocks.forEachIndexed { index, block - val box block.boundingBox // 将框的坐标从原图坐标转换到当前View的绘制坐标 val drawRect RectF( left box.left * scale, top box.top * scale, left box.right * scale, top box.bottom * scale ) // 根据是否被选中使用不同画笔 val currentPaint if (index selectedBlockIndex) selectedPaint else paint.apply { color Color.GREEN } canvas.drawRect(drawRect, currentPaint) // 3. 在框的上方绘制识别出的文字 canvas.drawText(block.text, drawRect.left, drawRect.top - 10, textPaint) } } } // 处理触摸事件让用户可以点击选择文本框进行编辑 override fun onTouchEvent(event: MotionEvent): Boolean { when (event.action) { MotionEvent.ACTION_DOWN - { originalBitmap?.let { bitmap - val scaleX width.toFloat() / bitmap.width val scaleY height.toFloat() / bitmap.height val scale scaleX.coerceAtMost(scaleY) val left (width - bitmap.width * scale) / 2 val top (height - bitmap.height * scale) / 2 // 将触摸点坐标转换回原图坐标 val touchXInBitmap (event.x - left) / scale val touchYInBitmap (event.y - top) / scale // 查找被点击的文本框 val clickedIndex textBlocks.indexOfFirst { block - block.boundingBox.contains(touchXInBitmap, touchYInBitmap) } if (clickedIndex ! -1) { selectedBlockIndex clickedIndex invalidate() // 触发编辑事件 onTextBlockSelectedListener?.onSelected(textBlocks[clickedIndex], clickedIndex) return true } } } } return super.onTouchEvent(event) } var onTextBlockSelectedListener: OnTextBlockSelectedListener? null interface OnTextBlockSelectedListener { fun onSelected(block: TextBlock, index: Int) } }在Activity中我们可以这样使用这个OverlayView并提供一个编辑界面// 在ScanActivity中 private fun displayOcrResult(result: GLMOCRPredictor.OcrResult) { if (!result.success) { Toast.makeText(this, 识别失败: ${result.error}, Toast.LENGTH_SHORT).show() return } // 1. 显示带标注的图片 overlayView.setOcrResult(processedBitmap, result.textBlocks) // 2. 在侧边或底部显示所有识别出的文本方便整体编辑 val allText result.textBlocks.joinToString(\n) { it.text } textResultTextView.text allText // 3. 设置点击监听点击某个文本框时弹出编辑对话框 overlayView.onTextBlockSelectedListener object : OcrResultOverlayView.OnTextBlockSelectedListener { override fun onSelected(block: GLMOCRPredictor.TextBlock, index: Int) { showTextEditDialog(block.text, index) } } } private fun showTextEditDialog(originalText: String, blockIndex: Int) { val editText EditText(this).apply { setText(originalText) } AlertDialog.Builder(this) .setTitle(编辑识别文本) .setView(editText) .setPositiveButton(确认) { _, _ - val newText editText.text.toString() // 更新数据源和UI // (这里需要维护一个可变的textBlocks列表) updatedTextBlocks[blockIndex] updatedTextBlocks[blockIndex].copy(text newText) overlayView.setOcrResult(processedBitmap, updatedTextBlocks) updateAllTextDisplay() } .setNegativeButton(取消, null) .show() }这样用户就能看到被绿色框框住的文字点击某个框还能修改识别有误的内容体验就完整了。3. 优化与进阶技巧基础功能跑通后我们可以考虑一些优化让应用更流畅、更准确、更好用。性能优化异步处理确保图像预处理和OCR推理都在后台线程进行避免阻塞UI。模型量化在转换模型时使用tf.lite.Optimize.DEFAULT和tf.float16能显著减少模型体积和提升推理速度。缓存与复用Interpreter的初始化比较耗时应该作为单例全局复用。降低分析频率在实时预览时不要每帧都进行OCR可以设置一个时间间隔如每秒1-2次或者只在用户点击“扫描”按钮时分析当前帧。准确率提升图像预处理增强尝试在预处理阶段加入更复杂的算法比如透视变换矫正倾斜文档自适应二值化应对光照不均去噪滤波让文字更清晰。后处理优化模型输出的文字可能有不连贯或错误字符。可以引入一个字典或语言模型进行纠错或者对相似位置的文本块进行合理的段落合并。多模型融合对于复杂场景可以尝试先用一个轻量模型检测文本区域再针对每个区域用精度更高的模型进行识别。功能扩展实时检测提示在预览时可以运行一个轻量的文本检测模型实时在屏幕上画出文本区域引导用户将文档对准。多语言支持如果GLM-OCR支持可以增加语言切换功能提升多语言文档的识别率。结果导出增加将识别结果导出为TXT、PDF或Word文件的功能并保留排版格式。历史记录将扫描记录保存到本地数据库方便用户查看和管理。4. 常见问题与调试集成过程中你可能会遇到下面这些问题模型加载失败检查模型文件是否正确放置在assets目录并且build.gradle中设置了aaptOptions { noCompress tflite }。同时确认模型格式与Interpreter兼容。推理速度慢首先检查是否在后台线程运行。如果还慢可以尝试降低输入图片的分辨率或者启用GPU/NNAPI加速需测试设备兼容性。识别准确率低检查预处理确保传递给模型的图片是清晰、方正、对比度足够的。可以在UI上先显示预处理后的图片看看效果。检查模型输入确认图片缩放、颜色通道RGB/BGR和归一化范围如[0,1]或[-1,1]与模型训练时完全一致。检查后处理模型输出的坐标和文字序列可能需要复杂的解码例如CTC解码确保你的后处理逻辑正确。内存溢出OOM大尺寸Bitmap是内存杀手。务必及时回收不再使用的Bitmap调用recycle()并在处理完成后及时关闭ImageProxy。调试时可以把预处理后的图片保存到手机相册看看是不是你期望的样子。也可以把模型输出的原始数据文本框坐标、字符概率打印出来验证后处理逻辑是否正确。整体走下来在Android里集成GLM-OCR的核心思路就是把模型转换好、把图片处理好、把结果展示好。一开始可能会在模型输入输出格式、坐标转换这些地方卡一下多调试几次就顺了。实际用起来识别效果很大程度上取决于图片预处理做得好不好这块值得多花点心思。如果你刚开始做建议先确保基础流程跑通再慢慢加上实时预览、纠错这些进阶功能。代码里有些地方我做了简化比如后处理逻辑你需要根据GLM-OCR模型的实际输出格式来完善它。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。

相关新闻