Edge TF Lite iOS-Anleitung

Umfang

In dieser Anleitung laden Sie ein exportiertes, benutzerdefiniertes TensorFlow Lite-Modell von AutoML Vision Edge herunter. Anschließend führen Sie eine vorab entwickelte iOS-App aus, die mithilfe des Modells mehrere Objekte in einem Bild (mit Begrenzungsrahmen) erkennt und die Objektkategorien mit einem benutzerdefinierten Label versieht.

Screenshot des Endprodukts auf Mobilgerät

Ziele

In dieser einführenden Schritt-für-Schritt-Anleitung verwenden Sie Code für die folgende Aufgabe:

  • In einer iOS-App mit dem TF Lite-Interpreter ein AutoML Vision Object Detection Edge-Modell ausführen

Hinweis

Git-Repository klonen

Klonen Sie das Git-Repository mit dem folgenden Befehl in der Befehlszeile:

git clone https://github.com/tensorflow/examples.git

Wechseln Sie zum ios-Verzeichnis des lokalen Klons des Repositorys (examples/lite/examples/object_detection/ios/). Sie führen alle folgenden Codebeispiele aus dem ios-Verzeichnis aus:

cd examples/lite/examples/object_detection/ios

Vorbereitung

  • Git muss installiert sein.
  • Unterstützte iOS-Versionen: iOS 12.0 und höher.

iOS-App einrichten

Arbeitsbereichsdatei erstellen und öffnen

Zu Beginn der Einrichtung der ursprünglichen iOS-App müssen Sie zuerst die Arbeitsbereichsdatei mit der entsprechenden Software erstellen:

  1. Rufen Sie den Ordner ios auf, falls Sie dies noch nicht getan haben:

    cd examples/lite/examples/object_detection/ios
  2. Installieren Sie den Pod, um die Arbeitsbereichsdatei zu erstellen:

    pod install

    Wenn Sie diesen Pod zuvor installiert haben, verwenden Sie den folgenden Befehl:

    pod update
  3. Nachdem Sie die Arbeitsbereichsdatei erstellt haben, können Sie das Projekt mit Xcode öffnen. Führen Sie zum Öffnen des Projekts über die Befehlszeile den folgenden Befehl aus dem ios-Verzeichnis aus:

    open ./ObjectDetection.xcworkspace

Eindeutige ID und App erstellen

Bei geöffnetem ObjectDetection.xcworkspace in Xcode müssen Sie zuerst den Bundle Identifier (Paket-ID) in einen eindeutigen Wert ändern.

  1. Wählen Sie links in der Projektnavigation das oberste ObjectDetection-Projektelement aus.

    Bild des ObjectDetection-Projekts in der Seitennavigation

  2. Achten Sie darauf, dass Sie Targets > ObjectDetection (Ziele > ObjectDetection) ausgewählt haben.

    Bild der ausgewählten Zieloptionen

  3. Ändern Sie im Bereich General > Identity (Allgemein > Identität) das Feld "Bundle Identifier" (Paket-Identifikator) in einen eindeutigen Wert. Der bevorzugte Stil ist die Reverse Domain Name Notation.

    Bild mit Paket-ID-Beispiel im Bereich "Identity" (Identität)

  4. Geben Sie im Bereich General > Signing (Allgemein > Signieren) unter Identity (Identität) ein Team aus dem Drop-down-Menü an. Dieser Wert wird über Ihre Entwickler-ID bereitgestellt.

    Bild des Drop-down-Menüs zur Auswahl eines Teams

  5. Verbinden Sie ein iOS-Gerät mit Ihrem Computer. Wählen Sie das Gerät, nachdem es erkannt wurde, aus der Liste der Geräte aus.

    Standardgerät ausgewählt

    Verbundenes Gerät auswählen

  6. Erstellen Sie, nachdem Sie alle Konfigurationsänderungen angegeben haben, die App in Xcode mit dem folgenden Befehl: Befehl + B.

Ursprüngliche App ausführen

Bei der Beispiel-App handelt es sich um eine Kamera-App, die mithilfe eines quantisierten MobileNet-SSD-Modells, das mit dem COCO-Dataset trainiert wurde, kontinuierlich Objekte (Begrenzungsrahmen und Labels) in den von der Rückkamera des Geräts aus sichtbaren Frames erkennt.

In dieser Anleitung erfahren Sie, wie Sie die Demo auf einem iOS-Gerät erstellen und ausführen.

Die Modelldateien werden beim Erstellen und Ausführen über Skripts in Xcode heruntergeladen. Sie müssen keine Schritte ausführen, um TF Lite-Modelle explizit in das Projekt herunterzuladen.

Bevor Sie das benutzerdefinierte Modell einfügen, sollten Sie die Basisversion der App, die das trainierte Grundmodell "mobilenet" verwendet, testen.

  1. Wählen Sie zum Starten der App im Simulator links oben im Xcode-Fenster die Schaltfläche Play Xcode-Wiedergabesymbol (Wiedergabe) aus.

    Bild der Eingabeaufforderung: Zugriff auf Kamera zulassen

  2. Sobald Sie der App mit der Option Allow (Zulassen) Zugriff auf Ihre Kamera gewährt haben, startet die App die Live-Erkennung und -Annotation. Objekte werden erkannt und mit einem Begrenzungsrahmen sowie einem Label in jedem Kamera-Frame markiert.

  3. Bewegen Sie das Gerät zu verschiedenen Objekten in Ihrer Umgebung und prüfen Sie, ob die App Bilder korrekt erkennt.

    Screenshot des Endprodukts auf Mobilgerät

Angepasste App ausführen

Ändern Sie die App so, dass sie das neu trainierte Modell mit benutzerdefinierten Bildkategorien für Objekte verwendet.

Modelldateien zum Projekt hinzufügen

Das Demoprojekt ist so konfiguriert, dass im ios/objectDetection/model-Verzeichnis nach zwei Dateien gesucht wird:

  • detect.tflite
  • labelmap.txt

Führen Sie den folgenden Befehl aus, um diese beiden Dateien durch Ihre benutzerdefinierten Versionen zu ersetzen:

cp tf_files/optimized_graph.lite ios/objectDetection/model/detect.tflite
cp tf_files/retrained_labels.txt ios/objectDetection/model/labelmap.txt

App ausführen

Wählen Sie für den Neustart der App auf Ihrem iOS-Gerät die Schaltfläche Play Xcode-Wiedergabesymbol (Wiedergabe) links oben im Xcode-Fenster aus.

Zum Testen der Änderungen bewegen Sie die Kamera des Geräts auf eine Vielzahl von Objekten, damit Sie Live-Vorhersagen sehen.

Die Ergebnisse sollten in etwa so aussehen:

Objekterkennung von Küchengegenständen mit der angepassten App

Funktionsweise

Sehen Sie sich, nachdem Sie die App ausgeführt haben, den TensorFlow Lite-spezifischen Code an.

TensorFlowLite-Pod

Diese App verwendet einen vorkompilierten TFLite CocoaPod. Die Pod-Datei enthält CocoaPod im Projekt:

Podfile

# Uncomment the next line to define a global platform for your project
platform :ios, '12.0'

target 'ObjectDetection' do
  # Comment the next line if you're not using Swift and don't want to use dynamic frameworks
  use_frameworks!

  # Pods for ObjectDetection
  pod 'TensorFlowLiteSwift'

end

Die Code-Schnittstelle zu TFLite ist in der Datei ModelDataHandler.swift enthalten. Diese Klasse übernimmt die gesamte Datenvorverarbeitung und führt Aufrufe durch, um Inferenz für einen bestimmten Frame auszuführen. Dazu wird der Interpreter aufgerufen. Anschließend werden die erhaltenen Inferenzen formatiert und die besten N Ergebnisse für eine erfolgreiche Inferenz zurückgegeben.

Code erforschen

ModelDataHandler.swift

Der erste relevante Block (nach den erforderlichen Importen) sind Attributdeklarationen. Die inputShape-Parameter (batchSize, inputChannels, inputWidth, inputHeight) des TF Lite-Modells finden Sie in der tflite_metadata.json. Sie erhalten diese Datei beim Exportieren des TF Llite-Modells. Weitere Informationen finden Sie im Anleitungsthema Edge-Modelle exportieren.

Das Beispiel tflite_metadata.json ähnelt dem folgenden Code:

{
    "inferenceType": "QUANTIZED_UINT8",
    "inputShape": [
        1,   // This represents batch size
        512,  // This represents image width
        512,  // This represents image Height
        3  //This represents inputChannels
    ],
    "inputTensor": "normalized_input_image_tensor",
    "maxDetections": 20,  // This represents max number of boxes.
    "outputTensorRepresentation": [
        "bounding_boxes",
        "class_labels",
        "class_confidences",
        "num_of_boxes"
    ],
    "outputTensors": [
        "TFLite_Detection_PostProcess",
        "TFLite_Detection_PostProcess:1",
        "TFLite_Detection_PostProcess:2",
        "TFLite_Detection_PostProcess:3"
    ]
}
...

Modellparameter:

Ersetzen Sie in Ihrem Modell die folgenden Werte gemäß der tflite_metadata.json-Datei.

let batchSize = 1 //Number of images to get prediction, the model takes 1 image at a time
let inputChannels = 3 //The pixels of the image input represented in RGB values
let inputWidth = 300 //Width of the image
let inputHeight = 300 //Height of the image
...

init

Die init-Methode, wodurch der Interpreter mit dem Model-Pfad und InterpreterOptions erstellt wird, weist dann Speicher zur Modelleingabe zu.

init?(modelFileInfo: FileInfo, labelsFileInfo: FileInfo, threadCount: Int = 1) {
    let modelFilename = modelFileInfo.name

    // Construct the path to the model file.
    guard let modelPath = Bundle.main.path(forResource: modelFilename,ofType: modelFileInfo.extension)

    // Specify the options for the `Interpreter`.
   var options = InterpreterOptions()
    options.threadCount = threadCount
    do {
      // Create the `Interpreter`.
      interpreter = try Interpreter(modelPath: modelPath, options: options)
      // Allocate memory for the model's input  `Tensor`s.
      try interpreter.allocateTensors()
    }

    super.init()

    // Load the classes listed in the labels file.
    loadLabels(fileInfo: labelsFileInfo)
  }
…

runModel

Die folgende runModel-Methode:

  1. Skaliert das Eingabebild auf das Seitenverhältnis, für das das Modell trainiert wurde.
  2. Entfernt die Alpha-Komponente aus dem Bilderzwischenspeicher, um RGB-Daten abzurufen.
  3. Kopiert die RGB-Daten in den Eingabe-Tensor.
  4. Führt Inferenz durch Aufrufen des Interpreter aus.
  5. Ruft die Ausgabe vom Interpreter ab.
  6. Formatiert die Ausgabe.
func runModel(onFrame pixelBuffer: CVPixelBuffer) -> Result? {

Schneidet das Bild auf das größte Quadrat in der Mitte zu und skaliert es auf die Modellabmessungen herunter:

let scaledSize = CGSize(width: inputWidth, height: inputHeight)
guard let scaledPixelBuffer = pixelBuffer.resized(to: scaledSize) else
{
    return nil
}
...

do {
  let inputTensor = try interpreter.input(at: 0)

Entfernt die Alpha-Komponente aus dem Bilderzwischenspeicher, um RGB-Daten abzurufen:

guard let rgbData = rgbDataFromBuffer(
  scaledPixelBuffer,
  byteCount: batchSize * inputWidth * inputHeight * inputChannels,
  isModelQuantized: inputTensor.dataType == .uInt8
) else {
  print("Failed to convert the image buffer to RGB data.")
  return nil
}

Kopiert die RGB-Daten in den Eingabe-Tensor:

try interpreter.copy(rgbData, toInputAt: 0)

Führt Inferenz durch Aufrufen des Interpreter aus:

    let startDate = Date()
    try interpreter.invoke()
    interval = Date().timeIntervalSince(startDate) * 1000
    outputBoundingBox = try interpreter.output(at: 0)
    outputClasses = try interpreter.output(at: 1)
    outputScores = try interpreter.output(at: 2)
    outputCount = try interpreter.output(at: 3)
  }

Formatiert die Ergebnisse:

    let resultArray = formatResults(
      boundingBox: [Float](unsafeData: outputBoundingBox.data) ?? [],
      outputClasses: [Float](unsafeData: outputClasses.data) ?? [],
      outputScores: [Float](unsafeData: outputScores.data) ?? [],
      outputCount: Int(([Float](unsafeData: outputCount.data) ?? [0])[0]),
      width: CGFloat(imageWidth),
      height: CGFloat(imageHeight)
    )
...
}

Filtert alle Ergebnisse mit Konfidenzwert < Schwellenwert heraus und gibt die besten N Ergebnisse in absteigender Reihenfolge zurück:

  func formatResults(boundingBox: [Float], outputClasses: [Float],
  outputScores: [Float], outputCount: Int, width: CGFloat, height: CGFloat)
  -> [Inference]{
    var resultsArray: [Inference] = []
    for i in 0...outputCount - 1 {

      let score = outputScores[i]

Filtert die Ergebnisse mit Konfidenzwert < Schwellenwert heraus:

      guard score >= threshold else {
        continue
      }

Ruft die Ausgabeklassennamen für erkannte Klassen aus der Labelliste ab:

      let outputClassIndex = Int(outputClasses[i])
      let outputClass = labels[outputClassIndex + 1]

      var rect: CGRect = CGRect.zero

Übersetzt den erkannten Begrenzungsrahmen in CGRect.

      rect.origin.y = CGFloat(boundingBox[4*i])
      rect.origin.x = CGFloat(boundingBox[4*i+1])
      rect.size.height = CGFloat(boundingBox[4*i+2]) - rect.origin.y
      rect.size.width = CGFloat(boundingBox[4*i+3]) - rect.origin.x

Die erkannten Ecken beziehen sich auf Modellabmessungen. Wir skalieren rect also in Bezug auf die tatsächlichen Bildabmessungen.

let newRect = rect.applying(CGAffineTransform(scaleX: width, y: height))

Ruft die für die Klasse zugewiesene Farbe ab:

let colorToAssign = colorForClass(withIndex: outputClassIndex + 1)
      let inference = Inference(confidence: score,
                                className: outputClass,
                                rect: newRect,
                                displayColor: colorToAssign)
      resultsArray.append(inference)
    }

    // Sort results in descending order of confidence.
    resultsArray.sort { (first, second) -> Bool in
      return first.confidence  > second.confidence
    }

    return resultsArray
  }

CameraFeedManager

CameraFeedManager.swift verwaltet alle Kamerafunktionen.

  1. Initialisiert und konfiguriert AVCaptureSession:

    private func configureSession() {
    session.beginConfiguration()
  2. Anschließend wird versucht, AVCaptureDeviceInput hinzuzufügen, und builtInWideAngleCamera wird als Geräteeingabe für die Sitzung hinzugefügt.

    addVideoDeviceInput()

    Dann wird versucht, AVCaptureVideoDataOutput hinzuzufügen:

    addVideoDataOutput()

       session.commitConfiguration()
        self.cameraConfiguration = .success
      }
  3. Startet die Sitzung.

  4. Stoppt die Sitzung.

  5. AVCaptureSession-Benachrichtigungen werden hinzugefügt und entfernt.

Fehlerbehandlung:

  • NSNotification.Name.AVCaptureSessionRuntimeError: wird angezeigt, wenn ein unerwarteter Fehler auftritt, während die AVCaptureSession-Instanz ausgeführt wird. Das userInfo-Schlüsselverzeichnis enthält ein NSError-Objekt für Schlüssel AVCaptureSessionErrorKey.
  • NSNotification.Name.AVCaptureSessionWasInterrupted: Wird angezeigt, wenn eine Unterbrechung (z. B. Telefonanruf, Alarm usw.) auftritt. Bei Bedarf wird die Ausführung der AVCaptureSession-Instanz als Reaktion auf die Unterbrechung automatisch gestoppt. UserInfo enthält AVCaptureSessionInterruptionReasonKey und gibt den Grund für die Unterbrechung an.
  • NSNotification.Name.AVCaptureSessionInterruptionEnded: Wird angezeigt, wenn AVCaptureSession die Unterbrechung beendet. Die Sitzungsinstanz kann nach einer Unterbrechung, wenn beispielsweise ein Telefonanruf beendet wird, fortgesetzt werden.

Die Klasse InferenceViewController.swift ist für den unteren Bildschirmbereich zuständig, wobei unser Hauptaugenmerk auf dem hervorgehobenen Teil liegt.

  • Resolution (Auflösung): Zeigt die Auflösung des aktuellen Frames an (Bild aus Videositzung).
  • Crop (Zuschneiden): Zeigt die Zuschneidegröße des aktuellen Frames an.
  • Inference Time (Inferenzzeit): Zeigt an, wie viel Zeit das Modell benötigt, um das Objekt zu erkennen.
  • Threads: Zeigt an, wie viele Threads ausgeführt werden. Sie können diese Zahl erhöhen oder verringern. Tippen Sie dafür auf das Zeichen + oder - des Steppers. Die aktuelle Thread-Anzahl, die vom TensorFlow Lite-Interpreter verwendet wird.

Bild: Threads anpassen

Die Klasse ViewController.swift enthält die Instanz CameraFeedManager, die die Kamerafunktionen verwaltet, und ModelDataHandler. ModelDataHandler verarbeitet das Model (trainiertes Modell) und ruft die Ausgabe für den Bildframe der Videositzung ab.

private lazy var cameraFeedManager = CameraFeedManager(previewView: previewView)

private var modelDataHandler: ModelDataHandler? =
    ModelDataHandler(modelFileInfo: MobileNetSSD.modelInfo, labelsFileInfo: MobileNetSSD.labelsInfo)

Startet die Kamerasitzung durch Aufruf von:

cameraFeedManager.checkCameraConfigurationAndStartSession()

Wenn Sie die Anzahl der Threads ändern, initialisiert diese Klasse das Modell mit der neuen Thread-Anzahl in der didChangeThreadCount-Funktion neu.

Die Klasse CameraFeedManager sendet ImageFrame als CVPixelBuffer an ViewController, das zur Vorhersage an das Modell gesendet wird.

Bei dieser Methode wird der pixelBuffer der Live-Kamera über TensorFlow ausgeführt, um das Ergebnis abzurufen.

@objc
func runModel(onPixelBuffer pixelBuffer: CVPixelBuffer) {

Führt den pixelBuffer der Live-Kamera über TensorFlow aus, um das Ergebnis abzurufen:

result = self.modelDataHandler?.runModel(onFrame: pixelBuffer)
...
let displayResult = result

let width = CVPixelBufferGetWidth(pixelBuffer)
    let height = CVPixelBufferGetHeight(pixelBuffer)

    DispatchQueue.main.async {

Zeigt die Ergebnisse durch Übergabe an InferenceViewController an:

self.inferenceViewController?.resolution = CGSize(width: width, height: height)

self.inferenceViewController?.inferenceTime = inferenceTime

Zeichnet die Begrenzungsrahmen und zeigt Klassennamen und Konfidenzwerte an:

self.drawAfterPerformingCalculations(onInferences: displayResult.inferences, withImageSize: CGSize(width: CGFloat(width), height: CGFloat(height)))
    }
  }

Nächste Schritte

Sie sind nun am Ende der Schritt-für-Schritt-Anleitung angelangt, in der gezeigt wird, wie Sie eine iOS-App zur Objekterkennung und Annotation mit einem Edge-Modell nutzen. Sie haben ein trainiertes Edge TensorFlow Lite-Modell verwendet, um eine App zur Objekterkennung zu testen, bevor Sie Änderungen daran vorgenommen und Beispielannotationen erhalten haben. Anschließend haben Sie TensorFlow Lite-spezifischen Code untersucht, um die zugrunde liegende Funktionalität nachzuvollziehen.

Die folgenden Informationen helfen Ihnen dabei, sich weiter mit TensorFlow-Modellen und AutoML Vision Edge vertraut zu machen: