ตรวจจับและติดตามวัตถุด้วย ML Kit บน Android

คุณสามารถใช้ ML Kit เพื่อตรวจจับและติดตามวัตถุในเฟรมวิดีโอที่ต่อเนื่องกันได้

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

ลองเลย

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

  1. ในไฟล์ build.gradle ระดับโปรเจ็กต์ ให้ตรวจสอบว่าได้รวม ที่เก็บ Maven ของ Google ไว้ในทั้งส่วน buildscript และ allprojects
  2. เพิ่มทรัพยากร Dependency สำหรับไลบรารี ML Kit สำหรับ Android ลงในไฟล์ Gradle ระดับแอปของโมดูล ซึ่งโดยปกติจะเป็นไฟล์ app/build.gradle ดังนี้
    dependencies {
      // ...
    
      implementation 'com.google.mlkit:object-detection:17.0.2'
    
    }

1. กำหนดค่าตัวตรวจจับออบเจ็กต์

หากต้องการตรวจจับและติดตามวัตถุ ให้สร้างอินสแตนซ์ของ ObjectDetector ก่อน แล้วระบุการตั้งค่าตัวตรวจจับที่ต้องการเปลี่ยนจากค่าเริ่มต้น (ไม่บังคับ)

  1. กำหนดค่าตัวตรวจจับวัตถุสำหรับ Use Case ของคุณด้วยออบเจ็กต์ ObjectDetectorOptions คุณสามารถเปลี่ยนการตั้งค่าต่อไปนี้ได้

    การตั้งค่าตัวตรวจจับวัตถุ
    โหมดการตรวจจับ STREAM_MODE (ค่าเริ่มต้น) | SINGLE_IMAGE_MODE

    ใน STREAM_MODE (ค่าเริ่มต้น) ตัวตรวจจับวัตถุจะทำงานโดยมีเวลาในการตอบสนองต่ำ แต่อาจให้ผลลัพธ์ที่ไม่สมบูรณ์ (เช่น กรอบล้อมรอบหรือป้ายกำกับหมวดหมู่ที่ไม่ได้ระบุ) ในการเรียกใช้ตัวตรวจจับ 2-3 ครั้งแรก นอกจากนี้ ใน STREAM_MODE ตัวตรวจจับจะกำหนดรหัสติดตามให้กับวัตถุ ซึ่งคุณสามารถใช้เพื่อ ติดตามวัตถุในเฟรมต่างๆ ได้ ใช้โหมดนี้เมื่อต้องการติดตาม วัตถุ หรือเมื่อเวลาในการตอบสนองต่ำมีความสำคัญ เช่น เมื่อประมวลผล สตรีมวิดีโอแบบเรียลไทม์

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

    ตรวจจับและติดตามวัตถุหลายรายการ false (ค่าเริ่มต้น) | true

    เลือกว่าจะตรวจจับและติดตามวัตถุได้สูงสุด 5 รายการหรือเฉพาะวัตถุที่โดดเด่นที่สุด (ค่าเริ่มต้น)

    จัดประเภทวัตถุ false (ค่าเริ่มต้น) | true

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

    API การตรวจจับและติดตามวัตถุได้รับการปรับให้เหมาะกับ Use Case หลัก 2 กรณีต่อไปนี้

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

    วิธีกำหนดค่า API สำหรับ Use Case เหล่านี้

    Kotlin

    // Live detection and tracking
    val options = ObjectDetectorOptions.Builder()
            .setDetectorMode(ObjectDetectorOptions.STREAM_MODE)
            .enableClassification()  // Optional
            .build()
    
    // Multiple object detection in static images
    val options = ObjectDetectorOptions.Builder()
            .setDetectorMode(ObjectDetectorOptions.SINGLE_IMAGE_MODE)
            .enableMultipleObjects()
            .enableClassification()  // Optional
            .build()

    Java

    // Live detection and tracking
    ObjectDetectorOptions options =
            new ObjectDetectorOptions.Builder()
                    .setDetectorMode(ObjectDetectorOptions.STREAM_MODE)
                    .enableClassification()  // Optional
                    .build();
    
    // Multiple object detection in static images
    ObjectDetectorOptions options =
            new ObjectDetectorOptions.Builder()
                    .setDetectorMode(ObjectDetectorOptions.SINGLE_IMAGE_MODE)
                    .enableMultipleObjects()
                    .enableClassification()  // Optional
                    .build();
  2. รับอินสแตนซ์ของ ObjectDetector ดังนี้

    Kotlin

    val objectDetector = ObjectDetection.getClient(options)

    Java

    ObjectDetector objectDetector = ObjectDetection.getClient(options);

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

หากต้องการตรวจจับและติดตามวัตถุ ให้ส่งรูปภาพไปยังเมธอด process() ของอินสแตนซ์ ObjectDetector

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

สำหรับแต่ละเฟรมของวิดีโอหรือรูปภาพในลำดับ ให้ทำดังนี้

คุณสามารถสร้าง 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() วิธีนี้มีประโยชน์เมื่อคุณ ใช้ Intent 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
)

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. ประมวลผลรูปภาพ

ส่งรูปภาพไปยังเมธอด process() ดังนี้

Kotlin

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

Java

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

4. ดูข้อมูลเกี่ยวกับวัตถุที่ตรวจพบ

หากการเรียก process() สำเร็จ ระบบจะส่งรายการ DetectedObject ไปยัง Listener ที่สำเร็จ

DetectedObject แต่ละรายการจะมีพร็อพเพอร์ตี้ต่อไปนี้

กรอบล้อมรอบ Rect ที่ระบุตำแหน่งของออบเจ็กต์ใน รูปภาพ
รหัสติดตาม จำนวนเต็มที่ระบุออบเจ็กต์ในรูปภาพ เป็น Null ใน SINGLE_IMAGE_MODE
ป้ายกำกับ
คำอธิบายป้ายกำกับ คำอธิบายข้อความของป้ายกำกับ ซึ่งจะเป็นค่าคงที่สตริงค่าใดค่าหนึ่งที่กำหนดไว้ใน PredefinedCategory
ดัชนีป้ายกำกับ ดัชนีของป้ายกำกับในป้ายกำกับทั้งหมดที่ตัวแยกประเภทรองรับ ซึ่งจะเป็นค่าคงที่จำนวนเต็มค่าใดค่าหนึ่งที่กำหนดไว้ใน PredefinedCategory
ความน่าจะเป็นของป้ายกำกับ ค่า Confidence ของการจัดประเภทออบเจ็กต์

Kotlin

for (detectedObject in detectedObjects) {
    val boundingBox = detectedObject.boundingBox
    val trackingId = detectedObject.trackingId
    for (label in detectedObject.labels) {
        val text = label.text
        if (PredefinedCategory.FOOD == text) {
            ...
        }
        val index = label.index
        if (PredefinedCategory.FOOD_INDEX == index) {
            ...
        }
        val confidence = label.confidence
    }
}

Java

// The list of detected objects contains one item if multiple
// object detection wasn't enabled.
for (DetectedObject detectedObject : detectedObjects) {
    Rect boundingBox = detectedObject.getBoundingBox();
    Integer trackingId = detectedObject.getTrackingId();
    for (Label label : detectedObject.getLabels()) {
        String text = label.getText();
        if (PredefinedCategory.FOOD.equals(text)) {
            ...
        }
        int index = label.getIndex();
        if (PredefinedCategory.FOOD_INDEX == index) {
            ...
        }
        float confidence = label.getConfidence();
    }
}

การรับประกันประสบการณ์ของผู้ใช้ที่ยอดเยี่ยม

เพื่อให้ผู้ใช้ได้รับประสบการณ์ที่ดีที่สุด ให้ปฏิบัติตามหลักเกณฑ์ต่อไปนี้ในแอป

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

นอกจากนี้ โปรดดู แอปตัวอย่าง ML Kit Material Design และ คอลเล็กชัน รูปแบบ Material Design สำหรับฟีเจอร์ที่ขับเคลื่อนด้วยแมชชีนเลิร์นนิง

การปรับปรุงประสิทธิภาพการทำงาน

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

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

  • ปิดใช้การจัดประเภทหากไม่จำเป็น

  • หากใช้ 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 รูปแบบ