En este post vamos a construir un clasificador de imágenes usando Xamarin.Forms y Custom Vision API.

1. Creando nuestro servicio de Custom Vision

El servicio de Custom Vision forma parte del conjunto de servicios de Cognitive Services (https://aka.ms/cognitive).

Este servicio en particular nos permite entrenar un clasificador en base a las imágenes que le proveamos, debemos subirlas y especificar la etiqueta(tag) a la que pertenecen.

Debemos iniciar registrándonos en https://customvision.ai.

Aquí crearemos un nuevo proyecto, puedes usar el nombre que prefieran.

Con el proyecto creado necesitamos agregar las imágenes para entrenar nuestro modelo. Subimos las imágenes y les asignamos una etiqueta dependiendo el tipo que queramos detectar.



Ahora, entrenamos nuestro modelo y con el modelo entrenado obtendremos un endpoint y un key para llamar al servicio.



2. Ahora creamos un cliente en Xamarin.Forms

Creamos un nuevo proyecto en blanco de tipo cross-platform, con Xamarin.Forms y de tipo PCL.

Vamos a comenzar cambiando el profile de nuestro PCL a Profile111, que al quitarle la compatibilidad con Windows Phone nos permite instalar Acr.UserDialogs.

Se recomienda actualizar el paquete de Xamarin.Forms, para luego poder actualizar todos los paquetes de soporte de Android (Android.Support) a la version 25 por lo menos.

En este proyecto vamos a utilizar los siguientes paquetes Nuget.

  • Xam.Plugin.Media v3.0.1, nos permite acceder de forma cross-platform a la camara y a las fotos en el celular. Asegurense de leer las instrucciones en el Readme.

  • Newtonsoft.Json v10.0.3, nos permite deserializar el formato JSON.

  • Microsoft.Net.Http v2.2.29,nos da acceso al HttpClient para poder llamar a los servicios web de forma sencilla.

  • Acr.UserDialogs v6.5.1, nos permite mostrar cuadros de dialogo de progreso, toasts, etc. No olvides inicializarlo en el MainActivity.

Vamos a crear la interfaz de usuario, agregaremos unos controles a nuestro MainPage.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
         xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
         xmlns:local="clr-namespace:Clasificador"
         x:Class="Clasificador.MainPage"
         Title="Clasificador">
    <Page.Resources>
        <ResourceDictionary>
            <Style TargetType="Button">
                <Setter Property="BackgroundColor" Value="Blue"/>
                <Setter Property="TextColor" Value="White"/>
                <Setter Property="BorderRadius" Value="20"/>
            </Style>
        </ResourceDictionary>
    </Page.Resources>
    <StackLayout Padding="20">

    <Image x:Name="ImgSource" Source="Icon.png" 
           MinimumHeightRequest="100"
           MinimumWidthRequest="100"
           HorizontalOptions="Center"/>

    <Button Text="Elige una foto" Clicked="ElegirClick"></Button>
    <Button Text="Tomar una foto" Clicked="TomarClick"/>

    <Button Text="Analizar" Clicked="ClasificarClick"/>

        <Label x:Name="Resultado"/>
        <ProgressBar x:Name="Precision" />
    </StackLayout>
</ContentPage>

Ahora crearemos los manejadores de eventos para elegir una foto de la galería y tomar una nueva foto en el code behind de la página(MainPage.xaml.cs).

private async void ElegirClick(object sender, EventArgs e)
{
        await CrossMedia.Current.Initialize();

        var foto = await CrossMedia.Current
            .PickPhotoAsync(new Plugin.Media.Abstractions.PickMediaOptions());

        _foto = foto;
        ImgSource.Source = FileImageSource.FromFile(foto.Path);
}

private async void TomarClick(object sender, EventArgs e)
{
        await CrossMedia.Current.Initialize();

        var foto = await CrossMedia.Current.TakePhotoAsync(new StoreCameraMediaOptions()
        {
            Directory = "clasificator",
            Name = "source.jpg"
        });
        _foto = foto;
        ImgSource.Source = FileImageSource.FromFile(foto.Path);
}

A partir de la foto elegida obtendremos el Stream de bytes y lo mandaremos como StreamContent hacia el servicio.

    private async void ClasificarClick(object sender, EventArgs e)
    {
        const string endpoint = "https://southcentralus.api.cognitive.microsoft.com/customvision/v1.0/Prediction/4b2e382f-59b0-4941-bbd5-a77b6d3ce8c2/image";
        var httpClient = new HttpClient();
        httpClient.DefaultRequestHeaders.Add("Prediction-Key", "c1561b9c08134ce69773e368ace40fe6");

        var contentStream = new StreamContent(_foto.GetStream());

        using (Acr.UserDialogs.UserDialogs.Instance.Loading("Uploading..."))
        {
            var response = await httpClient.PostAsync(endpoint, contentStream);

            if (!response.IsSuccessStatusCode)
            {
                UserDialogs.Instance.Toast("Un error ha ocurrido.");
                return;
            }

            var json = await response.Content.ReadAsStringAsync();
        }
    }

Ahora necesitamos crear el contrato para deserealizar la respuesta que hemos obtenido del servicio.

public class PredictionResponse
{
    public string Id { get; set; }
    public string Project { get; set; }
    public string Iteration { get; set; }
    public DateTime Created { get; set; }
    public Prediction[] Predictions { get; set; }
}

public class Prediction
{
    public string TagId { get; set; }
    public string Tag { get; set; }
    public float Probability { get; set; }
}

Usando JSON.NET deserealizamos la respuesta y con eso podemos mostrar los resultados en los controles en pantalla.

    private async void ClasificarClick(object sender, EventArgs e)
    {
        const string endpoint = "https://southcentralus.api.cognitive.microsoft.com/customvision/v1.0/Prediction/4b2e382f-59b0-4941-bbd5-a77b6d3ce8c2/image";
        var httpClient = new HttpClient();
        httpClient.DefaultRequestHeaders.Add("Prediction-Key", "c1561b9c08134ce69773e368ace40fe6");

        var contentStream = new StreamContent(_foto.GetStream());

        using (Acr.UserDialogs.UserDialogs.Instance.Loading("Uploading..."))
        {
            var response = await httpClient.PostAsync(endpoint, contentStream);

            if (!response.IsSuccessStatusCode)
            {
                UserDialogs.Instance.Toast("Un error ha ocurrido.");
                return;
            }

            var json = await response.Content.ReadAsStringAsync();

            var prediction = JsonConvert.DeserializeObject<PredictionResponse>(json);

            var tag = prediction.Predictions.First();

            Resultado.Text = $"{tag.Tag} - {tag.Probability:p0}";
            Precision.Progress = tag.Probability;
        }
    }

Y ya tenemos todo listo.

El video con las instrucciones completas también está disponible aquí.

Código fuente disponible en GitHub
https://github.com/jesulink2514/Ejemplo-CustomVisionService