Saltar al contenido
Ricardo RetamalSoftware y cosas que vale la pena construir
/
Conversemos
← Ver todos los escritos
AI5 min read

Escribiendo un clasificador de imágenes en .Net Maui

Para este caso en particular quería hacer algo distinto y quería poder hacer una pequeña aplicación que identificara o clasificara perros.

Publicado originalmente en Medium ↗

English version: https://medium.com/@rretamal.dev/writing-an-image-classifier-on-net-maui-f351bf28bc2c

Escribiendo un clasificador de imágenes en .Net Maui
Escribiendo un clasificador de imágenes en .Net Maui

Para este caso en particular quería hacer algo distinto y quería poder hacer una pequeña aplicación que identificara o clasificara perros.

Aprovechando que .Net Maui fue lanzado recientemente la pregunta fué ¿Por qué no?!

Lo primero en este caso para mi fué decidir que tecnología quería usar para el desarrollo del modelo, inicialmente hice algunas pruebas con ML.Net, generé un modelo clasificador preliminar pero que no pude usar en una app móvil, se sugería el uso de una API para ese caso pero como prefería que el procesamiento estuviera en el lado del dispositivo finalmente decidí irme por Tensorflow Lite.

Como no quería reinventar la rueda reentrene un modelo basado en MobileNetV2 que está orientado a dispositivos móviles, pero que es lo genial de esto? es que con la cantidad adecuada de imágenes puedes reentrenarlo para clasificar y reconocer todo tipo de objetos con un nivel aceptable de efectividad y alto rendimiento.

En caso de querer revisar mas información acerca del modelo pueden ir al siguiente link:

Review: MobileNetV2 — Light Weight Model (Image Classification)
Outperforms MobileNetV1, NASNet-A, and ShuffleNet V1towardsdatascience.com

Para el siguiente paso necesitas imágenes de perros, muchos perros, así que utilice el siguiente dataset:

Stanford Dogs Dataset
Over 20,000 images of 120 dog breedswww.kaggle.com

Ya una vez decidido el modelo y teniendo el dataset con las imágenes se reentrena el modelo resultando un archivo .tflite y un archivo labels.txt con los nombres de las clases de perros.

El siguiente es un tutorial en el que me basé para el entrenamiento, en mi caso mi notebook toma de un archivo zip las imagenes de Google Drive, reentrena el modelo y genera los archivos mencionados.

Retraining an Image Classifier | TensorFlow Hub
Image classification models have millions of parameters. Training them from scratch requires a lot of labeled training…www.tensorflow.org

Ya teniendo los archivos en cuestión, lo siguiente es crear en Visual Studio 2022 un nuevo proyecto en MAUI:

Seguidamente debes darle un nombre super genial a tu proyecto y seleccionar donde guardarlo:

Así que perfecto! tenemos nuestro proyecto creado.

Seguidamente estuve revisando librerías para poder usar Tensorflow en Xamarin y conseguí un par que estaban un poco desactualizadas, así que mejor nos creamos un proyecto Java Binding para usar directamente la librería de Tensorflow (aar).

El proyecto lo podemos estructurar de la siguiente manera:

  • Maui.DogClassifier: proyecto MAUI con la UI de la app y donde se captura la imagen desde la cámara
  • Maui.DogClassifier.Core: proyecto que contiene la lógica para cargar el modelo e identificar a tu mascota
  • Maui.DogClassifier.Lib: proyecto binding donde se implementa la librería aar de Tensorflow

Antes que nada el modelo entrenado nos generará dos archivos: un archivo tflite con el modelo y labels.txt que contiene todas las clases entrenadas, esos archivos deben ir en la carpeta Raw del proyecto como se indica en la imagen, estos quedan al momento de agregarse quedan automaticamente como un MauiAsset:

Ahora ejecutemos el proyecto tal cual está:

Perfecto! Tenemos una aplicación MAUI corriendo. Ahora tenemos que cambiar la interfaz para capturar la foto, mostrarla y dar el resultado de la clasificación.

Mantendremos el ejemplo sencillo así que solo cambiaremos el botón y agregaremos un picture para mostrar la foto y un label extra para el resultado, lo que quedaría de la siguiente manera:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="Maui.DogClassifier.MainPage">
			 
    <ScrollView>
        <VerticalStackLayout 
            Spacing="25" 
            Padding="10,0" 
            VerticalOptions="Center">


            <Image
                Source="dotnet_bot.png"
                SemanticProperties.Description="Cute dot net bot waving hi to you!"
                HeightRequest="150"
                HorizontalOptions="Center" />
                
            <Label 
                Text="Hello, Dog!"
                SemanticProperties.HeadingLevel="Level1"
                FontSize="30"
                HorizontalOptions="Center" />
            
            <Label 
                Text="Please take a picture of your dog"
                SemanticProperties.HeadingLevel="Level2"
                SemanticProperties.Description="Please take a picture of your dog"
                FontSize="18"
                HorizontalOptions="Center" />


            <Button Text="Take picture" x:Name="btnCapture" />


            <Image x:Name="imgCapture" IsVisible="false" HeightRequest="300"/>


            <Label 
                x:Name="lblResult"
                Text="It's a !"
                SemanticProperties.HeadingLevel="Level2"
                FontSize="18"
                HorizontalOptions="Center" IsVisible="false"/>




        </VerticalStackLayout>
    </ScrollView>
 
</ContentPage>

GitHub Gist ↗

Seguidamente en nuestro archivo MainPage.xaml.cs tendremos que hacer dos cosas, instanciar nuestro Classifier (que crearemos despues) y realizar el manejo de la cámara quedando de la siguiente manera:

public partial class MainPage : ContentPage
{	int count = 0;
	Classifier classifier;


	public MainPage()
	{
		InitializeComponent();
		classifier = new Classifier();


    }


    private async void BtnCapture_Clicked(object sender, EventArgs e)
    {
        var photo = await MediaPicker.CapturePhotoAsync();


        await LoadPhotoAsync(photo);


        byte[] image = File.ReadAllBytes(photo.FullPath);


        var result = classifier.ClassifyImage(image);


        lblResult.IsVisible = true;


        if (result != null)
        {
            if (result.Percentage > 0.7)
            {
                lblResult.IsVisible = true;
                lblResult.Text = $"It's a {result.Name}! ({result.Percentage.ToString()})";
                return;
            }
        }


        lblResult.Text = "Dog not recognized, please try again";
    }


    async Task LoadPhotoAsync(FileResult photo)
    {
        // save the file into local storage
        var newFile = Path.Combine(FileSystem.CacheDirectory, photo.FileName);
        using (var stream = await photo.OpenReadAsync())
        using (var newStream = File.OpenWrite(newFile))
        {
            await stream.CopyToAsync(newStream);
        }


        imgCapture.Source = newFile;
        imgCapture.Aspect = Aspect.Center;
    }
}

GitHub Gist ↗

Como veran creamos el clasificador, seguidamente cuando se pulse el botón abriremos la cámara invocando el método MediaPicker.CapturePhotoAsync y obtendremos la imagen para enviarla al clasificador.

El clasificador por su lado nos enviará el mejor resultado obtenido indicado la clase y el porcentaje de confianza.

Ahora tenemos que crear nuestro clasificador, para ello en el proyecto de librería veremos una carpeta por cada plataforma, en ellas deberemos crear la clase para que sea invocada dinámicamente por el proyecto Maui dependiendo de la plataforma (que genial!), así que crearemos un archivo .Classifier.cs en cada una de las carpetas quedando de la siguiente manera:

Por el momento nos centraremos en la clase de Android, donde por pasos cargaremos primero el modelo:

private MappedByteBuffer LoadModel()
{
    var assets = Android.App.Application.Context.Assets;


    var assetDescriptor = assets.OpenFd("model.tflite");
    var inputStream = new FileInputStream(assetDescriptor.FileDescriptor);


    var mappedByteBuffer = inputStream.Channel.Map(FileChannel.MapMode.ReadOnly, assetDescriptor.StartOffset, assetDescriptor.DeclaredLength);


    return mappedByteBuffer;
}

GitHub Gist ↗

Segundo, cargaremos los labels, limpiando un poco el texto:

private List<string> LoadLabels()
{
    var assets = Android.App.Application.Context.Assets;


    var streamReader = new StreamReader(assets.Open("labels.txt"));


    //Transform labels.txt into List<string>
    var labels = streamReader.ReadToEnd().Split('\n').Select(c => c.Split('-')[1]).ToList();


    return labels;
}

GitHub Gist ↗

Y tercero clasificaremos la imagen de la siguiente manera usando el interpreter de Tensorflow:

public ImageResult ClassifyImage(byte[] image)
{
    var tensor = interpreter.GetInputTensor(0);
    var shape = tensor.Shape();


    var width = shape[1];
    var height = shape[2];


    var byteBuffer = GetPhotoAsByteBuffer(image, width, height);


    var outputLocations = new float[1][] { new float[labels.Count] };
    var outputs = Java.Lang.Object.FromArray(outputLocations);


    interpreter.Run(byteBuffer, outputs);
    var classificationResult = outputs.ToArray<float[]>();


    //Map the results
    var classificationModelList = new List<ImageResult>();


    for (var i = 0; i < labels.Count; i++)
    {
        var label = labels[i]; classificationModelList.Add(new ImageResult()
        {
            Name = label,
            Percentage = classificationResult[0][i]
        });
    }


    var top = classificationModelList.OrderByDescending(c => c.Percentage).FirstOrDefault();


    return top;
}

GitHub Gist ↗

¿Que queda a partir de aquí? Rogar que funcione jaja!

La pantalla inicial quedó de la siguiente manera:

En la segunda pantalla les presento al Oky!

Lo clasificó como un Cocker Spaniel con un 99% de probabilidad así que bien!

El código lo puedes conseguir en el siguiente repositorio:

https://github.com/rretamal/DogClassifier

Comentario finales:

El entrenamiento del modelo fue por su parte un proyecto en sí mismo, probé distintos modelos teniendo mejores o peores resultados y existen artículos completos de como mejorar la efectividad de los modelos. Lo importante a destacar aquí es la cantidad de aplicaciones infinitas para el Machine Learning y lo fácil de su implementación en aplicaciones móviles. .Net Maui tiene lo genial de las aplicaciones de Xamarin llevadas a un nuevo nivel con mejoras de performance, el ecosistema de control y el manejo multiplataforma es genial.

Algunas cosas dignas de mencionar:

  • Debes tener images, muchas! para poder entrenar correctamente un modelo de lo contrario obtienes resultados raros en tus pruebas
  • Google Colab presta excelentes recursos de forma gratuita con uso de GPU
  • Si bien utilice un modelo pre-entrenado tu tambien puedes crear el tuyo y jugar mejorandolo contínuamente, realizar combinaciones de modelos, aumentar tus datos de entrenamiento, jugar con los epochs, etc. Este artículo es interesante y trata de como mejoraron un clasificador: https://towardsdatascience.com/building-the-hotdog-not-hotdog-classifier-from-hbos-silicon-valley-c0cb2317711f

Happy Coding!