Freigeben über


[Post Invitado] Gestionando grupos de personas con Azure Face API

Este artículo de Alberto Guerra Estévez, consultor de Ilitia Technologies, explica cómo gestionar los límites que posee Face API a la hora de agregar personas al servicio.

Introducción

Face Api es un conjunto de herramientas pertenecientes a los servicios cognitivos de Microsoft Azure ideales para poder identificar a las distintas personas dentro de un grupo. Podemos reconocer, a fecha de hoy, hasta un máximo de 1000 personas por grupo para la subscripción gratuita  y hasta 10.000 para la subscripción estándar[i].

Con los límites actuales podremos añadir de manera sencilla a un volumen suficiente de personas como para cubrir la mayoría de casos que se nos presenten, pero … ¿y si necesitamos superar estos límites?

Face API proporciona las herramientas para poder superar los límites actuales del servicio, simplemente necesitaremos un poco más de lógica en nuestra aplicación para distribuir las distintas personas en nuevos grupos. Si antes el límite que teníamos era de 1.000 personas por grupo para la subscripción gratuita (10.000 en el caso de la subscripción estándar), ahora podremos multiplicar ese techo hasta por 1.000.000, 1.000 usuarios en 1.000 grupos disponibles por servicio, si sabemos gestionar adecuadamente ambos recursos. La ganancia es indudable y conseguirlo es una tarea relativamente sencilla.

1.    Requisitos antes de empezar

Para aprender a gestionar los grupos de personas de Face Api asumimos que conoces y tienes listos los siguientes puntos:

  • Tienes correctamente desplegado un servicio de Face Api en tu portal de Azure con una subscripción válida, sea gratuita o estándar y su clave correspondiente.
  • Conoces los fundamentos básicos y usos de Face API. No deben resultarte extraños conceptos asociados como Face, Person o Group.

2.    Conectando nuestra aplicación a Face API

Para integrar el ejemplo de este artículo vamos a elegir una aplicación UWP, ya que existen librerías que integran perfectamente toda la funcionalidad de la API y nos pueden facilitar el trabajo sin tener que implementar las llamadas mediante un cliente HTTP directamente (como cualquier otra API Rest).

En primer lugar, crearemos un nuevo proyecto Universal Windows (Blank App) desde Visual Studio:

Añadimos al proyecto el NuGet Microsoft.ProjectOxford.Face, que nos dará las librerías de cliente que conecte con Face Api:

Ya tendemos un punto de partida sobre el que empezar a implementar nuestro ejemplo.

3.    Inicializaciones

Necesitaremos los siguientes namespaces del SDK para las funciones que vamos a crear:

 
using Microsoft.ProjectOxford.Face;
using Microsoft.ProjectOxford.Face.Contract;

Para poder hacer todas las llamadas a la Api, debemos tener en cuenta las limitaciones que ofrece nuestra subscripción. Para ello haremos uso del siguiente algoritmo que gestiona la cola de peticiones con respecto a los límites de nuestra subscripción, esperando, si es necesario, hasta que el servicio vuelva a admitir nuevas llamadas, ya que, si superamos el límite de peticiones por segundo, nos disparará una excepción. Este algoritmo está disponible en la documentación de Microsoft:

/en-us/azure/cognitive-services/face/face-api-how-to-topics/how-to-add-faces

 const int PersonCount = 10000;
const int CallLimitPerSecond = 10;
static Queue _timeStampQueue = new
Queue(CallLimitPerSecond);

private static async Task WaitCallLimitPerSecondAsync()
   {
     Monitor.Enter(_timeStampQueue);
     try
     {
        if (_timeStampQueue.Count >= CallLimitPerSecond)
        {
          TimeSpan timeInterval = DateTime.UtcNow - _timeStampQueue.Peek();
          if (timeInterval < TimeSpan.FromSeconds(1))
          {
              await Task.Delay(TimeSpan.FromSeconds(1) - timeInterval);
          } 
          _timeStampQueue.Dequeue();
        }
        _timeStampQueue.Enqueue(DateTime.UtcNow);
     }
     finally
     {
        Monitor.Exit(_timeStampQueue);
     }
   }

Como haremos varias llamadas al servicio,  automatizaremos un poco más la gestión de estas llamadas mediente el siguiente método donde pasaremos nuestro código con la llamada a FaceApi que  invocará automáticamente al gestor de colas:

 private async Task ExecuteFaceApiFunction(Func<Task> action)
        {
           try
           {
                await WaitCallLimitPerSecondAsync();
                await action();
           }
           catch (Exception e)
           {
               Debug.WriteLine(e.Message);
           }
        }

Por último en nuestra clase definimos e instanciamos nuestra variable de nuestro servicio de Face Api y una variable que indique el límite de personas por grupo que permita nuestra subscripción:

 private FaceServiceClient faceService = new FaceServiceClient("my_api_key", @"https://westeurope.api.cognitive.microsoft.com/face/v1.0");
private int maxPersonsPerGroup = 1000;

 

4.    Añadiendo a las personas en sus grupos correspondientes

 

Ten lista  una carpeta con imágenes de caras individuales, distintas entre ellas, que será nuestro ejemplo de partida.

Para poder añadir cada una de las caras que vayamos procesando en nuestro grupo, usaremos alguna de las características de esa cara para poder generar un grupo de asignación. Una buena estrategia puede ser definir el grupo al que pertenece la cara por género.

Crearemos para ello la siguiente función que, para una nueva cara  y una lista de grupos ya existentes en nuestro servicio,  le asignará a nuestra cara un id de grupo donde debería insertarse que no esté lleno, y, en caso de estar todos llenos, creará un nuevo grupo incrementando el índice):

 private async Task GetOrCreateProperGroupId(Face face, List grupos)
        {
               int subGroupIndex = 0;
               string resultGroupId;
               string subgroupName = "subgroup" + face.FaceAttributes.Gender;
               var compatibleGroups = grupos.Where(g => g.PersonGroupId.Contains(subgroupName));

               PersonGroup existentGroup = null;
               if (compatibleGroups != null && compatibleGroups.Any())
               {
                  //si hay grupos compatibles con la cara dada (por genero) buscamos en el primero que este vacio
                  //y vamos incrementando el índice por cada grupo lleno que vayamos encontrando
                  foreach (var group in compatibleGroups)
                  {
                       PersonGroup[] personsInGroup = null;

                       await ExecuteFaceApiFunction(async () =>;
                       {
                          personsInGroup = await faceService.ListPersonGroupsAsync();
                       });
                       if (personsInGroup?.Count() >= maxPersonsPerGroup)
                       {
                           subGroupIndex++;
                       }
                       else
                       {
                           existentGroup = group;
                           break;
                       }
                  }
        }
        //el grupo donde insertar la nueva cara sera el resultado del género + el indice calculado
          resultGroupId = existentGroup?.PersonGroupId ?? (subgroupName + subGroupIndex);

       //si el grupo es nuevo para esta cara (no existe ningun grupo existente para ese groupID) generamos uno nuevo con el indice donde lo dejamos
         if (existentGroup == null)
         {
              Debug.WriteLine("No hay grupos libres para el subgrupo, Se genera nuevo grupo" + resultGroupId);

              await ExecuteFaceApiFunction(async () =>;
              {
                     await faceService.CreatePersonGroupAsync(resultGroupId, resultGroupId);
               });
               grupos.Add(new PersonGroup() { PersonGroupId = resultGroupId });
         }
         
          return resultGroupId;
      }

Con esta función ya podremos crear nuestro algoritmo para que inserte una serie de imágenes de un directorio en nuestro servicio de manera correcta, teniendo en cuenta los límites de capacidad por grupo:

             private async void LoadFotosFromDirectory()
         {
              var filePicker = new Windows.Storage.Pickers.FileOpenPicker()
              {
                   SuggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.PicturesLibrary,
                   ViewMode = Windows.Storage.Pickers.PickerViewMode.Thumbnail
               };

               var files = await filePicker.PickMultipleFilesAsync();

               Debug.WriteLine("Cargando fotos...\n");
               PersonGroup[] existentGroups = null;
               await ExecuteFaceApiFunction(async () =>
                    {
                        existentGroups = await faceService.ListPersonGroupsAsync();
                    });
               foreach (var file in files)
               {
                    using (Stream imageFileStream = File.OpenRead(file.Path)) 
                    {
                          Face[] faces = null;
                          await ExecuteFaceApiFunction(async () =>
                          {
                                faces = await faceService.DetectAsync(imageFileStream, true, true, new FaceAttributeType[] { FaceAttributeType.Gender});
                          });

                          foreach (var face in faces)
                          {
                               string properGroupid = await GetOrCreateProperGroupId(face, existentGroups.ToList());
                               CreatePersonResult newPerson=null;
                               await ExecuteFaceApiFunction(async () =>
                               {
                                      newPerson = await faceService.CreatePersonAsync(properGroupid, face.FaceId.ToString());
                                });
                                await ExecuteFaceApiFunction(async () =>
                                {
                                      await faceService.AddPersonFaceAsync(properGroupid, newPerson.PersonId, File.OpenRead(file.Path));
                                 });
                                 Debug.WriteLine("Persona " + newPerson.PersonId + " agregada a grupo " + properGroupid);
                          }
                     }
                }
                Debug.WriteLine("TODAS LAS FOTOS CARGADAS)");

                await TrainAllGroups();
          }

          private async Task TrainAllGroups ()
          {
                  PersonGroup[] grupos = null;
                  await ExecuteFaceApiFunction(async () =>;
                  {
                        grupos = await faceService.ListPersonGroupsAsync();
                   });

                   foreach (var grupo in grupos)
                   {
                         await ExecuteFaceApiFunction(async () =>;
                         {
                              await faceService.TrainPersonGroupAsync(grupo.PersonGroupId);
                          });
                   }
          }

Cuando el proceso de entrenamiento haya concluido, tendremos todas las fotos cargadas en nuestro servicio superando el número de caras que el servicio nos permite por grupo.

Conclusiones

Con la técnica expuesta en este artículo podrás ir más allá de los límites que un grupo de Face Api permite. Sólo te queda aplicar las ideas y combinarlas con las diversas funcionalidades que el servicio cognitivo proporciona. Además, podrás mejorar y adaptar esta solución a tus necesidades concretas (permitir imágenes con varias fotos, identificar a las personas para no añadir dos personas repetidas al sistema, poder paralelizar varios de estos procesos para mejorar rendimiento…).

Referencias

Documentación oficial de Face Api:

/es-es/azure/cognitive-services/face/overview