Estaba yo pensando… ¿Cuál será la mejor manera de representar esta información?
Una vez que hemos adquirido la información de la escena que ve el sensor será necesario procesarla para extraer la parte que nos interesa o nos resulta útil. Se podría, por ejemplo, obtener medidas de las paredes que envuelven la escena; se podría extraer de la imagen todo lo que no fueran paredes, suelo o techo; se podrían extraer objetos de la escena para tratarlos de manera separada y otras muchas cosas. En fin, ésta es la parte más imaginativa.
Yo me he propuesto, simplemente, obtener una imagen de 360º de la escena. Para esto habrá que contar con varias “instantáneas” tomadas desde distintas posiciones del sensor alrededor de un eje. Cómo integrar estas distintas imágenes en una única representación de la escena lo dejo para otro post más adelante. Ahora contaré algo sobre la herramienta que he usado para procesar la información.
La información de profundidad organizada en los frames que acabamos de adquirir del sensor no parece la más conveniente para hacer este procesado. Esta representación es buena para hacer la trasferencia desde el sensor, porque es compacta, pero es mala para procesar la información que contiene ya que, incluso, es dependiente de la resolución del propio sensor.
Cuando uno busca algo sobre el tratamiento de escenas escaneadas en 3D siempre acaba en el mismo sitio: la librería PCL (Point Cloud Library).
Como ellos mismos aclaran en su sitio web (http://pointclouds.org), PCL es un gran proyecto de código abierto para el procesamiento de imágenes 2D/3D y nubes de puntos. Realmente, es un conjunto de librerías que incluyen algoritmos para filtrar, identificar características, reconstruir superficies, combinar imágenes, extraer objetos y otros. Estos algoritmos pueden ser usados, por ejemplo, para filtrar datos con ruido, combinar varias nubes de puntos, extraer partes relevantes de una escena, extraer puntos clave, calcular características que nos permitan reconocer objetos por su forma geométrica o reconstruir superficies por nombrar alguno.
La librería se distribuye en código fuente bajo licencia “3-clause BSD”. Su uso comercial y para investigación es gratuito.
Una nube de puntos es una estructura de datos usada para representar una colección de puntos que representa información en tres dimensiones. Los puntos de la nube se representan por sus tres coordenadas cartesianas X, Y y Z. Cuando la nube de puntos contiene información de color de cada uno de los puntos se puede hablar de una nube de puntos 4D.
Un aspecto muy importante es la representación de una nube de puntos en un fichero. Hay distintos formatos, el más común y el usado por las librerías PCL es el formato PCD (Point Cloud Data). Un fichero de este tipo acaba su nombre con la extensión .pcd, que lo identifica como tal.
Haciendo memoria de lo publicado en el post anterior, la información obtenida del sensor en un frame de profundidad estaba ordenada por pixels y para cada uno de ellos se incluía la distancia al plano de la cámara. Este es un formato muy cómodo para representar el streming en niveles de amarillo en función de la profundidad pero no son coordenadas cartesianas. Para poder procesar la información de los frames adquiridos con la librería PCL primero será necesario traducir este formato a una nube de puntos.
La primera tarea, por tanto, será generar un fichero PCD con la información capturada en un frame de profundidad.
También hay otra manera de pasar datos del sensor a las librerías PCL. Es haciendo uso del OpenNI grabber que permite el control de los streams de color y de profundidad del sensor desde la librería PCL pero no me pareció buena idea usarlo por:
- El grabber de la versión de PCL que usé no soportaba la versión 2 de OpenNI, que es la que yo había usado con éxito para obtener los datos del sensor.
- Me pareció buena idea mantener un interface claro entre OpneNI y PCL que me permitiera determinar con facilidad en qué lado estaban los problemas en caso de que los hubiera (que siempre los hay).
Para obtener la disposición de los puntos en coordenadas cartesianas se usa el método convertDepthToWorld de la clase CoordinateConverter. Este método toma como argumentos la dirección de la instancia de la clase VideoStream que representa el stream de profundidad, la posición X del pixel, la posición Y del pixel, su valor de profundidad y tres punteros a float en los que se devuelven los valores de X, Y y Z medidos en milímetros de la posición del pixel en sus coordenadas cartesianas.
El eje X de estas coordenadas coincide con una línea que pasa por el proyector infrarrojo y la cámara, es decir, sería el eje horizontal si el sensor estuviera de pie apoyado en el suelo. El eje Y estaría contenido en un plano paralelo al frontal de la cámara siendo perpendicular al eje X y el eje Z se proyectaría hacia la escena siendo perpendicular a los ejes X e Y. El origen de las coordenadas estaría en la cámara del sensor y, en cuanto a los signos, desde la perspectiva de la cámara, un objeto moviéndose desde la izquierda a la derecha estaría incrementando su valor en el eje X; moviéndose desde abajo hacia arriba estaría incrementando su valor en el eje Y; y moviéndose desde la cámara hacia la escena estaría incrementando su valor en el eje Z.
Como ya he comentado antes, los ficheros pcd contienen información de una nube de puntos. Están compuestos por dos partes. La primera parte del fichero es una cabecera codificada en ASCII en la que se indican distintas características del fichero. La segunda parte consiste en la información de los puntos, que puede estar codificada en ASCII o en binario.
La cabecera está formada por varias entradas, cada una de ellas en una línea separada por el carácter new line (\n). Se pueden poner comentarios precedidos por el carácter ‘#’. Cada entrada está definida por una palabra clave y describe una característica del fichero o de los puntos representados. Son las siguientes:
- VERSION. Se indica la versión del formato del fichero. La última es la v.7
- FIELDS. Se indican en esta entrada los campos de información que incluye cada punto. En el ejemplo a continuación se incluye información de las coordenadas (x y z) y del color del punto. También se pueden incluir otras características como las normales a la superficie en el punto u otros campos más complejos que no voy a comentar.
- SIZE. Especifica el tamaño en bytes de cada campo de información del punto.
- TYPE. Especifica el tipo de cada campo de información. Las opciones son I para enteros con signo, U para enteros sin signo y F para floats.
- COUNT. Especifica cuantos componentes tiene cada campo de información del punto. Siempre será 1 excepto para algunos campos complejos.
- WIDTH. Si la nube de puntos es organizada se indica el número de puntos en una fila de ésta. Si no lo es, indica el número total de puntos.
- HEIGHT. Indica el número de filas de la nube de puntos si es organizada. En caso de que no lo sea, tomará el valor 1.
- VIEWPOINT. Especifica un punto de vista para los puntos de la nube. Se define como una posición más un cuaternio, que define un giro. Son, por tanto, siete valores. En el ejemplo se indica su valor por defecto.
- POINTS. Indica el número de puntos de la nube. En la versión v.7 es un campo redundante.
- DATA. Indica el formato en el que se representan los datos. Puede tomar los valores “ascii”, como en el ejemplo, o “binary”. El formato ascii es más costoso de leer y escribir, pero es portable y el formato binary es una representación en memoria del array pcl::PointCloud.points escrito o leído del fichero con mmap/munmap en Linux. Es más rápido pero la portabilidad no está garantizada.
Se puede ver a continuación un ejemplo. Los valores de las coordenadas se representan en metros.
Ya tengo un fichero pcd con la información que me llega en dos frames sincronizados de los streams de color y de profundidad. Ya puedo procesar la nube de puntos. Lo primero es instalar las librerías PCL, compilarlas y poner en marcha un entorno que me permita editar, compilar y depurar cómodamente. De nuevo mi candidato es Eclipse que, además, ya estoy usando con OpenNI.
En la web de la librería PCL se facilita la descarga de los fuentes y de binarios precompilados para Linux, Windows y Mac OS X. La instalación de las liberías PCL sólo la he hecho en Linux, ya estaba metido en faena con Linux y no me aportaba nada hacerla en Windows.
Para la instalación del paquete en Linux y la compilación de los fuentes he seguido las instrucciones del tutorial http://pointclouds.org/documentation/tutorials/compiling_pcl_posix.php.
También hay un tutorial que indica cómo usar Eclipse para la compilación de las librerías y ejemplos de aplicaciones que las usan. Se puede encontrar en http://pointclouds.org/documentation/tutorials/using_pcl_with_eclipse.php. Esto me sirve para poner en marcha Eclipse.
Ahora ya puedo procesar la nube de puntos.
En primer lugar, es conveniente hacerle una “limpieza” a la información que nos llega directamente del sensor y que está reflejada en este fichero. Se trata de eliminar la información que, por redundante o por errónea, no aporta nada y complica el procesamiento de la información.
Se aplica un filtro para despreciar los puntos que quedan fuera del rango útil del sensor. Según las especificaciones del fabricante del sensor, el ASUS Xtion Pro Live funciona en el rango de distancias de 0,8 a 6,0 metros medidos desde el sensor. Se genera una instancia de la clase PassThrough y se configura para filtrar todos los puntos cuyos valores en la coordenada Z (con el método setFilterFieldName) queden fuera del rango mencionado (con el método setFilterLimits).
En segundo lugar, se aplica un submuestreo para un voxel de 1cm. Un voxel es una unidad cúbica. Si es de 1 cm, representa un espacio cúbico de 1cm de lado. Para nuestra aplicación es más que suficiente una resolución de la información de 1 cm3 (centímetro cúbico). De esta manera se elimina información innecesaria que hace más costosos los tratamientos posteriores. Por la tecnología del propio sensor habrá más densidad de puntos en las partes de la escena más cercanas al sensor, mediante este filtro se ajusta esta densidad a la necesaria en las partes más pobladas.
Se genera una instancia de la clase VoxelGrid, se configura el tamaño del voxel con el método setLeafSize y se aplica el filtro generando una nueva nube de punto filtrada.
Por último, otro tratamiento común es el filtro estadístico de “outliers”. Mediante este filtro se eliminan los puntos que están aislados en la nube de puntos. Probablemente, estos puntos aislados son debidos a reflejos del haz de leds sobre la escena u otros errores y no corresponden a puntos reales. Estos puntos atípicos se filtran en base a la media de las distancias a sus n vecinos más cercanos. Se genera una instancia de la clase StatisticalOutlierRemoval y la configuro para rechazar aquellos puntos cuya distancia media a sus 50 vecinos más cercanos sea superior a 3 veces la desviación típica.
Con esto ya tenemos una nube de puntos limpia y reluciente, preparada para su representación con un visualizador y su integración con otras instantáneas para generar una representación panorámica de la escena.