Loupe

Création automatique de thumbnails avec Azure Functions et Cognitives services

Dans mon précédent article, nous avons découvert les « Azure Functions », leur architecture et les concepts de base pour bien débuter. Dans cet article nous allons mettre en place une fonction qui utilisera les cognitives services pour traiter des images !

 

La problématique : “Générer automatiquement des thumbnails”

Une des problématiques récurrentes lors de l’upload d’images depuis une application est la génération de thumbnails. Ce processus peut vite devenir problématique, car selon la taille des images uploadées, leur traitement peut être relativement long et l’utilisateur peut se retrouver « bloqué » devant une interface non utilisable !

Pour répondre à ce besoin, il est possible de mettre en place l’architecture suivante :

    - Une application cliente depuis laquelle des utilisateurs vont uploader des images

    - Un « blob storage » dans lequel les images seront uploadées

    - Une « Azure Function » de type « blob Trigger » : cette fonction sera exécutée lors de l’upload d’une image dans le blob storage et effectuera les traitements nécessaires sur l’image

     

Le rôle de la fonction

La fonction que nous allons mettre en place exécutera le workflow suivant :

  1. Récupération du blob uploadé dans le blob storage sous forme d’un stream
  2. Traitement du stream récupéré  pour créer un thumbnail (utilisation d’une des API « vision » des cognitives services)
  3. Création d’un blob à partir du nouveau stream renvoyé par l’API “vision” et stockage de ce dernier dans un conteneur du compte de stockage nommé « thumbnails »
  4. Ajout d’une ligne contenant l’url du blob original et l’url du blob thumbnail dans une Azure Table

 

Traitement des images avec les « Cognitives services »

Pour créer un thumbnail, le traitement de l’image est assez simple, notre objectif est de donner une dimension plus petite à nos images pour pouvoir par exemple utiliser ces thumbnails dans des applications clientes ou mobiles sans charger des images volumineuses. La manière classique serait de réduire l’image en utilisant les librairies .net, cependant dans notre cas nous allons utiliser une API des « cognitive services » de Microsoft pour réaliser ce traitement.

Les « cognitives services » sont un ensemble d’API mises à disposition par Microsoft permettant d’utiliser les données et algorithmes d’analyse et de traitements d’images, de textes et de videos. On retrouve 5 types d’API « Vision », « Speech », « Langage », « Knowledge » et « Search » :

cognitives-services

Nous allons utiliser l’API « Vision », qui propose de nombreuses API pour analyser et traiter les images. L’API qui nous intéresse se nomme « generateThumbnail ».

Pour utiliser ces API, il est nécessaire de se créer un compte ici, il est alors possible de récupérer un ensemble de token qui doivent être placés dans les headers des requêtes adressées aux API.

 

Création et configuration de la fonction

Création des ressources Azure

Pour fonctionner convenablement notre fonction a besoin d’un compte de stockage contenant :

  • un blob storage avec deux conteneurs « imgs » et « thumbnails »
  • une Azure Table nommée « Imgs »

 

Ces opérations peuvent être rapidement exécutées en quelques lignes de PowerShell :

$resourGroupName = "azureFunctions-is-rg"
$location = "West Europe"
$storageAccountName = "azurefuncstorage"

Login-AzureRmAccount

#Create the resource group
New-AzureRmResourceGroup -Name $resourGroupName -Location $location

# Create the storage account
New-AzureRmStorageAccount -Name $storageAccountName -ResourceGroupName $resourGroupName -Type Standard_LRS -Location $location  

#Create a context to use this storage account
$storageAccountKey = Get-AzureRmStorageAccountKey -ResourceGroupName $resourGroupName -StorageAccountName $storageAccountName
$ctx = New-AzureStorageContext $storageAccountName -StorageAccountKey $storageAccountKey.Key1

# Create the containers
New-AzureStorageContainer -Context $ctx -Name imgs -Permission Container
New-AzureStorageContainer -Context $ctx -Name thumbnails -Permission Container

# Create the Azure table
New-AzureStorageTable -Context $ctx -Name Imgs

 

Création de la fonction

Dans mon précèdent article, nous avons vu comment créer une « Function App », depuis le portail Azure ou depuis le « portail Azure Function ». Nous allons donc nous concentrer maintenant sur la création de la fonction et sa configuration.

Pour répondre à notre besoin, il est tout d’abord nécessaire de créer une fonction de type « blobTrigger ». Une fois le Template « BlobTrigger – C# » sélectionné il faut scroller vers le bas de la page pour spécifier le compte de stockage qui servira de source pour le blob trigger :

BlobTrigger

 

Configuration de la fonction

Une fois créée, la fonction ne contient qu’une seule ligne. C’est à l’intérieur de l’onglet « Integrate » qu’il est possible de configurer les différents « bindings » de la fonction :

function-empty

 

La configuration nécessaire pour notre fonction est la suivante :

  • Trigger : le blob storage configuré lors de la création de la fonction. Une variable de type « Stream » nommée « blob » correspond au blob uploadé. (voir 1*)
  • Outputs :
    • un conteneur de blob storage nommé « thumbnails ». Une variable de type « Stream » nommée « outputBlob » correspond au blob cible dans lequel le thumbnails sera créé.
    • Une Azure table nommée « Imgs ». Une variable de type « ICollector<Image> » mappe la Table Azure dans laquelle la fonction écrit. (voir 2*)

 

bindings

 

1*: Configuration du blob Trigger (behavior de type “trigger”)

config-blobTrigger

 

2*: Configuration des « Outputs » (behavior de type “out”)

config-outputBlob

config-outputTable

 

Dans la configuration des deux conteneurs qui servent de source et de cible à la fonction, on remarque la syntaxe suivante : containerName/{name}

Cette syntaxe permet de récupérer le nom du blob uploadé sous forme d’une variable « name » de type « string » au sein de la fonction.

 

Voici ci-dessous la configuration complète de la fonction :

{
  "bindings": [
    {
      "path": "imgs/{name}",
      "connection": "inputblobs_STORAGE",
      "name": "blob",
      "type": "blobTrigger",
      "direction": "in"
    },
    {
      "path": "thumbnails/{name}",
      "connection": "inputblobs_STORAGE",
      "name": "outputBlob",
      "type": "blob",
      "direction": "out"
    },
    {
      "tableName": "Imgs",
      "partitionKey": "imgs",
      "rowKey": "%rand-guid%",
      "connection": "inputblobs_STORAGE",
      "name": "outputTable",
      "type": "table",
      "direction": "out"
    }
  ],
  "disabled": false
}

 

Cette configuration peut être créée directement par interface graphique, ou injectée / récupérée en bas de l’onglet « Integrate » depuis l’éditeur avancé.

 

Utilisation d’une dll dans la fonction

Pour générer un thumbnail, l’API « generateThumbnails » que nous utilisons prend en paramètre une largeur et une hauteur. Il convient donc de calculer la taille du thumbnail cible afin que le thumbnail conserve les proportions de l’image originale.

Pour cela, quelques lignes de code suffisent, cependant pour des raisons de confort et de test, il est plus facile de rédiger du code dans une application console classique que directement à l’intérieur de la fonction Azure. Il est donc tout à fait possible de rédiger et tester son code dans une application console, de l’encapsuler à l’intérieur d’une dll .net puis d’utiliser cette dernière depuis la fonction Azure.

J’ai donc mis en place un dll nommé « WorkOnImages » qui encapsule la logique de « resizing » d’image :

dll

using System;
using System.IO;
using System.Drawing;

namespace WorkOnImages.Utils
{
    public class Thumbnails
    {
        public static int[] GenerateThumbnailsSize(Stream input, Size size)
        {
            using (var img = Image.FromStream(input))
            {
                int originalWidth = img.Width;
                int originalHeight = img.Height;

                float percentWidth = (float)size.Width / (float)originalWidth;
                float percentHeight = (float)size.Height / (float)originalHeight;
                var percentage = Math.Max(percentHeight, percentWidth);

                var width = (int)Math.Max(originalWidth * percentage, size.Width);
                var height = (int)Math.Max(originalHeight * percentage, size.Height);

                return new int[] { width, height };
            }
        }
    }
}

 

Pour pouvoir utiliser cette dll dans la fonction Azure, il faut l’uploader via FTP dans un dossier « bin » à la racine du dossier contenant les fichiers de la fonction :

Ftp

 

Le code de la fonction

Maintenant que la fonction est configurée et que la dll (WorkOnImages.dll) est utilisable depuis cette dernière, l’étape finale consiste à implémenter la fonction.

Le squelette de la fonction est semblable aux classes que nous avons l’habitude de rédiger, elle est divisée en plusieurs parties :

  • Import de « assemblies » et « using »
  • Déclaration des variables
  • Les méthodes utilisées dans la méthode « run »*
  • La méthode « run » déclenchée lorsqu’un blob est uploadé dans le conteneur “imgs” du compte de stockage. Cette méthode prend en paramètre les « inputs trigger » et « outputs » définis plus tôt.

* La méthode “run” utilise une méthode nommée « CreateThumbnailWithVisionAPI », cette méthode wrappe l’utilisation d’un HttpClient qui effectue un appel vers l’API « generateThumbnails ». Cette API de type « GET » prend en paramètres la largeur et la hauteur du thumbnail à générer, toutes deux calculées par la méthode statique « GenerateThumbnailsSize(Stream input, Size size) » présente dans la dll « WorkOnImages ».

// Import assemblies
#r "System.Drawing"
#r "WorkOnImages.dll"
#r "Microsoft.WindowsAzure.Storage"

using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Configuration;
using System.Drawing;
using Microsoft.WindowsAzure.Storage.Table;
// Namespace from the custom dll
using WorkOnImages.Utils;

// Storage account url
private static string storage = "https://inputblobs.blob.core.windows.net";

// Base container where images are uploaded
private static string imgContainer = "imgs";

// Target container where thumnails are stored
private static string thumnailsContainer = "thumbnails";

// Token used to authenticate request on the cognitive api 
private static string apiToken = "xxxxxxxxxxxxxxxxxxxxxxxxxxxx";

// Image object type stored in the Azure Table
public class Image
{
    public string PartitionKey { get; set; }
    public string RowKey { get; set; }
    public string Name { get; set; }
    public string baseUrl { get; set; }
    public string ThumbnailUrl { get; set; }
}

// Create the thumnail
public async static Task<Stream> CreateThumbnailWithVisionApi(int width, int height, bool smartCropping, string blobUrl)
{
    string requestUrl = $"https://api.projectoxford.ai/vision/v1.0/generateThumbnail?width={width}&height={height}&smartCropping={smartCropping}";
    
    using (var client = new HttpClient())
    {
        // Set authorization header to call the vision API
        client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", apiToken);
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

        string postContent = $"{{ \"url\": \"{blobUrl}\" }}";

        StringContent stringContent = new StringContent(postContent, Encoding.UTF8, "application/json");

        HttpResponseMessage response = await client.PostAsync(requestUrl, stringContent);
        
        return await response.Content.ReadAsStreamAsync();
    }
}

public async static Task Run(Stream blob, string name, Stream outputBlob, ICollector<Image> outputTable, TraceWriter log)
{
    string blobUrl = $"{storage}/{imgContainer}/{name}"; 
    string thumnailUrl = $"{storage}/{thumnailsContainer}/{name}"; 
    
    log.Verbose($"new blob is uploaded : {name}"); 
    
    int[] sizes = Thumnails.GenerateThumbnailsSize(blob, new Size(50,50));
    int width = sizes[0];
    int height = sizes[1];
     
    var thumbnail = await CreateThumbnailWithVisionApi(width, height, true, blobUrl);
    
    if (thumbnail != null)
    {
        log.Verbose($"Thumnail was created !");
        
        // Create the output blob
        await thumbnail.CopyToAsync(outputBlob, 4096, CancellationToken.None);
        
        // Add an "Image" entity in the Azure Table
        outputTable.Add(
            new Image() 
            { 
                RowKey = Guid.NewGuid().ToString(),
                Name = name,
                PartitionKey = "imgs",
                baseUrl = blobUrl,
                ThumbnailUrl = thumnailUrl
            });
        
        log.Verbose($"Thumnail was stored in blob !"); 
    }
}
Happy Coding Sourire

Ces billets pourraient aussi vous intéresser

Vous nous direz ?!

Commentaires

comments powered by Disqus