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

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

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

ลองเลย

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

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

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 การตรวจจับและการติดตามออบเจ็กต์สำหรับกรณีการใช้งานหลัก 2 อย่างต่อไปนี้

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

    วิธีกำหนดค่า API สำหรับกรณีการใช้งานเหล่านี้

    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 หรือ YUV_420_888 media.Image โดยตรง ขอแนะนำให้สร้าง InputImage จากแหล่งที่มาเหล่านั้นหากคุณมีสิทธิ์เข้าถึงโดยตรง หากคุณสร้าง InputImage จากแหล่งที่มาอื่นๆ เราจะจัดการ Conversion ภายในให้เอง ซึ่งอาจมีประสิทธิภาพลดลง

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

คุณสร้างออบเจ็กต์ 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 ที่แสดงตำแหน่งของออบเจ็กต์ในรูปภาพ
รหัสติดตาม จำนวนเต็มที่ระบุวัตถุในรูปภาพต่างๆ ค่าใน SINGLE_IMAGE_mode
ป้ายกำกับ
คำอธิบายป้ายกำกับ คำอธิบายข้อความของป้ายกำกับ โดยจะเป็นหนึ่งในค่าคงที่สตริงที่กำหนดไว้ใน PredefinedCategory
ดัชนีป้ายกำกับ ดัชนีของป้ายกำกับในป้ายกำกับทั้งหมดที่ตัวแยกประเภทรองรับ โดยจะเป็นหนึ่งในค่าคงที่จำนวนเต็มที่กำหนดไว้ใน PredefinedCategory
ความเชื่อมั่นของป้ายกำกับ ค่าความเชื่อมั่นของการจัดประเภทออบเจ็กต์

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

ช่วยให้ผู้ใช้ได้รับประสบการณ์ที่ดี

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

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

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

Improving performance

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

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

  • ปิดใช้การแยกประเภทหากไม่ต้องการ

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