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

Improving performance

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

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

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

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