Reconociendo a personas mediante Azure FaceAPI

Azure

Han pasado ya las navidades, y toca la vuelta al trabajo, la vuelta a los viajes… y gracias a mi último viaje estuve cenando con un amigo, y me contó un proyecto que se trae entre manos, el cual requería del reconocimiento facial como parte de la identificación inequívoca de usuarios. En su momento, leí sobre los Cognitive Services de Azure, pero nunca me había aventurado a probarlos, y bueno, ya sabéis que soy curioso, así que en cuanto he tenido un momento, me he puesto a probarlo, ¡y la verdad es que ha sido realmente fácil! Vamos con ello:

Crear el servicio Azure FaceAPI

Lo primero que necesitamos, es tener una cuenta en Azure, no es necesario que sea de pago (aunque para confirmar la cuenta nos piden una tarjeta de crédito, no se efectúa ningún cargo).

Una vez que la tenemos, vamos a crear el servicio:

CrearServicio

Para ello, con eso, nos muestra una nueva ventana donde poner nuestros datos:

NombreServicio

En ella, basta con que le indiquemos el nombre, que suscripción queremos utilizar, donde queremos que este el servidor, el plan de precios (OJO!!, yo utilizo «S0» porque estoy haciendo otra prueba más en profundidad y tengo el «F0» ocupado, pero el gratuito es «F0») y por último nos pide el grupo de recursos al que queremos que pertenezca (si no tenemos ningún grupo, lo podemos crear fácilmente con el botón «Crear nuevo»).

Con este paso, pulsamos sobre «Crear», y como podemos ver en el dashboard, ya tenemos nuestro servicio creado:

dashboard

Solo nos queda conocer la url que debemos utilizar, y crear las claves para poder usarla, para eso, basta con pulsar sobre el servicio para ver su configuración:

overview

He indicado en amarillo la url de endpoint que tendremos que indicarle a la API en nuestro programa, en mi caso es:
https://westeurope.api.cognitive.microsoft.com/face/v1.0

Justo debajo, tenemos el botón para gestionar nuestras claves de acceso, lo que lanza una ventana donde nos las muestra:

keys

Debemos apuntarlas (o volver luego y consultarlas) al igual que la url del endpoint, ya que son datos que le vamos a tener que pasar a la API desde nuestro programa.

Creando nuestro programa

Para poder consumir nuestro servicio Azure FaceAPI, vamos a utilizar una aplicación de formularios en .Net Framework, pero por supuesto, podemos utilizarlo en nuestro proyecto NetCore (pero esta vez, la librería cambia de nombre). Para ello, creamos nuestro proyecto de formularios, y añadimos el paquete:

PM->Install-Package Microsoft.ProjectOxford.Face -v 1.4.0

Si estuviésemos en NetCore, por ejemplo en nuestra web ASP, el paquete seria:

PM->Install-Package Microsoft.ProjectOxford.Face.DotNetCore -Version 1.1.0

Al ser un código extenso, pongo el enlace para descargarlo desde GitHub, y aquí solo vamos a ver las partes claves de la API:

 
var faceServiceClient = new FaceServiceClient("Tu Key", "Url de la API");

Crearemos una instancia del cliente de la API, que luego utilizaremos.

 
var groups = await faceServiceClient.ListPersonGroupsAsync();

Listamos los grupos que ya existan creador previamente.

 
await faceServiceClient.DeletePersonGroupAsync(group.PersonGroupId);

Borramos un grupo.

 
await faceServiceClient.CreatePersonGroupAsync(GroupGUID, "FixedBuffer");

Creamos un grupo.

 
var personResult = await faceServiceClient.CreatePersonAsync(GroupGUID, personName);

Creamos una persona dentro del grupo.

 
var persistFace = await faceServiceClient.AddPersonFaceInPersonGroupAsync(GroupGUID, personResult.PersonId, fStream, file.FullName);

Añadimos una foto de training a la persona.

 
await faceServiceClient.TrainPersonGroupAsync(GroupGUID);

Hacemos el entrenamiento del grupo.

 
var faces = await faceServiceClient.DetectAsync(fStream);

Detectamos las caras de la imagen.

 
var results = await faceServiceClient.IdentifyAsync(GroupGUID, faces.Select(ff => ff.FaceId).ToArray());

Identificamos las caras dentro de la gente del grupo.

 
var person = await faceServiceClient.GetPersonAsync(GroupGUID, result.Candidates[0].PersonId);

Obtenemos a la persona por Guid.

Como se puede ver, la API nos provee de una manera de funcionar bastante lógica.
En primer lugar, tenemos que crear un grupo de personas, después añadir personas a ese grupo, añadir caras a esas personas, y después entrenar el grupo. Esto solo es necesario hacerlo una vez para entrenar el grupo, con eso, podemos pasar a identificar a personas.

Para identificar, los pasos son también sencillos, en primer lugar, detectar las caras en la imagen, en segundo lugar, detectar a quien puede pertenecer esa cara, y en último lugar (en caso de no haberlo almacenado locamente), obtener a la persona a quien pertenece esa cara.

Código completo:

 
using System;
using System.Collections.Generic;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Threading.Tasks;
using System.Configuration;
using System.Windows.Forms;
using Microsoft.ProjectOxford.Face;
using System.IO;

namespace PostAzureFaceAPI
{
    public partial class MainForm : Form
    {
        string FaceAPIKey = ConfigurationManager.AppSettings["FaceAPIKey"];
        string FaceAPIEndPoint = ConfigurationManager.AppSettings["FaceAPIEndPoint"];
        string GroupGUID = Guid.NewGuid().ToString();

        public MainForm()
        {
            InitializeComponent();
        }

        private async void btn_Train_Click(object sender, EventArgs e)
        {
            //Abrimos un dialogo de seleccion de carpetas
            FolderBrowserDialog dialog = new FolderBrowserDialog();
            if (dialog.ShowDialog() == DialogResult.OK)
            {
                //Si se ha seleccionado un directorio, hacemos su info
                DirectoryInfo directory = new DirectoryInfo(dialog.SelectedPath);

                //Comprobamos que el directorio tiene carpetas de personas
                if (directory.GetDirectories().Count() == 0)
                    return;

                //=====Empezamos a crear el grupo de trabajo
                //Creamos el cliente
                var faceServiceClient = new FaceServiceClient(FaceAPIKey, FaceAPIEndPoint);

                //Vamos a trabajar desde 0 siempre, asi que comprobamos si hay grupos, y si los hay los borramos
                var groups = await faceServiceClient.ListPersonGroupsAsync();
                foreach (var group in groups)
                {
                    await faceServiceClient.DeletePersonGroupAsync(group.PersonGroupId);
                }
                //Creamos un grupo
                await faceServiceClient.CreatePersonGroupAsync(GroupGUID, "FixedBuffer");

                foreach (var person in directory.GetDirectories())
                {
                    //Comprobamos que tenga imagenes
                    if (person.GetFiles().Count() == 0)
                        return;

                    //Obtenemos el nombre que le vamos a dar a la persona
                    var personName = person.Name;

                    lbl_Status.Text = $"Entrenando a {personName}";

                    //Añadimos a una persona al grupo
                    var personResult = await faceServiceClient.CreatePersonAsync(GroupGUID, personName);

                    //Añadimos todas las fotos a la persona
                    foreach (var file in person.GetFiles())
                    {
                        using (var fStream = File.OpenRead(file.FullName))
                        {
                            try
                            {
                                //Cargamos la imagen en el pictureBox
                                pct_Imagen.Image = new Bitmap(fStream);
                                //Reiniciamos el Stream
                                fStream.Seek(0, SeekOrigin.Begin);
                                // Actualizamos las caras en el servidor
                                var persistFace = await faceServiceClient.AddPersonFaceInPersonGroupAsync(GroupGUID, personResult.PersonId, fStream, file.FullName);
                            }
                            catch (FaceAPIException ex)
                            {
                                lbl_Status.Text = "";
                                MessageBox.Show($"Imposible seguir, razón:{ex.ErrorMessage}");
                                return;
                            }
                        }
                    }
                }

                try
                {
                    //Entrenamos el grupo con todas las personas que hemos metido
                    await faceServiceClient.TrainPersonGroupAsync(GroupGUID);

                    // Esperamos a que el entrenamiento acabe
                    while (true)
                    {
                        await Task.Delay(1000);
                        var status = await faceServiceClient.GetPersonGroupTrainingStatusAsync(GroupGUID);
                        if (status.Status != Microsoft.ProjectOxford.Face.Contract.Status.Running)
                        {
                            break;
                        }
                    }

                    //Si hemos llegado hasta aqui, el entrenamiento se ha completado
                    btn_Find.Enabled = true;
                    lbl_Status.Text = $"Entrenamiento completado";
                }
                catch (FaceAPIException ex)
                {
                    lbl_Status.Text = "";
                    MessageBox.Show($"Response: {ex.ErrorCode}. {ex.ErrorMessage}");
                }
                GC.Collect();
            }
        }

        private async void btn_Find_Click(object sender, EventArgs e)
        {
            lbl_Status.Text = "";
            OpenFileDialog dialog = new OpenFileDialog();
            dialog.DefaultExt = ".jpg";
            dialog.Filter = "Image files(*.jpg, *.png, *.bmp, *.gif) | *.jpg; *.png; *.bmp; *.gif";
            if (dialog.ShowDialog() == DialogResult.OK)
            {
                var imagePath = dialog.FileName;

                //Creamos el cliente
                var faceServiceClient = new FaceServiceClient(FaceAPIKey, FaceAPIEndPoint);

                using (var fStream = File.OpenRead(imagePath))
                {
                    //Cargamos la imagen en el pictureBox
                    pct_Imagen.Image = new Bitmap(fStream);
                    //Reiniciamos el Stream
                    fStream.Seek(0, SeekOrigin.Begin);

                    try
                    {
                        //Detectamos las caras
                        var faces = await faceServiceClient.DetectAsync(fStream);

                        //Detectamos a las personas
                        var results = await faceServiceClient.IdentifyAsync(GroupGUID, faces.Select(ff => ff.FaceId).ToArray());

                        //Creamos una lista de caras y nombres asociados
                        List<(Guid, string)> detections = new List<(Guid, string)>();
                        foreach (var result in results)
                        {
                            //En caso de no haber encontrado un candidato, nos lo saltamos
                            if (result.Candidates.Length == 0)
                                continue;
                            var faceId = faces.FirstOrDefault(f => f.FaceId == result.FaceId).FaceId;
                            //Consultamos los datos de la persona detectada
                            var person = await faceServiceClient.GetPersonAsync(GroupGUID, result.Candidates[0].PersonId);

                            //Añadimos a la lista la relacion
                            detections.Add((faceId,person.Name));            
                        }

                        var faceBitmap = new Bitmap(pct_Imagen.Image);

                        using (var g = Graphics.FromImage(faceBitmap))
                        {                           

                            var br = new SolidBrush(Color.FromArgb(200, Color.LightGreen));

                            // Por cada cara reconocida
                            foreach (var face in faces)
                            {
                                var fr = face.FaceRectangle;
                                var fa = face.FaceAttributes;

                                var faceRect = new Rectangle(fr.Left, fr.Top, fr.Width, fr.Height);
                                Pen p = new Pen(br);
                                p.Width = 50;
                                g.DrawRectangle(p, faceRect);                                

                                // Calculamos la posicon del rectangulo
                                int rectTop = fr.Top + fr.Height + 10;
                                if (rectTop + 45 > faceBitmap.Height) rectTop = fr.Top - 30;

                                // Calculamos las dimensiones del rectangulo                     
                                g.FillRectangle(br, fr.Left - 10, rectTop, fr.Width < 120 ? 120 : fr.Width + 20, 125);

                                //Buscamos en la lista de relaciones cara persona
                                var person = detections.Where(x => x.Item1 == face.FaceId).FirstOrDefault();
                                var personName = person.Item2;
                                Font font = new Font(Font.FontFamily,90);
                                //Pintamos el nombre en la imagen
                                g.DrawString($"{personName}",
                                             font, Brushes.Black,
                                             fr.Left - 8,
                                             rectTop + 4);
                            }
                        }
                        pct_Imagen.Image = faceBitmap;
                    }
                    catch (FaceAPIException ex)
                    {

                    }
                }
            }
        }
    }
}

Para poder utilizar el código, debemos modificar el App.config cambiando las claves:

 
  <appSettings> 
    <add key="FaceAPIKey" value=""/> 
    <add key="FaceAPIEndPoint" value=""/> 
  </appSettings> 

Por las que hemos obtenido al crear el servicio. Como se puede ver en el funcionamiento, la carga se hace desde seleccionando una carpeta, que a su vez tenga dentro una carpeta por cada persona que queramos entrenar y que se llame como la persona, será dentro de cada una de estas carpetas donde metamos las fotos de cada persona:

train

Una vez que hayamos completado el entrenamiento, podremos seleccionar una imagen en la que identificar a la persona:

resultado

Como hemos podido ver, es muy sencillo empezar a utilizar la API de reconocimiento e identificación facial. En próximas entradas, seguiremos profundizando en los Cognitive Services de Azure, tanto conociendo el resto de los servicios, como profundizando en este, que ofrece muchas más opciones aparte de la identificación de personas.

Deja un comentario