Wykrywanie i śledzenie obiektów za pomocą ML Kit na Androidzie

Za pomocą ML Kit możesz wykrywać i śledzić obiekty na poszczególnych klatkach filmu.

Gdy przekazujesz obrazy do ML Kit, zwraca on dla każdego z nich listę maksymalnie 5 wykrytych obiektów wraz z ich pozycją na obrazie. Podczas wykrywania obiektów w strumieniach wideo każdy obiekt ma identyfikator, za pomocą którego można go śledzić na różnych obrazach. Możesz też opcjonalnie włączyć ogólną klasyfikację obiektów, która oznacza obiekty opisami ogólnych kategorii.

Zanim zaczniesz

  1. Jeśli jeszcze tego nie zrobiono, dodaj Firebase do projektu na Androida.
  2. Dodaj zależności do bibliotek ML Kit na Androida do pliku Gradle modułu (na poziomie aplikacji) (zwykle app/build.gradle):
    apply plugin: 'com.android.application'
    apply plugin: 'com.google.gms.google-services'
    
    dependencies {
      // ...
    
      implementation 'com.google.firebase:firebase-ml-vision:24.0.3'
      implementation 'com.google.firebase:firebase-ml-vision-object-detection-model:19.0.6'
    }

1. Konfigurowanie detektora obiektów

Aby rozpocząć wykrywanie i śledzenie obiektów, najpierw utwórz instancję FirebaseVisionObjectDetector, opcjonalnie określając ustawienia wykrywacza, które chcesz zmienić w stosunku do domyślnych.

  1. Skonfiguruj wykrywanie obiektów pod kątem przypadku użycia za pomocą obiektu FirebaseVisionObjectDetectorOptions. Możesz zmienić te ustawienia:

    Ustawienia funkcji wykrywania obiektów
    Tryb wykrywania STREAM_MODE (domyślnie) | SINGLE_IMAGE_MODE

    W trybie STREAM_MODE (domyślnym) wykrywanie obiektów odbywa się z niską latencją, ale w pierwszych kilku wywołaniach może dawać niepełne wyniki (np. nieokreślone ramki lub etykiety kategorii). Ponadto w STREAM_MODE detektor przypisuje obiektom identyfikatory śledzenia, których możesz używać do śledzenia obiektów w ramkach poszczególnych klatek. Używaj tego trybu, gdy chcesz śledzić obiekty lub gdy krótki czas oczekiwania jest ważny, np. podczas przetwarzania strumieni wideo w czasie rzeczywistym.

    W trybie SINGLE_IMAGE_MODE wykrywanie obiektów czeka na otrzymanie ramki obiektu i (jeśli włączysz klasyfikację) etykiety kategorii, zanim zwróci wynik. W efekcie czas wykrywania może być dłuższy. Ponadto w SINGLE_IMAGE_MODE nie są przypisywane identyfikatory śledzenia. Użyj tego trybu, jeśli opóźnienie nie jest krytyczne i nie chcesz uzyskiwać częściowych wyników.

    Wykrywanie i śledzenie wielu obiektów false (domyślnie) | true

    Określa, czy wykrywać i śledzić do 5 obiektów, czy tylko najbardziej widoczny obiekt (domyślnie).

    Klasyfikowanie obiektów false (domyślnie) | true

    Określa, czy wykrywane obiekty mają być klasyfikowane do ogólnych kategorii. Po włączeniu detektor obiektów klasyfikuje obiekty w następujące kategorie: odzież, żywność, artykuły domowe, miejsca, rośliny i nieznane.

    Interfejs API do wykrywania i śledzenia obiektów jest zoptymalizowany pod kątem tych 2 głównych przypadków użycia:

    • Wykrywanie i śledzenie w czasie rzeczywistym najbardziej widocznego obiektu w wizjerze kamery
    • wykrywanie wielu obiektów na podstawie obrazu stałego;

    Aby skonfigurować interfejs API na potrzeby tych zastosowań:

    Java

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

    Kotlin

    // Live detection and tracking
    val options = FirebaseVisionObjectDetectorOptions.Builder()
            .setDetectorMode(FirebaseVisionObjectDetectorOptions.STREAM_MODE)
            .enableClassification()  // Optional
            .build()
    
    // Multiple object detection in static images
    val options = FirebaseVisionObjectDetectorOptions.Builder()
            .setDetectorMode(FirebaseVisionObjectDetectorOptions.SINGLE_IMAGE_MODE)
            .enableMultipleObjects()
            .enableClassification()  // Optional
            .build()
    
  2. Pobieranie instancji FirebaseVisionObjectDetector:

    Java

    FirebaseVisionObjectDetector objectDetector =
            FirebaseVision.getInstance().getOnDeviceObjectDetector();
    
    // Or, to change the default settings:
    FirebaseVisionObjectDetector objectDetector =
            FirebaseVision.getInstance().getOnDeviceObjectDetector(options);
    

    Kotlin

    val objectDetector = FirebaseVision.getInstance().getOnDeviceObjectDetector()
    
    // Or, to change the default settings:
    val objectDetector = FirebaseVision.getInstance().getOnDeviceObjectDetector(options)
    

2. Uruchamianie detektora obiektów

Aby wykrywać i śledzić obiekty, przekaż obrazy do metody FirebaseVisionObjectDetector instancji processImage().

W przypadku każdego klatki filmu lub obrazu w sekwencji wykonaj te czynności:

  1. Utwórz obiekt FirebaseVisionImage na podstawie obrazu.

    • Aby utworzyć obiekt FirebaseVisionImage na podstawie obiektu media.Image, na przykład podczas robienia zdjęcia aparatem urządzenia, przekaż obiekt media.Image i obrót obrazu do obiektu FirebaseVisionImage.fromMediaImage().

      Jeśli używasz biblioteki CameraX, klasy OnImageCapturedListener i ImageAnalysis.Analyzer obliczają wartość rotacji za Ciebie, więc przed wywołaniem funkcji FirebaseVisionImage.fromMediaImage() musisz tylko przekonwertować rotację na jedną z konstant ROTATION_ w ML Kit:

      Java

      private class YourAnalyzer implements ImageAnalysis.Analyzer {
      
          private int degreesToFirebaseRotation(int degrees) {
              switch (degrees) {
                  case 0:
                      return FirebaseVisionImageMetadata.ROTATION_0;
                  case 90:
                      return FirebaseVisionImageMetadata.ROTATION_90;
                  case 180:
                      return FirebaseVisionImageMetadata.ROTATION_180;
                  case 270:
                      return FirebaseVisionImageMetadata.ROTATION_270;
                  default:
                      throw new IllegalArgumentException(
                              "Rotation must be 0, 90, 180, or 270.");
              }
          }
      
          @Override
          public void analyze(ImageProxy imageProxy, int degrees) {
              if (imageProxy == null || imageProxy.getImage() == null) {
                  return;
              }
              Image mediaImage = imageProxy.getImage();
              int rotation = degreesToFirebaseRotation(degrees);
              FirebaseVisionImage image =
                      FirebaseVisionImage.fromMediaImage(mediaImage, rotation);
              // Pass image to an ML Kit Vision API
              // ...
          }
      }

      Kotlin

      private class YourImageAnalyzer : ImageAnalysis.Analyzer {
          private fun degreesToFirebaseRotation(degrees: Int): Int = when(degrees) {
              0 -> FirebaseVisionImageMetadata.ROTATION_0
              90 -> FirebaseVisionImageMetadata.ROTATION_90
              180 -> FirebaseVisionImageMetadata.ROTATION_180
              270 -> FirebaseVisionImageMetadata.ROTATION_270
              else -> throw Exception("Rotation must be 0, 90, 180, or 270.")
          }
      
          override fun analyze(imageProxy: ImageProxy?, degrees: Int) {
              val mediaImage = imageProxy?.image
              val imageRotation = degreesToFirebaseRotation(degrees)
              if (mediaImage != null) {
                  val image = FirebaseVisionImage.fromMediaImage(mediaImage, imageRotation)
                  // Pass image to an ML Kit Vision API
                  // ...
              }
          }
      }

      Jeśli nie używasz biblioteki aparatu, która zapewnia obrócenie obrazu, możesz obliczyć je na podstawie obrotu urządzenia i orientacji czujnika aparatu na urządzeniu:

      Java

      private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
      static {
          ORIENTATIONS.append(Surface.ROTATION_0, 90);
          ORIENTATIONS.append(Surface.ROTATION_90, 0);
          ORIENTATIONS.append(Surface.ROTATION_180, 270);
          ORIENTATIONS.append(Surface.ROTATION_270, 180);
      }
      
      /**
       * 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, Context context)
              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);
      
          // On most devices, the sensor orientation is 90 degrees, but for some
          // devices it is 270 degrees. For devices with a sensor orientation of
          // 270, rotate the image an additional 180 ((270 + 270) % 360) degrees.
          CameraManager cameraManager = (CameraManager) context.getSystemService(CAMERA_SERVICE);
          int sensorOrientation = cameraManager
                  .getCameraCharacteristics(cameraId)
                  .get(CameraCharacteristics.SENSOR_ORIENTATION);
          rotationCompensation = (rotationCompensation + sensorOrientation + 270) % 360;
      
          // Return the corresponding FirebaseVisionImageMetadata rotation value.
          int result;
          switch (rotationCompensation) {
              case 0:
                  result = FirebaseVisionImageMetadata.ROTATION_0;
                  break;
              case 90:
                  result = FirebaseVisionImageMetadata.ROTATION_90;
                  break;
              case 180:
                  result = FirebaseVisionImageMetadata.ROTATION_180;
                  break;
              case 270:
                  result = FirebaseVisionImageMetadata.ROTATION_270;
                  break;
              default:
                  result = FirebaseVisionImageMetadata.ROTATION_0;
                  Log.e(TAG, "Bad rotation value: " + rotationCompensation);
          }
          return result;
      }

      Kotlin

      private val ORIENTATIONS = SparseIntArray()
      
      init {
          ORIENTATIONS.append(Surface.ROTATION_0, 90)
          ORIENTATIONS.append(Surface.ROTATION_90, 0)
          ORIENTATIONS.append(Surface.ROTATION_180, 270)
          ORIENTATIONS.append(Surface.ROTATION_270, 180)
      }
      /**
       * 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, context: Context): 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)
      
          // On most devices, the sensor orientation is 90 degrees, but for some
          // devices it is 270 degrees. For devices with a sensor orientation of
          // 270, rotate the image an additional 180 ((270 + 270) % 360) degrees.
          val cameraManager = context.getSystemService(CAMERA_SERVICE) as CameraManager
          val sensorOrientation = cameraManager
                  .getCameraCharacteristics(cameraId)
                  .get(CameraCharacteristics.SENSOR_ORIENTATION)!!
          rotationCompensation = (rotationCompensation + sensorOrientation + 270) % 360
      
          // Return the corresponding FirebaseVisionImageMetadata rotation value.
          val result: Int
          when (rotationCompensation) {
              0 -> result = FirebaseVisionImageMetadata.ROTATION_0
              90 -> result = FirebaseVisionImageMetadata.ROTATION_90
              180 -> result = FirebaseVisionImageMetadata.ROTATION_180
              270 -> result = FirebaseVisionImageMetadata.ROTATION_270
              else -> {
                  result = FirebaseVisionImageMetadata.ROTATION_0
                  Log.e(TAG, "Bad rotation value: $rotationCompensation")
              }
          }
          return result
      }

      Następnie prześlij obiekt media.Image i wartość obrotu do funkcji FirebaseVisionImage.fromMediaImage():

      Java

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

      Kotlin

      val image = FirebaseVisionImage.fromMediaImage(mediaImage, rotation)
    • Aby utworzyć obiekt FirebaseVisionImage z identyfikatora URI pliku, prześlij kontekst aplikacji i identyfikator URI pliku do funkcji FirebaseVisionImage.fromFilePath(). Jest to przydatne, gdy używasz intencji ACTION_GET_CONTENT, aby poprosić użytkownika o wybranie obrazu z aplikacji Galeria.

      Java

      FirebaseVisionImage image;
      try {
          image = FirebaseVisionImage.fromFilePath(context, uri);
      } catch (IOException e) {
          e.printStackTrace();
      }

      Kotlin

      val image: FirebaseVisionImage
      try {
          image = FirebaseVisionImage.fromFilePath(context, uri)
      } catch (e: IOException) {
          e.printStackTrace()
      }
    • Aby utworzyć obiekt FirebaseVisionImageByteBuffer lub tablicy bajtów, najpierw oblicz obrót obrazu w sposób opisany powyżej w przypadku wejścia media.Image.

      Następnie utwórz obiekt FirebaseVisionImageMetadata, który zawiera wysokość, szerokość, format kodowania kolorów oraz obrót obrazu:

      Java

      FirebaseVisionImageMetadata metadata = new FirebaseVisionImageMetadata.Builder()
              .setWidth(480)   // 480x360 is typically sufficient for
              .setHeight(360)  // image recognition
              .setFormat(FirebaseVisionImageMetadata.IMAGE_FORMAT_NV21)
              .setRotation(rotation)
              .build();

      Kotlin

      val metadata = FirebaseVisionImageMetadata.Builder()
              .setWidth(480) // 480x360 is typically sufficient for
              .setHeight(360) // image recognition
              .setFormat(FirebaseVisionImageMetadata.IMAGE_FORMAT_NV21)
              .setRotation(rotation)
              .build()

      Użyj bufora lub tablicy oraz obiektu metadanych, aby utworzyć obiekt FirebaseVisionImage:

      Java

      FirebaseVisionImage image = FirebaseVisionImage.fromByteBuffer(buffer, metadata);
      // Or: FirebaseVisionImage image = FirebaseVisionImage.fromByteArray(byteArray, metadata);

      Kotlin

      val image = FirebaseVisionImage.fromByteBuffer(buffer, metadata)
      // Or: val image = FirebaseVisionImage.fromByteArray(byteArray, metadata)
    • Aby utworzyć obiekt FirebaseVisionImage z obiektu Bitmap:

      Java

      FirebaseVisionImage image = FirebaseVisionImage.fromBitmap(bitmap);

      Kotlin

      val image = FirebaseVisionImage.fromBitmap(bitmap)
      Obraz reprezentowany przez obiekt Bitmap musi być pionowy i nie wymagać dodatkowego obracania.
  2. Przekaż obraz do metody processImage():

    Java

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

    Kotlin

    objectDetector.processImage(image)
            .addOnSuccessListener { detectedObjects ->
                // Task completed successfully
                // ...
            }
            .addOnFailureListener { e ->
                // Task failed with an exception
                // ...
            }
    
  3. Jeśli wywołanie funkcji processImage() zakończy się powodzeniem, lista FirebaseVisionObject zostanie przekazana do odbiornika sukcesu.

    Każdy element FirebaseVisionObject zawiera te właściwości:

    Ramka ograniczająca Rect wskazujący pozycję obiektu na obrazie.
    Identyfikator śledzenia Całka, która identyfikuje obiekt na różnych obrazach. Wartość null w SINGLE_IMAGE_MODE.
    Kategoria Ogólna kategoria obiektu. Jeśli detektor obiektów nie ma włączonej klasyfikacji, zawsze będzie to wartośćFirebaseVisionObject.CATEGORY_UNKNOWN.
    Poziom ufności Wartość ufności klasyfikacji obiektu. Jeśli detektor obiektów nie ma włączonej klasyfikacji lub obiekt został zaklasyfikowany jako nieznany, wyświetli się ikona null.

    Java

    // The list of detected objects contains one item if multiple object detection wasn't enabled.
    for (FirebaseVisionObject obj : detectedObjects) {
        Integer id = obj.getTrackingId();
        Rect bounds = obj.getBoundingBox();
    
        // If classification was enabled:
        int category = obj.getClassificationCategory();
        Float confidence = obj.getClassificationConfidence();
    }
    

    Kotlin

    // The list of detected objects contains one item if multiple object detection wasn't enabled.
    for (obj in detectedObjects) {
        val id = obj.trackingId       // A number that identifies the object across images
        val bounds = obj.boundingBox  // The object's position in the image
    
        // If classification was enabled:
        val category = obj.classificationCategory
        val confidence = obj.classificationConfidence
    }
    

poprawa użyteczności i wydajności;

Aby zapewnić użytkownikom jak najlepsze wrażenia, postępuj w swojej aplikacji zgodnie z tymi wytycznymi:

  • Skuteczne wykrywanie obiektów zależy od ich złożoności wizualnej. Obiekty z niewielką liczbą cech wizualnych mogą wymagać większej części obrazu, aby mogły zostać wykryte. Należy poinformować użytkowników o tym, jak rejestrować dane wejściowe, które dobrze współpracują z rodzajem obiektów, które chcesz wykrywać.
  • Jeśli podczas korzystania z klasyfikacji chcesz wykrywać obiekty, które nie pasują do obsługiwanych kategorii, zastosuj specjalne przetwarzanie dla nieznanych obiektów.

Zapoznaj się też z [aplikacją ML Kit Material Design Showcase][showcase-link]{: .external } i kolekcją wzorców Material Design do tworzenia funkcji opartych na uczeniu maszynowym.

Aby uzyskać najlepszą liczbę klatek na sekundę, stosuj te wskazówki podczas korzystania z trybu strumieniowego w aplikacji działającej w czasie rzeczywistym:

  • Nie używaj wykrywania wielu obiektów w trybie strumieniowego przesyłania danych, ponieważ większość urządzeń nie będzie w stanie uzyskać odpowiedniej liczby klatek na sekundę.

  • Wyłącz klasyfikację, jeśli jej nie potrzebujesz.

  • ograniczać wywołania do tego detektora. Jeśli podczas działania detektora pojawi się nowa klatka wideo, odrzuć ją.
  • Jeśli używasz danych wyjściowych z detektora do nakładania grafiki na obraz wejściowy, najpierw uzyskaj wynik z ML Kit, a potem wyrenderuj obraz i nałóż go w jednym kroku. W ten sposób renderujesz na powierzchni wyświetlacza tylko raz w przypadku każdej ramki wejściowej.
  • Jeśli używasz interfejsu Camera2 API, rób zdjęcia w formacie ImageFormat.YUV_420_888.

    Jeśli używasz starszej wersji interfejsu Camera API, rób zdjęcia w formacie ImageFormat.NV21.