Integrando AutoDraw el AI API de Google, con Angular

Integrando AutoDraw el AI API de Google, con Angular
👋🏻
Este post fue escrito por Juan Delgadillo, un ingeniero de software internacional, mentor y viajero con más de una década de experiencia ayudando a startups y empresas de todo el mundo a diseñar, construir e implementar sus aplicaciones web y móviles con un fuerte énfasis en el valor agregado, la experiencia del usuario y la eficiencia.

En este post explico como integrar el AI API AutoDraw de Google, usando Angular y Canvas. AutoDraw es un experimento de inteligencia artificial AI que ellos lanzaron hace unas pocas semanas el cual te permite dibujar o al menos garabatear lo que te gustaría dibujar y luego mediante inteligencia artificial/aprendizaje profundo y redes neuronales artificiales pre-entrenadas te recomienda posibles sugerencia de dibujos los cuales tu estarías tratando de dibujar.


Tabla de contenidos


Instalando Angular CLI

Angular CLI nos permite crear proyectos Angular de una manera rápida, para instalarlo tienes que correr el siguiente comando:

npm install -g @angular/cli

Una vez el comando anterior ha finalizado, puedes verificar la versión de Angular CLI escribiendo:

ng --version 

Creando nuestra aplicación Angular

Ahora que ya tenemos Angular CLI instalado, vamos a crear nuestro proyecto corriendo el siguiente comando:

ng new angular-autodraw

Tendremos que esperar un poco para que el comando anterior termine, ya que creará la estructura de nuestra aplicación Angular así como también carpetas y archivos necesarios para correr nuestro proyecto, además de las dependencias npm.

Una vez que ha finalizado podremos movernos al directorio angular-autodraw corriendo:

cd angular-autodraw
npm start

Seremos capaz de ver nuestra aplicación corriendo en  http://localhost:4200

UI de AutoDraw

Ahora que tenemos nuestra aplicación corriendo, vamos a definir como se va a ver el UI desde una perspectiva de HTML, vamos a ver el código completo y luego lo discutiremos:

<div class="canvas">
  <canvas #canvas width="350" height="350"></canvas>
  <button type="button" class="clear-canvas-button" (click)="eraseCanvas()">Clear canvas</button>
</div>
<div class="autodraw-results">
  <ng-template ngFor [ngForOf]="drawSuggestions" let-suggestion>
    <figure class="autodraw-image" *ngFor="let icon of suggestion.icons" (click)="pickSuggestion(icon)">
      <img src="{{ icon }}" width="90" height="90" alt="{{ suggestion.name }}" title="{{ suggestion.name }}">
    </figure>
  </ng-template>
</div>

Como pueden ver es muy sencillo lo que tenemos como estructura html de nuestro componente AutoDraw, necesitamos un elemento cavas en el cual dibujaremos y también necesitamos un contenedor donde pondremos los resultados. Noten la variable local #canvas la necesitaremos para referenciar a nuestro elemento canvas desde el componente y también para subscribirnos a algunos eventos del mouse para tomar desiciones dependiendo de ellos.

Servicio de AutoDraw

Crearemos un servicio el cual nos permitirá realizar algunas solicitudes a el API AutoDraw de Google, y también, el servicio estará a cargo de cargar algunas plantillas las cuales posteriormente analizaremos buscando coincidencias con la respuesta a la solicitud para ofrecer sugerencias de dibujo.

import { Headers, RequestOptions, Http } from '@angular/http';
import { Injectable } from '@angular/core';
import 'rxjs/add/operator/map';
const API_ENDPOINT = 'https://inputtools.google.com/request?ime=handwriting&app=autodraw&dbg=1&cs=1&oe=UTF-8';
const STENCILS_ENDPOINT = 'src/data/stencils.json';
@Injectable()
export class AutoDrawService {
stencils;
constructor(
    private http: Http
  ) { }
loadStencils () {
    this.http.get(STENCILS_ENDPOINT).subscribe(response => this.stencils = response.json());
  }
drawSuggestions (
    shapes: Array<Array<number[]>>, drawOptions: {
    canvasWidth: number,
    canvasHeight: number
  }) {
    let headers = new Headers({
      'Content-Type': 'application/json; charset=utf-8'
    });
    let options = new RequestOptions({ headers });
return this.http.post(
      API_ENDPOINT,
      JSON.stringify({
        input_type: 0,
        requests: [{
          language: 'autodraw',
          writing_guide: {
            "width": drawOptions.canvasWidth,
            "height": drawOptions.canvasHeight
          },
          ink: shapes
        }]
      }),
      options
    ).map(response => {
      let data = response.json();
      let results = JSON.parse(data[1][0][3].debug_info.match(/SCORESINKS: (.*) Service_Recognize:/)[1])
        .map(result => {
          return {
            name: result[0],
            icons: (this.stencils[result[0]] || []).map(collection => collection.src)
          }
        });
      return results;
    });
  }
}

Este servicio tiene dos métodos principales, loadStencils() el cual carga todas las plantillas que nos servirán para buscar coincidencias con la respuesta a nuestra solicitud al API, y drawSuggestions() que esta a cargo de hacer la solicitud necesaria al API de AutoDraw retornando las coincidencias con nuestras plantillas las cuales a su vez serán las sugerencias de nuestro dibujo.

Ahora que ya tenemos el servicio y el UI, la única cosa que falta sería nuestra principal funcionalidad la cual estará ubicada en nuestro componente. Entonces, vamos a crearlo.

Funcionalidad de AutoDraw

Primero vamos a ver el código y luego lo discutimos, nos estaremos enfocando en las partes importantes:

import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { Subscription } from 'rxjs/Subscription';
import { Observable } from 'rxjs/Observable';
import { AutoDrawService } from './services';
import 'rxjs/add/observable/fromEvent';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit, OnDestroy {
constructor (
    private autoDrawService: AutoDrawService
  ) {}
@ViewChild('canvas') canvas;
drawSuggestions: Array<object>;
canvasMouseEventSubscriptions: Subscription[];
previousXAxis: number = 0;
  previousYAxis: number = 0;
  currentXAxis:  number = 0;
  currentYAxis:  number = 0;
context;
  pressedAt: number;
  pressing:  boolean = false;
  currentShape: Array<number[]>;
  shapes: Array<Array<number[]>> = [];
  intervalLastPosition: number[] = [-1, -1];
ngOnInit () {
    this.autoDrawService.loadStencils();
    this.context = this.canvas.nativeElement.getContext('2d');
    let mouseEvents = ['mousemove', 'mousedown', 'mouseup', 'mouseout'];
this.canvasMouseEventSubscriptions = mouseEvents.map(
      (mouseEvent: string) => Observable
        .fromEvent(this.canvas.nativeElement, mouseEvent)
        .subscribe((event: MouseEvent) => this.draw(event))
    );
  }
ngOnDestroy () {
    for (let mouseEventSubscription of this.canvasMouseEventSubscriptions) {
      mouseEventSubscription.unsubscribe();
    }
  }
eraseCanvas () {
    this.shapes = [];
    this.context.clearRect(0, 0, this.canvas.nativeElement.width, this.canvas.nativeElement.height);
  }
prepareNewShape () {
    this.currentShape = [
      [], // X coordinates
      [], // Y coordinates
      []  // Times
    ];
  }
storeCoordinates() {
    if (this.intervalLastPosition[0] !== this.previousXAxis && this.intervalLastPosition[1] !== this.previousYAxis) {
      this.intervalLastPosition = [this.previousXAxis, this.previousYAxis];
      this.currentShape = [
        [...this.currentShape[0], this.previousXAxis],
        [...this.currentShape[1], this.previousYAxis],
        [...this.currentShape[2], Date.now() - this.pressedAt]
      ];
    }
  }
onDrawingMouseDown (mouseEvent: MouseEvent) {
    let highlightStartPoint, drawColorStartingPoint = 'black';
this.previousXAxis = this.currentXAxis;
    this.previousYAxis = this.currentYAxis;
    this.currentXAxis = mouseEvent.clientX - this.canvas.nativeElement.offsetLeft;
    this.currentYAxis = mouseEvent.clientY - this.canvas.nativeElement.offsetTop;
this.pressing = true;
    this.pressedAt = Date.now();
    highlightStartPoint = true;
this.prepareNewShape();
if (highlightStartPoint) {
      this.context.beginPath();
      this.context.fillStyle = drawColorStartingPoint;
      this.context.fillRect(this.currentXAxis, this.currentYAxis, 2, 2);
      this.context.closePath();
      highlightStartPoint = false;
    }
// Stores coordinates every 9ms
    return window.setInterval(() => this.storeCoordinates(), 9);
  }
onDrawingMouseMove (mouseEvent: MouseEvent) {
    let drawStroke = 8, drawColor  = 'black';
this.previousXAxis = this.currentXAxis;
    this.previousYAxis = this.currentYAxis;
    this.currentXAxis = mouseEvent.clientX - this.canvas.nativeElement.offsetLeft;
    this.currentYAxis = mouseEvent.clientY - this.canvas.nativeElement.offsetTop;
this.context.beginPath();
    this.context.moveTo(this.previousXAxis, this.previousYAxis);
    this.context.lineTo(this.currentXAxis, this.currentYAxis);
    this.context.strokeStyle = drawColor;
    this.context.fillStyle = drawColor;
    this.context.lineCap = 'round';
    this.context.lineJoin = 'round';
    this.context.lineWidth = drawStroke;
    this.context.stroke();
    this.context.closePath();
  }
draw(mouseEvent: MouseEvent) {
    let storeCoordinateInterval;
if (mouseEvent.type === 'mousedown') {
      storeCoordinateInterval = this.onDrawingMouseDown(mouseEvent);
    }
if (mouseEvent.type === 'mouseup' || this.pressing && mouseEvent.type === 'mouseout') {
      this.pressing = false;
      clearInterval(storeCoordinateInterval);
      this.commitCurrentShape();
    }
if (mouseEvent.type === 'mousemove' && this.pressing) {
      this.onDrawingMouseMove(mouseEvent);
    }
  }
commitCurrentShape() {
    this.shapes.push(this.currentShape);
    let drawOptions = {
      canvasWidth: this.canvas.nativeElement.width,
      canvasHeight: this.canvas.nativeElement.height
    };
this.autoDrawService.drawSuggestions(this.shapes, drawOptions)
      .subscribe(suggestions => this.drawSuggestions = suggestions);
  }
pickSuggestion(source: string) {
    this.eraseCanvas();
    let image = new Image();
    image.onload = () => this.context.drawImage(image, 0, 0);
    image.src = source;
  }
}

Este componente inyecta el servicio AutoDrawService que creamos anteriormente, también crea una variable de instancia llamada #canvas apuntando a un hijo de nuestra vista/UI que ya hemos definido. En ngOnInit, uno de los ciclos de vida de los componentes de Angular, cargamos las plantillas de nuestro servicio y también nos subscribimos a algunos eventos del mouse los cuales estamos interesados escuchar, para luego posteriormente pasar la información del evento a nuestro método principal draw() el cual dependiendo del tipo de evento del mouse llamará otros métodos que realizarán actividades específicas.

Conclusión

Integrar esta API ha sido una experiencia bastante emocionante, el hecho de ver como la inteligencia artificial AI esta evolucionando cada día. Hoy en día nuevas herramientas y servicios usan profundas redes neuronales y AI para realizar ciertas actividades en muchas disciplinas como vision por computador, procesamiento de audio y habla, procesamiento del lenguaje natural, bio informática, química, motores de búsqueda y así sucesivamente.

Es realmente fácil integrar y usar cualquier API con Angular debido a la potente forma en la que esta diseñado y también por la fácil manera que se integra con la programación reactiva.

Actualmente estoy aprendiendo como crear redes neuronales profundas y aprovechar sus capacidades en muchos de los sistemas en los que esto trabajando. Estoy leyendo el libro “Deep Learning” escrito por Ian Goodfellow, Yoshua Bengio, and Aaron Courville.

Para el final de este año espero tener una red neuronal profunda implementada por mi mismo, les voy a compartir  toda la experiencia que obtuve y como integrarlas usando frameworks y herramientas actuales como Angular… estén al tanto ;)

Pueden verificar todo el código fuente aquí. Y ver un demo en vivo aquí.

Saludos y abrazos asíncronos.