ML Kit把文字识别和人脸检测放在同一个移动端视觉框架里,意味着你不用为了OCR单独集成Tesseract,也不用为了人脸框单独接OpenCV。虽然两个功能在算法上完全不同,但它们对输入图像的处理链路高度一致:先把相机帧或图片转成InputImage,再交给对应客户端,最后通过Task接口拿到异步结果。依赖配置和结果解析也遵循相同套路,熟悉其中一个后另一个上手很快。

依赖配置与模块选择
ML Kit提供两种发布形态。第一种是捆绑版,模型文件直接打进APK,安装后不依赖Google Play服务,适合国内设备或离线场景。第二种是Play服务版,功能通过系统级服务动态加载,能显著减小安装包体积,但设备必须装有兼容的Google Play服务。文字识别和人脸检测可以分别选择版本,但同一个功能不能同时依赖两种形式,否则会出现重复类或模型找不到。
以Android的Gradle配置为例,捆绑版需要引入以下依赖:
dependencies {
implementation 'com.google.mlkit:text-recognition:16.0.0'
implementation 'com.google.mlkit:face-detection:16.1.6'
}
如果使用Play服务版,依赖坐标换成com.google.android.gms:play-services-mlkit-text-recognition和com.google.android.gms:play-services-mlkit-face-detection。同步完成后,仅从相册选图或处理静态Bitmap不需要额外权限;如果从相机实时取流,记得在AndroidManifest.xml中声明CAMERA权限,Android 6.0以上还需要动态申请。
版本选择上,如果应用面向国内应用商店,捆绑版是更稳妥的选择;如果只上架Google Play且目标用户以海外机型为主,Play服务版可以减少包体积,还能利用系统更新。两个版本API类名基本一致,切换代价集中在依赖和初始化确认,不用重写业务逻辑。
文字识别的输入输出设计
文字识别使用TextRecognition.getClient()创建客户端,没有必须配置的参数。输入可以是Bitmap、media.Image、ByteBuffer或文件路径,统一包装成InputImage。输出是一个异步任务,回调里拿到Text对象。这个对象不是简单字符串,而是按结构组织的层级数据:Text包含多个TextBlock,每个TextBlock包含若干Line,每个Line又包含若干Element。每个层级都有独立的文本、边界框、角点和置信度。
下面的Kotlin代码从Bitmap中提取文字,并打印每个文本块和行:
val recognizer = TextRecognition.getClient()
val image = InputImage.fromBitmap(bitmap, 0)
recognizer.process(image)
.addOnSuccessListener { text ->
for (block in text.textBlocks) {
Log.d("OCR", "Block: ${block.text}, box: ${block.boundingBox}")
for (line in block.lines) {
Log.d("OCR", "Line: ${line.text}, confidence: ${line.confidence}")
for (element in line.elements) {
Log.d("OCR", "Element: ${element.text}")
}
}
}
}
.addOnFailureListener { e ->
Log.e("OCR", "Recognition failed", e)
}
这段代码中InputImage.fromBitmap(bitmap, 0)的第二个参数是旋转角度,必须与图片实际方向一致,否则边界框坐标会按旋转后的坐标系返回,你在画框时会发现位置偏移。建议在获取Bitmap时记录Exif方向,再转换成0、90、180、270之一传入。
实际做证件或票据识别时,直接遍历所有行不一定符合阅读顺序。ML Kit返回的文本块顺序通常接近版面布局,但对于多列文档仍可能错乱。可以按boundingBox.top或cornerPoints的y坐标排序,同一行再按x坐标排序。如果只想识别特定区域,先把原图裁剪成子Bitmap再喂给识别器,速度更快且干扰更少。
人脸检测的参数与结果解析
人脸检测通过FaceDetection.getClient(options)创建,参数集中在FaceDetectorOptions。构建器里最常改动的几个开关包括:性能模式设为PERFORMANCE_MODE_FAST或PERFORMANCE_MODE_ACCURATE;地标模式开启后返回眼睛、鼻子、嘴巴等关键点;轮廓模式开启后返回人脸外轮廓、眉毛、嘴唇等密集点位;分类模式开启后返回微笑概率和左右眼睁开概率。此外还有最小人脸尺寸setMinFaceSize,增大它可以过滤远处小人脸并提速。
val options = FaceDetectorOptions.Builder()
.setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_ACCURATE)
.setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_ALL)
.setContourMode(FaceDetectorOptions.CONTOUR_MODE_ALL)
.setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_ALL)
.setMinFaceSize(0.15f)
.build()
val detector = FaceDetection.getClient(options)
val image = InputImage.fromBitmap(bitmap, 0)
detector.process(image)
.addOnSuccessListener { faces ->
for (face in faces) {
val box = face.boundingBox
val smile = face.smilingProbability ?: 0f
val leftEyeOpen = face.leftEyeOpenProbability ?: 0f
Log.d("Face", "box=$box, smile=$smile, leftEyeOpen=$leftEyeOpen")
val contour = face.getContour(FaceContour.FACE_OVAL)
contour?.points?.forEach { point ->
Log.d("Face", "contour point: $point")
}
}
}
.addOnFailureListener { e ->
Log.e("Face", "Detection failed", e)
}
如果只做实时视频的简单人脸框,使用PERFORMANCE_MODE_FAST并关闭轮廓和分类,可以明显降低每帧耗时。静态照片分析才建议开PERFORMANCE_MODE_ACCURATE和全部开关,因为精确模式会调用更强的模型,低端机型单帧可能超过100毫秒。
解析结果时,boundingBox的坐标基于传入图像的像素坐标系。前置摄像头通常产生镜像预览,ML Kit返回的人脸框与屏幕显示方向不一致,需要根据预览View的缩放和镜像矩阵进行坐标变换。微笑概率不是二分类,而是一个0到1的浮点数,大于0.6基本可判为微笑。轮廓点中最实用的是FACE_OVAL,贴纸类功能可以直接用这些点计算脸颊位置。
混合检测的工程化与性能优化
在同一帧里同时跑文字识别和人脸检测,最容易犯的错误是在CameraX的Analyzer回调中同步执行两个耗时任务,导致帧积压或掉帧。ML Kit的process方法本身是异步的,但它内部仍然要占用CPU或GPU资源。合理的做法是区分优先级:人脸检测需要高帧率,可以每帧都跑;文字识别只做触发式,例如用户点击拍摄按钮或预览稳定后再跑。这样既能保证贴纸、美颜等人脸功能的流畅性,又不会浪费算力反复识别同一段文字。
下面是一个节流示例,在CameraX分析器里每500毫秒只执行一次OCR,人脸检测则每帧执行:
class AnalyzeUseCase(
private val textRecognizer: TextRecognitionClient,
private val faceDetector: FaceDetectionClient
) {
private var lastOcrTime = 0L
fun analyze(imageProxy: ImageProxy) {
val mediaImage = imageProxy.image ?: run {
imageProxy.close()
return
}
val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
val now = SystemClock.elapsedRealtime()
val shouldRunOcr = now - lastOcrTime >= 500
if (shouldRunOcr) {
lastOcrTime = now
}
faceDetector.process(inputImage)
.addOnSuccessListener { faces ->
drawFaceBoxes(faces)
}
.addOnCompleteListener {
if (shouldRunOcr) {
textRecognizer.process(inputImage)
.addOnCompleteListener {
imageProxy.close()
}
} else {
imageProxy.close()
}
}
}
}
这个示例在shouldRunOcr为true时,会等OCR任务完成后再关闭ImageProxy,避免提前释放媒体图像导致异常。生产环境中还需要给OCR和faceDetector添加失败回调,否则一旦识别失败,关闭动作可能永远不会执行。另一个更简单的做法是复制一份Bitmap专门给OCR使用,虽然多一次内存分配,但生命周期管理会清晰很多。
性能优化方面,可以限制相机输出的分辨率。默认CameraX可能生成1080p甚至更高分辨率的YUV帧,对两个检测器都是负担。通过ImageAnalysis.Builder().setTargetResolution(Size(1280, 720))能显著降低耗时。文字识别对分辨率更敏感,低于720p时小字可能漏检;人脸检测对分辨率相对宽容,480p也能稳定工作。具体数值需要根据业务场景在真机上调试。
混合使用时的内存峰值也要关注。每帧创建InputImage如果忘记关闭,或者Bitmap没有回收,长时间运行会触发OOM。建议在onDestroy或页面不可见时调用textRecognizer.close()和faceDetector.close()释放原生资源。ML Kit客户端对象可以重复使用,不要每帧重新创建,否则初始化开销会拖慢整个链路。
把文字识别和人脸检测放在同一个界面里并不复杂,关键在于理解两类任务的频率要求和结果坐标系,再通过节流与参数裁剪控制负载。ML Kit统一了输入输出模型,让这两个能力可以共用大部分图像预处理代码,适合需要快速交付的移动端视觉需求。