Publié par et
Il y a 6 mois · 11 minutes · Mobile

ARKit et Vision : détecter et positionner un objet

Dans notre article précédent (ARKit en 5 étapes), nous avons vu comment utiliser les API les plus simples offertes par ARKit pour créer une expérience de Réalité Augmentée basique. Aujourd’hui, nous souhaitons aller plus loin et démontrer comment se servir du framework Vision pour ajouter de la reconnaissance d’image en temps réel.

 


 

Bien que le framework ARKit, seul, permette de comprendre la géométrie de la scène analysée, ces informations ne sont pas suffisantes pour comprendre la sémantique de l’image lue par les capteurs.

Ceci étant, il est possible d’ajouter à notre système des fonctionnalités de reconnaissance avancée d’image, en utilisant des techniques de Machine Learning. Pour se faire, nous allons employer d’autres frameworks, également présentés lors de la WWDC de 2017 : Vision et Core ML.

Dans cette série de deux articles, nous verrons comment nous servir de Vision afin de résoudre un problème – totalement autoréférentiel – qui nous tient particulièrement à cœur : mettre à jour, automatiquement, les cartes des frameworks iOS.

Le contexte

Les cartes des frameworks iOS présentent, pour chaque projet, des indicateurs tels que le nombre d’étoiles GitHub ou encore le nombre de commit. Bien sûr, ces projets évoluant au jour le jour, leurs informations, une fois imprimées, ne sont rapidement plus d’actualité. Notre but est donc de reconnaitre chaque carte, à l’aide de Vision, et de superposer, à l’aide d’ARKit, une image de synthèse affichant les données les plus récentes, récupérées à partir des API GitHub.

Si vous avez eu l’occasion de récupérer ce jeu de carte pendant une conférence ou un meetup Xebia, alors c’est l’occasion de le dépoussiérer. Sinon, vous pouvez utiliser la carte ci-dessous :

Vision

Le framework Vision applique des techniques d’analyse d’image et de vision par ordinateur pour détecter les caractéristiques et classifier les scènes dans des images et des vidéos. Notamment, Vision permet de reconnaitre des rectangles, des code-barres, des visages ou encore des textes.

Pour simplifier la reconnaissance, nous avons intégré, sur chaque carte de jeu, un QR Code qui l’identifie ; celui-ci sera lu et interprété par Vision.

Détection d’une carte avec Vision

Tout d’abord un peu de théorie sur Vision ! Bien que les problématiques que Vision adresse soient complexes, Apple a réussi à fournir une API de très haut niveau, simple à utiliser. Pour retrouver un objet dans une image (ou un flux d’images) à l’aide de Vision, on utilise principalement trois objets :

  • une request,
  • un request handler, servant à la déclencher,
  • des observations, contenant les résultats de la requête.

Par exemple, dans un premier temps, nous voulons essayer de trouver un rectangle. Pour cela, rien de plus simple, il suffit d’écrire le code suivant :

let detectRectangleRequest = VNDetectRectanglesRequest { request, error in 
 guard let rectangleObservations = request.results as? [VNRectangleObservation], rectangleObservations.count > 0 else { return }

 rectangleObservations.forEach { observation in
  let confidence = observation.confidence 
  let topLeft = observation.topLeft 
 } 
} 

DispatchQueue.global(qos: .userInteractive).async {
 let requestHandler = VNImageRequestHandler(ciImage: CIImage(), options: [:])
 try? requestHandler.perform([detectRectangleRequest]) 
}

Chaque requête définit, à l’aide d’une closure, le traitement du résultat. L’objet VNDetectRectanglesRequest contient les observations effectuées dans son champs « results ». Ces observations ont toutes un indice de confiance compris entre 0 et 1, exprimant la certitude qu’a Vision de son résultat. Aussi, dans le cas d’une observation de rectangles, le framework offre quatre informations supplémentaires : la position de chacun des quatre sommets de l’image. VNImageRequestHandler peut prendre comme argument une CIImage, comme dans notre exemple, mais également d’autres formats. Aussi, notons qu’un handler peut déclencher plusieurs requêtes simultanément.

Ce bout de code n’est pas anodin, il va nous servir de support pour la détection de la carte que nous cherchons dans la scène filmée. En effet, la carte que nous cherchons est un rectangle, Vision nous permet donc, en partie, de la détecter.

Rappelons-nous du contexte, nous travaillons avec ARKit : nous avons accès à un flux vidéo, nul besoin donc de passer par AVFoundation. Nous allons plutôt utiliser une méthode de ARSCNViewDelegate qui nous permet d’exécuter des requêtes Vision fréquemment afin de trouver notre rectangle :

func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {}

Cette méthode est constamment appelée, à intervalle très court. C’est ici que nous allons lancer les requêtes.

func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) { 
 guard time - lastRefresh > refreshRate else { return } 
 lastRefresh = time 

 guard let currentFrame = sceneView.session.currentFrame else { return } 

 let detectRectangleRequest = VNDetectRectanglesRequest { [weak self] request, error in 
  guard let rectangleObservations = request.results as? [VNRectangleObservation], 
rectangleObservations.count > 0 else { return } 
  self?.handle(rectangleObservations: rectangleObservations) 
 } 

 DispatchQueue.global(qos: .userInteractive).async {
  detectRectangleRequest.maximumObservations = 0 

  let vnImage = VNImageRequestHandler(cvPixelBuffer: currentFrame.capturedImage, options: [:]) 
  try? vnImage.perform([detectRectangleRequest]) 
 } 
}

Vision étant coûteux en ressources, il est conseillé de ne pas requêter à chaque time interval, la session ARKit s’interrompant lorsqu’elle vient à manquer de ressources.

À ce point, nous savons qu’un rectangle est présent dans le champ de la caméra mais nous ignorons toujours si cette observation correspond à une carte et quelle est sa position réelle. Afin de savoir si ce rectangle est un bon candidat, il est possible de regarder ses dimensions : si celles-ci correspondent à celle de la carte physique, alors il est probable que nous filmions la carte. Dans ce cas, il restera à vérifier ce qu’elle contient pour confirmer notre hypothèse. Aussi, nous devons calculer la position réelle de cet objet. Si vous vous souvenez du précédent article, il est possible que vous ayez déjà une intuition du procédé à utiliser… Il s’agit du hit-test !

Pour rappel, le hit-test permet de convertir une coordonnée 2D, sur l’écran du smartphone, en une coordonnée 3D, dans le monde physique. Il permet donc de savoir à quelle distance se situe un point que l’on est en train de filmer. Comme évoqué, Vision nous fournit quatre points particuliers : les sommets du rectangle ; utilisons-les pour déterminer ses dimensions et son centre :

private func handle(rectangleObservations: [VNRectangleObservation]) { 
 let topLeft = sceneView.hitTest(sceneView.convertFromCamera(rectangle.topLeft), types: 
.existingPlaneUsingExtent) 
 let topRight = sceneView.hitTest(sceneView.convertFromCamera(rectangle.topRight), types: 
.existingPlaneUsingExtent) 
 let bottomLeft = sceneView.hitTest(sceneView.convertFromCamera(rectangle.bottomLeft), types: 
.existingPlaneUsingExtent) 
 let bottomRight = sceneView.hitTest(sceneView.convertFromCamera(rectangle.bottomRight), types: 
.existingPlaneUsingExtent) 

if let topLeftHit = topLeft.first, 
   let bottomLeftHit = bottomLeft.first, 
   let topRightHit = topRight.first, 
   let bottomRightHit = bottomRight.first { 
   // Calculate rectangle size 
   // Determine rectangle middle 
 } 
}

Votre regard expert n’aura pas manqué de remarquer que nous n’utilisons pas directement les sommets du rectangle ! En effet, il est nécessaire de convertir les coordonnées de la caméra en coordonnées sur une vue (en l’occurrence celles sur notre sceneView). Les coordonnées de la caméra sont un peu particulières: elles vont de 0 à 1 ou de -1 à 0 selon l’orientation. Un point positionné sur (x: 0.2, y: 0.5) signifie que celui-ci se situe à 20% de la largeur de la vue qui le contient et se trouve verticalement centré dans la vue (à 50% de sa hauteur). Si ces explications vous paraissent floues, vous pouvez directement utiliser cette méthode permettant de convertir les coordonnées entre elles :

extension UIView { 
 func convertFromCamera(_ point: CGPoint) -> CGPoint {
  let orientation = UIApplication.shared.statusBarOrientation 

  switch orientation { 
  case .portrait, .unknown: 
    return CGPoint(x: point.y * frame.width, y: point.x * frame.height) 
  case .landscapeLeft: 
    return CGPoint(x: (1 - point.x) * frame.width, y: point.y * frame.height) 
  case .landscapeRight: 
    return CGPoint(x: point.x * frame.width, y: (1 - point.y) * frame.height) 
  case .portraitUpsideDown: 
    return CGPoint(x: (1 - point.y) * frame.width, y: (1 - point.x) * frame.height) 
   } 
 } 
}

Avant de déterminer le périmètre et le centre du rectangle, un peu de maths ! Les vecteurs en 3 dimensions utilisés par SceneKit ont pour type SCNVector3 : ces classes n’ont pas beaucoup de méthodes associées pour faire les calculs classiques sur les vecteurs. Il convient donc d’en faire une extension :

extension SCNVector3 {
 func distance(from vector: SCNVector3) -> CGFloat {
  let deltaX = self.x - vector.x
  let deltaY = self.y - vector.y
  let deltaZ = self.z - vector.z
 
  return CGFloat(sqrt(deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ)) 
} 

func midpoint(from vector: SCNVector3) -> SCNVector3 {
  let midX = (self.x + vector.x) / 2 
  let midY = (self.y + vector.y) / 2 
  let midZ = (self.z + vector.z) / 2 

  return SCNVector3Make(midX, midY, midZ) 
 } 
}

Nous pouvons donc maintenant déterminer les informations dont nous avons besoin :

struct Corners {
 let topRight: SCNVector3 
 let topLeft: SCNVector3 
 let bottomRight: SCNVector3
 let bottomLeft: SCNVector3 
} 

struct Rectangle {
 private let corners: Corners

 init(corners: Corners) { 
  self.corners = corners 
 } 

 var width: CGFloat {
  get {
   return corners.topLeft.distance(from: corners.topRight) 
  } 
 } 

 var height: CGFloat {
  get { 
   return corners.topLeft.distance(from: corners.bottomLeft) 
  } 
 } 

 var center: SCNVector3 { 
  get { 
   return corners.topLeft.midpoint(from: corners.bottomLeft) 
  } 
 } 

 var orientation: Float { 
  get { 
   let distX = corners.topRight.x - corners.topLeft.x 
   let distZ = corners.topRight.z - corners.topLeft.z 
   return -atan(distZ / distX) 
  } 
 } 
}

Et voilà ! Nous avons toutes les informations nécessaires pour déterminer la taille de notre carte. Nous réutiliserons aussi ces informations pour en contruire une nouvelle, cette-fois ci virtuelle.Nous pouvons donc maintenant déterminer les informations dont nous avons besoin :

Mais, si vous avez bien suivi, vous remarquerez que l’on ne sait toujours pas si ce que nous filmons est réellement une carte. L’ultime étape est de vérifier si le candidat que nous avons contient bien les informations d’une carte de jeu. Pour cela il existe plusieurs approches possibles. Ceci étant, puisque le but ici est de découvrir Vision, nous allons essayer de l’exploiter au maximum. Par chance (ou pas), chaque carte est identifiée par un QR code que nous pouvons détecter et (quelle drôle de coïncidence, hein) Vision permet justement de le trouver ! Nous allons donc :

  1. Détecter ce QR code.
  2. Regarder sa position (est-elle bien dans le rectangle ?).
  3. Déchiffrer son contenu afin d’avoir le minimum d’information nécessaire pour récupérer les données à jour depuis GitHub.

En vertu du design de l’API de Vision, la première étape ressemble énormément à la toute première, pour trouver un rectangle nous écrirons donc le code suivant.

let barcodeDetectionRequest = VNDetectBarcodesRequest { [weak self] request, error in 
 guard let barcodeObservations = request.results as? [VNBarcodeObservation], barcodeObservations.count > 
0 else { return }  
 barcodeObservations.forEach { barcodeObservation in
   let payload = barcodeObservation.payloadStringValue
   let barcodeCornersCoordinates = sceneView.convertFromCamera(barcodeObservation.cornerCoordinates) 
} 

DispatchQueue.global(qos: .userInteractive).async { 
  guard let currentFrame = strongSelf.sceneView.session.currentFrame else { return }

  let handler = VNImageRequestHandler(cvPixelBuffer: currentFrame.capturedImage, options: [:])
  try? handler.perform([barcodeDetectionRequest]) 
}

Vision nous permet donc d’analyser le QR code et d’en extraire le contenu. En l’occurrence, ce dernier contient le nom d’un dépôt GitHub auquel la carte que nous filmons fait référence. Il ne reste plus qu’à récupérer les informations dont nous avons besoin pour les afficher en Réalité Augmentée.

Vous avez maintenant toutes les clefs en main pour détecter un objet dans une vidéo et en trouver les coordonnées réelles. Bien que notre cas d’usage soit simple, l’approche reste générique pour ce type de problématique. Pour répondre à des problèmes plus complexes, il faudra alors aller plus loin dans l’aspect mathématique (afin de convertir une forme sur son écran en une forme « incrustée » dans le monde réel) ou bien avoir un modèle permettant une classification plus poussée que celle proposée de base par Vision.Vision nous permet donc d’analyser le QR code et d’en extraire le contenu. En l’occurrence, ce dernier contient le nom d’un dépôt GitHub auquel la carte que nous filmons fait référence. Il ne reste plus qu’à récupérer les informations dont nous avons besoin pour les afficher en Réalité Augmentée.

Une autre solution : Core ML

Le framework Core ML permet d’intégrer des modèles entrainés de Machine Learning à bord d’une application iOS. Un modèle entraîné est le résultat de l’application d’un algorithme d’apprentissage automatique à un ensemble de données. Par exemple, un modèle qui a été formé sur les prix historiques d’une région peut être en mesure de prédire le prix d’un appartement en fonction du nombre de chambres et de salles de bain.

À l’aide de Core ML, il aurait été également possible d’embarquer un modèle personnalisé sur le device, permettant d’analyser certains objets dans un flux vidéo et de les reconnaitre. Une fois un modèle dédié aux cartes développé, nous aurions pu utiliser une request de type VNCoreMLRequest, proposée nativement par le framework Vision. Cette requête produira des résultats de type VNClassificationObservation, fournissant les identifiants de la carte reconnue.

À suivre !

Dans notre prochain article, nous dirons au revoir à Vision et nous concentrerons sur ARKit. L’objectif sera de reprendre la carte de jeu et d’en faire une copie en 3D, positionnée sur la carte réelle, avec les mêmes dimensions, afin de donner l’illusion que nous avons réellement mis à jour cette carte. A cette occasion, il faudra ressortir vos vieux bouquins de maths ainsi que vos talents artistiques !

Julien Datour
Développeur iOS, passionné par le monde de la mobilité. Ses sujets en ce moment: la réalité augmentée et le machine learning sur mobile.
Simone Civetta
Simone est responsable technique des équipes mobilités chez Xebia. Il est également formateur au sein de Xebia Training .

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *