安卓拍照OCR实战:从CameraX到ML Kit的工程落地与避坑指南
发布时间:2026/10/2 3:36:02来源:尧图网络
简介面向安卓开发者的文字识别应用项目包涵盖从拍照、图像显示到提取文字的完整流程适合需要快速集成离线识别功能的中初级开发者。压缩包内共808个文件主要类型包括Java源码、XML布局与配置、构建脚本、机器学习模型文件tflite、binarypb、本地so库、JSON配置以及编译产物等多种文件共同构成了一个可直接构建运行的安卓工程整体大小68.12MB。已有161人学习下载说明该项目对学习移动端文字识别有一定参考价值。项目可直接导入安卓开发环境运行主程序MainActivity.java演示了相机调用、动态权限申请、位图处理及机器学习套件文本识别的完整关键步骤离线模式下也能正常识别中文文本。资源中还包含模型文件、依赖配置和资源文件便于理解文字识别应用的整体项目结构也可作为二次开发的基础模板用于毕业设计或实际产品中的拍照识别功能。1. 安卓拍照 OCR 应用为什么值得用机器学习做一次联调翻车说起上个月帮朋友调一个 Android 端扫描名片的应用拍照、抠图、切成二值图再用传统模板匹配去读姓名和电话。结果十张名片里能正确读出来的不到四张得名片的反光、倾斜、字体和背景一换规则就全崩了。最后把识别层换成了机器学习 OCR——准确率肉眼可见地从“碰运气”变成“稳定可用”。这个项目标题说的正是这件事安卓手机 APP 拍照并使用机器学习进行 OCR 文字识别。它解决的不是“能不能识别”而是“在手机这种算力和拍照环境下怎么把拍摄图像稳定地读成可编辑文本”。适合手里有一份 APK 或 Android Studio 工程要落地、正为识别率和工程集成发愁的开发者往下看。后面所有代码和参数都按一条可复现的主线展开CameraX 拍照 → 图像预处理 → ML Kit 文本识别 → 结果解析。2. 三种 OCR 引擎选型ML Kit、Tesseract、PaddleOCR 在安卓端的边界与局限拿到“拍照 OCR”这类需求第一反应通常是找现成识别库。但安卓端能选的引擎就那么几条路Google 的 ML Kit 设备端文本识别、封装了 Tesseract 的 tess-two 或 Tesseract4Android、以及百度 PaddleOCR 的移动端方案。三者在模型体积、离线能力、中文识别效果和接入成本上差别非常大选错了后面每一步都在给错误买单。2.1 先区分“拍照—识别”链路与一次性批处理很多人把 OCR 工程想成“给一张图吐一行字”实际落地时有两个完全不同的链路。批量离线场景比如扫描一份 PDF 后逐页识别重点在准确率和多页稳定性可以用 Tesseract 在服务端跑也可以调在线 API。而“安卓手机 APP 拍照并使用机器学习进行 OCR 文字识别”这个标题强调的是一次性拍照、实时回传结果的交互链路。这种场景对延迟敏感用户举着手机等结果超过两三秒就会不耐烦同时设备环境不可控拍出来的图可能模糊、过暗、倾斜、反光。所以选型时要把“实时性”和“拍摄质量容错”排到最前面。链路不同引擎选择就完全不同。服务端 OCR 可以接受几百毫秒甚至秒级延迟和较大的模型端侧 OCR 必须把模型压在几十 MB 以内并且最好离线可用不让用户等网络。给业务方讲清楚“你要的是哪种识别”比直接推荐某个库更重要——这两类需求的验收标准都不一样。2.2 为什么第一版建议直接上 ML Kit 的设备端文本识别如果只是做一个能跑通的 MVP我一般建议先不用考虑 Tesseract 和 PaddleOCR直接上 ML Kit 的设备端文本识别Text Recognition v2。理由是它在安卓端的集成成本最低识别稳定性和语言支持都是开箱即用的水平尤其是对中英文混合文本的识别效果明显好过裸装 Tesseract。ML Kit 的设备端文本识别有两种模型标准模型Standard体积小速度快适合实时识别稀疏模型Sparse针对稀疏文本场景优化比如识别名片、发票、路牌等文字不密集的图像速度和准确率都有增益。拍照 OCR 场景下绝大多数是画面中文字区域占比不大的情况比如一张名片、一页纸张、一块屏幕截图所以稀疏模型往往是更好的选择。工程上只需要在TextRecognizerOptions.Builder里传模型类型不用改其他代码。需要说明的是ML Kit 的设备端文本识别依赖会在首次初始化时加载识别模型但模型不大对主流机型内存和存储的占用都可接受远小于 Tesseract 完整语言包动不动上百 MB 的体积。延迟方面在 2023 年之后的中端机型上识别一帧 1080p 图像耗时通常在 200~600 毫秒区间这个量级适合拍照后的准实时反馈。2.3 Tesseract 是备胎自定义训练与离线字典的取舍Tesseract 是老牌开源 OCR 引擎安卓上常见封装是 Tesseract4Android旧称 tess-two。它能出现在这类项目里主要原因是两个完全离线、可自定义训练。ML Kit 的设备端识别虽然也离线但模型是 Google 训练好的你改不了它的行为Tesseract 则允许你针对特定字体、特定版式做微调训练生成自定义的.traineddata语言包。对固定模板票据、统一字体印刷体这类高度受限场景Tesseract 反而更合适。但代价同样明显。一是精度未做训练的 Tesseract 在中文场景下的识别效果远不如现代深度学习模型字与字粘连、墨迹浓淡变化都会让结果惨不忍睹。二是预处理要求高它需要比较干净的灰度图和足够大的文字区域否则识别前图像增强的代码量会超过识别本身。三是接入成本要在 Gradle 里引入 NDK 依赖首次构建还可能因为下载预编译包慢而卡住。所以我的判断是如果你的需求是“通用文字识别什么环境都可能拍”Tesseract 不适合第一版如果需求是“识别某一种固定样式单据而且必须离线、可控”Tesseract 才值得投入。2.4 PaddleOCR 与开源模型的端侧适配成本PaddleOCR 在服务端是很强的一线方案中文识别准确率比上述两者都高社区里的训练和部署资料也多。但它原本就不是为安卓端设计的。官方虽然提供了 PaddleLite 移动端部署示例端侧模型也可以量化压缩到几十 MB但要真正塞进一个安卓 APP你需要处理模型转换Paddle 模型转 PaddleLite 格式、前后处理代码移植、动态 shape 设置、多线程调度等一系列工程问题。整个过程快则一两天慢则一周团队如果没有懂模型部署的人风险不小。它适合的安卓场景是你对识别准确率有硬指标且愿意为模型部署投入专门人力。对大多数做业务 APP 的团队来说ML Kit 的准确率已经能满足需求没必要在第一个版本就把技术难度拉高。后面如果业务数据确实暴露出 ML Kit 的短板再研究 PaddleOCR 迁移也不迟——但请让这个决策发生在“有真实样本证明”之后而不是前期“拍脑袋选最强模型”。对比维度ML Kit 文本识别TesseractPaddleOCR 端侧中文识别准确率良好一般未训练时偏差大优秀离线可用支持支持支持模型体积小语言包普遍偏大可量化后接受集成难度低Android Studio 直接加依赖中需处理 NDK 依赖高需模型转换和部署自定义训练不支持支持支持第一版推荐度高低视团队能力而定3. 用 CameraX 把“拍照”做成可供 OCR 的最低成本链路很多工程问题不是出在识别模型而是出在拍照那一环拍出来的图本身模糊、过曝或者旋转了 90 度后面识别再好也救不回来。这里需要一个稳定、可控的拍照链路。Android 上现在最省事的方案是 CameraX而不是直接用 Camera2 裸写。3.1 为什么要用 CameraX 而不是旧 Camera2 裸写Camera2 的问题是生命周期管理太琐碎。你要自己处理相机权限、Session 配置、Surface 生命周期、旋转角度任何一个环节出错在低端机上就容易黑屏或闪退。CameraX 把这一整套抽成了ProcessCameraProviderPreviewImageCapture的结构页面一打开就自动绑定到 LifecycleOwner离开页面自动释放相机资源。对于 OCR 这种“打开页面拍个照就走”的场景CameraX 可以省掉大约三分之一的样板代码而且它的ImageCapture输出图片时会自动带上 EXIF 方向信息这对后面 OCR 很重要——如果拿不到原始方向识别前要先猜图是不是歪的很麻烦。3.2 在 build.gradle 里加依赖与权限完整片段根目录build.gradle不用改只要在模块的build.gradleapp 那个里加依赖。注意 CameraX 的camera-camera2、camera-lifecycle、camera-view三个构件要统一版本号升级时一起改避免版本不匹配导致运行时找不到类。dependencies { implementation androidx.camera:camera-camera2:1.3.4 implementation androidx.camera:camera-lifecycle:1.3.4 implementation androidx.camera:camera-view:1.3.4 }版本号以你在 Android Studio 里实际能拉到的最新稳定版为准不要盲目跟着旧教程用 1.0.x。1.0 到 1.3 之间 API 有调整setTargetResolution的解析行为也变过老代码直接搬过来可能编译过不了。权限方面AndroidManifest.xml里声明相机权限即可。如果拍照后要读取或保存到公共目录还需要按安卓版本补充存储权限uses-permission android:nameandroid.permission.CAMERA / !-- 仅安卓 13 以下且需要保存公共目录时需要 -- uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE android:maxSdkVersion32 /运行时权限申请用ActivityResultContracts.RequestPermission就行注意在拒绝权限时给出引导说明否则用户只看到黑屏不知道怎么回事。3.3 拍一帧图转成 Bitmap核心代码与参数说明一般做法是先绑定相机预览然后设置ImageCapture的takePicture回调。这里我用OnImageCapturedCallback方式直接拿到ImageProxy转Bitmap省去先存文件再读文件的环节。private lateinit var imageCapture: ImageCapture private fun bindCamera(lifecycleOwner: LifecycleOwner) { val preview Preview.Builder() .setTargetResolution(Size(1920, 1080)) .build() imageCapture ImageCapture.Builder() .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) .setTargetRotation(WindowManagerCompat.getDefaultDisplay().rotation) .setTargetResolution(Size(1920, 1080)) .build() val cameraProviderFuture ProcessCameraProvider.getInstance(this) cameraProviderFuture.addListener({ val cameraProvider cameraProviderFuture.get() val cameraSelector CameraSelector.DEFAULT_BACK_CAMERA cameraProvider.unbindAll() cameraProvider.bindToLifecycle( lifecycleOwner, cameraSelector, preview, imageCapture ) }, ContextCompat.getMainExecutor(this)) } // UI 点击拍照后调用 imageCapture.takePicture( ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageCapturedCallback() { override fun onCaptureSuccess(image: ImageProxy) { val bitmap imageProxyToBitmap(image) image.close() // 把 bitmap 交给后续 OCR 处理 runOcr(bitmap) } override fun onError(exception: ImageCaptureException) { Log.e(CameraX, 拍照失败: ${exception.message}) } } )这里有几个参数是有讲究的CAPTURE_MODE_MINIMIZE_LATENCY优先保证快门响应快适合手持拍摄相对的是CAPTURE_MODE_MAXIMIZE_QUALITY画质优先但快门延迟明显手持容易糊。分辨率不是越高越好。OCR 场景下1920x1080已经足够个别细字号场景再上4032x3024反而拖慢识别速度并增加内存压力。方向问题交给setTargetRotation。如果这里不设置拍出来的横竖屏方向可能和用户看到的不一致后面识别就得多写一段旋转逻辑。CameraX 会把旋转角写进 EXIF你只需要在读取 Bitmap 时尊重 EXIF 方向即可。imageProxyToBitmap是常见的ImageProxy到Bitmap的转换private fun imageProxyToBitmap(image: ImageProxy): Bitmap { val planeProxy image.planes[0] val buffer planeProxy.buffer val pixelStride planeProxy.pixelStride val rowStride planeProxy.rowStride val rowPadding rowStride - pixelStride * image.width val bitmap Bitmap.createBitmap( image.width rowPadding / pixelStride, image.height, Bitmap.Config.ARGB_8888 ) bitmap.copyPixelsFromBuffer(buffer) return Bitmap.createBitmap(bitmap, 0, 0, image.width, image.height) }这段逻辑说明CameraX 默认输出的 YUV 格式planes[0]是亮度分量平面如果直接把整个 buffer 按 ARGB 格式拷贝会因为行字节对齐出现绿边或图像错位。所以要读取rowStride和pixelStride算出每行实际占用的字节数再按实际尺寸裁剪。这个坑几乎每个第一次对接 CameraX 的人都会踩不处理的话识别永远不对。3.4 拍到“看不清”的图怎么办先反推相机参数拍照链路里最容易翻车的不是代码而是“图糊了”。不是代码写错而是光线和抖动导致的。我常用两个缓解手段。第一个是手动对焦。CameraX 官方推荐用FocusMeteringAction用户在预览界面点击某个位置时触发对焦同时锁定曝光val meteringAction FocusMeteringAction.Builder( point, MeteringPointFactory.METERING_POINT_AREA_MODE_ON ).apply { setAutoCancelDuration(3, TimeUnit.SECONDS) }.build() camera.cameraControl.startFocusAndMetering(meteringAction)第二个是在低光环境下提示用户或者自动补光。CameraControl.enableTorch(true)可以打开闪光灯当手电筒用但要注意并不是所有机型都支持前后摄像头常亮手电调用前最好查一下CameraInfo.hasFlashUnit()否则部分机型会抛异常。拍完一张图之后建议立刻做一个“可识别性预检”如果 Bitmap 的灰度直方图方差过小说明对比度太低大概率是欠曝或过曝这种情况下在界面上给用户一个“请重拍”的提示比强行把图送进 OCR 再输出一堆乱码要好得多。这个预检代码我放在下一章讲预处理时一起给。4. 从拍照到 OCR 结果图片旋转、缩放与 ML Kit 的对接流水线拿到了 Bitmap接下来的问题是怎么把它送进 ML Kit 的识别器。这里有几层细节容易疏忽EXIF 旋转、图像缩放、模型初始化、结果解析。每一步不处理好识别结果都会差一个档次。4.1 正确创建 InputImageEXIF 旋转是绝大多数乱码的根源安卓手机拍出来的 Bitmap其像素数据本身可能已经是旋转过的也可能没有取决于你用哪种方式读取。如果直接用ImageProxy转出来的 Bitmap它是一张纯原始帧没有应用 EXIF 方向这时候直接送进 OCR相当于把原图旋转后的内容当正立图去看。正确的做法是拿到 Bitmap 后先用ExifInterface读取原始方向并把旋转应用到像素上val exif if (Build.VERSION.SDK_INT Build.VERSION_CODES.N) { ExifInterface(bitmap) } else { Suppress(DEPRECATION) ExifInterface(bitmap.path) } val orientation exif.getAttributeInt( ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL ) val rotatedBitmap when (orientation) { ExifInterface.ORIENTATION_ROTATE_90 - rotateBitmap(bitmap, 90f) ExifInterface.ORIENTATION_ROTATE_180 - rotateBitmap(bitmap, 180f) ExifInterface.ORIENTATION_ROTATE_270 - rotateBitmap(bitmap, 270f) else - bitmap }这里有个容易迷糊的点InputImage.fromBitmap(bitmap, rotationDegrees)的第二个参数不是“图片原本旋转了多少度”而是“要把图片旋转多少度才能显示为正立”。如果你已经手动旋转过 Bitmap这个参数传 0 就可以如果你拿到的是未处理的原始帧则需要把 EXIF 方向换算成对应的旋转角度传进去。两种方式殊途同归但别同时都做否则图片会被转两次OCR 结果也会反向。4.2 识别前要不要做预处理缩放到 1600px 宽度是性价比最高的操作很多人一提 OCR 就想到灰度化、二值化、降噪实际从工程结果看这些操作对 ML Kit 的识别效果提升远没有“把图片缩放到合适尺寸”大。ML Kit 内部有自己的图像归一化流程过度预处理反而可能破坏特征。真正有效的预处理是缩放和对比度增强但不要用太激进的方式。我常用的做法如下fun preprocessForOcr(original: Bitmap): Bitmap { // 1. 控制宽度不超过 1600px超出时等比缩小 val targetWidth 1600 val scale if (original.width targetWidth) { targetWidth.toFloat() / original.width } else 1f val scaled Bitmap.createScaledBitmap( original, (original.width * scale).toInt(), (original.height * scale).toInt(), true ) // 2. 对明显偏暗或偏亮的图做对比度拉伸保守参数 if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { val colorMatrix ColorMatrix().apply { setSaturation(1.2f) } val paint Paint().apply { colorFilter ColorMatrixColorFilter(colorMatrix) } val enhanced Bitmap.createBitmap(scaled.width, scaled.height, Bitmap.Config.ARGB_8888) Canvas(enhanced).drawBitmap(scaled, 0f, 0f, paint) return enhanced } return scaled }为什么是 1600px因为 ML Kit 的模型对输入尺寸有上限约束过大图片不会带来更高的识别精度只会增加内存和时间。1600px 宽度基本覆盖了手机拍摄文字的正常字号同时让单帧推理时间保持可控。灰度化和二值化我没有放在默认流程里。ML Kit 设备端模型是在自然图像上训练的直接喂彩色图反而有更好的泛化能力只有当你发现“黑底白字”或者“对比度极低”的图片识别失败时再做二值化兜底。那属于踩坑章节讨论的内容这里不展开。4.3 用 ML Kit 文本识别 v2 跑通中英文最小 Kotlin 代码ML Kit 文本识别依赖需要单独加进 Gradle。同样注意文本识别用到的构件是com.google.mlkit:text-recognition如果要中文识别能力还需要加对应的中文语言包com.google.mlkit:text-recognition-chinese。两个都加才能保证中文和英文都能被识别出来。// build.gradle 模块依赖 implementation com.google.mlkit:text-recognition:16.0.0 implementation com.google.mlkit:text-recognition-chinese:16.0.0 // 如果需要识别其他语言再按需加对应语言包 implementation com.google.mlkit:text-recognition-devanagari:16.0.0版本号同样以你拉取到的实际版本为准。这里最容易被忽略的是中文语言包只加基础依赖中文识别会直接失败或者输出空文本。识别调用代码如下private val recognizer: TextRecognizer by lazy { TextRecognition.getClient( TextRecognizerOptions.Builder() .setLanguageHints(listOf(zh, en)) .build() ) } fun runOcr(bitmap: Bitmap) { val inputImage InputImage.fromBitmap(preprocessForOcr(bitmap), 0) recognizer.process(inputImage) .addOnSuccessListener { result - parseResult(result) } .addOnFailureListener { e - Log.e(OCR, 识别失败: ${e.message}) } }setLanguageHints的作用是告诉模型“优先按这些语言识别”。不设置也能跑但遇到中英文混排时语言识别的准确性会下降。这里的语言代码要按 ML Kit 的语言代码规范写简体中文是zh不要写成zh-CN——那是 Android 区域设置里的写法OCR 模型不认。建议把recognizer做成单例或lazy避免每次拍一张照片都重新初始化一份模型。模型初始化开销大约在几百毫秒到一秒之间如果放在拍照回调里用户会明显感觉到卡顿。4.4 结果解析策略TextBlock、Line、Element 从粗到细ML Kit 的识别结果不是“一整块文本”而是三级结构TextBlock文本块、Text.Line行、Text.Element元素。一个文本块对应视觉上的一块区域比如名片上的姓名区、发票上的金额区一个块里包含若干行文本每行又由若干元素组成元素基本对应单词或单个汉字。工程上要按场景取值。如果只是要识别整页文本把每个Line拼接起来即可如果要提取结构化字段比如识别发票号、身份证号那就需要结合boundingBox做区域过滤。一个常见做法是按照文本块的中心点坐标排序确保输出的文本顺序和视觉顺序一致private fun parseResult(result: Text): String { val lines mutableListOfPairFloat, String() for (block in result.textBlocks) { for (line in block.lines) { val text line.text val box line.boundingBox if (box ! null) { val centerY box.centerY() lines.add(centerY to text) } } } // 按垂直位置排序保持阅读顺序 lines.sortBy { it.first } return lines.joinToString(\n) { it.second } }注意boundingBox可能为null所以要做空判断。按centerY排序适合大多数横版排版竖排文本场景需要换成centerX排序这个在避坑章节会展开讲。5. 避坑拍照 OCR 项目里最常见的 5 个翻车现场这部分是我在类似项目里反复踩过的坑每一条都按“现象 → 原因 → 解决”的顺序写。如果你照着前面的代码搭完识别率还是不对劲大概率命中的就是下面某一条。5.1 中文识别不出或大量乱码忘记引入中文语言包现象明明图片上是大段简体中文识别结果要么为空要么输出一行不知道什么语言的字符。更隐蔽的是图片里中英文混排时中文部分直接消失。原因ML Kit 文本识别的基础依赖基本只覆盖拉丁语系。中文识别需要显式引入text-recognition-chinese语言包没有这个包模型返回的语言假设里就没有中文。解决确认build.gradle里同时引入了com.google.mlkit:text-recognition和com.google.mlkit:text-recognition-chinese并且在TextRecognizerOptions.Builder中配置setLanguageHints(listOf(zh, en))。改完后 clean 再 build因为依赖变更偶尔会有增量编译不干净的问题。5.2 竖排文字识别出来却是横读完的现象一张竖排诗词或竖版广告照片识别后文本顺序是“从左到右按行跳读”完全读不通。原因ML Kit 的默认输出顺序基于西方阅读习惯按行、行内从左到右排列。竖排场景下文本行实际是按列排列的默认排序逻辑直接失效。解决解析结果时不要用行顺序拼接而是对TextBlock的boundingBox位置做判断。如果多个文本块的右上角点 X 坐标接近、Y 坐标递增说明是竖排此时按centerX从右到左排序输出// 竖排检测比较块的平均宽度与高度比 if (block.boundingBox.height() block.boundingBox.width()) { // 按右边界 X 值从大到小排列 }这是比较粗暴的启发式判断但实际够用。如果项目里大量出现竖排场景建议在拍照后的预检阶段就提示用户“将相机横持”横向构图对模型更友好。5.3 手机发烫、内存暴涨识别越跑越慢现象连续拍十几张照片后界面开始卡顿识别耗时从 300 毫秒涨到 1 秒以上最后甚至 OOM 崩溃。原因Bitmap.createBitmap产生大量对象识别结果持有的Bitmap没有被及时释放加上累计的ImageProxy没有调用close()YUV 缓冲被一直占着。安卓的内存回收对这种高频临时对象不友好内存水位一高GC 频繁触发卡顿就来了。解决三个地方确认到位。第一ImageProxy在onCaptureSuccess里用完必须调close()忘掉这个等于每次拍照泄漏一份不小的 buffer。第二preprocessForOcr里生成的中间Bitmap用完后及时recycle()。第三把识别逻辑放到Dispatchers.Default线程避免占用主线程。识别完成后再用runOnUiThread更新界面。scope.launch(Dispatchers.Default) { val result recognizer.process(inputImage).await() withContext(Dispatchers.Main) { binding.resultTextView.text result.text } }await()需要依赖kotlinx-coroutines-play-services这是 Kotlin 协程和 Google API Task 之间的桥接库加上这个依赖才能这样写。用它替代addOnSuccessListener代码会清爽很多也不需要一层层回调嵌套。5.4 识别结果被“重影”毁掉拍照抖动和长曝光现象识别出的文本单词间出现重复字母中文字出现笔画叠加坏掉明显是图像有重影。原因CAPTURE_MODE_MINIMIZE_LATENCY虽然快门快但部分手机在弱光下会偷偷降低快门速度来保证亮度导致手持抖动带来的运动模糊像素上形成重影。解决弱光环境下改成CAPTURE_MODE_MAXIMIZE_QUALITY或者手动增加曝光补偿但都不能根治。真正有效的手段是告诉用户“请拿稳手机”的提示文案并在识别前检测模糊度。检测方式不复杂把图像缩到 64x64 灰度后计算拉普拉斯方差数值低于阈值即判定为模糊。这个判断放进拍照回调里如果模糊就让用户重拍比事后识别失败更体面。5.5 黑底白字和反色图片识别率骤降现象同一个 OCR 引擎白底黑字识别得很好黑底白字几乎全军覆没。如果画面上是深色背景的二维码旁边一块浅色文字那块文字也经常读不出来。原因深度学习 OCR 模型的训练数据以自然图像为主其中白底黑字的占比极高。黑底白字相当于把颜色通道反转特征分布偏移模型置信度下降输出结果被大量过滤掉或拼接错乱。解决加一道“反色检测”逻辑。把 Bitmap 缩小到 16x16统计像素亮度均值如果均值低于 128说明整体偏暗可能是黑底白字就把图像取反色后再送识别。OpenCV 的Core.bitwise_not最省事如果不想引入 OpenCV用ColorMatrix的负矩阵也能实现val negative ColorMatrix( floatArrayOf( -1f, 0f, 0f, 0f, 255f, 0f, -1f, 0f, 0f, 255f, 0f, 0f, -1f, 0f, 255f, 0f, 0f, 0f, 1f, 0f ) )这个矩阵把 RGB 三个通道取负再加 255实现反色。注意只对“整体偏暗且文字区域反差大”的图使用正常照片强行反色反而会让识别变差。做一层均值判断再决定要不要反色而不是无脑全部反色。提示OCR 识别从来没有“一个模型打天下”。ML Kit 对印刷体、屏幕字、清晰手写体表现不错但遇到艺术字、严重透视畸变、超低分辨率小字仍然会翻车。上线前准备一批业务真实样张做回归测试比事后调参重要得多。6. 把准确率从“能用人眼确认”推进到“可上线”置信度过滤与回归验证前面的代码能跑通只是第一关。真正要把这个 OCR 功能交给用户还需要处理三个问题不可信结果的过滤、业务字段的定位、以及回归测试集的维护。6.1 置信度过滤只保留可信文本ML Kit 文本识别的TextBlock和Text.Line都带一个可空的confidence属性范围 0 到 1。实际工程里低于 0.5 的行基本都是误识别产物直接丢弃比展示出来更有价值。在解析结果时我习惯这样过滤private fun parseFilteredText(result: Text): String { val builder StringBuilder() for (block in result.textBlocks) { for (line in block.lines) { val confidence line.confidence ?: continue if (confidence 0.5f) continue builder.append(line.text).append(\n) } } return builder.toString() }注意confidence为null的场景比如某些离线模型不输出置信度此时不能空指针而是直接放行。过滤阈值不要定死在 0.5 以上。打印票据、扫描件这类清晰文本可以提高到 0.7但手持照片、光线不佳的情况下0.7 会把很多本来正确的行也滤掉。正确做法是先在回归集上跑一遍看置信度分布再定阈值。6.2 用固定样张做回归测试记录每张样本的识别文本这个习惯是我在一个发票识别项目里被逼出来的。当时开发阶段识别率很好上线后用户传的图五花八门识别率掉了一半。后来我把每类场景各收集了 20 张样张写了一个小脚本通过 adb 把样张推到应用沙箱目录再自动触发识别并导出结果之后每次改模型或改参数都跑一遍同一套图。Android 上做这个测试不需要复杂的自动化框架。你只需要把一批测试图片放在固定目录应用里写一个 Debug 入口依序读图识别把识别结果存成一个文本文件供比对。后期用一个小 Python 脚本计算字符级准确率diff 出变化即可import re def char_accuracy(pred, truth): pred re.sub(r\s, , pred) truth re.sub(r\s, , truth) correct sum(1 for p, t in zip(pred, truth) if p t) return correct / max(len(truth), 1) # 读取两个文件逐行比对对 OCR 项目来说跑通是及格回归稳定才是上线的前提。没有回归集任何一次模型升级都是在赌。6.3 留存一份“识别失败”测试集让踩坑成为长期资产最后建议你养成一个习惯把识别失败的图片单独存一份不要删。每一张失败图都是下一次优化的最佳切入点。遇到新版本第一件事就是拿旧失败集跑一遍看修复了多少、又新增了多少失败。如果新模型把旧问题解决了但引入了更多新问题说明是性能回退不能盲目升级。我在做这类拍照 OCR 应用时最深的体会是把“识别率”当成一个可观测的指标而不是一个玄学。每次改动都量一次问题会自己浮出来。希望这个思路能帮你在安卓拍照 OCR 的路上少走几段弯路。本文还有配套的精品资源点击获取
网站建设高端定制企业官网