Análisis de imágenes de profundidad para aplicaciones de realidad aumentada mediante el uso de la Kinect Visión Artificial Escuela Técnica Superior de Ingenieros Industriales Universitat Politècnica de València Primer cuatrimestre curso 2013-2014 Por Alfonso Arbona Gimeno Alfonso Arbona Gimeno Visión Artificial - ETSII - UPV Índice 1.- Introducción y objetivos..................................................................................................................2 2.- Obtención de imágenes...................................................................................................................2 2.1.- Hardware.................................................................................................................................2 2.2.- Software...................................................................................................................................2 2.3.- Preprocesado............................................................................................................................4 2.3.1.- Ruido................................................................................................................................4 2.3.2.- Detección de bordes.........................................................................................................4 2.4.- Imágenes de color....................................................................................................................8 3.- Análisis............................................................................................................................................9 3.1.- Datos de profundidad..............................................................................................................9 3.2.- Transformada de Hough..........................................................................................................9 3.2.1.- Primer método...............................................................................................................10 3.2.2.- Segundo método.............................................................................................................11 3.2.3.- Procesado de la imagen de Hough.................................................................................11 3.3.- Obtención de coordenadas del mundo real............................................................................15 3.3.1.- Cálculo de distancias.....................................................................................................15 3.3.2.- Puntos de interés............................................................................................................16 3.3.3.- Rectas u,v a x,y,z...........................................................................................................16 4.- Renderizado...................................................................................................................................19 5.- Optimizaciones aplicadas..............................................................................................................20 6.- Conclusiones.................................................................................................................................21 7.- Bibliografía y enlaces de interés...................................................................................................21 1 Alfonso Arbona Gimeno Visión Artificial - ETSII - UPV 1.- Introducción y objetivos El análisis de imágenes mediante ordenadores ha sido un tema muy estudiado y analizado en los últimos años. El objetivo último siempre ha sido el obtener información del mundo físico mediante el análisis de imágenes capturadas por una cámara. Dentro del gran rango de aplicaciones de estas técnicas, las que aquí nos conciernen son las que versan sobre la obtención de la posición y orientación de elementos concretos de una cierta escena. Esta información nos permite hacer una reconstrucción interna de la escena real, que podemos utilizar por ejemplo, para dibujar sobre la imagen obtenida por la cámara objetos y elementos que no existen en la realidad, aumentando así la escena real original. El mayor problema es la obtención de dichos parámetros de la escena para poder establecer dónde y cómo dibujar los objetos, y hacer esto en tiempo real. Por su simplicidad, el método más utilizado ha sido la introducción en la escena de un elemento cuyas propiedades son conocidas a priori, pero obviamente tiene la desventaja de que es necesario añadir y detectar un objeto concreto a la escena. La solución aquí presentada consiste en el uso de una cámara no-convencional que ofrece datos de profundidades en vez de color. De esta forma, es más sencillo detectar objetos físicos y su posterior análisis de la escena ya que tenemos información sobre el mundo 3D captado, y no solo sobre una proyección bidimensional. Como ejemplo de aplicación se adjunta un código en C capaz de detectar esquinas y sobre ellas dibujar un pequeño robot. Concretamente está diseñado para detectar las lámparas del techo del laboratorio en el que ha sido desarrollado. 2.- Obtención de imágenes 2.1.- Hardware Una cámara fácil de conseguir y de reducido coste que permite la obtención de profundidades de la escena es la Kinect producida por Microsoft. Además, existen librerías de fácil uso opensource disponibles en internet para la creación de programas en C/C++ multiplataforma. 2.2.- Software La librería opensource utilizada es libfreenect, cuyo código puede ser descargado de: https://github.com/OpenKinect/libfreenect O de la página web principal del proyecto: http://openkinect.org Su uso es relativamente sencillo, aunque la documentación es bastante pobre y hay que basarse en pruebas y análisis de los datos obtenidos para entender qué obtenemos con la cámara, y con ello y 2 Alfonso Arbona Gimeno Visión Artificial - ETSII - UPV los ejemplos existentes, adaptarlo todo a nuestro propio proyecto. El primer problema ha sido entender qué datos se guardan en la matriz de profundidades. Para ello ha sido necesario guardar una imagen en un archivo y analizarla mediante un editor hexadecimal. Imagen 1 De esta forma se observa que los valores obtenidos son de 2bytes por “pixel”, y en la documentación se indica el rango de valores que la cámara puede enviar. Cabe destacar que estos valores no son directamente la profundidad de la escena, si no que para obtener la distancia en milímetros es necesario aplicar la siguiente ecuación: 1000 d mm = −0.00307⋅d raw +3.33 Sabiendo esto podemos representar ya los valores obtenidos por la cámara en la pantalla. Lo más lógico sería utilizar el valor en milímetros y representarlo por pantalla, pero puesto que la ecuación que transforma el valor obtenido en distancia real es costosa de calcular cada frame para cada píxel, es mejor representar únicamente el valor obtenido sin ningún tipo de preprocesado, como se observa en la Imagen 2: Obtención de datos en bruto imagen de la derecha. 3 Alfonso Arbona Gimeno Visión Artificial - ETSII - UPV 2.3.- Preprocesado 2.3.1.- Ruido Como queda evidente en la imagen 2, hay una serie de puntos negros que aparecen. Estas zonas indican que la cámara no es capaz de obtener información sobre la profundidad de la escena, y por tanto están representados ahí de color negro. Esta falta de información puede ser debida a muchas causas, las más comunes son una superficie reflectante (el suelo a partir de cierto ángulo), superficies que no reflejen los infrarrojos (las pantallas de ordenador), superficies pequeñas o bordes en las que el mallado de infrarrojos no sea detectado por la cámara (las patas de las mesas), sombras producidas sobre el haz de luz infrarroja de la cámara, o elementos que generen su propia luz infrarroja (lámparas, luz solar, …). Todos estos elementos nos pueden dar problemas en un futuro, especialmente el ruido que cambia con el tiempo que se observa en la animación temblor_sin.gif. La primera solución adoptada es la de mostrar una imagen filtrada temporalmente. Para ello en vez de mostrar la última imagen obtenida, lo que hacemos es rellenar la zona con datos que falten con una imagen obtenida anteriormente. El resultado se puede ver en el archivo temblor_filtrado.gif, pero como se aprecia el ruido no desaparece lo suficiente como para que sea útil, y se producen distorsiones en la imagen que tampoco nos interesan. Por ello optamos finalmente por no realizar este filtrado. Imagen 3: Filtro temporal 2.3.2.- Detección de bordes Puesto que nuestro objetivo es encontrar las esquinas de objetos, deberemos antes realizar un preprocesado de la imagen con un filtro paso alto para detectar los cambios bruscos. Normalmente, en una imagen tradicional esto quiere decir detectar cambios de color, pero puesto que aquí estamos trabajando con profundidades, lo que obtenemos son puntos en los que la distancia de la cámara al objeto varía de forma brusca. Es decir, obtenemos puntos en los que el objeto termina. Tras probar varios tipos de filtros, el que mejores resultados ha generado es el filtro basado en el laplaciano: 0 -1 0 -1 4 -1 0 -1 0 4 Alfonso Arbona Gimeno Imagen 4: Original Imagen 5: Tras detección de bordes 5 Visión Artificial - ETSII - UPV Alfonso Arbona Gimeno Visión Artificial - ETSII - UPV Por otra parte, antes de utilizar el laplaciano como filtro definitivo, también probamos varios filtros. Entre ellos el de Sobel daba buenos resultados, pero requería dos pasadas por la imagen, y multiplicaba el tiempo de procesado significativamente. Además, el resultado de Sobel debe de calcularse mediante la distancia geométrica de sus dos componentes direccionales, y por tanto introducimos cálculos muy costosos en la CPU por cada píxel de la imagen, y en definitiva, no haciendo rentable el uso de dicho filtro. Otro elemento probado era el realizar pasadas de suavizado (filtro paso bajo) antes y tras el filtro paso alto. El filtrado inicial es relativamente útil en entornos con mucho ruido, y por tanto se ha dejado en el código como una opción cambiable en tiempo real (variable do_presmooth) aunque no tiene asignada ninguna tecla actualmente ya que su uso no era especialmente útil en las situaciones en las que ha sido ensayado el programa. Por otra parte, el filtrado tras la detección de bordes tiene sentido únicamente en situaciones en las que el filtro paso alto deja líneas de borde no continuas. Inicialmente el código realizaba dicho post-procesado a la imagen, pero tras comenzar con el análisis de rectas se hizo completamente innecesario, y por tanto borrado del código ya que introducía una pasada más y eliminaba la binariedad de la imagen obtenida por el filtro paso alto aumentando el tiempo de cómputo significativamente. En las imágenes 6 y 7 se puede apreciar como el filtrado paso bajo permitía detectar rectas con una mayor resolución, pero en nuestro caso no era realmente útil ya que buscábamos detectar únicamente cambios fuertes en la profundidad de la imagen y no la cantidad de detalles. Cabe destacar que el tiempo de procesado es realmente muy elevado en la primera imagen en comparación con la segunda. 6 Alfonso Arbona Gimeno Imagen 6: Filtrado paso bajo antes y después del paso alto Imagen 7: Sin filtro paso bajo 7 Visión Artificial - ETSII - UPV Alfonso Arbona Gimeno Visión Artificial - ETSII - UPV 2.4.- Imágenes de color La Kinect dispone de una cámara tradicional a color junto con la cámara de profundidad, y será la que utilizaremos para ser superpuesta con los elementos ficticios que queramos una vez hayamos procesado la imagen de profundidad. Uno de los mayores problemas que hay cuando se utilizan varias cámaras es que no están en la misma posición, y que los datos obtenidos no se corresponden a nivel de píxel. Es decir, que una coordenada en píxeles (u1, v1) de la cámara de color no coincide con el mismo punto físico para la cámara de profundidad. Tampoco coinciden los tamaños de las matrices obtenidos por ambas cámaras. La de color es de Imagen 8: Cámara de color 640x480 mientras que la de profundidad es de 640x488. Estos efectos se observan en las imágenes 8 y 9, ambas tomadas sin mover la cámara de posición. Es especialmente visible en la parte superior de la puerta como ambas imágenes no se corresponden directamente tanto en posición como en escala. La librería se puede configurar para que realice de manera transparente el escalado y desplazado de la imagen de profundidad de tal forma que se ajuste pixel a pixel con la imagen a color (imagen 101). Obviamente se trata de una transformación lenta y Imagen 9: Comparación color - profundidad por tanto la omitimos en este punto del programa, pero para ser coherentes con la imagen a color deberíamos de realizar la transformación una vez hayamos analizado la imagen y tan solo nos interesen unos poco puntos. Por simplicidad y porque el cambio de posición es pequeño en los puntos centrales de la imagen, en el código no está aplicada esta transformación a la hora de situar el modelo 3D sobre la cámara de color. Imagen 10: Mapeado automático por la librería 1 8 En este caso los puntos sin información de profundidad se representan con una cuadrícula blanca y negra. Por otra parte, la librería realiza también la transformación a milímetros y por ello se notan mayores cambios en el color. Alfonso Arbona Gimeno Visión Artificial - ETSII - UPV 3.- Análisis 3.1.- Datos de profundidad Como hemos anticipado en el punto 2.2, los datos obtenidos no son directamente profundidades, si no que hay que aplicar una ecuación de cómputo relativamente costoso para obtener la distancia en milímetros reales. 1000 Recordemos que la relación es: d mm = −0.00307⋅d raw +3.33 Si calculamos la diferencia en unidades obtenidas para un incremento de 1cm a una distancia de 1m y a una distancia de 3m obtenemos: d mm =1000 ⇒d raw =758.9577 d mm =1010 ⇒d raw =762.1827 Δ d raw =3.2250 d mm=3000 ⇒ d raw=976.1129 d mm =3010 ⇒d raw =976.4736 Δ d raw=0.3607 Como observamos, el sistema es muy poco lineal, y por tanto, el filtrado debería de haberse realizado tras haber calculado la distancia real (aunque fuese de forma aproximada). Pero puesto que como dicho cómputo es costoso y que los elementos que queremos analizar están a una distancia más o menos constante siempre, podemos realizar la aproximación y filtrar respecto a los datos raw obtenidos por la cámara. Obviamente, a la hora de calcular el valor final de profundidad nos vemos obligados a recurrir a esta expresión, pero intentaremos evitarlo en la medida de lo posible para reducir el tiempo de cómputo. 3.2.- Transformada de Hough Una vez obtenida una matriz binaria de los bordes de los objetos pasamos a buscar las rectas que los definen. Este paso es importante ya que nos permite diferenciar puntos no interesantes en la imagen preprocesada de los puntos que definen aristas y bordes físicos de los objetos que componen la escena. El método es sencillo. Para cada punto se guarda en una matriz los elementos que definen todas las posibles rectas que lo contienen. Obviamente, puesto que hay infinitas rectas que pasan por un plano se tiende a generar un número concreto de rectas cuyo ángulo varía de forma constante. 9 Alfonso Arbona Gimeno Visión Artificial - ETSII - UPV 3.2.1.- Primer método Inicialmente se optó por utilizar la ecuación de la recta y=m⋅x +n , utilizando como coordenadas de la matriz las variables m y n. El problema surge cuando las rectas se acercan a la vertical, ya que m tiende a valer ±∞ , valor que no puede ser una coordenada de una matriz. La solución a este problema fue diferenciar rectas “normales” (cuya pendiente es un valor real), de las rectas con pendiente infinita. De esta forma la matriz quedaba de la siguiente manera: Imagen 11: Matriz de Hough para m y n El segundo problema que se observó es evidente: las dimensiones de la matriz. La memoria RAM no es infinita, y por tanto hace falta establecer unos límites en cómo de grande es la matriz generada por Hough. En este caso concreto forzamos la n a estar en el rango [-width*height, width*height], es decir 2⋅640⋅480=614400 posibles n. Por su parte, la m no puede almacenarse directamente ya que los valores se dispersan muy rápidamente a infinito. Para solucionarlo se creó una matriz que trasladaba un valor concreto de coordenada de la matriz en una pendiente. Si por ejemplo quisiésemos representar rectas cada grado necesitaríamos 180 posiciones (de -89 a +89 y la vertical). La matriz de transformación nos daría la m en función de dicho ángulo. Pero siguen habiendo problemas. La ordenada en el origen puede salirse del rango, la pendiente puede dar errores de cálculo en valores cercanos a la vertical, la recta vertical está separada del resto de rectas y la necesidad de memoria es elevada. Por tanto se optó por cambiar por completo el sistema de Hough por uno mucho más lógico: guardar como coordenadas el ángulo y la distancia de la recta al origen. 10 Alfonso Arbona Gimeno Visión Artificial - ETSII - UPV 3.2.2.- Segundo método Otra forma de representar una recta es como d =x⋅cos (θ)+ y⋅sin (θ) , así pues, dado un punto (x,y) concreto podemos ir haciendo pasar todas los ángulos que queramos y marcando el punto (d,θ) en la matriz. Siguiendo el ejemplo anterior: necesitaríamos igual que antes 180 posiciones para un eje, pero por otra parte tan solo 2 2 √ 640 + 480 =800 posiciones para el otro eje ya que es la máxima distancia que hay al origen. Esto es una reducción de más del 780000 % en memoria, a parte de ser más rápido de calcular si se realizan ciertos trucos que se explicarán más adelante. El objetivo de estos cálculos es obtener una nube de puntos en dicha matriz que puede verse como una nueva imagen a analizar (imagen 12). Imagen 12: Zoom en una nube de puntos generados por Hough tras un primer filtrado. En este caso el eje vertical representa el ángulo de las rectas y el eje horizontal la distancia de dicha recta al origen de coordenadas (situado en la esquina inferior izquierda de la imagen de profundidades). 3.2.3.- Procesado de la imagen de Hough La imagen generada por Hough debe de ser también analizada para extraer información importante sobre las rectas que realmente forman la imagen. Se trata de una imagen no binaria cuyo valor representa cuántos puntos comparten una recta concreta. Puesto que tan solo nos importan las rectas que contienen muchos puntos de nuestra imagen original, un primer filtro podría ser por valor mínimo, eliminando así todas las rectas que solo han sido generadas por unos pocos puntos (Imagen 12). El problema, como se puede apreciar en la imagen, es que ahora tenemos gran cantidad de puntos inconexos en esta imagen, y por tanto al separarlos en objetos de interés, lo que debería de ser una nube termina siendo un conjunto de pequeños elementos. Para solucionarlo, inicialmente optamos por realizar dos inflados seguido de dos erosiones para intentar generar nubes más grandes, aunque el resultado no es del todo el esperado, y cada inflado o erosión implica una pasada por la imagen ralentizando por tanto el proceso. Como observamos en la imagen 13, el resultado es muy similar a la imagen 12 de la que parte, aunque por otra parte sí que es cierto que muchas de las islas que originalmente estaban sueltas han terminado unidas a la zona principal. 11 Alfonso Arbona Gimeno Visión Artificial - ETSII - UPV Por otra parte, hay gran cantidad de puntos que se han vuelto a separar en elementos más pequeños tras las erosiones. Por todo ello terminamos ejecutando únicamente tres inflados y ninguna erosión. Esto provoca que se generen rectas que realmente no existen, pero a efectos prácticos no tienen relevancia ya que al realizar los inflados los valores introducidos son equivalentes a que un único punto generase esa recta, que en comparación con la cantidad de puntos que generan el resto de puntos es despreciable. Es decir, el valor guardado en la matriz es de 1, mientras que el resto tienen que superar un cierto umbral relativamente elevado. El siguiente paso a aplicar es la separación en distintos objetos de interés. Imagen 13: Hough tras 2 inflados y 2 erosiones Para ello nos valemos de la técnica flood-fill, similar a la usada en programas de dibujo estilo Paint para rellenar bloques continuos, pero modificada para que funcione en nuestra nube de puntos. El algoritmo está bien documentado y no vamos a entrar en detalle en su funcionamiento interno, pero cabe destacar que ha sido modificado para optimizar su ejecución en esta situación concreta y para obtener también distintas características de los objetos en cuestión. En resumen, dada la imagen 13, al final obtenemos una lista de objetos de interés con sus propiedades básicas: masa, área y coordenadas del centro de masas. Al contrario que en los programas de dibujo, en nuestro caso únicamente necesitamos estas propiedades y no la imagen “pintada”, pero para poder ver cómo se está comportando el sistema también está implementado en el código la parte que dibuja con colores los distintos objetos como se puede ver en las imágenes 14 y 15. 12 Alfonso Arbona Gimeno Visión Artificial - ETSII - UPV Imagen 14: Objetos de interés sin inflado Imagen 15: Objetos de interés tras 3 inflados Como se puede observar el área ha variado mucho, pero internamente la masa de los objetos de interés sigue constante y el centro de masas sigue bien situado ya que la masa de los elementos añadidos por los inflados es muy pequeña. Una vez tenemos esta lista de elementos hay que volver a filtrarlos, ahora utilizando como variables datos mucho más elaborados como la masa y la densidad, de tal forma que solo nos queden los elementos con una masa mínima y una densidad relativamente elevada. Estas rectas son las únicas que utilizaremos para calcular las propiedades de la escena tridimensional que estamos analizando. Podemos antes representarlas para poder ver que todo es correcto hasta ahora, como se muestra en la imagen 16. 13 Alfonso Arbona Gimeno Imagen 16: Rectas obtenidas sobre imagen de bordes 14 Visión Artificial - ETSII - UPV Alfonso Arbona Gimeno Visión Artificial - ETSII - UPV 3.3.- Obtención de coordenadas del mundo real 3.3.1.- Cálculo de distancias El primer paso para pasar de coordenadas locales de la imagen (u,v) a coordenadas reales (x,y,z) es obtener un modelo de la cámara que nos permita aplicar operaciones matemáticas trigonométricas necesarias. Recordemos que para cada coordenada (u,v) podemos sacar su distancia a la cámara d. Esta distancia nos ahorra muchísimo trabajo que de otra forma debería de hacerse mediante elementos cuya medida sea conocida para poder realizar la homografía y así saber la orientación y posición del objeto. Para calcular de forma aproximada el tamaño de un objeto primero necesitamos saber cómo proyecta la cámara los objetos. En una cámara convencional la proyección se realiza mediante esta operación: [][ ][ ] αu 0 u 0 x u = v 0 αv v 0 ⋅ y w 0 0 1 z Aunque en nuestro caso no perdemos una dimensión ya que guardamos la distancia, y por tanto podríamos hacer una reconstrucción 3D del entorno sin necesidad de elementos externos. Mediante varios ensayos (imagen 17) con objetos de medidas conocidas a distintas distancias calculamos que: αu≈α v ≈574.9822 Imagen 17: Obtención de los parámetros de la cámara Y con la siguiente ecuación podemos calcular aproximadamente la distancia entre dos puntos en un plano paralelo a la cámara: d mm = d px⋅profundidad 574.9822 Esto es especialmente útil para sacar rápidamente (el coste computacional es una simple multiplicación) la distancia entre dos puntos a una profundidad similar, aunque finalmente no ha sido necesario utilizarlo en el programa principal. 15 Alfonso Arbona Gimeno Visión Artificial - ETSII - UPV 3.3.2.- Puntos de interés Con toda esta información únicamente falta encontrar el punto que queremos analizar y del que queremos sacar las coordenadas en las que situaremos el objeto virtual. Como ejemplo, el código busca la intersección de dos rectas obtenidas por el método de Hough, concretamente de las lámparas del laboratorio. De todas las rectas posibles busca dos cuyo ángulo no sea similar y el punto de intersección esté cerca del centro de la cámara. Esto es especialmente importante ya que en la cámara podrían producirse muchas intersecciones de rectas (por ejemplo si se ven varias lámparas), y no queremos que se intente dibujar el objeto en un punto casi fuera de la pantalla si tenemos otra intersección mejor en el centro de la pantalla. El código comprueba una a una todas las rectas que han pasado los filtros anteriores y calcula las coordenadas (u,v) del punto de intersección. Y de todos estos puntos retorna las rectas que producen aquella intersección más cercana al centro de la pantalla. Con más tiempo se podrían haber desarrollado algoritmos más complejos para detectar objetos concretos, pero estos algoritmos dejan de tratar ya temas de visión artificial para pasar más a sistemas de inteligencia artificial, ya que la obtención de información del entorno ya ha sido realizado y únicamente queda la selección de puntos de interés. 3.3.3.- Rectas u,v a x,y,z Una vez hemos decidido qué dos rectas analizar pasamos al cálculo del plano que las contiene. Si la selección de las rectas es correcto, ambas comparten un punto concreto (la intersección) y por tanto definen un plano en el mundo real. El problema es que únicamente tenemos su ángulo y distancia en coordenadas (u,v) de la pantalla, y el paso a coordenadas 3D no es trivial. El algoritmo desarrollado realiza los siguientes pasos: Para cada recta, empezando en el punto de intersección nos desplazamos una cierta distancia por ella en una dirección. Si la información de profundidad está disponible en dicho punto, la guardamos y terminamos de buscar un punto. Si no está disponible, volvemos a desplazarnos por la recta en la misma dirección hasta que lo encontremos o nos salgamos fuera de la pantalla (en cuyo caso retornamos error). Realizamos la misma operación con un segundo punto siguiendo la otra dirección Imagen 18: Cambio de coordenadas de la recta. 16 Alfonso Arbona Gimeno Visión Artificial - ETSII - UPV De esta forma tendremos por cada recta 3 puntos distintos con sus coordenadas (u,v) y la distancia real a la cámara. Uno de esos puntos (el central) será compartido entre las rectas por ser el punto de intersección. De la documentación sacamos esta información importante sobre la cámara de profundidades: Distancia focal: 580px. Field of View: 57.8º. Y analizando la imagen 18 para el eje x, y aplicándolo de forma similar para el eje y obtenemos las siguientes expresiones: α=asin ( u px −640/ 2 fd Siendo d la profundidad en milímetros obtenida por la cámara, fd la distancia focal y upx y vpx las coordenadas en píxeles. ) d 640 Como podemos observar, no es necesario realizar operaciones x=d⋅sin (α)= ⋅ u px − costosas como senos y cosenos. De hecho, se trata únicamente de fd 2 un escalado lineal tras cambiar el origen de coordenadas de la d 480 cámara a uno centrado en el centro de la pantalla en vez de en la y=d⋅sin (ρ)= ⋅ v px− fd 2 esquina inferior izquierda. z≈d La única aproximación relativamente incorrecta es asumir que z es directamente la profundidad, pero recordemos que estamos tratando puntos cercanos al centro de la pantalla, y que por tanto los ángulos son muy pequeños, y con ello la coordenada z es prácticamente la profundidad obtenida por la cámara. ( ( ) ) Con estas ecuaciones podemos pasar los puntos (u,v) obtenidos anteriormente con de las rectas, a coordenadas (x,yz) situadas en el espacio real de la escena. Utilizando 3 de esos puntos podemos calcular el plano que contiene a ambas rectas, concretamente nos interesa el vector normal al plano: ⃗ n =( p⃗2− p⃗1 )∧( p⃗3 − p⃗1) Con el punto de intersección y el vector normal tenemos ya todo lo necesario para dibujar nuestro modelo. 17 Alfonso Arbona Gimeno 18 Visión Artificial - ETSII - UPV Alfonso Arbona Gimeno Visión Artificial - ETSII - UPV 4.- Renderizado Puesto que el software ha sido desarrollado en un entorno linux, el software utilizado es opensource libre y gratuíto, pero además las librerías utilizadas son multi-plataforma para permitir su uso en otros entornos. Para crear la ventana, dibujar sobre texturas y controlar las acciones y eventos del usuario y el sistema operativo se ha utilizado la librería SDL sin ningún extra. Inicialmente el proyecto se creó utilizando SDL también para el dibujado en la pantalla, pero al llegar al punto en el que se necesitaba dibujar ya el modelo 3D sobre la imagen obtenida por la cámara se requería utilizar OpenGL, así que se tuvo que migrar la totalidad del código de dibujo a OpenGL, cambiando gran cantidad de elementos (entre ellos texto informativo y elementos similares), que no fueron reintroducidos en el programa ya que ya no eran necesarios en ese punto tan avanzado del trabajo. Las funciones que cargan el modelo 3ds fueron desarrolladas por mí con anterioridad para un proyecto personal y están disponibles bajo la licencia MIT. Su único objetivo es cargar un archivo 3ds y transformarlo en listas de vectores, normales, caras y texturas que OpenGL es capaz de dibujar en la pantalla. Para cargar texturas en formatos más complejos que un simple bmp se ha utilizado la librería DevIL, y se utiliza tanto en el código para cargar los modelos como para cargar alguna textura extra que se utiliza en el programa. El resumen de la línea de dibujo es el siguiente: Se borra la pantalla, se crean las superficies SDL con los datos obtenidos de la cámara y más tarde se actualizan las texturas OpenGL con los datos de dichas superficies SDL. En función del modo de dibujo se representa en la pantalla una de las texturas (color, profundidad o Hough), y se indica que no se desea controlar la coordenada z de la textura, de tal forma que para la GPU la textura estará siempre detrás de todo. Una vez dibujada esta textura se dibuja el modelo en las coordenadas y orientación especificadas. Cabe destacar que estas coordenadas van dadas en milímetros reales, y alineadas con las coordenadas reales que se detectan con la cámara gracias a los cálculos de los puntos anteriores, y por tanto no hay que hacer ningún cálculo en este punto. 19 Alfonso Arbona Gimeno Visión Artificial - ETSII - UPV 5.- Optimizaciones aplicadas Puesto que este proyecto busca realizar los cálculos en tiempo real, se ha optado por realizar muchas aproximaciones y optimizaciones para mejorar la velocidad, en gran parte a costa de aumentar la cantidad de memoria requerida. Esta es una pequeña lista de las optimizaciones más importantes del código: 20 • Multithreading en la captura de de la cámara. Puesto que la cámara va a su propia frecuencia de muestreo, puede que se de el caso en el que recibamos demasiadas imágenes o muy pocas, o que la cámara bloquee la ejecución del código, por tanto, todo el código de control y de obtención de imágenes se realiza en un hilo independiente sincronizado con el hilo principal de ejecución. • Caché de valores y cálculos. Muchos de los cálculos, especialmente los relacionados con funciones trigonométricas utilizadas en la matriz de Hough son extremadamente lentos computacionalmente. Por ello, para agilizar el proceso, al arrancar el programa se calculan todos los senos y cosenos posibles y se guardan en una matriz. Esto es posible porque conocemos a priori todos los ángulos posibles que van a tener las rectas en hough (por ejemplo, todos los ángulos con 1º de diferencia entre 0º y 179º). En otros trozos de código que se requieren varias veces el seno y el coseno de otros ángulos se precalculan una vez y se guardan para futuros usos (por ejemplo en el código de desplazamiento por las rectas para encontrar puntos con los que generar el plano) • Uso de operaciones de copia rápidos para la CPU. Por ejemplo memcpy o memset para las matrices e imágenes. Estas funciones están optimizadas a nivel de ensamblador para ser realmente rápidas. • Aceleración por GPU para dibujar. Al utilizar la librería OpenGL estamos aprovechando el poder del procesador gráfico paralelamente con la CPU para todos los cálculos de dibujo de modelos y texturas en el mundo virtual tridimensional que se proyecta sobre la pantalla para dar la sensación de que los modelos son 3D. • Evitar pasadas por las imágenes innecesarias. Como se ha visto en el punto de preprocesado, se ha evitado a toda costa realizar pasadas por las matrices de manera innecesaria a costa de un filtrado menos correcto siempre que ha sido posible y los resultados eran aceptables. • Uso de filtros rápidos antes que los filtros más costosos. De esta forma quitamos primero los elementos sencillos (por ejemplo, filtrado por nivel mínimo antes de filtrar por área o masa del objeto de interés) dejando menos elementos a analizar con los algoritmos más costosos. • Programación de bajo nivel en C. Al tratarse de un lenguaje tan lineal y de bajo nivel, el código compilado es mucho menor que si se usasen otros lenguajes como java o Matlab. • Utilización inteligente de las variables. No se utilizan por ejemplo variables de coma flotante cuando pueden ser utilizadas las enteras. Por ejemplo, al calcular los senos y cosenos de los ángulos, en vez de guardar el valor en coma flotante (entre 0.0 y 1.0) se guarda el valor multiplicado por 1000 y se elimina la parte decimal. A la hora de operar se divide al final por 1000 como un valor entero siempre realizando las operaciones muy rápidamente. El único momento en el que se realizan de manera lenta es al crear la lista de senos y cosenos precalculados, pero esto se realiza únicamente al arrancar el programa. Alfonso Arbona Gimeno Visión Artificial - ETSII - UPV 6.- Conclusiones Hemos visto una forma de aplicar los algoritmos que comúnmente se utilizan en imágenes de colores en los mapas de profundidad capturados por una cámara Kinect. Dichos algoritmos han sido cambiados y ajustados para esta nueva situación, y utilizados para obtener varios puntos de interés sobre los que hemos realizado una des-proyección para calcular las coordenadas en la escena real en las que dichos puntos estaban situados. Con esta información hemos sido capaz de introducir un elemento de manera virtual que no existía a priori en la escena real, de tal forma que parece que el objeto estaba realmente situado allí. Todo esto, además, se hace sin tener que introducir elementos conocidos en la escena como marcadores, y se ejecuta en tiempo real a una velocidad más que aceptable en un ordenador de gama media con poco más de 3600 líneas de código y el uso únicamente de software libre multiplataforma. 7.- Bibliografía y enlaces de interés http://openkinect.org/wiki/Main_Page – Código fuente y página web principal de la librería libfreenect, contiene información interesante sobre el uso de la librería. http://openkinect.org/wiki/Getting_Started#Ubuntu.2FDebian – Instalación de la librería en entornos linux y solución al problema de permisos de root. https://groups.google.com/forum/#!topic/openkinect/gTyZjqqGay4 – Discusión en la lista de correo oficial de libfreenect sobre el formato de los datos capturados por la cámara. http://wiki.ros.org/kinect_node – Información por parte de la librería ROS (para proyectos de robótica) sobre el uso de la kinect mediante libfreenect. Contiene mucha información importante sobre el funcionamiento interno de la cámara y cálculos útiles. http://docs.opencv.org/modules/calib3d/doc/camera_calibration_and_3d_reconstruction.html? highlight=findhomography – Cálculos matemáticos para la reconstrucción del espacio 3D y la calibración de la cámara (matrices de proyección y otras propiedades interesantes). https://en.wikipedia.org/wiki/Flood_fill – Pseudocódigo utilizado para la detección de elementos independientes en las nubes generadas por la transformada de Hough. http://lists.libsdl.org/pipermail/sdl-libsdl.org/1999-November/003589.html – Código utilizado como base para dibujar rectas dados 2 puntos en una superficie SDL. https://en.wikipedia.org/wiki/Hough_transform – Información y pseudocódigo de la transformada de Hough. http://www.gamedev.net/topic/184477-sdl_surface-to-opengl-texture/ - Código de ejemplo utilizado como base para pasar de una superficie SDL a una de OpenGL. Aunque no funcionaba correctamente, gran parte del código de esta función ha sido extraído de ese hilo. http://www.nakerium.com/wiki/tiki-read_article.php?articleId=1 – Artículo escrito por mí en el 2010 del que he sacado gran parte del código base del proyecto. https://libsdl.org/ y https://www.opengl.org/ - Descargas y documentación de ambas librerías. 21