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.

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, như được đo trên Pixel 4.

Dùng thử

  • Hãy thử nghiệm với ứng dụng mẫu để xem ví dụ về cách sử dụng API này.

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ả mục buildscriptallprojects.
  2. Thêm các phần phụ thuộc cho thư viện Android của Bộ công cụ học máy 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

Tuỳ chọn trình phân đoạn

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

Chế độ phát hiện

Segmenter hoạt động ở hai chế độ. Hãy nhớ chọn một 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 khung hình từ video hoặc máy ảnh. Ở chế độ này, trình phân đoạn sẽ tận dụng kết quả từ các khung 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 các hình ảnh đơn lẻ không liên quan. Ở chế độ này, trình phân đoạn sẽ xử lý từng hình ảnh một cách độc lập, 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 trình 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 tuỳ chọn này.

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

Chỉ định các tuỳ chọn của trình phân đoạn:

KotlinJava
val options =
        SelfieSegmenterOptions.Builder()
            .setDetectorMode(SelfieSegmenterOptions.STREAM_MODE)
            .enableRawSizeMask()
            .build()
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 tuỳ chọn bạn đã chỉ định:

KotlinJava
val segmenter = Segmentation.getClient(options)
Segmenter segmenter = Segmentation.getClient(options);

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

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

Bạn có thể tạo đố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 đối tượng InputImage từ đối tượng media.Image, chẳng hạn như khi bạn chụp ảnh từ máy ảnh của thiết bị, hãy truyền đối tượng media.Image và độ 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.

KotlinJava
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
            // ...
        }
    }
}
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 sử dụng thư viện máy ảnh cung cấp cho bạn độ xoay của hình ảnh, bạn có thể tính toán độ xoay đó từ độ xoay của thiết bị và hướng của cảm biến máy ảnh trong thiết bị:

KotlinJava
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
}
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 đó, truyền đối tượng media.Image và giá trị độ xoay vào InputImage.fromMediaImage():

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

Sử dụng URI tệp

Để tạo đối tượng InputImage 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 sử 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.

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

Sử dụng ByteBuffer hoặc ByteArray

Để tạo đối tượng InputImage từ ByteBuffer hoặc ByteArray, trước tiên, hãy tính độ xoay 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 bộ nhớ đệ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:

KotlinJava
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
)
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:

KotlinJava
val image = InputImage.fromBitmap(bitmap, 0)
InputImage image = InputImage.fromBitmap(bitmap, rotationDegree);

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

3. Xử lý hình ảnh

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

KotlinJava
Task<SegmentationMask> result = segmenter.process(image)
       .addOnSuccessListener { results ->
           // Task completed successfully
           // ...
       }
       .addOnFailureListener { e ->
           // Task failed with an exception
           // ...
       }
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:

KotlinJava
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()
  }
}
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();
  }
}

Để biết 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 Bộ công cụ học máy.

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:

  • Để Bộ công cụ học máy 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.
  • Độ nét của hình ảnh 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.
  • Cân nhắc chụp ảnh ở độ phân giải thấp hơn. Tuy nhiên, hãy lưu ý đến các yêu cầu về kích thước hình ảnh của API này.
  • Cân nhắc bật tuỳ 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í dụ: thay vì để API điều chỉnh lại 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 lại tỷ lệ để phù hợp với kích thước Chế độ xem hiển thị, bạn 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 sử dụng API Camera hoặc camera2, hãy điều tiết các lệnh gọi đến trình phát hiện. Nếu có khung hình video mới trong khi trình 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 nhớ đặt chiến lược áp lực ngược thành giá trị mặc định là ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST. Điều này đảm bảo rằng mỗi lần chỉ có một hình ảnh được phân phối để phân tích. Nếu có nhiều hình ảnh được tạo khi trình phân tích đang bận, thì các 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 được đó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 sử dụng kết quả của trình phát hiện để phủ hình ảnh đồ hoạ lên hình ảnh đầu vào, trước tiên, hãy lấy kết quả từ Bộ công cụ học máy, sau đó kết xuất hình ảnh và phủ trong một bước. Việc này chỉ kết xuất một lần cho mỗi khung đầu vào trên bề mặt hiển thị. 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 API Máy ảnh cũ, hãy chụp ảnh ở định dạng ImageFormat.NV21.