CyStack logo
  • Sản phẩm & Dịch vụ
  • Giải pháp
  • Bảng giá
  • Công ty
  • Tài liệu
Vi

vi

Trang chủHướng dẫnXây dựng chức năng nhận diện khuôn mặt trên Android
Android

Xây dựng chức năng nhận diện khuôn mặt trên Android

CyStack blog 8 phút để đọc
CyStack blog18/08/2025
Locker Avatar

Chris Pham

Technical Writer

Locker logo social
Reading Time: 8 minutes

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.

nhận diện khuôn mặt trên Android

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()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

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:

  • imageArray chứ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. FaceDetector có thể được đặt ở chế độ FAST_MODE hoặc ACCURATE_MODE tùy theo yêu cầu. Trong đoạn mã trên, chúng ta đặt trackingfalse vì đ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()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 TextView phí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.

Nhận diện khuôn mặt trên Android

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).

0 Bình luận

Đăng nhập để thảo luận

Chuyên mục Hướng dẫn

Tổng hợp các bài viết hướng dẫn, nghiên cứu và phân tích chi tiết về kỹ thuật, các xu hướng công nghệ mới nhất dành cho lập trình viên.

Đăng ký nhận bản tin của chúng tôi

Hãy trở thành người nhận được các nội dung hữu ích của CyStack sớm nhất

Xem chính sách của chúng tôi Chính sách bảo mật.

Đăng ký nhận Newsletter

Nhận các nội dung hữu ích mới nhất