À la découverte d'Azure Functions

Dans cet article, nous allons découvrir comment utiliser Azure Functions, nous développerons un nano-service de reconnaissance faciale se basant sur les APIs Microsoft Cognitive. Vous pourrez ensuite réutiliser ce nano-service dans vos projets, quel que soit le langage que vous utilisez.

Azure Functions ?

Microsoft a très récemment mis à disposition un nouveau joujou s'appellant Azure Functions (accessible via l'interface d'Azure) permettant de développer des micro/nano-services "serverless"[1].

Ce service est particulier dans le sens où vous n'avez plus à gérer de serveurs, ni à vous soucier de la charge qu'ils pourraient subir ou encore de leurs mises à jours.

Aperçu d'Azure Functions

Azure Functions permet, comme son nom l'indique, de développer des fonctions et ... de les héberger sur Azure !
Ces fonctions pourront être exécutées de plusieurs façons :

  • Déclenchées par un événement (ex. : une image qui est déposée dans un BLOB storage, une ligne qui est ajoutée dans une base de données, etc.)
  • Déclenchées à intervalle régulier (un CRON en gros, ce truc dont je n'arrive jamais à retenir l'ordre des arguments)
  • Déclenchées par un appel à une URL (c'est la méthode que j'ai utilisée pour cet article)

Choix du type de fonction

Ces fonctions pourront être développées dans différents langages : C#, Python, PHP, JavaScript, PowerShell, Bash, ... Il est même possible d'utiliser du code compilé.

Choix du langage

Il existe déjà pas mal de templates de fonctions (pas loin de 60 à l'heure où j'écris ces lignes) mais vous pouvez bien sûr partir de zéro.

Pour cet article, je vais prendre le template de Webhook en C#.

Les templates disponibles

Une fois la fonction créée [1:1], vous devriez arriver sur une page avec un éditeur de texte, du code déjà en place, un fenêtre de log, etc.

Aperçu de l'IDE

Pour l'exemple, j'ai développé un petit service de reconnaissance facial utilisant Microsoft Cognitive Face API. Il faudra donc que vous récupériez des clés d'API pour ce service (il existe une version gratuite limitée à 20 appels / minute (ce qui est plus que suffisant pour notre test).

Les prix de Microsoft Cognitive Face API

Une fois cette clé récupérée, il y aura quelques étapes[1:2] à effectuer au niveau de MS Cognitive :

  • Création d'un Person Group
  • Ajout d'une ou deux personnes et des photos correspondantes

Pour vous faciliter la vie, il existe un projet de Microsoft sur GitHub permettant de faire ça facilement avec une petite interface sympa : Intelligent Kiosk (vous trouverez ça dans le menu -> Face Identification Setup).

Nous allons maintenant récupérer la clé du Person Group créé (vous pouvez vous aider de l'open API testing Console pour ça).

Une fois ces deux clés récupérées, on va les rentrer au niveau des paramètres de notre app. Pour ça, il faut se rendre dans les "Functions app settings" -> "App Service Setting" (sous la catégorie "Manage") -> Application settings -> Et là, nous pouvons rentrer nos deux clés en tant que nouveaux settings (n'oubliez pas de faire un petit save) :

  • FACE_API_KEY
  • PERSON_GROUP_ID

Rentre des clés d'API dans les settings

Ces deux variables pourront maintenant être utilisées dans notre code seulement en appelant la méthode Environment.GetEnvironmentVariable().

La partie fun : le code !

Ici, je vais faire une fonction qui prend une image en entrée. Pour lui envoyer l'image à analyser, on fera un simple POST avec un body en binaire (qui contiendra donc l'image).
La fonction nous renverra simplement un Json avec les noms des personnes reconnues (et qui auront donc été entrées dans notre Person Group auparavant).

Il y aura quelques étapes à développer :

  • Réception de la photo
  • Détection des visages sur la photo
  • Identification de chaque visage[1:3]
  • Obtention du nom de la personne correspondant à un visage
  • Renvoi du (des) nom(s) trouvé(s) ou d'un message d'erreur si nécessaire

On commence par virer tout ce qui se trouve dans la fonction Run et on rajoute quelques using que nous allons utiliser.
(Pour info : Le #r permet de référencer un assembly externe)

#r "Newtonsoft.Json"

using System.Net;
using System.Net.Http.Headers;
using Newtonsoft.Json;
using System.Text;

public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
{
}

On va tout d'abord récupérer l'image et l'envoyer à une fonction qui devra la traiter :

public static TraceWriter logs; // Just for logging purpose

public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
{
    logs = log;
    var picture = await req.Content.ReadAsStreamAsync();

    string answer = await LaunchDetection(picture);

    if (String.IsNullOrEmpty(answer))
    {
        return req.CreateResponse(HttpStatusCode.BadRequest);
    }

    var response = new HttpResponseMessage()
    {
        Content = new StringContent(answer),
        StatusCode = HttpStatusCode.OK,
    };
    response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");

    return response;
}

Le code de cette fonction va appeler différentes autres fonctions qui auront pour but de détecter les visages sur l'image, de trouver à qui ça correspond dans notre Person Group et de nous renvoyer les informations à propos de ces personnes.
Nous renvoyons un résultat au format Json : soit une erreur, soit la liste des personnes reconnues.

static async Task<string> LaunchDetection(Stream image)
{
    var personsIdentified = new List<Person>();

    // First step : detecting faces
    var facesDetected = await DetectFaces(image); 
    if (facesDetected.Count() > 0)
    {
        // We will try to recognize each faces in the picture
        foreach(var face in facesDetected)
        {
            // We try to identify to whom the face belong
            var faceIdentifyResult = await IdentifyFace(face.faceId);
            if (faceIdentifyResult.Count() > 0)
            {
                if (faceIdentifyResult[0].candidates.Count() > 0)
                {
                    // We assume that the 1st candidate returned is the good one
                    var person = await IdentifyPerson(faceIdentifyResult[0].candidates[0].personId);

                    // Add the person to the persons list
                    personsIdentified.Add(new Person {Name = person.name});
                }
            }
            else
            {
                // If no face is *recognized* in the picture we send an error message
                return "{\"Error\" : \"No face recognized.\"}";
            }
        }
    }
    else
    {
        // If no face is *detected* in the picture we send an error message
        return "{\"Error\" : \"No face detected.\"}";
    }

    if(personsIdentified.Count() == 0)
    {
        // If we've found faces in the picture but don't know the persons
        return "{\"Error\" : \"Only unknow person identified.\"}";
    }

    return JsonConvert.SerializeObject(personsIdentified);
}

public class Person
{
    [JsonProperty("Name")]
    public string Name {get; set;}
}

Au tour de la fonction de détection des visages. Ce n'est qu'un appel à une API, donc le code reste assez simple.

// We send our picture as a stream to the MS Cognitive Face APIs and ask it to return us the informations about the face
static async Task<List<DetectedFacesDTO>> DetectFaces(Stream image)
{
    var faceDetectedResult = new List<DetectedFacesDTO>();
    using (var client = new HttpClient())
    {

        logs.Info("Face detection has started ...");

        var content = new StreamContent(image);
        var url = "https://api.projectoxford.ai/face/v1.0/detect?returnFaceId=true&returnFaceLandmarks=false";
        client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", Environment.GetEnvironmentVariable("FACE_API_KEY"));

        content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
        var httpResponse = await client.PostAsync(url, content);

        if (httpResponse.StatusCode == HttpStatusCode.OK)
        {
            logs.Info("- Face(s) have been detected ...");

            string faceDetectedResultJson = await httpResponse.Content.ReadAsStringAsync();
            faceDetectedResult = JsonConvert.DeserializeObject<List<DetectedFacesDTO>>(faceDetectedResultJson);

            logs.Info("- Number of face(s) detected : " + faceDetectedResult.Count());

        }
        else
        {
            logs.Info("No face(s) have been detected ...");
        }

        logs.Info("Face detection has finished ...");
    }
    return faceDetectedResult;
}

public class DetectedFacesDTO
{
    [JsonProperty("faceId")]
    public string faceId { get; set; }

    [JsonProperty("faceRectangle")]
    public FaceRectangle faceRectangle { get; set; }

    public class FaceRectangle
    {
        [JsonProperty("top")]
        public int top { get; set; }

        [JsonProperty("left")]
        public int left { get; set; }

        [JsonProperty("width")]
        public int width { get; set; }

        [JsonProperty("height")]
        public int height { get; set; }
    }
}

La fonction d’identification des visages. Encore une fois ce n'est qu'un appel à une API.

// We send the data about the face and ask it to return us to whom the face belong
static async Task<List<IdentifiedFaceDTO>> IdentifyFace(string faceId)
{
    var faceIdentifyResult = new List<IdentifiedFaceDTO>();

    using (var client = new HttpClient())
    {

        logs.Info("Face Identification has started ...");
        var infos = new FaceIdentityRequestDTO()
        {
            personGroupId = Environment.GetEnvironmentVariable("PERSON_GROUP_ID"),
            faceIds = new String[] { faceId },
            maxNumOfCandidatesReturned = 1,
            confidenceThreshold = 0.5f,
        };

        var url = "https://api.projectoxford.ai/face/v1.0/identify";
        client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", Environment.GetEnvironmentVariable("FACE_API_KEY"));

        var jsonObject = JsonConvert.SerializeObject(infos);
        var content = new StringContent(jsonObject.ToString(), Encoding.UTF8, "application/json");


        var httpResponse = await client.PostAsync(url, content);


        if (httpResponse.StatusCode == HttpStatusCode.OK)
        {
            logs.Info("- Face(s) have been identified ...");

            string faceIdentifyResultJson = await httpResponse.Content.ReadAsStringAsync();
            faceIdentifyResult = JsonConvert.DeserializeObject<List<IdentifiedFaceDTO>>(faceIdentifyResultJson);
        }
        else
        {
            logs.Info("- No face(s) have been identified ...");
        }

        logs.Info("Face identification has finished ...");
    }
    return faceIdentifyResult;
}

public class IdentifiedFaceDTO
{
    [JsonProperty("faceId")]
    public string faceId { get; set; }

    [JsonProperty("candidates")]
    public Candidate[] candidates { get; set; }

    public class Candidate
    {
        [JsonProperty("personId")]
        public string personId { get; set; }

        [JsonProperty("confidence")]
        public float confidence { get; set; }
    }
}

public class FaceIdentityRequestDTO
{
    public string personGroupId { get; set; }
    public string[] faceIds { get; set; }
    public int maxNumOfCandidatesReturned { get; set; }
    public float confidenceThreshold { get; set; }
}

Et enfin, la fonction qui va récupérer (entre autre) le nom de la personne. Encore de simples appels à des APIs.


// We send the data about the person and get some information (faces list, name, etc.) in return
static async Task<IdentifiedPersonDTO> IdentifyPerson(string personId)
{
    var getPersonResult = new IdentifiedPersonDTO(); 
    using (var client = new HttpClient())
    {

        logs.Info("People information retrieving has started ...");

        var personGroupId = Environment.GetEnvironmentVariable("PERSON_GROUP_ID");
        var url = $"https://api.projectoxford.ai/face/v1.0/persongroups/{personGroupId}/persons/{personId}";
        client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", Environment.GetEnvironmentVariable("FACE_API_KEY"));

        var httpResponse = await client.GetAsync(url);


        if (httpResponse.StatusCode == HttpStatusCode.OK)
        {
            logs.Info("- People information have been retrieved ...");

            string getPersonResultJson = await httpResponse.Content.ReadAsStringAsync();
            getPersonResult = JsonConvert.DeserializeObject<IdentifiedPersonDTO>(getPersonResultJson);

            logs.Info("- People name : " + getPersonResult.name);
        }
        else
        {
            logs.Info("- No people information have been retrieved ...");
        }
        logs.Info("People information retrieving has finished ...");
    }
    return getPersonResult;
}

public class IdentifiedPersonDTO
{
    [JsonProperty("personId")]
    public string personId { get; set; }

    [JsonProperty("persistedFaceIds")]
    public string[] persistedFaceIds { get; set; }

    [JsonProperty("name")]
    public string name { get; set; }

    [JsonProperty("userData")]
    public object userData { get; set; }
}

Quelques tests

Et voilà, votre fonction devrait compiler et est prête à être utilisée.
Pour tester, prenons un logiciel comme Postman et envoyons une photo à notre fonction.

Essayons d'abord avec une personne qui n'est pas dans notre Person Group, j'ai pris une image d'Obama :

Test de la fonction avec une personne inconnue
On peut voir qu'il a bien reconnu un visage, mais ne connait pas la personne ... Nickel !
Au niveau des logs ça donne ça :

2016-12-07T23:13:33.961 Function started (Id=de20422b-cc18-4c08-be76-4fad2f0abd4b)
2016-12-07T23:13:33.961 Face detection has started ...
2016-12-07T23:13:34.483 - Face(s) have been detected ...
2016-12-07T23:13:34.483 - Number of face(s) detected : 1
2016-12-07T23:13:34.483 Face detection has finished ...
2016-12-07T23:13:34.483 Face Identification has started ...
2016-12-07T23:13:35.028 - Face(s) have been identified ...
2016-12-07T23:13:35.028 Face identification has finished ...
2016-12-07T23:13:35.028 Function completed (Success, Id=de20422b-cc18-4c08-be76-4fad2f0abd4b)

Et maintenant avec une photo de moi (qui suis dans le Person Group) :

Test de la fonction avec une personne connue

Et les logs du coté d'Azure Functions :

2016-12-07T23:17:32.560 Function started (Id=a1cd1f73-b1ed-473a-8601-16c198c1931e)
2016-12-07T23:17:32.560 Face detection has started ...
2016-12-07T23:17:33.161 - Face(s) have been detected ...
2016-12-07T23:17:33.161 - Number of face(s) detected : 1
2016-12-07T23:17:33.161 Face detection has finished ...
2016-12-07T23:17:33.161 Face Identification has started ...
2016-12-07T23:17:34.416 - Face(s) have been identified ...
2016-12-07T23:17:34.431 Face identification has finished ...
2016-12-07T23:17:34.431 People information retrieving has started ...
2016-12-07T23:17:35.444 - People information have been retrieved ...
2016-12-07T23:17:35.444 - People name : Thomas
2016-12-07T23:17:35.444 People information retrieving has finished ...
2016-12-07T23:17:35.444 Function completed (Success, Id=a1cd1f73-b1ed-473a-8601-16c198c1931e)

Parfait, un dernier essai avec deux personnes qui sont dans le Person Group ? Allez :
Test de la fonction avec deux personnes connues

2016-12-07T23:23:45.184 Function started (Id=c5aa20fb-d992-4541-967e-8e50d6e0af18)
2016-12-07T23:23:45.184 Face detection has started ...
2016-12-07T23:23:45.766 - Face(s) have been detected ...
2016-12-07T23:23:45.766 - Number of face(s) detected : 2
2016-12-07T23:23:45.766 Face detection has finished ...
2016-12-07T23:23:45.766 Face Identification has started ...
2016-12-07T23:23:47.061 - Face(s) have been identified ...
2016-12-07T23:23:47.061 Face identification has finished ...
2016-12-07T23:23:47.061 People information retrieving has started ...
2016-12-07T23:23:47.271 - People information have been retrieved ...
2016-12-07T23:23:47.271 - People name : Thomas
2016-12-07T23:23:47.271 People information retrieving has finished ...
2016-12-07T23:23:47.271 Face Identification has started ...
2016-12-07T23:23:47.637 - Face(s) have been identified ...
2016-12-07T23:23:47.637 Face identification has finished ...
2016-12-07T23:23:47.637 People information retrieving has started ...
2016-12-07T23:23:47.863 - People information have been retrieved ...
2016-12-07T23:23:47.863 - People name : Fred
2016-12-07T23:23:47.863 People information retrieving has finished ...
2016-12-07T23:23:47.863 Function completed (Success, Id=c5aa20fb-d992-4541-967e-8e50d6e0af18)

On dirait bien que notre petite fonction ... fonctionne :D

Le code final de cette fonction est disponible sur le GitHub du MIC.

N'hésitez pas à laisser vos retours d'expérience ou vos questions sur Azure Functions dans les commentaires !



  1. Cette étape nous renvoie en fait une liste de personnes pouvant correspondre ainsi qu'un niveau de confiance. Pour simplifier, nous allons demander à l'API de nous retourner une seule personne avec un niveau de confiance au dessus de 50% ↩︎ ↩︎ ↩︎ ↩︎