برچسب‌گذاری تصاویر با مدل سفارشی در Android

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

دو روش برای ادغام برچسب‌گذاری تصویر با مدل‌های سفارشی وجود دارد: ازطریق دسته‌بندی خط لوله به‌عنوان بخشی از برنامه، یا ازطریق استفاده از خط لوله دسته‌بندی‌نشده که به خدمات Google Play وابسته است. اگر خط لوله غیریکپارچه را انتخاب کنید، برنامه شما کوچک‌تر خواهد بود. برای جزئیات، جدول زیر را ببینید.

موجود در بسته‌بندیتفکیک‌شده
نام کتابخانهcom.google.mlkit:image-labeling-customcom.google.android.gms:play-services-mlkit-image-labeling-custom

پیاده‌سازی
خط لوله در زمان ساخت به‌صورت ایستا به برنامه شما پیوند داده می‌شود.خط لوله به‌صورت پویا بااستفاده از «خدمات Google Play» بارگیری می‌شود.
اندازه برنامهحدود ۳٫۸ مگابایت افزایش اندازه.حدود ۲۰۰ کیلوبایت افزایش اندازه.
زمان مقداردهی اولیهخط لوله بلافاصله دردسترس است.ممکن است لازم باشد قبل‌از اولین استفاده، منتظر بمانید تا خط لوله بارگیری شود.
مرحله چرخه حیات میانای برنامه‌سازی کاربردیدردسترس عموم (GA)بتا

دو روش برای یکپارچه کردن مدل سفارشی وجود دارد: مدل را با قرار دادن آن در پوشه دارایی برنامه دسته‌بندی کنید، یا آن را به‌صورت پویا از Firebase بارگیری کنید. جدول زیر این دو گزینه را مقایسه می‌کند.

مدل بسته‌ای مدل میزبانی‌شده
مدل بخشی از APK برنامه‌تان است که اندازه آن را افزایش می‌دهد. این مدل بخشی از APK شما نیست. با بارگذاری در Cloud Storage میزبانی می‌شود. توصیه می‌کنیم از Cloud Storage برای Firebase استفاده کنید.
مدل فوراً دردسترس قرار می‌گیرد، حتی وقتی دستگاه Android آفلاین باشد برنامه‌تان باید حاوی کدی برای بارگیری مدل درصورت درخواست باشد
نیازی به پروژه Firebase نیست به پروژه Firebase نیاز دارد (درصورت استفاده از Cloud Storage for Firebase).
برای به‌روز کردن مدل باید برنامه‌تان را دوباره منتشر کنید به‌روزرسانی‌های مدل را بدون بازانتشار برنامه خود ارسال کنید
نداشتن آزمایش آ/ب داخلی آزمایش آ/ب با پیکربندی از راه دور Firebase

امتحان کنید

قبل‌از شروع

  1. در فایل build.gradle.kts سطح پروژه، حتماً مخزن Maven متعلق به Google را در هر دو بخش buildscript و allprojects اضافه کنید.

  2. وابستگی‌های کتابخانه‌های ML Kit Android را به فایل gradle سطح برنامه واحدتان اضافه کنید، که معمولاً app/build.gradle.kts است. براساس نیازهایتان، یکی از وابستگی‌های زیر را انتخاب کنید:

    برای دسته‌بندی خط لوله با برنامه:

    dependencies {
      // ...
      // Use this dependency to bundle the pipeline with your app
      implementation("com.google.mlkit:image-labeling-custom:17.0.3")
    }
    

    برای استفاده از خط لوله در «خدمات Google Play»:

    dependencies {
      // ...
      // Use this dependency to use the dynamically downloaded pipeline in Google Play services
      implementation("com.google.android.gms:play-services-mlkit-image-labeling-custom:16.0.0-beta5")
    }
    
  3. اگر انتخاب کنید که از خط لوله در «خدمات Google Play» استفاده کنید، می‌توانید برنامه‌تان را پیکربندی کنید تا خط لوله را به‌طور خودکار در دستگاه پس‌از نصب برنامه از «فروشگاه Play» بارگیری کند. برای انجام این کار، اظهارنامه زیر را به فایل AndroidManifest.xml برنامه‌تان اضافه کنید:

    <application ...>
        ...
        <meta-data
            android:name="com.google.mlkit.vision.DEPENDENCIES"
            android:value="custom_ica" />
        <!-- To use multiple downloads: android:value="custom_ica,download2,download3" -->
    </application>
    

    همچنین می‌توانید دردسترس بودن خط لوله را به‌طور صریح بررسی کنید و ازطریق خدمات Google Play ModuleInstallClient API درخواست بارگیری کنید.

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

  4. اگر می‌خواهید مدلی را بااستفاده از «فضای ذخیره‌سازی ابری ویژه Firebase» بارگیری کنید، مطمئن شوید که Firebase را به پروژه Android خود اضافه کرده‌اید، اگر قبلاً این کار را نکرده‌اید. این کار هنگام دسته‌بندی مدل الزامی نیست.

۱. بار کردن مدل

می‌توانید مدل را از منبع بسته‌بندی‌شده محلی یا منبع میزبانی‌شده از دور بار کنید.

پیکربندی منبع مدل محلی

برای دسته‌بندی مدل با برنامه:

  1. فایل مدل (معمولاً با .tflite یا .lite تمام می‌شود) را در پوشه assets/ برنامه کپی کنید. (ممکن است ابتدا لازم باشد پوشه را با کلیک راست روی پوشه app/ و سپس کلیک روی جدید > پوشه > پوشه دارایی‌ها ایجاد کنید.)

  2. شیء LocalModel را ایجاد کنید و مسیر فایل مدل را مشخص کنید:

    Kotlin

    val localModel = LocalModel.Builder()
            .setAssetFilePath("model.tflite")
            // or .setAbsoluteFilePath(absolute path to model file)
            // or .setUri(URI to model file)
            .build()

    جاوا

    LocalModel localModel =
        new LocalModel.Builder()
            .setAssetFilePath("model.tflite")
            // or .setAbsoluteFilePath(absolute path to model file)
            // or .setUri(URI to model file)
            .build();

پیکربندی منبع مدل میزبانی‌شده از راه دور

برای استفاده از مدل میزبانی‌شده از دور، باید فایل مدل را بااستفاده از منطق برنامه خودتان در فضای ذخیره‌سازی محلی دستگاه بارگیری کنید و سپس آن را به‌عنوان مدل محلی بار کنید. توصیه می‌کنیم از Cloud Storage for Firebase برای میزبانی مدل استفاده کنید. برای جزئیات پیاده‌سازی، راهنمای انتقال Firebase ML به Cloud Storage را ببینید.

پیکربندی برچسب‌زن تصویر

پس‌از پیکربندی منابع مدل، یک شیء ImageLabeler از یکی از آن‌ها ایجاد کنید.

گزینه‌های زیر دردسترس است:

گزینه‌ها
confidenceThreshold

حداقل امتیاز اطمینان برچسب‌های شناسایی‌شده. اگر تنظیم نشده باشد، از هر آستانه طبقه‌بندی‌کننده‌ای که در فراداده مدل مشخص شده است استفاده خواهد شد. اگر مدل هیچ فراداده‌ای نداشته باشد یا فراداده آستانه طبقه‌بندی را مشخص نکند، از آستانه پیش‌فرض ۰٫۰ استفاده خواهد شد.

maxResultCount

حداکثر تعداد برچسب‌هایی که برگردانده می‌شود. اگر تنظیم نشده باشد، از مقدار پیش‌فرض ۱۰ استفاده خواهد شد.

اگر فقط مدل دسته‌بندی محلی دارید، کافی است از LocalModel شیء خود یک برچسب‌زن ایجاد کنید:

Kotlin

val customImageLabelerOptions = CustomImageLabelerOptions.Builder(localModel)
    .setConfidenceThreshold(0.5f)
    .setMaxResultCount(5)
    .build()
val labeler = ImageLabeling.getClient(customImageLabelerOptions)

جاوا

CustomImageLabelerOptions customImageLabelerOptions =
        new CustomImageLabelerOptions.Builder(localModel)
            .setConfidenceThreshold(0.5f)
            .setMaxResultCount(5)
            .build();
ImageLabeler labeler = ImageLabeling.getClient(customImageLabelerOptions);

اگر مدل از دور میزبانی می‌شود، باید بررسی کنید که قبل‌از اجرا شدن بارگیری شده باشد.

اگرچه فقط باید این مورد را قبل‌از اجرای برچسب‌زن تأیید کنید، اما اگر هم مدل از دور میزبانی‌شده و هم مدل بسته‌بندی‌شده محلی دارید، ممکن است منطقی باشد که این بررسی را هنگام نمونه‌سازی برچسب‌زن تصویر انجام دهید: اگر مدل از دور بارگیری شده است، برچسب‌زنی از مدل از دور ایجاد کنید، و درغیراین‌صورت از مدل محلی ایجاد کنید.

Kotlin

val modelFile = File(context.cacheDir, "my_downloaded_model.tflite")
val model = if (modelFile.exists()) {
    // Use the downloaded model if available
    LocalModel.Builder().setAbsoluteFilePath(modelFile.absolutePath).build()
} else {
    // Fall back to the bundled model
    LocalModel.Builder().setAssetFilePath("model.tflite").build()
}
val options = CustomImageLabelerOptions.Builder(model)
    .setConfidenceThreshold(0.5f)
    .setMaxResultCount(5)
    .build()
val labeler = ImageLabeling.getClient(options)

جاوا

File modelFile = new File(context.getCacheDir(), "my_downloaded_model.tflite");
LocalModel model;
if (modelFile.exists()) {
    // Use the downloaded model if available
    model = new LocalModel.Builder().setAbsoluteFilePath(modelFile.getAbsolutePath()).build();
} else {
    // Fall back to the bundled model
    model = new LocalModel.Builder().setAssetFilePath("model.tflite").build();
}
CustomImageLabelerOptions options = new CustomImageLabelerOptions.Builder(model)
    .setConfidenceThreshold(0.5f)
    .setMaxResultCount(5)
    .build();
ImageLabeler labeler = ImageLabeling.getClient(options);

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

Kotlin

val localFile = File(context.cacheDir, "my_remote_model.tflite")
if (localFile.exists()) {
    initializeLabeler(localFile)
} else {
    showLoadingUI()
    val storage = Firebase.storage
    val modelRef = storage.getReferenceFromUrl("gs://YOUR_BUCKET/path/to/model.tflite")
    modelRef.getFile(localFile)
        .addOnSuccessListener {
            hideLoadingUI()
            initializeLabeler(localFile)
        }
        .addOnFailureListener {
            showErrorUI()
        }
}

private fun initializeLabeler(modelFile: File) {
    val localModel = LocalModel.Builder().setAbsoluteFilePath(modelFile.absolutePath).build()
    val options = CustomImageLabelerOptions.Builder(localModel).build()
    val labeler = ImageLabeling.getClient(options)
    enableMLFeatures(labeler)
}

جاوا

File localFile = new File(context.getCacheDir(), "my_remote_model.tflite");
if (localFile.exists()) {
    initializeLabeler(localFile);
} else {
    showLoadingUI();
    FirebaseStorage storage = FirebaseStorage.getInstance();
    StorageReference modelRef = storage.getReferenceFromUrl("gs://YOUR_BUCKET/path/to/model.tflite");
    modelRef.getFile(localFile)
        .addOnSuccessListener(new OnSuccessListener<FileDownloadTask.TaskSnapshot>() {
            @Override
            public void onSuccess(FileDownloadTask.TaskSnapshot taskSnapshot) {
                hideLoadingUI();
                initializeLabeler(localFile);
            }
        })
        .addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception exception) {
                showErrorUI();
            }
        });
}

private void initializeLabeler(File modelFile) {
    LocalModel localModel = new LocalModel.Builder().setAbsoluteFilePath(modelFile.getAbsolutePath()).build();
    CustomImageLabelerOptions options = new CustomImageLabelerOptions.Builder(localModel).build();
    ImageLabeler labeler = ImageLabeling.getClient(options);
    enableMLFeatures(labeler);
}

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

سپس، برای هر تصویری که می‌خواهید برچسب‌گذاری کنید، یک شیء InputImage از تصویرتان ایجاد کنید. وقتی از Bitmap استفاده می‌کنید، برچسب‌زن تصویر سریع‌ترین عملکرد را دارد یا اگر از Camera2 API استفاده می‌کنید، از YUV_420_888 media.Image استفاده کنید که درصورت امکان توصیه می‌شود.

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

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

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

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

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

جاوا

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

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

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
}

جاوا

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() منتقل کنید:

Kotlin

val image = InputImage.fromMediaImage(mediaImage, rotation)

Java

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

استفاده از نشانی وب فایل

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

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

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

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

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
)

جاوا

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، بیانیه زیر را اعلام کنید:

Kotlin

val image = InputImage.fromBitmap(bitmap, 0)

Java

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

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

۳. اجرای برچسب‌زن تصویر

برای برچسب‌گذاری اشیا در تصویر، شیء image را به روش ImageLabeler's process() منتقل کنید.

Kotlin

labeler.process(image)
        .addOnSuccessListener { labels ->
            // Task completed successfully
            // ...
        }
        .addOnFailureListener { e ->
            // Task failed with an exception
            // ...
        }

جاوا

labeler.process(image)
        .addOnSuccessListener(new OnSuccessListener<List<ImageLabel>>() {
            @Override
            public void onSuccess(List<ImageLabel> labels) {
                // Task completed successfully
                // ...
            }
        })
        .addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                // Task failed with an exception
                // ...
            }
        });

۴. دریافت اطلاعات درباره نهادهای برچسب‌گذاری‌شده

اگر عملیات برچسب‌گذاری تصویر موفقیت‌آمیز باشد، فهرستی از اشیاء ImageLabel به شنونده موفقیت ارسال می‌شود. هر شیء ImageLabel نشان‌دهنده چیزی است که در تصویر برچسب‌گذاری شده است. می‌توانید شرح نوشتاری هر برچسب (درصورت دردسترس بودن در فراداده فایل مدل LiteRT)، امتیاز اطمینان، و نمایه را دریافت کنید. برای مثال:

Kotlin

for (label in labels) {
    val text = label.text
    val confidence = label.confidence
    val index = label.index
}

جاوا

for (ImageLabel label : labels) {
    String text = label.getText();
    float confidence = label.getConfidence();
    int index = label.getIndex();
}

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

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

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