Vigilancia casera con Kinect
En artículos anteriores presenté un sistema casero de videovigilancia, la aplicación ThiefWatcher. Se trata de una aplicación extensible que trabaja combinando varios protocolos, como cámaras, disparadores, canales de comunicación y sistemas de almacenamiento. En este artículo mostraré como implementar algunos de estos protocolos usando el sensor Kinect de Microsoft.
Este es el enlace al primer artículo de la serie sobre el sistema de vigilancia ThiefWatcher, donde podrás descargar esta aplicación y su código fuente. Parte de ese código es necesario para compilar el proyecto que acompaña a este artículo, en concreto, el proyecto WatcherCommons.
En este otro enlace puedes descargar el código fuente de la solución KinectProtocol, o, si lo prefieres, puedes descargar los archivos ejecutables de KinectProtocol ya generados. En el primer caso puedes añadir el proyecto a la solución ThiefWatcher, en el segundo caso, simplemente descomprime los archivos en el directorio donde tengas el ejecutable de la aplicación. Esta aplicación está escrita en C# usando Visual Studio 2015.
También necesitaréis tener instalado el SDK de Kinect. La versión dependerá del modelo de sensor que tengáis, en mi implementación he utilizado la versión 2.0, que se corresponde con el sensor Kinect para Xbox One. En caso de que tengáis el modelo para Xbox 360, tendréis que usar una versión anterior, y tendréis que modificar la clase Sensor del proyecto, para adaptarla a dicha versión.
El sensor Kinect es muy apropiado para esta aplicación, ya que no solo proporciona una cámara en color y otra infrarroja, sino dos medios diferentes para detectar intrusos, el sensor de distancias y la detección de cuerpos de personas. Por ello, en el proyecto se implementan dos protocolos diferentes, el de cámara, KinectCamera, y el de disparador, KinectTrigger.
La instalación de los protocolos en el programa ThiefWatcher es muy sencilla. Una vez hayáis copiado las librerías en el directorio con los ejecutables de la aplicación, simplemente usad la opción Instalar protocolo del menú Archivo y seleccionad la librería de clases KinectProtocol.dll. Para crear una nueva cámara que use el sensor Kinect, una vez instalado el protocolo, usad la opción Nueva cámara del menú Archivo y seleccionad Kinect Camera. Esta cámara no necesita ningún usuario, contraseña o url, por lo que podéis dejar estos valores en blanco. La única configuración que admite es la selección de modo infrarrojo o color. Podéis tener una cámara de cada tipo al mismo tiempo, ya que en el sensor funcionan ambas al mismo tiempo. Si queréis instrucciones más completas os recomiendo visitar los artículos sobre la aplicación enlazados anteriormente.
En cuanto al protocolo disparador de alarma, KinectTrigger, se configura en el panel de control de la aplicación. Como protocolo de alarma hay que seleccionar Kinect Trigger y proporcionar la cadena de conexión apropiada.
Existen dos modos de funcionamiento para disparar la alarma, detección de cuerpos, que solo detectará personas, pero es muy conveniente si tenéis animales en casa que puedan disparar sensores de proximidad, por ejemplo. En este caso deberemos indicar en la cadena de conexión source=body, no son necesarios más parámetros de configuración.
El otro modo utiliza el sensor de profundidad de la cámara, indicando source=depth. Este sensor asigna a cada píxel de la imagen la distancia en milímetros entre ese punto y la cámara. En este caso, lo que haremos será comparar cada imagen con la anterior, mediante una simple resta, y contabilizaremos los puntos cuya distancia haya cambiado más de un determinado umbral. Si esta cuenta alcanza un determinado porcentaje de los puntos de la imagen, haremos saltar la alarma. Este umbral se configura en la cadena de conexión con el parámetro thres, por ejemplo, para poner un umbral de 100, usaremos thres=100. El porcentaje de puntos se indica con el parámetro sens. Por ejemplo source=depth;thres=100;sens=15 configura un umbral de 100 y un porcentaje del 15% de los puntos.
La clase Sensor
Esta clase encapsula toda la funcionalidad que tiene que ver con el sensor Kinect, de manera que es la única que hay que adaptar para usar una versión diferente del SDK. Se trata de una clase static, por lo que no hay que instanciarla, y solo sirve para utilizar un sensor, aunque en teoría podríamos tener varios.
La enumeración SensorCaps se utiliza para indicar el tipo de entradas que deben procesarse, para optimizar el uso:
[Flags]
public enum SensorCaps
{
None = 0,
Color = 1,
Infrared = 2,
Depth = 4,
Body = 8
}
En la propiedad Sensor.Caps podemos indicar la combinación adecuada de estos valores.
Para las imágenes en color, tenemos dos propiedades, Sensor.ColorFrameSize, de solo lectura, que nos devuelve el tamaño de la imagen en una estructura Size, y la propiedad Sensor.ColorFrame, que devuelve un objeto Bitmap con la última imagen capturada.
Las imágenes infrarrojas se manejan con dos propiedades homólogas: Sensor.InfraredFrameSize y Sensor.InfraredFrame.
La detección de cuerpos se realiza a través de la propiedad booleana Sensor.BodyTracked, y los datos del sensor de profundidad se obtienen, en forma de un array de enteros cortos sin signo, de la propiedad Sensor.DepthFrame.
El sensor se pone en funcionamiento con el método OpenSensor, que recibe un parámetro de tipo SensorCaps. Como este método es llamado por cada cámara y por el protocolo de alarma y solo es necesario iniciar el sensor una vez, llevamos una cuenta de instancias que han llamado al método en la variable global _instances:
public static void OpenSensor(SensorCaps caps)
{
Caps |= caps;
if (_sensor == null)
{
_instances = 1;
_sensor = KinectSensor.GetDefault();
if (!_sensor.IsOpen)
{
_sensor.Open();
}
Initialize();
_reader = _sensor.OpenMultiSourceFrameReader(FrameSourceTypes.Color
| FrameSourceTypes.Depth
| FrameSourceTypes.Infrared
| FrameSourceTypes.Body);
_reader.MultiSourceFrameArrived +=
new EventHandler<MultiSourceFrameArrivedEventArgs>(OnNewFrame);
}
else
{
_instances++;
}
}
La variable global _sensor, de tipo KinectSensor, valdrá null la primera vez que llamemos al método. En este caso también creamos un lector de frames de tipo MultiSourceFrameReader, en la variable _reader, que nos permite configurar varios tipos de datos para leer al mismo tiempo. En este caso siempre leeremos las imágenes en color, infrarrojas, de profundidad y la lista de cuerpos detectados.
La forma de leerlos será mediante el evento MultiSourceFrameArrived, que se disparará cada vez que haya una nueva imagen disponible.
El método para detener una instancia abierta con OpenSensor es CloseSensor, que irá reduciendo la cuenta de instancias hasta llegar a la última, dejando en este caso el sistema en su estado inicial y liberando recursos:
public static void CloseSensor()
{
_instances--;
if (_instances <= 0)
{
if (_sensor != null)
{
if (_sensor.IsOpen)
{
_sensor.Close();
_reader.Dispose();
_reader = null;
_sensor = null;
}
}
_instances = 0;
Caps = SensorCaps.None;
}
}
Clases KinectCamera y KinectTrigger
Estas clases implementan los interfaces propios de la aplicación ThiefWatcher que las convierten en un protocolo de cámara, IWatcherCamera y en uno de alarma, ITrigger. Para una explicación sobre estos interfaces os remito al artículo sobre los detalles de la configuración de cada uno de los protocolos y de las cámaras, de la serie de artículos original.
La tarea que se encarga de leer las cámaras está implementada de la siguiente manera:
private void CaptureThread(object interval)
{
while (_bCapturing)
{
try
{
DateTime tm = DateTime.Now;
Bitmap bmp = _nResolution == 0 ?
Sensor.ColorFrame :
Sensor.InfraredFrame;
if (bmp != null)
{
OnNewFrame?.Invoke(this, new FrameEventArgs(bmp));
TimeSpan ts;
do
{
ts = DateTime.Now.Subtract(tm);
} while (((ts.Seconds * 1000) +
ts.Milliseconds) <= (int)interval);
}
}
catch
{
}
}
}
Podéis ver que el uso de la clase Sensor es trivial. La implementación de la tarea de control de la alarma también es muy sencilla:
private void TriggerTest()
{
while (_bRunning && (OnTriggerFired != null))
{
try
{
if (_bSource)
{
ushort[] frame = Sensor.DepthFrame;
if (frame != null)
{
if (_oldFrame != null)
{
int max = (_sens * frame.Length) / 100;
if (max > 0)
{
int cnt = 0;
for (int ix = 0; ix < frame.Length; ix++)
{
if (Math.Abs(frame[ix] - _oldFrame[ix]) >
_threshold)
{
cnt++;
if (cnt >= max)
{
OnTriggerFired?.Invoke(this,
EventArgs.Empty);
Thread.Sleep(2000);
break;
}
}
}
}
}
_oldFrame = frame;
}
}
else
{
if (Sensor.BodyTracked)
{
OnTriggerFired?.Invoke(this, EventArgs.Empty);
Thread.Sleep(2000);
}
}
}
catch
{
}
}
}
Donde _bSource indica el modo de funcionamiento.