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

หลังจากฝึกโมเดลของคุณเองโดยใช้ AutoML Vision Edge แล้ว คุณจะใช้โมเดลดังกล่าวในแอปเพื่อติดป้ายกำกับรูปภาพได้ การผสานรวมโมเดลที่ฝึกจาก AutoML Vision Edge ทำได้ 2 วิธี ได้แก่ คุณสามารถรวมโมเดลโดยใส่ไว้ในโฟลเดอร์ชิ้นงานของแอป หรือจะดาวน์โหลดจาก Firebase แบบไดนามิกก็ได้
ตัวเลือกการรวมโมเดล
รวมไว้ในแอป
  • โมเดลเป็นส่วนหนึ่งของ APK ของแอป
  • โมเดลพร้อมใช้งานทันทีแม้ว่าอุปกรณ์ Android จะออฟไลน์อยู่ก็ตาม
  • ไม่ต้องมีโปรเจ็กต์ Firebase
โฮสต์ด้วย Firebase

ลองเลย

ก่อนเริ่มต้น

1. ในไฟล์ build.gradle ระดับโปรเจ็กต์ ให้ตรวจสอบว่าได้รวม ที่เก็บ Maven ของ Google ไว้ในทั้งส่วน buildscript และ allprojects

2. เพิ่มทรัพยากร Dependency สำหรับไลบรารี Android ของ ML Kit ลงในไฟล์ Gradle ระดับแอปของโมดูล ซึ่งโดยปกติคือ app/build.gradle: หากต้องการรวมโมเดลไว้กับแอป ให้ทำดังนี้
    dependencies {
      // ...
      // Image labeling feature with bundled automl model
      implementation 'com.google.mlkit:image-labeling-automl:16.2.1'
    }
    
หากต้องการดาวน์โหลดโมเดลจาก Firebase แบบไดนามิก ให้เพิ่มทรัพยากร Dependency linkFirebase ดังนี้
    dependencies {
      // ...
      // Image labeling feature with automl model downloaded
      // from firebase
      implementation 'com.google.mlkit:image-labeling-automl:16.2.1'
      implementation 'com.google.mlkit:linkfirebase:16.0.1'
    }
    
3. หากต้องการดาวน์โหลดโมเดล โปรดตรวจสอบว่าคุณได้ เพิ่ม Firebase ลงในโปรเจ็กต์ Android แล้ว หากยังไม่ได้ดำเนินการ การดำเนินการนี้ไม่จำเป็นเมื่อคุณรวมโมเดล

1. โหลดโมเดล

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

วิธีรวมโมเดลกับแอป

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

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

ก. หากไม่มีโฟลเดอร์ชิ้นงานในโปรเจ็กต์ ให้สร้างโฟลเดอร์โดย คลิกขวาที่โฟลเดอร์ app/ แล้วคลิก ใหม่ > โฟลเดอร์ > โฟลเดอร์ชิ้นงาน

ข. สร้างโฟลเดอร์ย่อยในโฟลเดอร์ชิ้นงานเพื่อเก็บไฟล์โมเดล

ค. คัดลอกไฟล์ model.tflite, dict.txt และ manifest.json ไปยังโฟลเดอร์ย่อย (ไฟล์ทั้ง 3 รายการต้องอยู่ในโฟลเดอร์เดียวกัน)

3. เพิ่มโค้ดต่อไปนี้ลงในไฟล์ build.gradle ของแอปเพื่อให้แน่ใจว่า Gradle จะไม่บีบอัดไฟล์โมเดลเมื่อสร้างแอป
    android {
        // ...
        aaptOptions {
            noCompress "tflite"
        }
    }
    
ไฟล์โมเดลจะรวมอยู่ในแพ็กเกจแอปและพร้อมใช้งานกับ ML Kit เป็นชิ้นงานดิบ

หมายเหตุ: ตั้งแต่ปลั๊กอิน Android Gradle เวอร์ชัน 4.1 เป็นต้นไป ระบบจะเพิ่ม .tflite ลงในรายการ noCompress โดยค่าเริ่มต้น และไม่จำเป็นต้องดำเนินการข้างต้นอีกต่อไป

4. สร้างออบเจ็กต์ LocalModel โดยระบุเส้นทางไปยังไฟล์ Manifest ของโมเดล

Kotlin

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

Java

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

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

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

Kotlin

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

Java

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

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

Kotlin

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

Java

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

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

สร้างโปรแกรมติดป้ายกำกับรูปภาพจากโมเดล

หลังจากกำหนดค่าแหล่งที่มาของโมเดลแล้ว ให้สร้างออบเจ็กต์ ImageLabeler จากแหล่งที่มาใดแหล่งที่มาหนึ่ง

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

Kotlin

val autoMLImageLabelerOptions = AutoMLImageLabelerOptions.Builder(localModel)
    .setConfidenceThreshold(0)  // Evaluate your model in the Firebase console
                                // to determine an appropriate value.
    .build()
val labeler = ImageLabeling.getClient(autoMLImageLabelerOptions)

Java

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

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

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

Kotlin

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

Java

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

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

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

Kotlin

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

Java

RemoteModelManager.getInstance().download(remoteModel, conditions)
        .addOnSuccessListener(new OnSuccessListener() {
            @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.
            }
        });

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

จากนั้นสร้าง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
            // ...
        }
    }
}

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

หากไม่ได้ใช้คลังกล้องที่ให้องศาการหมุนของรูปภาพ คุณ สามารถคำนวณได้จากองศาการหมุนของอุปกรณ์และการวางแนวของเซ็นเซอร์กล้อง ในอุปกรณ์

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

จากนั้นส่งmedia.Imageออบเจ็กต์และค่าองศาการหมุนไปยัง InputImage.fromMediaImage():

Kotlin

val image = InputImage.fromMediaImage(mediaImage, rotation)

Java

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

การใช้ URI ของไฟล์

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

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
)

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

การใช้ Bitmap

หากต้องการสร้างออบเจ็กต์ InputImage จากออบเจ็กต์ Bitmap ให้ประกาศดังนี้

Kotlin

val image = InputImage.fromBitmap(bitmap, 0)

Java

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

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

3. เรียกใช้เครื่องมือติดป้ายกำกับรูปภาพ

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

Kotlin

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

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

4. ดูข้อมูลเกี่ยวกับออบเจ็กต์ที่มีป้ายกำกับ

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

Kotlin

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

Java

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

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

หากต้องการติดป้ายกำกับรูปภาพในแอปพลิเคชันแบบเรียลไทม์ ให้ทำตาม หลักเกณฑ์เหล่านี้เพื่อให้ได้อัตราเฟรมที่ดีที่สุด

  • หากคุณใช้ API ของ Camera หรือ camera2 ให้จำกัดการเรียกใช้เครื่องติดป้ายกำกับรูปภาพ หากมีเฟรมวิดีโอใหม่ ขณะที่เครื่องมือติดป้ายกำกับรูปภาพทำงานอยู่ ให้ทิ้งเฟรมนั้น ดูตัวอย่างได้ที่คลาส VisionProcessorBase ในแอปตัวอย่างการเริ่มต้นอย่างรวดเร็ว
  • หากคุณใช้ CameraX API โปรดตรวจสอบว่าได้ตั้งค่ากลยุทธ์การควบคุมปริมาณการรับส่งเป็นค่าเริ่มต้น ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST ซึ่งจะรับประกันว่าระบบจะส่งรูปภาพเพียงรูปเดียวเพื่อวิเคราะห์ในแต่ละครั้ง หากมีการสร้างรูปภาพเพิ่มเติมเมื่อเครื่องมือวิเคราะห์ทำงานอยู่ ระบบจะทิ้งรูปภาพเหล่านั้นโดยอัตโนมัติและจะไม่จัดคิวเพื่อส่ง เมื่อปิดรูปภาพที่กำลังวิเคราะห์โดยเรียกใช้ ImageProxy.close() ระบบจะส่งรูปภาพล่าสุดถัดไป
  • หากคุณใช้เอาต์พุตของเครื่องมือติดป้ายกำกับรูปภาพเพื่อซ้อนทับกราฟิกบน รูปภาพอินพุต ให้รับผลลัพธ์จาก ML Kit ก่อน จากนั้นจึงแสดงรูปภาพ และซ้อนทับในขั้นตอนเดียว ซึ่งจะแสดงในพื้นผิวการแสดงผล เพียงครั้งเดียวสำหรับแต่ละเฟรมอินพุต ดูตัวอย่างได้ที่คลาส CameraSourcePreview และ GraphicOverlay ในแอปตัวอย่างการเริ่มต้นอย่างรวดเร็ว
  • หากใช้ API ของ Camera2 ให้ถ่ายภาพในรูปแบบ ImageFormat.YUV_420_888 หากใช้ Camera API เวอร์ชันเก่า ให้ถ่ายภาพในรูปแบบ ImageFormat.NV21