Créer un jeu de contrÎle du sourire

salut! Je m'appelle Ivan Shafran, j'ai rĂ©cemment rejoint l'Ă©quipe vidĂ©o de VK en tant que dĂ©veloppeur Android. Je participe Ă  la crĂ©ation d'applications produit et de SDK. De temps en temps, je visite des hackathons oĂč vous pouvez mettre en Ɠuvre des idĂ©es folles. Aujourd'hui, je vais vous dire comment faire un prototype de jeu mobile avec des commandes inhabituelles en quelques heures: un personnage rĂ©agira Ă  un sourire et Ă  un clin d'Ɠil.







Comment est née l'idée



L'idĂ©e de crĂ©er un tel jeu est venue juste lors du hackathon. Le format supposait qu'il y avait une journĂ©e de travail pour le dĂ©veloppement, soit 8 heures. Pour rĂ©aliser un prototype dans le temps, j'ai choisi le SDK Android. Peut-ĂȘtre que les moteurs de jeu seraient mieux adaptĂ©s, mais je ne les comprends pas.



Le concept de contrĂŽle Ă  l'aide d'Ă©motions a Ă©tĂ© suggĂ©rĂ© par un autre jeu: lĂ , les mouvements du personnage pouvaient ĂȘtre dĂ©finis en modifiant le volume de votre voix. Peut-ĂȘtre que quelqu'un a dĂ©jĂ  utilisĂ© les Ă©motions dans le contrĂŽle du jeu. Mais je connais peu d'exemples de ce type, alors j'ai optĂ© pour ce format.



Méfiez-vous des vidéos bruyantes!




Mise en place de l'environnement de développement



Nous n'avons besoin que d' Android Studio sur l'ordinateur. S'il n'y a pas de véritable appareil Android à exécuter, vous pouvez utiliser un émulateur avec une webcam activée .



Créer un projet avec ML Kit







ML Kit est un excellent outil pour impressionner le jury du hackathon: vous utilisez l'IA dans un prototype! En général, il permet d'intégrer des solutions basées sur l'apprentissage automatique dans des projets, par exemple des fonctionnalités d'identification d'objets dans un cadre, de traduction et de reconnaissance de texte.



Il est important pour nous que ML Kit dispose d'une API hors ligne gratuite pour reconnaßtre les sourires et les yeux ouverts ou fermés.



Auparavant, pour crĂ©er un projet avec ML Kit, vous deviez d'abord vous enregistrer dans la console Firebase . Cette Ă©tape peut dĂ©sormais ĂȘtre ignorĂ©e pour les fonctionnalitĂ©s hors ligne.



Application Android



Supprimer inutile



Afin de ne pas écrire de logique pour travailler avec la caméra à partir de zéro, prenons l' exemple officiel et en retirons ce dont nous n'avons pas besoin.







Tout d'abord, téléchargez l' exemple et essayez de l'exécuter. Explorez le mode Détection de visage: il ressemblera à l'aperçu de l'article.



Manifeste



Commençons par éditer AndroidManifest.xml. Supprimez toutes les balises d'activité à l'exception du premier. Et à sa place, nous mettrons CameraXLivePreviewActivity pour démarrer immédiatement à partir de la caméra. Dans la valeur de l'attribut android: value, nous ne laissons que le visage afin d'exclure les ressources inutiles de l'APK.



<meta-data
 android:name="com.google.mlkit.vision.DEPENDENCIES"
  android:value="face"/>
<activity
  android:name=".CameraXLivePreviewActivity"
  android:exported="true"
  android:theme="@style/AppTheme">
  <intent-filter>
      <action android:name="android.intent.action.MAIN"/>
      <category android:name="android.intent.category.LAUNCHER"/>
  </intent-filter>
</activity>


Différence à pas complet.



Caméra



Gagnez du temps - nous ne supprimerons pas les fichiers inutiles, nous nous concentrerons plutÎt sur les éléments de l'écran CameraXLivePreviewActivity.



  • À la ligne 117, dĂ©finissez le mode de dĂ©tection de visage:

    private String selectedModel = FACE_DETECTION;
  • Sur la ligne 118, allumez la camĂ©ra frontale:

    private int lensFacing = CameraSelector.LENS_FACING_FRONT;
  • À la fin de la mĂ©thode onCreate sur les lignes 198-199, masquez les paramĂštres

    findViewById( R.id.settings_button ).setVisibility( View.GONE );
    findViewById( R.id.control ).setVisibility( View.GONE );


Nous pouvons nous arrĂȘter ici. Mais si le rendu FPS et la grille de visage sont visuellement gĂȘnants, vous pouvez les dĂ©sactiver comme ceci:



  • Dans le fichier VisionProcessorBase.java, supprimez les lignes 213-215 pour masquer le FPS:

    graphicOverlay.add(
           new InferenceInfoGraphic(
              graphicOverlay, currentLatencyMs, shouldShowFps ? framesPerSecond : null));
  • Dans le fichier FaceDetectorProcessor.java, supprimez les lignes 75 Ă  78 pour masquer le maillage de la face:

    for (Face face : faces) {
        graphicOverlay.add(new FaceGraphic(graphicOverlay, face));
        logExtrasForTesting(face);
    }


Différence à pas complet.



Reconnaßtre les émotions



La détection de sourire est désactivée par défaut, mais il est facile de commencer. Ce n'est pas pour rien que nous avons pris l'exemple de code comme base! Sélectionnons les paramÚtres dont nous avons besoin dans une classe séparée et déclarons l'interface d'écoute:



FaceDetectorProcessor.java

//   FaceDetectorProcessor.java
public class FaceDetectorProcessor extends VisionProcessorBase<List<Face>> {
    public static class Emotion {
        public final float smileProbability;
        public final float leftEyeOpenProbability;
        public final float rightEyeOpenProbability;
        public Emotion(float smileProbability, float leftEyeOpenProbability, float rightEyeOpenProbability) {
           this.smileProbability = smileProbability;
            this.leftEyeOpenProbability = leftEyeOpenProbability;
           this.rightEyeOpenProbability = rightEyeOpenProbability;
        }
    }
    public interface EmotionListener {
        void onEmotion(Emotion emotion);
    }
    private EmotionListener listener;
    public void setListener(EmotionListener listener) {
       this.listener = listener;
    }
    
    @Override
    protected void onSuccess(@NonNull List<Face> faces, @NonNull GraphicOverlay graphicOverlay) {
        if (!faces.isEmpty() && listener != null) {
            Face face = faces.get(0);
            if (face.getSmilingProbability() != null &&
                    face.getLeftEyeOpenProbability() != null && face.getRightEyeOpenProbability() != null) {
                listener.onEmotion(new Emotion(
                        face.getSmilingProbability(),
                        face.getLeftEyeOpenProbability(),
                        face.getRightEyeOpenProbability()
                ));
            }
        }
    }
}


Pour activer la classification des émotions, configurez FaceDetectorProcessor dans la classe CameraXLivePreviewActivity et abonnez-vous pour recevoir l'état d'émotion. Ensuite, nous transformons les probabilités en indicateurs booléens. Pour les tests, ajoutons un TextView à la mise en page, dans lequel nous montrerons des émotions à travers des émoticÎnes.







Différence à pas complet.



Divisez et jouez



Puisque nous créons un jeu, nous avons besoin d'un endroit pour dessiner les éléments. Supposons qu'il fonctionne sur le téléphone en mode portrait. Alors, divisons l'écran en deux parties: la caméra en haut et le jeu en bas.



ContrĂŽler un personnage avec le sourire est difficile, et il y a peu de temps au hackathon pour mettre en Ɠuvre des mĂ©canismes avancĂ©s. Par consĂ©quent, notre personnage collectera des nishtyaks en cours de route, soit en haut du terrain de jeu, soit en bas. Nous ajouterons des actions avec les yeux fermĂ©s ou ouverts comme complication du jeu: si vous attrapez un nishtyak avec un Ɠil fermĂ©, les points sont doublĂ©s ( ou la moitiĂ© de l'Ă©cran n'est pas visible et vous pouvez voler les vaches ).



Si vous souhaitez implémenter un gameplay différent, je peux vous proposer des options intéressantes:



  • Guitar Hero / Just Dance - analogique, oĂč vous devez montrer une certaine Ă©motion Ă  la musique;
  • une course avec franchissement d'obstacles, oĂč vous devez atteindre la ligne d'arrivĂ©e dans un certain temps ou sans s'Ă©craser;
  • tireur oĂč le joueur cligne de l'Ɠil et tire sur l'ennemi.


Nous afficherons le jeu dans une vue Android personnalisée - là, dans la méthode onDraw, nous dessinerons un personnage sur Canvas. Dans le premier prototype, nous nous limiterons aux primitives géométriques.



Joueur







Notre personnage est un carré. Lors de l'initialisation, nous définirons sa taille et sa position à gauche, car il sera en place. La position de l'axe Y dépendra du sourire du joueur. Toutes les valeurs absolues seront calculées par rapport à la taille de la zone de jeu. C'est plus facile que de choisir des tailles spécifiques - et nous obtiendrons une apparence acceptable sur les nouveaux appareils.



private var playerSize = 0
private var playerRect = RectF()
//       View
private fun initializePlayer() {
    playerSize = height / 4
    playerRect.left = playerSize / 2f
    playerRect.right = playerRect.left + playerSize
}
//      
private var flags: EmotionFlags
//      
private fun movePlayer() {
    playerRect.top = getObjectYTopForLine(playerSize, isTopLine = flags.isSmile).toFloat()
    playerRect.bottom = playerRect.top + playerSize
}
//   top     size,
//        
private fun getObjectYTopForLine(size: Int, isTopLine: Boolean): Int {
    return if (isTopLine) {
        width / 2 - width / 4 - size / 2
    } else {
        width / 2 + width / 4 - size / 2
    }
}
//  paint   ,        
private val playerPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    style = Paint.Style.FILL
    color = Color.BLUE
}
//     Canvas
private fun drawPlayer(canvas: Canvas) {
    canvas.drawRect(playerRect, playerPaint)
}


gĂąteau



Notre personnage «court» et essaie d'attraper des gĂąteaux afin de marquer le plus de points possible. Nous utilisons la technique standard avec le passage au systĂšme de rĂ©fĂ©rence relatif au joueur: il restera immobile, et les gĂąteaux voleront vers lui. Si le carrĂ© du gĂąteau croise le carrĂ© du joueur, le point est comptĂ©. Et si en mĂȘme temps au moins un Ɠil de l'utilisateur est fermĂ© - deux points ÂŻ \ _ (ツ) _ / ÂŻ



Aussi dans notre univers il n'y aura qu'un seul gĂąteau d' Ă©lectrons . DĂšs que le personnage le mange, il se dĂ©place hors de l'Ă©cran vers une bande alĂ©atoire avec une coordonnĂ©e alĂ©atoire. Cela empĂȘchera le sourire du joueur de rĂ©sonner avec l'apparence prĂ©visible du gĂąteau.



//        
private fun initializeCake() {
    cakeSize = height / 8
    moveCakeToStartPoint()
}
private fun moveCakeToStartPoint() {
    //      
    cakeRect.left = width + width * Random.nextFloat()
    cakeRect.right = cakeRect.left + cakeSize
    //      
    val isTopLine = Random.nextBoolean()
    cakeRect.top = getObjectYTopForLine(cakeSize, isTopLine).toFloat()
    cakeRect.bottom = cakeRect.top + cakeSize
}
//        
private fun moveCake() {
    val currentTime = System.currentTimeMillis()
    val deltaTime = currentTime - previousTimestamp
    val deltaX = cakeSpeed * width * deltaTime
    cakeRect.left -= deltaX
    cakeRect.right = cakeRect.left + cakeSize
    previousTimestamp = currentTime
}
//     ,   
private fun checkPlayerCaughtCake() {
    if (RectF.intersects(playerRect, cakeRect)) {
        score += if (flags.isLeftEyeOpen && flags.isRightEyeOpen) 1 else 2
        moveCakeToStartPoint()
    }
}
//    ,      
private fun checkCakeIsOutOfScreenStart() {
    if (cakeRect.right < 0) {
        moveCakeToStartPoint()
    }
}


Qu'est-il arrivé



Rendons l'affichage des points trÚs simple. Nous afficherons le numéro au centre de l'écran. Il vous suffit de prendre en compte la hauteur du texte et d'indenter le haut pour la beauté.



private val scorePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    color = Color.GREEN
    textSize = context.resources.getDimension(R.dimen.score_size)
}
private var score: Int = 0
private var scorePoint = PointF()
private fun initializeScore() {
    val bounds = Rect()
    scorePaint.getTextBounds("0", 0, 1, bounds)
    val scoreMargin = resources.getDimension(R.dimen.score_margin)
    scorePoint = PointF(width / 2f, scoreMargin + bounds.height())
    score = 0
}


Voyons quel genre de jouet nous avons fabriqué:





Différence à pas complet.



Graphonium



Pour ne pas avoir honte de montrer le jeu lors de la présentation du hackathon, ajoutons du grafonium!







Images



Nous partons du fait que nous ne pouvons pas dessiner des graphismes impressionnants. Heureusement, il existe des sites avec des actifs de jeu gratuits. J'ai bien aimé celui-ci , bien qu'il ne soit plus disponible directement pour une raison inconnue de moi.







Animation



Nous dessinons sur Canvas, ce qui signifie que nous devons implĂ©menter l'animation nous-mĂȘmes. S'il y a des images avec une animation, il sera facile de la programmer. Nous introduisons une classe pour un objet avec des images changeantes.



class AnimatedGameObject(
        private val bitmaps: List<Bitmap>,
        private val duration: Long
) {
    fun getBitmap(timeInMillis: Long): Bitmap {
        val mod = timeInMillis % duration
        val index = (mod / duration.toFloat()) * bitmaps.size
        return bitmaps[index.toInt()]
    }
}


Pour obtenir l'effet de mouvement, l'arriĂšre-plan doit Ă©galement ĂȘtre animĂ©. Avoir une sĂ©rie d'images d'arriĂšre-plan en mĂ©moire est une histoire rĂ©trospective. Par consĂ©quent, faisons-le plus habilement: nous allons dessiner une image avec un dĂ©calage temporel. Aperçu de l'idĂ©e:







Terminez l'étape diff.



Résultat final



Il est difficile d’appeler cela un chef-d’Ɠuvre, mais c’est bien pour un prototype le soir. Le code peut ĂȘtre trouvĂ© ici . Fonctionne localement sans manigances supplĂ©mentaires.





En conclusion, j'ajouterai que ML Kit Face Detection peut ĂȘtre utile pour d'autres scĂ©narios.



Par exemple, pour prendre des selfies parfaits avec des amis: vous pouvez analyser toutes les personnes dans le cadre et vous assurer que tout le monde a souri et ouvert les yeux. La détection de plusieurs visages dans un flux vidéo fonctionne hors de la boßte, donc la tùche n'est pas difficile.



En utilisant la reconnaissance des contours du visage du module de détection des visages, il est possible de reproduire des masques qui sont maintenant populaires dans presque toutes les applications de caméra. Et si vous ajoutez des éléments interactifs - grùce à la définition d'un sourire et d'un clin d'oeil - alors les utiliser sera doublement amusant.



Cette fonctionnalitĂ© - le contour du visage - peut ĂȘtre utilisĂ©e pour plus que le divertissement. Ceux qui ont essayĂ© de dĂ©couper une photo pour des documents eux-mĂȘmes l'apprĂ©cieront. Nous prenons le contour du visage, dĂ©coupons automatiquement la photo avec le rapport hauteur / largeur souhaitĂ© et la bonne position de la tĂȘte. Le capteur du gyroscope aidera Ă  dĂ©terminer l'angle de prise de vue correct.



All Articles