Skip to content

javascript

js-aruco: De 2 a 3 dimensiones

Realidad AumentadaHe actualizado los fuentes de js-aruco con los últimos cambios en los que he estado trabajando últimamente. La principal novedad es la posibilidad de calcular la pose en tres dimensiones de un marcador a partir de su proyección en dos dimensiones. Aunque también hay otros cambios importantes, así como varias mejoras realizadas para optimizar el rendimiento.

He publicado una nueva demo online con todos los cambios realizados:
/files/aruco/debug-posit/debug-posit.html

Estimación de la Pose
Para calcular las poses en tres dimensiones he utilizado el método descrito en «Iterative Pose Estimation using Coplanar Feature Points». Este no es el método que utiliza la librería original que tomé como punto de partida, pero lo he preferido porque no requiere los parámetros de calibración de la cámara. Sus únicos parámetros de entrada son el tamaño real del marcador y la longitud focal de la cámara. El primero es sencillo, es lo que mide realmente de lado a lado el marcador (en milímetros). Y aunque el segundo suena un poco más difícil de calcular, en realidad se puede aproximar asumiendo simplemente que es igual al ancho de la imagen con la que se esté trabajando (en pixels).

El algoritmo devuelve dos poses estimadas caracterizadas por una matriz de rotación y un vector de traslación. Esto es así porque la proyección de un cuadrilátero en dos dimensiones puede corresponderse con dos posiciones distintas del mismo cuadrilátero en tres dimensiones. Para averiguar cual es la proyección más correcta, si es que existe alguna, se calcula el error que existe entre el modelo del marcador de partida, y el resultado de aplicar cada rotación y traslación calculada a la proyección dada. La pose estimada que produce el menor error se considera la más correcta de las dos.

He acabado realizando dos implementaciones, la primera basada en el código original utilizado por Daniel DeMenthon escrito en C, y la segunda basada en el código en C# descrito en un artículo por Andrew Kirillow. Dan resultados bastantes similares, aunque hay ciertas diferencias en el cálculo del método del error. Las he exportado con el mismo nombre, ya que la idea es utilizar un método u otro, y para ello basta con incluir posit1.js o posit2.js según cual método se prefiera. Está explicado en la web del proyecto.

Como añadido, he tenido que implementar en JavaScript el algoritmo de descomposición en valores singulares de una matriz, más conocido por su siglas en inglés «SVD». Para ello he seguido la implementación original descrita en «Numerical Recipes in C – Second Edition».

Stack Box Blur
Para aumentar el rendimiento general de la librería he sustituido el cálculo del Gaussian Blur, que se utilizaba para realizar el Threshold Adaptativo, por un Stack Box Blur.

El cálculo del Gaussian Blur era la parte que más tiempo de proceso llevaba de toda la librería. Utilizaba un tamaño de kernel de 7, lo que implicaba que para cada pixel de la imagen se tenían que realizar del orden de un centenar de accesos a memoria y otras tantas operaciones matemáticas. El Stack Box Blur utiliza un tamaño de kernel de 2, reduce considerablemente el número de accesos a memoria utilizando una pequeña pila (stack), evita tener que utilizar un buffer intermedio del mismo tamaño que la imagen, y gracias a una tabla precalculada apenas realiza unas pocas operaciones aritméticas por pixel. Y lo mejor de todo es que el resultado final es apenas indistinguible del original Gaussian Blur cuando se utiliza para calcular el Threshold Adaptativo.

He implementado una versión adaptada a las necesidades de la librería, que en este punto concreto trabaja con imágenes en escala de grises, utilizando un sólo canal, frente a las implementaciones originales, que sólo permiten trabajar con imágenes de tres o cuatro canales.

Supersampling
Una de las cosas que tenía pendiente prácticamente desde que implementé la librería era mejorar el warp. Este proceso es el que extrae de la imagen las áreas donde se encuentran los marcadores detectados y las proyecta en cuadriláteros con la idea de reconstruir el aspecto original real de cada uno de ellos. Su principal carencia era la falta de algún tipo de interpolación entre los pixeles adyacentes, por lo que las imágenes resultantes no eran todo lo buenas que podían llegar a ser cuando los marcadores estaban en ángulos «complicados» en vez de directamente enfrentados a la cámara.

Los cambios realizados han sido de dos tipos. Por una parte optimización del código existente, y por otra parte adición de supersampling. La optimización no ha sido demasiado difícil, ya que originalmente la función no estaba nada optimizada y ha sido fácil obtener una ganancia de rendimiento rápidamente. Desgraciadamente la implementación del supersampling se ha comido la ganancia y algo más. No estoy nada contento con la implementación, se basa en el uso de un par de decenas de variables locales, y eso resulta difícil que la máquina virtual pueda optimizarlo tratando de cachear valores en registros del microprocesador. La parte positiva es que las imágenes que se obtienen ahora son de muchísima más calidad, con bordes rectos en vez de dentados como ocurría antes.

Rendimiento
Utilizando como referencia la última demo, el rendimiento global del sistema en mi equipo es de 60 FPS estables con un consumo de entre el 7 y el 9 por ciento de CPU. Los FPS están perfectos, ya que es el máximo que permite el navegador, y la CPU no está demasiado mal, aunque me haría un poco más feliz ver una cantidad menor ahí. Afortunadamente creo que aún hay bastante margen de mejora.

js-aruco: Coplanar POSIT

En el vídeo adjunto se pueden ver las primeras pruebas que estoy haciendo sobre js-aruco, mi detector de marcadores de realidad aumentada, con el objeto de añadirle un algoritmo que resuelva el llamado «problema de la pose» para obtener la orientación de un objeto 3D a partir de su proyección en 2D.

El código es una implementación en JavaScript del método llamado «Coplanar POSIT», y lo que se ve en el vídeo es la ejecución en Chrome 18 con el flag «–enable-media-stream» activo.

En la parte superior está la captura de la webcam y un simple cubo que se mueve en función de como lo hace el marcador. La distancia la calcula bastante bien en general, pero con las rotaciones aún tengo problemas. En la parte inferior se muestran las dos posibles orientaciones que devuelve el algoritmo. La que tiene menor error a la izquierda, y la de mayor error a la derecha. Todavía es bastante inestable y queda trabajo por hacer, pero ya empiezan a verse los resultados.

Referencias

– «Iterative Pose Estimation using Coplanar Feature Points»
Denis Oberkampf, Daniel F. DeMenthon, Larry S. Davis

– js-aruco: Augmented Reality Marker Detector
http://code.google.com/p/js-aruco/

– Three.js: 3D Engine
https://github.com/mrdoob/three.js

Revisitando js-lzma

He añadido un nuevo método público a js-lzma para que pueda descomprimir directamente un fichero generado con el compresor de línea de comandos que viene con el SDK.

js-lzma es un librería que escribí en JavaScript para poder descomprimir ficheros en formato LZMA. Originalmente obligaba a leer el fichero y extraer la cabecera por parte del cliente. Hoy lo que he hecho es añadir una función en la interface pública que ya lo hace todo por si sola.

Con la función original hay que realizar la llamada de esta forma:

LZMA.decompress(properties, inStream, outStream, outSize);

Con la nueva función basta con hacerlo de esta forma:

LZMA.decompressFile(inStream, outStream);

He aprovechado para actualizar la licencia, porque buscando por la web he visto que algunos proyectos están empezando a utilizarla.

gl-matrix: No tan buena idea

En mis últimos proyectos he venido utilizando gl-matrix, una librería escrita en JavaScript para operar con vectores, matrices y quaterniones. Es obra de Brandon Jones, un ingeniero que trabaja para el área de móviles de Motorola (no hace mucho adquirida por Google), conocido por su trabajo con WebGL, sobre todo por algunas demos de carga de ficheros, ya sean de modelos independientes o escenarios completos.

Empecé a utilizar la librería por la inercia de haberme basado en código copiado directamente de learningwebgl.com, que es la versión para WebGL de los clásicos tutoriales de NeHe para OpenGL. Y si bien me pareció una buena idea al principio, ahora ya no lo tengo tan claro.

Para realizar un par de operaciones con matrices, que es lo único que se necesita habitualmente para renderizar algo por pantalla, pues la verdad es que casi cualquier cosa sirve, incluida gl-matrix. Pero cuando se trata de realizar algo más elaborado enseguida se tropieza uno con ciertos aspectos de diseño de la librería que resultan bastante molestos.

La motivación principal de la librería es permitir trabajar con vectores, matrices o quaterniones utilizando el tipo Array clásico de JavaScript, o el más reciente ArrayBuffer si está disponible en el navegador, e incluso mezclarlos indistintivamente. Y sobre todo poder acceder a los elementos individuales de los arrays utilizando la notación clásica de corchetes con el propósito de aumentar el rendimiento al eliminar cualquier tipo de acceso intermedio y ser lo más transparente posible en su uso. El inconveniente es que para conseguir sus propósitos obliga a pagar un precio un tanto alto.

Veamos un código de ejemplo clásico para sumar dos vectores:

vec3.add(a, b, c);

Lo que uno espera es a = b + c, pero lo que realmente ocurre es c = a + b. Resulta un poco confuso, pero consultando la documentación se aclara que es algo intencionado, y que por diseño el resultado se suele dejar por lo general en el último parámetro pasado a todas las funciones de la librería.

El inconveniente de esta solución es que dificulta la lectura del código si no se conoce la librería, resulta muy poco intuitivo. E incluso después de haber trabajado con ella es complicado, ya que también se dice en la documentación que el último parámetro de la función es opcional, por lo que el siguiente código es válido:

vec3.add(a, b);

Lo que uno espera ahora es resultado = a + b, pero lo que realmente ocurre es a += b. Es decir, ahora recibe el resultado el primer parámetro y no el último. Una misma función que hace dos cosas distintas no suele ser buena idea, quizás lo mejor en este caso sería haber hecho dos funciones distintas, una para cada caso en particular en vez de basar su funcionamiento en la existencia o no de un tercer parámetro.

Incluso es posible escribir el mismo código de una tercera manera:

vec3.add(a, b, a);

Lo que uno espera ahora intuitivamente es a = b + a, pero lo que se obtiene es a = a + b. El resultado final en este caso es el mismo porque la suma de vectores es conmutativa, pero para otro tipo de operaciones, como la multiplicación de matrices por ejemplo, que no es generalmente conmutativa, no se tendría el resultado esperado.

Un problema más derivado de este diseño puede verse en el siguiente código donde se intenta concatenar dos operaciones:

vec3.add( vec3.add(a, b), c);

Lo que uno espera es resultado = a + b + c. Y es lo que se obtiene, pero tiene el feo efecto lateral de que el valor original del vector a se pierde. La forma correcta de escribir la expresión anterior sin perder ningún valor es introduciendo una nueva variable:

vec3.add( vec3.add(a, b, d), c);

Pero realmente esa nueva variable es un artificio que la librería nos obliga a introducir, a nuestro programa no le aporta nada, y además dificulta la escritura de operaciones concatenadas como se observa en el siguiente código donde se han añadido la declaración de las variables necesarias:

var a = vec3.create(),
  b = vec3.create(),
  c = vec3.create(),
  d;
...
vec3.add( vec3.add(a, b, d), c);

Lo que uno espera es que el código anterior funcione, pero lo que ocurre ahora es que se produce un error en tiempo de ejecución. Y es que la librería espera además que la variable que almacena el resultado intermedio le llegue inicializada desde el cliente igual que el resto:

...
  d = vec3.create();
...

La clave está en que la librería evita crear cualquier tipo de objeto intermedio con el propósito de optimizar su rendimiento, delegando en el cliente la responsabilidad de instanciar la cantidad mínima necesaria de objetos intermedios que realmente necesite para el cálculo que pretende realizar. El problema es que a pesar de esto no consigue su objetivo, y no resulta más optima que otras librerías equivalentes según los tests de rendimiento.

Por otra parte, a estas alturas supongo que más de uno ya se habrá percatado por la nomenclatura de que la librería no crea ningún tipo de objeto a la manera clásica, con sus atributos y métodos, sino que más bien todos los métodos son expuestos públicamente de manera «estática», lo que obliga a escribir una y otra vez el tipo de los datos con los que se trabaja (vec3 en los ejemplos).

Esta última característica concreta del diseño lo utilizan bastantes librerías, pero tiene dos inconvenientes. El primero es que obliga a saber el tipo de la variable con la que se está trabajando en todo momento, en vez favorecer la abstracción y centrarse en la operación en vez del tipo de los operadores. Y el segundo es que si desea cambiar de tipo, para pasar de tres a cuatro dimensiones por ejemplo, entonces hay que realizar la sustitución en todo el código en vez de tan sólo en la declaración.

¡Demasiadas cosas en las que pensar para hacer una simple suma de vectores!