ติดป้ายกำกับรูปภาพด้วยโมเดลที่ได้รับการฝึกอบรมจาก AutoML บน Android

หลังจากที่คุณ ฝึกโมเดลของคุณเองโดยใช้ AutoML Vision Edge คุณสามารถใช้โมเดลนั้นในแอปของคุณเพื่อติดป้ายกำกับรูปภาพ

มีสองวิธีในการผสานรวมโมเดลที่ฝึกจาก AutoML Vision Edge: คุณสามารถรวมโมเดลโดยใส่ไว้ในโฟลเดอร์แอสเซทของแอป หรือดาวน์โหลดแบบไดนามิกจาก Firebase

ตัวเลือกการรวมโมเดล
รวมอยู่ในแอพของคุณ
  • โมเดลนี้เป็นส่วนหนึ่งของ APK . ของแอปของคุณ
  • รุ่นพร้อมใช้งานทันทีแม้ในขณะที่อุปกรณ์ Android ออฟไลน์
  • ไม่จำเป็นต้องมีโปรเจ็กต์ Firebase
โฮสต์กับ Firebase
  • โฮสต์โมเดลด้วยการอัปโหลดไปยัง Firebase Machine Learning
  • ลดขนาด APK
  • ดาวน์โหลดแบบจำลองได้ตามต้องการ
  • ผลักดันการอัปเดตโมเดลโดยไม่ต้องเผยแพร่แอปของคุณซ้ำ
  • การทดสอบ A/B อย่างง่ายด้วย Firebase Remote Config
  • ต้องมีโปรเจ็กต์ Firebase

ก่อนจะเริ่ม

  1. เพิ่มการพึ่งพาสำหรับไลบรารี ML Kit Android ลงในไฟล์ gradle ระดับแอปของโมดูล ซึ่งมักจะเป็น app/build.gradle :

    ในการรวมโมเดลเข้ากับแอปของคุณ:

    dependencies {
      // ...
      // Image labeling feature with bundled automl model
      implementation 'com.google.mlkit:image-labeling-custom:16.3.1'
    }
    

    สำหรับการดาวน์โหลดโมเดลแบบไดนามิกจาก Firebase ให้เพิ่มการพึ่งพา linkFirebase :

    dependencies {
      // ...
      // Image labeling feature with automl model downloaded
      // from firebase
      implementation 'com.google.mlkit:image-labeling-custom:16.3.1'
      implementation 'com.google.mlkit:linkfirebase:16.1.0'
    }
    
  2. หากคุณต้องการดาวน์โหลด model อย่าลืม เพิ่ม Firebase ในโครงการ Android ของคุณ หากคุณยังไม่ได้ดำเนินการดังกล่าว ไม่จำเป็นเมื่อคุณรวมโมเดล

1. โหลดโมเดล

กำหนดค่าแหล่งที่มาของโมเดลในพื้นที่

วิธีรวมโมเดลกับแอปของคุณ:

  1. แยกโมเดลและข้อมูลเมตาออกจากไฟล์ zip ที่คุณดาวน์โหลดจากคอนโซล Firebase เราขอแนะนำให้คุณใช้ไฟล์ตามที่ดาวน์โหลดมา โดยไม่ต้องแก้ไข (รวมถึงชื่อไฟล์)

  2. รวมโมเดลของคุณและไฟล์ข้อมูลเมตาในแพ็คเกจแอพของคุณ:

    1. หากคุณไม่มีโฟลเดอร์เนื้อหาในโปรเจ็กต์ ให้สร้างขึ้นโดยคลิกขวาที่ app/ โฟลเดอร์ จากนั้นคลิก ใหม่ > โฟลเดอร์ > โฟลเดอร์เนื้อหา
    2. สร้างโฟลเดอร์ย่อยภายใต้โฟลเดอร์ทรัพย์สินเพื่อเก็บไฟล์โมเดล
    3. คัดลอกไฟล์ model.tflite , dict.txt และ manifest.json ไปยังโฟลเดอร์ย่อย (ทั้งสามไฟล์ต้องอยู่ในโฟลเดอร์เดียวกัน)
  3. เพิ่มสิ่งต่อไปนี้ในไฟล์ build.gradle ของแอปเพื่อให้แน่ใจว่า Gradle จะไม่บีบอัดไฟล์โมเดลเมื่อสร้างแอป:

    android {
        // ...
        aaptOptions {
            noCompress "tflite"
        }
    }
    

    ไฟล์โมเดลจะรวมอยู่ในแพ็คเกจแอปและพร้อมให้ ML Kit เป็นสินทรัพย์ดิบ

  4. สร้างวัตถุ LocalModel โดยระบุพาธไปยังไฟล์รายการโมเดล:

    Java

    AutoMLImageLabelerLocalModel localModel =
        new AutoMLImageLabelerLocalModel.Builder()
            .setAssetFilePath("manifest.json")
            // or .setAbsoluteFilePath(absolute file path to manifest file)
            .build();
    

    คอตลิน

    val localModel = LocalModel.Builder()
        .setAssetManifestFilePath("manifest.json")
        // or .setAbsoluteManifestFilePath(absolute file path to manifest file)
        .build()
    

กำหนดค่าแหล่งที่มาของโมเดลที่โฮสต์โดย Firebase

ในการใช้โมเดลที่โฮสต์จากระยะไกล ให้สร้างออบเจ็กต์ CustomRemoteModel โดยระบุชื่อที่คุณกำหนดให้กับโมเดลเมื่อคุณเผยแพร่:

Java

// Specify the name you assigned in the Firebase console.
FirebaseModelSource firebaseModelSource =
    new FirebaseModelSource.Builder("your_model_name").build();
CustomRemoteModel remoteModel =
    new CustomRemoteModel.Builder(firebaseModelSource).build();

คอตลิน

// Specify the name you assigned in the Firebase console.
val firebaseModelSource = FirebaseModelSource.Builder("your_model_name")
    .build()
val remoteModel = CustomRemoteModel.Builder(firebaseModelSource).build()

จากนั้น เริ่มงานดาวน์โหลดแบบจำลอง โดยระบุเงื่อนไขที่คุณต้องการอนุญาตให้ดาวน์โหลด หากโมเดลนั้นไม่มีอยู่ในอุปกรณ์ หรือมีเวอร์ชันที่ใหม่กว่าอยู่ งานจะดาวน์โหลดโมเดลแบบอะซิงโครนัสจาก Firebase:

Java

DownloadConditions downloadConditions = new DownloadConditions.Builder()
        .requireWifi()
        .build();
RemoteModelManager.getInstance().download(remoteModel, downloadConditions)
        .addOnSuccessListener(new OnSuccessListener<Void>() {
            @Override
            public void onSuccess(@NonNull Task<Void> task) {
                // Success.
            }
        });

Kotlin

val downloadConditions = DownloadConditions.Builder()
    .requireWifi()
    .build()
RemoteModelManager.getInstance().download(remoteModel, downloadConditions)
    .addOnSuccessListener {
        // Success.
    }

แอพจำนวนมากเริ่มงานดาวน์โหลดในรหัสเริ่มต้น แต่คุณสามารถทำได้ทุกเมื่อก่อนจะต้องใช้โมเดล

สร้างเครื่องติดฉลากรูปภาพจากโมเดลของคุณ

หลังจากที่คุณกำหนดค่าแหล่งที่มาของแบบจำลองแล้ว ให้สร้างวัตถุ ImageLabeler จากหนึ่งในนั้น

หากคุณมีเฉพาะโมเดลที่รวมอยู่ในเครื่อง ให้สร้าง labeler จากออบเจ็กต์ CustomImageLabelerOptions ของคุณและกำหนดค่าเกณฑ์คะแนนความมั่นใจที่คุณต้องการ (ดู ประเมินโมเดลของคุณ ):

Java

CustomImageLabelerOptions customImageLabelerOptions = new CustomImageLabelerOptions.Builder(localModel)
    .setConfidenceThreshold(0.0f)  // Evaluate your model in the Cloud console
                                   // to determine an appropriate value.
    .build();
ImageLabeler labeler = ImageLabeling.getClient(customImageLabelerOptions);

คอตลิน

val customImageLabelerOptions = CustomImageLabelerOptions.Builder(localModel)
    .setConfidenceThreshold(0.0f)  // Evaluate your model in the Cloud console
                                   // to determine an appropriate value.
    .build()
val labeler = ImageLabeling.getClient(customImageLabelerOptions)

หากคุณมีโมเดลที่โฮสต์จากระยะไกล คุณจะต้องตรวจสอบว่าได้ดาวน์โหลดโมเดลดังกล่าวแล้วก่อนที่จะเรียกใช้ คุณสามารถตรวจสอบสถานะของงานดาวน์โหลดโมเดลได้โดยใช้ isModelDownloaded() ของตัวจัดการโมเดล

แม้ว่าคุณจะต้องยืนยันสิ่งนี้ก่อนเรียกใช้ labeler เท่านั้น หากคุณมีทั้งโมเดลที่โฮสต์จากระยะไกลและโมเดลที่รวมอยู่ในเครื่อง คุณควรดำเนินการตรวจสอบนี้เมื่อสร้างอินสแตนซ์ labeler: สร้าง labeler จากโมเดลระยะไกลหาก มันถูกดาวน์โหลดและจากรุ่นในพื้นที่เป็นอย่างอื่น

Java

RemoteModelManager.getInstance().isModelDownloaded(remoteModel)
        .addOnSuccessListener(new OnSuccessListener<Boolean>() {
            @Override
            public void onSuccess(Boolean isDownloaded) {
                CustomImageLabelerOptions.Builder optionsBuilder;
                if (isDownloaded) {
                    optionsBuilder = new CustomImageLabelerOptions.Builder(remoteModel);
                } else {
                    optionsBuilder = new CustomImageLabelerOptions.Builder(localModel);
                }
                CustomImageLabelerOptions options = optionsBuilder
                        .setConfidenceThreshold(0.0f)  // Evaluate your model in the Cloud console
                                                       // to determine an appropriate threshold.
                        .build();

                ImageLabeler labeler = ImageLabeling.getClient(options);
            }
        });

คอตลิน

RemoteModelManager.getInstance().isModelDownloaded(remoteModel)
    .addOnSuccessListener { isDownloaded ->
        val optionsBuilder =
            if (isDownloaded) {
                CustomImageLabelerOptions.Builder(remoteModel)
            } else {
                CustomImageLabelerOptions.Builder(localModel)
            }
        // Evaluate your model in the Cloud console to determine an appropriate threshold.
        val options = optionsBuilder.setConfidenceThreshold(0.0f).build()
        val labeler = ImageLabeling.getClient(options)
}

หากคุณมีเฉพาะโมเดลที่โฮสต์จากระยะไกล คุณควรปิดใช้งานฟังก์ชันที่เกี่ยวข้องกับโมเดล เช่น สีเทาหรือซ่อนส่วนหนึ่งของ UI ของคุณ จนกว่าคุณจะยืนยันว่าได้ดาวน์โหลดโมเดลแล้ว คุณสามารถทำได้โดยแนบ Listener เข้ากับเมธอด download() ของ model manager:

Java

RemoteModelManager.getInstance().download(remoteModel, conditions)
        .addOnSuccessListener(new OnSuccessListener<Void>() {
            @Override
            public void onSuccess(Void v) {
              // Download complete. Depending on your app, you could enable
              // the ML feature, or switch from the local model to the remote
              // model, etc.
            }
        });

คอตลิน

RemoteModelManager.getInstance().download(remoteModel, conditions)
    .addOnSuccessListener {
        // Download complete. Depending on your app, you could enable the ML
        // feature, or switch from the local model to the remote model, etc.
    }

2. เตรียมภาพอินพุต

จากนั้น สำหรับแต่ละภาพที่คุณต้องการติดป้ายกำกับ ให้สร้างวัตถุ InputImage จากภาพของคุณ ตัวติดป้ายกำกับรูปภาพทำงานเร็วที่สุดเมื่อคุณใช้ Bitmap หรือหากคุณใช้ camera2 API สื่อ media.Image ซึ่งแนะนำเมื่อเป็นไปได้

คุณสามารถสร้าง InputImage จากแหล่งต่างๆ ได้ โดยแต่ละแหล่งจะอธิบายไว้ด้านล่าง

การใช้ media.Image

ในการสร้างวัตถุ InputImage จากวัตถุ media.Image เช่น เมื่อคุณถ่ายภาพจากกล้องของอุปกรณ์ ให้ส่งวัตถุ media.Image และการหมุนของรูปภาพไปที่ InputImage.fromMediaImage()

หากคุณใช้ไลบรารี CameraX คลาส OnImageCapturedListener และ ImageAnalysis.Analyzer คำนวณค่าการหมุนสำหรับคุณ

Java

private class YourAnalyzer implements ImageAnalysis.Analyzer {

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

Kotlin+KTX

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 static final SparseIntArray ORIENTATIONS = new SparseIntArray();
static {
    ORIENTATIONS.append(Surface.ROTATION_0, 90);
    ORIENTATIONS.append(Surface.ROTATION_90, 0);
    ORIENTATIONS.append(Surface.ROTATION_180, 270);
    ORIENTATIONS.append(Surface.ROTATION_270, 180);
}

/**
 * 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, Context context)
        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);

    // On most devices, the sensor orientation is 90 degrees, but for some
    // devices it is 270 degrees. For devices with a sensor orientation of
    // 270, rotate the image an additional 180 ((270 + 270) % 360) degrees.
    CameraManager cameraManager = (CameraManager) context.getSystemService(CAMERA_SERVICE);
    int sensorOrientation = cameraManager
            .getCameraCharacteristics(cameraId)
            .get(CameraCharacteristics.SENSOR_ORIENTATION);
    rotationCompensation = (rotationCompensation + sensorOrientation + 270) % 360;

    // Return the corresponding FirebaseVisionImageMetadata rotation value.
    int result;
    switch (rotationCompensation) {
        case 0:
            result = FirebaseVisionImageMetadata.ROTATION_0;
            break;
        case 90:
            result = FirebaseVisionImageMetadata.ROTATION_90;
            break;
        case 180:
            result = FirebaseVisionImageMetadata.ROTATION_180;
            break;
        case 270:
            result = FirebaseVisionImageMetadata.ROTATION_270;
            break;
        default:
            result = FirebaseVisionImageMetadata.ROTATION_0;
            Log.e(TAG, "Bad rotation value: " + rotationCompensation);
    }
    return result;
}

Kotlin+KTX

private val ORIENTATIONS = SparseIntArray()

init {
    ORIENTATIONS.append(Surface.ROTATION_0, 90)
    ORIENTATIONS.append(Surface.ROTATION_90, 0)
    ORIENTATIONS.append(Surface.ROTATION_180, 270)
    ORIENTATIONS.append(Surface.ROTATION_270, 180)
}
/**
 * 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, context: Context): 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)

    // On most devices, the sensor orientation is 90 degrees, but for some
    // devices it is 270 degrees. For devices with a sensor orientation of
    // 270, rotate the image an additional 180 ((270 + 270) % 360) degrees.
    val cameraManager = context.getSystemService(CAMERA_SERVICE) as CameraManager
    val sensorOrientation = cameraManager
            .getCameraCharacteristics(cameraId)
            .get(CameraCharacteristics.SENSOR_ORIENTATION)!!
    rotationCompensation = (rotationCompensation + sensorOrientation + 270) % 360

    // Return the corresponding FirebaseVisionImageMetadata rotation value.
    val result: Int
    when (rotationCompensation) {
        0 -> result = FirebaseVisionImageMetadata.ROTATION_0
        90 -> result = FirebaseVisionImageMetadata.ROTATION_90
        180 -> result = FirebaseVisionImageMetadata.ROTATION_180
        270 -> result = FirebaseVisionImageMetadata.ROTATION_270
        else -> {
            result = FirebaseVisionImageMetadata.ROTATION_0
            Log.e(TAG, "Bad rotation value: $rotationCompensation")
        }
    }
    return result
}

จากนั้นส่งวัตถุ media.Image และค่าองศาการหมุนไปที่ InputImage.fromMediaImage() :

Java

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

Kotlin+KTX

val image = InputImage.fromMediaImage(mediaImage, rotation)

การใช้ไฟล์ URI

ในการสร้างวัตถุ InputImage จากไฟล์ URI ให้ส่งบริบทของแอพและไฟล์ URI ไปที่ InputImage.fromFilePath() สิ่งนี้มีประโยชน์เมื่อคุณใช้เจตนา ACTION_GET_CONTENT เพื่อแจ้งให้ผู้ใช้เลือกรูปภาพจากแอปแกลเลอรีของตน

Java

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

Kotlin+KTX

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

การใช้ ByteBuffer หรือ ByteArray

ในการสร้างวัตถุ InputImage จาก ByteBuffer หรือ ByteArray ก่อนอื่นให้คำนวณระดับการหมุนของรูปภาพตามที่อธิบายไว้ก่อนหน้านี้สำหรับ media.Image input จากนั้น สร้างวัตถุ InputImage ด้วยบัฟเฟอร์หรืออาร์เรย์ พร้อมกับความสูง ความกว้าง รูปแบบการเข้ารหัสสี และระดับการหมุนของรูปภาพ:

Java

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

Kotlin+KTX

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

การใช้ Bitmap

ในการสร้างวัตถุ InputImage จากวัตถุ Bitmap ให้ประกาศต่อไปนี้:

Java

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

Kotlin+KTX

val image = InputImage.fromBitmap(bitmap, 0)

รูปภาพแสดงด้วยวัตถุ Bitmap พร้อมกับองศาการหมุน

3. เรียกใช้เครื่องติดฉลากรูปภาพ

หากต้องการติดป้ายกำกับวัตถุในรูปภาพ ให้ส่งวัตถุ image ไปยังเมธอด process() ของ ImageLabeler

Java

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

Kotlin

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

4. รับข้อมูลเกี่ยวกับวัตถุที่มีป้ายกำกับ

หากการดำเนินการติดป้ายกำกับรูปภาพสำเร็จ รายการของออบเจ็กต์ ImageLabel จะถูกส่งต่อไปยังผู้ฟังความสำเร็จ แต่ละอ็อบเจ็กต์ ImageLabel แสดงถึงบางสิ่งที่มีป้ายกำกับในภาพ คุณสามารถรับคำอธิบายข้อความของป้ายกำกับ คะแนนความเชื่อมั่นของการแข่งขัน และดัชนีของการแข่งขัน ตัวอย่างเช่น:

Java

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

คอตลิน

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

เคล็ดลับในการปรับปรุงประสิทธิภาพแบบเรียลไทม์

หากคุณต้องการติดป้ายกำกับรูปภาพในแอปพลิเคชันแบบเรียลไทม์ ให้ปฏิบัติตามหลักเกณฑ์เหล่านี้เพื่อให้ได้อัตราเฟรมที่ดีที่สุด:
  • คันเร่งเรียกผู้ติดฉลากรูปภาพ หากมีเฟรมวิดีโอใหม่ในขณะที่ใช้ป้ายกำกับรูปภาพ ให้วางเฟรม ดูตัวอย่างคลาส VisionProcessorBase ในแอปตัวอย่างการเริ่มต้นอย่างรวดเร็ว
  • หากคุณกำลังใช้เอาต์พุตของตัวติดป้ายกำกับรูปภาพเพื่อซ้อนทับกราฟิกบนรูปภาพอินพุต ขั้นแรกให้รับผลลัพธ์ จากนั้นแสดงรูปภาพและโอเวอร์เลย์ในขั้นตอนเดียว เมื่อทำเช่นนั้น คุณจะแสดงผลไปยังพื้นผิวการแสดงผลเพียงครั้งเดียวสำหรับแต่ละเฟรมอินพุต ดู CameraSourcePreview และ GraphicOverlay ในแอปตัวอย่างการเริ่มต้นอย่างรวดเร็วสำหรับตัวอย่าง
  • หากคุณใช้ Camera2 API ให้จับภาพในรูปแบบ ImageFormat.YUV_420_888

    หากคุณใช้ Camera API รุ่นเก่า ให้จับภาพในรูปแบบ ImageFormat.NV21