Blog

Juan Mellado, 19 Marzo, 2012 - 17:39

Un vídeo que muestra en acción la última demo de js-aruco, mi librería de realidad aumentada escrita en JavaScript:

Demo online:
http://inmensia.com/files/aruco/debug-posit/debug-posit.html

Lo que se ve en el vídeo es la ejecución en Chrome 18 con el flag "--enable-media-stream" activo.

Juan Mellado, 19 Marzo, 2012 - 15:59

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:
- http://inmensia.com/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.

Juan Mellado, 4 Marzo, 2012 - 16:16

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

Temas: Chrome Dart
Juan Mellado, 25 Febrero, 2012 - 11:13

Algunos apuntes sueltos sobre Dart, el lenguaje de programación para la web que está desarrollando Google.

Programación estructurada
Proporciona librerías, clases, interfaces, herencia simple, getters y setters, métodos estáticos, genéricos, sobrecarga de operadores, ... pero como contrapartida también permite omitir los tipos en las declaraciones.

#library("test");

class Test {
  void run() {
    print("Hello world!");
  }
}

#import("test.dart");

void main() {
  new Test().run();
}


Constructores con nombre
No permite sobrecargar los métodos, pero ofrece una solución para sobrecargar los constructores mediante generative constructors (¿constructores generativos?) utilizando una notación de punto. Útil para los contructores de conversión.

class Test {
  Test() {}
  Test.fromString(final String param) {...}
  Test.fromInteger(final int param) {...}
}


Privacidad
Los métodos y variables de instancia privados se denotan anteponiendo un guión bajo ("_") al nombre. De esta forma no hace falta consultar las declaraciones para averiguar el ámbito de aplicación. La privacidad es a nivel de librería, no de clase.

class Test {
  int _secret;
  void _private() {...}
}


Inicialización de variables de instancia
Permite inicializar las variables de instancia directamente en la lista de parámetros de los constructores utilizando "this.".

class Test {
  int _variable;
  Test(this._variable) {...}
}


Notación abreviada
El cuerpo de los constructores vacíos se puede omitir. De igual forma, las funciones se pueden escribir de forma abreviada utilizando la notación "=>". Esto reduce la cantidad de código a escribir y mantener.

class Calculator {
  num _memory;
  Calculator(this._memory);
  num add(final num a, final num b) => a + b;
  num sub(final num a, final num b) => a - b;
}


Parámetros opcionales
Soporta parámetros opcionales, declarados mediante una notación con corchetes, y con un valor por defecto si se quiere. En las invocaciones se pueden referenciar por su nombre e incluso reordenar.

class Test {
  void method(final int a, [final int b = 0, final int c]) {...}
}
final Test test = new Test();
test.method(1);
test.method(1, 2);
test.method(1, 2, 3);
test.method(1, c:3);
test.method(1, c:3, b:2);


Interpolación de String
Aunque las cadenas de texto son constantes y no se pueden modificar en tiempo de ejecución, en el momento de su declaración se evalúa su contenido y se puede incluir en ellas referencias a variables e incluso bloques de código completo mediante el uso del carácter dolar y llaves.

final String hello = "Hello";
final String world = "World";
 
print("$hello ${world.toUpperCase()}!");


Operador parte entera de la división
Tiene un operador "~/" que devuelve la parte entera de una división. Es un complemento al clásico operador módulo "%" que devuelve el resto de una división.

value ~/= 17;


Enteros de tamaño arbitrario
Dart declara una interface num con dos clases que la implementan: double e int. El tipo double tiene un tamaño predefinido de 64 bits y se rige por el estándar IEEE 754. El tipo int no tiene ningún tamaño predefinido, sólo está restringido por la implementación y la memoria disponible en la máquina. Esto último es cuanto menos bastante curioso, y quiere decir que se le puede sumar 1 a un número entero de forma casi infinita, sin que se produzca overflow.

Relación con JavaScript
Dart se está presentando como la alternativa a JavaScript en lo referente a la programación de aplicaciones web de gran tamaño. Su objetivo no es sólo permitir desarrollar aplicaciones para su ejecución en los clientes desde un navegador, sino también en los servidores de forma independiente.

Dart trata de resolver los problemas que plantea JavaScript, como el uso de this por ejemplo. Y además de lo que es el lenguaje de programación en si mismo, también proporciona una serie de librerías completas, como hizo Java en su tiempo. Desde colecciones como arrays, listas, mapas y conjuntos, hasta soporte para realizar operaciones de entrada/salida con ficheros, uso de sockets, etc...

Ejecución
Actualmente las aplicaciones escritas en Dart se pueden ejecutar:
- Desde una línea de comandos invocando directamente la máquina virtual
- Mediante Dartium, un build específico de Chrome que incorpora la máquina virtual
- Generando una versión para JavaScript a partir del código original escrito en Dart

Errores
Dart es un lenguaje que está en desarrollo, lo que hay disponible actualmente es una versión alpha basada en una especificación que aún dista mucho de estar cerrada.

El entorno de desarrollo es prácticamente un editor, basado en Eclipse, que apenas permite realizar las operaciones más básicas con los proyectos. Un ejemplo de esto es que ni siquiera se le puede cambiar el nombre a un fichero, ni borrarlo para crear uno nuevo con el nombre corregido. Pero lo peor sin duda es que aún no se puede depurar. Lógicamente están trabajando para añadir todas estas características.

Durante mis pruebas he encontrado varios errores. He abierto las incidencias correspondientes en dartbug.com y todas las han aceptado como errores, e incluso alguno ya está corregido en el último build nocturno. Aunque creo que hay alguno que les va a llevar un poco más tiempo.

He encontrado un error que hace el código se comporte de forma distinta en función de como se escriba. ¡Me ha traído literalmente de cabeza!. Al final he decidido no continuar la migración que estaba haciendo a Dart de LZMA. El algoritmo que está implementado funciona bien cuando se genera código en JavaScript, pero nunca va a poder funcionar bien en Dart con la especificación actual del lenguaje. El motivo es que LZMA basa su funcionamiento en operaciones a nivel de bit con una aritmética de tamaño entero (32 ó 64 bits) y precisamente en Dart los enteros no tienen tamaño predefinido. Estoy por borrar el proyecto.

Juan Mellado, 22 Febrero, 2012 - 18:47

DartHoy he liberado los fuentes de dart-lzma, una versión para Dart del algoritmo de compresión LZMA. Es un nuevo proyecto que he empezado para ver en que estado está el lenguaje y las posibilidades que ofrece. Y la verdad es que me he encontrado que está todo en general en estado absolutamente alpha, aunque ya es posible editar, compilar y ejecutar programas.

El uso de la librería es muy sencillo, basta con importarla y llamar a la función de descompresión:

#import("lzma.dart", prefix: "LZMA");

LZMA.decompress(inStream, outStream);

Aunque Dart viene con librerías que incluyen varios tipos de streams, he decidido aislar un poco mi librería de los posibles cambios en la implementación y definir dos interfaces propias muy sencillas:

interface InStream {
  int readByte();
  int get length();
}

interface OutStream {
  void writeByte(final int value);
  int get length();
}

Llama la atención que en una interface haya definido un método get, pero parece que el lenguaje lo soporta.

He puesto un ejemplo completo en la página del proyecto, pero al final todo se reduce a instanciar dos objetos y llamar a una función:

final InStream inStream = new InStream(data);
final OutStream outStream = new OutStream();
   
LZMA.decompress(inStream, outStream);

La función de compresión está en desarrollo. He retrasado su desarrollo porque me ha desanimado bastante que el programa no funcione con la máquina virtual de Dart utilizando la línea de comandos. Eleva una excepción ".\vm\object.cc:7453: error: unimplemented code" por que hay código de la máquina virtual que aún no está desarrollado. La única forma de hacer las pruebas es realizando una compilación cruzada del código en Dart a JavaScript para poder llamarlo desde una página web en vez de ejecutarlo de forma nativa.

El entorno y el lenguaje tienen cosas curiosas, a ver si me animo a escribir un post con las cuatro cosas que he ido viendo.