Reconnaissance d'objets en temps réel sur iOS à l'aide de YOLOv3


Bonjour à tous!

Dans cet article, nous allons écrire un petit programme pour résoudre le problème de la détection et de la reconnaissance d'objets (détection d'objets) en temps réel. Le programme sera écrit dans le langage de programmation Swift pour la plate-forme iOS. Pour détecter des objets, nous utiliserons un réseau neuronal convolutif avec une architecture appelée YOLOv3. Dans l'article, nous apprendrons comment travailler dans iOS avec des réseaux de neurones en utilisant le framework CoreML, un peu de compréhension de ce qu'est le réseau YOLOv3 et comment utiliser et traiter les sorties de ce réseau. Nous allons également vérifier le fonctionnement du programme et comparer plusieurs variantes de YOLOv3: YOLOv3-tiny et YOLOv3-416.

Les sources seront disponibles à la fin de l'article, afin que chacun puisse tester le fonctionnement du réseau neuronal sur son appareil.

Détection d'objets


Pour commencer, nous allons brièvement comprendre quelle est la tâche de détecter des objets (détection d'objets) dans l'image et quels outils sont utilisés pour cela aujourd'hui. Je comprends que beaucoup connaissent bien ce sujet, mais je me permets quand même d'en parler un peu.

Maintenant, de nombreuses tâches dans le domaine de la vision par ordinateur sont résolues à l'aide de réseaux de neurones convolutifs (Convolutional Neural Networks), ci-après CNN. En raison de leur structure, ils extraient bien les caractéristiques de l'image. Les CNN sont utilisés dans la classification, la reconnaissance, la segmentation et bien d'autres.

Architectures CNN populaires pour la reconnaissance d'objets:

  • R-CNN. On peut dire le premier modèle à résoudre ce problème. Fonctionne comme un classificateur d'image ordinaire. Différentes régions de l'image sont transmises à l'entrée du réseau et des prédictions sont faites pour elles. Très lent car il exécute une seule image plusieurs milliers de fois.
  • R-CNN rapide. Une version améliorée et plus rapide de R-CNN fonctionne sur un principe similaire, mais d'abord l'image entière est envoyée à l'entrée CNN, puis les régions sont générées à partir de la représentation interne reçue. Mais toujours assez lent pour les tâches en temps réel.
  • R-CNN plus rapide. La principale différence avec les précédents est qu'au lieu de l'algorithme de recherche sélective, il utilise un réseau de neurones pour sélectionner des régions pour les «mémoriser».
  • YOLO. Un principe de fonctionnement complètement différent des précédents n'utilise pas du tout les régions. Le plus rapide. Plus de détails à ce sujet seront discutés dans l'article.
  • SSD Il est similaire en principe à YOLO, mais utilise VGG16 comme réseau pour extraire des fonctionnalités. Aussi assez rapide et adapté au travail en temps réel.
  • Réseaux de pyramides (FPN). Un autre type de réseau tel que Single Shot Detector, en raison des fonctionnalités d'extraction des fonctionnalités, est meilleur que le SSD reconnaît les petits objets.
  • RetinaNet. Il utilise une combinaison de FPN + ResNet et, grâce à une fonction d'erreur spéciale (perte focale), donne une précision plus élevée.

Dans cet article, nous utiliserons l'architecture YOLO, à savoir sa dernière modification, YOLOv3.

Pourquoi YOLO?




YOLO ou You Only Look Once est l'architecture très populaire de CNN, qui est utilisée pour reconnaître plusieurs objets dans une image. Des informations plus complètes à ce sujet peuvent être obtenues sur le site officiel , au même endroit, vous pouvez trouver des publications dans lesquelles la théorie et la composante mathématique de ce réseau sont décrites en détail, ainsi que le processus de sa formation.

La principale caractéristique de cette architecture par rapport aux autres est que la plupart des systèmes appliquent CNN plusieurs fois à différentes régions de l'image; dans YOLO, CNN est appliqué une fois à l'image entière à la fois. Le réseau divise l'image en une sorte de grille et prédit des boîtes englobantes et la probabilité qu'il y ait un objet souhaité pour chaque section.

Les avantages de cette approche sont que le réseau regarde la totalité de l'image à la fois et prend en compte le contexte lors de la détection et de la reconnaissance d'un objet. YOLO est également 1000 fois plus rapide que R-CNN et environ 100 fois plus rapide que Fast R-CNN. Dans cet article, nous allons lancer un réseau sur un appareil mobile pour le traitement en ligne, c'est donc la qualité la plus importante pour nous.

Des informations plus détaillées sur la comparaison des architectures peuvent être trouvées ici .

YOLOv3


YOLOv3 est une version avancée de l'architecture YOLO. Il se compose de 106 couches convolutives et détecte mieux les petits objets par rapport à son prédécesseur YOLOv2. La principale caractéristique de YOLOv3 est qu'il y a trois couches à la sortie, chacune étant conçue pour détecter des objets de différentes tailles.
L'image ci-dessous montre sa structure schématique:



YOLOv3-tiny - Une version recadrée de l'architecture YOLOv3, se compose de moins de couches (il n'y a que 2 couches de sortie). Il prédit des objets plus petits et est destiné aux petits ensembles de données. Mais, en raison de la structure tronquée, les poids du réseau occupent une petite quantité de mémoire (~ 35 Mo) et produisent un FPS plus élevé. Par conséquent, une telle architecture est préférable pour une utilisation sur un appareil mobile.

Nous écrivons un programme de reconnaissance d'objets


La partie amusante commence!

Créons une application qui reconnaîtra divers objets dans l'image en temps réel à l'aide de l'appareil photo du téléphone. Tout le code sera écrit dans le langage de programmation Swift 4.2 et exécuté sur un appareil iOS.

Dans ce tutoriel, nous prendrons un réseau prêt à l'emploi avec des échelles pré-formées sur un jeu de données COCO . Il présente 80 classes différentes. Par conséquent, notre neurone sera capable de reconnaître 80 objets différents.

De Darknet à CoreML


L'architecture YOLOv3 d'origine est implémentée à l'aide du framework Darknet .

Sur iOS, à partir de la version 11.0, il existe une merveilleuse bibliothèque CoreML qui vous permet d'exécuter des modèles d'apprentissage automatique directement sur l'appareil. Mais il y a une limitation: le programme ne peut être exécuté que sur un appareil exécutant iOS 11 et supérieur.

Le problème est que CoreML ne comprend que le format spécifique du modèle .coreml . Pour les bibliothèques les plus populaires, telles que Tensorflow, Keras ou XGBoost, il est possible de convertir directement au format CoreML. Mais pour Darknet, une telle possibilité n'existe pas. Afin de convertir le modèle enregistré et formé de Darknet en CoreML, vous pouvez utiliser diverses options, par exemple, enregistrer Darknet en ONNX , puis le convertir d'ONNX en CoreML.

Nous utiliserons une manière plus simple et nous utiliserons l'implémentation Keras de YOLOv3. L'algorithme d'action est le suivant: chargez les poids Darknet dans le modèle Keras, enregistrez-le au format Keras et convertissez-le directement en CoreML à partir de cela.

  1. Téléchargez Darknet. Téléchargez ici les fichiers du modèle Darknet-YOLOv3 formé. Dans cet article, j'utiliserai deux architectures: YOLOv3-416 et YOLOv3-tiny. Nous aurons besoin des fichiers cfg et des poids.
  2. De Darknet à Keras. Tout d'abord, clonez le référentiel , accédez au dossier repo et exécutez la commande:

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

    où yolov3.cfg et yolov3.weights ont téléchargé des fichiers Darknet. Par conséquent, nous devrions avoir un fichier avec l'extension .h5 - il s'agit du modèle YOLOv3 enregistré au format Keras.
  3. De Keras à CoreML. La dernière étape est restée. Afin de convertir le modèle en CoreML, vous devez exécuter le script python (vous devez d'abord installer la bibliothèque coremltools pour 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') 

Les étapes décrites ci-dessus doivent être effectuées pour les deux modèles YOLOv3-416 et YOLOv3-tiny.
Lorsque nous avons fait tout cela, nous avons deux fichiers: yolo.mlmodel et yolo-tiny.mlmodel. Vous pouvez maintenant commencer à écrire le code de l'application elle-même.

Création d'une application iOS


Je ne décrirai pas tout le code de l'application, vous pouvez le voir dans le référentiel, dont un lien sera donné à la fin de l'article. Permettez-moi de dire que nous avons trois UIViewController-a: OnlineViewController, PhotoViewController et SettingsViewController. Le premier est la sortie de la caméra et la détection en ligne d'objets pour chaque image. Dans le second, vous pouvez prendre une photo ou sélectionner une photo dans la galerie et tester le réseau sur ces images. Le troisième contient les paramètres, vous pouvez choisir le modèle YOLOv3-416 ou YOLOv3-minuscule, ainsi que les seuils IoU (intersection sur l'union) et la confiance de l'objet (la probabilité qu'il y ait un objet dans la section d'image actuelle).

Chargement de modèles dans CoreML

Après avoir converti le modèle formé du format Darknet en CoreML, nous avons un fichier avec l'extension .mlmodel . Dans mon cas, j'ai créé deux fichiers: yolo.mlmodel et yolo-tiny.mlmodel , pour les modèles YOLOv3-416 et YOLOv3-tiny, respectivement. Vous pouvez maintenant charger ces fichiers dans un projet dans Xcode.

Nous créons la classe ModelProvider; elle stockera le modèle et les méthodes actuels pour invoquer de manière asynchrone le réseau neuronal pour exécution. Le modèle est chargé de cette manière:

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

La classe YOLO est directement responsable du chargement des fichiers .mlmodel et de la gestion des sorties de modèle. Téléchargez les fichiers modèles:

  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 } 

Code complet du fournisseur de modèle.
 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) } } 


Traitement de sortie de réseau neuronal

Voyons maintenant comment traiter les sorties du réseau neuronal et obtenir les boîtes de délimitation correspondantes. Dans Xcode, si vous sélectionnez un fichier de modèle, vous pouvez voir ce qu'il est et voir les couches de sortie.


Entrée et sortie YOLOv3-tiny.


Entrée et sortie YOLOv3-416.

Comme vous pouvez le voir dans l'image ci-dessus, nous en avons trois pour YOLOv3-416 et deux pour les couches de sortie minuscules YOLOv3 dans chacune desquelles des boîtes englobantes pour divers objets sont prévues.
Dans ce cas, il s'agit d'un tableau régulier de nombres, voyons comment l'analyser.

Le modèle YOLOv3 utilise trois couches en sortie pour diviser l'image en une grille différente, les tailles de cellule de ces grilles ont les valeurs suivantes: 8, 16 et 32. Supposons que nous ayons une image de 416x416 pixels en entrée, alors les matrices de sortie (grilles) auront une taille de 52x52 , 26x26 et 13x13 (416/8 = 52, 416/16 = 26 et 416/32 = 13). Dans le cas de YOLOv3-tiny, tout est pareil, mais au lieu de trois grilles, nous en avons deux: 16 et 32, c'est-à-dire des matrices de dimensions 26x26 et 13x13.

Après avoir démarré le modèle CoreML chargé, nous obtenons deux (ou trois) objets de la classe MLMultiArray sur la sortie. Et si vous regardez la propriété de forme de ces objets, nous verrons l'image suivante (pour YOLOv3-tiny):


Comme prévu, la dimension des matrices sera 26x26 et 13x13. Mais que signifiera le nombre 255? Comme mentionné précédemment, les couches de sortie sont des matrices 52x52, 26x26 et 13x13. Le fait est que chaque élément de cette matrice n'est pas un nombre, c'est un vecteur. Autrement dit, la couche de sortie est une matrice tridimensionnelle. Ce vecteur a la dimension B x (5 + C), où B est le nombre de boîtes englobantes dans la cellule, C est le nombre de classes. D'où vient le chiffre 5? La raison en est la suivante: pour chaque case-a, la probabilité est prédite qu'il existe un objet (confiance d'objet) - c'est un nombre, et les quatre autres sont x, y, largeur et hauteur pour la case-a prédite. La figure ci-dessous montre une représentation schématique de ce vecteur:


Représentation schématique de la couche de sortie (carte d'entités).

Pour notre réseau formé dans 80 classes, 3 box-a sont prédits pour chaque cellule de la grille de partition, pour chacune d'entre elles - 80 probabilités de classe + confiance d'objet + 4 nombres responsables de la position et de la taille de cette box-a. Total: 3 x (5 + 80) = 255.

Pour obtenir ces valeurs de la classe MLMultiArray, il est préférable d'utiliser un pointeur brut vers un tableau de données et une arithmétique d'adresse:

  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 } 

Vous devez maintenant traiter un vecteur de 255 éléments. Pour chaque case, vous devez obtenir une distribution de probabilité pour 80 classes, vous pouvez le faire en utilisant la fonction softmax.

Qu'est-ce que softmax
La fonction convertit un vecteur dimension K en un vecteur de la même dimension, où chaque coordonnée le vecteur résultant est représenté par un nombre réel dans l'intervalle [0,1] et la somme des coordonnées est 1.

où K est la dimension du vecteur.

Fonction Softmax sur 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) } 

Pour obtenir les coordonnées et les tailles de la boîte englobante-a, vous devez utiliser les formules:


- coordonnées x, y prévues, largeur et hauteur, respectivement, Est la fonction sigmoïde, et - valeurs des ancres (ancres) pour trois boîtes. Ces valeurs sont déterminées lors de la formation et sont définies dans le fichier 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] //     


Représentation schématique du calcul de la position d'une boîte englobante.

Code complet pour le traitement des couches de sortie.
  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 } 


Suppression non max

Une fois que vous avez reçu les coordonnées et les tailles des boîtes englobantes et les probabilités correspondantes pour tous les objets trouvés dans l'image, vous pouvez commencer à les dessiner au-dessus de l'image. Mais il y a un problème! Une telle situation peut se produire lorsque plusieurs cases avec des probabilités plutôt élevées sont prédites pour un objet. Que faire dans ce cas? Ici, un algorithme assez simple appelé suppression non maximale vient à notre aide.

L'algorithme est le suivant:

  1. Nous recherchons une boîte englobante avec la plus forte probabilité d'appartenir à l'objet.
  2. Nous parcourons toutes les boîtes englobantes qui appartiennent également à cet objet.
  3. Nous les supprimons si Intersection over Union (IoU) avec la première boîte englobante est supérieur au seuil spécifié.

L'IoU est calculé à l'aide d'une formule simple:

Calcul de l'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)) } 


Suppression non max.
  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 } } 


Après cela, le travail directement avec les résultats de la prédiction du réseau neuronal peut être considéré comme terminé. Ensuite, vous devez écrire des fonctions et des classes pour obtenir les images de la caméra, afficher l'image à l'écran et rendre les zones de délimitation prévues. Je ne décrirai pas tout ce code dans cet article, mais il peut être consulté dans le référentiel.

Il convient également de mentionner que j'ai ajouté un peu de lissage des cadres de délimitation lors du traitement des images en ligne, dans ce cas, il s'agit de la moyenne habituelle de la position et de la taille du carré prédit sur les 30 dernières images.

Test du programme


Nous testons maintenant l'application.

Permettez-moi de vous rappeler à nouveau: il y a trois ViewControllers dans l'application, un pour le traitement des photos ou des instantanés, un pour le traitement des flux vidéo en ligne et un troisième pour la configuration du réseau.

Commençons par le troisième. Vous pouvez y choisir l'un des deux modèles YOLOv3-tiny ou YOLOv3-416, choisir le seuil de confiance et le seuil IoU, vous pouvez également activer ou désactiver l'anticrénelage en ligne.


Voyons maintenant comment le neurone entraîné fonctionne avec des images réelles, pour cela nous prenons une photo de la galerie et la passons à travers le réseau. L'image ci-dessous montre les résultats de YOLOv3-tiny avec différents paramètres.


Différents modes de fonctionnement de YOLOv3-tiny. L'image de gauche montre le mode de fonctionnement habituel. Au milieu - le seuil IoU = 1 i.e. comme si la suppression Non-max était manquante. À droite se trouve un seuil bas de confiance des objets, c'est-à-dire toutes les zones de délimitation possibles sont affichées.

Ce qui suit est le résultat de YOLOv3-416. Vous remarquerez peut-être que, par rapport à YOLOv3-tiny, les cadres résultants sont plus corrects, ainsi que les objets plus petits de l'image sont reconnus, ce qui correspond au travail du troisième calque de sortie.


Image traitée à l'aide de YOLOv3-416.

Lorsque le mode de fonctionnement en ligne a été activé, chaque trame a été traitée et une prédiction a été faite pour elle, des tests ont été effectués sur l'iPhone XS, donc le résultat était tout à fait acceptable pour les deux options de réseau. YOLOv3-tiny produit en moyenne 30 à 32 ips, YOLOv3-416 - de 23 à 25 ips. L'appareil sur lequel il a été testé est assez productif, donc sur les modèles précédents les résultats peuvent différer, auquel cas il est bien sûr préférable d'utiliser YOLOv3-tiny. Un autre point important: yolo-tiny.mlmodel (YOLOv3-tiny) prend environ 35 Mo, tandis que yolo.mlmodel (YOLOv3 -16) pèse environ 250 Mo, ce qui est une différence très importante.

Conclusion


En conséquence, une application iOS a été écrite qui, à l'aide d'un réseau de neurones, peut reconnaître des objets dans l'image. Nous avons vu comment travailler avec la bibliothèque CoreML et comment l'utiliser pour exécuter divers modèles pré-formés (à propos, vous pouvez également vous entraîner avec). Le problème de reconnaissance d'objets a été résolu en utilisant le réseau YOLOv3. Sur iPhone XS, ce réseau (YOLOv3-tiny) est capable de traiter des images à une fréquence de ~ 30 images par seconde, ce qui est largement suffisant pour un fonctionnement en temps réel.

Le code d'application complet peut être consulté sur GitHub .

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


All Articles