ตรวจจับและติดตามวัตถุด้วย 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 การตรวจจับและติดตามวัตถุได้รับการเพิ่มประสิทธิภาพเพื่อการใช้งานหลัก 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 หรือ 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 ที่ระบุตำแหน่งของวัตถุในรูปภาพ
รหัสติดตาม จำนวนเต็มที่ระบุวัตถุในรูปภาพ ค่า Null ใน 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 Design ของ ML Kit และรูปแบบสำหรับฟีเจอร์ที่ทำงานด้วยแมชชีนเลิร์นนิงใน Material Design

Improving performance

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

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

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

  • หากคุณใช้ 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