imageimage

Il y a un an nos interactions avec les jeux et divertissements se sont trouvées bouleversées avec le capteur Kinect pour Xbox 360, concrétisant une idée qui relevait jusque-là de la science-fiction : contrôler ses jeux et divertissements à l'aide de la voix et des gestes, sans manette ni télécommande.

Au-delà des expériences de divertissement que l’accessoire propose, cela a ouvert de nouvelles perspectives et de nouvelles formes d’interaction homme/machine au travers des interfaces naturelles en créant des ponts entre son propre corps et la technologie, pour de toutes nouvelles expériences sensorielles. Ainsi,

Des applications aussi imaginatives que novatrices dans des domaines variés comme la santé, l'éducation, l'art et bien d'autres encore, ont vu le jour amenant des technophiles à parler d’ « effet Kinect » pour caractériser le phénomène d’émulation qui se créait autour de l’accessoire.

De telles applications sont rendues possibles par le kit de développement (SDK) Kinect pour Windows SDK aujourd’hui en Bêta 2. Celui-ci donne aux développeurs un accès facile aux capacités offertes par le capteur Microsoft Kinect connecté à des ordinateurs sous Windows 7. Celui-ci comprend les pilotes pour le capteur, les interfaces du capteur et API avec la documentation technique associée, ainsi que les exemples de code source.

Avec tous ces éléments à disposition, qu’est-ce qui nous empêche d’avoir un regard sur les perspectives du mouvement Open Data sous le signe d’une innovation centrée sur l’humain en termes d’applications et de services ? Et ainsi de concevoir et mettre à la disposition du plus grand nombre ainsi des applications aussi imaginatives que novatrices alliant la technologie Kinect à des données ouvertes ? Rien du tout !

image

Au cours de ce billet, nous allons donc illustrer un exemple d’application Kinect exploitant un jeu de données exposé par l’instance OGDI (Open Government Data Initiative) du site Open Data 71 du Conseil Général de Saône-et-Loire (Cf. billet Le département de Saône-et-Loire lance son site Open Data).

Présentation du projet

L’idée de base consiste à mettre à disposition une application permettant de mettre en avant les lieux touristiques du département de Saône-et-Loire. Pour cela, nous nous sommes fondé sur un ensemble de données de cartes postales de collection.

A l’arrivée, l’application comprend une liste navigable par la gestuelle de cartes postales des lieux historiques de Saône-et-Loire avec la possibilité de sélectionner une carte postale et d’afficher alors i) la localisation du lieu touristique représenté par la carte postale sur une carte Bing Cartes et ii) un code-barres 2D généré permettant de récupérer les informations du lieu sélectionné sur un smartphone par exemple.

image

Architecture

L’application réalisée est structurée à l’aide des composants suivants :

  • ReactiveKinectExtensions : Ce composant permet de s’abstraire de la logique évènementielle des bibliothèques de base du SDK Kinect pour Windows en s’appuyant sur la technologie Reactive Extensions. Cette approche donne la possibilité de voir conceptuellement le capteur Kinect comme une collection observable de squelettes. De cette manière, on bénéficie d’une sorte de « LINQ pour Kinect » ;
  • KinectSensitiveUI : Ce composant contient divers composants d’interfaces graphiques « Kinectisés », c’est-à-dire adaptés d’une réactivité de type « un clic de souris sur le contrôle déclenche une action » à « si la main droite de l’utilisateur reste un certain temps au-dessus d’un contrôle, alors déclencher une action » ;
  • KinectGestureRecognizer : Ce composant fournit une fondation commune permettant de faire une recherche algorithmique sur des gestes plus complexes qu’une simple simulation de clic. Il propose, par exemple, un détecteur de cercle et un détecteur de mouvement « Pinch to Zoom ». Les détecteurs de gestes dérivent d’une classe abstraite commune, ce qui autorise de mettre en pratique des patterns tels que l’injection de dépendance ou encore l’inversion de contrôle et rend le système complètement extensible par l’ajout de nouvelles classes ;
  • KinectPostalCards : Ce composant représente tout simplement notre application qui s’appuie sur les trois précédentes briques présentées ci-avant.

image

La suite de ce billet se concentre sur les implications liées aux interactions avec le capteur Kinect et à la prise en charge de gestes (gesture) multiples pour interagir avec l’interface homme/machine ; l’interaction avec le service de données OGDI est en tout point similaire à ce que nous avons précédent développé dans de multiples billets sur ce blog.

Si vous n’êtes pas familier avec le SDK Kinect pour Windows, une introduction sur le développement avec le capteur Kinect est disponible ici. Nous vous invitons également à visionner les webcasts de l’après-midi du développement sur Kinect pour Windows.

Composant ReactiveKinectExtensions

image

La bibliothèque ReactiveKinectExtensions comprend un composant principal, à savoir ReactiveKinectRuntime qui est, en fait, un wrapper autour du runtime Kinect classique.

Cette classe tire parti des Reactive Extensions afin de « LINQifier » le capteur Kinect. Ainsi, au lieu d’utiliser l’ancienne approche évènementielle avec déclaration d’un callback par le développeur pour capter l’évènement « SkeletonFrameReady », il suffit maintenant d’utiliser une approche typiquement LINQ, par exemple :

(from skeleton in ReactiveKinectRuntime.Current.SkeletonCaptures

 where skeleton.TrackingState == SkeletonTrackingState.Tracked

select skeleton.Joints)

.Subscribe(joints =>

{

    foreach (Joint j in joints)

    {

       if (j.ID == JointID.HandRight)

          Console.WriteLine("Position Main droite => " + j.Position);

    }

 });

Comme vous pouvez le constater, on accède au ReactiveKinectRuntime sous la forme d’un Singleton.

Nous pouvons donc, à partir de cette classe, accéder :

  1. Aux coordonnées du squelette de l’utilisateur dans un espace en 3 dimensions (propriété SkeletonCaptures) ;
  2. Et aux coordonnées du squelette de l’utilisateur rapportées dans un espace en 2 dimensions (propriété Skeleton2DJointsCaptures).

Pour utiliser les coordonnées dans un espace en 2 dimensions, il faut paramétrer la surface de dessin ainsi que la sensibilité à appliquer grâce aux propriétés DisplaySurfaceheight, DisplaySurfaceWidth, HorizontalSensibility et VerticalSensibility.

Composant KinectSensitiveUI

image

La bibliothèque KinectSensitiveUI comprend les contrôles WPF Button, CheckBox et ListBox « Kinectifiés ». Ces contrôles réagissent dès que le contrôle KinectCursor qui suit la main droite de l’utilisateur passe au-dessus du contrôle en question et reste un certain temps au-dessus.

Pour notifier l’utilisateur de ce type d’interaction, le contrôle KinectCursor est dynamique et réagit dès qu’il passe au-dessus d’un contrôle « kinectifié ». Par exemple, pour bénéficier d’une interface réactive à Kinect, il suffit d’écrire le code XAML suivant :

<Window x:Class="KinectPostalCards.MainWindow"

        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

        xmlns:kinectui="clr-namespace:KinectSensitiveUI;assembly=KinectSensitiveUI"

        xmlns:views="clr-namespace:KinectPostalCards.Views"

        Title="MainWindow" Height="720" Width="1280">

   <Grid>

      <kinectui:KinectButton Height="200" Width="200" Click="ClickHandler" />

      <kinectui:KinectCursor />   

   </Grid>

</Window>

Ainsi, dès que l’utilisateur passera la main au-dessus du bouton Kinect, l’évènement ClickHandler se déclenchera. A noter que vous pouvez tout à fait utiliser l’approche basée sur les Command plutôt que l’approche évènementielle pour découpler la logique applicative de votre interface utilisateur ; ce qui rend ces contrôles totalement utilisables dans une architecture (design pattern) de type MVVM (Model - View - ViewModel).

Composant KinectGestureRecognizer

image

La bibliothèque KinectGestureRecognizer fournit un pattern basé sur une recherche algorithmique sur un tableau de structures SkeletonData du SDK Kinect (qui représente un squelette humain avec la position d’une vingtaine de jointures du corps humain (mains, coudes, épaules, colonne vertébrale, etc.) et leurs positions respectives dans un espace en 3 dimensions dont l’origine est la position de la Kinect).

Le tableau en question est un tableau circulaire (classe CircularBag<T>) qui déclenche l’évènement CircularLoopCompleted dès que le tableau d’objets qu’elle contient atteint la taille du tableau et on recommence à partir de l’indice 0 du tableau ensuite.

On ajoute un élément au tableau circulaire via la méthode Add dont voici le code :

public void Add(T item)

{

   long index = iterationsCounter % bagSize;

   bag[index] = item;

   iterationsCounter++;

   if ((iterationsCounter % bagSize == 0) && (CircularLoopCompleted != null))

   {

      CircularLoopCompletedEventArgs<T> args = new CircularLoopCompletedEventArgs<T>(bagSize);

      bag.CopyTo(args.Bag, 0);

      CircularLoopCompleted(this, args);

   }

}

La classe abstraite GestureRecognizer prend en paramètre de construction un tableau circulaire de structures SkeletonData et s’abonne à l’évènement CircularLoopCompleted pour déclencher le test de reconnaissance de geste via la méthode privée RunRecognitionWorkflow dont voici le code :

public GestureRecognizer(CircularBag<SkeletonData> skeletonBag)

{

   IsActive = true;

   this.skeletonBag = skeletonBag;

   this.skeletonBag.CircularLoopCompleted += (sender, e) =>

   {

      RunRecogitionWorkflow(e.Bag);

      };

   }

 

private void RunRecogitionWorkflow(SkeletonData[] skeletons)

{

   if (IsActive == true)

   {

      bool result = TestRecognition(skeletons);

      if ((result == true) && (GestureDetected != null))

      {

          GestureDetectedEventArgs args = new GestureDetectedEventArgs { GestureName = this.GestureName };

          GestureDetected(this, args);

      }

   }

}

Cette méthode vérifie d’abord que l’objet est actif et, le cas échéant, exécute la méthode abstraite TestRecognition. Si cette dernière renvoie true (, c’est-à-dire si, par exemple, qu’un cercle de la main a été détecté,) déclenche l’évènement GestureDetected auquel il suffit juste de s’abonner.

Les classes dérivant de GestureRecognizer n’ont dès lors plus qu’à implémenter la méthode abstraite TestRecognition et la propriété abstraite GestureName pour être fonctionnelles. Par exemple, la classe CircleGestureDetector est simplement codée de la façon suivante :

public class CircleGestureRecognizer : GestureRecognizer

{

   private string gestureName;

  

   protected override string GestureName

   {

      get { return gestureName; }

   }

 

   public const string CounterClockwiseCircleGesture = "CounterClockwiseCircleGesture";

   public const string ClockwiseGesture = "ClockwiseGesture";

 

   public JointID TrackingJoint { get; set; }

 

   public CircleGestureRecognizer(CircularBag<SkeletonData> vectorBag)

      : base(vectorBag)

   {}

 

   protected override bool TestRecognition(SkeletonData[] skeletons)

   {

      List<Vector> trackedJoints = new List<Vector>();

      foreach(Joint j in skeletons.SelectMany(s => s.Joints.OfType<Joint>()))

      {

         if (j.TrackingState == JointTrackingState.Tracked && j.ID == TrackingJoint)

         {

            trackedJoints.Add(j.Position);

         }

      }

 

      if (trackedJoints.Count == 0)

      {

         return false;

      }

 

      float maxX = trackedJoints.Max(p => p.X);

      float maxY = trackedJoints.Max(p => p.Y);

      float minX = trackedJoints.Min(p => p.X);

      float minY = trackedJoints.Min(p => p.Y);

 

      Vector top = trackedJoints.First(p => p.Y ==  minY);

      Vector bottom = trackedJoints.First(p => p.Y ==  maxY);

      Vector left = trackedJoints.First(p => p.X ==  minX);

      Vector right = trackedJoints.First(p => p.X == maxX);

 

      float deltaX = maxX - minX;

      float deltaY = maxY - minY;

      float differentialBetweenDeltas = deltaX / deltaY;

      if (differentialBetweenDeltas > 0.7 &&

          differentialBetweenDeltas < 1.0 &&

          deltaX > 0.3 &&

          deltaY > 0.3)

      {

         int indexOfLeft = trackedJoints.IndexOf(left);

         int indexOfRight = trackedJoints.IndexOf(right);

         int indexOfTop = trackedJoints.IndexOf(top);

         int indexofBottom = trackedJoints.IndexOf(bottom);

 

         if ((indexOfTop > indexOfLeft && indexOfLeft > indexofBottom && indexofBottom > indexOfRight) ||

             (indexOfLeft > indexofBottom && indexofBottom > indexOfRight && indexOfRight > indexOfTop) ||

             (indexofBottom > indexOfRight && indexOfRight > indexOfTop && indexOfTop > indexOfLeft) ||

             (indexOfRight > indexOfTop && indexOfTop > indexOfLeft && indexOfLeft > indexofBottom))

         {

            gestureName = CounterClockwiseCircleGesture;

         }

         else

         {

            gestureName = ClockwiseGesture;

         }

         return true;

      }         

     

      return false;

   }

}

Une telle approche polymorphique rend la création de nouvelles classes détectant des gestes personnalisés plus facile et permet en plus de rendre le système extensible et configurable au runtime grâce à des Frameworks d’injection de dépendance (Dependency Injection ou DI) comme Unity ou MEF (Managed Extensibility Framework).

Application KinectPostalCards

image

L’application KinectPostalCards est architecturée selon le paradigme MVVM. Parmi les principaux espaces de noms de l’application, on retrouve notamment :

  • KinectPostalCards.OData.CG71 : Cet espace de noms contient les classes proxy vers le service OData du CG71. Ces classes sont auto-générées par l’assistant Visual Studio 2010 d’ajout de référence de service. En l’occurrence, on se sert dans l’application seulement des classes INDEXAD71CartesPostalesItem et dataDataService ;
  • KinectPostalCards.Models : Cet espace de noms contient la classe CarteDetailsModel qui permet d’associer une longitude et une latitude à une instance d’INDEXAD71CartesPostalesItem ;
  • KinectPostalCards.ViewModels : Cet espace de noms contient la classe CardsListViewModel qui représente la logique applicative associée à la vue CardsListView. Cette classe contient notamment la collection observable CartesPostales sur laquelle la vue va se lier pour afficher les photos des cartes postales ainsi que la propriété CarteDetailsModel de type CarteDetailsModel qui représente la carte actuellement sélectionnée par l’utilisateur ;
  • KinectPostalCards.Views : Cet espace de noms contient la vue CardsListView qui affiche la liste des cartes postales, ainsi qu’une carte Bing Cartes. Cette vue se sert des détecteurs de gestes précédemment évoqués afin de déclencher des animations ainsi que des contrôles WPF « Kinectifiés » et aussi de la bibliothèque Kinect.Toolbox disponible sur Codeplex pour bénéficier des SwipeGestureDetector ;
  • KinectPostalCards.BingMaps.GeoCodeService : Cet espace de noms contient les classes proxy vers le service de géocodage de Bing Cartes afin d’obtenir les coordonnées de géolocalisation de la carte postale à partir de la commune associée. (Vous devez, pour cela, disposer d’une clé d’API Bing Cartes disponible gratuitement sur https://www.bingmapsportal.com) ;
  • KinectPostalCards.Converters : Cet espace de noms contient la classe CarteDetailsModelToQRCodeConverter qui permet de générer le code-barres 2D correspondant à une carte postale et ainsi afficher ce dernier sur l’interface utilisateur. Pour ce faire, nous utilisons la bibliothèque GoogleQRGenerator disponible depuis la galerie NuGet ;
  • KinectPostalCards.Controls : Cet espace de noms contient la classe ScrollViewerOffsetMediator qui permet de résoudre un problème que nous exposons ci-après.

Animer la propriété HorizontalOffSet d’un ScrollViewer

Si vous ouvrez la vue CardsListView.xaml, vous verrez que la collection de cartes postales est affichée dans un contrôle de type ItemsControl encapsulé par un ScrollViewer permettant de naviguer au sein de la collection.

L’application permet de faire défiler les cartes postales à l’aide de mouvements de types « swipe to left » ou « swipe to right ».

Sachant que le ScrollViewer possède une propriété HorizontalOffset, notre première idée était de tout simplement de déclencher une animation sur cette propriété dès qu’un geste « swipe » était détectée. Mais comme la propriété HorizontalOffset est une propriété en lecture seule, il n’est pas possible de déclencher des animations sur cette propriété. Le seul moyen d’agir de façon programmatique sur cette propriété est via la méthode ScrollToHorizontalOffset ; ce qui altère considérablement l’expérience utilisateur.

Dès lors comment peut-on altérer le comportement d’un objet alors qu’une approche basée sur le polymorphisme ne s’avère pas possible ?

La réponse se trouve une fois de plus dans la bible de tous les développeurs (Design Patterns : Elements of Reusable Object-Oriented Software) et se nomme le Médiateur. Un Médiateur est un objet qui permet à deux objets qui ne se comprennent pas de communiquer entre eux par le biais du médiateur.

Ainsi est née la classe ScrollViewerOffsetMediator :

image

Comme vous pouvez le constater, cette classe contient comme propriétés un ScrollViewer et HorizontalOffset accessibles cette fois-ci en lecture et écriture et enregistrée comme propriétés de dépendance. Ceci permet de réagir à chaque fois que la propriété est mise à jour comme, par exemple, pour appeler la méthode ScrollToHorizontalOffset de l’objet ScrollViewer appartenant à la classe. Ce qui nous amène au code suivant :

public double HorizontalOffset

{

   get { return (double)GetValue(HorizontalOffsetProperty); }

   set { SetValue(HorizontalOffsetProperty, value); }

}

 

public static readonly DependencyProperty HorizontalOffsetProperty =

           DependencyProperty.Register(

                "HorizontalOffset",

                typeof(double),

                typeof(ScrollViewerOffsetMediator),

                new PropertyMetadata(OnHorizontalOffsetChanged));

 

public static void OnHorizontalOffsetChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)

{

   var mediator = (ScrollViewerOffsetMediator)o;

   if (null != mediator.ScrollViewer)

   {

      mediator.ScrollViewer.ScrollToHorizontalOffset((double)(e.NewValue));

   }

}

Ainsi, dans la vue CardsListView.xaml, il suffit de définir le code suivant pour associer le ScrollViewer au ScrollViewerMediator :

<controls:ScrollViewerOffsetMediator x:Name="ScrollViewerMediator"

                                     ScrollViewer="{Binding ElementName=ScrollViewer}" />

Et, pour déclencher des animations, il suffit d’écrire le code suivant dans le code-behind de la classe CardsListView :

void swipteToRightDetector_OnGestureDetected(string gesture)

{

   if (gesture == "SwipeToRight")

   {

      MoveList(-(ScrollViewer.ActualWidth - 100));

   }

}

 

void swipeToLeftdetector_OnGestureDetected(string gesture)

{

   if (gesture == "SwipeToLeft")

   {

      MoveList(ScrollViewer.ActualWidth - 100);

   }

}

 

private void MoveList(double offset)

{

    double currentPosition = ScrollViewer.HorizontalOffset;

    DoubleAnimation animation = new DoubleAnimation(

                       currentPosition + offset,

                       new Duration(TimeSpan.FromSeconds(2)));

          

    animation.EasingFunction = new CircleEase() { EasingMode = EasingMode.EaseOut };

 

    Storyboard story = new Storyboard();

    story.Children.Add(animation);

 

    Storyboard.SetTarget(story, this.ScrollViewerMediator);

    Storyboard.SetTargetProperty(story, new PropertyPath("HorizontalOffset"));

 

    story.Begin();

}

Ceci conclut notre rapide tour de l’application.

Dépendances

La solution proposée en téléchargement dans le cadre de ce billet comporte les dépendances suivantes en dehors des composants déjà fournis :

Si jamais vous rencontrez des problèmes lors de la compilation avec les packages Coding4Fun.Kinect et Kinect.Toolbox il se peut que le problème se situe au niveau de la cible du SDK Kinect pour Windows de ces projets. Pour pallier à ce problème, il suffit tout simplement de télécharger les sources des projets et de changer la version de la bibliothèque Microsoft.Research.Kinect référencée.

En guise de conclusion

Voilà ça en est terminé pour ce billet sous forme de retour d’expérience sur un développement Open Data avec Kinect pour Windows.

Nous croyons beaucoup à la création de nouveaux usages et services pour les citoyens par le biais des interfaces naturelles, cette application en est l’exemple. La galerie des projets Coding4Fun vous propose de multiples usages créatifs avec Kinect pour Windows pour continuer cette réflexion.

N’hésitez pas à rêver avec Kinect. Et qui sait… devenez les visionnaires et créateurs des usages de demain des données ouvertes !