AWS Rekognition para generar un Avatar

18 de septiembre de 2019

AWS es la plataforma en la nube de Amazon repleta de servicios listos para consumir. En esta entrada vamos a echar un ojo a uno de los servicios de AWS, concretamente a Rekognition, que es un servicio de Machine Learning capaz de analizar fotografías y videos en busca de objetos, personas o texto.

Pero más que analizar el servicio en sí, vamos a ver un caso práctico de uso, en el que desarrollaremos una aplicación que hará uso de este servicio. El objetivo será generar un avatar en función de las características de la persona que aparezca en la foto que queramos analizar.

Las tecnologías con las que vamos a trabajar son:

Otras opciones

AWS no es el único en ofrecernos servicios de reconocimiento facial. Tanto Google como Microsoft también incorporan servicios similares en sus plataformas.

AWS Rekognition

Tenemos 12 meses para utilizar la API de forma gratuita, con restricción a 5.000 imágenes al mes.

La API cuenta con una biblioteca cliente para múltiples lenguajes, lo que facilita su uso y configuración.

En cuanto a la respuesta de la API, además de detectar la posición de los elementos de la cara, nos ofrece diferentes etiquetas para saber si la cara posee una determinada característica, por ejemplo, si tiene la boca o los ojos abiertos. La mayoría de etiquetas además de la probabilidad, incorpora un boolean, es decir:

mouthOpen=false 97.0: Nos indica que la foto tiene la boca cerrada con un 97% de probabilidad

Google Vision AI

La API de Google para el tratamiento de imágenes se llama Vision AI. Al igual que AWS, nos ofrece el servicio de forma gratuita durante 12 meses y para las primeras 1.000 imágenes por mes.

También dispone de una biblioteca, en varios lenguajes, para facilitar la vida a los clientes de la plataforma.

En cuanto a la API, ofrece diferentes opciones de análisis, en función del objetivo, aunque podemos combinarlas. Así, en función de lo que queramos analizar tenemos:

A diferencia de la API de AWS, no nos ofrece booleanos en las etiquetas, sino que solamente nos da el porcentaje de coincidencia. Además el numero de etiquetas es más reducido.

Azure Computer Vision

Azure dispone de Computer Vision Face

En este caso disponemos también de 12 meses de servicios gratuitos, para hacer 30.000 transacciones al mes con la API de reconocimiento facial.

Disponemos también de una SDK en Java que podemos obtener via Maven para facilitar la integración de nuestro cliente.

En cuanto a la respuesta de la API nuevamente nos detecta la posición de los elementos de la cara, para así obtener el estado de ánimo de la persona.

En este caso, además del estado de ánimo, la API devuelve más características de la cara, como por ejemplo el color del pelo, o si es calvo, si lleva maquillaje o accesorios como sombrero o gafas. En este caso tenemos atributos que se devuelven con un porcentaje de probabilidad y otros que se devuelven como booleanos.


¡Empecemos!

AWS Rekognition

Empezaremos por configurar una cuenta en AWS que nos permita utilizar el servicio. Dado que es la primera vez que lo hacemos, dispondremos del periodo de prueba de 12 meses, que para esta prueba de concepto ya nos viene bien.

¡Ah!, por cierto, podéis tener más detalle sobre AWS Rekognition y su configuración con la propia documentación de AWS: https://docs.aws.amazon.com/rekognition/latest/dg/rekognition-dg.pdf

Creación de la cuenta en AWS Rekognition

Si ya tienes cuenta en AWS, puedes saltarte este paso, pero si no…no te queda otra

  1. Accederemos a https://aws.amazon.com para crear la cuenta pulsando en el botón "Cree una cuenta de AWS"

  1. Introducimos los datos que nos solicita para crear la cuenta a través de varias pantallas:

Bien, ya tenemos creada nuestra cuenta, con algun que otro mail de bienvenida en nuestro correo, por lo que entraremos a nuestro panel de administración con el email y contraseña que hemos dado, en el que tendremos acceso a todos los servicios de AWS y que tendrá el siguiente aspecto.

Debemos tener en cuenta que lo que hemos creado es la cuenta “maestra”, que tendrá asociado todo el espacio de trabajo de nuestra “compañía”, así como los datos de contacto y facturación de todo lo que hagamos (no temáis, recordad que estamos en el plan básico).

Generar credenciales de acceso

Todos los servicios que utilicemos en AWS están protegidos, por lo que para poder acceder a ellos será necesario que proporcionemos unas credenciales.

Para obtener estas credenciales utilizaremos el servicio de IAM (Identity & Access Management), que nos permitirá administrar las cuentas de usuario que queramos y asociarles los permisos que necesitemos.

Así pues, debemos crear una nueva cuenta de usuario que tenga acceso al servicio de Amazon Rekognition.

  1. Accedemos al servicio IAM

  2. Creamos el Usuario seleccionando la opción Usuarios y Añadir usuario(s).

  1. A partir de aquí tendremos un wizard de 5 pasos. En el primer paso tendremos que dar el nombre de usuario rekogUser, e indicaremos que el tipo de acceso es Acceso mediante programación, lo que nos permitirá obtener unas credenciales de acceso.

  1. A continuación asignaremos al usuaro el permiso a Rekognition. Para simplificar le asignaremos la política directamente al usuario, sin necesidad de crear grupos. Las políticas definen permisos de acceso sobre los servicios o recursos de AWS. Dado que nosotros queremos acceder al servicio de Rekognition para consulta, asignaremos la política AmazonRekognitionReadOnlyAccess a nuestro usuario.

  1. Una vez seleccionada la política, avanzamos en el wizard hasta llegar al resumen y aceptamos los datos para la creación del usuario.

  2. Ya creado el usuario veremos una pantalla de “Éxito” junto con el usuario creado y sus credenciales. En este punto es importante que nos guardemos tanto el ID de clave de acceso (Access Key ID) como la Clave de acceso secreta (Secret Access key), ya que no se volverán a mostrar nunca más. Si por algún motivo los perdieras, deberás volver a generar un nuevo par de claves para el usuario. Podemos descargar estas claves en formato CSV. También podemos observar el enlace a la consola de administración que deberán utilizar estos usuarios, en caso que necesiten acceder a la consola (que no es nuestro caso)

SpringBoot

El acceso a AWS se realiza mediante una API Rest, por lo que podríamos acceder directamente desde nuestro FrontEnd, pero como vamos a necesitar otros servicios, crearemos un middle-tier con SpringBoot y centraremos en él todas las llamadas. Para desarrollar este proyecto utilizaré IntelliJ, aunque podéis utilizar el IDE que mejor se adapte a vuestros gustos. IntelliJ incorpora la funcionalidad de Spring Initializr, lo que nos facilita la creación de nuestro proyecto. Para utilizarlo basta con crear un nuevo proyecto (File -> New -> Project -> Spring Initializr. También podeis generarlo directamente desde la web de Spring Initializr: https://start.spring.io

Introducimos la información que se nos solicita para identificar nuestro proyecto y sus principales características

No añadiremos ninguna dependencia, ya las pondremos después a mano.

Para finalizar, damos el nombre de nuestro proyecto y la ruta en la que se generará el código.

A continuación, IntelliJ creará el proyecto SpringBoot con el nombre “avatar”, donde podemos encontrar las siguientes características:

Antes de entrar en materia, añadiremos al fichero pom.xml las dependencias que necesitamos para la conexión con AWS, lombok y las dependencias de Springboot web:

Nota: Para poder utilizar Lombok es necesario que lo tengáis instalado en vuestro IDE. Podeis ver las instrucciones en https://projectlombok.org

Modelo

El modelo de nuestro proyecto consiste en una clase con los atributos del Avatar. Para evitar problemas con los valores del modelo, limitaremos la lista de valores de cada atributo con un enum. Lombok se encargará de crear los metodos de acceso a nuestros atributos.

package es.mimacom.demo.avatar.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Avatar {

    public enum AVATAR_STYLE {Circle, Transparent}

    public enum TOP {
        NoHair,
        Eyepatch,
        Hat,
        Hijab,
        Turban,
        WinterHat1,
        WinterHat2,
        WinterHat3,
        WinterHat4,
        LongHairBigHair,
        LongHairBob,
        LongHairBun,
        LongHairCurly,
        LongHairCurvy,
        LongHairDreads,
        LongHairFrida,
        LongHairFro,
        LongHairFroBand,
        LongHairNotTooLong,
        LongHairShavedSides,
        LongHairMiaWallace,
        LongHairStraight,
        LongHairStraight2,
        LongHairStraightStrand,
        ShortHairDreads01,
        ShortHairDreads02,
        ShortHairFrizzle,
        ShortHairShaggyMullet,
        ShortHairShortCurly,
        ShortHairShortFlat,
        ShortHairShortRound,
        ShortHairShortWaved,
        ShortHairSides,
        ShortHairTheCaesar,
        ShortHairTheCaesarSidePart
    }

    public enum ACCESSORIES {
        Blank,
        Kurt,
        Prescription01,
        Prescription02,
        Round,
        Sunglasses,
        Wayfarers
    }

    public enum HAIRCOLOR {
        Auburn,
        Black,
        Blonde,
        BlondeGolden,
        Brown,
        BrownDark,
        PastelPink,
        Platinum,
        Red,
        SilverGray
    }

    public enum FACIALHAIR {
        Blank,
        BeardMedium,
        BeardLight,
        BeardMagestic,
        MoustacheFancy,
        MoustacheMagnum
    }

    public enum CLOTHE {
        BlazerShirt,
        BlazerSweater,
        CollarSweater,
        GraphicShirt,
        Hoodie,
        Overall,
        ShirtCrewNeck,
        ShirtScoopNeck,
        ShirtVNeck
    }

    public enum CLOTHECOLOR {
        Black,
        Blue01,
        Blue02,
        Blue03,
        Gray01,
        Gray02,
        Heather,
        PastelBlue,
        PastelGreen,
        PastelOrange,
        PastelRed,
        PastelYellow,
        Pink,
        Red,
        White
    }

    public enum EYE {
        Close,
        Cry,
        Default,
        Dizzy,
        EyeRoll,
        Happy,
        Hearts,
        Side,
        Squint,
        Surprised,
        Wink,
        WinkWacky
    }

    public enum EYEBROW {
        Angry,
        AngryNatural,
        Default,
        DefaultNatural,
        FlatNatural,
        RaisedExcited,
        RaisedExcitedNatural,
        SadConcerned,
        SadConcernedNatural,
        UnibrowNatural,
        UpDown,
        UpDownNatural
    }

    public enum MOUTH {
        Concerned,
        Default,
        Disbelief,
        Eating,
        Grimace,
        Sad,
        ScreamOpen,
        Serious,
        Smile,
        Tongue,
        Twinkle,
        Vomit
    }

    public enum SKINCOLOR {
        Tanned,
        Yellow,
        Pale,
        Light,
        Brown,
        DarkBrown,
        Black
    }

    AVATAR_STYLE avatarStyle;
    TOP top;
    ACCESSORIES accessories;
    HAIRCOLOR hairColor;
    FACIALHAIR facialHair;
    CLOTHE clothe;
    CLOTHECOLOR clotheColor;
    EYE eye;
    EYEBROW eyebrow;
    MOUTH mouth;
    SKINCOLOR skinColor;

    public static Avatar getDefault() {
        Avatar avatar = Avatar.builder().build();
        avatar.setAvatarStyle(AVATAR_STYLE.Circle);
        avatar.setTop(TOP.Hat);
        avatar.setAccessories(ACCESSORIES.Wayfarers);
        avatar.setFacialHair(FACIALHAIR.Blank);
        avatar.setClothe(CLOTHE.BlazerShirt);
        avatar.setEye(EYE.Default);
        avatar.setEyebrow(EYEBROW.Default);
        avatar.setMouth(MOUTH.Default);
        avatar.setSkinColor(SKINCOLOR.Light);

        return avatar;
    }
}

Servicio

Antes de empezar a ver el código del servicio, necesitamos configurar las credenciales que hemos generado anteriormente. Estas credenciales las pondremos en el archivo application.properties de la siguiente manera:

En nuestra clase podremos leer estas propiedades mediante la anotación @Value, que inyectará el valor de la propiedad indicada en el atributo precedido de esta anotación.

Posteriormente, la conexión a AWS la configuraremos en un método anotado con @PostConstruct, lo que nos garantiza haber leído las propiedades antes de establecer la conexión.

Ya sólo nos falta el método de nuestro servicio que se ocupará de:

Para completar nuestra clase, crearemos un método más que devolverá la lista de valores que se solicite, de forma que, si el avatar no se parece a la foto original, daremos la opción al usuario de modificar algún aspecto destacable, como el color del pelo o el tipo de peinado.

package es.mimacom.demo.avatar.service;

import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.regions.Region;
import com.amazonaws.regions.RegionUtils;
import com.amazonaws.services.rekognition.AmazonRekognition;
import com.amazonaws.services.rekognition.AmazonRekognitionClient;
import com.amazonaws.services.rekognition.AmazonRekognitionClientBuilder;
import com.amazonaws.services.rekognition.model.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import es.mimacom.demo.avatar.model.Avatar;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.PostConstruct;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;


@Service
public class AvatarService {

    private AmazonRekognition rekognitionClient;
    private static Logger logger = LoggerFactory.getLogger(AvatarService.class);

    @Value("${aws.accessKey}")
    private String accessKey;
    @Value("${aws.secretKey}")
    private String secretKey;
    @Value("${aws.region}")
    private String region;

    @PostConstruct
    private void initializeAmazon() {
        AWSCredentials credentials = new BasicAWSCredentials(this.accessKey, this.secretKey);
        rekognitionClient = new AmazonRekognitionClient(credentials);
        rekognitionClient.setRegion(RegionUtils.getRegion(region));
    }


    public List<String> getList(String id) {
        logger.debug("Obtaining list for: " + id);
        switch (id) {
            case "TOP":
                return Stream.of(Avatar.TOP.values()).map(Avatar.TOP::name).collect(Collectors.toList());
            case "ACCESSORIES":
                return Stream.of(Avatar.ACCESSORIES.values()).map(Avatar.ACCESSORIES::name).collect(Collectors.toList());
            case "HAIRCOLOR":
                return Stream.of(Avatar.HAIRCOLOR.values()).map(Avatar.HAIRCOLOR::name).collect(Collectors.toList());
            case "FACIALHAIR":
                return Stream.of(Avatar.FACIALHAIR.values()).map(Avatar.FACIALHAIR::name).collect(Collectors.toList());
            case "CLOTHE":
                return Stream.of(Avatar.CLOTHE.values()).map(Avatar.CLOTHE::name).collect(Collectors.toList());
            case "CLOTHECOLOR":
                return Stream.of(Avatar.CLOTHECOLOR.values()).map(Avatar.CLOTHECOLOR::name).collect(Collectors.toList());
            case "EYE":
                return Stream.of(Avatar.EYE.values()).map(Avatar.EYE::name).collect(Collectors.toList());
            case "EYEBROW":
                return Stream.of(Avatar.EYEBROW.values()).map(Avatar.EYEBROW::name).collect(Collectors.toList());
            case "MOUTH":
                return Stream.of(Avatar.MOUTH.values()).map(Avatar.MOUTH::name).collect(Collectors.toList());
            case "SKINCOLOR":
                return Stream.of(Avatar.SKINCOLOR.values()).map(Avatar.SKINCOLOR::name).collect(Collectors.toList());
            default:
                return new ArrayList<String>();
        }
    }

    public Avatar generateAvatar(MultipartFile multipartFile) {
        Avatar avatar = Avatar.getDefault();
        try {
            logger.debug("Preparing avatar for image:" + multipartFile.getName());
            Image image = new Image().withBytes(ByteBuffer.wrap(multipartFile.getBytes()));
            DetectFacesResult faceResult = awsDetectFaces(image, Attribute.ALL);
            if (faceResult.getFaceDetails() != null && faceResult.getFaceDetails().size() > 0) {
                FaceDetail face = faceResult.getFaceDetails().get(0);
                avatar = buildAvatar(face);  
                if (logger.isDebugEnabled()) {
                    ObjectMapper objectMapper = new ObjectMapper();
                    logger.debug("Face features:");
                    logger.debug(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(face));
                }
            } else {
                logger.debug("No face detected by Rekognition");
            }

            if (logger.isDebugEnabled()) {
                ObjectMapper objectMapper = new ObjectMapper();
                logger.debug("Avatar features:");
                logger.debug(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(avatar));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        return avatar;
    }

    private DetectFacesResult awsDetectFaces(Image image, Attribute attributes) {
        DetectFacesRequest request = new DetectFacesRequest()
                .withImage(image)
                .withAttributes(attributes);
        return rekognitionClient.detectFaces(request);
    }
    
    
    private Avatar buildAvatar(FaceDetail face) {
        //TODO: Os lo dejo a vuestra imagniación
        return null;
    }
}

Controller

Por último, solo nos quedará publicar la API. Para ello crearemos una nueva clase AvatarController, que se encargará de recibir las peticiones del cliente, redirigir al servicio correspondiente y montar la respuesta en formato JSON.

Para evitar problemas de CORS, daremos acceso universal a nuestra api ('*')

package es.mimacom.demo.avatar.controller;

import es.mimacom.demo.avatar.model.Avatar;
import es.mimacom.demo.avatar.service.AvatarService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

@CrossOrigin(origins = "*", methods= {RequestMethod.GET,RequestMethod.POST})
@RestController
@RequestMapping("/api")
public class AvatarController {

    @Autowired
    private AvatarService avatarService;

    @PostMapping("/uploadFile")
    public Avatar uploadFile(@RequestPart(value = "file") MultipartFile photo) {
        return this.avatarService.generateAvatar(photo);
    }

    @GetMapping("/list/{id}")
    public List<String> getList(@PathVariable String id) {
        return this.avatarService.getList(id);
    }
}

React

Para poder empezar a trabajar con React, necesitaremos instalar un gestor de paquetes de node.js. En este ejemplo utilizaré YARN, aunque podéis utilizar cualquier gestor de paquetes que ya tengáis instalado.

Primero crearemos nuestra aplicación:

yarn create react-app avatar-app

Instalaremos también algunas dependencias que utilizaremos en el proyecto

yarn add reactstrap react-dom bootstrap avataaars

Esto nos creará toda la estructura básica para empezar nuestra aplicación. Podremos probarla ejecutando el siguiente comando y accediendo en un navegador a http://localhost:3000

yarn start

Con el mismo intelliJ podremos cargar nuestro proyecto React y editar/guardar todo lo que necesitemos. El servidor detectará los cambios y los actualizará en caliente, por lo que no será necesario reiniciar

Componente ImageUpload

Primero crearemos nuestro componente de carga de imágenes. ¿Qué queremos de este componente?.

La creación de nuestro componente la haremos en la carpeta src/imageUpload. De forma que en esta carpeta podamos poner todos los ficheros de los que se compone el componente (css, js, …)

import React, { Component } from 'react';
import {Form, Button, Input, Label} from 'reactstrap'
import 'bootstrap/dist/css/bootstrap.min.css'
import './ImageUpload.css'

class ImageUpload extends Component {

    constructor(props) {
        super(props);
        this.state = {
            file: '',
            imagePreviewUrl: '',
            onSubmit: this.props.onSubmit
        };
        this._handleImageChange = this._handleImageChange.bind(this);
        this._handleReset = this._handleReset.bind(this);
    }

    _handleImageChange(e) {
        e.preventDefault();

        let reader = new FileReader();
        let file = e.target.files[0];

        reader.onloadend = () => {
            this.setState({
                file: file,
                imagePreviewUrl: reader.result
            });
        }
        reader.readAsDataURL(file)
    }

    _handleReset(e) {
        e.preventDefault();

        this.setState({
            file: '',
            imagePreviewUrl: ''
        })
    }

    render() {
        let state = this.state;
        let $imagePreview = null;
        if (state.imagePreviewUrl != '') {
            $imagePreview = (
                <div>
                    <div className="image-container">
                        <img src={state.imagePreviewUrl} alt="Your Photo"/>
                    </div>
                    <p/>
                    <div>
                        <Button color="primary" onClick={() => this.state.onSubmit(this.state.file)}>Analyze Image</Button>
                        {' '}
                        <Button color="danger" onClick={this._handleReset}>Reset Iamge</Button>
                    </div>
                    <p/>
                </div>)
        }

        return (
            <div>
                <Form onSubmit={this._handleSubmit}>
                    <Label className="file-upload btn-primary">
                        <Input type="file" id="photo" onChange={this._handleImageChange} key={this.state.file}/>
                        <i className="fa fa-cloud-upload>"/>Upload your photo
                    </Label>
                    <div>
                        {$imagePreview}
                    </div>
                </Form>
            </div>
        )
    }
}
export default ImageUpload;

Analizando el código, vemos que el estado se compone de 3 atributos:

En el constructor inicializamos estos atributos en blanco, excepto la acción del botón, que la recuperamos de los parámetros de entrada al componente Tendremos 2 botones más, que gestionaremos con las acciones

En el render del componente comprobaremos si se ha seleccionado alguna foto, en cuyo caso mostraremos el bloque de previsualización y acciones

Componente Avatar

Nuestro componente de Avatar estará basado en el existente Avataaars. Mostrará una imagen con el Avatar generado por nuestro servicio en Java, pero además tendrá un formulario que nos debe permitir modificar sus características, por si hay alguna característica que necesitemos añadir (p.ej. sombrero, camiseta, pelo, …) que el servicio de AWS no pueda reconocer.

Para la implementación de este componente, debemos tener en cuenta qué necesitamos gestionar:

E identificamos la única acción que podrá realizarse en el componente: redibujar el avatar cada vez que se cambie algún atributo

import React, {Component} from 'react';
import {Col, Form, FormGroup, Input, Label} from 'reactstrap';
import Avatar from "avataaars";

class AvatarComponent extends Component {

    avatarProperties = ["TOP", "ACCESSORIES", "HAIRCOLOR", "FACIALHAIR", "CLOTHE", "CLOTHECOLOR", "EYE", "EYEBROW", "MOUTH", "SKINCOLOR"];

    constructor(props) {
        super(props);
        if (this.props.avatar) {
            this.state = {
                avatar: this.props.avatar,
                TOP: [],
                ACCESSORIES: [],
                HAIRCOLOR: [],
                FACIALHAIR: [],
                CLOTHE: [],
                CLOTHECOLOR: [],
                EYE: [],
                EYEBROW: [],
                MOUTH: [],
                SKINCOLOR: []
            }
        }
        this._handleChange = this._handleChange.bind(this);
    }

    fetchList(id) {
        const upstream = "http://localhost:8080/api/list/" + id;
        return fetch(upstream)
            .then((results) => results.json())
    }

    componentDidMount(): void {
        for (const property of this.avatarProperties) {
            this.fetchList(property).then(data => this.setState({[property]: data}))
        }
    }

    componentWillReceiveProps(nextProps: Readonly<P>, nextContext: any): void {
        this.setState({avatar: nextProps.avatar})
    }

    _handleChange(event) {
        const target = event.target;
        const value = target.value;
        const name = target.name;
        let avatar = {...this.state.avatar};
        avatar[name] = value;
        this.setState({avatar: avatar});
    }

    render() {
        const {avatar} = this.state;

        return <div>
            <div>
                <Avatar
                    style={{width: '150px', height: '150px'}}
                    avatarStyle={avatar.avatarStyle}
                    topType={avatar.top}
                    accessoriesType={avatar.accessories}
                    hairColor={avatar.hairColor}
                    facialHairType={avatar.facialHair}
                    clotheType={avatar.clothe}
                    clotheColor={avatar.clotheColor}
                    eyeType={avatar.eye}
                    eyebrowType={avatar.eyebrow}
                    mouthType={avatar.mouth}
                    skinColor={avatar.skinColor}
                />
            </div>
            <p/>
            <div>
                <Form horizontal>
                    <FormGroup row>
                        <Col sm={3}>
                            <Label for="avatarStyle">Avatar Style</Label>
                        </Col>
                        <Col sm={2}>
                            <Input type="radio" name="avatarStyle" id="avatarStyle" value="Circle"
                                   checked={(avatar.avatarStyle === "Circle") ? "checked" : ""}
                                   onChange={this._handleChange}/> Circle
                        </Col>
                        <Col sm={2}>
                            <Input type="radio" name="avatarStyle" id="avatarStyle" value="Transparent"
                                   checked={(avatar.avatarStyle === "Transparent") ? "ckecked" : ""}
                                   onChange={this._handleChange}/> Transparent
                        </Col>
                    </FormGroup>
                    <FormGroup row>
                        <Col sm={3}>
                            <Label for="top">Top</Label>
                        </Col>
                        <Col sm={4}>
                            <Input type="select" name="top" id="top" className="form-control" defaultValue={avatar.top}
                                   value={avatar.top} onChange={this._handleChange.bind(this)}>
                                {this.state.TOP.map((element) => {return <option value={element}>{element}</option>})}
                            </Input>
                        </Col>
                    </FormGroup>
                    <FormGroup row>
                        <Col sm={3}>
                            <Label for="accesories">Accesories</Label>
                        </Col>
                        <Col sm={4}>
                            <Input type="select" name="accessories" id="accessories" className="form-control"
                                   defaultValue={avatar.accessories} value={avatar.accessories}
                                   onChange={this._handleChange.bind(this)}>
                                {this.state.ACCESSORIES.map((element) => {return <option value={element}>{element}</option>})}
                            </Input>
                        </Col>
                    </FormGroup>
                    <FormGroup row>
                        <Col sm={3}>
                            <Label for="hairColor">Hair Color</Label>
                        </Col>
                        <Col sm={4}>
                            <Input type="select" name="hairColor" id="hairColor" className="form-control"
                                   defaultValue={avatar.hairColor} value={avatar.hairColor}
                                   onChange={this._handleChange.bind(this)}>
                                {this.state.HAIRCOLOR.map((element) => {return <option value={element}>{element}</option>})}
                            </Input>
                        </Col>
                    </FormGroup>
                    <FormGroup row>
                        <Col sm={3}>
                            <Label for="facialHair">Facial Hair</Label>
                        </Col>
                        <Col sm={4}>
                            <Input type="select" name="facialHair" id="facialHair" className="form-control"
                                   defaultValue={avatar.facialHair} value={avatar.facialHair}
                                   onChange={this._handleChange.bind(this)}>
                                {this.state.FACIALHAIR.map((element) => {return <option value={element}>{element}</option>})}
                            </Input>
                        </Col>
                    </FormGroup>
                    <FormGroup row>
                        <Col sm={3}>
                            <Label for="clothe">Clothes</Label>
                        </Col>
                        <Col sm={4}>
                            <Input type="select" name="clothe" id="clothe" className="form-control"
                                   defaultValue={avatar.clothe} value={avatar.clothe}
                                   onChange={this._handleChange.bind(this)}>
                                {this.state.CLOTHE.map((element) => {return <option value={element}>{element}</option>})}
                            </Input>
                        </Col>
                    </FormGroup>
                    <FormGroup row>
                        <Col sm={3}>
                            <Label for="clotheColor">Clothes color</Label>
                        </Col>
                        <Col sm={4}>
                            <Input type="select" name="clotheColor" id="clotheColor" className="form-control"
                                   defaultValue={avatar.clotheColor} value={avatar.clotheColor}
                                   onChange={this._handleChange.bind(this)}>
                                {this.state.CLOTHECOLOR.map((element) => {return <option value={element}>{element}</option>})}
                            </Input>
                        </Col>
                    </FormGroup>
                    <FormGroup row>
                        <Col sm={3}>
                            <Label for="eye">Eyes</Label>
                        </Col>
                        <Col sm={4}>
                            <Input type="select" name="eye" id="eye" className="form-control" defaultValue={avatar.eye}
                                   value={avatar.eye} onChange={this._handleChange.bind(this)}>
                                {this.state.EYE.map((element) => {return <option value={element}>{element}</option>})}
                            </Input>
                        </Col>
                    </FormGroup>
                    <FormGroup row>
                        <Col sm={3}>
                            <Label for="eyebrow">Eyebrow</Label>
                        </Col>
                        <Col sm={4}>
                            <Input type="select" name="eyebrow" id="eyebrow" className="form-control"
                                   defaultValue={avatar.eyebrow} value={avatar.eyebrow}
                                   onChange={this._handleChange.bind(this)}>
                                {this.state.EYEBROW.map((element) => {return <option value={element}>{element}</option>})}
                            </Input>
                        </Col>
                    </FormGroup>
                    <FormGroup row>
                        <Col sm={3}>
                            <Label for="mouth">Mouth</Label>
                        </Col>
                        <Col sm={4}>
                            <Input type="select" name="mouth" id="mouth" className="form-control"
                                   defaultValue={avatar.mouth} value={avatar.mouth}
                                   onChange={this._handleChange.bind(this)}>
                                {this.state.MOUTH.map((element) => {return <option value={element}>{element}</option>})}
                            </Input>
                        </Col>
                    </FormGroup>
                    <FormGroup row>
                        <Col sm={3}>
                            <Label for="skinColor">Skin</Label>
                        </Col>
                        <Col sm={4}>
                            <Input type="select" name="skinColor" id="skinColor" className="form-control"
                                   defaultValue={avatar.skinColor} value={avatar.skinColor}
                                   onChange={this._handleChange.bind(this)}>
                                {this.state.SKINCOLOR.map((element) => {return <option value={element}>{element}</option>})}
                            </Input>
                        </Col>
                    </FormGroup>
                </Form>
            </div>
        </div>
    }
}

export default AvatarComponent;

El constructor podrá recibir los atributos del avatar. Esto es así porque cada vez que analicemos una foto con nuestro servicio Java, el componente recibirá el resultado de ese análisis en forma JSON con los atributos del Avatar.

Cargamos las LOVs en la acción componentDidMount que, de acuerdo al ciclo de vida de React, se invoca después del método ender, pero antes de presentar al usuario el componente.

En los campos input de nuestro formulario vemos cómo montamos la lista de “options" con la lista de valores que hemos obtenido del servidor. Además, asociamos la función _handleChange a cada Input. Esta función se ejecutará cada vez que se elige una opción diferente de cada selector. En ella obtenemos el valor seleccionado por el usuario, cogemos el objeto avatar del estado, y actualizamos el valor del atributo seleccionado. Automáticamente éste se verá reflejado en el avatar, ya que actualizamos el estado del componente al invocar la función setState

Por último, tenemos la función componentWillReceiveProps, que nos permite actualizar el estado del Avatar cada vez que cambia el estado del componente padre. Esto nos permitirá actualizar el estado cada vez que analizamos una fotografía

Componente App

Ya sólo nos queda poner todo en común, y eso lo haremos en App.js, que será nuestro componente integrador de los 2 componentes anteriores que hemos creado.

Por un lado, utilizaremos el componente ImageUpload, al que pasaremos como parámetro la acción a ejecutar. Esta acción la definiremos aquí, y su función será la de enviar la foto al servidor y recibir la respuesta en forma de atributos de Avatar. Esta respuesta la guardaremos en el estado de nuestro componente App.

Por otro lado, utilizaremos el componente AvatarComponent, al que pasaremos los atributos del Avatar utilizando el estado de éste componente. Inicialmente pasaremos unos atributos por defecto. Cuando recibamos la respuesta del servidor actualizaremos el estado de App. Al actualizar el estado, automáticamente se notificará al componente AvatarComponent, que ejecutará su función componentWillReceiveProps.

import React, {Component} from 'react';
import './App.css';
import {Col, Container, Row, NavLink} from 'reactstrap';
import ImageUpload from "./imageUpload/ImageUpload";
import AvatarComponent from "./avatar/AvatarComponent";

class App extends Component {

    upstream = "http://localhost:8080/api/uploadFile"

    defaultAvatar = {
        avatarStyle: 'Circle',
        top: 'LongHairMiaWallace',
        accessories: 'Prescription02',
        hairColor: 'BrownDark',
        facialHair: 'Blank',
        clothe: 'Hoodie',
        clotheColor: 'PastelBlue',
        eye: 'Happy',
        eyebrow: 'Default',
        mouth: 'Smile',
        skinColor: 'Light'
    }

    constructor(props) {
        super(props);

        this.state = {
            avatar: this.defaultAvatar
        }
        this._handleSubmit = this._handleSubmit.bind(this);
    }

    _handleSubmit(component) {
        const formData = new FormData();
        formData.append('file', component);

        const config = {
            headers: {
                'content-type': 'multipart/form-data'
            }
        }
        fetch(this.upstream, {
                method: 'POST',
                config: config,
                body: formData
            }
        )
            .then((response) => {
                return response.json()
            })
            .then((response) => {
                let avatar = {...response};
                this.setState({avatar})
            })
    }

    render() {
        return (
            <div className="App">
                <header className="App-header">
                    <h1>Create your Avatar</h1>
                </header>
                <body>
                <Container className="page-wrap">
                    <Row>
                        <Col>
                            <p/>
                            <ImageUpload onSubmit={this._handleSubmit}/>
                        </Col>
                        <Col sm={1}/>
                        <Col>
                            <AvatarComponent avatar={this.state.avatar}/>
                        </Col>
                    </Row>
                </Container>
                </body>
                <footer className="App-footer">
                    <NavLink href="https://www.mimacom.com">Mimacom</NavLink>
                </footer>
            </div>
        )
    }
}

export default App;

Resultado

Hora de probarlo todo...

Levantamos el componente Backend

./mvnw spring-boot:run

Levantamos el componente Frontend

yarn start

Y aquí os pongo algunos resultados obtenidos...¿Se parecen?

Todavía le queda un poco de camino por delante a AWS Rekognition para que dé un buen resultado a nuestro propósito, especialmente en el reconocimiento del pelo, cosa que sí ofrece Azure.

AWS no es capaz de detectar la calvicie, el color del pelo, ni si tiene el pelo largo o no. Pero como se suele decir...tiempo al tiempo

Sobre el autor: Pedro Canet
Comments
Únete a nosotros