Phân đoạn ảnh tự chụp chân dung bằng Bộ công cụ học máy trên Android

Bộ công cụ học máy cung cấp một SDK được tối ưu hoá để phân đoạn ảnh tự chụp chân dung.

Các thành phần của Selfie Segmenter được liên kết tĩnh với ứng dụng của bạn tại thời điểm tạo bản dựng. Điều này sẽ làm tăng kích thước tải xuống của ứng dụng thêm khoảng 4,5 MB và độ trễ API có thể dao động từ 25 mili giây đến 65 mili giây, tuỳ thuộc vào kích thước hình ảnh đầu vào, theo số liệu đo được trên Pixel 4.

Dùng thử

Trước khi bắt đầu

  1. Trong tệp build.gradle cấp dự án, hãy nhớ thêm kho lưu trữ Maven của Google vào cả hai mục buildscriptallprojects.
  2. Thêm các phần phụ thuộc cho thư viện ML Kit Android vào tệp gradle cấp ứng dụng của mô-đun, thường là app/build.gradle:
dependencies {
  implementation 'com.google.mlkit:segmentation-selfie:16.0.0-beta6'
}

1. Tạo một thực thể của Segmenter

Các lựa chọn về công cụ phân đoạn

Để phân đoạn trên hình ảnh, trước tiên, hãy tạo một phiên bản của Segmenter bằng cách chỉ định các lựa chọn sau.

Chế độ phát hiện

Segmenter hoạt động ở 2 chế độ. Hãy nhớ chọn mẫu phù hợp với trường hợp sử dụng của bạn.

STREAM_MODE (default)

Chế độ này được thiết kế để truyền trực tuyến các khung hình từ video hoặc camera. Ở chế độ này, bộ phân đoạn sẽ tận dụng kết quả từ các khung hình trước đó để trả về kết quả phân đoạn mượt mà hơn.

SINGLE_IMAGE_MODE

Chế độ này được thiết kế cho những hình ảnh riêng lẻ không liên quan đến nhau. Ở chế độ này, bộ phân đoạn sẽ xử lý từng hình ảnh một cách độc lập mà không làm mượt các khung hình.

Bật mặt nạ kích thước thô

Yêu cầu bộ phân đoạn trả về mặt nạ kích thước thô khớp với kích thước đầu ra của mô hình.

Kích thước mặt nạ thô (ví dụ: 256x256) thường nhỏ hơn kích thước hình ảnh đầu vào. Vui lòng gọi SegmentationMask#getWidth()SegmentationMask#getHeight() để lấy kích thước mặt nạ khi bật lựa chọn này.

Nếu không chỉ định lựa chọn này, bộ phân đoạn sẽ điều chỉnh tỷ lệ mặt nạ thô để khớp với kích thước hình ảnh đầu vào. Hãy cân nhắc sử dụng lựa chọn này nếu bạn muốn áp dụng logic điều chỉnh tỷ lệ tuỳ chỉnh hoặc không cần điều chỉnh tỷ lệ cho trường hợp sử dụng của mình.

Chỉ định các lựa chọn cho bộ phân đoạn:

Kotlin

val options =
        SelfieSegmenterOptions.Builder()
            .setDetectorMode(SelfieSegmenterOptions.STREAM_MODE)
            .enableRawSizeMask()
            .build()

Java

SelfieSegmenterOptions options =
        new SelfieSegmenterOptions.Builder()
            .setDetectorMode(SelfieSegmenterOptions.STREAM_MODE)
            .enableRawSizeMask()
            .build();

Tạo một thực thể của Segmenter. Truyền các lựa chọn mà bạn đã chỉ định:

Kotlin

val segmenter = Segmentation.getClient(options)

Java

Segmenter segmenter = Segmentation.getClient(options);

2. Chuẩn bị hình ảnh đầu vào

Để phân đoạn trên hình ảnh, hãy tạo một đối tượng InputImage từ Bitmap, media.Image, ByteBuffer, mảng byte hoặc một tệp trên thiết bị.

Bạn có thể tạo một đối tượng InputImage từ nhiều nguồn, mỗi nguồn được giải thích bên dưới.

Sử dụng media.Image

Để tạo một đối tượng InputImage từ một đối tượng media.Image, chẳng hạn như khi bạn chụp ảnh bằng camera của thiết bị, hãy truyền đối tượng media.Image và hướng xoay của hình ảnh đến InputImage.fromMediaImage().

Nếu bạn sử dụng thư viện CameraX, các lớp OnImageCapturedListenerImageAnalysis.Analyzer sẽ tính toán giá trị xoay cho bạn.

Kotlin

private class YourImageAnalyzer : ImageAnalysis.Analyzer {

    override fun analyze(imageProxy: ImageProxy) {
        val mediaImage = imageProxy.image
        if (mediaImage != null) {
            val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
            // Pass image to an ML Kit Vision API
            // ...
        }
    }
}

Java

private class YourAnalyzer implements ImageAnalysis.Analyzer {

    @Override
    public void analyze(ImageProxy imageProxy) {
        Image mediaImage = imageProxy.getImage();
        if (mediaImage != null) {
          InputImage image =
                InputImage.fromMediaImage(mediaImage, imageProxy.getImageInfo().getRotationDegrees());
          // Pass image to an ML Kit Vision API
          // ...
        }
    }
}

Nếu không dùng thư viện máy ảnh cho bạn biết độ xoay của hình ảnh, bạn có thể tính độ xoay đó từ độ xoay của thiết bị và hướng của cảm biến camera trong thiết bị:

Kotlin

private val ORIENTATIONS = SparseIntArray()

init {
    ORIENTATIONS.append(Surface.ROTATION_0, 0)
    ORIENTATIONS.append(Surface.ROTATION_90, 90)
    ORIENTATIONS.append(Surface.ROTATION_180, 180)
    ORIENTATIONS.append(Surface.ROTATION_270, 270)
}

/**
 * Get the angle by which an image must be rotated given the device's current
 * orientation.
 */
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Throws(CameraAccessException::class)
private fun getRotationCompensation(cameraId: String, activity: Activity, isFrontFacing: Boolean): Int {
    // Get the device's current rotation relative to its "native" orientation.
    // Then, from the ORIENTATIONS table, look up the angle the image must be
    // rotated to compensate for the device's rotation.
    val deviceRotation = activity.windowManager.defaultDisplay.rotation
    var rotationCompensation = ORIENTATIONS.get(deviceRotation)

    // Get the device's sensor orientation.
    val cameraManager = activity.getSystemService(CAMERA_SERVICE) as CameraManager
    val sensorOrientation = cameraManager
            .getCameraCharacteristics(cameraId)
            .get(CameraCharacteristics.SENSOR_ORIENTATION)!!

    if (isFrontFacing) {
        rotationCompensation = (sensorOrientation + rotationCompensation) % 360
    } else { // back-facing
        rotationCompensation = (sensorOrientation - rotationCompensation + 360) % 360
    }
    return rotationCompensation
}

Java

private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
static {
    ORIENTATIONS.append(Surface.ROTATION_0, 0);
    ORIENTATIONS.append(Surface.ROTATION_90, 90);
    ORIENTATIONS.append(Surface.ROTATION_180, 180);
    ORIENTATIONS.append(Surface.ROTATION_270, 270);
}

/**
 * Get the angle by which an image must be rotated given the device's current
 * orientation.
 */
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private int getRotationCompensation(String cameraId, Activity activity, boolean isFrontFacing)
        throws CameraAccessException {
    // Get the device's current rotation relative to its "native" orientation.
    // Then, from the ORIENTATIONS table, look up the angle the image must be
    // rotated to compensate for the device's rotation.
    int deviceRotation = activity.getWindowManager().getDefaultDisplay().getRotation();
    int rotationCompensation = ORIENTATIONS.get(deviceRotation);

    // Get the device's sensor orientation.
    CameraManager cameraManager = (CameraManager) activity.getSystemService(CAMERA_SERVICE);
    int sensorOrientation = cameraManager
            .getCameraCharacteristics(cameraId)
            .get(CameraCharacteristics.SENSOR_ORIENTATION);

    if (isFrontFacing) {
        rotationCompensation = (sensorOrientation + rotationCompensation) % 360;
    } else { // back-facing
        rotationCompensation = (sensorOrientation - rotationCompensation + 360) % 360;
    }
    return rotationCompensation;
}

Sau đó, hãy truyền đối tượng media.Image và giá trị độ xoay đến InputImage.fromMediaImage():

Kotlin

val image = InputImage.fromMediaImage(mediaImage, rotation)

Java

InputImage image = InputImage.fromMediaImage(mediaImage, rotation);

Sử dụng URI tệp

Để tạo một đối tượng InputImage từ một URI tệp, hãy truyền ngữ cảnh ứng dụng và URI tệp đến InputImage.fromFilePath(). Điều này hữu ích khi bạn dùng ý định ACTION_GET_CONTENT để nhắc người dùng chọn một hình ảnh trong ứng dụng thư viện của họ.

Kotlin

val image: InputImage
try {
    image = InputImage.fromFilePath(context, uri)
} catch (e: IOException) {
    e.printStackTrace()
}

Java

InputImage image;
try {
    image = InputImage.fromFilePath(context, uri);
} catch (IOException e) {
    e.printStackTrace();
}

Sử dụng ByteBuffer hoặc ByteArray

Để tạo một đối tượng InputImage từ ByteBuffer hoặc ByteArray, trước tiên, hãy tính độ xoay của hình ảnh như mô tả trước đó cho dữ liệu đầu vào media.Image. Sau đó, hãy tạo đối tượng InputImage bằng vùng đệm hoặc mảng, cùng với chiều cao, chiều rộng, định dạng mã hoá màu và độ xoay của hình ảnh:

Kotlin

val image = InputImage.fromByteBuffer(
        byteBuffer,
        /* image width */ 480,
        /* image height */ 360,
        rotationDegrees,
        InputImage.IMAGE_FORMAT_NV21 // or IMAGE_FORMAT_YV12
)
// Or:
val image = InputImage.fromByteArray(
        byteArray,
        /* image width */ 480,
        /* image height */ 360,
        rotationDegrees,
        InputImage.IMAGE_FORMAT_NV21 // or IMAGE_FORMAT_YV12
)

Java

InputImage image = InputImage.fromByteBuffer(byteBuffer,
        /* image width */ 480,
        /* image height */ 360,
        rotationDegrees,
        InputImage.IMAGE_FORMAT_NV21 // or IMAGE_FORMAT_YV12
);
// Or:
InputImage image = InputImage.fromByteArray(
        byteArray,
        /* image width */480,
        /* image height */360,
        rotation,
        InputImage.IMAGE_FORMAT_NV21 // or IMAGE_FORMAT_YV12
);

Sử dụng Bitmap

Để tạo đối tượng InputImage từ đối tượng Bitmap, hãy khai báo như sau:

Kotlin

val image = InputImage.fromBitmap(bitmap, 0)

Java

InputImage image = InputImage.fromBitmap(bitmap, rotationDegree);

Hình ảnh được biểu thị bằng một đối tượng Bitmap cùng với độ xoay.

3. Xử lý hình ảnh

Truyền đối tượng InputImage đã chuẩn bị cho phương thức process của Segmenter.

Kotlin

Task<SegmentationMask> result = segmenter.process(image)
       .addOnSuccessListener { results ->
           // Task completed successfully
           // ...
       }
       .addOnFailureListener { e ->
           // Task failed with an exception
           // ...
       }

Java

Task<SegmentationMask> result =
        segmenter.process(image)
                .addOnSuccessListener(
                        new OnSuccessListener<SegmentationMask>() {
                            @Override
                            public void onSuccess(SegmentationMask mask) {
                                // Task completed successfully
                                // ...
                            }
                        })
                .addOnFailureListener(
                        new OnFailureListener() {
                            @Override
                            public void onFailure(@NonNull Exception e) {
                                // Task failed with an exception
                                // ...
                            }
                        });

4. Nhận kết quả phân đoạn

Bạn có thể nhận được kết quả phân đoạn như sau:

Kotlin

val mask = segmentationMask.getBuffer()
val maskWidth = segmentationMask.getWidth()
val maskHeight = segmentationMask.getHeight()

for (val y = 0; y < maskHeight; y++) {
  for (val x = 0; x < maskWidth; x++) {
    // Gets the confidence of the (x,y) pixel in the mask being in the foreground.
    val foregroundConfidence = mask.getFloat()
  }
}

Java

ByteBuffer mask = segmentationMask.getBuffer();
int maskWidth = segmentationMask.getWidth();
int maskHeight = segmentationMask.getHeight();

for (int y = 0; y < maskHeight; y++) {
  for (int x = 0; x < maskWidth; x++) {
    // Gets the confidence of the (x,y) pixel in the mask being in the foreground.
    float foregroundConfidence = mask.getFloat();
  }
}

Để xem ví dụ đầy đủ về cách sử dụng kết quả phân đoạn, vui lòng xem mẫu bắt đầu nhanh của ML Kit.

Mẹo cải thiện hiệu suất

Chất lượng của kết quả phụ thuộc vào chất lượng của hình ảnh đầu vào:

  • Để ML Kit nhận được kết quả phân đoạn chính xác, hình ảnh phải có kích thước tối thiểu là 256x256 pixel.
  • Hình ảnh không rõ nét cũng có thể ảnh hưởng đến độ chính xác. Nếu bạn không nhận được kết quả chấp nhận được, hãy yêu cầu người dùng chụp lại hình ảnh.

Nếu bạn muốn sử dụng tính năng phân đoạn trong một ứng dụng theo thời gian thực, hãy làm theo các nguyên tắc sau để đạt được tốc độ khung hình tốt nhất:

  • Sử dụng STREAM_MODE.
  • Hãy cân nhắc chụp ảnh ở độ phân giải thấp hơn. Tuy nhiên, bạn cũng cần lưu ý các yêu cầu về kích thước hình ảnh của API này.
  • Cân nhắc việc bật lựa chọn mặt nạ kích thước thô và kết hợp tất cả logic điều chỉnh tỷ lệ với nhau. Ví dụ: thay vì để API điều chỉnh tỷ lệ mặt nạ cho phù hợp với kích thước hình ảnh đầu vào trước, sau đó bạn điều chỉnh tỷ lệ lại cho phù hợp với kích thước Khung hiển thị để hiển thị, chỉ cần yêu cầu mặt nạ kích thước thô và kết hợp hai bước này thành một.
  • Nếu bạn dùng API Camera hoặc camera2, hãy điều chỉnh tốc độ gọi đến trình phát hiện. Nếu có khung hình video mới trong khi bộ phát hiện đang chạy, hãy thả khung hình đó. Hãy xem lớp VisionProcessorBase trong ứng dụng mẫu bắt đầu nhanh để biết ví dụ.
  • Nếu bạn sử dụng API CameraX, hãy đảm bảo rằng chiến lược áp suất ngược được đặt thành giá trị mặc định ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST. Điều này đảm bảo rằng chỉ có một hình ảnh được phân phối để phân tích tại một thời điểm. Nếu có nhiều hình ảnh được tạo ra khi trình phân tích đang bận, thì những hình ảnh đó sẽ tự động bị loại bỏ và không được đưa vào hàng đợi để phân phối. Sau khi hình ảnh đang được phân tích bị đóng bằng cách gọi ImageProxy.close(), hình ảnh mới nhất tiếp theo sẽ được phân phối.
  • Nếu bạn dùng đầu ra của bộ nhận diện để phủ đồ hoạ lên hình ảnh đầu vào, trước tiên, hãy lấy kết quả từ ML Kit, sau đó kết xuất hình ảnh và phủ trong một bước. Thao tác này chỉ kết xuất vào bề mặt hiển thị một lần cho mỗi khung hình đầu vào. Hãy xem các lớp CameraSourcePreview GraphicOverlay trong ứng dụng mẫu bắt đầu nhanh để biết ví dụ.
  • Nếu bạn sử dụng API Camera2, hãy chụp ảnh ở định dạng ImageFormat.YUV_420_888. Nếu bạn sử dụng Camera API cũ, hãy chụp ảnh ở định dạng ImageFormat.NV21.