Presentación final

29 de mayo de 2012 2 comentarios

Para despedirnos, os colgamos el vídeo final de nuestra práctica innovadora. Se trata de una versión extendida del vídeo que os pusimos en el Hito 3, dado el poco tiempo de que disponíamos. En este vídeo encontraréis el proceso seguido paso a paso para ir configurando la aplicación, así como todas las aplicaciones y utilidades que proporciona.

Recomendamos verlo en alta definición (1080p a ser posible), para ver los detalles de la interfaz gráfica, especialmente en cuanto a los tiempos por vuelta y por sector, así como los gráficos de fuerza G y velocidad instantánea (todo ello situado en la parte derecha de la pantalla).

También os dejamos un enlace a la memoria del proyecto, por si tenéis curiosidad por el más mínimo detalle 😉

Esperamos que os haya gustado y hayáis disfrutado a lo largo de estos 3 meses de trabajo. Gracias por habernos seguido!

Si tenéis alguna duda o sugerencia, no dudéis en contactarnos!!

Anuncios
Categorías:Sin categoría

Comparación entre carreras

23 de mayo de 2012 Deja un comentario

Por último, nos queda implementar una última funcionalidad a nuestra aplicación: disponer de una interfaz sencilla que nos permita visualizar los resultados obtenidos en cada una de las carreras visualizadas hasta la fecha, con el objetivo de poder sacar conclusiones y desarrollar más rápidamente el robot.

Para ello, creamos una nueva sección, “Comparación de carreras”, accesible desde el Menú principal, que tiene el siguiente aspecto:

Vemos lo siguiente:

  1. En la parte izquierda de la pantalla, aparece una lista con todo el conjunto de carreras visualizadas, ordenadas por fecha (la más reciente primero). Se muestra también el tiempo total de carrera (a la izquierda). Al finalizar cada carrera, almacenamos en la SD del terminal Android un fichero de texto en el que anotamos los datos de la carrera:
    • Parámetros PID con los que se ha configurado el robot en esa carrera.
    • Tiempo por sector, para cada una de las vueltas.
    • Tiempo total de vuelta, para cada una de las vueltas

    En esta Activity, simplemente leemos el directorio en el que hemos guardado cada uno de los ficheros de texto, y realizamos un parseo sencillo de los datos contenidos en ellos para acceder a la información de carrera.

    Una vez el usuario selecciona una carrera, aparece más información en la parte derecha de la pantalla:

  2. En la parte superior, tenemos el valor de los parámetros PID con los que hemos configurado el robot en la carrera seleccionada (Kp, Kd, Ki, velocidad base, velocidad máxima y velocidad mínima). Disponemos del botón “Enviar parámetros al robot”, gracias al cual podremos cargar esa misma configuración PID al robot, y así repetir una carrera como la almacenada y comprobar si el comportamiento del robot es el que buscábamos.
  3. En la parte inferior, aparecen desglosados los tiempos marcados en cada una de las vueltas. Se hace una distinción por sectores, tal y como describimos en la entrada anterior, y finalmente se muestra el tiempo final de la vuelta.

Esta sección resultará de gran interés para el desarrollador, que podrá analizar los tiempos marcados en cada uno de los sectores del circuito para diferentes carreras (en las que se habrá configurado el robot con diferentes parámetros PID), y ver cuál es la configuración que mejor se adapta a las condiciones del circuito para así obtener el máximo rendimiento.

Categorías:Sin categoría

Análisis del circuito por sectores

23 de mayo de 2012 Deja un comentario

Otra mejora que hemos considerado puede resultar de gran utilidad es proporcionar al usuario resultados sobre los tiempos marcados por el robot en cada sector del circuito. Así, por ejemplo, podemos tener un sector puramente de rectas, otro de curvas, y otro mixto, y analizar los tiempos marcados en cada uno de los tres sectores de forma independiente. Esto permitirá al desarrollador ajustar los parámetros de configuración del PID estableciendo un compromiso entre velocidad punta y estabilidad en curva.

Detección de nuevas referencias

Lo primero es establecer las referencias que van a representar el comienzo de cada nuevo sector. Por comodidad, hemos decidido utilizar líneas perpendiculares a la línea del circuito, de igual forma que la línea de salida, que marcará el inicio del sector 0. El circuito queda ahora como se muestra en la imagen:

Ahora bien, esto va a requerir una mejora sobre el procesado de imagen descrito anteriormente. Los pasos a seguir para detectar las nuevas referencias (de las mismas características que la línea de salida) son las siguientes:

  1. Detección de las zonas de la imagen que verifican el “patrón de intersección”, del que ya hemos hablado en entradas anteriores. Es decir, se trata de buscar las zonas en las que potencialmente hay un cruce de líneas, que representarán necesariamente las zonas en las que se sitúan las marcas de comienzo de sector. Tras ejecutar este paso, dispondremos en un array bidimensional del conjunto de píxeles de la imagen que verifican esta condición.
  2. Aplicación del algoritmo K-Means (ya estudiado) para determinar las posiciones finales de cada uno de los 3 sectores diferentes. El algoritmo devolverá un array bidimensional de 3 filas y 2 columnas con las posiciones en pantalla de las marcas de sectores.
  3. Ordenación de los sectores. Para facilitar el control de carrera, e identificar por qué sector tiene que pasar el robot a continuación, conviene ordenar la posición de los sectores en función del orden en que van a ser alcanzados por el robot. Para ello, asumimos lo siguiente: de las 3 marcas de sector, la línea de salida se sitúa entre las otras 2, lo cual no es una condición demasiado restrictiva. Para saber cuál de las otras dos marcas es la siguiente, analizamos la posición de salida del robot: si empieza a la izquierda de la línea de salida, se moverá hacia la derecha, y por tanto el sector situado a la derecha de la línea de salida va a ser el siguiente que va a alcanzar el robot. Lo mismo ocurre si el robot va a dar la vuelta en sentido contrario. Por supuesto, esto sólo lo podemos hacer asumiendo estas condiciones (si el siguiente sector está a la izquierda, debido a la forma del circuito, se detectará como el sector final).
En el array, situaremos en la posición 0 las coordenadas de la línea de salida, en la posición 1 el sector inmediatamente posterior, y en la posición 2 la marca correspondiente al último sector.

El resultado de realizar el nuevo procesado de imagen se puede observar en la siguiente figura:

En color rojo aparecen las referencias utilizadas para llevar a cabo la localización sobre el circuito, el punto verde se corresponde con la línea de salida, y los dos puntos azules representan el inicio de los dos sectores restantes. Obsérvese el pequeño número que tienen asociado, indicando su posición en el array (en este caso, suponemos un sentido de recorrido de la vuelta contrario a las agujas del reloj).

Control de carrera

Debemos realizar una serie de modificaciones al método de control de carrera para tener en cuenta el paso por cada sector:

  • El algoritmo de cálculo de distancia del robot al punto de interés sigue mismo principio de detección de mínimo. Ahora bien, en lugar de seleccionar como referencia la línea de salida, se calcula la distancia del robot a la siguiente referencia que va a alcanzar, cuya posición se almacena en el array checkPoints[3][2]. El puntero que indica cuál es el siguiente checkpoint por el que debe pasar el robot lo denominamos currentSector. Por tanto, calculamos la distancia a la posición checkPoints[currentSector].
  • Cada vez que el robot pasa por un nuevo sector, se realizan las siguientes operaciones:
  1. Almacenamos el tiempo marcado en el sector situado entre el checkpoint que se acaba de alcanzar, y el anterior, que definen el sector correspondiente.
  2. Comprobamos si currentSector =0, lo que indica que el robot tenía que alcanzar la salida. En este caso, seguimos el algoritmo de control de carrera ya descrito: se almacena el tiempo de vuelta, como la suma de los 3 tiempos marcados en cada sector, y se reinicializa la variable currentLap. Comprobamos si hemos dado ya todas las vueltas, en cuyo caso almacenamos toda la información de carrera y damos orden de detenerse al robot
  3. En caso de quecurrentSector!=0, se incrementa en una unidad. En caso de que sea superior a 2, se reinicia su valor a 0, lo que indica que el siguiente punto es la línea de meta.

Además, se muestra en pantalla la diferencia de tiempo entre el sector recién recorrido y el sector análogo en la mejor vuelta. Para ello, almacenamos en el array bestSectorLap[]  los tiempos en cada sector marcados en la vuelta más rápida, y comparamos con el marcado en la nueva vuelta. Mostramos debajo del cronómetro de vuelta la diferencia de tiempos, con un código de colores verde/rojo en función de si la diferencia sea positiva (más lento) o negativa (más rápido).

 

Categorías:Sin categoría

Vuelta fantasma

22 de mayo de 2012 Deja un comentario

Otra mejora que consideramos puede resultar de interés es la representación de la vuelta fantasma: un gráfico que se desplaza siguiendo la posición del robot en la vuelta anterior. Este gráfico se superpone a la imagen en tiempo real del circuito (y por tanto, al robot), de forma que podemos ver la evolución entre una vuelta y otra del robot.

Funcionamiento

El funcionamiento es sencillo, tal y como se ilustra en la siguiente figura:

  • Creamos una clase, denominada Lap.java, con la que representaremos la información de una vuelta. Como campos privados, contiene un array bidimensional: float[1200][3]. Dispone de 3 columnas, para almacenar las coordenadas X e Y del robot, y una tercera para almacenar el tiempo de vuelta hasta ese momento. Se tienen 1200 filas, con el objetivo de permitir almacenar la información de una vuelta de duración máxima 1200 · 50 ms = 60 segundos. Es un tiempo más que suficiente para la mayoría de circuitos, especialmente en el caso de los velocistas. Además, dispondremos de un campo short lapTime, que contendrá el tiempo final de vuelta, en milisegundos. Se ha escogido esta opción, en lugar de considerar el tiempo en la última posición del array, por ser más precisa (se comete menos error, ya que al realizar sucesivas medidas cada 50 ms se va acumulando error, en lugar de hacer una medida cada 10-20 segundos).
  • En la figura, cada vuelta se representa como un array. Durante esta vuelta, almacenamos la posición del robot a intervalos de 50 ms en este array, en la posición indicada por el puntero p, que se autoincrementará al finalizar la rutina del temporizador.
  • Durante la vuelta, se van almacenando las posiciones del robot en la variable Lap currentLap, que podemos considerar en “modo escritura“. En cuanto se detecta el paso por vuelta (tal y como se describió en entradas anteriores), la vuelta recién completa se almacena en un array de Lap, de dimensiones el número de vueltas que el usuario haya configurado, y del que podremos obtener toda la información detallada de la carrera. Esta vuelta, por tanto, pasa a estar disponible para lectura. Tras haberla almecenado, reiniciamos la variable, de la forma  currentLap = new Vuelta(), para comenzar a almacenar la posición de la vuelta. Asímismo, se reinicia el puntero p,  para comenzar a escribir en el principio del array.
  • Como vemos, en las vueltas sucesivas realizamos una lectura y escritura en paralelo de dos arrays diferentes: uno correspondiente a la vuelta actual, y otro a la vuelta anterior. Los valores de las coordenadas leídos del array de lectura son los que representamos gráficamente como la vuelta fantasma. La posición se irá actualizando conforme se vaya autoincrementando en la rutina del temporizador.

 Igualdad de la base de tiempos

Como vemos en la figura, la referencia de tiempos es la misma para la vuelta actual que para la vuelta fantasma. Esto se puede deducir fácilmente sin más que observar que se utiliza el mismo puntero para lectura y escritura, y se reinicia en los mismos instantes de tiempo (al comienzo de la vuelta). La posición del array equivale a la posición temporal en que se tomó la muestra de la posición del robot, y por tanto la correspondencia entre una vuelta y otra está asegurada.

Coordenadas utilizadas

No hemos mencionado un detalle a la hora de declarar el array de posiciones del robot que vamos almacenando: es de tipo float. Esto es así porque las coordenadas que almacenamos no son las coordenadas en pantalla del robot, sino las coordenadas relativas a las referencias. Estas coordenadas, como vimos en entradas anteriores, deben expresarse en formato float, pues están normalizadas por el módulo de los vectores que constituyen la base vectorial para solucionar problemas relacionados con el acercamiento y alejamiento de la cámara.

El empleo de coordenadas relativas en lugar de absolutas permitirá al usuario, al igual que con la detección de la línea de salida, disfrutar de la vuelta fantasma de forma compatible con desplazamientos del Tablet, de forma que se recalculará la posición de la vuelta fantasma en relación a la nueva posición de las referencias.

Próximamente subiremos un vídeo en el que mostramos una carrera acelerando y frenando al robot, a la que superpondremos la vuelta fantasma, comprobando efectivamente cómo ha afectado este cambio de velocidad al desarrollo de la vuelta.

Categorías:Sin categoría

Configuración de los parámetros PID desde la aplicación

22 de mayo de 2012 Deja un comentario

Presentamos una de las mejoras que hemos incluido a la aplicación: la posibilidad de configurar los parámetros PID del robot (6 parámetros diferentes) desde la propia aplicación. La inclusión de esta mejora viene motivada por un hecho: el proceso de sintonizado de los parámetros PID es lento y costoso, y los valores obtenidos cambian de un circuito a otro. Cada vez que se quiere cambiar un parámetro, el usuario debe abandonar el circuito, ir a su puesto de trabajo, modificar el código del programa, reprogramar el robot y volver al circuito. Todo ello para comprobar, en muchas ocasiones, que el resultado no es el esperado, o simplemente no se aprecian diferencias evidentes. En definitiva, se pierde una gran cantidad de tiempo en este proceso.

Gracias a esta mejora, el usuario podrá introducir los parámetros PID directamente en el Tablet, que se los enviará por medio de la comunicación Bluetooth al robot, que a su vez reconfigurará sus parámetros PID internos de forma apropiada. A continuación el usuario podrá realizar una carrera de entrenamiento a una vuelta, y analizar el tiempo por vuelta marcado por el robot, comparando con vueltas anteriores para evaluar si el cambio realizado ha surtido el efecto deseado.

Recepción de datos en el robot

Hasta ahora, la recepción se hacía por medio de interrupción por RX en la UART, y mediante comandos simples (1 byte). Para permitir el envío de parámetros PID, necesitamos definir un protocolo de comunicación algo más complejo. En concreto, establecemos el siguiente formato de trama de datos PID, que enviará el Tablet al robot:

  • El primer byte representa el ID el robot, para evitar enviar datos al robot que no le corresponde.
  • El segundo byte es el código de operación, que proporcionará mayor flexibilidad a la hora de añadir comandos al robot. En concreto, hemos definido los siguiente comandos, que estarán disponibles en la aplicación:

  • Los siguientes 6 bytes se corresponden con los 6 parámetros de PID que se pueden enviar al robot, en cuyo caso se debe utilizar OP_CODE = OP_CONFIG. En caso de utilizar cualquier otro comando, estos 6 bytes se deberán poner a 0.
  • A continuación, le siguen dos bytes de CheckSum, tal y como se realizó en su momento para la comunicación desde el robot al Tablet
  • Por último, un byte que indica el fin de trama. A partir de la recepción del mismo, el robot procesará la trama recibida y ejecutará la operación correspondiente al OP_CODE recibido.

Para realizar la recepción de datos en el robot, se siguen los siguientes diagramas de flujo:

Como vemos, se almacena en un buffer cada byte recibido, hasta la recepción del byte 0xFF, momento en que indicamos que hay datos disponibles, mediante el flag disp=1. En el programa principal, comprobamos si este flag está a 1, en cuyo caso iniciamos el proceso de la trama: comprobamos la integridad de los datos mediante checkCRC, verificamos que el ID se corresponda con el del robot, y analizamos el OP_CODE. Si es OP_CONFIG, extraemos la información del buffer de recepción y reinicializamos la configuración del PID. En caso contrario, analizamos el código de operación y modificamos en consecuencia los flags flagStart y flagSpeed, que permiten el inicio/parada de la carrera, así como el cambio de velocidad del robot.

Envío de parámetros desde el Tablet

Para enviar los parámetros de configuración, utilizamos el método write(byte[] data)  del objeto connection (clase BTConnection), que permite enviar un conjunto de bytes por el socket Bluetooth establecido. Creamos además un método que añade la cabecera necesaria para la trama de información, addHeader(byte[]data), que incluye los 2 bytes de CheckSum más el byte de fin de trama.

Por otra parte, creamos una nueva sección en la parte de Configuraciones  del menú principal, añadiendo lo siguiente:

  • Una serie de cajas de texto en las cuales el usuario puede introducir el valor de PID con el que quiere enviar al robot.
  • Un botón que envía al robot la orden de envío de parámetros PID, con el objetivo de recibir en el Tablet estos parámetros, para que así el usuario disponga de un punto de partida sobre el que modificar los parámetros. Para ello, se envía una trama al robot con el código OP_SEND_PID.
  • Finalmente, un botón de envío de parámetros PID, que recoge la información contenida en las cajas de texto, las encapsula en una trama de datos como la definida, y se la envía al robot, que la procesará y cambiará su configuración.

Tratamiento de la coma flotante

Los parámetros del PID tienen un inconveniente: 3 de ellos (kp, ki y kd) se expresan en coma flotante, ya que los cambios que se realizan deben ser muy sutiles para conseguir el resultado adecuado. El problema es que los datos que debemos enviar tienen un tamaño limitado a 1 byte por campo, para optimizar la trama de datos.

Para solucionar el problema, premultiplicamos por un factor los parámetros (de 10 en caso de kp y kd, y de 1000 en ki) antes de enviar la información al robot, que posteriormente dividirá el valor recibido por este mismo factor, convirtiendo el resultado a float. 

Utilizar sólo un byte para los datos conlleva por tanto una pérdida de resolución y de amplitud importante. En concreto, los valores configurables serán:

  • Kp, Kd: entre 0.0 y 25.5, con resolución de 0.1. Teniendo en cuenta que los valores típicos raramente superarán  el valor de 10.0, y que cambios inferiores a 0.1 resultan imperceptibles, esta resolución es más que suficiente.
  • Ki: entre 0.000 y 0.255. Esta constante rara vez supera el valor de 0.010 , y se modifica en intervalos de 0.001

Con esto finaliza la mejora de configurar el PID del robot. Próximamente subiremos un video en el que se comprueba la facilidad y la rapidez con que se pueden cambiar los parámetros hasta conseguir que el robot siga el circuito de la mejor forma posible.

Categorías:Sin categoría

Mejora del algoritmo de actualización de referencias

22 de mayo de 2012 Deja un comentario

Recordando entradas anteriores relativas al procesado de imagen, vimos que para realizar el cálculo de la posición de las referencias en tiempo real hacíamos un análisis local  de la imagen, identificando los píxeles negros en un área de un tamaño fijo centrada en la última posición conocida de cada una de las referencias. La nueva posición se calculaba como el centro de masas de dicho conjunto de píxeles negros.

Tras una revisión con nuestro tutor, pudimos comprobar varios defectos:

  • Los movimientos debían ser extremadamente suaves para no perder las referencias (en caso de perderlas, se conservaba la última posición). El problema residía en que imponíamos una condición demasiado estricta a la hora de decidir si debíamos actualizar las referencias o no: en caso de que el píxel central del bloque considerado para el análisis local no fuera negro, consideramos que el desplazamiento ha sido demasiado grande y no actualizamos la posición de la referencia. Esto se implementó así con el objetivo de no perder las referencias con pequeños movimientos, lo que ocasionaba que o bien se desplazaran hacia líneas del circuito adyacentes, o bien que desaparecieran por completo de la pantalla.
  • Por otra parte, cuando nos alejábamos demasiado, comenzaba a haber problemas para detectar las referencias de menor tamaño, en incluso empezaba a haber conflictos con las líneas adyacentes del circuito. Esto se debe, fundamentalmente, a escoger un tamaño de bloque fijo para realizar el análisis local: al alejarnos demasiado, el bloque es demasiado grande y captura puntos de la línea del circuito que a una distancia normal no serían detectados.

Las soluciones adoptadas han sido las siguientes:

  • Para el primer problema, relajamos las condiciones de actualización de referencias: en lugar de comprobar si el píxel central es negro, comprobamos si el número de píxeles negros que contiene el bloque considerado. En caso de que no contenga ninguno, es decir, en caso de que se esté analizando una zona de la imagen totalmente blanca (lo que ocurrirá con movimientos muy bruscos), conservaremos la posición de la referencia anterior. En cualquier otro caso (existe, al menos, un píxel negro en el bloque considerado.
  • Para el segundo problema, establecemos un tamaño de bloque dinámico, en lugar del fijo que veníamos utilizando. En concreto, el tamaño de bloque (dimensión al cuadrado) será el mínimo número de píxeles negros contabilizados en el proceso de actualización de referencias anterior, más un pequeño margen (de 5 píxeles por lado) para darle un poco de flexibilidad. Esto permitirá escoger el bloque estrictamente necesario para detectar exclusivamente la referencia, por lo que es prácticamente imposible que se lleguen a detectar el resto de líneas del circuito. Al alejarnos o acercarnos, el tamaño de bloque se verá modificado en consecuencia y se podrá seguir detectando las referencias perfectamente.

Tras estas pequeñas modificaciones, hemos comprobado que la experiencia de usuario es mucho más cómoda, ya que rara vez se pierden las referencias, y por tanto el usuario no tiene que prestar tanta atención a un correcto situado de las mismas, y puede disfrutar del núcleo de la aplicación de Realidad Aumentada.

Categorías:Sin categoría

Detección del paso por meta

22 de mayo de 2012 Deja un comentario

Disponiendo de un sistema de referencias robusto, podemos hacer uso del mismo para detectar el paso por meta del robot y calcular automática el tiempo por vuelta marcado, en lugar de que sea el robot el que tenga que enviarnos ese dato, facilitando así la labor del desarrollador del mismo.

Para ello, analizaremos en tiempo real la distancia del robot a la línea de meta, y estableceremos que el robot ha pasado por la misma cuando la distancia sea mínima. Resulta muy improbable (por no decir imposible) que esta distancia llegue alguna vez a ser cero, lo que implicaría que el robot se sitúa exactamente en el mismo píxel que la línea de salida, lo cual es una probabilidad de, al menos 1/(1280*720) (considerando el caso mejor en que la frecuencia de actualización de la posición del robot fuera muy alta).

Calculamos la distancia del robot a la línea de salida como la distancia euclídea en el plano de las coordenadas en pantalla. El objetivo es hallar el mínimo global de dicha función con respecto al tiempo, instante en que consideraremos que el robot ha pasado por la línea de meta. La función distancia tendrá una forma aproximada a la siguiente, en función del tiempo:

Podemos observar en t_1 un mínimo local, que podrá producirse al pasar por una zona del circuito próxima a la línea de meta en cuanto a distancia (pero no en cuanto a distancia restante de circuito), y que, al no corresponderse con la línea de meta, no nos interesa. El punto clave será en t=t_2 , en el que el mínimo de distancia es global.

Para poder distinguir los mínimos, efectuaremos el análisis en caso de que la distancia entre el robot y la línea de meta sea inferior a un cierto umbral, que denominamos d_{th} , y que establecemos en un valor de 50 píxeles, suficiente para detectar el paso del robot a una velocidad razonable, y para evitar mínimos locales como el representado.

Detección del mínimo de distancia

Para calcular el mínimo de la función distancia, lo lógico es hacer la primera derivada y comprobar dónde se anula. Esto se puede computar mediante procedimientos de diferencias finitas, considerando la función distancia muestreada a intervalos constantes:

d'[n] \approx d[n]-d[n-1]

Esto es fácilmente implementable mediante una resta de las distancias en la iteración actual y en la anterior. Para hallar el mínimo, lo ideal sería comprobar si dicha resta es igual a cero. De nuevo, tenemos el problema de que es prácticamente imposible que esto llegue a ocurrir, dado a que la frecuencia a la que se calcula distancia no es infinita, lo que conduce a una pérdida inevitable de resolución.

Por tanto, en lugar de comprobar cuándo es cero la derivada, comprobamos cuándo es positiva, es decir, la distancia empieza a crecer, lo que significa que el robot se está alejando de la línea de salida. Esto equivale a realizar la comprobación de cuándo la distancia actual es superior a la distancia anterior, que es lo que hemos implementado a nivel de código.

Con todo ello, nuestra aplicación detecta perfectamente el paso del robot a cualquier velocidad. Para calcular el tiempo en cada vuelta, recurrimos a la función System.nanoTime(), que devuelve el tiempo en nanosegundos. Esta función debe utilizarse para medir intervalos de tiempo (y no tiempos absolutos), tal y como indica la documentación de Android. Para ello, almacenamos en una variable el resultado de esta función en cuanto el robot inicia una vuelta, y volvemos a llamar a esta función al acabar la vuelta. El tiempo por vuelta será la resta de los dos tiempos. Dividimos finalmente por 1000000 para expresarlo en milisegundos, y ese es el tiempo que finalmente mostramos al usuario en la interfaz gráfica.

Categorías:Sin categoría