Este sitio utiliza cookies de Google para prestar sus servicios y analizar su tráfico. Tu dirección IP y user-agent se comparten con Google, junto con las métricas de rendimiento y de seguridad, para garantizar la calidad del servicio, generar estadísticas de uso y detectar y solucionar abusos.Más información

View site in english Ir a la página de inicio Contacta conmigo
domingo, 13 de mayo de 2018

Reconocimiento de posturas con Kinect III

En el artículo anterior de la serie mostré las estructuras, enumeraciones y clases que utiliza la aplicación para ser independiente de la versión del sensor y el SDK de Kinect. En este tercer artículo voy a mostrar una posible implementación de una clase que se ocupa de leer y convertir los esqueletos usando la versión 2.0 del SDK, para el sensor de Xbox One.

En este enlace puedes acceder al primer artículo de esta serie. En este otro puedes descargar el código fuente de la solución KinectGestures, escrita en csharp con Visual Studio 2015.

El nombre del proyecto es KTSensor20, que genera un ensamblado llamado KTSensor.dll. En este proyecto solamente existe una clase, la clase Sensor, que implementa la captura y conversión de la clase Body, propia del SDK de Kinect en la clase BodyVector, propia de la aplicación.

La idea es que, en caso de querer hacer una aplicación que pueda trabajar indistintamente con varias versiones del sensor, solamente sea necesario modificar esta clase Sensor, generando siempre un ensamblado de nombre KTSensor.dll. La aplicación funcionaría con cualquier versión de sensor simplemente sustituyendo esta librería de clases por la versión adecuada.

El interfaz ISensor

En el proyecto KinectInterface se define el interfaz ISensor, en el espacio de nombres KinectInterface.Interfaces, que deben implementar todas las clases que encapsulen la lectura del sensor Kinect.

public class KinectSensorEventArgs
{
public KinectSensorEventArgs(bool av)
{
IsAvailable = av;
}
public bool IsAvailable { get; private set; }
}
public delegate void KinectSensorEventHandler(object sender, KinectSensorEventArgs e);
public interface ISensor
{
event KinectSensorEventHandler AvailableChanged;
bool IsOpen { get; }
BodyVector NextBody { get; }
void Start();
void Stop();
}

Este interfaz define un evento AvailableChanged para avisar a la aplicación cuando la disponibilidad del sensor cambia.

La propiedad IsOpen se utiliza para comprobar cuándo está activo o inactivo el sensor.

Con la propiedad NextBody se obtiene el siguiente esqueleto leído desde el sensor, como un objeto de la clase BodyVector.

Los dos métodos Start y Stop se utilizan para poner en marcha el sensor o detener la captura.

La clase Sensor

Esta clase implementa el interfaz ISensor. Para aislar por completo esta librería de clases de la aplicación, en lugar de poner una referencia al ensamblado en la aplicación principal, se han implementado en el proyecto KTSensor20 dos eventos posteriores a la compilación para copiar la librería de clases generada, además de la librería de clases Microsoft.Kinect.dll, necesaria para el control del sensor. La versión de esta última librería dependerá de la versión del SDK que estemos utilizando.

copy $(TargetPath) $(SolutionDir)KinectGestures\$(OutDir)
copy $(TargetDir)Microsoft.Kinect.dll $(SolutionDir)KinectGestures\$(OutDir)

En el constructor de la clase, obtenemos una instancia de la clase KinectSensor en la variable _kSensor y suscribimos el evento IsAvailableChanged, que informa cuándo el sensor está disponible para ser utilizado o deja de estarlo.

public Sensor()
{
_kSensor = KinectSensor.GetDefault();
_kSensor.IsAvailableChanged +=
new EventHandler<IsAvailableChangedEventArgs>(
Sensor_IsAvailableChanged);
}

La clase Sensor expone a su vez el evento AvailableChanged para recibir estas mismas notificaciones.

Para poner en marcha el sensor hay que llamar al método Start:

public void Start()
{
if (!_kSensor.IsOpen)
{
_bodies.Clear();
_bodiesV.Clear();
_kSensor.Open();
_bReader = _kSensor.BodyFrameSource.OpenReader();
_bReader.FrameArrived +=
new EventHandler<BodyFrameArrivedEventArgs>(
Body_FrameArrived);
_cancel = new CancellationTokenSource();
_convertTask = Task.Run(() =>
{ ConvertBodies(); }, _cancel.Token);
}
}

Esta clase utiliza dos procesos diferentes. La captura de los esqueletos desde el sensor se realiza mediante un objeto BodyFrameReader y la suscripción al evento FrameArrived, que se dispara cada vez que el sensor tiene un nuevo objeto de tipo Body disponible. Para la conversión de estos objetos Body en los objetos genéricos BodyVector, se lanza otra tarea mediante la clase Task, a la que se asigna un token de cancelación para que podamos detenerla.

Los objetos Body leídos se almacenan en un objeto de tipo Queue<BodyTM>, siendo BodyTM una estructura que contiene el objeto Body leído y un objeto DateTime con la fecha y la hora de la lectura. La tarea que realiza la conversión de los objetos Body en objetos BodyVector consume los elementos de esta cola y almacena los objetos BodyVector resultantes en otro objeto Queue<BodyVector> para que sean consumidos a su vez por la aplicación, mediante la propiedad NextBody.

public BodyVector NextBody
{
get
{
BodyVector bv = null;
if (_bodiesV.Count > 0)
{
lock (_vLock)
{
bv = _bodiesV.Dequeue();
}
}
return bv;
}
}

Utilizamos la instrucción lock para evitar que la aplicación y la tarea de conversión accedan simultáneamente a la cola que contiene los cuerpos. Hay que tener presente que esta propiedad puede devolver null en caso de no haber ningún cuerpo disponible.

Para detener el sensor y la captura se utiliza el método Stop:

public void Stop()
{
if (_kSensor.IsOpen)
{
_bReader.FrameArrived -=
new EventHandler<BodyFrameArrivedEventArgs>(Body_FrameArrived);
_bReader = null;
_kSensor.Close();
try
{
_cancel.Cancel();
Task.WaitAll(new Task[1] { _convertTask });
}
catch
{
}
finally
{
_convertTask.Dispose();
_cancel.Dispose();
_convertTask = null;
_cancel = null;
}
}
}

Este método simplemente detiene el sensor y cancela la tarea de conversión de esqueletos.

El controlador del evento FrameArrived está implementado de la siguiente manera:

private void Body_FrameArrived(object sender,
BodyFrameArrivedEventArgs e)
{
try
{
using (BodyFrame bf = e.FrameReference.AcquireFrame())
{
if (bf != null)
{
Body[] bodies =
new Body[_kSensor.BodyFrameSource.BodyCount];
bf.GetAndRefreshBodyData(bodies);
foreach (Body b in bodies)
{
if (b.IsTracked)
{
lock (_bLock)
{
_bodies.Enqueue(
new BodyTM(b, DateTime.Now));
}
break;
}
}
}
}
}
catch
{
}
}

En primer lugar se obtiene un objeto BodyFrame usando el método AcquireFrame. Este objeto contiene todos los cuerpos capturados por el sensor, que con esta versión del SDK pueden ser hasta 6. Esta cuenta la obtenemos de la propiedad BodyCount del objeto BodyFrameSource. Estos cuerpos los obtenemos mediante el método GetAndRefreshBodyData del objeto BodyFrame, en el array bodies. Aunque podemos tener hasta 6 cuerpos, solamente dos de ellos como máximo pueden estar completos. El resto solamente estarían definidos por un único punto que representa más o menos el centro del cuerpo. Para determinar esto, se utiliza la propiedad IsTracked del objeto Body, que solo valdrá true para los esqueletos completos. En este caso nos quedamos con el primero que encontremos.

Por último, este cuerpo se encapsula en una estructura BodyTM junto con la fecha y hora actuales y se encola en la cola correspondiente.

En cuanto a la conversión del objeto Body a un objeto BodyVector, de esto se encarga el método ConvertBodies:

private void ConvertBodies()
{
while (!_cancel.Token.IsCancellationRequested)
{
if (_bodies.Count > 0)
{
BodyTM b;
lock (_bLock)
{
b = _bodies.Dequeue();
}
BodyPoint[] vector = new BodyPoint[25];
vector[(int)KinectInterface.Joint.Head] =
TranslateBodyJoint(JointType.Head, b.Body);
vector[(int)KinectInterface.Joint.Neck] =
TranslateBodyJoint(JointType.Neck, b.Body);
vector[(int)KinectInterface.Joint.SpineShoulder] =
TranslateBodyJoint(JointType.SpineShoulder, b.Body);
...
vector[(int)KinectInterface.Joint.AnkleRight] =
TranslateBodyJoint(JointType.AnkleRight, b.Body);
vector[(int)KinectInterface.Joint.FootRight] =
TranslateBodyJoint(JointType.FootRight, b.Body);
BodyVector bv = new BodyVector(vector,
HState(b.Body.HandLeftState,
b.Body.HandLeftConfidence),
HState(b.Body.HandRightState,
b.Body.HandRightConfidence),
new ClippedEdges(
b.Body.ClippedEdges.HasFlag(FrameEdges.Top),
b.Body.ClippedEdges.HasFlag(FrameEdges.Right),
b.Body.ClippedEdges.HasFlag(FrameEdges.Bottom),
b.Body.ClippedEdges.HasFlag(FrameEdges.Left)),
new SizeF(_kSensor.DepthFrameSource.FrameDescription.Width,
_kSensor.DepthFrameSource.FrameDescription.Height),
b.Time);
lock (_vLock)
{
_bodiesV.Enqueue(bv);
}
}
}
}

En primer lugar, obtenemos el siguiente Body de la cola correspondiente y construimos un array de estructuras BodyPoint a partir de los objetos BodyJoint que representan las articulaciones en la forma proporcionada por el SDK. Para esto se utiliza el método TranslateBodyJoint:

private BodyPoint TranslateBodyJoint(JointType bj, Body b)
{
BodyPoint v;
CameraSpacePoint position = b.Joints[bj].Position;
if (position.Z < 0)
{
position.Z = 0.1f;
}
DepthSpacePoint dp =
_kSensor.CoordinateMapper.MapCameraPointToDepthSpace(position);
if (!(float.IsInfinity(dp.X) || float.IsInfinity(dp.Y)))
{
v = new BodyPoint(new Vector3D(position.X, position.Y, position.Z),
new System.Drawing.PointF(dp.X, dp.Y),
TrackingAccuracy(b.Joints[bj].TrackingState));
}
else
{
v = new BodyPoint(new Vector3D(position.X, position.Y, position.Z),
TrackingAccuracy(b.Joints[bj].TrackingState));
}
return v;
}

Con el método MapCameraPointToDepthSpace se obtiene un punto que representa la proyección de la articulación en dos dimensiones. Este será el punto que utilicemos para dibujar el esqueleto en la aplicación. La estructura BodyPoint la construimos usando un Vector3D con las coordenadas en tres dimensiones proporcionadas por el sensor, la proyección en dos dimensiones en forma de PointF, y la precisión de la medida obtenida con el método TrackingAccuracy. Este método devolverá -1 si no se ha podido determinar la posición de la articulación, 0 si el valor es inferido o 1 si se trata de una medida exacta.

El objeto BodyVector que genera el método ConvertBodies se construye con este array de articulaciones BodyPoint, el estado de las dos manos, obtenido mediante el método HState, una estructura ClippedEdges que nos indicará si el esqueleto se está saliendo del encuadre del sensor por alguna dirección, y la fecha y hora en la que ha sido capturado el cuerpo, para poder procesar movimientos compuestos además de posiciones. Por último, el BodyVector se pone en la cola correspondiente para ser consumido por la aplicación.

Esto es todo con respecto a la implementación de la lectura del sensor Kinect. En el próximo artículo mostraré las diferentes clases dedicadas a la normalización de las distintas partes del cuerpo.

Comparte este artículo: Compartir en Twitter Compártelo en Facebook Compartir en Google Plus Compartir en LinkedIn
Comentarios (0):
* (Su comentario será publicado después de la revisión)

E-Mail


Nombre


Web


Mensaje


CAPTCHA
Change the CAPTCHA codeSpeak the CAPTCHA code