noviembre 7, 2020

~ 7 MIN

Producción - Front End

< Blog RSS

Front End

En el post anterior aprendimos a implementar un servidor web simple con Flask capaz de recibir imágenes a través de internet, usando una URL proporcionada por Heroku. Una vez recibida la imagen, el programa se la da a una red neuronal que hemos entrenado previamente para clasificar imágenes, la cual la procesa y calcula la probabilidad de que la imagen en cuestión pertenezca a las diferentes clases en las que fue entrenada. Finalmente devolvemos esta predicción. Utilizando el programa CURL fuimos capaces de probar nuestro servidor a través del terminal utilizando imágenes de muestra.

Sin embargo, para poder hacer que nuestra API sea lo más accesible posible, lo mejor es implementar una interfaz de usuario para poder interactuar con nuestra red neuronal desde un smartphone o un ordenador. Para ello vamos a construir una aplicación web, que será el front end de nuestro sistema.

Puedes aprender más sobre las diferentes tecnologías web que usaremos en nuestros vídeos, dónde también encontrarás otros recursos de aprendizaje.

La aplicación

La red neuronal que utilizamos en nuestra API ha sido entrenada para la clasificación de imágenes aéreas, útil para la generación automática de mapas de uso del terreno.

Así pues, nuestro front end consistirá en un mapa con cobertura global de imágenes aéreas (similar a google maps) en el que un usuario será capaz de seleccionar un área de interés en concreto, enviar la imagen al servidor y visualizar la predicción. Aquí puedes ver el resultado final.

Puedes encontrar todo el código de la aplicacion aquí

Setup del proyecto

Empezamos creando un nuevo repositorio en Github en el que tendremos nuestro código. Esto no sólo es importante desde el punto de vista de desarrollo de software, si no que también nos permitirá desplegar nuestra aplicación de manera sencilla (y gratuíta) como veremos más adelante.

Aquí tienes un par de vídeos para aprender más sobre Git y Github.

HTML

Empezamos definiendo la estructura de nuestra aplicación en un archivo llamado index.html. En él definiremos los siguientes elementos:

  • El mapa que usaremos para seleccionar el área de interés y extraer las imágenes
  • Un panel con:
    • Un botón para enviar la imagen al servidor
    • Un elemento vacío en el que insertaremos la predicción
  • Un rectángulo para indicar el área que seleccionaremos
  • Un elemento canvas para recortar el centro del mapa (sus atributos width and height determinarán las dimensiones de la imagen usada para la predicción)

Para renderizar el mapa y generar las imágenes que enviaremos al servidor, utilizaremos la librería Leaflet y el plugin Leaflet Image, por lo que linkaremos estas librería en nuestro html. Por último, linkaremos también nuestros archivos style.css y index.js con los estilos y la lógica de nuestra aplicación, respectivamente.

<!DOCTYPE html>
<html lang="es">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Mapa</title>
    <!-- Importamos Leaflet -->
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css"
        integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A=="
        crossorigin="" />
    <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"
        integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA=="
        crossorigin=""></script>
    <script src="leaflet-image.js"></script>
    <!-- Importamos nuestros estilos y lógica -->
    <link rel="stylesheet" href="style.css">
    <script src="index.js"></script>
</head>

<body>
    <div id="map"></div>
    <div class="panel">
        <button id="btn">CLASIFICA</button>
        <p id="resultado"></p>
    </div>
    <div class="rect"></div>
    <canvas id="crop_canvas" width="224" height="224" style="display: none"></canvas>
</body>

</html>

Aprende sobre HTML en este vídeo.

CSS

Estos son los estilos que usaremos en nuestra APP. El mapa ocupará toda la pantalla, mientras que el panel con el botón y la información estará en la esquina superior derecha. El rectángulo estará centrado en el mapa, con un borde rojo, para indicar a un usuario la zona del mapa que se usará exactamente para la predicción. En este caso usaremos un cuadrado de 224 x 224 pixeles ya que es el tamaño que usamos para entrenar nuestra red. Aún así, puedes usar otro tamaño (la API de Flask se asegura de hacer un re-escalado de la imagen si las dimensiones no son las esperadas).

* {
   margin: 0;
   padding: 0; 
}

body {
    position: relative;
}

#map { 
    width: 100%;
    height: 100vh;
    z-index: 1;
}

.panel {
    position: absolute;
    top: 10px; 
    right: 10px;
    width: 200px;
    height: 120px;
    background-color: #fafafa;
    z-index: 2;
    box-sizing: border-box;
    padding: 20px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: space-around;
}

#btn {
    background-color: black;
    color: white;
    border: none;
    width: 100px;
    height:30px;
}

.rect {
    position: absolute;
    width: 224px;
    height: 224px;
    top: 50%; 
    left: 50%;
    transform: translateX(-50%) translateY(-50%);
    border: 3px solid red;
    background: none;
    z-index: 3;
}

Aprende más sobre CSS en este vídeo.

Javascript

Y aquí tienes la lógica de la APP. El proceso es el siguiente:

  1. Una vez se carga el contenido de la página, instanciamos el mapa (centrado en unas coordenadas determinadas) y le asignamos una capa, en este caso imágenes aéreas proporcionadas por google.
  2. Asignamos las acciones a llevar a cabo cuando un usuario hace click en el botón:
    • Deshabilitamos el botón para evitar llamadas mútliples
    • Generamos una imagen del mapa entero usando el plugin leaflet-image.
    • Cortamos el centro de la imagen a las dimensiones deseadas (ancho y alto del elemento crop_canvas).
    • Enviamos la imagen a la API
    • Cuando recibimos la respuesta, se la mostramos al usuario y volvemos a habilitar el botón.
// Pon aquí la URL de tu API
const url = 'https://juansensio-blog-flask.herokuapp.com/predict'

// Puedes usar una url local durante el desarrollo para asegurarte que todo funciona bien
//const url = 'http://127.0.0.1:5000/predict'

// esperar a que se cargue el contenido de la app
document.addEventListener('DOMContentLoaded', function () {

    // instanciar el mapa con coordenadas y zoom inicial
    const map = L.map('map', {
        preferCanvas: true
    }).setView([41.39, 2.15], 17);

    // assignar capa al mapa con imágenes aéreas de google maps
    L.tileLayer('http://{s}.google.com/vt/lyrs=s&x={x}&y={y}&z={z}', {
        maxZoom: 20,
        subdomains: ['mt0', 'mt1', 'mt2', 'mt3']
    }).addTo(map);

    // asignar acciones al botón
    const btn = document.getElementById('btn')
    btn.addEventListener('click', function () {

        // cambiar el texto y deshabilitar el botón para evitar llamadas concurrentes
        btn.innerText = 'Cargando ...'
        btn.disabled = true

        // convertir el mapa a imagen 
        return leafletImage(map, function (err, canvas) {

            // generamos una imagen con el mapa
            var img = new Image();
            img.src = canvas.toDataURL();

            // cuando la imagen ha sido cargada, cortamos el centro y lo enviamos a la API
            img.onload = function () {

                // cortamos el centro al tamaño deseado (224 x 224)
                crop_canvas = document.getElementById('crop_canvas');
                const w = crop_canvas.width
                const h = crop_canvas.height
                const sx = (canvas.width - w) / 2
                const sy = (canvas.height - h) / 2
                crop_canvas.getContext('2d').drawImage(img, sx, sy, w, h, 0, 0, w, h);

                // enviamos la imagen a la API 
                return crop_canvas.toBlob(function (blob) {
                    const formData = new FormData()
                    formData.append('file', blob)
                    return fetch(url, {
                        method: 'post',
                        body: formData
                    })
                        // recibimos la respuesta y la pintamos en la app
                        .then(res => res.json())
                        .then(res => {
                            const panel = document.getElementById('resultado')
                            panel.innerText = res.label

                            // habilitamos de nuevo el botón
                            btn.innerText = 'Clasifica'
                            btn.disabled = false
                        })
                })
            }
        })
    })
})

Si quieres jugar con el código te aconsejo utilizar tu API en local, así podrás ver de manera sencilla qué es lo que recibe, por ejemplo puedes guardar la imagen en un archivo para asegurarte que estás enviando imágenes correctas.

Desplegando en Github Pages

Una vez nos hemos asegurado que todo funciona correctamente, podemos desplegar la aplicación en Github Pages. Puedes ver un ejemplo aquí.

Aprender más sobre Github Pages con este vídeo.

Resumen

En este post hemos visto cómo implementar una interfaz de usuario para comunicarnos con nuestro servidor Flask. Esta aplicación está implementada con tecnologías web: HTML, CSS y Javascript. Una vez implementado el código, podemos usar Github Pages para desplegar nuestra aplicación, obteniendo una URL para poder conectarnos a través de un navegador. De esta manera hemos conseguido cerrar el círculo completo de la puesta en producción de una red neuronal, desde su entrenamiento, puesto en marcha en un servidor web y accesible a través de una aplicación.

< Blog RSS