Instructivo de TF Lite en Edge para iOS

Qué compilarás

En este instructivo, descargarás un modelo personalizado de TensorFlow Lite exportado desde AutoML Vision Edge. Luego, ejecutarás una app para iOS prediseñada que usa el modelo a fin de detectar múltiples objetos dentro de una imagen (con cuadros de límite) y proporcionar un etiquetado personalizado de categorías de objetos.

Captura de pantalla de un dispositivo móvil del producto final

Objetivos

En esta introducción detallada, usarás el código para lo siguiente:

  • Ejecutar un modelo de Edge de la detección de objetos de AutoML Vision en una app para iOS mediante el intérprete de TF Lite

Antes de comenzar

Clona el repositorio de Git

Mediante la línea de comandos, clona el repositorio de Git con el siguiente comando:

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

Navega al directorio ios del clon local del repositorio (examples/lite/examples/object_detection/ios/). Debes ejecutar todas las siguientes muestras de código desde el directorio ios:

cd examples/lite/examples/object_detection/ios

Requisitos previos

  • Git debe estar instalado.
  • Versiones de iOS compatibles: iOS 12.0 y versiones posteriores.

Configura la app para iOS

Genera y abre el archivo del espacio de trabajo

Si deseas comenzar a configurar la app para iOS original, primero debes generar el archivo del espacio de trabajo mediante el software necesario:

  1. Navega a la carpeta ios si aún no lo hiciste:

    cd examples/lite/examples/object_detection/ios
  2. Instala el pod para generar el archivo del espacio de trabajo:

    pod install

    Si ya instalaste este pod antes, usa el siguiente comando:

    pod update
  3. Después de haber generado el archivo del espacio de trabajo, puedes abrir el proyecto con Xcode. Para abrir el proyecto a través de la línea de comandos, ejecuta el siguiente comando desde el directorio ios:

    open ./ObjectDetection.xcworkspace

Crea un identificador único y compila la app

Con el ObjectDetection.xcworkspace abierto en Xcode, primero debes cambiar el identificador del paquete (ID del paquete) a un valor único.

  1. Selecciona el elemento de proyecto ObjectDetection de arriba en el navegador de proyectos que se encuentra a la izquierda.

    Imagen del proyecto ObjectDetection en el navegador lateral

  2. Asegúrate de tener seleccionado Destinos > ObjectDetection (Targets > ObjectDetection).

    Imagen de las opciones de Destinos (Targets) seleccionados

  3. En la sección General > Identity (General > Identidad), cambia el campo del identificador del paquete a un valor único. El estilo preferido es el de notación de nombre de dominio inverso.

    Imagen del ejemplo del ID del paquete en la sección Identidad

  4. En la sección General > Signing (General > Firma), que se encuentra debajo de Identity (Identidad), especifica un Team (Equipo) en el menú desplegable. Este valor lo proporciona tu ID de desarrollador.

    Imagen para elegir un equipo del menú desplegable

  5. Conecta un dispositivo iOS a tu computadora. Después de que se detecte el dispositivo, selecciónalo de la lista de dispositivos.

    Dispositivo predeterminado seleccionado

    Selecciona tu dispositivo conectado

  6. Después de especificar todos los cambios de configuración, compila la app en Xcode mediante el siguiente comando: Comando + B.

Ejecuta la app original

La app de muestra es una app de cámara que detecta de manera continua los objetos (cuadros de límite y etiquetas) en los marcos que ve la cámara trasera de tu dispositivo, mediante un modelo entrenado y cuantizado de SSD de MobileNet en un conjunto de datos de COCO.

Estas instrucciones te permitirán compilar y ejecutar la demostración en un dispositivo iOS.

Los archivos del modelo se descargan mediante secuencias de comandos en Xcode cuando compilas y ejecutas. No debes seguir ningún paso para descargar los modelos de TF Lite en el proyecto de forma explícita.

Antes de insertar tu modelo personalizado, prueba la versión de modelo de referencia de la app que usa el modelo base entrenado de “mobilenet”.

  1. Para iniciar la app en el simulador, selecciona el botón de reproducir Ícono de reproducción de Xcode en la esquina superior izquierda de la ventana de Xcode.

    Imagen del mensaje para permitir acceso a la cámara

  2. Luego de que hayas permitido que la app acceda a tu cámara con el botón Permitir, la app iniciará la detección y la anotación en vivo. Los objetos se detectarán y se marcarán con un cuadro de límite y una etiqueta en cada marco de la cámara.

  3. Mueve el dispositivo hacia diferentes objetos de tu entorno y verifica que la app detecte las imágenes de forma correcta.

    Captura de pantalla de un dispositivo móvil del producto final

Ejecuta la app personalizada

Modifica la app para que use el modelo que se volvió a entrenar con categorías de imágenes de objetos personalizadas.

Agrega los archivos del modelo al proyecto

El proyecto de demostración está configurado para buscar dos archivos en el directorio ios/objectDetection/model:

  • detect.tflite
  • labelmap.txt

Para reemplazar esos dos archivos con las versiones personalizadas, ejecuta el siguiente comando:

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

Ejecuta tu app

Para reiniciar la app en tu dispositivo iOS, selecciona el botón de reproducir Ícono de reproducción de Xcode que se encuentra en la esquina superior izquierda de la ventana de Xcode.

Para probar las modificaciones, mueve la cámara de tu dispositivo a distintos objetos a fin de ver las predicciones en vivo.

Los resultados deberían ser similares a esto:

Detección de objetos de la app personalizada con artículos de cocina

¿Cómo funciona?

Ahora que la app está en ejecución, observa el código específico de TensorFlow Lite.

Pod de TensorFlowLite

Esta app usa un CocoaPod de TFLite ya compilado. El Podfile incluye el CocoaPod en el proyecto:

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

El código que interactúa con TF Lite está incluido en el archivo ModelDataHandler.swift. Esta clase controla todos los procesamientos previos de datos y realiza llamadas para ejecutar inferencias en un marco determinado mediante la invocación del Interpreter. Luego, da formato a las inferencias obtenidas y muestra los N resultados principales de una inferencia correcta.

Explora el código

ModelDataHandler.swift

Las declaraciones de propiedad son el primer bloque de interés (después de las importaciones necesarias). Los parámetros inputShape del modelo de TF Lite (batchSize, inputChannels, inputWidth y inputHeight) se pueden encontrar en tflite_metadata.json. Tendrás este archivo cuando exportes el modelo de TF Lite. Para obtener más información, consulta la guía práctica Exporta modelos de Edge.

El ejemplo de tflite_metadata.json es similar al siguiente código:

{
    "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"
    ]
}
...

Parámetros del modelo:

Reemplaza los siguientes valores según el archivo tflite_metadata.json de tu modelo.

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

El método init, que crea el Interpreter con la ruta de acceso de Model y InterpreterOptions, asigna memoria para la entrada del modelo.

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

El método runModel realiza las siguientes acciones:

  1. Ajusta la escala de la imagen de entrada según la relación de aspecto para la que se entrena el modelo.
  2. Quita el componente Alfa del búfer de imágenes para obtener los datos RGB.
  3. Copia los datos RGB en el tensor de entrada.
  4. Ejecuta la inferencia mediante la invocación de Interpreter.
  5. Obtiene el resultado del intérprete.
  6. Da formato al resultado.
func runModel(onFrame pixelBuffer: CVPixelBuffer) -> Result? {

Recorta la imagen en el cuadrado más grande del centro y la ajusta según las dimensiones del modelo:

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)

Quita el componente Alfa del búfer de imágenes para obtener los datos RGB.

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
}

Copia los datos RGB en el Tensor de entrada:

try interpreter.copy(rgbData, toInputAt: 0)

Ejecuta la inferencia mediante la invocación de Interpreter:

    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)
  }

Da formato a los resultados:

    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)
    )
...
}

Filtra todos los resultados con una puntuación de confianza menor que el umbral y muestra los N resultados principales en orden descendente:

  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]

Filtra los resultados con una confianza menor que el umbral:

      guard score >= threshold else {
        continue
      }

Obtiene los nombres de las clases de salida para las clases detectadas en la lista de etiquetas:

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

      var rect: CGRect = CGRect.zero

Traduce el cuadro de límite detectado a 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

Las esquinas detectadas corresponden a las dimensiones del modelo. Por lo tanto, ajustamos la escala de rect según las dimensiones reales de la imagen.

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

Obtiene el color asignado para la clase:

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 administra todas las funciones relacionadas con la cámara.

  1. Inicializa y configura AVCaptureSession:

    private func configureSession() {
    session.beginConfiguration()
  2. Luego, intenta agregar un AVCaptureDeviceInput y agrega builtInWideAngleCamera como entrada de dispositivo para Session.

    addVideoDeviceInput()

    Después intenta agregar un AVCaptureVideoDataOutput:

    addVideoDataOutput()

       session.commitConfiguration()
        self.cameraConfiguration = .success
      }
  3. Inicia la sesión.

  4. Detiene la sesión.

  5. Agrega y quita las notificaciones AVCaptureSession.

Manejo de errores:

  • NSNotification.Name.AVCaptureSessionRuntimeError: Esto se publica cuando se produce un error inesperado mientras se ejecuta la instancia AVCaptureSession. El diccionario userInfo contiene un objeto NSError para la clave AVCaptureSessionErrorKey.
  • NSNotification.Name.AVCaptureSessionWasInterrupted: esto se publica cuando se produce una interrupción (p. ej., una llamada telefónica, una alarma, etcétera). Cuando corresponda, la instancia AVCaptureSession dejará de ejecutarse de forma automática ante la interrupción de la respuesta. El diccionario userInfo contiene AVCaptureSessionInterruptionReasonKey que indica el motivo de la interrupción.
  • NSNotification.Name.AVCaptureSessionInterruptionEnded: esto se publica cuando AVCaptureSession finaliza la interrupción. La instancia de sesión puede reanudarse una vez que termine la interrupción, como una llamada telefónica.

La clase InferenceViewController.swift se encarga de que aparezca la siguiente pantalla, en la que deberíamos enfocarnos en la parte destacada.

  • Resolution (Resolución): Muestra la resolución del marco actual (imagen de la sesión de video).
  • Recortar (Crop): Muestra el tamaño de recorte del marco actual.
  • Tiempo de inferencia (Inference Time): Muestra cuánto tiempo tarda el modelo en detectar el objeto.
  • Threads (Subprocesos): Muestra la cantidad de subprocesos que están en ejecución. El usuario puede presionar los signos + o - para aumentar o disminuir. El recuento de subprocesos actual que usa el intérprete de TensorFlow Lite.

Imagen de ajuste de subprocesos

La clase ViewController.swift contiene la instancia de CameraFeedManager, que administra las funciones relacionadas con la cámara y ModelDataHandler. ModelDataHandler maneja el Model (modelo entrenado) y obtiene el resultado para el marco de la imagen de la sesión de video.

private lazy var cameraFeedManager = CameraFeedManager(previewView: previewView)

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

Inicia la sesión de la cámara con una llamada al siguiente método:

cameraFeedManager.checkCameraConfigurationAndStartSession()

Cuando cambias el recuento de subprocesos, esta clase vuelve a inicializar el modelo con un nuevo recuento de subprocesos en la función didChangeThreadCount.

La clase CameraFeedManager enviará ImageFrame como CVPixelBuffer a ViewController, que se enviará al modelo para la predicción.

Este método ejecuta pixelBuffer de la cámara en vivo a través de TensorFlow para obtener el resultado.

@objc
func runModel(onPixelBuffer pixelBuffer: CVPixelBuffer) {

Ejecuta el pixelBuffer de la cámara en vivo a través de TensorFlow para obtener el resultado:

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

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

    DispatchQueue.main.async {

Muestra los resultados mediante la transferencia de InferenceViewController:

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

self.inferenceViewController?.inferenceTime = inferenceTime

Dibuja los cuadros de límite y muestra los nombres de las clases y las puntuaciones de confianza:

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

Próximos pasos

Ya completaste un instructivo de una app de anotación y detección de objetos para iOS mediante un modelo de Edge. Usaste un modelo entrenado de Edge de Tensorflow Lite para probar una app de detección de objetos antes de modificarla y obtener anotaciones de muestra. Luego, examinaste el código específico de TensorFlow Lite para comprender la funcionalidad subyacente.

Los siguientes recursos pueden ayudarte a seguir aprendiendo sobre los modelos de TensorFlow y AutoML Vision Edge: