Loupe

#Windows : comment détecter un QRcode devant la webcam

giphyLe grand rush des Techdays est maintenant passé depuis un moment et il est temps de revenir un peu sur ce qu’il s’est passé. Vous l’avez peut-être vu, la ré-édition de badges était réalisé à l’aide de tablettes Surface (et d’hôtesses :)). L’utilisation se devait d’être la plus simple possible : je scanne mon badge existant, il est reconnu et je n’ai rien d’autre à faire. Dans cet article, nous allons présenter le code mis en place pour arriver à cette solution.

 

Mise en place du contrôle

Un lecteur de QRCOde est quelque chose que nous allons potentiellement réutiliser dans plusieurs projets et il est donc intéressant de le créer sous la forme d’un contrôle. Pour cela, nous passons par Visual Studio et le template associé (ajouter –> nouveau –> TemplatedControl) qui va nous créer la classe et le fichier generic.xaml où nous définirons le template.

 

Dans son template, nous allons simplement mettre un progress ring à afficher pendant l’initialisation de la caméra et un Control de type CaptureElement qui affichera le flux vidéo à l’utilisateur.

<ControlTemplate TargetType="qrCodeRecognizer:QRCodeRecognizer">
    <Border Background="{TemplateBinding Background}"
            BorderBrush="{TemplateBinding BorderBrush}"
            BorderThickness="{TemplateBinding BorderThickness}">
        <Grid>
            <ProgressRing VerticalAlignment="Center"
                          HorizontalAlignment="Center"
                          Width="70"
                          Height="70"
                          Foreground="White"
                          IsActive="True" />
            <CaptureElement x:Name="PART_captureElement"
                            RenderTransformOrigin="0.5,0.5">
                <CaptureElement.RenderTransform>
                    <CompositeTransform />
                </CaptureElement.RenderTransform>
            </CaptureElement>
        </Grid>
    </Border>
</ControlTemplate>

Aussi, étant donné que nous allons vouloir éxecuter un bout de code lorsqu’un QRCode est détecté, nous allons ajouter une dependency property de type ICommand a ce contrôle! Rien de compliqué de ce côté là, il s’agit d’une déclaration classique :

public static readonly DependencyProperty CommandProperty 
  = DependencyProperty.Register(
             "Command",
             typeof(ICommand), 
             typeof(QRCodeRecognizer),
             null
             );

public ICommand Command
{
    get
    {
        return (ICommand)GetValue(CommandProperty);
    }

    set
    {
        SetValue(CommandProperty, value);
    }
}

 

Cycle de vie du contrôle

Il est très important de bien gérer le cycle de vie de notre control car nous allons utiliser la caméra qui est un contrôle natif qu’il faut considérer comme une ressource. Dans notre cas nous allons :

  • Démarrer le processus de lecture lorsque le contrôle est ajouté à l’arbre visuel (événement Loaded)
  • Stopper le processus de lecture lorsque le contrôle est retiré de l’arbre visuel (événément UnLoaded)
  • Et surtout, s’abonner aux changements de visibilité de la fenêtre correspondant à l’application pour stopper/allumer le processus.

 

//Abonnement fait dans l'handler de Loaded
Window.Current.VisibilityChanged += Current_VisibilityChanged;

//Traitement intéressant
private void Current_VisibilityChanged(object sender, VisibilityChangedEventArgs e)
{
    if (e.Visible && _relaunchOnWindowsVisibleAgain)
    {
        StartAsync();
    }
    else
    {
        _relaunchOnWindowsVisibleAgain = Stop();
    }
}

Boucle de détection du QRCode

Plongeons maintenant dans le code de cette méthode “StartAsync” chargée de déclencher la reconnaissance du QRCode. Pour cela nous allons utiliser le package ZXing disponible sur Nuget.

Voici en synthèse ce que fait l’extrait ci-dessous (un peu long en effet :)) :

  1. J’intialise l’objet MediaCapture qui servira à effectuer la capture des images de la caméra
  2. Je récupère la caméra frontale de la tablette dans les devices disponibles
  3. J’initialise mon media capture avec cette caméra frontale
  4. Je vais ensuite récupérer toutes les configurations de captures possibles sur le device choisi et prendre la première. Cela correspond à celle de meilleure qualité dans notre cas.
  5. J’assigne la configuration et je démarre la preview pour que l’utilisateur puisse voir ce que voit la caméra
  6. Ensuite je lance une boucle infinie conditionnée par un cancellation token qui va faire plusieurs choses :
    1. Lire une image de la caméra
    2. L’encoder en jpg et la mettre dans un WriteableBitmap
    3. Extraire les pixels correspondant à l’image
    4. Demander à ZXing d’essayer de décoder un QRCode  (en utilisant la classe BarCodeReader de ce SDK)
    5. Si un QRCode est trouvé, executer la commande de notre Control
    6. Attendre 500 ms, et…. recommencer !
Dans notre code en production, nous avons une gestion un peu plus poussé pour permettre d'annuler le processus en utilisant un CancellationToken mais je vous passe les détails pour essayer de simplifier un peu l'exemple.

 

public async Task StartAsync()
{
   Stop();

    MediaCapture = new MediaCapture();

    try
    {
        var videodevices = await DeviceInformation.FindAllAsync(DeviceClass.VideoCapture);
        var frontCamera = videodevices
            .FirstOrDefault(
            item => item.EnclosureLocation != null 
                && item.EnclosureLocation.Panel == Windows.Devices.Enumeration.Panel.Front);

        if (frontCamera == null)
        {
            return;
        }

        await MediaCapture.InitializeAsync(
          new MediaCaptureInitializationSettings
        {
            PhotoCaptureSource = PhotoCaptureSource.VideoPreview,
            StreamingCaptureMode = StreamingCaptureMode.Video,
            VideoDeviceId = frontCamera.Id
        });
    }
    catch
    {
        Stop();
        return;
    }

    //Récupération de la première information disponible (correspond à 
    // meilleure qualitée dans notre cas
    var videoSettings = MediaCapture.VideoDeviceController
        .GetAvailableMediaStreamProperties(
        MediaStreamType.VideoPreview).FirstOrDefault() as VideoEncodingProperties;

    //Assignation de ce settings
    await MediaCapture.VideoDeviceController.SetMediaStreamPropertiesAsync(
        MediaStreamType.VideoPreview, videoSettings);

    CaptureElement.Source = MediaCapture;

    //Démarrage de la preview pour l'utilisateur
    await MediaCapture.StartPreviewAsync();

    while (!cancellationTokenSource.IsCancellationRequested)
    {
        try
        {
            //Création d'un JPEG à la taille de la preview
            var properties = ImageEncodingProperties.CreateJpeg();
            properties.Width = videoSettings.Width;
            properties.Height = videoSettings.Height;

            MediaCapture.VideoDeviceController.
                         Focus.TrySetValue(50);

            //Capture de l'image dans un writeable bitmap
            WriteableBitmap writeableBitmap;
            using (var memoryStream = new InMemoryRandomAccessStream())
            {
                await MediaCapture.CapturePhotoToStreamAsync(properties, memoryStream);
                if (cancellationTokenSource.IsCancellationRequested)
                {
                    return;
                }

                writeableBitmap = new WriteableBitmap((int)properties.Width, (int)properties.Height);
                memoryStream.Seek(0);
                await writeableBitmap.SetSourceAsync(memoryStream);
            }

            var array = writeableBitmap.PixelBuffer.ToArray();

            //Décodage dans un autre thread
            var text = await Task.Run(() =>		
	new BarcodeReader().Decode(array, properties.Width, properties.Height, BitmapFormat.BGRA32));

            if (text.Length > 0)
            {
                if (Command != null)
                {
                    Command.Execute(text);
                }

             }
        }
        catch (Exception ex)
        {
            Debug.WriteLine("Error when looking for QRCode : " + ex);
        }

        //Attente de 500 ms
        await Task.Delay(500);

    }
}

 

Et maintenant, il reste à éteindre le processus de lecture avec la méthode Stop. Pour le coup, c’est plus court car il s’agit simplement d’appeler Dispose sur notre MediaCapture :

if (MediaCapture != null)
{
    MediaCapture.Dispose();
}

MediaCapture = null;

Envie d’en savoir plus ?

Je vous invite à lire les différents articles de Thomas Ouvré sur le sujet : http://blogs.infinitesquare.com/b/touvre/tags/wp8

Bon code à tous !

Photo de profil

Ces billets pourraient aussi vous intéresser

Vous nous direz ?!

Commentaires

comments powered by Disqus