Cómo entrenar tu propia red para detección de objetos con Azure Custom Vision y realizar predicciones con Blazor (.NET Core 3.0)

En este post, vamos a hablar sobre uno de los muchos servicios que nos ofrece Microsoft en su directorio de Cognitive Services, el Custom Vision Service. Comprobaremos lo fácil que es entrenar tu propia red neuronal para la detección de objetos personalizados utilizando Custom Vision y poder hacer predicciones sobre el modelo entrenado.

Hablaremos primero de los servicios y herramientas que se han usado para la prueba de concepto y luego se describirán los pasos a seguir para poder utilizar nuestro modelo y pedirle predicciones desde una aplicación desarrollada en ASP.NET Core con Blazor, y sin nada de JavaScript 😉

Cognitive Services

Cognitive Services es un directorio de servicios que nos ofrece Microsoft para poder utilizar Inteligencia Artificial de una manera muy sencilla y poder incorporarla como Inteligencia de Negocio en nuestros proyectos.

Posee una cantidad de servicios bastante grande (ampliándose de manera constante). Algunos de ellos son:

Todo esto está muy bien, pero ¿como están de precio?

La mayoría de los servicios tienen una capa gratuita para que podamos jugar y hacer nuestras pruebas de concepto. Se pueden comprobar sus precios y los límites que nos ofrecen para cada servicio en la siguiente url:

https://azure.microsoft.com/es-es/pricing/calculator/?service=cognitive-services

Custom Vision

Custom Vision es el servicio de Cognitive Services que nos permite poder entrenar nuestra propia red con nuestras imágenes y así, crear un modelo personalizado para poder hacer predicciones para detección de objetos o clasificación de imágenes.

Figura 1 - Custom Vision Service

Las predicciones podremos hacerlas llamando al modelo publicado en Azure vía API REST o SDK, pero también, podemos exportar nuestro modelo para poder utilizarlo para predicciones de manera desconectada (off-line) y así ahorrarnos las llamadas a internet cada vez que queramos hacer una predicción.

Microsoft nos proporciona un SDK para varios lenguajes (C#, Python, Java, NodeJS, …) con el que podemos interactuar con el servicio, para poder subir nuestras imágenes, etiquetarlas, entrenar el modelo, publicarlo y poder llamarlo para predecir. Pero todos estos pasos los podemos hacer también en un portal que Microsoft ha creado expresamente para este servicio y que lo hace extremadamente fácil de usar. El portal está en la siguiente url:

https://www.customvision.ai

Blazor

Blazor es un framework, aún en versión preliminar (preview), pero que saldrá oficialmente con la version de .NET Core 3.0. Es una evolución sobre ASP.NET Core MVC, con el que podremos desarrollar aplicaciones utilizando plantillas Razor de toda la vida, como si fuera un Single Page Application (SPA) pero sin utilizar JavaScript. Todo esto apoyado por WebAssembly para mayor rendimiento. Al desarrollar con .NET Core, podremos ejecutar nuestra aplicación en cualquier plataforma (Windows, Linux, macOS).

Figura 2 - Arquitectura de Blazor

Todavía le queda camino que recorrer, pero están haciendo un gran trabajo y hemos querido utilizarlo hoy para esta pequeña prueba.

Hay bastante información de Blazor en los siguientes enlaces:

Prueba de concepto

Hemos creado una pequeña prueba de concepto para poder utilizar todo lo anteriormente mencionado. La meta es intentar reconocer marcas de tabaco utilizando una foto de un escaparate. Para lograr esto, se entrenará una red neuronal, con imágenes de 4 marcas de tabaco para que se pueda identificar correctamente.

Una vez entrenada la red, se creará un proyecto con Blazor para poder hacer llamadas al modelo vía API REST, pasarle imágenes de estancos y que se pueda identificar qué paquetes hay de las marcas que hemos entrenado.

¿Qué necesitamos?

Manos a la obra

Lo primero, entrar en el portal de customvision.ai y crearnos un proyecto.

Nos pedirá una serie de datos para poder crearlo:

Figura 3 - Nuevo proyecto

Una vez creado nuestro proyecto, lo siguiente que debemos hacer es subir nuestra batería de imágenes para entrenar a nuestra red. Esta etapa es muy importante, cuantas más imágenes agreguemos a nuestro modelo, mejor entrenado estará. Además, es muy recomendable agregar diferentes imágenes con distintos tamaños, orientación, calidad, … para que la red pueda entrenarse en diferentes condiciones.

Figura 4 - Etiquetado del logo de la marca

En el portal podremos ir pasando por todas las imágenes que hemos agregado para ir etiquetándolas como deseamos que el modelo aprenda.

Figura 5 - Diferentes imágenes de cajetillas en diferentes orientaciones y tamaños

Una vez que se tengan diferentes imágenes de cada una de las marcas cargadas y etiquetadas, se empieza a realizar el entrenamiento del modelo.

Figura 6 - Opciones para realizar el entrenamiento

Para jugar, podemos usar el entrenamiento rápido, pero si queremos algo un poco más serio, podemos marcar el avanzado y seleccionar la cantidad de horas que queremos que se dedique a su entrenamiento.

Figura 7 - La capa de precios gratuita es suficiente para hacer nuestras pruebas

Una vez que hayamos entrenado nuesto modelo, podemos ver datos de cómo de bueno es (Figura 8). Como veis, para las pocas imágenes que se han utilizado, no tiene mala pinta.

Figura 8 - Opciones para realizar el entrenamiento

En el mismo portal, podemos probar nuestro modelo entrenado, subiendo imágenes.

Figura 9 - Resultado de predicción

Una vez que nos convenza nuestro modelo, lo siguiente que debemos hacer es publicarlo. Esto nos proporcionará una url e instrucciones que podemos usar en nuestra aplicación para poder realizar predicciones.

Figura 10 - Publicar modelo

A continuación, crearemos un proyecto de ASP.NET Core con Visual Studio 2019 Preview para poder hacer llamadas a la URL del modelo publicado.

Os explicaré los pasos a seguir para crearlo a continuación, pero si queréis acceder al código directamente lo teneis aquí en GitHub.

En Visual Studio, simplemente pulsar en crear un nuevo proyecto, seleccionar ASP.NET Core Web Application y luego Blazor Server App.

Figura 11 - Nuevo proyecto de ASP.NET Core WEb Application

Figura 12 - Nuevo Blazor Server App

Dentro del proyecto, hemos creado un servicio para poder hacer las llamadas al modelo publicado, mediante peticiones web, y una página de razor que pide una url de una imagen y llama al servicio de IA para pedir la predicción. Después, una vez obtenidos los resultados, se dibuja sobre la imagen una caja con la etiqueta y su porcentaje de predicción para cada logo identificado por el servicio.

Aquí tenéis la página de razor y la llamada al servicio de AI, que es lo que más nos interesa en este artículo.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@page "/predictions"
@using BlazorCustomVision.Data

@inject TobaccoPredictionsService tobaccoPredictionsService

<div class="container-fluid">
    <h1>Detección de marcas de tabaco</h1>
    <p>Aqui podrás probar tus images para detectar marcas de tabaco. Introduce la URL de tu imagen.</p>
</div>

<div class="container-fluid">
    <div class="row">
        <div class="col-10">
            <input type="text" style="width:100%" @bind="@urlFile" />
        </div>
        <div class="col-2">
            <button @onclick="@ReadFile">Cargar imagen</button>
        </div>
    </div>
</div>

<hl></hl>

<div class="container-fluid">
    <div class="row">
        <div class="col-md-auto">
            @if (loading)
            {
                <p><em>Procesando, por favor espere ...</em></p>
            }

            @if (response != null && !string.IsNullOrWhiteSpace(response.ImageInBase64) && string.IsNullOrWhiteSpace(response.Error))
            {
                <p><em>Tiempo llamada Custom Vision API: @response.ElapsedTime</em></p>
                <img src="data:image/png;base64, @response.ImageInBase64" />
            }

            @if (response != null && !string.IsNullOrWhiteSpace(response.Error))
            {
                <p><em>@response.Error</em></p>
            }
        </div>
    </div>
</div>

@code
{
    bool loading = false;
    public string urlFile { get; set; } = "";
    CustomVisionResponse response = null;

    public async Task ReadFile()
    {
        loading = true;
        response = await tobaccoPredictionsService.PredictFromURL(urlFile);
        loading = false;
    }
}

En este fichero, lo más interesante es cómo blazor nos permite mediante la etiqueta @code, añadir funciones de c# y enlazarlas con nuestras etiquetas HTML. ¡Y todo esto como si fuera un Single Page Application (SPA)! 😲

Tambien fijaros que podemos inyectar un servicio en nuestra vista para poder usarlo en la etiqueta @code de la siguiente forma:

@inject TobaccoPredictionsService tobaccoPredictionsService

Sencillo ¿verdad? Ahora vamos a ver nuestro servicio al que le pasamos la url de la imagen que queremos analizar y llamamos a nuestro modelo publicado mediante la url que obtuvimos anteriormente del portal de customvision.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
namespace BlazorCustomVision.Data
{
    public class TobaccoPredictionsService
    {
        private const string predictionKey = "<Your prediction key here>";
        private const string projectId = "<Your project id here>";
        private const string publishedModelName = "<Your iteration model name here>";
        private readonly string predictionFromURL = "<Your prediction url here>";
        private const double minimumThreshold = 0.3D;

        public async Task<CustomVisionResponse> PredictFromURL(string imageFileUrlPath)
        {
            if (string.IsNullOrWhiteSpace(imageFileUrlPath)) return null;

            try
            {
                var stopWatch = new Stopwatch();
                HttpResponseMessage response;
                var customVisionResponse = new CustomVisionResponse();
                byte[] byteData = null;

                using (var client = new HttpClient())
                {
                    client.DefaultRequestHeaders.Add("Prediction-Key", predictionKey);

                    var url = predictionFromURL;
                    var request = new CustomVisionRequest { Url = imageFileUrlPath };
                    var json = JsonConvert.SerializeObject(request);
                    var content = new StringContent(json, Encoding.UTF8, "application/json");

                    stopWatch.Start();
                    response = await client.PostAsync(url, content).ConfigureAwait(false);
                    stopWatch.Stop();
                }

                string responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);

                if (!response.IsSuccessStatusCode)
                {
                    customVisionResponse.Error = $"Error {response.StatusCode}: {responseContent}";
                    return customVisionResponse;
                }

                var tempCustomVisionResponse = JsonConvert.DeserializeObject<CustomVisionResponse>(responseContent);
                customVisionResponse.Id = tempCustomVisionResponse.Id;
                customVisionResponse.Created = tempCustomVisionResponse.Created;
                customVisionResponse.Iteration = tempCustomVisionResponse.Iteration;
                customVisionResponse.Project = tempCustomVisionResponse.Project;
                customVisionResponse.Predictions = new List<Prediction>();

                foreach (var prediction in tempCustomVisionResponse.Predictions.Where(x => x.Probability >= minimumThreshold))
                {
                    var objPrediction = new Prediction();
                    objPrediction.TagId = prediction.TagId;
                    objPrediction.TagName = prediction.TagName;
                    objPrediction.Probability = prediction.Probability;

                    objPrediction.BoundingBox = new BoundingBox
                    {
                        Height = prediction.BoundingBox.Height,
                        Width = prediction.BoundingBox.Width,
                        Left = prediction.BoundingBox.Left,
                        Top = prediction.BoundingBox.Top
                    };

                    customVisionResponse.Predictions.Add(objPrediction);
                }

                byteData = await GetImageAsByteArrayAsync(imageFileUrlPath).ConfigureAwait(false);
                customVisionResponse = AddCustomVisionResponseToImage(customVisionResponse, byteData);

                var ts = stopWatch.Elapsed;
                string elapsedTime = string.Format("{0:00}:{1:00}:{2:00}", ts.Hours, ts.Minutes, ts.Seconds);
                customVisionResponse.ElapsedTime = elapsedTime;

                return customVisionResponse;
            }
            catch (Exception e)
            {
                return null;
            }
        }

        private CustomVisionResponse AddCustomVisionResponseToImage(CustomVisionResponse customVisionResponse, byte[] content)
        {
            var redPen = new Pen(Color.Red, 3);
            var font = new Font(FontFamily.GenericSansSerif, 8, FontStyle.Regular);

            using (var ms = new MemoryStream(content))
            {
                using (var img = new Bitmap(ms))
                {
                    using (var graphics = Graphics.FromImage(img))
                    {
                        graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;

                        foreach (var predict in customVisionResponse.Predictions)
                        {
                            var x = predict.BoundingBox.Left * img.Width;
                            var y = predict.BoundingBox.Top * img.Height;
                            var width = predict.BoundingBox.Width * img.Width;
                            var height = predict.BoundingBox.Height * img.Height;

                            var rect = new Rectangle(
                                (int)(x),
                                (int)(y),
                                (int)(width),
                                (int)(height));

                            // Prediction box
                            graphics.DrawRectangle(redPen, rect);

                            // Set up string.
                            string measureString = $"{predict.TagName}: {predict.Probability:P1}";

                            // Set maximum layout size.
                            SizeF layoutSize = new SizeF((float)width, (float)height);

                            // Measure string.
                            SizeF stringSize = new SizeF();
                            stringSize = graphics.MeasureString(measureString, font, layoutSize);

                            // Draw rectangle representing size of string.
                            graphics.FillRectangle(Brushes.Red, (int)x, (int)(y - stringSize.Width * 0.2), stringSize.Width, stringSize.Height);

                            // Draw string to screen.
                            graphics.DrawString(measureString, font, Brushes.White, new PointF((float)x, (float)(y - stringSize.Width * 0.15)));
                        }

                        using (var newMS = new MemoryStream())
                        {
                            img.Save(newMS, img.RawFormat);
                            byte[] imageBytes = newMS.ToArray();
                            var base64String = Convert.ToBase64String(imageBytes);
                            customVisionResponse.ImageInBase64 = base64String;
                        }

                        return customVisionResponse;
                    }
                }
            }
        }

        ...
    }
}

Como veis, lo primero que hacemos es hacer la llamada al servicio de Custom Vision con los datos del proyecto y modelo, y la url de la imagen que se quiere analizar. El pedido se realiza mediante un HttpClient.

La respuesta de la API de Custom Vision con las predicciones es como podéis ver a continuación:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class CustomVisionResponse
{
	public string Id { get; set; }
	public string Project { get; set; }
	public string Iteration { get; set; }
	public string Created { get; set; }
	public List<Prediction> Predictions { get; set; }
	public string ImageInBase64 { get; set; }
	public string Error { get; set; }
	public string ElapsedTime { get; set; }
}

public class Prediction
{
	public string TagId { get; set; }
	public string TagName { get; set; }
	public double Probability { get; set; }
	public BoundingBox BoundingBox { get; set; }
}

public class BoundingBox
{
	public double Left { get; set; }
	public double Top { get; set; }
	public double Width { get; set; }
	public double Height { get; set; }
}

En dicha respuesta, obtenemos una lista de predicciones que ha encontrado el modelo entrenado en la imagen que se le ha mandado, y por cada una de ellas, indica la etiqueta que ha detectado, el porcentaje de probabilidad de que sea esa etiqueta, y un objeto BoundingBox con información para indicar dónde está dicha etiqueta en la imagen. Estos últimos datos se utilizan para pintar un recuadro con el nombre de la etiqueta y su porcentaje.

A continuación, podemos ver el resultado de algunas pruebas realizadas:

Figura 13 - Prueba 1

Figura 14 - Prueba 2

Figura 15 - Prueba 3

Figura 16 - Prueba 4

Conclusiones

Como habéis podido comprobar, con estas herramientas y poquitos pasos, tenemos una manera muy fácil y rápida de poder entrar en el mundo de las redes neuronales y tener nuestros propios modelos de predicción. Poder realizar toda la carga y etiquetado de nuestras imágenes y su entrenamiento desde un portal web, es de lo más cómodo y rápido.

Al poder tener acceso al modelo vía API REST, nos abre una cantidad enorme de posibilidades para explotar nuestro modelo vía web, app móvil, escritorio, etc, además de tener la posibilidad de exportar nuestro modelo a un modelo TensorFlow y así poder usarlo con más eficiencia y sin la dependencia de la red.

Así que bueno, espero que os haya gustado y os pique (si no lo ha hecho ya) el gusanillo de las redes neuronales.

Ah y por cierto, ¡algo hace! 😜

Compartir con:

Carlos Bonilla Cruz

Arquitecto .NET especializado en Cloud. Ingeniero informático, graduado en la universidad de Almería con vasta experiencia en el sector de la consultoría (SharePoint, .NET, Azure, C#).

Seguir a Carlos Bonilla Cruz en LinkedIn