Suite de l’article « Utiliser la reconnaissance faciale pour contrôler une application »
Bon, c’est la suite de cet article, où j’ai décrit comment j’ai construit une application qui suivait mon regard. L’application a fonctionné raisonnablement bien et pourtant n’a pas fonctionné. Après avoir examiné le code et pensé à le restructurer à plusieurs reprises, je voulais créer une solution qui apprendrait par elle-même ou quelque chose de proche.
En réfléchissant davantage au défi, il m’est venu à l’esprit que cela pourrait être un excellent projet pour Core ML. Je pouvais créer un modèle des valeurs des différentes formes de mélange et l’utiliser pour décider dans quelle direction je cherchais. C’était un bon choix pour la modélisation puisque j’avais un ensemble de valeurs et de résultats distincts, et mieux encore, c’était un problème du monde réel autour duquel je pouvais construire un modèle d’apprentissage automatique.
Au cours de mon voyage, le premier changement que j’ai apporté a été d’implémenter une télécommande pour mon application. Je n’étais pas sûr d’en avoir besoin, mais cela pourrait m’aider avec le problème de l’observateur. Pour ce faire, j’ai utilisé le MultipeerConnectivity
framework, en le mettant en œuvre en utilisant le même schéma décrit dans ce papier. En toute justice pour vous, le lecteur – voici un raccourci vers Télécharger le code multipair. Veuillez copier et coller ce code dans son propre fichier Swift. j’ai appelé le mien MultiPeer
.
Le code que vous téléchargez est configuré en tant qu’annonceur ; vous devez l’inclure dans l’application principale. Après avoir fait cela, j’ai créé une deuxième application appelée « remote », qui nécessitait également que le code décommente les lignes du navigateur et commente les lignes de l’annonceur dans le init()
méthode pour le côté distant. La télécommande ici est le navigateur ; l’application principale est l’annonceur.
override init() {
...
serviceBrowser.delegate = self
serviceBrowser.startBrowsingForPeers()
}
J’ai construit l’écran de la deuxième application pour qu’il contienne deux boutons : un bouton d’arrêt pour mettre les choses en pause et un bouton de démarrage pour les relâcher. L’objectif de la télécommande est de revoir les valeurs du regard sur l’écran de l’appareil sans changer lesdites valeurs lorsque je la regarde – un problème d’observateur classique.
Vous trouverez un ContentView
de ma télécommande avec ce lien essentiel. Utilisez ces deux fichiers source pour créer votre propre application distante et n’oubliez pas d’apporter les modifications nécessaires aux propriétés d’information.
J’avais besoin d’ajouter la fonctionnalité à l’autre application, j’ai donc ajouté une instance et des références audit objet.
var connectSession = MultipeerSession.shared
Ensuite, j’ai mis à jour le code de session pour changer la variable pause en true ou false en fonction des données envoyées via le lien.
Note: le changement est déjà présent dans le code multipeer que vous avez téléchargé.
Vous devez commenter ce code sur la version « distante ».
let foo = String(decoding: data, as: UTF8.self)
DispatchQueue.main.async {
if foo == "free" {
looked.paused = false
} else {
looked.paused = true
}
}
Le second, un changement sans doute plus critique que j’ai apporté à l’application, était d’inclure une classe de discours. C’était important car je devais continuer à regarder loin de l’écran pour capter les différents regards. OK, j’avais créé une télécommande pour la geler, mais la faire lire les instructions ajouterait une autre corde à mon arc. Voici la source du code vocal ; vous pouvez le télécharger ici.
En supposant que vous avez déclaré une instance de la classe comme talk
tout ce dont vous avez besoin pour le faire fonctionner est une ligne comme celle-ci :
talk.speaker(words: ["Minority Report"])
OK, sur le plat principal. Le principal défi avec la version précédente était de reconnaître les huit emplacements indiqués par mon regard. J’avais précédemment codé en dur ce qui semblait être des valeurs raisonnables dans l’application, donc le code ressemblait à ceci :
if eyesLookUp > 0.2 && eyesLookSide < 0.3 && eyesLookDown < 0.1 {
sphereNode.simdPosition = SIMD3(x: 0, y: height + 1, z: -6)
sphereNode.geometry?.firstMaterial?.diffuse.contents = UIColor.red.withAlphaComponent(1)
}
Mais cela n’a pas fonctionné aussi bien que je l’avais espéré. Je pouvais bien faire fonctionner les coins, mais j’avais du mal avec les côtés et vice versa. J’ai créé une feuille de calcul simple pour décrire les valeurs des différents regards.
C’était un match parfait pour la modélisation puisque chaque chemin avait des valeurs définitives. Je l’ai ensuite exporté sous forme de fichier « CSV » et lancé l’outil Create ML sous Xcode.
Note: J’avais besoin de modifier le fichier « CSV » avant de l’importer car les nombres ajoutaient une ligne de virgules à mon fichier.
Pour ce faire, j’ai utilisé ceci excellente vidéo pour me guider dans les tâches nécessaires à la création d’un fichier modèle. Si vous n’avez pas le temps de regarder cela, voici un bref aperçu.
J’avais besoin de sélectionner une variable cible : la « direction ». Les dix autres variables/colonnes de ma feuille de calcul étaient nécessaires pour déterminer la valeur. Une fois que j’ai créé le modèle, j’ai utilisé ceci deuxième vidéo pour me rappeler comment l’inclure dans mon projet. Pour moi, le code final ressemblait à ceci:
Remarque : j’ai réglé le eyeLook
variables utilisant la ARFaceAnchor
valeurs.
let eyeLookIn_L = faceAnchor?.blendShapes[.eyeLookInLeft]?.doubleValue
let eyeLookIn_R = faceAnchor?.blendShapes[.eyeLookInRight]?.doubleValue
let eyeLookOut_R = faceAnchor?.blendShapes[.eyeLookOutRight]?.doubleValue
let eyeLookOut_L = faceAnchor?.blendShapes[.eyeLookOutLeft]?.doubleValue
let eyeLookDown_L = faceAnchor?.blendShapes[.eyeLookDownLeft]?.doubleValue
let eyeLookUp_L = faceAnchor?.blendShapes[.eyeLookUpLeft]?.doubleValue
let eyeLookDown_R = faceAnchor?.blendShapes[.eyeLookDownRight]?.doubleValue
let eyeLookUp_R = faceAnchor?.blendShapes[.eyeLookUpRight]?.doubleValue
Maintenant, en utilisant ce modèle simple, je pouvais différencier toutes les directions sauf le regard sans direction – un oubli que j’ai initialement corrigé avec cette ligne de code :
if eyesLookLeft < 0.1 && eyesLookRight < 0.1 && eyesLookDown < 0.1 && eyesLookUp < 0.1 {
sphereNode.simdPosition = SIMD3(x: 0, y: height, z: -6)
sphereNode.geometry?.firstMaterial?.diffuse.contents = UIColor.green.withAlphaComponent(0.8)
looked.gazeX = ""
return
}
Pour agir sur la prédiction du modèle, j’ai combiné cette instruction switch avec une clause if pour m’aider à garder la tête droite face à la caméra.
Je l’ai testé à nouveau avec le GIF animé ici montrant le résultat :
J’étais satisfait de la nouvelle construction; il avait plus de finesse et, contrairement à la version codée en dur, était un jeu d’enfant à peaufiner. A propos de peaufinage, j’ai essayé sept versions différentes [changing threshold values] du modèle avant de créer celui-ci, même si je dois avouer que je devais rester très immobile pour faire les huit positions. J’ai aussi essayé quelques autres algorithmes; ils n’ont fait aucune différence.
Au fond de moi, je voulais enregistrer les valeurs réelles vues, les enregistrer dans un fichier CSV et les utiliser pour reconstruire le modèle. J’avais besoin de mieux comprendre comment le déplacement de l’orientation de ma tête modifiait les valeurs du regard. Comprendre cela me permettrait de l’intégrer au modèle, ce qui, en théorie, pourrait signifier que je pourrais bouger un peu plus la tête.
J’ai créé une méthode pour enregistrer toutes les valeurs vues dans un tableau. Voici le code pour cela :
func logEyes(look:String) {
let eyeLookIn_L = faceAnchor?.blendShapes[.eyeLookInLeft]?.doubleValue
let eyeLookIn_R = faceAnchor?.blendShapes[.eyeLookInRight]?.doubleValue
let eyeLookOut_R = faceAnchor?.blendShapes[.eyeLookOutRight]?.doubleValue
let eyeLookOut_L = faceAnchor?.blendShapes[.eyeLookOutLeft]?.doubleValuelet eyeLookDown_L = faceAnchor?.blendShapes[.eyeLookDownLeft]?.doubleValue
let eyeLookUp_L = faceAnchor?.blendShapes[.eyeLookUpLeft]?.doubleValue
let eyeLookDown_R = faceAnchor?.blendShapes[.eyeLookDownRight]?.doubleValue
let eyeLookUp_R = faceAnchor?.blendShapes[.eyeLookUpRight]?.doubleValue
let angleX = rad2deg(Double(self.cubeNode.eulerAngles.x))
let angleY = rad2deg(Double(self.cubeNode.eulerAngles.y))
let rec = Regard(direct: look, eyeLookIn_L: eyeLookIn_L, eyeLookOut_R: eyeLookOut_R, eyeLookIn_R: eyeLookIn_R, eyeLookOut_L: eyeLookOut_L, eyeLookDown_L: eyeLookDown_L, eyeLookDown_R: eyeLookDown_R, eyeLookUp_L: eyeLookUp_L, eyeLookUp_R: eyeLookUp_R, angleX: angleX, angleY: angleY)
log.append(rec)
}
J’ai ensuite assemblé le code nécessaire pour me donner des valeurs minimales et maximales pour les différents regards. Cela ressemble à ceci :
if eyesTrue {
let foo = log.filter({$0.direct == "eyesLeft"})
let foo2 = foo.filter({$0.eyeLookIn_L > 0})
let minValue = foo2.min { $0.eyeLookIn_L < $1.eyeLookIn_L }?.eyeLookIn_L
let maxValue = foo2.max { $0.eyeLookIn_L < $1.eyeLookIn_L }?.eyeLookIn_L
let sortedValues = foo2.sorted(by: { $0.eyeLookIn_L > $1.eyeLookIn_L} )for foos in foo2 {
print("(foos.eyeLookIn_L!)")
}
print("lookLeft (minValue) (maxValue)")
}
J’ai essayé de déclencher ledit code avec ma télécommande, mais j’ai décidé qu’il serait plus facile d’utiliser un autre geste pour le déclencher. Voici le code pour le faire :
let eyesTrue = (faceAnchor?.blendShapes[.browOuterUpLeft]!.doubleValue)! > 0.1
J’ai mesuré les valeurs, exécuté l’application et collecté des échantillons. Voici les valeurs que j’ai récupérées :
lookUp min 0.010353747755289078 max 0.5442964434623718 eyeLookUp_L
lookLeftUp min 0.10131984949111938 max 0.42720866203308105) eyeLookUp_L
lookLeft min 0.054532475769519806 max 0.6156175136566162 eyeLookOut_R
lookLeft min 0.4086191952228546) max 0.7494118809700012 eyeLookIn_L
J’ai changé les valeurs dans le modèle pour les regards gauche et droit et ajouté eyescenter
valeurs au modèle. Cela ressemblait à ceci après avoir résolu le problème mentionné précédemment:
Et cela a fonctionné raisonnablement bien. Je pense que j’avais besoin d’un modèle derrière l’iPhone pour concentrer mes yeux sur le bon endroit. Pour aller plus loin, j’avais besoin d’un casque monté sur caméra, un peu comme celui qu’ils utilisent pour les effets spéciaux d’Avatar 2. Si vous voulez l’essayer, voici le code source.