Reconocimiento de objetos en tiempo real en iOS usando YOLOv3


Hola a todos!

En este artículo escribiremos un pequeño programa para resolver el problema de detección y reconocimiento de objetos (detección de objetos) en tiempo real. El programa se escribirá en el lenguaje de programación Swift para la plataforma iOS. Para detectar objetos, utilizaremos una red neuronal convolucional con una arquitectura llamada YOLOv3. En el artículo, aprenderemos cómo trabajar en iOS con redes neuronales usando el marco CoreML, un poco de comprensión de qué es la red YOLOv3 y cómo usar y procesar los resultados de esta red. También verificaremos el funcionamiento del programa y compararemos varias variaciones de YOLOv3: YOLOv3-tiny y YOLOv3-416.

Las fuentes estarán disponibles al final del artículo, por lo que todos podrán probar el funcionamiento de la red neuronal en su dispositivo.

Detección de objetos


Para comenzar, comprenderemos brevemente cuál es la tarea de detectar objetos (detección de objetos) en la imagen y qué herramientas se utilizan para esto hoy. Entiendo que muchos están bastante familiarizados con este tema, pero aún me permito contar un poco al respecto.

Ahora, muchas de las tareas en el campo de la visión por computadora se resuelven con la ayuda de redes neuronales convolucionales (Redes neuronales convolucionales), en adelante CNN. Debido a su estructura, extraen características de la imagen. Las CNN se utilizan en clasificación, reconocimiento, segmentación y muchas otras.

Arquitecturas populares de CNN para el reconocimiento de objetos:

  • R-CNN. Podemos decir el primer modelo para resolver este problema. Funciona como un clasificador de imagen normal. Se alimentan diferentes regiones de la imagen a la entrada de red y se hacen predicciones para ellas. Muy lento ya que ejecuta una sola imagen varias miles de veces.
  • R-CNN rápido. Una versión mejorada y más rápida de R-CNN funciona con un principio similar, pero primero se alimenta toda la imagen a la entrada de CNN, luego se generan regiones a partir de la representación interna recibida. Pero sigue siendo bastante lento para tareas en tiempo real.
  • R-CNN más rápido. La principal diferencia con respecto a las anteriores es que, en lugar del algoritmo de búsqueda selectiva, utiliza una red neuronal para seleccionar regiones para "memorizarlas".
  • Yolo Un principio de funcionamiento completamente diferente en comparación con los anteriores no utiliza regiones en absoluto. El mas rapido. Se discutirán más detalles al respecto en el artículo.
  • SSD Es similar en principio a YOLO, pero usa VGG16 como red para extraer funciones. También bastante rápido y adecuado para el trabajo en tiempo real.
  • Feature Pyramid Networks (FPN). Otro tipo de red, como Single Shot Detector, debido a las características de extracción de características, es mejor que SSD reconoce objetos pequeños.
  • RetinaNet. Utiliza una combinación de FPN + ResNet y, gracias a una función de error especial (pérdida focal), proporciona una mayor precisión.

En este artículo, utilizaremos la arquitectura YOLO, es decir, su última modificación, YOLOv3.

¿Por qué YOLO?




YOLO o You Only Look Once es la arquitectura muy popular de CNN, que se utiliza para reconocer múltiples objetos en una imagen. Puede obtener información más completa al respecto en el sitio web oficial , allí también puede encontrar publicaciones que describen en detalle la teoría y el componente matemático de esta red, así como también se describe el proceso de su capacitación.

La característica principal de esta arquitectura en comparación con otras es que la mayoría de los sistemas aplican CNN varias veces a diferentes regiones de la imagen; en YOLO, CNN se aplica una vez a toda la imagen a la vez. La red divide la imagen en una especie de cuadrícula y predice cuadros delimitadores y la probabilidad de que haya un objeto deseado para cada sección.

Las ventajas de este enfoque es que la red mira la imagen completa a la vez y tiene en cuenta el contexto al detectar y reconocer un objeto. YOLO también es 1000 veces más rápido que R-CNN y aproximadamente 100 veces más rápido que Fast R-CNN. En este artículo, lanzaremos una red en un dispositivo móvil para el procesamiento en línea, por lo que esta es la calidad más importante para nosotros.

Puede encontrar información más detallada sobre la comparación de arquitecturas aquí .

YOLOv3


YOLOv3 es una versión avanzada de la arquitectura YOLO. Consiste en 106 capas convolucionales y detecta mejor los objetos pequeños en comparación con su predecesor YOLOv2. La característica principal de YOLOv3 es que hay tres capas en la salida, cada una de las cuales está diseñada para detectar objetos de diferentes tamaños.
La siguiente imagen muestra su estructura esquemática:



YOLOv3-tiny : una versión recortada de la arquitectura YOLOv3, consta de menos capas (solo hay 2 capas de salida). Predice peor los objetos más pequeños y está destinado a pequeños conjuntos de datos. Pero, debido a la estructura truncada, los pesos de la red ocupan una pequeña cantidad de memoria (~ 35 MB) y produce un FPS más alto. Por lo tanto, dicha arquitectura es preferible para su uso en un dispositivo móvil.

Estamos escribiendo un programa para el reconocimiento de objetos.


¡Comienza la parte divertida!

Creemos una aplicación que reconozca varios objetos en la imagen en tiempo real usando la cámara del teléfono. Todo el código se escribirá en el lenguaje de programación Swift 4.2 y se ejecutará en un dispositivo iOS.

En este tutorial tomaremos una red preparada con escalas pre-entrenadas en un conjunto de datos COCO . Presenta 80 clases diferentes. Por lo tanto, nuestra neurona podrá reconocer 80 objetos diferentes.

De Darknet a CoreML


La arquitectura original de YOLOv3 se implementa utilizando el marco Darknet .

En iOS, a partir de la versión 11.0, existe una maravillosa biblioteca CoreML que le permite ejecutar modelos de aprendizaje automático directamente en el dispositivo. Pero hay una limitación: el programa solo se puede ejecutar en un dispositivo con iOS 11 y superior.

El problema es que CoreML solo comprende el formato específico del modelo .coreml . Para las bibliotecas más populares, como Tensorflow, Keras o XGBoost, es posible convertir directamente al formato CoreML. Pero para Darknet no existe tal posibilidad. Para convertir el modelo guardado y entrenado de Darknet a CoreML, puede usar varias opciones, por ejemplo, guardar Darknet a ONNX y luego convertirlo de ONNX a CoreML.

Utilizaremos una forma más simple y utilizaremos la implementación Keras de YOLOv3. El algoritmo de acción es el siguiente: cargue los pesos Darknet en el modelo Keras, guárdelo en el formato Keras y conviértalo directamente a CoreML.

  1. Descargar Darknet. Descargue los archivos del modelo entrenado Darknet-YOLOv3 desde aquí . En este artículo, usaré dos arquitecturas: YOLOv3-416 y YOLOv3-tiny. Necesitaremos archivos cfg y pesos.
  2. De Darknet a Keras. Primero, clone el repositorio , vaya a la carpeta repo y ejecute el comando:

    python convert.py yolov3.cfg yolov3.weights yolo.h5 

    donde yolov3.cfg y yolov3.weights descargaron archivos Darknet. Como resultado, deberíamos tener un archivo con la extensión .h5 : este es el modelo YOLOv3 guardado en formato Keras.
  3. De Keras a CoreML. El último paso quedó. Para convertir el modelo a CoreML, debe ejecutar el script python (primero debe instalar la biblioteca coremltools para python):

     import coremltools coreml_model = coremltools.converters.keras.convert( 'yolo.h5', input_names='image', image_input_names='image', input_name_shape_dict={'image': [None, 416, 416, 3]}, #      image_scale=1/255.) coreml_model.input_description['image'] = 'Input image' coreml_model.save('yolo.mlmodel') 

Los pasos que se describen arriba deben realizarse para los dos modelos YOLOv3-416 y YOLOv3-tiny.
Cuando hicimos todo esto, tenemos dos archivos: yolo.mlmodel y yolo-tiny.mlmodel. Ahora puede comenzar a escribir el código de la aplicación en sí.

Crear una aplicación para iOS


No describiré todo el código de la aplicación, puede verlo en el enlace del repositorio que se proporcionará al final del artículo. Permítanme decir que tenemos tres UIViewController-a: OnlineViewController, PhotoViewController y SettingsViewController. El primero es la salida de la cámara y la detección en línea de objetos para cada cuadro. En el segundo, puede tomar una foto o seleccionar una imagen de la galería y probar la red en estas imágenes. El tercero contiene la configuración, puede elegir el modelo YOLOv3-416 o YOLOv3-minúsculo, así como elegir los umbrales IoU (intersección sobre unión) y la confianza del objeto (la probabilidad de que haya un objeto en la sección de imagen actual).

Cargando modelos en CoreML

Después de convertir el modelo entrenado del formato Darknet a CoreML, tenemos un archivo con la extensión .mlmodel . En mi caso, creé dos archivos: yolo.mlmodel y yolo-tiny.mlmodel , para los modelos YOLOv3-416 y YOLOv3-tiny, respectivamente. Ahora puede cargar estos archivos en un proyecto en Xcode.

Creamos la clase ModelProvider; almacenará el modelo y los métodos actuales para invocar asincrónicamente la red neuronal para su ejecución. El modelo se carga de esta manera:

  private func loadModel(type: YOLOType) { do { self.model = try YOLO(type: type) } catch { assertionFailure("error creating model") } } 

La clase YOLO es directamente responsable de cargar archivos .mlmodel y manejar los resultados del modelo. Descargar archivos de modelo:

  var url: URL? = nil self.type = type switch type { case .v3_Tiny: url = Bundle.main.url(forResource: "yolo-tiny", withExtension:"mlmodelc") self.anchors = tiny_anchors case .v3_416: url = Bundle.main.url(forResource: "yolo", withExtension:"mlmodelc") self.anchors = anchors_416 } guard let modelURL = url else { throw YOLOError.modelFileNotFound } do { model = try MLModel(contentsOf: modelURL) } catch let error { print(error) throw YOLOError.modelCreationError } 

Código de proveedor de modelo completo.
 import UIKit import CoreML protocol ModelProviderDelegate: class { func show(predictions: [YOLO.Prediction]?, stat: ModelProvider.Statistics, error: YOLOError?) } @available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4.0, *) class ModelProvider { struct Statistics { var timeForFrame: Float var fps: Float } static let shared = ModelProvider(modelType: Settings.shared.modelType) var model: YOLO! weak var delegate: ModelProviderDelegate? var predicted = 0 var timeOfFirstFrameInSecond = CACurrentMediaTime() init(modelType type: YOLOType) { loadModel(type: type) } func reloadModel(type: YOLOType) { loadModel(type: type) } private func loadModel(type: YOLOType) { do { self.model = try YOLO(type: type) } catch { assertionFailure("error creating model") } } func predict(frame: UIImage) { DispatchQueue.global().async { do { let startTime = CACurrentMediaTime() let predictions = try self.model.predict(frame: frame) let elapsed = CACurrentMediaTime() - startTime self.showResultOnMain(predictions: predictions, elapsed: Float(elapsed), error: nil) } catch let error as YOLOError { self.showResultOnMain(predictions: nil, elapsed: -1, error: error) } catch { self.showResultOnMain(predictions: nil, elapsed: -1, error: YOLOError.unknownError) } } } private func showResultOnMain(predictions: [YOLO.Prediction]?, elapsed: Float, error: YOLOError?) { if let delegate = self.delegate { DispatchQueue.main.async { let fps = self.measureFPS() delegate.show(predictions: predictions, stat: ModelProvider.Statistics(timeForFrame: elapsed, fps: fps), error: error) } } } private func measureFPS() -> Float { predicted += 1 let elapsed = CACurrentMediaTime() - timeOfFirstFrameInSecond let currentFPSDelivered = Double(predicted) / elapsed if elapsed > 1 { predicted = 0 timeOfFirstFrameInSecond = CACurrentMediaTime() } return Float(currentFPSDelivered) } } 


Procesamiento de salida de red neuronal

Ahora veamos cómo procesar las salidas de la red neuronal y obtener los cuadros delimitadores correspondientes. En Xcode, si selecciona un archivo de modelo, puede ver cuáles son y ver las capas de salida.


Entrada y salida YOLOv3-tiny.


Entrada y salida YOLOv3-416.

Como puede ver en la imagen de arriba, tenemos tres para YOLOv3-416 y dos para capas de salida YOLOv3-diminutas en cada una de las cuales se predicen cuadros delimitadores para varios objetos.
En este caso, esta es una matriz regular de números, descubramos cómo analizarla.

El modelo YOLOv3 utiliza tres capas como salida para dividir la imagen en una cuadrícula diferente, los tamaños de celda de estas cuadrículas tienen los siguientes valores: 8, 16 y 32. Supongamos que tenemos una imagen de 416x416 píxeles de tamaño, luego las matrices de salida (cuadrículas) tendrán un tamaño de 52x52 , 26x26 y 13x13 (416/8 = 52, 416/16 = 26 y 416/32 = 13). En el caso de YOLOv3-tiny, todo es igual, pero en lugar de tres cuadrículas, tenemos dos: 16 y 32, es decir, matrices de dimensiones 26x26 y 13x13.

Después de iniciar el modelo CoreML cargado, obtenemos dos (o tres) objetos de la clase MLMultiArray en la salida. Y si observa la propiedad de forma de estos objetos, veremos la siguiente imagen (para YOLOv3-tiny):

[1,1,255,26,26][1,1,255,13,13]


Como se esperaba, la dimensión de las matrices será 26x26 y 13x13, pero ¿qué significará el número 255? Como se mencionó anteriormente, las capas de salida son matrices 52x52, 26x26 y 13x13. El hecho es que cada elemento de esta matriz no es un número, es un vector. Es decir, la capa de salida es una matriz tridimensional. Este vector tiene la dimensión B x (5 + C), donde B es el número de cuadro delimitador en la celda, C es el número de clases. ¿De dónde viene el número 5? La razón es esta: para cada cuadro-a, la probabilidad de que haya un objeto (confianza de objeto) se predice es un número, y los cuatro restantes son x, y, ancho y alto para el cuadro-a predicho. La siguiente figura muestra una representación esquemática de este vector:


Representación esquemática de la capa de salida (mapa de características).

Para nuestra red entrenada en 80 clases, se predice 3 recuadros delimitadores-a para cada celda de la cuadrícula de partición, para cada uno de ellos: 80 probabilidades de clase + confianza del objeto + 4 números responsables de la posición y el tamaño de este recuadro-a. Total: 3 x (5 + 80) = 255.

Para obtener estos valores de la clase MLMultiArray, es mejor usar un puntero sin formato a una matriz de datos y aritmética de direcciones:

  let pointer = UnsafeMutablePointer<Double>(OpaquePointer(out.dataPointer)) //    if out.strides.count < 3 { throw YOLOError.strideOutOfBounds } let channelStride = out.strides[out.strides.count-3].intValue let yStride = out.strides[out.strides.count-2].intValue let xStride = out.strides[out.strides.count-1].intValue func offset(ch: Int, x: Int, y: Int) -> Int { //     return ch * channelStride + y * yStride + x * xStride } 

Ahora necesita procesar un vector de 255 elementos. Para cada cuadro, necesita obtener una distribución de probabilidad para 80 clases, puede hacerlo utilizando la función softmax.

¿Qué es softmax?
La función convierte un vector  mathbbx dimensión K en un vector de la misma dimensión, donde cada coordenada  mathbbxi el vector resultante está representado por un número real en el intervalo [0,1] y la suma de las coordenadas es 1.

 sigma( mathbbx)i= fracexi sumKk=1exk

donde K es la dimensión del vector.

Función Softmax en Swift:

  private func softmax(_ x: inout [Float]) { let len = vDSP_Length(x.count) var count = Int32(x.count) vvexpf(&x, x, &count) var sum: Float = 0 vDSP_sve(x, 1, &sum, len) vDSP_vsdiv(x, 1, &sum, &x, 1, len) } 

Para obtener las coordenadas y los tamaños del cuadro delimitador-a, debe usar las fórmulas:

x= sigma( hatx)+cxy= sigma( haty)+cyw=pwe hatwh=phe hath


donde  hatx, haty, hatw, hath - coordenadas pronosticadas x, y, ancho y alto, respectivamente,  sigma(x) Es la función sigmoidea y pw,ph - valores de anclajes (anclajes) para tres cajas. Estos valores se determinan durante el entrenamiento y se establecen en el archivo Helpers.swift:

 let anchors1: [Float] = [116,90, 156,198, 373,326] //     let anchors2: [Float] = [30,61, 62,45, 59,119] //     let anchors3: [Float] = [10,13, 16,30, 33,23] //     


Representación esquemática del cálculo de la posición de un cuadro delimitador.

Código completo para procesar capas de salida.
  private func process(output out: MLMultiArray, name: String) throws -> [Prediction] { var predictions = [Prediction]() let grid = out.shape[out.shape.count-1].intValue let gridSize = YOLO.inputSize / Float(grid) let classesCount = labels.count print(out.shape) let pointer = UnsafeMutablePointer<Double>(OpaquePointer(out.dataPointer)) if out.strides.count < 3 { throw YOLOError.strideOutOfBounds } let channelStride = out.strides[out.strides.count-3].intValue let yStride = out.strides[out.strides.count-2].intValue let xStride = out.strides[out.strides.count-1].intValue func offset(ch: Int, x: Int, y: Int) -> Int { return ch * channelStride + y * yStride + x * xStride } for x in 0 ..< grid { for y in 0 ..< grid { for box_i in 0 ..< YOLO.boxesPerCell { let boxOffset = box_i * (classesCount + 5) let bbx = Float(pointer[offset(ch: boxOffset, x: x, y: y)]) let bby = Float(pointer[offset(ch: boxOffset + 1, x: x, y: y)]) let bbw = Float(pointer[offset(ch: boxOffset + 2, x: x, y: y)]) let bbh = Float(pointer[offset(ch: boxOffset + 3, x: x, y: y)]) let confidence = sigmoid(Float(pointer[offset(ch: boxOffset + 4, x: x, y: y)])) if confidence < confidenceThreshold { continue } let x_pos = (sigmoid(bbx) + Float(x)) * gridSize let y_pos = (sigmoid(bby) + Float(y)) * gridSize let width = exp(bbw) * self.anchors[name]![2 * box_i] let height = exp(bbh) * self.anchors[name]![2 * box_i + 1] for c in 0 ..< 80 { classes[c] = Float(pointer[offset(ch: boxOffset + 5 + c, x: x, y: y)]) } softmax(&classes) let (detectedClass, bestClassScore) = argmax(classes) let confidenceInClass = bestClassScore * confidence if confidenceInClass < confidenceThreshold { continue } predictions.append(Prediction(classIndex: detectedClass, score: confidenceInClass, rect: CGRect(x: CGFloat(x_pos - width / 2), y: CGFloat(y_pos - height / 2), width: CGFloat(width), height: CGFloat(height)))) } } } return predictions } 


Supresión no máxima

Una vez que haya recibido las coordenadas y los tamaños de los cuadros delimitadores y las probabilidades correspondientes para todos los objetos encontrados en la imagen, puede comenzar a dibujarlos en la parte superior de la imagen. ¡Pero hay un problema! Tal situación puede surgir cuando se predicen varias cajas con probabilidades bastante altas para un objeto. ¿Qué hacer en este caso? Aquí un algoritmo bastante simple llamado supresión no máxima viene en nuestra ayuda.

El algoritmo es el siguiente:

  1. Estamos buscando un cuadro delimitador con la mayor probabilidad de pertenecer al objeto.
  2. Revisamos todos los cuadros delimitadores que también pertenecen a este objeto.
  3. Los eliminamos si Intersection over Union (IoU) con el primer cuadro delimitador es mayor que el umbral especificado.

IoU se calcula usando una fórmula simple:

 textIoU= frac textÁreadeintersección textÁreadeasociación

ÁóÁó
Cálculo de IoU.
  static func IOU(a: CGRect, b: CGRect) -> Float { let areaA = a.width * a.height if areaA <= 0 { return 0 } let areaB = b.width * b.height if areaB <= 0 { return 0 } let intersection = a.intersection(b) let intersectionArea = intersection.width * intersection.height return Float(intersectionArea / (areaA + areaB - intersectionArea)) } 


Supresión no máxima.
  private func nonMaxSuppression(boxes: inout [Prediction], threshold: Float) { var i = 0 while i < boxes.count { var j = i + 1 while j < boxes.count { let iou = YOLO.IOU(a: boxes[i].rect, b: boxes[j].rect) if iou > threshold { if boxes[i].score > boxes[j].score { if boxes[i].classIndex == boxes[j].classIndex { boxes.remove(at: j) } else { j += 1 } } else { if boxes[i].classIndex == boxes[j].classIndex { boxes.remove(at: i) j = i + 1 } else { j += 1 } } } else { j += 1 } } i += 1 } } 


Después de eso, trabajar directamente con los resultados de la predicción de la red neuronal puede considerarse completo. A continuación, debe escribir funciones y clases para obtener el metraje de la cámara, mostrar la imagen en la pantalla y representar los cuadros delimitadores predichos. No describiré todo este código en este artículo, pero se puede ver en el repositorio.

También vale la pena mencionar que agregué un poco de suavizado de los cuadros delimitadores al procesar imágenes en línea, en este caso es el promedio habitual de la posición y el tamaño del cuadrado predicho en los últimos 30 cuadros.

Probar el programa


Ahora probamos la aplicación.

Permítame recordarle una vez más: hay tres ViewControllers en la aplicación, uno para procesar fotos o instantáneas, uno para procesar una transmisión de video en línea y un tercero para configurar la red.

Comencemos con el tercero. En él puede elegir uno de los dos modelos YOLOv3-tiny o YOLOv3-416, elegir el umbral de confianza y el umbral de IoU, también puede habilitar o deshabilitar el suavizado en línea.


Ahora veamos cómo funciona la neurona entrenada con imágenes reales, para esto tomamos una foto de la galería y la pasamos a través de la red. La siguiente imagen muestra los resultados de YOLOv3-tiny con diferentes configuraciones.


Diferentes modos de funcionamiento de YOLOv3-tiny. La imagen de la izquierda muestra el modo de operación habitual. En el medio: el umbral IoU = 1, es decir como si faltara la supresión no máxima. A la derecha hay un umbral bajo de confianza del objeto, es decir Se muestran todos los cuadros delimitadores posibles.

El siguiente es el resultado de YOLOv3-416. Puede notar que, en comparación con YOLOv3-tiny, los marcos resultantes son más correctos, así como también se reconocen los objetos más pequeños en la imagen, lo que corresponde al trabajo de la tercera capa de salida.


Imagen procesada con YOLOv3-416.

Cuando se activó el modo de operación en línea, se procesó cada fotograma y se realizó una predicción, se realizaron pruebas en el iPhone XS, por lo que el resultado fue bastante aceptable para ambas opciones de red. YOLOv3-tiny produce un promedio de 30 - 32 fps, YOLOv3-416 - de 23 a 25 fps. El dispositivo en el que se probó es bastante productivo, por lo que en modelos anteriores los resultados pueden diferir, en cuyo caso, por supuesto, es preferible usar YOLOv3-tiny. Otro punto importante: yolo-tiny.mlmodel (YOLOv3-tiny) ocupa unos 35 MB, mientras que yolo.mlmodel (YOLOv3 -16) pesa unos 250 MB, lo que es una diferencia muy significativa.

Conclusión


Como resultado, se escribió una aplicación para iOS que con la ayuda de una red neuronal puede reconocer objetos en la imagen. Vimos cómo trabajar con la biblioteca CoreML y cómo usarla para ejecutar varios modelos previamente entrenados (por cierto, también puedes entrenar con ella). El problema de reconocimiento de objetos se resolvió utilizando la red YOLOv3. En el iPhone XS, esta red (YOLOv3-tiny) es capaz de procesar imágenes a una frecuencia de ~ 30 cuadros por segundo, lo cual es suficiente para la operación en tiempo real.

El código completo de la aplicación se puede ver en GitHub .

Source: https://habr.com/ru/post/460869/


All Articles