GLM-OCR在Android端应用开发:移动扫描识别App实战

GLM-OCR在Android端应用开发:移动扫描识别App实战 GLM-OCR在Android端应用开发移动扫描识别App实战最近在做一个需要处理大量纸质文档数字化的项目手动录入信息简直让人头大。正好看到GLM-OCR这个工具识别准确率据说挺高就想着能不能把它集成到手机App里实现随拍随识别。折腾了一阵子还真做出来了效果不错。今天就来聊聊怎么把一个部署在服务端的OCR能力“搬”到你的Android应用里打造一个属于自己的文档扫描识别工具。整个过程其实不复杂核心就是在Android端拍好照、处理好图片然后调用远端的API再把识别结果拿回来展示和编辑。我会把几个关键环节比如相机调用、图片预处理、网络请求封装还有结果的处理都拆开揉碎了讲清楚。如果你也有类似的需求或者对移动端AI应用集成感兴趣这篇应该能给你一些直接的参考。1. 整体思路与准备工作在动手写代码之前我们先理清整个App要干什么以及需要准备些什么。这样后面做起来才不会手忙脚乱。简单来说我们想做的App工作流是这样的用户打开App - 用手机摄像头拍摄文档 - 对拍到的图片进行裁剪、纠偏、增强等处理 - 将处理后的图片上传到我们自己的服务器上面部署了GLM-OCR服务 - 服务器返回识别出的文字和位置信息 - 在App里把这些文字展示出来并且允许用户编辑、复制或者导出。1.1 核心组件与依赖要实现上面这个流程我们的Android项目需要引入一些帮手库。打开你项目里的build.gradle文件在dependencies区块里加上下面这些dependencies { // 相机和图片处理 implementation androidx.camera:camera-core:1.3.0 implementation androidx.camera:camera-camera2:1.3.0 implementation androidx.camera:camera-lifecycle:1.3.0 implementation androidx.camera:camera-view:1.3.0 // 网络请求和图片加载 implementation com.squareup.retrofit2:retrofit:2.9.0 implementation com.squareup.retrofit2:converter-gson:2.9.0 implementation com.github.bumptech.glide:glide:4.16.0 // 图片裁剪和增强这里用了一个流行的库 implementation com.github.yalantis:ucrop:2.2.8 // 权限请求 implementation com.guolindev.permissionx:permissionx:1.7.1 // 可选用于显示加载状态 implementation com.github.ybq:Android-SpinKit:1.4.0 }这些库各自负责一摊事CameraX帮我们搞定相机调用Retrofit负责网络通信UCrop用来裁剪图片PermissionX简化权限申请。用它们能省下我们大量造轮子的时间。1.2 服务端GLM-OCR API准备这是整个应用的“大脑”。你需要有一台服务器并把GLM-OCR服务部署上去。具体部署过程不是本文重点假设你已经有一个可以访问的API端点比如https://your-server.com/api/ocr。这个API通常接收一张图片比如通过表单上传然后返回一个JSON格式的结果。一个典型的响应可能长这样{ code: 200, message: success, data: { text: 这是识别出的完整文本..., blocks: [ { box: [[10, 20], [210, 20], [210, 50], [10, 50]], text: 这是第一行文字 }, // ... 更多文本块 ] } }其中text字段是所有识别文字的合并blocks里则保存了每一段文字的位置信息一个多边形框的四个顶点坐标和对应的文本。我们客户端就要根据这个结构来设计数据模型和解析逻辑。2. Android端核心功能实现思路理清了依赖也加好了接下来我们进入实战环节看看每个功能模块具体怎么写。2.1 相机拍摄与权限处理首先得让用户能拍照。我们使用CameraX这是Google官方推荐的相机库比直接调用Camera2 API要简单得多。第一步申请权限。在AndroidManifest.xml里声明需要相机和存储权限uses-permission android:nameandroid.permission.CAMERA / uses-feature android:nameandroid.hardware.camera android:requiredtrue / uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE android:maxSdkVersion28 / uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE /然后在拍照的Activity里用PermissionX动态申请权限class CameraActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_camera) PermissionX.init(this) .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE) .request { allGranted, _, _ - if (allGranted) { startCamera() } else { Toast.makeText(this, 需要相机和存储权限才能使用, Toast.LENGTH_SHORT).show() finish() } } } private fun startCamera() { // 配置CameraX预览 val cameraProviderFuture ProcessCameraProvider.getInstance(this) cameraProviderFuture.addListener({ val cameraProvider cameraProviderFuture.get() val preview Preview.Builder().build().also { it.setSurfaceProvider(binding.previewView.surfaceProvider) } val imageCapture ImageCapture.Builder() .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) .build() // 选择后置摄像头 val cameraSelector CameraSelector.DEFAULT_BACK_CAMERA try { cameraProvider.unbindAll() cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture) } catch(exc: Exception) { Log.e(Camera, 相机绑定失败, exc) } }, ContextCompat.getMainExecutor(this)) } }第二步实现拍照和保存。在布局里放一个拍照按钮点击时调用imageCapture.takePicture()方法将图片保存到应用的私有目录或公共相册。2.2 图片预处理裁剪与增强直接拍出来的照片往往包含多余的背景或者有透视畸变比如从侧面拍的文件。上传前做一下预处理能显著提升OCR的识别准确率。图片裁剪我们使用UCrop库它提供了非常流畅的裁剪体验。// 假设 originalUri 是刚拍好的照片的Uri val destinationUri Uri.fromFile(File(cacheDir, cropped_${System.currentTimeMillis()}.jpg)) UCrop.of(originalUri, destinationUri) .withAspectRatio(1f, 1.414f) // 设置裁剪框比例接近A4纸 .withMaxResultSize(2000, 2000) // 限制最大输出尺寸 .start(this)在onActivityResult里接收裁剪后的图片Uri。图片增强简单的增强可以在客户端做比如调整对比度、二值化让文字更突出。对于更复杂的处理如去阴影、去模糊如果GLM-OCR服务端支持也可以把原图上传在服务端做。这里展示一个简单的使用RenderScript提高对比度的例子注意RenderScript已废弃可考虑使用ColorMatrix或其他库替代fun enhanceContrast(bitmap: Bitmap): Bitmap { // 这是一个简化的示例实际生产环境建议使用更成熟的图像处理库 val outputBitmap Bitmap.createBitmap(bitmap.width, bitmap.height, bitmap.config) val canvas Canvas(outputBitmap) val paint Paint().apply { colorFilter ColorMatrixColorFilter(ColorMatrix().apply { setSaturation(0f) // 去色 // 调整对比度1.5为对比度系数 val scale 1.5f val translate (-0.5f * scale 0.5f) * 255f set(arrayOf( scale, 0f, 0f, 0f, translate, 0f, scale, 0f, 0f, translate, 0f, 0f, scale, 0f, translate, 0f, 0f, 0f, 1f, 0f )) }) } canvas.drawBitmap(bitmap, 0f, 0f, paint) return outputBitmap }处理好的图片就可以准备上传了。2.3 网络请求封装与API调用这是连接手机和云端OCR服务的桥梁。我们用Retrofit来构建网络请求层。第一步定义数据模型和API接口。根据之前假设的API响应我们定义对应的Kotlin数据类data class OcrResponse( val code: Int, val message: String, val data: OcrData? ) data class OcrData( val text: String, val blocks: ListTextBlock ) data class TextBlock( val box: ListListInt, // [[x1,y1], [x2,y2], [x3,y3], [x4,y4]] val text: String )然后定义Retrofit的Service接口interface OcrApiService { Multipart POST(ocr) // 你的API端点路径 suspend fun recognizeImage( Part image: MultipartBody.Part ): OcrResponse // 如果你的API还需要其他参数比如识别语言可以这样加 // Part(language) language: RequestBody }第二步创建Retrofit实例并调用。object ApiClient { private const val BASE_URL https://your-server.com/api/ private val retrofit: Retrofit by lazy { Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create()) .client(OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) // OCR可能较慢超时设长点 .readTimeout(60, TimeUnit.SECONDS) .build()) .build() } val ocrService: OcrApiService by lazy { retrofit.create(OcrApiService::class.java) } }第三步在ViewModel或Repository中执行上传和识别。class OcrRepository { suspend fun recognizeDocument(imageFile: File): ResultOcrData { return try { // 准备图片文件 val requestBody imageFile.asRequestBody(image/jpeg.toMediaTypeOrNull()) val imagePart MultipartBody.Part.createFormData(image, imageFile.name, requestBody) // 发起网络请求 val response ApiClient.ocrService.recognizeImage(imagePart) if (response.code 200 response.data ! null) { Result.success(response.data) } else { Result.failure(Exception(识别失败: ${response.message})) } } catch (e: Exception) { Result.failure(e) } } }在UI层如Activity或Fragment中使用viewModelScope.launch或lifecycleScope.launch来调用这个挂起函数并在IO线程中执行在主线程更新UI。2.4 识别结果的可视化与编辑拿到识别结果后我们不能只显示一串文字。更好的体验是让用户看到文字在原图上的位置并且可以针对识别有误的地方进行编辑。结果可视化我们可以在图片上绘制文本框。将服务器返回的box坐标通常是相对于上传图片的坐标转换到当前屏幕ImageView上显示的图片的坐标。自定义一个DrawView覆盖在ImageView上在onDraw方法中遍历所有TextBlock用Canvas.drawRect或Canvas.drawPath画出多边形框。class TextOverlayView JvmOverloads constructor( context: Context, attrs: AttributeSet? null ) : View(context, attrs) { var textBlocks: ListTextBlock emptyList() set(value) { field value invalidate() // 触发重绘 } private val paint Paint().apply { color Color.GREEN style Paint.Style.STROKE strokeWidth 4f isAntiAlias true } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) for (block in textBlocks) { // 假设 points 是已经转换好的屏幕坐标点数组 FloatArray val path Path() val points convertBoxToPoints(block.box) // 需要实现坐标转换函数 if (points.size 8) { // 4个点每个点x,y path.moveTo(points[0], points[1]) for (i in 1 until 4) { path.lineTo(points[i*2], points[i*21]) } path.close() canvas.drawPath(path, paint) } } } }文本编辑点击某个文本框时可以弹出一个对话框 (AlertDialog或BottomSheetDialog)里面放一个EditText显示识别出的原文允许用户修改。修改完成后更新数据源并刷新UI。结果导出导出功能很简单利用Android的分享机制即可。fun shareText(text: String) { val intent Intent(Intent.ACTION_SEND).apply { type text/plain putExtra(Intent.EXTRA_TEXT, text) } startActivity(Intent.createChooser(intent, 分享识别结果)) }也可以提供保存到文件的功能利用Context.getExternalFilesDir或MediaStoreAPI将文本保存为.txt文件。3. 性能优化与体验提升基础功能跑通后我们还得让App更好用、更稳定。这里有几个在实际开发中总结出来的小点。图片上传优化如果图片太大上传慢、耗流量服务器处理也慢。我们可以在上传前对图片进行合理的压缩。fun compressImage(file: File, maxSizeKB: Long 500): File { var quality 90 var outputFile File.createTempFile(compressed_, .jpg, context.cacheDir) file.copyTo(outputFile, overwrite true) while (outputFile.length() / 1024 maxSizeKB quality 10) { val bitmap BitmapFactory.decodeFile(outputFile.absolutePath) val stream ByteArrayOutputStream() bitmap.compress(Bitmap.CompressFormat.JPEG, quality, stream) outputFile.writeBytes(stream.toByteArray()) quality - 15 bitmap.recycle() } return outputFile }网络状态与错误处理网络请求一定要做好错误处理。使用try-catch包裹对不同的异常如连接超时、服务器错误、JSON解析错误给出友好的用户提示。可以使用Sealed Class来封装网络请求状态在UI中根据不同的状态Loading, Success, Error显示不同的界面。离线能力考虑虽然核心识别依赖网络但我们可以做一些离线工作。比如允许用户先拍摄多张图片将它们加入一个“待识别队列”等有网络时再批量上传。这需要本地数据库如Room来存储图片路径和任务状态。内存管理图片处理是内存消耗大户。务必注意及时回收Bitmap在onDestroy或页面不可见时清理资源。对于大图采用采样率加载 (BitmapFactory.Options.inSampleSize)。4. 总结把GLM-OCR集成到Android应用里开发一个文档扫描识别App听起来有点技术含量但拆解开来无非就是拍照、处理、上传、展示这几个步骤。CameraX让拍照变得简单Retrofit处理网络请求也很顺手难点可能更多在于图片预处理坐标的转换和UI交互细节的打磨。实际做下来最大的感受是预处理真的非常重要。一张裁剪端正、对比度清晰的图片识别准确率会比原图高出一大截。另外网络请求的稳定性和错误处理也决定了用户体验的下限这块多花点心思是值得的。这个Demo跑通之后其实还有很多可以深挖的地方。比如能不能加入批量处理识别结果除了文本能不能直接导出成Word或者PDF甚至结合更高级的模型实现表格识别、公式识别这些都可以作为后续迭代的方向。如果你正准备开始建议先从最核心的“拍-传-显”流程做起快速验证可行性然后再一步步添加更酷的功能。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。