jueves, 17 de mayo de 2012

Software para el reconocimiento de gestos (Parte II)

Con esta entrada terminamos la explicación del software que hemos desarrollado para el sistema de reconocimiento de gestos mediante el acelerómetro de un dispositivo Android:

Interfaz Gesture.java

En esta interfaz encontramos 3 métodos abstractos:
  • public int getDistance(Gesture g): nos indicará si un gesto es válido o no.
  • public double getDistance(Data[] d): calcula la distancia entre un array en el que se almacenan coordenadas de aceleración y un gesto patrón.
  • public int getId(): devuelve el número que identifica a un gesto patrón.
Esta interfaz es implementada por la clase AccelerometerGesture.


Clase AccelerometerGesture.java

Un objeto de esta clase, que implementa la interfaz Gesture, representará un gesto patrón que ya ha sido enseñado en la fase de entrenamiento.

Guardamos en memoria cada gesto patrón caracterizándolo por dos elementos, que son las 2 variables de las que consta esta clase:
  • private AccelerometerData[][] data: Es un array bidimensional donde tenemos almacenados todos los valores medidos de coordenadas de aceleración en las 3 iteraciones de un gesto patrón, con la estructura que ya hemos explicado al hablar de la clase GestureFactory.
  • private int id: Es el número que identifica a ese gesto en concreto.
Como ya hemos visto, al terminar la fase de entrenamiento, el método stopLearning() de la clase GestureFactory nos devolverá un objeto de esta clase AccelerometerData. Por lo tanto, se crearán tantos objetos AccelerometerData como gestos se hayan enseñado.

En esta clase encontramos un método fundamental para llevar a cabo la fase de comparación de nuevos gestos con los patrones almacenados: public double getDistance(Data[] d). A este método se le pasa como parámetro un array “d” de coordenadas de aceleración que se han obtenido en la fase de realización de gestos y devuelve un double que representa la distancia entre el gesto patrón y el nuevo gesto realizado. Esto lo hace a través de los siguientes pasos:
  1. En el array de coordenadas “d” del gesto que se ha realizado, se busca el último elemento no nulo, pues a partir de la primera tripla de coordenadas nulas el gesto ya habrá terminado.
  2. Se crea un nuevo array “nd” de objetos Data, que contendrá únicamente estas coordenadas no nulas.
  3. Se realiza esta misma operación para los array de coordenadas de cada iteración del gesto patrón, creando nuevos arrays “nd2” que sólo contengan coordenadas no nulas.
  4. Mediante el algoritmo de alineamiento temporal dinámico (DTW) se calcula la distancia entre los arrays “nd” y “nd2”, haciendo la comparación con cada una de las 3 iteraciones almacenadas del gesto patrón. Con esto, obtendremos 3 distancias distintas.
  5. De las 3 distancias obtenidas, al final nos quedará almacenada en la variable “xMinDist” la menor de ellas, que será la que este método devolverá.

Interfaz Device.java

Los métodos de esta interfaz son:
  • public void addEventListener(EventListener e)
  • public void enableDevice()
  • public void disableDevice()
Entenderemos cuál es la función de cada uno de estos métodos abstractos cuando hablemos de la clase AndroidDevice, que implementa esta interfaz.

Esta interfaz y la clase que la implementa serán usadas en la fase de realización de gestos.


Clase AndroidDevice.java

El principal objetivo de esta clase, que implementa la interfaz Device, es eliminar el efecto de la gravedad en la lectura del acelerómetro en la fase de realización de gestos.

Hay que tener en cuenta la importancia de que las coordenadas de aceleración que almacenemos tengan ya aislado el efecto de la gravedad para que sean coherentes con las coordenadas de los gestos en la fase de entrenamiento. Si aislamos este efecto en una fase y no en la otra, no podremos comparar los patrones.

Las variables con las que trabajaremos en esta clase son:
  • Objetos de las clases Sensor, SensorEventListener y SensorManager, proporcionadas por las librerías de Android.
  • Un objeto de la propia clase AndroidDevice.
  • Un EventListener.
Además del método que se encarga de aislar la contribución de la gravedad en nuestras medidas (funciona exactamente igual que el ya explicado para la fase de entrenamiento), esta clase implementa los 3 métodos abstractos de la interfaz Device:
  • public void addEventListener(EventListener e): permite asociarle a este objeto AndroidDevice un listener de eventos.
  • public void enableDevice(): a través del método public boolean registerListener (SensorEventListener listener, Sensor sensor, int rate) que nos proporciona Android en su clase SensorManager, asocia un SensorEventListener al acelerómetro y se especifica que la obtención de las lecturas del sensor sea lo más rápida posible (SENSOR_DELAY_FASTEST).
  • public void disableDevice(): de manera análoga, mediante el método public void unregisterListener (SensorEventListener listener) de SensorManager, elimina la asociación entre el SensorEventListener y el acelerómetro.

Clase GestureManager.java

En esta clase encontramos toda la inteligencia que interviene tanto en la fase de realización de gestos como en la de comparación del gesto realizado con los patrones.

Aunque el algoritmo que utilizamos tiene cierta complejidad, podemos resumirlo en el siguiente diagrama de estados (hacer click sobre la imagen para verla en grande):



Antes de pasar a explicar cada uno de los estados, es importante entender el significado de 4 constantes que utilizamos en el algoritmo. El valor que adquiere cada una de ellas ha sido determinado de forma empírica.
  • UMBRAL1=0,8: nivel de “energía” por debajo del cual consideramos que estamos en reposo. La “energía” de la aceleración la calculamos como el módulo de la aceleración sufrida:


    Aunque continuamente hablemos de este módulo como una energía, sabemos que en realidad no lo es, pero nos ayuda a entender lo que está ocurriendo de forma intuitiva.
  • UMBRAL2=1,2: nivel de energía por encima del cual consideramos que se está realizando un gesto. Valores intermedios entre los umbrales 1 y 2 se utilizan para detectar que es posible que se esté iniciando un movimiento.
  • TIME1=15: tiempo mínimo durante el cual el nivel de energía tiene que superar el umbral 2 para considerar que realmente se está realizando un gesto.
  • TIME2=15: tiempo mínimo durante el cual el nivel de energía tiene que estar por debajo del umbral 2 para establecer que se ha dejado de realizar un gesto.
Conociendo ya estos 4 umbrales, podremos entender mejor lo que ocurre en cada estado:
  1. REPOSO
    En este estado no se está realizando ningún gesto ni estamos detectando movimiento. Las variables booleanas “makingGesture” y “someDetected” toman el valor “false”.
    Saldremos de este estado cuando se detecte una aceleración (almacenada en la variable “tot”) que supere el nivel de energía del UMBRAL1. Pasaremos al estado “algo detectado”, cambiando a “true” el valor de “someDetected”.

  2. ALGO DETECTADO
    Entramos en este estado cuando se detecta cierto movimiento, aunque todavía no consideramos que se está haciendo un gesto, pues puede ser una falsa alarma. Al entrar en él tenemos “makingGesture=false” y “someDetected=true”.
    En este estado empezamos a guardar las coordenadas de aceleración medidas por el acelerómetro, aunque luego sólo las conservaremos si verdaderamente se estaba realizando un gesto.
    En el momento en el que el nivel de energía supera el UMBRAL2, empezamos a incrementar el contadorvaluesDetected”. Si se detecta movimiento con energía por encima del “UMBRAL2” durante un tiempo superior a “TIME1” (“valuesDetected>TIME1”), consideramos que ya estamos haciendo gesto y ponemos a “true” la variable “makingGesture”. Con esto pasamos al estado “haciendo gesto”.
    Si se supera “UMBRAL1” pero no llega a superarse “UMBRAL2”, era una falsa alarma y en realidad no se estaba haciendo ningún gesto, por lo que volvemos al estado de reposo inicial. Se eliminan todos los datos de coordenadas que se habían empezado a almacenar y se pone de nuevo “someDetected” a “false”.

  3. HACIENDO GESTO:
    Mientras “makingGesture” tome el valor “true”, será porque la energía está por encima del “UMBRAL2” y se almacenarán todas las coordenadas de aceleración medidas por el acelerómetro (siempre que tengamos menos de 512 triplas de coordenadas, que es el tamaño máximo que hemos establecido para los arrays de Data).
    En el momento en el que detectemos que la energía ya está por debajo del “UMBRAL2”, empezaremos a incrementar el contadorvaluesDetected”. Si el valor de este contador supera el umbral de tiempo “TIME2”, consideraremos que ya se ha parado de hacer un gesto. Es en ese momento cuando comienza la fase de comparación con los patrones.

  4. COMPARANDO GESTOS:
    Al comenzar la comparación de gestos, suponemos que el gesto que se está realizando no coincide con ninguno de los patrones almacenados y lo identificamos con “gestureid=-1”.
    Se hace una llamada al método public double getDistance(Data[] d) de cada uno de los gestos patrón, para buscar aquel que se parezca más al que acabamos de realizar. Le asignaremos a este gesto el id de aquel gesto patrón de distancia mínima a él.
    Una vez que ya tenemos identificado el gesto, llamamos al método onGestureDetected() de la interfaz GestureListener, pasándole como parámetro el entero que lo identifica. A partir de este momento ya es la aplicación Android quien se encarga de enviar al robot el comando que se corresponde con este movimiento.
    Al terminar la comparación, se vuelven a poner todas las variables booleanas de estado a “false”, el contador “valuesDetected” a 0, se borran los valores de todas las coordenadas de aceleración del gesto que se acaba de realizar y se vuelve al estado inicial de reposo.

Clase DTW.java

De todas las clases que forman el software de reconocimiento de gestos, esta es la única que no ha sido desarrollada por nosotros, sino que hemos cogido el código de otro autor y la hemos adaptado.

En esta clase encontramos el algoritmo de alineamiento temporal dinámico o DTW (Dynamic Time Warping), que nos permite medir la similitud entre dos secuencias que pueden variar en el tiempo o en el espacio. Gracias a él podremos calcular la distancia entre un gesto que realicemos y cada uno de los gestos patrón.

Más adelante dedicaremos una entrada del blog a explicar la finalidad, los fundamentos matemáticos y  los detalles de implementación de este algoritmo.

Software para el reconocimiento de gestos (parte I)

Para profundizar en el funcionamiento de nuestro sistema de reconocimiento de gestos mediante el acelerómetro de un dispositivo Android, en esta entrada presentaremos de forma detallada cada una de las clases e interfaces que hemos desarrollado.

La estructura de este software tiene cierta complejidad y podemos verla resumida en el siguiente diagrama. En él, los cuadros naranjas representan interfaces y, los azules, clases; las flechas con línea continua representan una relación de extensión (AccelerometerData extiende a Data) y las de línea discontinua implementación (AccelerometerGesture implementa a la interfaz Gesture y AndroidDevice a Device).


Aunque al ver la estructura de las interfaces y clases pueda parecer que algunas de las interfaces no serían necesarias en nuestro proyecto, lo hemos diseñado de esta forma pensando en una futura extensión de este sistema de reconocimiento de gestos a más dispositivos.

De todas ellas, la única que no ha sido desarrollada por nosotros es la clase DTW.java, cuyo autor es Cheol-Woo Jung; nosotros simplemente la hemos adaptado a nuestro proyecto.

A continuación iremos describiéndolas una a una, aunque por la complejidad del software, tendremos que dividir su explicación en 2 entradas distintas:


Clase Data.java

Únicamente tiene como variable un array de floats llamado “values”, con su correspondiente getter: el método getValues(). A su constructor tendremos que pasarle como parámetro un array de floats.

Un objeto de la clase Data.java contendrá coordenadas de la aceleración medidas con el acelerómetro. Por lo tanto, normalmente la variable “values” será un array formado por 3 floats:
  • values[0]: coordenada X de la aceleración
  • values[1]: coordenada Y de la aceleración
  • values[2]: coordenada Z de la aceleración

Clase AccelerometerData.java

Como esta clase extiende a Data.java, a su constructor tendremos que pasarle como parámetro un array de floats para poder hacer la llamada correspondiente al constructor de la clase padre.

La función de esta subclase es acceder por separado a cada uno de los elementos que forman el array “values” de un objeto Data, para poder trabajar con cada coordenada de la aceleración de manera independiente.

Para ello, consta de los 3 métodos getXAcceleration(),getYAcceleration() y getZAcceleration(), que acceden respectivamente a la primera, segunda y tercera posición del array “values”.  


Interfaz EventListener.java

En esta interfaz sólo encontramos un método abstracto: public void onEventReceived(float[] values, Device d), que posteriormente utilizarán otras clases en la fase de realización de gestos para determinar qué hacer al detectar un evento del acelerómetro.


Interfaz GestureListener.java

Como ocurría con la interfaz anterior, ésta consta de un único método abstracto: public void onGestureDetected(int idGesture). 

Este método se usará en la fase de comparación con los patrones y especificará qué hacer en caso de detectar una coincidencia entre un gesto realizado y un patrón guardado.


Clase FactorySensorListener.java

Es una clase que implementa la interfaz SensorEventListener (proporcionada por las liberías Android) y que se encarga de crear una estructura de datos en la que se almacena un gesto realizado en la fase de entrenamiento.

La estructura de datos elegida para almacenar los gestos es un array de 512 objetos de la clase AccelerometerData; a su vez, cada uno de ellos, contiene un array de 3 floats (las coordenadas x, y, z de la aceleración). En la siguiente figura se ve de manera sencilla cómo es la estructura:


Esta clase comienza a trabajar cuando nos encontramos en la fase de entrenamiento, indicamos que queremos enseñar un nuevo gesto y el acelerómetro del móvil detecta un evento.

Antes de explicar el funcionamiento interno de la clase, es importante señalar que en todo momento trabajaremos con aceleraciones lineales, aislando el efecto producido por la gravedad. Como ya explicamos en la entrada “El acelerómetro de un dispositivo Android”, este sensor nos proporciona medidas de aceleración total, teniendo en cuenta la gravedad.

Si queremos trabajar únicamente con la aceleración lineal que producen nuestros movimientos, tendremos que implementar un filtro paso bajo que aísle la fuerza de la gravedad y, posteriormente, restar estas aceleraciones filtradas a las medidas por el acelerómetro.

Las librerías de Android nos indican cómo hay que implementar este filtro:



En él, la constante “alpha” que se utiliza se calcula como:


En esta expresión, tau es la constante de tiempo del filtro paso bajo y dT la velocidad a la que los eventos son detectados. Las librerías Android nos aconsejan ponerle un valor de 0,8 a la constante “alpha”; como hemos comprobado que con este valor funcionaba, es el que estamos utilizando.

Conociendo ya este detalle importante, pasaremos a explicar el algoritmo que hay detrás de esta clase:

  1. Antes de empezar a almacenar información, comprueba que el evento detectado por el acelerómetro del móvil ha sido causado realmente por un movimiento hecho a propósito. Para ello, primero calcula el módulo de la aceleración lineal producida (restando el efecto de la gravedad):


    Se compara este valor con un umbral de 0,8 (establecido así de forma empírica) y, si no lo supera, consideraremos que seguimos en estado de reposo.

  2.  Cuando se supera el umbral de movimiento, pasamos al estado de almacenamiento de gestos. En él, se van tomando lecturas del acelerómetro, se les resta el efecto de la gravedad y se almacenan en la estructura que hemos creado. Todo esto se realiza mientras tengamos menos de 512 conjuntos de coordenadas (mientras la variable “index” sea menor que 512).

  3. Cuando la variable “index” toma como valor 512, ya tenemos información suficiente sobre el gesto realizado y se termina el almacenamiento.

  4. Sólo volveremos al estado de reposo del punto 1 si indicamos a través de la aplicación Android que vamos a realizar de nuevo un entrenamiento del mismo gesto o queremos enseñar otro gesto distinto.


Clase GestureFactory.java




Esta clase implementa toda la inteligencia en la que se fundamenta la fase de entrenamiento, ayudándose de la clase FactorySensorListener. Aquí podemos ver el diagrama de estados en el que se ve el funcionamiento conjunto de ambas clases (hacer click sobre la imagen para ver en grande):



Para comprender el diagrama, es necesario conocer los 4 métodos más importantes de esta clase:
  • public void startLearning(int type, int iterations, int idGesture):
    Llamamos a este método cuando queremos empezar el entrenamiento de un gesto determinado. Como parámetro se le pasa un entero asociado al tipo de sensor que vamos a utilizar (en nuestro caso, siempre tomará el valor 0, que identifica al acelerómetro), el número de iteraciones que hay que realizar de un mismo gesto para completar el entrenamiento (nosotros pediremos siempre 3 iteraciones) y un identificador del gesto que se va a enseñar (si será el de avanzar, retroceder, parar…).
    La principal misión de este método es crear la estructura de datos donde se almacenará la información sobre cada gesto: no será más que un nuevo array formado por las 3 estructuras que crea la clase FactorySensorListener y que ya hemos explicado:

     

    Al ser llamado, este método pone la variable “learning” a “true” para indicar que hemos empezado la fase de entrenamiento.

  • public void startIteration():
    Podremos empezar una iteración de un gesto sólo si anteriormente hemos llamado al método startLearning() (se comprueba si “learning=true”) y si hemos realizado menos de 3 iteraciones del gesto que estamos enseñando. Si no se cumplen estas dos condiciones, lanzará una excepción.
    Una vez que se han satisfecho ambas condiciones, se asocia el FactorySensorListener al acelerómetro para que empiece a almacenar los 512 conjuntos de coordenadas del movimiento realizado, como ya hemos visto anteriormente.

  • public void stopIteration()
    Simplemente incrementa el contador de iteraciones realizadas del gesto actual y elimina la asociación entre el FactorySensorListener y el acelerómetro.

  • public Gesture stopLearning()
    Este método lanzará una excepción si es llamado cuando no estamos en fase de entrenamiento (si “learning=false”) o si el número de iteraciones realizadas no es 3.
    Si no se da ninguna de estas condiciones de excepción, pone la variable “learning” a “false” para indicar que salimos de la fase de entrenamiento y devuelve un objeto de la clase AccelerometerGesture. A este objeto se le pasan como parámetro el identificador del gesto que hemos entrenado y el array bidimensional donde tenemos almacenados todos los valores medidos de coordenadas de aceleración en las 3 iteraciones.

En la siguiente entrada completaremos la explicación de las clases e interfaces de las que todavía no hemos hablado.



miércoles, 16 de mayo de 2012

Control por reconocimiento de gestos


Como ya mencionamos al hablar del acelerómetro de un dispositivo Android, hemos decidido implementar como mejora para nuestro proyecto el reconocimiento de gestos mediante este sensor que incorporan todos los móviles Android.

Aunque al diseñar el proyecto y la planificación de los hitos pensamos como mejoras en la integración de Wiimote y Kinect, hemos decidido sustituir Wiimote por el reconocimiento de gestos mediante el acelerómetro del móvil, pues esta es una opción mucho más interesante y que tiene más sentido a la hora de integrarla en la aplicación Android que ya tenemos diseñada. De esta manera, en lugar de tener que manejar otro mando especial para ello, es haciendo gestos con su propio móvil en la mano con lo que el usuario controlará a Kigo.

En esta entrada explicaremos de forma cualitativa el funcionamiento de nuestro sistema de reconocimiento de gestos y todo lo que el usuario necesitaría saber para manejarlo correctamente. Dejaremos para entradas posteriores la explicación de la estructura compleja de Software que hemos desarrollado, las modificaciones introducidas en el software de la aplicación Android y la explicación del Algoritmo de Alineamiento Temporal (DTW-Dynamic Time Warping), gracias al que calculamos distancias entre dos vectores de aceleraciones.


FASES DEL RECONOCIMIENTO DE GESTOS

Podemos dividir el reconocimiento de gestos mediante el acelerómetro de un dispositivo Android en 4 fases distintas:





  1. Fase de entrenamiento  

    Para poder comparar gestos que realicemos con unos patrones, inicialmente tendremos que realizar un entrenamiento de aquellos gestos que queremos que se consideren “patrón”.

    Cada gesto que enseñemos irá asociado mediante un número de identificación a uno de los comandos que puede reconocer el robot Kigo y permanecerá en memoria hasta que se cierre la aplicación Android.

    En la aplicación Android, el entrenamiento se realiza a través del
    menú principal. En primer lugar, tenemos que seleccionar en una lista desplegable a qué comando queremos asociar el nuevo gesto cuyo entrenamiento vamos a realizar. Después, hay que accionar el botón de “Empezar Entrenamiento”.

    El entrenamiento de un gesto patrón se completa cuando hayamos realizado
    3 iteraciones del mismo. Esto nos permitirá almacenar en 3 estructuras de datos distintas las coordenadas de la aceleración sufrida por el dispositivo móvil al realizar ese movimiento. Hemos decidido realizar 3 iteraciones porque, si fuesen menos, sería menos precisa la comparación de gestos y, si fuesen más, la fase de entrenamiento se alargaría demasiado y no obtendríamos mucha más precisión.

    Para completar una iteración, accionamos el botón “
    Empezar gesto”, realizamos el movimiento que queremos y accionamos “Finalizar gesto”. Repetimos este proceso 3 veces y, cuando hayamos terminado, accionamos “Finalizar entrenamiento”.

    Tras esto, podemos entrenar un nuevo gesto cambiando la selección en la lista desplegable, modificar el que ya hemos enseñado o pasar a la fase de realización de gestos.

  2. Fase de realización de gestos   

    Si  ya tenemos algún gesto almacenado en memoria como patrón, podemos empezar a realizar movimientos para dar órdenes al robot Kigo.
    Para ello, nos moveremos a la ventana de “
    Gestos” en la aplicación Android y accionaremos el botón “Iniciar gesto”. Esto hará que se cree un Listener de gestos que continuamente esté comprobando si realizamos algún movimiento.

    Cuando se detecte que la energía de la aceleración sufrida por el dispositivo Android supera ciertos umbrales durante un determinado tiempo, se considerará que estamos realizando un gesto y se empezarán a almacenar coordenadas de esta aceleración. Este proceso será explicado con mucho más detalle cuando hablemos del software de reconocimiento de gestos.

    Esto supone que podemos realizar gestos de una manera continua, pues los umbrales nos permiten distinguir lo que son movimientos hechos a propósito y lo que es estado de reposo.

  3. Fase de comparación con los patrones

    Una vez que se han almacenado las coordenadas del gesto que acabamos de realizar y se detecta que el movimiento ha terminado, se calcula la distancia de este gesto con todos los que tenemos como patrones mediante el algoritmo de alineamiento temporal dinámico (DTW) para saber a cuál se parece más.
    Se le asocia al gesto realizado el número de identificación de aquel patrón respecto al cual su distancia es mínima.

  4. Fase de envío de comandos

    Cuando el gesto que hemos realizado ya tiene asignado un número de identificación, sólo queda enviarle por Bluetooth al robot el comando asociado a ese identificador:



    Es la aplicación Android quien se encarga del envío de los comandos; además, nos muestra en la pantalla del móvil cuál de los comandos es el que se ha reconocido.



Cuando queramos dejar de controlar al robot mediante este sistema de reconocimiento de gestos, tendremos que accionar el botón “Detener gesto” para deshabilitar el Listener de gestos. De lo contrario, el acelerómetro seguirá constantemente detectando los movimientos del dispositivo y estaremos consumiendo recursos del móvil.

Es importante señalar que, con el sistema que hemos implementado, mientras tengamos habilitado el reconocimiento de gestos podemos realizar un gesto detrás de otro y se le irán enviando los comandos al robot de forma secuencial.


domingo, 13 de mayo de 2012

Nuevo algoritmo de clasificación de residuos


Conociendo ya como funciona el algoritmo de detección, vamos a explicar ahora el de clasificación de residuos, definido en el Behavior ProcessData().

Hay que tener en cuenta que, para poder llevar a cabo la clasificación, necesitamos partir de un ArrayList de objetos de la clase Container que se han creado durante la fase de detección. En él tendremos almacenados la posición de los 4 contenedores (tanto la distancia como el ángulo) y su color.


Behavior ProcessData()

En la entrada titulada “Software básico del robot (parte III)” mencionamos que este comportamiento todavía no se estaba usando, pero lo habíamos creado pensando que en el futuro lo necesitaríamos. El código de comando que lleva asociado es el “c2 c1 c0 = 100”.

El funcionamiento de este algoritmo es mucho más sencillo que el de detección de contenedores:

  1. El sensor de color que apunta a las pelotas comprueba de qué color es la que tiene preparada para disparar.
  2. Accedemos al ArrayList de contenedores y buscamos en qué posición respecto al polo Norte magnético se encuentra el contenedor del mismo color que esta pelota.
  3. El robot gira hasta que la brújula indica que ha llegado a la posición del contenedor.
  4. Se dispara la pelota.
  5. Comprueba con su sensor de color si todavía le queda en el cargador alguna pelota roja, amarilla, azul o verde. En caso de que queden, la variable “quedanBolas” será “true” y volveremos al punto 1. Si ya se han clasificado todos los residuos (“quedanBolas=false”), se termina el proceso de clasificación y el robot se para.
En la aplicación Android, este algoritmo se inicia accionando el botón de “Procesar” en la página de “Comandos”.


Con la implementación de estos dos algoritmos, damos por terminada nuestra práctica básica. Ahora estamos trabajando en mejoras para controlar el robot mediante reconocimiento de gestos.

Nuevo algoritmo de detección de contenedores


Como ya se pudo ver en el vídeo que publicamos hace unos días, tras la implementación de la brújula hemos mejorado los algoritmos que permitían a nuestro robot detectar contenedores y clasificar los residuos de una manera autónoma.

En esta entrada explicaremos con más detalle el algoritmo de detección, que forma parte del Behavior Mapping(); en la siguiente, nos centraremos en el de clasificación de residuos, definido en el Behavior ProcessData().


Behavior Mapping()

Como ya vimos en la entrada titulada “Software básico del robot (parte III)”, anteriormente este comportamiento hacía que el robot diese unas vueltas de reconocimiento y, cuando detectaba un contenedor, medíamos a través del tacómetro de los motores en qué posición se encontraba.

Con el nuevo algoritmo, medimos ángulos mediante la brújula I2C e introducimos algunas comprobaciones para evitar que se realicen falsas detecciones de contenedores.

En el siguiente diagrama de estados (hacer click sobre la imagen para ver en grande) se puede obtener una visión global del funcionamiento del algoritmo, que pasaremos a describir con más detalle estado a estado a continuación:



  • Establecimiento de posición inicial.
    Inicialmente, el robot considera que delante tiene un objeto (la variable booleana “libre” toma el valor “false”) y gira hasta que el sensor de ultrasonidos no detecta nada (hasta tener “libre=true”). Las medidas del sensor de ultrasonidos se realizan de 5 en 5: si al menos 3 de ellas devuelven un valor de no detección, se considerará que no hay ningún contenedor delante y se cambiará el valor de “libre” a “true”. Los valores de no detección son -1 (quiere decir que ningún objeto es detectado por el sensor de ultrasonidos) y todos aquellos superiores a 90 (el objeto detectado se encuentran a más de 90 cm de nuestro robot).
    Hemos implementado esta fase inicial para evitar errores: si el robot tuviese delante un contenedor nada más empezar el mapeo, no habría podido detectar en qué ángulo comienza a haber contenedor y tampoco podría calcular a qué ángulo se encuentra el punto medio del contenedor.
    Cuando ya nos aseguramos de que no hay nada delante del robot, tomamos una primera lectura de la brújula para saber cuál es su posición inicial y la almacenamos como un double en la variable “inicio”; en ese momento empieza la búsqueda de contenedores.

  • Buscar contenedor.
    El robot comienza a girar en sentido antihorario y a buscar con el sensor de ultrasonidos posibles contenedores. Va tomando medidas con el sensor de ultrasonidos, de 5 en 5, como en la fase inicial.
    En el momento en el que se obtienen 3 medidas válidas de estas 5 (se detecta 3 veces seguidas un objeto entre 0 y 90 cm de distancia), la variable “detectado” pasa a tomar el valor “true” y el robot pasa al estado de detección de contenedor.
    Nos quedaremos en el estado de búsqueda de contenedor mientras “detectado” sea “false".

  • Detectando contenedor.
    En cuanto “detectado” cambia su valor a “true”, se lee con la brújula el ángulo inicial de ese contenedor y se almacena como un entero en la variable “di”.
    El robot sigue tomando lecturas del sensor de ultrasonidos y, en cuanto una de ellas sea mayor a 90 o indique que no hay ningún objeto (-1), sabe que ha llegado al final del contenedor. En ese momento se para y mide con la brújula el ángulo final del contenedor (variable “df”).
    El robot calcula el ángulo en el que se encuentra el punto medio del contenedor (destDegrees) como:



    Hay que tener en cuenta el caso particular de que el contenedor esté en el norte, pues en esa situación su ángulo inicial será cercano a 0 y el ángulo final cercano a 360. Para poder seguir aplicando esta fórmula, habrá que sumar 360 al ángulo inicial “di”. Ahora “destDegrees” podrá ser menor o mayor de 360; si es mayor, tendremos que restarle 360 grados para obtener todas las posiciones en un rango de 0 a 360º.
    Con este ángulo medio ya calculado, el robot comprueba que no hay otro contenedor que ya haya sido detectado y se encuentre cercano a esta posición (permitiendo que exista una desviación de 15º). De esta forma, evitamos que haya dobles detecciones de un mismo contenedor. Si anteriormente había detectado un contenedor en el intervalo [destDegrees-15, destDegrees+15], pondrá la variable “añadido” a “true” y seguirá buscando nuevos contenedores. Si efectivamente es un contenedor nuevo, dejará “añadido” a “false” y pasará a la fase de comprobación de color.

  • Comprobando color.
    Para poder comprobar el color de un contenedor, en primer lugar el robot se tiene que orientar hacia su punto medio. Por lo tanto, empieza a girar en sentido contrario hasta que mide con la brújula que se encuentra en una posición igual a “destDegrees”.
    Cuando ya se ha situado en la posición correcta, estima con el sensor de ultrasonidos a qué distancia se encuentra el contenedor y la almacena en la variable “dist”. El robot avanzará una distancia “dist”-5 cm, pues para tomar una lectura con el sensor de color necesita estar muy cerca del contenedor. Debido a la poca precisión del sensor de ultrasonidos, la estimación de esta distancia no es muy buena y, en algunas ocasiones, el robot se acerca demasiado al contenedor.
    Cuando el robot alcanza el contenedor, comprueba su color con el sensor de color. En ese momento se asegura de que anteriormente no había detectado un contenedor de ese mismo color (nunca deberíamos llegar a detectar dos veces un contenedor del mismo color, pues en el estado “detectando contenedor” ya hacíamos una comprobación para evitar dobles detecciones). Si ya teníamos un contenedor de ese color, se pone a “true” la variable “contenido” y se sigue buscando nuevos contenedores. Si es un color que todavía no tenemos y se encuentra dentro de la gama de colores válidos (rojo, amarillo, azul y verde), pasamos al estado de “añadir contenedor".

  • Añadir contenedor.
    Tras comprobar que, efectivamente, hemos encontrado un contenedor que todavía no teníamos almacenado, creamos un nuevo objeto de la clase Container, que se almacena en un ArrayList de objetos Container.
    Al constructor le pasamos como parámetros: la posición del contenedor respecto al polo Norte magnético, la distancia en línea recta al punto central donde se coloca el robot en el escenario y su color.
    El robot retrocede la misma distancia que había avanzado para detectar el color del contenedor. Si tiene almacenados menos de 4 contenedores, seguirá buscando. Si ya tiene 4 contenedores, se termina la detección.

  • Fin de la detección.
    La detección se termina cuando en el ArrayList de contenedores ya hay 4 elementos y todos ellos son de distinto color: rojo, amarillo, azul y verde. La última acción del robot es seguir girando hasta que se encuentra en la posición en la que había empezado la búsqueda de contenedores, almacenada en la variable “inicio”.
    El robot se parará y se quedará esperando a que accionemos el botón de “Procesar” desde la aplicación Android para poder proceder a la clasificación de los residuos.

viernes, 11 de mayo de 2012

Vídeo: Clasificación autónoma de residuos

Con la mejora en la detección de contenedores mostrada en el último vídeo, sólo faltaba implementar el algoritmo de clasificación de residuos en función de su color para completar la implementación del modo autónomo de clasificación de Kigo.

Al accionar en la aplicación Android el botón "Mapear", el robot buscará detectores en sus alrededores (como vimos en el vídeo ya mencionado) y almacenará sus posiciones en grados en unas variables. Cuando termina el proceso de mapeo, se puede accionar el botón "Procesar": el robot mirará con el sensor de color de qué color es la primera pelota de su cargador, accederá a la variable correspondiente al contenedor de ese mismo color para saber dónde se encuentra, girará hasta esa posición y lanzará la pelota. Seguirá estos mismos pasos para todos los residuos que tiene que clasificar, hasta que el cargador esté vacío.

En este vídeo vemos únicamente la clasificación de los residuos, una vez que el robot ya ha detectado la posición de cada uno de los contenedores. El color de las pelotas que tiene que clasificar en este caso es: rojo (minuto 0:02), azul (0:09), amarillo (0:13) y verde (0:26).

Aunque por la construcción de los contenedores que tenemos actualmente la pelota no se queda en su interior, sí que podemos ver que el robot se orienta bien hacia el punto medio de los contenedores y no comete ningún error.

jueves, 10 de mayo de 2012

Aplicación Android con brújula integrada


Además de mejorar el algoritmo de detección de contenedores con nuestra nueva brújula I2C, hemos incorporado en la aplicación Android la monitorización de la posición del robot respecto al Polo Norte magnético en tiempo real.

Como resultado de ello, en la página de “sensores” de nuestra interfaz gráfica aparece ahora la imagen de una brújula, cuya aguja irá rotando en función de las lecturas obtenidas del sensor brújula.

Para poder llevar a cabo esta monitorización, hemos tenido que introducir algunos cambios en la clase principal del software de nuestro robot:
  • Main.java: el nuevo array de datos que manda el robot de manera continua por el stream de salida Bluetooth está formado por 7 bytes: distancia, color de la pelota, color del contenedor, pelotas que le quedan, batería y 2 bytes con la medida de la brújula.


Para que estos cambios también se vean reflejados en la propia aplicación Android, hemos realizado algunas modificaciones más en las siguientes clases del software de la aplicación:
  • Robot.java: Su método run() lee los 7 bytes que le llegan por su stream de entrada y los almacena en las variables “distance”, “ballColor”, “containerColor”, “balls”, “battery”, “reg2” y“reg3”. Llama a distintos métodos de la actividad principal (pasando estas variables como parámetro) para que se muestre al usuario la información en la pantalla del dispositivo Android.
  • NXJAndroidActivity.java: En la actividad principal de la aplicación, trabajamos con algunas variables nuevas:
    • ImageView img: imagen de una brújula
    • Float rotCompass: ángulo de la rotación que había sufrido la imagen al representar la última medida de la brújula.
      Además, hemos añadido el método
      public void setCompassValue (byte reg2, byte reg3). Éste se encarga de unir los 2 bytes que llegan por el stream de entrada con la medida de la brújula y obtener el float “newRot”, que contiene la lectura de la brújula en grados sexagesimales. Hacemos que la aguja de la brújula rote desde su posición inicial (definida por un ángulo “rotCompass”) hasta la nueva posición tras la medida (hasta un ángulo “rotCompass+newRot”).
Con esta implementación, ya tenemos completa la monitorización de los sensores de nuestro robot en tiempo real.