ML Kit 是一款移动 SDK,可将 Google 的设备端机器学习专业知识融入到 Android 和 iOS 应用中。使用我们功能强大且易于使用的生成式 AI、Vision 和 Natural Language API 来解决应用中的常见难题,或打造全新的用户体验。所有功能均由 Google 的一流机器学习模型提供支持,并且免费提供给您。
随着移动端计算性能的增强,实时人脸检测与表情识别的需求日益增加,如社交应用的趣味滤镜、视频会议中的人脸跟踪、以及安防领域的面部分析等。本文章通过结合 CameraX 和 ML Kit,展示了如何在Android上快速实现高效、稳定的人脸检测与表情识别功能。
项目亮点:
1 实时检测人脸特征:包括微笑指数、眼睛睁开状态等关键信息。
2 直观的视觉化界面:自定义绘制层显示检测的轮廓点、表情符号和辅助信息。
3 现代化架构:基于CameraX,摆脱传统复杂的Camera API,兼顾易用性与性能。
4 高度可扩展性:可以轻松拓展到AR特效、行为分析等高级应用场景。
1. CameraX简介
1.1 CameraX介绍
Q1: 什么是 CameraX?
CameraX 是 Google 提供的 Android 相机库,旨在简化 Android 开发中相机功能的实现,并提供更好的跨设备兼容性。它通过统一的 API 接口来简化底层硬件的管理,使得开发者能够轻松地在不同的 Android 设备上实现相机相关的功能,而不需要处理复杂的设备差异。
Q2: CameraX 的工作原理是什么?
CameraX 的核心是相机功能的封装:它提供了几个模块来处理常见的相机操作:
Preview:展示实时的相机预览。
ImageCapture:拍摄照片。
ImageAnalysis:对图像流进行实时分析,用于人脸识别、物体检测等应用。
VideoCapture:录制视频。
这些模块通过统一的 API 接口进行操作。开发者通过 CameraX 可以轻松访问相机硬件的功能,而无需关心设备差异。CameraX 自动处理设备的适配工作,确保应用能够在各种 Android 设备上正常运行。
Q3: CameraX 一般用于哪里?有什么优缺点?
适用场景:
相机预览:展示实时摄像头画面,例如视频聊天、相机应用中的实时预览。
图像分析:进行人脸检测、物体识别、条码扫描等实时图像处理。
拍照与视频录制:在应用中实现拍照、录像功能。
优点:
简化开发:CameraX 提供了易于使用的 API 接口,开发者可以快速实现相机功能。
跨设备兼容性:CameraX 自动适配不同型号的设备,减少了设备差异带来的问题。
与 Jetpack 集成:与 Android Jetpack 库紧密集成,支持生命周期管理,提升了开发效率。
图像分析支持:通过 ImageAnalysis 模块,CameraX 可以与 ML Kit 等机器学习工具结合,进行实时图像分析。
缺点:
硬件限制:虽然 CameraX 提供了广泛的兼容性,但仍可能受设备硬件性能和系统限制的影响,特别是在低端设备上。
功能相对简单:尽管 CameraX 提供了很多基本的相机功能,但对于一些复杂的相机操作(如深度摄像、手动对焦等)可能不如传统相机 API 灵活。
2. ML Kit简介
2.1 ML Kit介绍
Q1: 什么是 ML Kit?
ML Kit 是 Google 提供的一个跨平台的机器学习工具包,旨在简化 Android 和 iOS 应用中机器学习功能的集成。它提供了一些现成的 API,帮助开发者在应用中快速实现各种机器学习任务,如图像分析、文字识别、语言处理等,无需深入了解机器学习的细节。
Q2: ML Kit 的工作原理是什么?
ML Kit 的核心是通过集成各种机器学习模型和预训练的 API,简化开发者的工作。开发者只需要调用相应的 API 接口,提供输入数据(如图像、文字、声音等),ML Kit 会自动进行处理并返回结果。ML Kit 支持两种主要的工作模式:
基于云端的 ML(Cloud-based):
ML Kit 使用 Google Cloud 的机器学习模型来处理数据,适用于需要高计算能力或大规模数据集的任务。例如,文字识别、面部识别等任务,处理速度和准确度较高,但需要网络连接。
基于设备的 ML(On-device):
ML Kit 使用本地设备的硬件资源来处理数据,无需互联网连接。它通过优化后的轻量级模型进行计算,适用于低延迟、隐私敏感的任务。例如,面部检测、条码扫描、语言翻译等任务。
Q3: ML Kit 一般用于哪里?有什么优缺点?
适用场景:
文字识别:将图像中的文字转换为文本,例如在文档扫描、图片文字识别中使用。
条码识别:快速扫描条形码和二维码,广泛应用于购物、票务等场景。
人脸检测:分析图像中的人脸,识别面部特征,用于人脸识别、面部表情分析等应用。
图像标记:识别图像中的常见对象,如动物、植物、物品等,用于图像分类、物体识别等。
语言翻译:通过机器学习技术对不同语言的文本进行实时翻译。
姿势检测:识别人体的关键姿势,用于运动、健身、游戏等场景。
优点:
易于使用:ML Kit 提供简单的 API 接口,开发者可以快速集成各种机器学习功能,而无需深入了解机器学习的技术细节。
预训练模型:ML Kit 提供的许多功能已经包含了预训练好的机器学习模型,减少了开发和训练模型的时间和成本。
跨平台支持:ML Kit 不仅支持 Android 平台,也支持 iOS,开发者可以在不同平台之间共享机器学习代码。
本地化支持:部分功能(如人脸检测、条码识别等)支持本地计算,无需网络连接,适合离线使用。
高效性:基于设备的模型使用本地硬件计算,具有较低的延迟和较好的性能。
项目效果图
:有人脸边框 ,眼睛 鼻子 嘴巴 眉毛


1 。导入依赖包
java
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
// ML Kit 人脸检测
implementation 'com.google.mlkit:face-detection:16.1.5'
// // CameraX
def camerax_version = "1.3.1"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
implementation "androidx.camera:camera-view:${camerax_version}"
2 。AndroidManifest.xml 添加权限 和配置Activity
java
<!-- 摄像头权限 -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- 声明使用摄像头特性 -->
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<activity
android:name=" .RenLianActivity"
android:screenOrientation="portrait"
android:theme="@style/NoActionBarCustoms" />
3.RenLianActivity
java
package com.sbas.mybledemohk;
import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.Gravity;
import android.view.Surface;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.camera.core.AspectRatio;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureException;
import androidx.camera.core.ImageProxy;
import androidx.camera.core.Preview;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.camera.view.PreviewView;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import com.google.android.material.button.MaterialButton;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.mlkit.vision.common.InputImage;
import com.google.mlkit.vision.face.Face;
import com.google.mlkit.vision.face.FaceDetection;
import com.google.mlkit.vision.face.FaceDetector;
import com.google.mlkit.vision.face.FaceDetectorOptions;
import com.google.mlkit.vision.face.FaceContour;
import com.google.mlkit.vision.face.FaceLandmark;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import android.annotation.SuppressLint;
@SuppressWarnings("deprecation")
@SuppressLint("UnsafeOptInUsageError")
public class RenLianActivity extends AppCompatActivity {
private Handler handler = new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
takePhoto();
}
};
private static final String TAG = "MainActivity";
private static final int REQUEST_CODE_PERMISSIONS = 10;
private static final String[] REQUIRED_PERMISSIONS = new String[]{Manifest.permission.CAMERA};
private PreviewView viewFinder;
private ExecutorService cameraExecutor;
private FaceDetector faceDetector;
private FaceOverlayView faceOverlayView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
viewFinder = findViewById(R.id.viewFinder);
faceOverlayView = findViewById(R.id.faceOverlay);
// 检查权限
if (allPermissionsGranted()) {
startCamera();
} else {
ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS);
}
// 设置人脸检测器 它用于处理从相机获取的图像并检测人脸。使用FaceDetectorOptions来设置检 测的性能模式、轮廓模式和分类模式。
FaceDetectorOptions options = new FaceDetectorOptions.Builder()
.setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST)
.setContourMode(FaceDetectorOptions.CONTOUR_MODE_ALL)
.setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_ALL)
.build();
faceDetector = FaceDetection.getClient(options);
cameraExecutor = Executors.newSingleThreadExecutor();
}
ImageCapture imageCapture;
private void startCamera() {
ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(this);
cameraProviderFuture.addListener(() -> {
try {
ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
//在此方法中,使用ProcessCameraProvider来绑定前置相机(CameraSelector.DEFAULT_FRONT_CAMERA)并设置预览、图像分析等。
//Preview:用于显示相机的预览视图。
//ImageAnalysis:处理每一帧图像,进行人脸分析。
Preview preview = new Preview.Builder()
.setTargetRotation(Surface.ROTATION_0)
.build();
preview.setSurfaceProvider(viewFinder.getSurfaceProvider());
viewFinder.post(() -> {
faceOverlayView.setPreviewSize(
viewFinder.getWidth(),
viewFinder.getHeight()
);
});
//拍照 使用这个类 。
imageCapture = new ImageCapture.Builder().build();
ImageAnalysis imageAnalysis = new ImageAnalysis.Builder()
.setTargetRotation(Surface.ROTATION_0)
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build();
imageAnalysis.setAnalyzer(cameraExecutor, this::analyzeFace);
CameraSelector cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA;
cameraProvider.unbindAll();
cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageAnalysis,imageCapture);
} catch (ExecutionException | InterruptedException e) {
Log.e(TAG, "相机启动失败", e);
}
}, ContextCompat.getMainExecutor(this));
}
private void takePhoto() {
if(imageCapture == null){
Toast.makeText(this,"拍照异常",Toast.LENGTH_SHORT).show();
return;
}
File file = new File(getExternalFilesDir(null), System.currentTimeMillis() + ".jpg");
ImageCapture.OutputFileOptions outputFileOptions = new ImageCapture.OutputFileOptions.Builder(file).build();
Toast.makeText(this,file.getAbsolutePath().toString(),Toast.LENGTH_SHORT).show();
imageCapture.takePicture(outputFileOptions, cameraExecutor, new ImageCapture.OnImageSavedCallback() {
@Override
public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
Uri uri = outputFileResults.getSavedUri();
if(uri != null){
File absoluteFile = file.getAbsoluteFile();
Log.i(TAG, "onImageSaved: absoluteFile:" + absoluteFile);
}
}
@Override
public void onError(@NonNull ImageCaptureException exception) {
Log.i(TAG, "onError: " + exception.getMessage(),exception);
}
});
}
private void UploadImg(File file ) {
}
long last_time = 0;
@SuppressWarnings("deprecation")
private void analyzeFace(@NonNull ImageProxy image) {
// 更新图像信息
faceOverlayView.setImageSourceInfo(
image.getWidth(),
image.getHeight(),
image.getImageInfo().getRotationDegrees()
);
//在analyzeFace()方法中,通过InputImage.fromMediaImage()将ImageProxy转换为InputImage,然后调用faceDetector.process(inputImage)进行人脸检测。
InputImage inputImage = InputImage.fromMediaImage(
image.getImage(),
image.getImageInfo().getRotationDegrees()
);
faceDetector.process(inputImage)
.addOnSuccessListener(faces -> {
runOnUiThread(() -> {
if((System.currentTimeMillis() - last_time)>150000) {
for (Face face : faces) {
for (FaceContour contour : face.getAllContours()) {
if (contour.getFaceContourType() == FaceContour.LOWER_LIP_BOTTOM) { //识别到人脸自动拍照 ,间隔15S。
last_time = System.currentTimeMillis();
handler.sendEmptyMessageDelayed(0,1000);
}
}
}
}
//绘制人脸信息
//通过自定义的 FaceOverlayView 绘制检测结果,包括: 微笑指数、眼睛睁开状态等文本信息。
faceOverlayView.updateFaces(faces);
});
})
.addOnFailureListener(e -> Log.e(TAG, "人脸检测失败", e))
.addOnCompleteListener(result -> image.close());
}
private boolean allPermissionsGranted() {
for (String permission : REQUIRED_PERMISSIONS) {
if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CODE_PERMISSIONS) {
if (allPermissionsGranted()) {
startCamera();
} else {
Toast.makeText(this, "未授予权限,无法使用相机", Toast.LENGTH_SHORT).show();
finish();
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
cameraExecutor.shutdown();
handler.removeMessages(0);
}
}
4 activity_main2.xml
java
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- CameraX预览视图 -->
<androidx.camera.view.PreviewView
android:id="@+id/viewFinder"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!-- 自定义绘制人脸数据的视图 -->
<com.sbas.mybledemohk.FaceOverlayView
android:id="@+id/faceOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
FaceOverlayView
java
package com.sbas.mybledemohk;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import com.google.mlkit.vision.face.Face;
import com.google.mlkit.vision.face.FaceContour;
import java.util.ArrayList;
import java.util.List;
//代码定义了一个自定义的FaceOverlayView类,它继承自View,用于在Android应用中显示基于ML Kit人脸检测的结果,包括面部轮廓、表情(如微笑、眼睛睁开等)和其他信息(如表情符号等)。下面是代码的详细解释:
public class FaceOverlayView extends View {
private Paint facePaint;
private Paint contourPaint;
private Paint textPaint;
private Paint emojiBgPaint;
private List<Face> faces;
private int previewWidth;
private int previewHeight;
private float scaleX;
private float scaleY;
private int imageWidth;
private int imageHeight;
private int rotation;
private float emojiSize = 60f;
private String currentMood = "😐";
private float blinkProgress = 0f;
private boolean isBlinking = false;
public FaceOverlayView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
facePaint = new Paint();
facePaint.setColor(Color.WHITE);
facePaint.setStyle(Paint.Style.STROKE);
facePaint.setStrokeWidth(2.0f);
contourPaint = new Paint();
contourPaint.setColor(Color.YELLOW);
contourPaint.setStyle(Paint.Style.FILL);
contourPaint.setStrokeWidth(2.0f);
textPaint = new Paint();
textPaint.setColor(Color.WHITE);
textPaint.setTextSize(40f);
textPaint.setAntiAlias(true);
emojiBgPaint = new Paint();
emojiBgPaint.setColor(Color.argb(100, 0, 0, 0));
emojiBgPaint.setStyle(Paint.Style.FILL);
faces = new ArrayList<>();
}
public void setImageSourceInfo(int width, int height, int rotation) {
this.imageWidth = width;
this.imageHeight = height;
this.rotation = rotation;
calculateScaleFactor();
}
public void setPreviewSize(int width, int height) {
previewWidth = width;
previewHeight = height;
calculateScaleFactor();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
calculateScaleFactor();
}
private void calculateScaleFactor() {
if (imageWidth > 0 && imageHeight > 0 && previewWidth > 0 && previewHeight > 0) {
float targetWidth = previewWidth * 0.4f;
scaleX = targetWidth / imageWidth;
scaleY = scaleX;
scaleX = (float) 3;
scaleY = (float) 3;
Log.i("tag",scaleX +" "+scaleY);
}
}
public void updateFaces(List<Face> faces) {
this.faces = faces;
invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (faces == null || faces.isEmpty() || imageWidth == 0 || imageHeight == 0) {
return;
}
for (Face face : faces) {
String mood = "😐";
if (face.getSmilingProbability() != null) {
float smileProb = face.getSmilingProbability();
if (smileProb > 0.8) mood = "😄";
else if (smileProb > 0.3) mood = "🙂";
}
if (face.getLeftEyeOpenProbability() != null && face.getRightEyeOpenProbability() != null) {
float leftEye = face.getLeftEyeOpenProbability();
float rightEye = face.getRightEyeOpenProbability();
if (leftEye < 0.3 && rightEye < 0.3) {
mood = "😉";
}
}
currentMood = mood;
for (FaceContour contour : face.getAllContours()) {
switch (contour.getFaceContourType()) {
case FaceContour.FACE:
contourPaint.setColor(Color.RED);
break;
case FaceContour.LEFT_EYE:
case FaceContour.RIGHT_EYE:
contourPaint.setColor(Color.CYAN);
break;
case FaceContour.LEFT_EYEBROW_TOP:
case FaceContour.RIGHT_EYEBROW_TOP:
case FaceContour.LEFT_EYEBROW_BOTTOM:
case FaceContour.RIGHT_EYEBROW_BOTTOM:
contourPaint.setColor(Color.BLUE);
break;
case FaceContour.NOSE_BRIDGE:
case FaceContour.NOSE_BOTTOM:
contourPaint.setColor(Color.RED);
break;
case FaceContour.UPPER_LIP_TOP:
case FaceContour.UPPER_LIP_BOTTOM:
case FaceContour.LOWER_LIP_TOP:
case FaceContour.LOWER_LIP_BOTTOM:
contourPaint.setColor(Color.MAGENTA);
break;
default:
contourPaint.setColor(Color.YELLOW);
}
for (PointF point : contour.getPoints()) {
float x = point.x * scaleX + 0 ;
float y = point.y * scaleY + 0 ;
// Log.i("tag",x +" "+y);
canvas.drawCircle(x, y, 3f, contourPaint);
if (contour.getPoints().size() > 1) {
Paint linePaint = new Paint(contourPaint);
linePaint.setStyle(Paint.Style.STROKE);
linePaint.setStrokeWidth(2f);
List<PointF> points = contour.getPoints();
for (int i = 0; i < points.size() - 1; i++) {
PointF p1 = points.get(i);
PointF p2 = points.get(i + 1);
float x1 = p1.x * scaleX + 0;
float y1 = p1.y * scaleY + 0;
float x2 = p2.x * scaleX + 0;
float y2 = p2.y * scaleY + 0;
canvas.drawLine(x1, y1, x2, y2, linePaint);
}
}
}
}
float offsetX = 50;
float offsetY = 50;
float bgLeft = offsetX;
float bgTop = offsetY + imageHeight * scaleY + 20;
float bgRight = bgLeft + 300;
float bgBottom = bgTop + 150;
// canvas.drawRoundRect(bgLeft, bgTop, bgRight, bgBottom, 20, 20, emojiBgPaint);
// canvas.drawText(currentMood, bgLeft + 20, bgTop + 50, textPaint);
// textPaint.setTextSize(30f);
// if (face.getSmilingProbability() != null) {
// String smileText = String.format("微笑指数: %.0f%%", face.getSmilingProbability() * 100);
// canvas.drawText(smileText, bgLeft + 20, bgTop + 100, textPaint);
// }
//
// if (face.getLeftEyeOpenProbability() != null) {
// String eyeText = String.format("眼睛睁开: %.0f%%",
// (face.getLeftEyeOpenProbability() + face.getRightEyeOpenProbability()) * 50);
// canvas.drawText(eyeText, bgLeft + 20, bgTop + 140, textPaint);
// }
}
invalidate();
}
}
代码定义了一个自定义的FaceOverlayView类,它继承自View,用于在Android应用中显示基于ML Kit人脸检测的结果,包括面部轮廓、表情(如微笑、眼睛睁开等)和其他信息(如表情符号等)。下面是代码的详细解释:
(1) 成员变量
facePaint、contourPaint、textPaint 和 emojiBgPaint:这些是Paint对象,分别用于绘制面部边框、面部轮廓、文本(例如显示微笑指数、眼睛睁开程度等)和表情符号背景。
faces:一个List对象,用于存储当前的所有检测到的人脸。
previewWidth、previewHeight:相机预览的宽度和高度。
scaleX、scaleY:缩放因子,用于将图像的坐标映射到预览视图的坐标。
imageWidth、imageHeight:图像的宽度和高度(即传入的原始图像尺寸)。
rotation:图像的旋转角度。
emojiSize:表情符号的大小。
blinkProgress 和 isBlinking:用于处理眼睛睁开状态和眨眼的进度。