Saturday, December 15, 2018

Hey Vector! A quién me parezco?


He jugado con Cozmo en el pasado...Así que cuando Vector salió al mercado, sabía que tenía que hacer algo con el ;)

Así que...que es Vector?


Más que nada un Cozmo de color negro? Bueno...si y no :) Vector tiene un mejor procesador, 4 cores, un microfono, casi el doble de partes, y una mejor y colorida camara.

Como saben...Soy un verdadero fanático de los APIs de Machine Learning de SAP Leonardo...puesto que te permiten facilmente consumir servicios de Machine Learning.

Para este blog quería hacer algo que siempre me ha gustado...tomar la foto de alguien y luego comparárla con fotos de actores y/o actrices famosas y ver a quien se parece más la persona ;)

Así que, empecemos :D

Instalándo el SDK de Vector

Asegurate de que Vector está conectado a Internet utilizándo la aplicación de Vector app en IPhone o Android. Aquí hay un muy buen video de como hacer eso.

Una vez que Vector está conectado a Internet...asegurate de terminar la aplicación de Vector en tu celular.

El SDK de Vector solo estaba disponible para las personas que apoyaron a Anki en su Campaña de Kickstarter...pero desde el 11 de Noviembre, el SDK está en Alpha público! :D Lo cual significa...que finalmente podemos ensuciarnos las manos con el ;)

Si por alguna razón ya teniamos el SDK instalado...hay que eliminarlo antes de continuar…

python3 -m pip uninstall anki_vector

Luego, simplemente hay que hacer esto…

python3 -m pip install --user anki_vector

Luego, tenemos que autenticar a nuestro Vector…

python3 -m anki_vector.configure

Se te preguntará por el nombre de Vector, dirección IP y número de serie. También necesitaremos nuestras credenciales del Anki Cloud.

Para obtener esta información, simplemente pon a Vector en su cargador...y presiona dos veces en un ¨espalda. Esto te dará su nombre, luego sube y baja su brazo para obtener el IP. El número de serie está en la parte de abajo de Vector’s.

La fase de aprendizaje


Primero, lo primero...necesitamos varias fotos de gente famosa...y para eso utilicé la página web the The Movie DB...


Ahí me descargué 100 imagenes casi aleatorias tanto de hombre como mujeres. No entré a cada perfil sino que simplemente descargué las fotos tal como se ven.

Ahora, hay un API de SAP Leonardo que se llama “Inference Service for Face Feature Extraction” el cual basicamente toma una imagen, determina si hay o no un rostro y luego extrae sus características...como el color de los ojos, forma de la boca, cabello y demás...y esta información es retornada en un bonito aunque casi imposible de decifrar Vector de Características. Es decir...se ven solo como números...y pueden significar cualquier cosa :P

En fín...Creé una carpeta llamada “People” y le pasé las 100 imagenes. Así que, el suguiente paso es por supuesto obtener todas la características de todas las imagenes...y manualmente es obviamente no solo dificil, pero también sin sentido...es mucho mejor optimizar el proceso ;)

Un lenguaje de programación que he llegado a adorar es Crystal...Rápido como C, elegante como Ruby, sí...un mucho mejor manera de hacer Ruby :)

La instalación es muy sencilla y podemos encontrar información aquí pero yo estoy utilizándo Ubuntu en VMWare, así que aquí están sus instrucciones…

En una ventana de terminal copia y pega este código…

curl -sSL https://dist.crystal-lang.org/apt/setup.sh | sudo bash

Luego, simplemente hacemos esto…

sudo apt-get update

sudo apt install crystal

La instalación de los siguiente módulos es opcional pero recomendada…

sudo apt install libssl-dev      # for using OpenSSL
sudo apt install libxml2-dev     # for using XML
sudo apt install libyaml-dev     # for using YAML
sudo apt install libgmp-dev      # for using Big numbers
sudo apt install libreadline-dev # for using Readline

Una vez que hemos terminado...es hora de escribir nuestra aplicación…primero debemos crear una carpeta llamada “Features”.

Llamaremos a nuestro script PeopleGenerator.cr y copiaremos y pegaremos el suguiente código…


PeopleGenerator.cr
require "http"
require "json"

class FaceFeature
  JSON.mapping({
    face_feature: Array(Float64)
  })
end

class Predictions
  JSON.mapping({
 faces: Array(FaceFeature)
  })
end

class Person
  JSON.mapping({
 id: String,
 predictions: Array(Predictions)
  })
end

folder = Dir.new("#{__DIR__}/People")
while photo = folder.read
  if photo != "." && photo != ".." && photo != "Features"
 io = IO::Memory.new
 builder = HTTP::FormData::Builder.new(io)

 File.open("#{__DIR__}/People/" + photo) do |file|
  metadata = HTTP::FormData::FileMetadata.new(filename: photo)
  headers = HTTP::Headers{"Content-Type" => "image/jpg"}
  builder.file("files", file, metadata, headers)
 end
 builder.finish

 headers = HTTP::Headers{"Content-Type" => builder.content_type, 
                                "APIKey" => "YourAPIKey",
                                "Accept" => "application/json"}
 response = HTTP::Client.post("https://sandbox.api.sap.com/ml/
                                      facefeatureextraction/
                                      face-feature-extraction", body: io.to_s , 
                                      headers: headers)
 
 feature_name = "#{__DIR__}/Features/" + File.basename(photo, ".jpg") + ".txt"
 
 puts photo 
 
 File.write(feature_name, Person.from_json(response.body).predictions[0].
                   faces[0].face_feature)
 sleep  2.second
  end
end

command = "zip -r -j features.zip #{__DIR__}/Features"
Process.run("sh", {"-c", command})

puts "Done."

Expliquemos el código un momento, antes de ver los resultados…

require "http"
require "json"

Necesitamos esta dos librerías para poder llamar al API de SAP Leonardo pero también para poder leer y extraer los resultados…

class FaceFeature
  JSON.mapping({
    face_feature: Array(Float64)
  })
end

class Predictions
  JSON.mapping({
faces: Array(FaceFeature)
  })
end

class Person
  JSON.mapping({
id: String,
predictions: Array(Predictions)
  })
end

Este es el mapeo de JSON que necesitamos para extraer la información que regresa del API..

folder = Dir.new("#{__DIR__}/People")
while photo = folder.read
  if photo != "." && photo != ".." && photo != "Features"
io = IO::Memory.new
builder = HTTP::FormData::Builder.new(io)

File.open("#{__DIR__}/People/" + photo) do |file|
metadata = HTTP::FormData::FileMetadata.new(filename: photo)
headers = HTTP::Headers{"Content-Type" => "image/jpg"}
builder.file("files", file, metadata, headers)
end
builder.finish

headers = HTTP::Headers{"Content-Type" => builder.content_type, "APIKey" => "YourAPIKey,"Accept" => "application/json"}
response = HTTP::Client.post("https://sandbox.api.sap.com/ml/facefeatureextraction/face-feature-extraction", body: io.to_s , headers: headers)

feature_name = "#{__DIR__}/Features/" + File.basename(photo, ".jpg") + ".txt"

puts photo

File.write(feature_name, Person.from_json(response.body).predictions[0].faces[0].face_feature)
sleep  2.second
  end
end

command = "zip -r -j features.zip #{__DIR__}"
Process.run("sh", {"-c", command})

puts "Done."

Esta sección es más larga, primero especificamos la carpeta de la cuál serán leidas las imagenes. Luego por cada imagen determinaremos si es una imagen o una estructura de archivo...por supuesto nosotros solo queremos imagenes…

Luego, creamos un FormData builder para poder evitar tener que codificar las imagenes en base64...ponerlas en un JSON payload...etc...esta manera es mucho más sencilla y nativa…

Abrimos cada imagen y alimentamos el metadada y cabeceras del FormData.

También, necesitamos para las “cabeceras” extras requeridas por SAP Leonardo.

Una vez que esto está listo, podemos simplemente llamar al REST API, y luego crear un “Nombre de Característica” el cual va a ser el nombre del archivo generado...basicamente la imagen con una extensión “.txt”.

Para cada archivo vamos a extraer el vector de características de la respuesta del JSON, escribir el archivo y darle 2 segundos de descanzo entre llamadas para no sobregargar a llamada del API…

Una vez que está todo listo, simplemente llamamos un comando “zip” del terminal y los zipeamos…


Ahora, el archivo zip contiene 100 archivos...cada uno con las características de cada una de las fotos que tenemos almacenadas en la carpeta “People”.

Simply as that...we have trained our application ;)

La fase de pruebas y execución


Yo sé que normalmente primero haces pruebas para validar tu modelo...pero por esta vez...podemos hacer las dos cosas al mismo tiempo ;)

Vamos a crear un script en Python que va a hacer que se nos tome una foto...llamaremos al API de características en esa foto y luego llamaremos a otro API para determinar a quien nos parecemos…

Creemos un script llamado GuessWho.py


GuessWho.py
import anki_vector
import threading
import requests
import os
import json
import time
import subprocess
import re
import math
from PIL import Image
from anki_vector.events import Events
from anki_vector.util import degrees

event_done = False
said_text = False
new_width  = 184
new_height = 96

def main():
    args = anki_vector.util.parse_command_args()
    with anki_vector.Robot(args.serial, enable_face_detection=True, 
                           enable_camera_feed=True) as robot:
        evt = threading.Event()

        def on_robot_observed_face(event_type, event):

            global said_text
            if not said_text
                said_text = True
                robot.say_text("Taking Picture!")
                image = robot.camera.latest_image
                image.save("Temp.png")
                robot.say_text("Picture Taken!")
                evt.set()

        robot.behavior.set_head_angle(degrees(45.0))
        robot.behavior.set_lift_height(0.0)

        robot.events.subscribe(on_robot_observed_face, Events.robot_observed_face)

        try:
            if not evt.wait(timeout=10):
                print("---------------------------------")
        except KeyboardInterrupt:
            pass

def guess_who():
    args = anki_vector.util.parse_command_args()
    with anki_vector.Robot(args.serial) as robot: 
        url = "https://sandbox.api.sap.com/ml/facefeatureextraction/
               face-feature-extraction"
                
        img_path = "Temp.png"
        files = {'files': open (img_path, 'rb')}

        headers = {
            'APIKey': "YourAPIKey",
            'Accept': "application/json",
        }
    
        response = requests.post(url, files=files, headers=headers)
  
        robot.say_text("I'm processing your picture!")
    
        json_response = json.loads(response.text)
        json_text = json_response['predictions'][0]['faces'][0]['face_feature']
    
        f = open("myfile.txt", "w")
        f.write(str(json_text))
        f.close()
    
        time.sleep(1)
    
        p = subprocess.Popen('zip -u features.zip myfile.txt', shell=True)
    
        time.sleep(1)
    
        url = "https://sandbox.api.sap.com/ml/similarityscoring/similarity-scoring"
    
        files = {'files': ("features.zip", open ("features.zip", 'rb'), 
                 'application/zip')}
        params = {'options': '{"numSimilarVectors":100}'}
    
        response = requests.post(url, data=params, files=files, headers=headers)
        json_response = json.loads(response.text)

        robot.say_text("I'm comparing your picture with one hundred other pictures!")

        for x in range(len(json_response['predictions'])):
            if json_response['predictions'][x]['id'] == "myfile.txt":
                name, _ = os.path.splitext(json_response['predictions'][x]
                          ['similarVectors'][0]['id']) 
                name = re.findall('[A-Z][^A-Z]*', name)
                full_name = " ".join(name)
                pic_name = "People/" + "".join(name) + ".jpg"
                avg = json_response['predictions'][x]['similarVectors'][0]['score']
                robot.say_text("You look like " + full_name + 
                               " with a confidence of " + 
                                str(math.floor(avg * 100)) + " percent")
                image_file = Image.open(pic_name)
                image_file = image_file.resize((new_width, new_height), 
                                                Image.ANTIALIAS)  
                screen_data = anki_vector.screen.convert_image_to_screen_data(
                                                                   image_file)
                robot.behavior.set_head_angle(degrees(45.0))
                robot.conn.release_control()
                time.sleep(1)
                robot.conn.request_control()                
                robot.screen.set_screen_with_image_data(screen_data, 0.0)
                robot.screen.set_screen_with_image_data(screen_data, 25.0)
                
                print(full_name)
                print(str(math.floor(avg * 100)) + " percent")

                time.sleep(5)

if __name__ == '__main__':
    main()
    guess_who()

Este script es mucho más largo...así que tratemos se asegurárnos de que entendemos todo…

import anki_vector
import threading
import requests
import os
import json
import time
import subprocess
import re
import math
from PIL import Image
from anki_vector.events import Events
from anki_vector.util import degrees

Son muchas librerías :) La primera es bastante obvia...es como nos conectamos a Vector ;)

La segunda es para manejar “threads” (hilos) puesto que necesitamos hacer alguna cosas de forma asíncrona.

La tercera es para manejar la llamadas a los APIs.

La cuarta es para manejar el acceso a carpetas.

La quinta es para manejar las respuestas JSON que regresar de las llamadas a los APIs.

La sexta es para que podamos retrazar la ejecución de nuestra aplicación.

La séptima es para poder llamar comandos del terminal.

La octava es para utilizar Expresiones Regulares.

La novena es para majenar funciones matemáticas.

La décima es para manejar manipulación de imágnes.

La onceava es para manejar eventos puesto que queremos que Vector trate de detectar nuestro rostro.

La última la utilizamos para poder mover la cabeza de Vector.

def main():
    args = anki_vector.util.parse_command_args()
    with anki_vector.Robot(args.serial, enable_face_detection=True, enable_camera_feed=True) as robot:
        evt = threading.Event()

        def on_robot_observed_face(event_type, event):

            global said_text
            if not said_text:
                said_text = True
                robot.say_text("Taking Picture!")
                image = robot.camera.latest_image
                image.save("Temp.png")
                robot.say_text("Picture Taken!")
                evt.set()

        robot.behavior.set_head_angle(degrees(45.0))
        robot.behavior.set_lift_height(0.0)

        robot.events.subscribe(on_robot_observed_face, Events.robot_observed_face)

        try:
            if not evt.wait(timeout=5):
                print("---------------------------------")
        except KeyboardInterrupt:
            pass

Esté es de hecho...nuestro evento principal :) Aquí vamos a abrir una conexión con Vector, y puesto que podemos tener múltiples Vectors...necesitamos el número de serie para especificar cual de todos queremos utilizar...tambien debemos activar la detección de rostros y la camara.

Vamos a iniciar un hilo puesto que debemos iniciar el evento donde Vector intenta detectar nuestro rostro. Si nos puede ver, entonces nos dirá “Taking Picture!”...tomará la imagen, la grabará y luego dirá “Picture Taken!”. Después de esto, nuestro evento está terminado...pero...pero mientras esto sucede podemos subir y bajar su cabeza, así como bajar su brazos para que Vector pueda vernos mejor.

Como podemos ver estamos subscritos a dos eventos, uno que observa nuestro rostro y otro cuando nuestro rostro ha sido identificado…

def guess_who():
    args = anki_vector.util.parse_command_args()
    with anki_vector.Robot(args.serial) as robot:
        url = "https://sandbox.api.sap.com/ml/facefeatureextraction/face-feature-extraction"
                
        img_path = "Temp.png"
        files = {'files': open (img_path, 'rb')}

        headers = {
            'APIKey': "YourAPIKey",
            'Accept': "application/json",
        }

        response = requests.post(url, files=files, headers=headers)

        robot.say_text("I'm processing your picture!")

        json_response = json.loads(response.text)
        json_text = json_response['predictions'][0]['faces'][0]['face_feature']

        f = open("myfile.txt", "w")
        f.write(str(json_text))
        f.close()

        time.sleep(1)

        p = subprocess.Popen('zip -u features.zip myfile.txt', shell=True)

        time.sleep(1)

        url = "https://sandbox.api.sap.com/ml/similarityscoring/similarity-scoring"

        files = {'files': ("features.zip", open ("features.zip", 'rb'), 'application/zip')}
        params = {'options': '{"numSimilarVectors":100}'}

        response = requests.post(url, data=params, files=files, headers=headers)
        json_response = json.loads(response.text)

        robot.say_text("I'm comparing your picture with one hundred other pictures!")

        for x in range(len(json_response['predictions'])):
            if json_response['predictions'][x]['id'] == "myfile.txt":
                name, _ = os.path.splitext(json_response['predictions'][x]['similarVectors'][0]['id']) 
                name = re.findall('[A-Z][^A-Z]*', name)
                full_name = " ".join(name)
                pic_name = "People/" + "".join(name) + ".jpg"
                avg = json_response['predictions'][x]['similarVectors'][0]['score']
                robot.say_text("You look like " + full_name + " with a confidence of " + str(math.floor(avg * 100)) + " percent")
                image_file = Image.open(pic_name)
                image_file = image_file.resize((new_width, new_height), Image.ANTIALIAS)  
                screen_data = anki_vector.screen.convert_image_to_screen_data(image_file)
                robot.behavior.set_head_angle(degrees(45.0))
                robot.conn.release_control()
                time.sleep(1)
                robot.conn.request_control()                
                robot.screen.set_screen_with_image_data(screen_data, 0.0)
                robot.screen.set_screen_with_image_data(screen_data, 25.0)
                
                print(full_name)
                print(str(math.floor(avg * 100)) + " percent")

                time.sleep(5)

Este método maneja las partes complicadas de la aplicación…

Nos conectamos a Vector nuevamente...aunque esta vez no debemos activar nada puesto que la imagen ya ha sido tomada.

Pasamos el URL para el API de características.

Luego abrimos nuestro archivo “Temp.png”, el cual es la foto que Vector nos tomó previamente.

Debemos pasar la cabecera extra al API de SAP Leonardo.

Llamamos al API y obtenemos el JSON de respuesta.

Nuevamente, debemos extraer la información de las características de la respuesta JSON. Esta vez sin embargo, vamos a crear un archivo llamado “myfile.txt”. Vamos a hacer que la aplicación duerma por un segundo y luego llamar a un proceso en el terminal para poder agregar “myfile.txt” a nuestro archivo Features.zip…

Luego, lo hacemos dormir por un segundo más...y esto es solo para nos sobrecargar de llamadas al API…

Aquí, vamos a llamar a un API diferente, el cual se llama Inference Service for Similarity Scoring

Esta API leerá los 101 archivos de características y determinará el coseno de la distancia (-1 a 1) para archivo comparado el uno con el otro. De esta manera, puede determinar que archivos están mas cercanos los unos de lo otros y por lo tanto a quien nos parecemos más...brindándonos además un porcentaje de confidencia.

Esta llamada es un poco más complicada que las anteriores puesto que debemos cargar el archivo zip…

        files = {'files': ("features.zip", open ("features.zip", 'rb'), 'application/zip')}
        params = {'options': '{"numSimilarVectors":100}'}

        response = requests.post(url, data=params, files=files, headers=headers)
        json_response = json.loads(response.text)

Tengamos en cuenta de que aunque tenemos 101 archivos...debemos comparar 1 contra los 100 restántes...así que pasamos 100 al parámetro “numSimilarVectors”.

Una vez que hemos hecho esto, debemos leer cada sección de la respuesta JSON hasta que encontremos un ïd"con el valor “myfile.txt”. Una vez que tenemos eso, uilizamos Expresiones Regulares para extraer solo el nombre sin la extensiòn. Además, necesitamos tener el nombre de la imagen...así que al fín y al cabo, debemos tener algo como esto…

full_name = “Nicolas Cage”
pic_name = “People/NicolasCage.jpg”

También debemos extraer el porcentaje de confianza…

avg = json_response['predictions'][x]['similarVectors'][0]['score'] 

Así que, podemos tener a Vector diciéndo “You look like Nicolas Cage with a confidence of 75 percent”.

Ahora...aquí viene la parte divertida ;) Ya sabemos a quien nos parecemos...pero digamos...que no nos acordamos realmente como es la cara de Nicolas Cage...así que tomamos ventaja de la elegante pantalla de Vector para mostrar una imagen ahí ;) Por cierto...debemos perder el control, tomarlo nuevamente y mostrar la imagen por cero segundos y luego volver a mostrarla...esto es basicamente por que si no lo hacemos, los ojos de Vector tapan toda la pantalla...y esta es una forma de corregir ese comportamiento;)

           image_file = Image.open(pic_name)
                image_file = image_file.resize((new_width, new_height), Image.ANTIALIAS)  
                screen_data = anki_vector.screen.convert_image_to_screen_data(image_file)
                robot.behavior.set_head_angle(degrees(45.0))
                robot.conn.release_control()
                time.sleep(1)
                robot.conn.request_control()                
                robot.screen.set_screen_with_image_data(screen_data, 0.0)
                robot.screen.set_screen_with_image_data(screen_data, 25.0)

Primero abrimos la imagen, luego le cambiamos el tamaño para que entre en la pantalla, luego los convertimos a formato de Vector y finalmente podemos mostrárla por pantalla, especificándo por cuanto tiempo queremos que permanence ahí…

                print(full_name)
                print(str(math.floor(avg * 100)) + " percent")

                time.sleep(5)

Mostramos un poco de información en la pantalla y dejamos que duerma por 5 segundos para que la imagen no se desaparezca demasiado rápido ;)

Finalmente! La parte más importante de todo el script...llamar a las funciones :P

if __name__ == '__main__':
    main()
    guess_who()

Y basicamente eso es todo :) Abrimos un ventana de Terminar y escribimos…

python3 GuessWho.py

Vector va a tratar de mirárnos y detectar nuesto rostro...tomará una foto...las APIs de SAP Leonardo serán llamadas...escucharemos y veremos a quien nos parecemos ;)

Espero que disfruten de este blog...Yo obviamente lo hice :D

Y solo para cerrar el tema de forma elegánte...aquí les dejo un pequeño video…


Porsiacaso...aquí está la foto que Vector me tomó...


Saludos,

Blag.
SAP Labs Network.

No comments: