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与Cloud
ML Kit从设计上就分了两条腿走路,这个划分是理解整个SDK的关键。
On-device API的特点是模型打包在App里或者首次使用时下载,推理完全在本地完成。优点是零网络依赖、延迟低、免费无限调用;代价是模型体积会占几MB到几十MB,精度相比云端大模型略低。适合的场景是实时性要求高、数据敏感、调用量大的功能。
Cloud API走的是Google服务器,基于更强大的模型,精度更高,但需要网络、有免费额度限制(超出要付费)、数据要上传。适合对精度要求极高、调用频率不高的场景,比如文档中的复杂表格解析。
我的一般建议是:能用端侧就用端侧。除非你的场景对精度有极端要求,否则端侧方案的体验和成本都更优。下面这张表是我实际选型时用的对照:
| 维度 | On-device | Cloud |
|---|---|---|
| 网络依赖 | 无 | 必须 |
| 典型延迟 | 50-300ms | 500ms-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把这些全封装了,你只需要关心输入输出。
代价是灵活性受限。比如OCR,ML 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包含若干Line,Line包含若干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 ACCURATE:FAST模式适合实时视频流,每帧都能跑;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() // 直接丢弃 return@setAnalyzer } 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功能才能真正稳定跑在用户的设备上,而不是只在你的开发机上好看。