تقسیم‌بندی سلفی با ML Kit در اندروید

کیت ML یک SDK بهینه شده برای تقسیم‌بندی سلفی ارائه می‌دهد.

فایل‌های Selfie Segmenter در زمان ساخت به صورت استاتیک به برنامه شما متصل می‌شوند. این کار حجم دانلود برنامه شما را حدود ۴.۵ مگابایت افزایش می‌دهد و تأخیر API می‌تواند بسته به اندازه تصویر ورودی، از ۲۵ میلی‌ثانیه تا ۶۵ میلی‌ثانیه متغیر باشد، همانطور که در Pixel 4 اندازه‌گیری شده است.

امتحانش کن.

قبل از اینکه شروع کنی

  1. در فایل build.gradle سطح پروژه خود، مطمئن شوید که مخزن Maven گوگل را هم در بخش‌های buildscript و هم allprojects خود وارد کرده‌اید.
  2. وابستگی‌های کتابخانه‌های اندروید ML Kit را به فایل gradle سطح app ماژول خود که معمولاً app/build.gradle است، اضافه کنید:
dependencies {
  implementation 'com.google.mlkit:segmentation-selfie:16.0.0-beta6'
}

۱. یک نمونه از Segmenter ایجاد کنید

گزینه‌های تقسیم‌بندی

برای انجام قطعه‌بندی روی یک تصویر، ابتدا با مشخص کردن گزینه‌های زیر، یک نمونه از Segmenter ایجاد کنید.

حالت آشکارساز

Segmenter در دو حالت کار می‌کند. مطمئن شوید که حالتی را انتخاب می‌کنید که با مورد استفاده شما مطابقت دارد.

STREAM_MODE (default)

این حالت برای پخش فریم‌ها از ویدیو یا دوربین طراحی شده است. در این حالت، قطعه‌ساز از نتایج فریم‌های قبلی برای بازگرداندن نتایج قطعه‌بندی روان‌تر استفاده می‌کند.

SINGLE_IMAGE_MODE

این حالت برای تصاویر تکی که به هم مرتبط نیستند طراحی شده است. در این حالت، قطعه‌ساز هر تصویر را به‌طور مستقل و بدون هموارسازی فریم‌ها پردازش می‌کند.

فعال کردن ماسک اندازه خام

از قطعه‌ساز می‌خواهد که ماسک اندازه خام را که با اندازه خروجی مدل مطابقت دارد، برگرداند.

اندازه ماسک خام (مثلاً 256x256) معمولاً کوچکتر از اندازه تصویر ورودی است. لطفاً هنگام فعال کردن این گزینه، برای دریافت اندازه ماسک SegmentationMask#getWidth() و SegmentationMask#getHeight() را فراخوانی کنید.

بدون مشخص کردن این گزینه، قطعه‌ساز، ماسک خام را برای مطابقت با اندازه تصویر ورودی، تغییر مقیاس می‌دهد. اگر می‌خواهید منطق تغییر مقیاس سفارشی اعمال کنید یا تغییر مقیاس برای مورد استفاده شما لازم نیست، استفاده از این گزینه را در نظر بگیرید.

گزینه‌های تقسیم‌بندی را مشخص کنید:

کاتلین

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

جاوا

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

یک نمونه از Segmenter ایجاد کنید. گزینه‌هایی که مشخص کرده‌اید را به آن بدهید:

کاتلین

val segmenter = Segmentation.getClient(options)

جاوا

Segmenter segmenter = Segmentation.getClient(options);

۲. تصویر ورودی را آماده کنید

برای انجام قطعه‌بندی روی یک تصویر، یک شیء InputImage از Bitmap ، media.Image ، ByteBuffer ، آرایه بایت یا یک فایل روی دستگاه ایجاد کنید.

شما می‌توانید یک شیء InputImage را از منابع مختلفی ایجاد کنید که هر کدام در زیر توضیح داده شده‌اند.

استفاده از یک media.Image

برای ایجاد یک شیء InputImage از یک شیء media.Image ، مانند زمانی که از دوربین یک دستگاه تصویر می‌گیرید، شیء media.Image و چرخش تصویر را به InputImage.fromMediaImage() ارسال کنید.

اگر از کتابخانه CameraX استفاده می‌کنید، کلاس‌های OnImageCapturedListener و ImageAnalysis.Analyzer مقدار چرخش را برای شما محاسبه می‌کنند.

کاتلین

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
          // ...
        }
    }
}

اگر از کتابخانه دوربینی که درجه چرخش تصویر را به شما بدهد استفاده نمی‌کنید، می‌توانید آن را از درجه چرخش دستگاه و جهت سنسور دوربین در دستگاه محاسبه کنید:

کاتلین

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;
}

سپس، شیء media.Image و مقدار درجه چرخش را به InputImage.fromMediaImage() ارسال کنید:

کاتلین

val image = InputImage.fromMediaImage(mediaImage, rotation)

Java

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

استفاده از یک URI فایل

برای ایجاد یک شیء InputImage از یک URI فایل، متن برنامه و URI فایل را به InputImage.fromFilePath() ارسال کنید. این زمانی مفید است که از یک ACTION_GET_CONTENT برای وادار کردن کاربر به انتخاب یک تصویر از برنامه گالری خود استفاده می‌کنید.

کاتلین

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

استفاده از ByteBuffer یا ByteArray

برای ایجاد یک شیء InputImage از یک ByteBuffer یا یک ByteArray ، ابتدا درجه چرخش تصویر را همانطور که قبلاً برای ورودی media.Image توضیح داده شد، محاسبه کنید. سپس، شیء InputImage را با بافر یا آرایه، به همراه ارتفاع، عرض، فرمت کدگذاری رنگ و درجه چرخش تصویر ایجاد کنید:

کاتلین

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

استفاده از Bitmap

برای ایجاد یک شیء InputImage از یک شیء Bitmap ، تعریف زیر را انجام دهید:

کاتلین

val image = InputImage.fromBitmap(bitmap, 0)

Java

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

تصویر توسط یک شیء Bitmap به همراه درجه چرخش نمایش داده می‌شود.

۳. تصویر را پردازش کنید

شیء InputImage آماده‌شده را به متد process از Segmenter ارسال کنید.

کاتلین

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
                                // ...
                            }
                        });

۴. نتیجه تقسیم‌بندی را دریافت کنید

شما می‌توانید نتیجه‌ی تقسیم‌بندی را به صورت زیر دریافت کنید:

کاتلین

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

برای مشاهده‌ی مثال کاملی از نحوه‌ی استفاده از نتایج تقسیم‌بندی، لطفاً به نمونه‌ی شروع سریع ML Kit مراجعه کنید.

نکاتی برای بهبود عملکرد

کیفیت نتایج شما به کیفیت تصویر ورودی بستگی دارد:

  • برای اینکه ML Kit نتیجه‌ی قطعه‌بندی دقیقی داشته باشد، تصویر باید حداقل ۲۵۶x۲۵۶ پیکسل باشد.
  • فوکوس ضعیف تصویر نیز می‌تواند بر دقت تأثیر بگذارد. اگر نتایج قابل قبولی به دست نیاوردید، از کاربر بخواهید که دوباره تصویر را ثبت کند.

اگر می‌خواهید از قطعه‌بندی در یک برنامه‌ی بلادرنگ استفاده کنید، برای دستیابی به بهترین نرخ فریم، این دستورالعمل‌ها را دنبال کنید:

  • از STREAM_MODE استفاده کنید.
  • ضبط تصاویر با وضوح پایین‌تر را در نظر بگیرید. با این حال، الزامات ابعاد تصویر این API را نیز در نظر داشته باشید.
  • فعال کردن گزینه ماسک با اندازه خام و ترکیب تمام منطق تغییر مقیاس را با هم در نظر بگیرید. برای مثال، به جای اینکه به API اجازه دهید ابتدا ماسک را برای مطابقت با اندازه تصویر ورودی شما تغییر مقیاس دهد و سپس شما دوباره آن را برای مطابقت با اندازه نمایش، تغییر مقیاس دهید، کافیست ماسک با اندازه خام را درخواست کنید و این دو مرحله را در یک مرحله ترکیب کنید.
  • اگر از API Camera یا camera2 استفاده می‌کنید، فراخوانی‌های throttle به آشکارساز انجام می‌شود. اگر در حین اجرای آشکارساز، یک فریم ویدیویی جدید در دسترس قرار گرفت، فریم را حذف کنید. برای مثال، به کلاس VisionProcessorBase در برنامه نمونه شروع سریع مراجعه کنید.
  • اگر از API CameraX استفاده می‌کنید، مطمئن شوید که استراتژی فشار معکوس (backpressure strategy) روی مقدار پیش‌فرض خود ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST تنظیم شده است. این تضمین می‌کند که فقط یک تصویر در هر زمان برای تجزیه و تحلیل تحویل داده می‌شود. اگر تصاویر بیشتری هنگام مشغول بودن تحلیلگر تولید شوند، به طور خودکار حذف می‌شوند و برای تحویل در صف قرار نمی‌گیرند. پس از بسته شدن تصویر در حال تجزیه و تحلیل با فراخوانی ImageProxy.close()، آخرین تصویر بعدی تحویل داده می‌شود.
  • اگر از خروجی آشکارساز برای همپوشانی گرافیک روی تصویر ورودی استفاده می‌کنید، ابتدا نتیجه را از کیت ML دریافت کنید، سپس تصویر و همپوشانی را در یک مرحله رندر کنید. این کار فقط یک بار برای هر فریم ورودی روی سطح نمایشگر رندر می‌شود. برای مثال به کلاس‌های CameraSourcePreview و GraphicOverlay در برنامه نمونه شروع سریع مراجعه کنید.
  • اگر از API دوربین ۲ استفاده می‌کنید، تصاویر را با فرمت ImageFormat.YUV_420_888 ضبط کنید. اگر از API دوربین قدیمی‌تر استفاده می‌کنید، تصاویر را با فرمت ImageFormat.NV21 ضبط کنید.