Dot.Blog

C#, XAML, Xamarin, UWP/Android/iOS

Silverlight 4, Webcam, Capture et Prise de vue…

Silverlight 4 nous a amené beaucoup de nouveautés intéressantes. La gestion de la Webcam en fait partie. Comme activer un camera, comment afficher un flux vidéo, comment permettre des capture d’image et les sauvegarder en jpeg ? Autant de question auxquelles ce billet va tenter de répondre !

L’exemple

J’aime bien commencer par l’exemple car on comprend mieux la suite…

La démo ci-dessous permet de faire toutes les choses présentées en introduction. Elle n’est pas parfaite, c’est juste une démo à laquelle j’ai ajouté plusieurs fonctions au fur et à mesure. Donc pas un modèle, ni de programmation, ni de design. Mais enfin, elle fait le job et elle illustrera parfaitement le code (que nous verrons plus bas).

Nota: A nos amis qui lisent le blog avec un flux RSS, sachez qu’aussi pratique que ces flux soient, vous vous privez à chaque fois des démos vivantes que je publie dont vous ne voyez que la balise. N’hésitez à venir sur le billet, ici sur Dot.Blog, pour bénéficier de la totalité du show !

[silverlight:source=/SLSamples/SLWebCam/SLWebLinq.xap;width=400;height=400]

Quelques explications et scenarii à tester.

L’application est plus intéressante en mode plein écran, commencez par cliquer sur le bouton vert “Full Screen”.

Cliquez sur le bouton bleu “Select Camera”. Un dialogue Silverlight vous avertira que le grand méchant que je suis veux prendre possession de votre Webcam. Comme l’application est purement locale, n’hésitez pas à répondre par l’affirmative, promis je n’ai pas mis d’espion, vous pouvez tester même en slip. Ce qui ne m’intéresse pas d’ailleurs (sauf si vous êtes une jolie blonde, là je regrette mon honnêteté :-) ). A noter que la sélection de la caméra peut être entérinée pour cette application en cochant la checkbox du dialogue; cela évite d’avoir à répondre sans cesse à la question. Notons aussi que c’est à ce niveau qu’une application réelle pourrait laisser le choix de la caméra à utiliser si plusieurs étaient détectées. Dans la démo j’ai optée pour celle que vous avez indiquée comme “défaut” dans les réglages de votre PC ou Mac.

Cliquez sur le bouton jaune “Formats”, cela affichera un dialogue (qui arrivera de la gauche) contenant une listbox avec tous les formats reconnus par votre WebCam. Nous verrons plus loin que par un petite requête Linq le programme va choisir automatiquement la configuration qui offre la meilleure résolution (on pourrait faire le contraire, bien entendu).

Maintenant cliquez sur le bouton orange “Capture”.

Si tout va bien, et selon comment est réglée votre Webcam, vous devriez voir apparaitre une tête que vous connaissez bien : la vôtre !

Au passage, le bouton “Capture” s’est transformé en bouton “Stop” pour arrêter la capture. De même le texte tout en bas de l’écran rappelle la résolution qui a été choisie par l’application. Enfin un petit bouton rouge centré en bas vous propose “Shoot it!”.

Lorsque vous aurez recoiffé votre mèche (ou remis un coup de rouge à lèvre), prenez la pause et cliquez sur ce dernier bouton. Clic-clac ! (je fais le bruit car je n’ai ajouté de bruitage à l’application). Le bouton rouge se transforme en “SAVE IT!”

Cliquez dessus et sélectionner un répertoire où stocker ce merveilleux cliché. (Mes documents, mes images, au hasard).

Allez vérifier dans le répertoire en question, votre photo s’y trouve bien ! Merveilleux non ?

Le code

Plusieurs choses sont à dire sur ce code. Je vais essayé de d’être clair sur chaque fonction.

Sélection de la caméra

Pour sélectionner la caméra j’ai fait une méthode qui retourne un objet VideoCaptureDevice :

   1: private VideoCaptureDevice getCamera()
   2: {
   3:   if (!CaptureDeviceConfiguration.AllowedDeviceAccess && !      
   4:                CaptureDeviceConfiguration.RequestDeviceAccess())
   5:   return null;
   6:   return CaptureDeviceConfiguration.GetDefaultVideoCaptureDevice();
   7: }

Le test sert à éviter de reposer la question que Silverlight vous pose la première fois et ce dans le cas où vous avez coché la checkbox. Ce choix est mémorisé par le plugin et cela évite d’ennuyer l’utilisateur s’il a déjà autorisé l’application à utiliser la caméra.

Si tout est ok, la méthode retourne la device de capture video par défaut.

Obtenir les formats

Voici le code :

   1: private void btnFormats_Click(object sender, RoutedEventArgs e)
   2: {
   3:   if (currentCamera == null) currentCamera = getCamera();
   4:   if (currentCamera == null) return;
   5:   lbFormats.ItemsSource = 
   6:        (from VideoFormat f in currentCamera.SupportedFormats select f).ToList();
   7:   lbFormats.ItemTemplate = Helper.GenerateFrom(typeof(VideoFormat));
   8:   OpenFormatsAnim.Begin();
   9:  }

Plusieurs “feintes” ici. Je passe la logique qui vérifie si la caméra a déjà été sélectionnée pour arriver à l’affichage des formats : une requête Linq toute simple qui remonte la liste de tous les formats supportés. La feinte la plus intéressante est la ligne 7 où j’attribue à ItemTemplate de la listbox un mystérieux “Helper.GenerateFrom(..)”. J’y reviens dans deux secondes. La séquence est bouclée par l’appel à l’animation qui fait apparaître la listbox.

DataTemplate automatique

J’avais commencé le projet sous VS, donc pas terrible pour le templating, et vu la simplicité du code il n’y a pas non plus de datasource pratique pour écrire le DataTemplate sous Blend. Bref, je suis parti en code, ma punition c’est d’y rester… Mais autant s’en sortir de façon souple, originale et réutilisable.

Regardons la classe Helper :

   1: public static class Helper
   2:     {
   3:         public static DataTemplate GenerateFrom(Type type)
   4:         {
   5:             var pf = type.GetProperties();
   6:             var sb = new StringBuilder();
   7:             sb.Append(
   8:             "<DataTemplate xmlns=\"http://schemas.microsoft.com/client/2007\">");
   9:             sb.Append(
  10:             "<Border BorderBrush=\"#FF001170\" BorderThickness=\"1\" CornerRadius=
  11:                                        \"5\" RenderTransformOrigin=\"0.5,0.5\">");
  12:             sb.Append("<StackPanel>");
  13:             foreach (var fieldInfo in pf)
  14:             {
  15:                 sb.Append("<StackPanel Orientation=\"Horizontal\" >");
  16:                 sb.Append(
  17:                   "<TextBlock Text=\"" + fieldInfo.Name + "\" Margin=\"1\" />");
  18:                 sb.Append(
  19:          "<TextBlock Text=\"{Binding " + fieldInfo.Name + "}\" Margin=\"1\" />");
  20:                 sb.Append("</StackPanel>");
  21:             }
  22:             sb.Append("</StackPanel>");
  23:             sb.Append("</Border>");
  24:             sb.Append("</DataTemplate>");
  25:  
  26:             return (DataTemplate)XamlReader.Load(sb.ToString());
  27:         }
  28:     }

La présentation n’est pas au mieux j’ai du limiter en largeur pour que cela reste lisible pour vous.

Cette classe contient une méthode GenerateFrom qui prend un type en paramètre. Et elle retourne un DataTemplate.

La ruse consiste ici à construire dans un StringBuilder toute la syntaxe Xaml d’un DataTemplate totalement adapté au type passé en paramètre.

D’abord un border entoure l’ensemble, ensuite un StackPanel vertical contiendra la liste des propriétés publiques, enfin, chaque propriété est enchâssée dans un StackPanel Horizontal qui contient un premier TextBlock affichant le nom de la propriété ainsi qu’un second dont la propriété Text est bindée au nom de cette même propriété.

La réflexion est utilisée pour itérer sur l’ensemble des propriétés publiques.

En fin de séquence, avec l’aide d’un XamlReader je transforme la chaine de caractères Xaml construire par code en un véritable objet transtypé correctement en Datatemplate.

De la génération dynamique de Xaml au runtime rapide et pratique. On peut imaginer plus sophistiquer, réutiliser une sorte de modèle pour les balises de présentation créé sous Blend afin de récupérer un code Xaml moins “brut” niveau look.

N’empêche, cette petite séquence génère des DataTemplate à la demande, au runtime, pour n’importe quelle classe. A conserver dans un coin, ça vous servira peut-être.

La capture

Passons à la vidéo. D’abord il faut savoir que les vidéos s’utilisent comme des brosses, et une brosse ça sert à peindre. Donc notre application est en réalité remplie par un simple Rectangle. C’est lui qui sera “peint” par la brosse video que nous créerons.

Le choix du format

   1: var format = (from VideoFormat f in currentCamera.SupportedFormats
   2:                 orderby f.PixelWidth * f.PixelHeight descending
   3:                 select f).FirstOrDefault<VideoFormat>();
   4:  if (format != null) currentCamera.DesiredFormat = format;

Une autre requête Linq nous sert ici à sélectionner automatiquement le format offrant la meilleure résolution. On pourrait bien entendu prendre en compte la profondeur en bits de l’image, ou d’autres critères ou bien les traiter autrement (prendre au contraire l’image la plus petite si c’est pour créer un photomaton pour une fiche client par exemple).

Lancer la capture

   1: currentSource = new CaptureSource();
   2: currentSource.VideoCaptureDevice = currentCamera;
   3: if ((currentSource.State == CaptureState.Started) 
   4:        || (currentSource.State == CaptureState.Failed)) return;
   5:  
   6: var videoBrush = new VideoBrush { Stretch = Stretch.None };
   7: videoBrush.SetSource(currentSource);
   8: rectVideo.Fill = videoBrush; // this is a rectangle
   9: currentSource.Start();

Il faut obtenir un nouvel objet CaptureSource puis lui affecter la device de capture sélectionnée.

Quelques tests permettent d’éviter les problèmes (si la capture est déjà commencée ou si la source n’a pu acquérir la device, on annule la séquence par un return. Un vrai programme tenterait d’être plus user friendly en affichant des messages et en essayant de proposer une solution.

Ensuite on crée une VideoBrush à laquelle on assigne comme source la CaptureSource initialisée au dessus. Puis on remplit le rectangle, notre toile de ciné en quelque sorte, en attribuant la brosse à sa propriété Fill. Et on démarre la capture. Et votre bobine ébahie apparaît à l’écran:-)

On note qu’en utilisant le même procédé on peut remplir des lettres ou des formes avec une video. Ce n’est pas un truc très courant, mais ça peut donner un certain style à un écran d’accueil par exemple.

Prendre une photo

   1: private void btnShoot_Click(object sender, RoutedEventArgs e)
   2: {
   3:   if (btnShoot.Tag == null) // no image to save
   4:    {
   5:      if (currentSource == null) return;
   6:      if (currentSource.State != CaptureState.Started) return;
   7:      currentSource.CaptureImageCompleted += currentSource_CaptureImageCompleted;
   8:      currentSource.CaptureImageAsync();
   9:      return;
  10:    }
  11:    var sd = 
  12:          new SaveFileDialog { DefaultExt = "jpg", Filter = "Jpeg Images|*.jpg" };
  13:    if (sd.ShowDialog() != true) return;
  14:    try
  15:    {
  16:       using (var fs = (Stream)sd.OpenFile())
  17:        {
  18:          fs.Write((byte[])btnShoot.Tag, 0, ((byte[])btnShoot.Tag).Length);
  19:          fs.Close();
  20:          btnShoot.Tag = null;
  21:          btnShoot.Content = "Shoot it!";
  22:         }
  23:    }
  24:    catch (Exception)
  25:    {... }
  26:         
  27: }

Le bouton de prise de vue sert à deux chose comme nous l’avons vu. Au départ il déclenche le processus d’enregistrement de la vue. Comme ce processus est asynchrone on ne peut pas lancer le dialogue SaveFile à la suite puisque la photo ne sera pas encore arrivée.

En revanche dans le code recevant la photo, on place cette dernière dans le Tag du bouton. Son titre devient “Save It!” et quand on clique dessus, puisque le Tag n’est pas nul il va remplir la fonction de sauvegarde du fichier. Ici il est possible d’afficher le dialogue SafeFile car l’appel à ce dernier est bien intégré dans une méthode déclenchée directement par l’utilisateur.

Une application réelle utiliserait plutôt une liste de photos qui s’agrandirait à chaque “shoot” et présenterait certainement un bandeau de tous les clichés. C’est depuis ce bandeau que l’utilisateur pourrait choisir quelles photos sauvegarder ou supprimer. Cela serait plus propre. Ma démo ne va pas jusqu’à là bien entendu, d’où la “combine” du Tag, un peu spartiate mais qui marche.

De WriteableBitmap à Jpeg

Je vous ai un peu menti… mea culpa, mais c’est pour la bonne cause, pour ne pas encombrer trop le discours.

En réalité dans la gestion de l’événement CaptureImageComplet ce n’est pas directement le résultat que je place dans le Tag du bouton, car ce résultat est un WriteableBitmap, pas un jpeg.

De plus Silverlight ne sait pas encoder du jpeg :-(

Alors il y a encore une feinte !

Ce qui est stocké dans le Tag du bouton est une image jpeg correctement encodée. Mais par quelle magie ? … L’utilisation d’une librairie externe.

Il en existe plusieurs, plus ou moins complètes, voire payantes pour certaines.

Ici j’ai choisi JFCore, qui est un encoder JPEG simple et rapide. Pour faire tourner le code de l’exemple il vous faudra télécharger JFCore à cette adresse : JFCore.

Attention c’est l’adresse d’un Trunk Subversion, il vous faudra Turtoise ou Ankh ou un outil Subversion pour récupérer le paquet. Attention n°2 : le code est écrit pour SL 2, quand on le charge dans VS 2010 il y a une étape de traduction mais tout se passe bien, le code est fiable et passe sans problème. N’oubliez pas de faire passer les projets de la solution en mode Silverlight 4 et .NET 4.0.

Il existe une autre librairie du même genre pour encoder en PNG : Dynamic image generation in Silverlight. A vous de tester.

Le code magique est donc celui qui transforme la WriteableBitmap en Jpeg, une fois compris qu’on utilise ici un encoder à ajouter (JFCore) :

   1: void currentSource_CaptureImageCompleted(object sender, CaptureImageCompletedEventArgs e)
   2:         {
   3:             btnShoot.Tag = null;
   4:             if (e.Result == null) return;
   5:             var width = e.Result.PixelWidth;
   6:             var height = e.Result.PixelHeight;
   7:             const int bands = 3;
   8:             var raster = new byte[bands][,];
   9:  
  10:             //Convert the Image to pass into FJCore
  11:             //Code From http://stackoverflow.com/questions/1139200/using-fjcore-to-encode-silverlight-writeablebitmap
  12:             for (int i = 0; i < bands; i++)
  13:             { raster[i] = new byte[width, height]; }
  14:  
  15:             for (int row = 0; row < height; row++)
  16:             {
  17:                 for (int column = 0; column < width; column++)
  18:                 {
  19:                     int pixel = e.Result.Pixels[width * row + column];
  20:                     raster[0][column, row] = (byte)(pixel >> 16);
  21:                     raster[1][column, row] = (byte)(pixel >> 8);
  22:                     raster[2][column, row] = (byte)pixel;
  23:                 }
  24:             }
  25:             var model = new ColorModel { colorspace = ColorSpace.RGB };
  26:             var img = new FluxJpeg.Core.Image(model, raster);
  27:  
  28:             //Encode the Image as a JPEG
  29:             var stream = new MemoryStream();
  30:             var encoder = new FluxJpeg.Core.Encoder.JpegEncoder(img, 100, stream);
  31:             encoder.Encode();
  32:  
  33:             //Back to the start
  34:             stream.Seek(0, SeekOrigin.Begin);
  35:  
  36:             //Get the Bytes and write them to the stream
  37:             var binaryData = new Byte[stream.Length];
  38:             long bytesRead = stream.Read(binaryData, 0, (int)stream.Length);
  39:  
  40:             btnShoot.Content = "SAVE IT!";
  41:             btnShoot.Tag = binaryData;
  42:         }
  43:     }

Je vous laisse regarder (en sautant les parties purement cosmétiques gérant les boutons).

Conclusion

Jouer avec les webcams, prendre des clichés, les enregistrer en Jpeg, tout cela n’est pas si compliqué, mais réclame la connaissance de quelques astuces. J’espère que ce billet vous aura apporter le minimum vital pour commencer à jouer avec cette technologie qui, bien utilisée, peut largement agrémenter des tas de logiciels (comme l’intégration de la photo d’un produit ou d’un client ou d’un salarié directement en quelques clics). Fabriquer un “trombinoscope” qui permet à chacun en appelant l’application de se prendre en photo avec sa webcam au lieu de payer un photographe qui loupera la moitié des gens (partis en extérieur) le jour de son passage (couteux) est un exemple. Vous en trouverez bien d’autres j’en suis certain !

Stay Tuned !

Le projet :

blog comments powered by Disqus