Cùng với việc phát hành Google Play services phiên bản 7.8, Google đã giới thiệu Mobile Vision API, cho phép thực hiện các chức năng như phát hiện khuôn mặt, quét mã vạch và nhận dạng văn bản.
Trong hướng dẫn này, chúng ta sẽ xây dựng một ứng dụng Android có khả năng nhận diện khuôn mặt người trong hình ảnh.

Chức năng nhận diện khuôn mặt trên Android
Face Detection API trên Android theo dõi khuôn mặt trong ảnh, video bằng cách sử dụng một số điểm đặc trưng như mắt, mũi, tai, má và miệng. Thay vì nhận diện từng đặc điểm riêng lẻ, API sẽ phát hiện toàn bộ khuôn mặt trước, sau đó nếu được yêu cầu, sẽ nhận diện các điểm đặc trưng và phân loại khuôn mặt. Ngoài ra, API còn có thể phát hiện khuôn mặt ở nhiều góc độ khác nhau.
Nhận diện khuôn mặt trên Android – Các điểm đặc trưng
Điểm đặc trưng là một điểm nổi bật nằm trong khuôn mặt. Mắt trái, mắt phải và gốc mũi là những ví dụ về các điểm đặc trưng. Dưới đây là danh sách các điểm đặc trưng mà API hiện tại có thể phát hiện:
- Mắt trái và mắt phải
- Tai trái và tai phải
- Đỉnh tai trái và đỉnh tai phải
- Gốc mũi
- Má trái và má phải
- Khóe miệng trái và khóe miệng phải
- Gốc miệng
Khi sử dụng các thuật ngữ “trái” và “phải”, cần hiểu rằng đó là theo hướng của chủ thể trong ảnh. Ví dụ, điểm LEFT_EYE sẽ tương ứng với mắt bên trái của người được chụp, không phải là mắt nằm bên trái theo góc nhìn của người quan sát ảnh.
Phân loại
Phân loại giúp xác định xem một đặc điểm khuôn mặt cụ thể có hiện diện hay không. API phát hiện khuôn mặt của Android hiện hỗ trợ hai dạng phân loại:
- Mắt mở: sử dụng các phương thức
getIsLeftEyeOpenProbability()vàgetIsRightEyeOpenProbability() - Cười: sử dụng phương thức
getIsSmilingProbability()
Hướng khuôn mặt
Hướng của khuôn mặt được xác định bằng cách sử dụng các Góc Euler. Các góc này thể hiện mức độ xoay của khuôn mặt quanh ba trục X, Y và Z.
- Euler Y cho biết khuôn mặt đang nhìn sang trái hay phải
- Euler Z cho biết khuôn mặt đang bị xoay nghiêng
- Euler X cho biết khuôn mặt đang nhìn lên hay xuống (hiện chưa được hỗ trợ)
Lưu ý: Nếu có một xác suất nào đó không thể tính toán được thì giá trị của nó sẽ được gán là -1.
Bây giờ, chúng ta sẽ đi vào phần chính của hướng dẫn này. Ứng dụng của chúng ta sẽ chứa một số hình ảnh mẫu và chức năng chụp ảnh trực tiếp.
Lưu ý: API này chỉ hỗ trợ phát hiện khuôn mặt (face detection). Tính năng nhận diện khuôn mặt (face recognition) hiện không được hỗ trợ trong Mobile Vision API.
Cấu trúc dự án ví dụ về nhận diện khuôn mặt trên Android

Mã nguồn phát hiện khuôn mặt trên Android
Thêm dòng phụ thuộc sau vào bên trong tệp build.gradle của ứng dụng:
compile 'com.google.android.gms:play-services-vision:11.0.4'
Thêm thẻ meta-data sau vào bên trong thẻ <application> trong tệp AndroidManifest.xml như minh họa dưới đây:
<meta-data
android:name="com.google.android.gms.vision.DEPENDENCIES"
android:value="face"/>
Thẻ này cho thư viện Vision biết rằng bạn có kế hoạch phát hiện khuôn mặt trong ứng dụng.
Thêm các quyền sau vào bên trong thẻ <manifest> trong tệp AndroidManifest.xml để cấp quyền sử dụng camera:
<uses-feature
android:name="android.hardware.camera"
android:required="true"/>
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
Mã nguồn cho tệp giao diện activity_main.xml như sau:
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="<https://schemas.android.com/apk/res/android>"
xmlns:app="<https://schemas.android.com/apk/res-auto>"
xmlns:tools="<https://schemas.android.com/tools>"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:context="com.journaldev.facedetectionapi.MainActivity">
<ImageView
android:id="@+id/imageView"
android:layout_width="300dp"
android:layout_height="300dp"
android:layout_marginTop="8dp"
android:src="@drawable/sample_1"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btnProcessNext"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="PROCESS NEXT"
app:layout_constraintHorizontal_bias="0.501"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imageView" />
<ImageView
android:id="@+id/imgTakePic"
android:layout_width="250dp"
android:layout_height="250dp"
android:layout_marginTop="8dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/txtSampleDescription"
app:srcCompat="@android:drawable/ic_menu_camera" />
<Button
android:id="@+id/btnTakePicture"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="TAKE PICTURE"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imgTakePic" />
<TextView
android:id="@+id/txtSampleDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginTop="8dp"
android:gravity="center"
app:layout_constraintBottom_toTopOf="@+id/txtTakePicture"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btnProcessNext"
app:layout_constraintVertical_bias="0.0" />
<TextView
android:id="@+id/txtTakePicture"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btnTakePicture" />
</android.support.constraint.ConstraintLayout>
</ScrollView>
Trong bố cục trên, chúng ta định nghĩa hai ImageView, hai TextView và hai Button. Một bộ điều khiển sẽ lặp qua các hình ảnh mẫu và hiển thị kết quả. Bộ còn lại được sử dụng để chụp ảnh từ camera.
Mã nguồn cho tệp MainActivity.java được cung cấp bên dưới.
package com.journaldev.facedetectionapi;
import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.net.Uri;
import android.os.Environment;
import android.provider.MediaStore;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.SparseArray;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.gms.vision.Frame;
import com.google.android.gms.vision.face.Face;
import com.google.android.gms.vision.face.FaceDetector;
import com.google.android.gms.vision.face.Landmark;
import java.io.File;
import java.io.FileNotFoundException;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
ImageView imageView, imgTakePicture;
Button btnProcessNext, btnTakePicture;
TextView txtSampleDesc, txtTakenPicDesc;
private FaceDetector detector;
Bitmap editedBitmap;
int currentIndex = 0;
int[] imageArray;
private Uri imageUri;
private static final int REQUEST_WRITE_PERMISSION = 200;
private static final int CAMERA_REQUEST = 101;
private static final String SAVED_INSTANCE_URI = "uri";
private static final String SAVED_INSTANCE_BITMAP = "bitmap";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
imageArray = new int[]{R.drawable.sample_1, R.drawable.sample_2, R.drawable.sample_3};
detector = new FaceDetector.Builder(getApplicationContext())
.setTrackingEnabled(false)
.setLandmarkType(FaceDetector.ALL_CLASSIFICATIONS)
.setClassificationType(FaceDetector.ALL_CLASSIFICATIONS)
.build();
initViews();
}
private void initViews() {
imageView = (ImageView) findViewById(R.id.imageView);
imgTakePicture = (ImageView) findViewById(R.id.imgTakePic);
btnProcessNext = (Button) findViewById(R.id.btnProcessNext);
btnTakePicture = (Button) findViewById(R.id.btnTakePicture);
txtSampleDesc = (TextView) findViewById(R.id.txtSampleDescription);
txtTakenPicDesc = (TextView) findViewById(R.id.txtTakePicture);
processImage(imageArray[currentIndex]);
currentIndex++;
btnProcessNext.setOnClickListener(this);
btnTakePicture.setOnClickListener(this);
imgTakePicture.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btnProcessNext:
imageView.setImageResource(imageArray[currentIndex]);
processImage(imageArray[currentIndex]);
if (currentIndex == imageArray.length - 1)
currentIndex = 0;
else
currentIndex++;
break;
case R.id.btnTakePicture:
ActivityCompat.requestPermissions(MainActivity.this, new
String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_WRITE_PERMISSION);
break;
case R.id.imgTakePic:
ActivityCompat.requestPermissions(MainActivity.this, new
String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_WRITE_PERMISSION);
break;
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode) {
case REQUEST_WRITE_PERMISSION:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
startCamera();
} else {
Toast.makeText(getApplicationContext(), "Permission Denied!", Toast.LENGTH_SHORT).show();
}
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == CAMERA_REQUEST && resultCode == RESULT_OK) {
launchMediaScanIntent();
try {
processCameraPicture();
} catch (Exception e) {
Toast.makeText(getApplicationContext(), "Failed to load Image", Toast.LENGTH_SHORT).show();
}
}
}
private void launchMediaScanIntent() {
Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
mediaScanIntent.setData(imageUri);
this.sendBroadcast(mediaScanIntent);
}
private void startCamera() {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
File photo = new File(Environment.getExternalStorageDirectory(), "photo.jpg");
imageUri = Uri.fromFile(photo);
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
startActivityForResult(intent, CAMERA_REQUEST);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
if (imageUri != null) {
outState.putParcelable(SAVED_INSTANCE_BITMAP, editedBitmap);
outState.putString(SAVED_INSTANCE_URI, imageUri.toString());
}
super.onSaveInstanceState(outState);
}
private void processImage(int image) {
Bitmap bitmap = decodeBitmapImage(image);
if (detector.isOperational() && bitmap != null) {
editedBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap
.getHeight(), bitmap.getConfig());
float scale = getResources().getDisplayMetrics().density;
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.GREEN);
paint.setTextSize((int) (16 * scale));
paint.setShadowLayer(1f, 0f, 1f, Color.WHITE);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(6f);
Canvas canvas = new Canvas(editedBitmap);
canvas.drawBitmap(bitmap, 0, 0, paint);
Frame frame = new Frame.Builder().setBitmap(editedBitmap).build();
SparseArray<Face> faces = detector.detect(frame);
txtSampleDesc.setText(null);
for (int index = 0; index < faces.size(); ++index) {
Face face = faces.valueAt(index);
canvas.drawRect(
face.getPosition().x,
face.getPosition().y,
face.getPosition().x + face.getWidth(),
face.getPosition().y + face.getHeight(), paint);
canvas.drawText("Face " + (index + 1), face.getPosition().x + face.getWidth(), face.getPosition().y + face.getHeight(), paint);
txtSampleDesc.setText(txtSampleDesc.getText() + "FACE " + (index + 1) + "\\n");
txtSampleDesc.setText(txtSampleDesc.getText() + "Smile probability:" + " " + face.getIsSmilingProbability() + "\\n");
txtSampleDesc.setText(txtSampleDesc.getText() + "Left Eye Is Open Probability: " + " " + face.getIsLeftEyeOpenProbability() + "\\n");
txtSampleDesc.setText(txtSampleDesc.getText() + "Right Eye Is Open Probability: " + " " + face.getIsRightEyeOpenProbability() + "\\n\\n");
for (Landmark landmark : face.getLandmarks()) {
int cx = (int) (landmark.getPosition().x);
int cy = (int) (landmark.getPosition().y);
canvas.drawCircle(cx, cy, 8, paint);
}
}
if (faces.size() == 0) {
txtSampleDesc.setText("Scan Failed: Found nothing to scan");
} else {
imageView.setImageBitmap(editedBitmap);
txtSampleDesc.setText(txtSampleDesc.getText() + "No of Faces Detected: " + " " + String.valueOf(faces.size()));
}
} else {
txtSampleDesc.setText("Could not set up the detector!");
}
}
private Bitmap decodeBitmapImage(int image) {
int targetW = 300;
int targetH = 300;
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
bmOptions.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), image,
bmOptions);
int photoW = bmOptions.outWidth;
int photoH = bmOptions.outHeight;
int scaleFactor = Math.min(photoW / targetW, photoH / targetH);
bmOptions.inJustDecodeBounds = false;
bmOptions.inSampleSize = scaleFactor;
return BitmapFactory.decodeResource(getResources(), image,
bmOptions);
}
private void processCameraPicture() throws Exception {
Bitmap bitmap = decodeBitmapUri(this, imageUri);
if (detector.isOperational() && bitmap != null) {
editedBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap
.getHeight(), bitmap.getConfig());
float scale = getResources().getDisplayMetrics().density;
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.GREEN);
paint.setTextSize((int) (16 * scale));
paint.setShadowLayer(1f, 0f, 1f, Color.WHITE);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(6f);
Canvas canvas = new Canvas(editedBitmap);
canvas.drawBitmap(bitmap, 0, 0, paint);
Frame frame = new Frame.Builder().setBitmap(editedBitmap).build();
SparseArray<Face> faces = detector.detect(frame);
txtTakenPicDesc.setText(null);
for (int index = 0; index < faces.size(); ++index) {
Face face = faces.valueAt(index);
canvas.drawRect(
face.getPosition().x,
face.getPosition().y,
face.getPosition().x + face.getWidth(),
face.getPosition().y + face.getHeight(), paint);
canvas.drawText("Face " + (index + 1), face.getPosition().x + face.getWidth(), face.getPosition().y + face.getHeight(), paint);
txtTakenPicDesc.setText("FACE " + (index + 1) + "\\n");
txtTakenPicDesc.setText(txtTakenPicDesc.getText() + "Smile probability:" + " " + face.getIsSmilingProbability() + "\\n");
txtTakenPicDesc.setText(txtTakenPicDesc.getText() + "Left Eye Is Open Probability: " + " " + face.getIsLeftEyeOpenProbability() + "\\n");
txtTakenPicDesc.setText(txtTakenPicDesc.getText() + "Right Eye Is Open Probability: " + " " + face.getIsRightEyeOpenProbability() + "\\n\\n");
for (Landmark landmark : face.getLandmarks()) {
int cx = (int) (landmark.getPosition().x);
int cy = (int) (landmark.getPosition().y);
canvas.drawCircle(cx, cy, 8, paint);
}
}
if (faces.size() == 0) {
txtTakenPicDesc.setText("Scan Failed: Found nothing to scan");
} else {
imgTakePicture.setImageBitmap(editedBitmap);
txtTakenPicDesc.setText(txtTakenPicDesc.getText() + "No of Faces Detected: " + " " + String.valueOf(faces.size()));
}
} else {
txtTakenPicDesc.setText("Could not set up the detector!");
}
}
private Bitmap decodeBitmapUri(Context ctx, Uri uri) throws FileNotFoundException {
int targetW = 300;
int targetH = 300;
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
bmOptions.inJustDecodeBounds = true;
BitmapFactory.decodeStream(ctx.getContentResolver().openInputStream(uri), null, bmOptions);
int photoW = bmOptions.outWidth;
int photoH = bmOptions.outHeight;
int scaleFactor = Math.min(photoW / targetW, photoH / targetH);
bmOptions.inJustDecodeBounds = false;
bmOptions.inSampleSize = scaleFactor;
return BitmapFactory.decodeStream(ctx.getContentResolver()
.openInputStream(uri), null, bmOptions);
}
@Override
protected void onDestroy() {
super.onDestroy();
detector.release();
}
}
Một số kết luận rút ra từ đoạn mã trên:
imageArraychứa các hình ảnh mẫu sẽ được quét để phát hiện khuôn mặt khi nhấn nút “PROCESS NEXT”.- Đối tượng
detectorđược khởi tạo bằng đoạn mã sau:
FaceDetector detector = new FaceDetector.Builder(getContext())
.setTrackingEnabled(false)
.setLandmarkType(FaceDetector.ALL_LANDMARKS)
.setMode(FaceDetector.FAST_MODE)
.build();
- Các điểm đặc trưng làm tăng thời gian xử lý, do đó cần được thiết lập một cách tường minh.
FaceDetectorcó thể được đặt ở chế độFAST_MODEhoặcACCURATE_MODEtùy theo yêu cầu. Trong đoạn mã trên, chúng ta đặttrackinglàfalsevì đang xử lý ảnh tĩnh. Trong trường hợp xử lý video, có thể đặt làtrueđể phát hiện khuôn mặt theo thời gian thực. - Các phương thức
processImage()vàprocessCameraPicture()chứa mã xử lý để phát hiện khuôn mặt và vẽ hình chữ nhật bao quanh khuôn mặt đó. detector.isOperational()được sử dụng để kiểm tra xem thư viện Google Play Services trên thiết bị có hỗ trợ Vision API hay không (nếu không, Google Play sẽ tự động tải xuống các thư viện gốc cần thiết để cung cấp hỗ trợ).- Đoạn mã thực hiện chức năng phát hiện khuôn mặt là:
Frame frame = new Frame.Builder().setBitmap(editedBitmap).build();
SparseArray faces = detector.detect(frame);
- Sau khi phát hiện, ta sẽ duyệt qua mảng
facesđể lấy vị trí và các thuộc tính của từng khuôn mặt. - Các thuộc tính của từng khuôn mặt sẽ được hiển thị trong
TextViewphía dưới nút. - Quy trình hoạt động tương tự khi ảnh được chụp từ camera, ngoại trừ việc cần yêu cầu quyền truy cập camera tại thời điểm chạy, và lưu lại
uri,bitmapđược trả về từ ứng dụng camera. - Kết quả đầu ra của ứng dụng khi chạy được thể hiện bên dưới.

Hãy thử chụp ảnh một con chó và bạn sẽ thấy Vision API không phát hiện khuôn mặt của nó (API chỉ phát hiện khuôn mặt người).