Leap Motion (III): analizamos la API

Con esta entrada cerramos el análisis de Leap Motion que hemos estado haciendo en este blog. Primero te explicamos las características técnicas del dispositivo y, después, su principio de funcionamiento. Hoy vamos a hablar de la parte que más información nos dan en la página oficial, la SDK (versión 2.2.5.26752). Para ello vamos a ver sus partes fundamentales e intentaremos analizarlas ayudándonos de figuras.

Nosotros vamos a analizar la API desde el punto de vista del lenguaje de programación Java, pero debes saber que Leap Motion se puede programar en los siguientes lenguajes de programación y plataformas de desarrollo:

IMG1

Desde la API se puede obtener todo tipo de información tridimensional referente a antebrazos, manos, herramientas, dedos e incluso a los “huesos” de los dedos que se tratan como objetos. Además, desde la versión 2.1. se puede acceder a la información de las imágenes en formato RAW que recoge el dispositivo.

Visión general

El dispositivo utiliza el sistema de coordenadas cartesianas, en donde, desde la posición del usuario, el eje Y tiene valores positivos hacia arriba, el X hacia la derecha y el Z hacia el usuario.

IMG2

La API mide magnitudes físicas con las siguientes unidades:

IMG3

A continuación, vamos a analizar los componentes principales de esta versión. Para ello iremos viendo los objetos principales que el dispositivo puede rastrear, junto con el modelo anatómico que se usa para identificar cada uno, y también analizaremos las clases principales que se describen en la API.

Componentes principales

El principal objeto es Controller(), que se encarga de hacer de interfaz entre la aplicación que desarrollemos y el dispositivo. A continuación podemos ver los métodos que contiene:

IMG4

El que más nos interesa de esta clase es el método frame(), a través del cual podemos acceder al objeto Frame que deseemos; por defecto, al último que ha llegado desde el dispositivo. Además, como se puede observar en uno de los métodos constructores, se puede asociar un objeto de tipo Listener al objeto Controller para poder estar pendientes de los eventos que se produzcan en dicho Controller.

En la siguiente imagen vemos cuáles son los eventos que se producen en el Controller y pueden ser atendidos por el objeto Listener:

IMG5

Como podemos ver, hay diferentes eventos asociados al estado del objeto Controller que al final se traducen en eventos asociados al dispositivo hardware.

Otro método interesante —al igual que en el caso de Controller— es el onFrame(), evento que se produce cada vez que el dispositivo captura una imagen. Este es el evento principal, ya que dentro de este se pueden implementar todas las acciones que queremos hacer sobre los objetos que más adelante veremos (manos, dedos, gestos, etc.).

Así pues, hay dos modos de acceder a un objeto Frame:

  1. Desde el objeto Controller, a través del metodo frame()
  2. Desde el objeto Listener, a través de los eventos que se van disparando

Usar un modo u otro dependerá de si queremos analizar o no todos los frames que lleguen desde Controller. Si queremos analizarlos todos, accederemos a la información del objeto Frame a través de Listener; pero si, por el contrario, queremos analizar un objeto Frame cada cierto tiempo, se accederíamos desde el objeto Controller.

A continuación, podemos ver un diagrama de la relación entre el objeto Frame y los que se puede acceder a través de él:

IMG6

Como vemos, el objeto Frame es la raíz de todos los datos de los objetos que analiza Leap Motion. Frame nos da la posibilidad de acceder a todas las clases List, a partir de las cuales se puede acceder a una lista de objetos del mismo tipo y que aparecen en cada Frame.

La parte que nos interesa en ShowLeap es la que depende de HandList. Esta clase es una lista de objetos de tipo Hand que, a su vez, contiene objetos de tipo Arm, Finger, Pointable y Tool para cada objeto Hand.

Dentro del objeto Hand, podemos acceder a una lista de objetos Finger para cada Hand, en la que podemos acceder a información de cada objeto Finger que aparece en el frame y que forma parte del objeto Hand. A su vez, cada objeto de tipo Finger contiene objetos de tipo Bone. Además, como podemos observar, los objetos Finger y Bone son de tipo Pointable.

A continuación podemos ver un esquema de las partes que componen el objeto Hand con su representación anatómica:

IMG7

A continuación podemos ver la información principal que se puede extraer de cada objeto que depende de Hand:

Información principal incluida en el objeto Hand

  • posición y velocidad de la palma
  • dirección y vectores normales
  • base ortonormal

Información incluida en el objeto Finger

  • posición y velocidad de la punta
  • vector de dirección
  • base ortonormal
  • anchura y longitud

Información incluida en el objeto Bone

  • posición de la articulación
  • base ortonormal
  • anchura y longitud

Información incluida en el objeto Arm

  • posiciones de muñeca y codo
  • vector de dirección
  • base ortonormal
  • anchura y longitud

Información incluida en el objeto Pointable Tool

  • posición y velocidad de la punta
  • vector de dirección
  • base ortonormal
  • anchura y longitud

La siguiente clase a analizar es HandList, que es la encargada de almacenar los objetos Hand:

IMG8

Como se puede observar, a través de esta clase se puede extraer un objeto de tipo Hand mediante el método get(). Además, se puede saber cual es el objeto Hand que está más cerca del dispositivo, más a la izquierda o más a la derecha.

Como ya se ha dicho, HandList provee una lista de objetos Hand que aparecen en el Frame que se está analizando. Vamos a ver el objeto Hand:

IMG9

Como podemos ver, a través de esta clase se puede extraer muchísima información referente al objeto Hand. También se puede acceder a los objetos Finger y Arm que dependen de cada objeto Hand en concreto. Es decir, podemos extraer objetos de tipo dedo y antebrazo de cada mano que aparece en la imagen.

Este objeto puede extraer información basándose en el modelo anatómico de la mano, que fue mejorado en la versión 2.0 y ahora se basa en el esqueleto:

IMG10

Como podemos ver, ahora el modelo de la mano se basa en una estructura de puntos y líneas que describen mejor a un objeto de tipo mano. Con este nuevo modelo aparece el objeto Bones, que ha ayudado a que el dispositivo tenga mejor precisión a la hora de determinar la posición y el gesto de la mano, ya que gracias a la definición de estos puntos si el dispositivo no ve alguno de ellos puede determinar por ejemplo donde está el dedo que no ve o en qué posición está la mano.

En la API también se puede acceder a la información de cada dedo. Para ello se hace uso del objeto Finger, objeto que hay que extraer de la clase FingerList:

IMG11

Como vemos, esta clase es muy parecida a HandList, ya que es una clase que contiene objetos de tipo Finger. Además, también se puede extraer información del objeto Finger más cercano al dispositivo, ya sea a la derecha o a la izquierda. Por lo tanto, dentro de la clase FingerList aparecen los objetos Finger:

IMG12

Este objeto, en el modelo, equivaldría a la siguiente figura:

IMG9 IMG13

Como se puede ver, también se pueden extraer de cada objeto Finger diferentes objetos Bone. Estos objetos representan los puntos que definen al dedo en el modelo y que, en la anatomía definida en la API, estarían representados de la siguiente manera:

IMG14

Como vemos, cada objeto Bone puede ser de un tipo que será el que determine su identificador.

A continuación podemos ver mejor como es interpretado esto por la API:

IMG15

A continuación, podemos ver el objeto Bone:

IMG16

Como se puede observar en el diagrama de la clase, se puede extraer bastante información de cada objeto de tipo Bone. Gracias a este nuevo modelo matemático, se puede determinar mejor la posición de cada objeto y las direcciones, y esto dio lugar a la aparición de un nuevo objeto, el objeto Arm:

IMG17

Además, la API también nos provee de una serie de objetos que ayudan a poder extraer los datos que necesitemos, como por ejemplo el objeto Vector.

Conclusión

Como vemos, el dispositivo Leap Motion cuenta con una API muy extensa y optimizada para la extracción de datos característicos de las partes que forman una mano. Por lo tanto, es un dispositivo óptimo para nuestro proyecto, en el que vamos a analizar el comportamiento de las manos al realizar diferentes signos.

Leap Motion (II): principio de funcionamiento

¿Recordáis que en la entrada anterior vimos las partes de las que consta el sensor Leap Motion? Pues hoy nos toca analizar su funcionamiento. ¡Allá vamos!

Una de las razones por las que hemos elegido Leap Motion como sensor para nuestro traductor de lengua de signos es porque, para identificar correctamente los gestos, necesitamos obtener la máxima información posible de la configuración y del movimiento realizado por las manos. Tener información tridimensional es imprescindible para nuestro proyecto.

Existen diferentes técnicas para obtener parámetros de profundidad mediante un sistema de visión (por ejemplo, Kinect obtiene imágenes a color e imágenes de profundidad proyectando un mallado de puntos de los que analiza la distancia a partir de la distorsión del mallado). Con esta información se pueden obtener variables suficientes para poder determinar un gesto realizado. Nosotros vamos a centrarnos en el funcionamiento de Leap Motion; como siempre, ¡paso a paso!

Cómo funciona el controlador Leap Motion

El dispositivo ilumina la zona de cobertura mediante una luz infrarroja emitida a través de sus tres LEDs, con una longitud de onda de 850 nm. Esta zona de cobertura está limitada por el ángulo de visión de los sensores (esto también lo vimos en la entrada anterior) y por la corriente máxima que puede entregar la conexión USB.

IMG 1-Blog de ShowLeap-Leap Motion-Principio de funcionamientoCuando un objeto —en nuestro caso, las manos— es iluminado, se produce una reflexión de luz que llega al dispositivo e incide sobre las lentes de las dos cámaras. Estas lentes, de tipo biconvexas, concentran los rayos en el sensor de cada cámara; y los datos recogidos por los sensores se almacenan en una matriz (imagen digitalizada) en la memoria del controlador USB, en donde se realizan los ajustes de resolución adecuados mediante el microcontrolador del dispositivo.

Una vez ajustada la resolución, los datos de los sensores se envían directamente al driver instalado en el ordenador. Estos datos representan un valor de intensidad luminosa por cada píxel de la imagen capturada y se guardan en un buffer. El valor de intensidad luminosa se cuantifica a 8 bits para generar una imagen RAW en escala de grises —por tanto, hay un total de 256 posibles valores de luminosidad. Y nos falta otro dato: cada imagen tiene un tamaño de 640 x 120 px, con lo que en total hay 76.800 píxeles por imagen. ¿Y aún así es rápido el dispositivo? Pues si, porque las imágenes no son tratadas en el propio dispositivo; ¡este solo recoge y envía datos! 😉

Una vez que las imágenes de las dos cámaras llegan al driver son analizadas para identificar las manos y los dedos a partir de un modelo matemático de caracterización anatómico. Además, se obtiene la profundidad mediante un algoritmo que os explicamos  a continuación. Primero, mirad esto:

IMG 2-Blog de ShowLeap-Leap Motion-Principio de funcionamiento

Estas son las dos imágenes que llegan al driver del Leap Motion (se puede acceder a ellas a partir de la versión 2.1 de la API).

Pero antes de aplicar el algoritmo de identificación y el de profundidad hay que tener en cuenta que las lentes del dispositivo producen una distorsión en la imagen óptica , deformando el objeto observado. Vayamos por partes: ¿sabes que existen diferentes tipos de distorsión, dependiendo del tipo de lente que se use?

IMG 3-Blog de ShowLeap-Leap Motion-Principio de funcionamiento

Pues en Leap Motion se produce lo que conocemos como distorsión compleja: una mezcla entre la distorsión barril y la distorsión cojín.

IMG 4-Blog de ShowLeap-Leap Motion-Principio de funcionamiento

Para mejorar esta distorsión, Leap Motion tiene una opción de calibrado mediante la cual se obtiene un mapa de mallado de puntos de calibrado que se superpone a la imagen captada por cada sensor.

IMG 5-Blog de ShowLeap-Leap Motion-Principio de funcionamiento

Por lo tanto, cada buffer de datos de imagen que se envía al driver va acompañado de otro buffer que contiene los datos de distorsión. Estos datos son una rejilla de 64 x 64 puntos con dos valores de 32 bits cada uno. Cada uno de estos puntos representa un rayo proyectado en la cámara. El valor de un punto del mallado define la luminosidad de un píxel en la imagen y se pueden obtener los datos de luminosidad de todos los píxeles mediante interpolación.

Así pues, se puede obtener el valor de brillo para cualquier rayo proyectado. Los valores de la cuadrícula que caen fuera del rango [0…1] no corresponden a un valor de datos de la imagen y, por tanto, debemos ignorar estos puntos. Vamos a tratar de que se entienda mejor con un ejemplo:

IMG 6-Blog de ShowLeap-Leap Motion-Principio de funcionamiento

La imagen nos muestra una reconstrucción de los datos de una imagen con la distorsión corregida. El valor de brillo de cada píxel de la imagen se originó a partir de un rayo de luz que entró en la cámara desde una dirección específica. La imagen se reconstruye mediante el cálculo de las pistas horizontales y verticales representados por cada píxel y se puede encontrar el valor de brillo verdadero de los datos de la imagen utilizando el mapa de calibración. Las partes rojas de la imagen representan las áreas dentro de la prestación para la que ningún valor de brillo está disponible (el campo de visión real es de menos de 150 grados).

Una vez que han llegado las imágenes, las hemos corregido debidamente y el driver ha identificado las manos y los dedos, podemos determinar la posición de estas en el sistema de coordenadas cartesianas de Leap Motion a través de técnicas de visión estereoscópica. Porque, como ya hemos visto, Leap Motion es un sistema de captación de imágenes basado en la visión binocular y por ello podemos obtener distancias.

Básicamente, un sistema estereoscópico funciona del siguiente modo: gracias a la separación de las dos cámaras en el eje X se obtienen dos imágenes con pequeñas diferencias (disparidad).

IMG 7-Blog de ShowLeap-Leap Motion-Principio de funcionamiento

Como se puede observar en la imagen anterior, las dos cámaras —representadas por Oi y Od— están en el mismo plano Z, sobre la línea base. Si trazamos una línea epipolar entre las dos imágenes Ii e Id, dado que Oi y Od están en el mismo plano Z y las dos cámaras tienen la misma distancia focal, podemos ver la proyección del punto P en las dos imágenes. Por tanto, se puede obtener un valor de disparidad para cada par de puntos emparejados Pi(xi,yi) y Pd(xd,yd) dado por d = XI-XD.

IMG 8-Blog de ShowLeap-Leap Motion-Principio de funcionamiento

Considerando igual la distancia focal f en las dos cámaras y conociendo la distancia entre cámaras b:

IMG 9-Blog de ShowLeap-Leap Motion-Principio de funcionamiento

Como vemos, a partir del sistema anterior podemos obtener las coordenadas del punto P.

Y ahora sin tantos tecnicismos… ¿Cómo funciona Leap Motion? 🙂

Para terminar este post te nombramos los pasos básicos que realiza Leap Motion para entregar variables descriptivas de una configuración de mano y de un gesto:

  1. Obtiene las imágenes desde los sensores de las cámaras del dispositivo.
  2. Aplica una corrección de la distorsión que producen los sensores.
  3. Aplica un modelo para determinar la configuración de cada mano y ejecuta un algoritmo de visión estereoscópica entre cada par de imágenes para obtener la posición en el plano tridimensional.

 

Créditos del post
Imágenes:
- Página oficial de Leap Motion
- TFM «Técnicas de visión estereoscópica para determinar la estructura tridimensional de la escena». Autor: Martín Montalvo Martínez / Director: Gonzalo Pajares Martinsanz