
1. 设备端AI到底在解决什么问题第一次接触ML Kit是在一个离线票据识别的需求里。当时团队的第一反应是调云端OCR接口结果实测发现两个致命问题一是弱网环境下响应时间从300ms飙到4秒以上二是票据上带有客户名称和金额走云端意味着数据要出设备合规那边直接卡死。后来换成设备端方案识别耗时稳定在200ms以内数据全程不出设备这才把项目推下去。这个经历基本代表了设备端AI的核心价值低延迟、离线可用、数据隐私。ML Kit就是Google把这一整套能力封装成开箱即用的SDK让Android和iOS开发者不用自己训模型、不用懂TensorFlow几行代码就能调用文本识别、人脸检测、条码扫描、语言识别、翻译、智能回复这些能力。它解决的是我不想成为ML工程师但我需要AI功能这个矛盾。适合谁用三类人一是做工具类App的独立开发者二是需要在现有产品里快速加AI能力的移动端团队三是对数据合规有硬要求的行业应用开发者。如果你正在纠结这个功能到底走云端还是走端侧这篇文章会把选型逻辑、实操细节和踩坑经验都摊开讲。2. ML Kit的整体架构与方案选型2.1 两条技术路线On-device与CloudML Kit从设计上就分了两条腿走路这个划分是理解整个SDK的关键。On-device API的特点是模型打包在App里或者首次使用时下载推理完全在本地完成。优点是零网络依赖、延迟低、免费无限调用代价是模型体积会占几MB到几十MB精度相比云端大模型略低。适合的场景是实时性要求高、数据敏感、调用量大的功能。Cloud API走的是Google服务器基于更强大的模型精度更高但需要网络、有免费额度限制超出要付费、数据要上传。适合对精度要求极高、调用频率不高的场景比如文档中的复杂表格解析。我的一般建议是能用端侧就用端侧。除非你的场景对精度有极端要求否则端侧方案的体验和成本都更优。下面这张表是我实际选型时用的对照维度On-deviceCloud网络依赖无必须典型延迟50-300ms500ms-3s数据隐私数据不出设备需上传费用免费有免费额度超出计费模型体积几MB到几十MB无精度中高高离线可用是否2.2 核心能力清单与适用判断ML Kit目前提供的能力大致可以分成几类我按实际使用频率排个序Vision类文本识别OCR、人脸检测、条码扫描、图像标注、对象检测与追踪、数字墨水识别Language类语言识别、翻译、智能回复其他自定义模型集成TensorFlow Lite模型选型时有个容易踩的坑人脸检测和人脸识别是两回事。ML Kit的人脸检测只做检测到人脸关键点轮廓表情概率不做身份识别。如果你要做这是不是同一个人得自己拿关键点去比对或者上自定义模型。很多新手看到Face Detection就以为能做人脸登录结果发现API里根本没有比对功能。2.3 为什么选ML Kit而不是自己训模型这个问题我被问过很多次。自己训模型的路径是收集数据→标注→训练→量化→转TFLite→集成→调优一套下来少说几周还得有ML背景的人。ML Kit把这些全封装了你只需要关心输入输出。代价是灵活性受限。比如OCRML Kit的中文识别对印刷体效果很好但对手写体、艺术字就一般。如果你的场景是识别特定格式的手写单据可能还是得自己训。所以判断标准很简单通用能力用ML Kit垂直场景考虑自训。3. 文本识别从集成到落地的完整实操3.1 依赖配置与模型下载策略先看Gradle配置。这里有个关键选择用打包模型还是动态下载。// 方式一模型打包进APK体积大但首次即用 dependencies { implementation com.google.mlkit:text-recognition:16.0.0 implementation com.google.mlkit:text-recognition-chinese:16.0.0 } // 方式二依赖Google Play Services动态下载 dependencies { implementation com.google.android.gms:play-services-mlkit-text-recognition:19.0.0 }两种方式的取舍很实际。打包方式APK会增大中文模型约几MB但用户装完就能用不依赖设备上的服务框架。动态下载方式APK小但首次调用要等模型下载而且依赖设备装了Google Play Services——国内很多设备没有这是硬伤。我的经验是面向国内市场的App一律用打包方式别赌用户设备上有服务框架。面向海外的可以用动态下载省体积。3.2 识别流程的代码骨架核心调用其实很简单但细节决定成败// 1. 创建识别器指定中文 val recognizer TextRecognition.getClient( ChineseTextRecognizerOptions.Builder().build() ) // 2. 从Bitmap或InputImage构建输入 val image InputImage.fromBitmap(bitmap, 0) // 3. 异步识别 recognizer.process(image) .addOnSuccessListener { visionText - for (block in visionText.textBlocks) { for (line in block.lines) { val text line.text val box line.boundingBox // 关键拿到位置 // 处理每一行 } } } .addOnFailureListener { e - // 处理失败 }这里有个新手常忽略的点识别结果是树状结构。TextBlock包含若干LineLine包含若干Element词每个层级都有boundingBox和confidence。如果你只取visionText.text拿整段文字就丢掉了位置信息。做票据结构化提取时位置信息才是关键——比如金额这个词右边同一行的数字才是金额值。3.3 图像预处理识别率的分水岭直接拿相机原图去识别效果往往不理想。我实测下来预处理能把识别率提升20%以上。几个关键操作灰度化二值化彩色信息对OCR没用转灰度再自适应二值化能显著提升对比度。用OpenCV的话// 灰度化 Imgproc.cvtColor(src, gray, Imgproc.COLOR_RGBA2GRAY) // 自适应二值化比全局阈值更抗光照不均 Imgproc.adaptiveThreshold(gray, binary, 255, Imgproc.ADAPTIVE_THRESH_GAUSSIAN_C, Imgproc.THRESH_BINARY, 15, 8)尺寸归一化ML Kit对输入图像有尺寸偏好太小识别不清太大浪费算力。我的经验是把长边缩放到1280-1600像素之间最平衡。缩放用双线性插值别用最近邻后者会产生锯齿影响识别。旋转校正如果拍摄角度歪了识别率断崖式下跌。可以用ML Kit返回的文本行角度做粗校正或者集成一个轻量的方向检测。注意预处理不是越多越好。我见过有人叠加了七八种滤波结果把文字边缘磨没了识别率反而下降。灰度二值化尺寸归一化这三步覆盖90%的场景剩下的按需加。3.4 结构化提取的实战思路OCR出来是一堆文本行业务要的是结构化字段。以识别一张名片为例我的做法是先拿到所有Line及其boundingBox按y坐标聚类把同一水平区域的Line归为一行用关键词匹配定位字段含电话Tel的行提取数字含的判为邮箱用位置关系兜底如果关键词没匹配到取最上方最大的文本块作为姓名这套逻辑没有银弹本质是规则位置的组合。我试过纯规则遇到版式变化就崩也试过纯位置遇到字段缺失就错。两者结合再针对具体业务调阈值才能稳定。4. 人脸检测与条码扫描的关键细节4.1 人脸检测的参数调优人脸检测的API看起来简单但几个参数直接决定体验val options FaceDetectorOptions.Builder() .setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST) .setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_ALL) .setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_ALL) .setMinFaceSize(0.15f) .enableTracking() .build()PERFORMANCE_MODE_FAST vs ACCURATEFAST模式适合实时视频流每帧都能跑ACCURATE适合单张照片精度更高但慢。做相机预览时用FAST拍照定格后用ACCURATE复检这个组合我用了很多次。setMinFaceSize这个值是人脸相对图像宽度的最小比例。默认0.1意味着人脸至少要占图像宽度的10%才能被检测到。做远距离检测时调小到0.05但误检率会上升。做自拍场景调到0.3能过滤掉背景里的路人脸。enableTracking开启后会给每张脸分配一个ID跨帧追踪同一个人。做检测到人脸才拍照这类功能时必开否则每帧都当新脸处理逻辑会乱。4.2 条码扫描的格式与性能条码扫描支持一维码EAN、UPC、Code128等和二维码QR、Data Matrix、PDF417等。配置时只开你需要的格式这是性能优化的关键val options BarcodeScannerOptions.Builder() .setBarcodeFormats( Barcode.FORMAT_QR_CODE, Barcode.FORMAT_CODE_128 ) .build()我做过对比测试全格式开启时单帧处理约80ms只开QR和Code128后降到25ms。原因很简单每多一种格式就多一轮解码尝试。所以按业务需要精确指定格式别图省事全开。另一个坑是扫描区域。默认扫描整帧但用户往往把码放在屏幕中央。可以设置感兴趣区域ROI只处理中间部分既提速又减少误扫。ROI的坐标要按图像实际尺寸算别用屏幕坐标直接套两者比例可能不同。4.3 实时视频流的处理节奏做实时扫描时最容易犯的错是每帧都处理。相机预览通常30fps如果每帧都跑识别CPU直接拉满手机发烫。我的做法是节流跳帧用ImageAnalysis的STRATEGY_KEEP_ONLY_LATEST策略保证只处理最新帧再在回调里加个时间判断比如每100ms才处理一次。这样既保证响应及时又不至于过载。imageAnalysis.setAnalyzer(executor) { imageProxy - val now System.currentTimeMillis() if (now - lastProcessTime 100) { imageProxy.close() // 直接丢弃 returnsetAnalyzer } lastProcessTime now // 正常处理 }关键提醒imageProxy一定要close否则缓冲区耗尽相机直接卡死。这个坑我踩过排查了半天才发现是没释放。5. 语言能力与自定义模型的进阶玩法5.1 语言识别与翻译的组合使用语言识别Language ID和翻译Translation经常配合使用。典型场景是用户输入一段文字先识别是什么语言再翻译成目标语言。语言识别返回的是语言代码和置信度比如en英语置信度0.98。这里要注意短文本识别不准。输入OK这种可能被识别成多种语言。我的处理是设置一个置信度阈值比如0.7低于阈值就提示用户手动选择语言。翻译模型是按语言对下载的比如英译中是一个模型中译英是另一个。这意味着首次使用某语言对时要下载模型约30MB下载后离线可用支持的语言对有限不是任意两种语言都能互译// 下载英译中模型 val conditions TranslatorOptions.Builder() .setSourceLanguage(TranslateLanguage.ENGLISH) .setTargetLanguage(TranslateLanguage.CHINESE) .build() val translator Translation.getClient(conditions) translator.downloadModelIfNeeded() .addOnSuccessListener { translator.translate(Hello) .addOnSuccessListener { result - /* 处理结果 */ } }模型下载要处理网络失败、存储不足这些情况别假设一定成功。我一般会加个重试和降级逻辑。5.2 自定义TensorFlow Lite模型集成ML Kit允许你集成自己的TFLite模型这是它从开箱即用走向可定制的入口。流程是训练并导出TFLite模型记得做量化否则体积和速度都难看把.tflite文件放到assets目录用CustomRemoteModel或CustomLocalModel加载通过Interpreter跑推理val interpreter Interpreter(loadModelFile(model.tflite)) val input ByteBuffer.allocateDirect(224 * 224 * 3 * 4) .order(ByteOrder.nativeOrder()) val output Array(1) { FloatArray(1000) } interpreter.run(input, output)这里的关键是输入输出的张量形状和数据类型必须和模型严格对齐。我见过最常见的错误是输入buffer的字节数算错导致推理结果全是乱码。算字节数的公式是宽 × 高 × 通道数 × 每元素字节数。Float32是4字节别漏乘。5.3 模型量化对端侧性能的影响自定义模型上端侧量化是绕不开的话题。对比一下量化方式模型体积推理速度精度损失Float32基准基准无Float16约50%略快极小INT8约25%快2-4倍小到中等我的建议优先Float16体积减半精度几乎无损兼容性也好。INT8虽然更快更小但需要校准数据集且部分算子不支持容易踩坑。除非你的模型确实跑不动否则别一上来就INT8。6. 常见问题排查与避坑实录6.1 识别率低的排查路径识别率不理想时按这个顺序排查图像质量先看原图清不清晰。模糊、过曝、欠曝的图任何OCR都救不了分辨率太小640px识别不清太大4000px反而因为缩放损失细节文字方向倾斜超过15度识别率明显下降需要校正语言模型中文文本用了英文识别器结果必然是乱码字体艺术字、手写体是通用OCR的天敌考虑自训我整理了一张速查表现象可能原因解决方向完全识别不出语言模型选错换对应语言的识别器部分字符错图像模糊/分辨率低预处理增强位置框错乱图像旋转方向校正中文识别成乱码用了默认英文模型引入中文模型首次调用超时模型在下载预下载或打包模型6.2 内存与性能问题端侧AI最怕OOM和卡顿。几个实测有效的优化Bitmap复用别每次new一个Bitmap用inBitmap复用内存。相机预览场景下这个优化能减少大量GC。及时释放识别器用完要close()尤其是自定义模型不释放会一直占内存。后台线程所有识别操作放子线程主线程只做UI更新。ML Kit的API本身是异步的但图像预处理是同步的别在主线程做。降采样如果只是检测有没有文字不需要高清图降采样到720p足够速度能快一倍。6.3 跨平台与版本兼容ML Kit在Android和iOS上API设计思路一致但细节不同。Android依赖Google Play Services动态下载方式iOS依赖CocoaPods集成。跨平台项目要注意两端模型文件格式不同不能共用返回的数据结构字段名有差异封装一层统一接口版本更新节奏不同锁版本号避免意外我的经验跨平台项目里把ML Kit的调用封装成平台无关的接口层业务代码只依赖接口。这样换实现或升级SDK时改动范围可控。7. 我踩过的几个真实坑说几个文档里不会写、但实际会遇到的坑。第一个是模型下载的静默失败。动态下载方式下downloadModelIfNeeded在某些设备上会一直pending既不成功也不失败。后来发现是设备存储空间不足但没报错。解决办法是加超时机制超时后降级到打包模型或提示用户。第二个是并发调用。同一个识别器实例被多个线程同时调用结果会串。ML Kit的识别器不是线程安全的要么加锁要么每个线程一个实例。我现在的做法是用对象池管理识别器实例。第三个是图像格式。InputImage.fromBitmap和InputImage.fromMediaImage的旋转角度处理不一样。相机预览的MediaImage自带rotationDegrees直接转Bitmap会丢这个信息导致识别结果方向错乱。正确做法是把rotation传给InputImage.fromMediaImage(image, rotationDegrees)。第四个是ProGuard混淆。Release包开启混淆后ML Kit的某些类被裁掉运行时报ClassNotFound。需要在proguard-rules里加keep规则。这个坑很隐蔽Debug包正常Release包崩溃排查起来费劲。这些坑的共同点是都能在文档里找到蛛丝马迹但只有真正踩过才知道疼。所以我的建议是集成ML Kit时先跑通最小demo再逐步加业务逻辑每加一步都测Release包别等最后才发现问题。设备端AI这条路ML Kit算是把门槛降到了最低。但能用和好用之间隔着图像预处理、参数调优、异常处理这些脏活累活。把上面这些细节吃透你做的AI功能才能真正稳定跑在用户的设备上而不是只在你的开发机上好看。