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

Za pomocą ML Kit możesz wykrywać i śledzić obiekty w klatkach filmu.

Gdy przekażesz obrazy do ML Kit, ML Kit zwróci dla każdego obrazu listę maksymalnie 5 wykrytych obiektów i ich położenie na obrazie. Podczas wykrywania obiektów w strumieniach wideo każdy obiekt ma identyfikator, którego możesz używać do śledzenia obiektu na różnych obrazach. Możesz też opcjonalnie włączyć klasyfikację obiektów zgrubną, która przypisuje do obiektów etykiety z ogólnymi opisami kategorii.

Zanim zaczniesz

  1. Jeśli nie korzystasz jeszcze z Firebase, dodaj tę usługę do projektu aplikacji na Androida.
  2. Dodaj zależności dla 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ę klasy FirebaseVisionObjectDetector, opcjonalnie określając ustawienia detektora, które chcesz zmienić w stosunku do domyślnych.

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

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

    W przypadku ustawienia STREAM_MODE (domyślnego) detektor obiektów działa z niskim opóźnieniem, ale podczas pierwszych kilku wywołań może zwracać niepełne wyniki (np. nieokreślone ramki ograniczające lub etykiety kategorii). W STREAM_MODE detektor przypisuje też identyfikatory śledzenia do obiektów, których możesz używać do śledzenia obiektów w klatkach. Używaj tego trybu, gdy chcesz śledzić obiekty lub gdy ważny jest krótki czas oczekiwania, np. podczas przetwarzania strumieni wideo w czasie rzeczywistym.

    SINGLE_IMAGE_MODE detektor obiektów czeka, aż pole ograniczające wykrytego obiektu i (jeśli włączysz klasyfikację) etykieta kategorii będą dostępne, zanim zwróci wynik. W konsekwencji czas oczekiwania na wykrycie może być dłuższy. Poza tym w SINGLE_IMAGE_MODE nie są przypisywane identyfikatory śledzenia. Użyj tego trybu, jeśli opóźnienie nie jest krytyczne i nie chcesz mieć do czynienia z częściowymi wynikami.

    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 wykryte obiekty mają być klasyfikowane w kategoriach ogólnych. Gdy ta opcja jest włączona, detektor obiektów klasyfikuje obiekty w tych kategoriach: odzież, żywność, artykuły gospodarstwa domowego, miejsca, rośliny i nieznane.

    Interfejs API do wykrywania i śledzenia obiektów jest zoptymalizowany pod kątem tych 2 głównych zastosowań:

    • Wykrywanie i śledzenie na żywo najważniejszego obiektu w wizjerze aparatu
    • Wykrywanie wielu obiektów na obrazie statycznym

    Aby skonfigurować interfejs API w tych przypadkach użycia:

    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. Uzyskaj instancję 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 FirebaseVisionObjectDetectorinstancji processImage().

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

  1. Utwórz obiekt FirebaseVisionImage na podstawie obrazu.

    • Aby utworzyć obiekt FirebaseVisionImage z obiektu media.Image, np. podczas przechwytywania obrazu z aparatu urządzenia, przekaż obiekt media.Image i obrót obrazu do FirebaseVisionImage.fromMediaImage().

      Jeśli używasz biblioteki CameraX, klasy OnImageCapturedListenerImageAnalysis.Analyzer obliczają wartość rotacji, więc wystarczy przekonwertować rotację na jedną ze stałych ROTATION_ ML Kit przed wywołaniem FirebaseVisionImage.fromMediaImage():

      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 podaje rotację obrazu, możesz obliczyć ją na podstawie rotacji urządzenia i orientacji czujnika aparatu w 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 przekaż obiekt media.Image i wartość obrotu do FirebaseVisionImage.fromMediaImage():

      Java

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

      Kotlin

      val image = FirebaseVisionImage.fromMediaImage(mediaImage, rotation)
    • Aby utworzyć obiekt FirebaseVisionImage z identyfikatora URI pliku, przekaż 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 galerii.

      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 FirebaseVisionImage z obiektu ByteBuffer lub tablicy bajtów, najpierw oblicz obrót obrazu zgodnie z opisem powyżej dla danych wejściowych media.Image.

      Następnie utwórz FirebaseVisionImageMetadata obiekt, który zawiera wysokość, szerokość, format kodowania kolorów i rotację 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ć w pozycji pionowej 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() się powiedzie, do odbiorcy sukcesu zostanie przekazana lista obiektów FirebaseVisionObject.

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

    Ramka ograniczająca Rect wskazujący pozycję obiektu na obrazie.
    Identyfikator śledzenia Liczba całkowita, która identyfikuje obiekt na obrazach. Wartość null w przypadku SINGLE_IMAGE_MODE.
    Kategoria Ogólna kategoria obiektu. Jeśli wykrywanie obiektów nie ma włączonej klasyfikacji, ta wartość jest zawsze FirebaseVisionObject.CATEGORY_UNKNOWN.
    Poziom ufności Wartość ufności klasyfikacji obiektu. Jeśli detektor obiektów nie ma włączonej klasyfikacji lub obiekt jest sklasyfikowany jako nieznany, wartość ta wynosi 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
    }
    

Ulepszanie łatwości obsługi i wydajności

Aby zapewnić użytkownikom jak największy komfort, postępuj w 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ć zajmowania większej części obrazu, aby można było je wykryć. Udziel użytkownikom wskazówek dotyczących rejestrowania danych wejściowych, które dobrze sprawdzają się w przypadku typów obiektów, jakie chcesz wykrywać.
  • Jeśli używasz klasyfikacji i chcesz wykrywać obiekty, które nie pasują do obsługiwanych kategorii, zaimplementuj specjalną obsługę nieznanych obiektów.

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

Jeśli używasz trybu przesyłania strumieniowego w aplikacji działającej w czasie rzeczywistym, postępuj zgodnie z tymi wskazówkami, aby uzyskać najlepszą liczbę klatek na sekundę:

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

  • Jeśli nie potrzebujesz klasyfikacji, wyłącz ją.

  • Ograniczanie liczby połączeń z wykrywaczem. Jeśli podczas działania detektora stanie się dostępna nowa klatka wideo, odrzuć ją.
  • Jeśli używasz danych wyjściowych detektora do nakładania grafiki na obraz wejściowy, najpierw uzyskaj wynik z ML Kit, a następnie w jednym kroku wyrenderuj obraz i nałóż na niego grafikę. Dzięki temu renderowanie na powierzchnię wyświetlania odbywa się tylko raz dla 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 starszego interfejsu Camera API, rób zdjęcia w formacie ImageFormat.NV21.