Universidad de Costa Rica Facultad de Ingeniería Escuela de Ingeniería Eléctrica IE – 0502 Proyecto Eléctrico Implementación de un sistema de captura de video y detección de movimiento utilizando el sistema Video4Linux Por: Andrés Díaz Soto Ciudad Universitaria Rodrigo Facio Junio del 2005 Implementación de un sistema de captura de video y detección de movimiento utilizando el sistema Video4Linux Por: Andrés Díaz Soto Sometido a la Escuela de Ingeniería Eléctrica de la Facultad de Ingeniería de la Universidad de Costa Rica como requisito parcial para optar por el grado de: BACHILLER EN INGENIERÍA ELÉCTRICA Aprobado por el tribunal: Ing. Federico Ruiz Ugalde Profesor Guía Ing. Francisco Siles Canales Profesor lector Ing. Enrique Coen Alfaro Profesor lector II DERECHOS DE PROPIEDAD INTELECTUAL Linux es una marca registrada de Linus Torvalds. Intel es una marca registrada de Intel Corporation. ImageMagick es una marca registrada de ImageMagick Studio LLC. USB (Universal Serial Bus Specification) es una marca registrada de Compaq Computer Corporation, Intel Corporation, NEC Corporation y otros. Las demás marcas registradas son propiedad de sus respectivos dueños. III DEDICATORIA A toda mi familia y a mis amigos, que han estado a mi lado a lo largo de estos años. IV ÍNDICE GENERAL ÍNDICE DE FIGURAS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . VIII ÍNDICE DE CUADROS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . IX NOMENCLATURA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . X RESUMEN . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . XII CAPÍTULO 1: Introducción . . . . . 1.1. Justificación . . . . . . . . . . 1.2. Objetivos . . . . . . . . . . . 1.2.1. Objetivo General . . . 1.2.2. Objetivos Específicos . 1.3. Metodología . . . . . . . . . . 1.4. Herramientas Utilizadas . . . . 1.4.1. Hardware . . . . . . . 1.4.2. Software . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1 2 2 2 2 3 3 3 CAPÍTULO 2: Desarrollo Teórico . . . . . . . . . . . . . . . 2.1. El sistema operativo GNU/Linux . . . . . . . . . . . 2.2. Acceso a dispositivos y funciones de entrada y salida 2.2.1. open() . . . . . . . . . . . . . . . . . . . . . 2.2.2. close() . . . . . . . . . . . . . . . . . . . . . 2.2.3. read() . . . . . . . . . . . . . . . . . . . . . 2.2.4. write() . . . . . . . . . . . . . . . . . . . . . 2.2.5. ioctl() . . . . . . . . . . . . . . . . . . . . . 2.3. Programación con múltiples hilos de procesamiento . 2.3.1. pthread_create() . . . . . . . . . . . . . . . 2.3.2. pthread_exit() . . . . . . . . . . . . . . . . . 2.3.3. pthread_join() . . . . . . . . . . . . . . . . . 2.3.4. pthead_mutex_init() . . . . . . . . . . . . . 2.3.5. pthread_mutex_lock() . . . . . . . . . . . . 2.3.6. pthread_mutex_unlock() . . . . . . . . . . . 2.3.7. pthread_mutex_destroy() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 5 6 6 7 7 8 8 8 9 10 10 10 11 11 11 V . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.4. El sistema Video4Linux . . . . . . . . . . . . . . . . 2.4.1. La interfaz de programación de Video4Linux 2.5. Algoritmos de detección de movimiento . . . . . . . 2.6. Reducción de ruido en las imágenes . . . . . . . . . 2.6.1. Filtro de media . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 12 16 18 18 CAPÍTULO 3: Descripción general del sistema de captura de video y detección de movimiento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.1. Estructura de la aplicación y aspectos generales . . . . . . . . . . . . . . . . 3.2. Estructura del servidor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2.1. Inicialización y comunicación con el cliente . . . . . . . . . . . . . . 3.2.2. Captura de video . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2.3. Detección de movimiento . . . . . . . . . . . . . . . . . . . . . . . 3.2.4. Envío del video . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.3. Estructura del cliente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.3.1. Interfaz gráfica de usuario y mensajería TCP . . . . . . . . . . . . . 3.3.2. Recepción de los cuadros de video . . . . . . . . . . . . . . . . . . . 20 20 21 21 22 23 23 25 26 26 CAPÍTULO 4: Rutinas de captura de video . . . . . . . . . . . . . . . . . . . . . . . 29 4.1. Inicialización del sistema de video . . . . . . . . . . . . . . . . . . . . . . . 29 4.2. Ciclo de captura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 CAPÍTULO 5: Rutinas de comunicación con el cliente y transmisión de video sobre la red . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.1. Creación del socket TCP en el servidor . . . . . . . . . . . . . . . . . . . . . 5.2. Envío y recepción de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.3. Envío del video capturado al cliente . . . . . . . . . . . . . . . . . . . . . . 5.3.1. Creación del socket UDP . . . . . . . . . . . . . . . . . . . . . . . . 5.3.2. Fragmentación y envío de los cuadros de video . . . . . . . . . . . . 36 36 38 41 41 42 CAPÍTULO 6: Rutinas de detección de movimiento y escritura a disco duro 6.1. Algoritmo de detección de movimiento . . . . . . . . . . . . . . . . . 6.2. Filtrado de las imágenes . . . . . . . . . . . . . . . . . . . . . . . . . 6.3. Determinación de eventos de movimiento . . . . . . . . . . . . . . . . 6.4. Rutinas de escritura a disco duro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44 44 46 48 52 CAPÍTULO 7: Rutinas de comunicación y despliegue de video en el cliente . 7.1. Envío de mensajes al servidor . . . . . . . . . . . . . . . . . . . . . . 7.2. Recepción de los cuadros de video . . . . . . . . . . . . . . . . . . . . 7.3. Despliegue de la imagen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58 58 61 64 CAPÍTULO 8: Resultados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 8.1. Captura de video . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 8.2. Reducción de ruido . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 VI 8.3. Determinación de eventos de movimiento . . . . . . . . . . . . . . . . . . . 67 CAPÍTULO 9: Conclusiones y Recomendaciones . . . . . . . . . . . . . . . . . . . . 71 9.1. Conclusiones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71 9.2. Recomendaciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72 BIBLIOGRAFÍA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72 APÉNDICES . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73 VII ÍNDICE DE FIGURAS Figura 3.1 Partes principales de la aplicación . . . . . . . . . . . . . . . . . . . . . . . . . Figura 3.2 Diagrama de flujo del hilo de inicialización y comunicación con el cliente. . . Figura 3.3 Diagrama de flujo del hilo de captura de video. . . . . . . . . . . . . . . . . . . Figura 3.4 Diagrama de flujo del hilo de detección de movimiento. . . . . . . . . . . . . . Figura 3.5 Diagrama de flujo del hilo de envío del video. . . . . . . . . . . . . . . . . . . . Figura 3.6 Estructura de la aplicación cliente. . . . . . . . . . . . . . . . . . . . . . . . . . Figura 3.7 Diagrama de flujo del hilo encargado de la interfaz gráfica y la mensajería TCP en el cliente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Figura 3.8 Diagrama de flujo del hilo de recepción de video. . . . . . . . . . . . . . . . . . 20 22 23 24 25 26 27 28 Figura 4.1 Diagrama del algoritmo de captura de video utilizado. . . . . . . . . . . . . . . 32 Figura 6.1 Diagrama de la máquina de estados encargada de detectar los eventos de movimiento. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49 Figura 8.1 Aplicación del filtro de media para reducir el ruido en las imágenes capturadas 67 Figura 8.2 Porcentaje de pixeles con diferencias durante un evento de movimiento . . . . 69 Figura 8.3 Respuesta del sistema ante un cambio rápido en la iluminación . . . . . . . . . 70 VIII ÍNDICE DE CUADROS Cuadro 2.1 Información contenida por la estructura video_capability. Cuadro 2.2 Información contenida por la estructura video_buffer. . . . Cuadro 2.3 información contenida por la estructura video_window. . . Cuadro 2.4 Información contenida por la estructura video_channel.. . Cuadro 2.5 Información contenida por la estructura video_picture. . . Cuadro 2.6 Información contenida por la estructura video_tuner. . . . Cuadro 2.7 Información contenida por la estructura video_mbuf. . . . Cuadro 2.8 Información contenida por la estructura video_mmap.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 13 14 14 15 15 16 16 Cuadro 5.1 Mensajes implementados para la comunicación entre el cliente y el servidor . 39 Cuadro 6.1 Estados necesarios para el algoritmo de detección de eventos de movimiento 48 Cuadro 8.1 Cuadros por segundo capturados. . . . . . . . . . . . . . . . . . . . . . . . . . 66 Cuadro 8.2 Parámetros del sistema de detección de eventos de movimiento utilizados durante la prueba . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68 IX NOMENCLATURA BSD: Berkeley Software Distribution. Familia de versiones de UNIX implementadas a partir de 1977. Muchas de las características de estos sistemas se han convertido en un estándar para implementaciones posteriores. buffer: Sector de memoria utilizado para el almacenamiento temporal de datos. FSF: Free Software Foundation, Fundación del Software Libre. GNU: Acrónimo recursivo: GNU is Not UNIX. Proyecto de FSF que busca crear un sistema operativo completo utilizando únicamente software libre. GTK: Gimp Toolkit. Conjunto de herramientas para la creación de interfaces de usuario gráficas. JPEG: Joint Picture Experts Group. Formato estandarizado de almacenamiento y compresión de imágenes. libc: Biblioteca Estándar del lenguaje C del sistema operativo GNU/Linux. LZO: Biblioteca de compresión de datos en tiempo real. MPEG: Moving Picture Expert Group. Formato estandarizado de compresión de video. POSIX: Portable Operating System Interface. Conjunto de estándares definidos por la IEEE que definen una interfaz de programación para software diseñado para ser ejecutado en variantes del sistema operativo UNIX. pthreads: POSIX threads. Implementación de hilos múltiples de procesamiento que forma parte del estándar POSIX. socket: Mecanismo para la creación de conexiones virtuales entre procesos, los cuales pueden ser ejecutados en una misma máquina, o en computadoras distintas. SYSV: UNIX System V. Versión de UNIX desarrollada en 1983 por AT&T. Esta versión dio lugar a un estándar conocido como System V Interface Definition. X TCP: Transfer Control Protocol. Protocolo de transmisión de datos orientado a conexión, que forma parte del conjunto de protocolos TCP/IP. TCP/IP: Conjunto de protocolos desarrollados para la transmisión de datos sobre Internet. UDP: User Datagram Protocol. Protocolo no orientado a conexión, que forma parte del conjunto de protocolos TCP/IP. UNIX: Sistema Operativo multitareas desarrollado en 1969 por Ken Thompson. Entre 1972 y 1974 fue implementado completamente en el lenguaje C, lo que lo convirtió en el primer sistema operativo portable a nivel de código fuente. Esto ha dado lugar a múltiples versiones e implementaciones, entre ellas Linux. USB: Universal Serial Bus, bus serial universal. V4L: Video4Linux. Interfaz para el acceso a dispositivos de video del sistema operativo GNU/Linux. V4L2: Video4Linux versión 2. XI RESUMEN Este proyecto tiene como objetivo desarrollar un sistema de adquisición de video que implemente algoritmos de detección de movimiento, con el fin de utilizarlo como parte de una aplicación de vigilancia, utilizando el conjunto de controladores y rutinas de acceso a dispositivos de video conocido como Video4Linux disponibles en el sistema operativo GNU/Linux. El sistema se implementó en el lenguaje de programación C, utilizando un modelo clienteservidor, con una aplicación principal encargada de llevar a cabo la adquisición y el procesamiento del video, y una aplicación cliente que se comunicaba con el servidor utilizando el protocolo TCP/IP y permitía controlar el sistema y observar el video capturado en tiempo real. Se utilizaron múltiples hilos de procesamiento, tanto en la aplicación cliente como en el servidor, con el fin de separar las distintas partes del sistema y de optimizar el desempeño. Se creó un sistema de adquisición de video que colocaba los cuadros capturados en memoria, a disposición de las otras partes del sistema . Posteriormente se desarrolló un conjunto de rutinas que permitían detectar movimiento con base en las diferencias de intensidad entre dos cuadros consecutivos de la secuencia capturada. También se implementó un sistema encargado de determinar la existencia de eventos de movimiento significativos, con el fin de eliminar eventos falsos, y de grabar los eventos detectados al disco duro de la computadora para su posterior revisión. Además, se creó un sistema utilizando sockets de red para llevar a cabo la transmisión de video y la comunicación entre el cliente y el servidor. Se logró capturar video a la mayor velocidad permitida por el dispositivo. Además, se obtuvieron buenos resultados en las rutinas de detección de movimiento, las cuales funcionaron de manera satisfactoria en las pruebas realizadas. El sistema final desarrollado resultó estable y eficiente, adecuado para una amplia gama de aplicaciones. XII CAPÍTULO 1: Introducción 1.1. Justificación En la actualidad, la mayoría de los sistemas de video están construidos utilizando sistemas propietarios, los cuales, además de ser costosos, limitan en gran medida la escalabilidad y la flexibilidad. Generalmente, estos productos utilizan protocolos cerrados, que dificultan e incluso impiden la interoperabilidad con dispositivos de otras marcas. Esto limita las opciones a la hora de una actualización y dificulta el mantenimiento del sistema. El desarrollo de estos sistemas sobre plataformas abiertas brinda una mayor versatilidad, dado que amplía la gama de opciones disponibles, además de que permite crear soluciones que se adapten de una mejor forma a cada situación particular. Hoy en día, una opción cada vez más utilizada consiste en utilizar sistemas de computo para el almacenamiento y el procesamiento del video. Esto proporciona una mayor versatilidad, debido a que facilita realizar cambios y añadir elementos. Actualmente pueden encontrarse en el mercado varios sistemas de esta clase, tanto propietarios como abiertos, para múltiples sistemas operativos. Para este proyecto se ha elegido implementar un conjunto de aplicaciones de adquisición y procesamiento de video sobre el sistema operativo GNU/Linux, utilizando el conjunto de controladores y herramientas Video4Linux, presente en este sistema. Entre las razones principales para esta elección se encuentran la gran estabilidad de este sistema operativo, así como sus características de seguridad. Además, existe una cantidad importante de dispositivos de bajo costo compatibles con Video4Linux, lo que reduce la inversión en equipo y brinda mayor flexibilidad. Por último, tanto la interfaz de acceso al hardware como el sistema operativo y demás aplicaciones necesarias son completamente abiertas, con todas las ventajas expuestas anteriormente. Si bien es cierto la aplicación principal para este sistema es la vigilancia, es posible adaptarlo para su utilización en una amplia gama de aplicaciones, lo que convierte a este sistema en una plataforma de de desarrollo para proyectos futuros. 1 2 1.2. Objetivos 1.2.1. Objetivo General Desarrollar un sistema de adquisición y procesamiento de video que implemente algoritmos de detección de movimiento, utilizando el sistema Video4Linux. 1.2.2. Objetivos Específicos Desarrollar un conjunto de rutinas que sean capaces de capturar video de uno o varios dispositivos a la máxima velocidad posible. Implementar un algoritmo que permita detectar movimiento y que sea capaz de registrar y capturar eventos para su posterior revisión. Implementar una interfaz gráfica de usuario que permita visualizar en tiempo real la imagen adquirida por cada una de las cámaras conectadas al sistema, así como los eventos registrados mediante los algoritmos de detección de movimiento. 1.3. Metodología La metodología utilizada para la realización del trabajo fue la siguiente: Se definen los objetivos y los alcances del proyecto, con el fin de estructurar el plan de trabajo a realizar. Se realiza una investigación bibliográfica sobre GNU/Linux y la interfaz de programación de Video4Linux, con el fin de conocer las características del sistema. Además se investiga sobre las principales técnicas y algoritmos para llevar a cabo la detección de movimiento. Se inicia el desarrollo de las rutinas de captura de video, capturando primero imágenes fijas y posteriormente secuencias de imágenes. Se investiga como optimizar el proceso de captura con el fin de obtener la mayor cantidad de cuadros por segundo posibles. Una vez implementadas las rutinas de captura de video se realizan pruebas utilizando varios dispositivos, con el fin de evaluar el desempeño obtenido. 3 Se decide utilizar el protocolo TCP/IP para llevar a cabo la comunicación entre la interfaz gráfica de usuario y el sistema de captura y procesamiento. Se desarrolla un sistema de mensajería, así como un método para enviar los cuadros de video para su visualización. Se implementa el algoritmo de detección de movimiento y se desarrolla un sistema de almacenamiento de las imágenes con base en los eventos de movimiento. Se realizan pruebas con el fin de evaluar el desempeño del sistema de detección de movimiento y el almacenamiento a disco duro. Paralelamente a cada uno de estos pasos, se lleva a cabo la documentación del trabajo realizado y la redacción del informe final. 1.4. Herramientas Utilizadas 1.4.1. Hardware Dispositivos de video El desarrollo y las pruebas del sistema implementado se llevó a cabo utilizando dos dispositivos de adquisición de video compatibles con Video4Linux: 1. Cámara USB marca Genius, modelo VideoCAM Express V2. 2. Tarjeta de televisión y adquisición de video marca Haupage, modelo TVPhone 98. 1.4.2. Software Lenguaje de programación y compilador El desarrollo del sistema se llevó a cabo utilizando el lenguaje de programación C. El compilador utilizado fue gcc1 , el cual forma parte del proyecto GNU. Se utilizaron las funciones de la biblioteca estándar C de GNU/Linux (libc), además de otras bibliotecas que forman parte de este sistema operativo, tales como pthreads. 1 http://gcc.gnu.org/ 4 Editor de texto La edición del programa, así como la este informe, se llevó utilizando el editor de texto Vim2 , tanto en su versión de consola como en su versión gráfica (gvim). Sistema operativo El desarrollo se llevó a cabo utilizando el sistema operativo GNU/Linux. La distribución empleada fue Debian3 «Sid» (versión inestable). Procesamiento tipográfico Este documento fue preparado utilizando el sistema de procesamiento tipográfico LATEX4 . 2 http://www.vim.org/ http://www.debian.org/ 4 http://www.latex-project.org/ 3 CAPÍTULO 2: Desarrollo Teórico 2.1. El sistema operativo GNU/Linux Linux es un kernel1 tipo UNIX, creado en 1991 por el finlandés Linus Torvalds. Linux se caracteriza por ser una implementación libre del estándar POSIX, con extensiones SYSV y BSD. Si bien es cierto, es compatible en muchos aspectos con UNIX, todo el código fue escrito de nuevo y no contiene partes de ninguna versión o variante de UNIX existente. Linux fue escrito inicialmente para funcionar en procesadores Intel 386 y compatibles, sin embargo hoy en día ha sido portado a un gran número de plataformas, entre las que se encuentran Alpha, PowerPC, Motorola 68K y Sparc. Un aspecto que es importante resaltar es que Linux es solamente el kernel o núcleo del sistema operativo, o sea, el que se encarga de realizar las tareas de más bajo nivel, como la gestión del hardware, el acceso a memoria o el manejo de los sistemas de archivos. Para poder utilizar el sistema es necesario contar con aplicaciones tales como un intérprete de comandos (conocido como shell), compiladores, herramientas para el manejo de archivos, procesadores de texto, entre otras. En el caso de Linux, la mayoría de estas aplicaciones forman parte del proyecto GNU, iniciado por la Fundación del Software Libre (FSF), el tiene como fin crear un sistema operativo completo libre. El conjunto de estas aplicaciones GNU y el kernel Linux es conocido generalmente como GNU/Linux. Desde el punto de vista técnico, algunas de las características principales de Linux son las siguientes: Mutitareas. Multiusario. Soporte para múltiples arquitecturas. Soporte para sistemas multiprocesador. Funciona en modo protegido en los procesadores Intel 386 y compatibles. 1 Núcleo de un sistema operativo. Componente de software responsable de proveer acceso seguro al hardware de la máquina y a los procesos que están siendo ejecutados[9]. 5 6 Posee protección de memoria entre procesos. Esto impide que un programa pueda provocar una falla en el sistema. Soporte para memoria virtual con paginación en el disco duro, con el fin de proveer más memoria al sistema en caso de ser necesaria (Esto se conoce como área de intercambio o swap). Soporte para una gran cantidad de sistemas de archivos, entre ellos ReiserFS, XFS, Ext2, Ext3, MSDOS, FAT16, FAT32 y NTFS (lectura, el soporte para escritura es limitado y aún se encuentra en desarrollo). Soporte para una gran cantidad de protocolos de red, entre ellos TCP, IPv4, IPv6, IPX, AppleTalk y SMB. 2.2. Acceso a dispositivos y funciones de entrada y salida En Linux, al igual que en todos los sistemas tipo UNIX, la mayoría de los dispositivos pueden ser accedidos como si se tratara de archivos. Esto permite llevar a cabo todas las operaciones de entrada y salida a través de un pequeño conjunto de llamadas al sistema, las cuales se encuentran definidas en el estándar POSIX. A continuación se brinda una descripción general de las llamadas más utilizadas. 2.2.1. open() La llamada al sistema open() se encarga de abrir un archivo y asignarlo a una variable conocida como descriptor de archivo. El descriptor de archivo se utiliza para referirse al archivo a lo largo del programa, y es utilizado como argumento para las otras funciones de entrada y salida. La sintaxis de open() es la siguiente: #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int open(const char *pathname, int flags). int open(const char *pathname, int flags, mode_t mode); 7 donde pathname corresponde al nombre del archivo, flags especifica las condiciones en que se abrirá el archivo, y mode define los permisos que se utilizarán cuando se crea un nuevo archivo. Por ejemplo, para abrir el dispositivo /dev/ttyS0 (primer puerto serial) para lectura se debe utilizar la llamada int fd; /* Descriptor de archivo que se utilizará para referirse al dispositivo */ fd = open("/dev/ttyS0", O_RDONLY); 2.2.2. close() La llamada close() se encarga de cerrar un archivo que ha sido abierto con anterioridad, una vez que no se va a utilizar más. close() recibe como argumento el descriptor de archivo correspondiente al archivo que se quiere cerrar, y retorna 0 en caso de éxito, o −1 si se produjo algún error. La sintaxis de esta llamada es la siguiente: #include <unistd.h> int close(int fd); 2.2.3. read() La llamada al sistema read() se utiliza para leer datos desde un archivo. la sintaxis es la siguiente: #include <unistd.h> ssize_t read(int fd, void *buf, size_t count); donde fd es el descriptor de archivo, buf es un puntero al sector de memoria donde se almacenarán los datos y count corresponde a la cantidad de información —en bytes— que será leída. El valor retornado por read es el número de bytes leídos, o −1 en caso de que se presente un error. 8 2.2.4. write() La llamada al sistema write() permite escribir datos a un archivo. La sintaxis de write() es la siguiente: #include <unistd.h> ssize_t write(int fd, const void *buf, size_t count); El parámetro fd es el descriptor de archivo, buf corresponde a un puntero dirigido al sector de memoria en el cual se encuentran los datos que se desean escribir al archivo y count es el número de bytes que se desean escribir. La llamada write() retorna el número de bytes escritos, o −1 en caso de que ocurra algún error. 2.2.5. ioctl() La llamada al sistema ioctl() es utilizada casi exclusivamente con los dispositivos. Esta llamada se encarga de llevar a cabo operaciones sobre un archivo, o bien definir parámetros asociados a un archivo en particular. Estas operaciones y parámetros dependen del archivo, y corresponden generalmente a características del dispositivo. Por ejemplo, ioctl() se puede utilizar para definir la frecuencia de muestreo, el número de canales o la cantidad de bits de una tarjeta de sonido. La sintaxis de ioctl() es la siguiente: #include <sys/ioctl.h> int ioctl(int d, int request, ...) donde d corresponde al descriptor de archivo y request denota la operación que se quiere realizar. La llamada ioctl() también puede recibir otros argumentos requeridos por alguna operación en particular. 2.3. Programación con múltiples hilos de procesamiento La programación con múltiples hilos de procesamiento es un esquema que permite desarrollar aplicaciones que ejecuten varios flujos de procesamiento dentro de un mismo proceso. De esta forma, cada uno de los flujos o hilos se ejecuta de forma independiente, sin interferir 9 con los otros. Además, cada uno de los hilos tiene acceso a los distintos recursos del proceso, tales como las variables globales y el entorno. Cada uno de los hilos de procesamiento es administrado por el planificador del sistema de archivos. De esta forma se obtiene un esquema similar a programación multiproceso tradicional de Linux, con la ventaja de que es más sencillo compartir recursos entre los flujos de procesamiento, sin necesidad de recurrir a recursos tales como colas (queues) o tuberías (pipes). Utilizando hilos múltiples de procesamiento es posible obtener ejecución paralela en sistemas multiprocesador, lo cual incrementa el desempeño de una aplicación de forma significativa. En el caso de los sistemas monoprocesador, se obtiene un pseudo-paralelismo debido a la planificación llevada a cabo por el sistema de archivos, lo cual trae consigo también mejoras en el desempeño, así como una simplificación del diseño de la aplicación, debido a que es posible separar las tareas que deben ejecutarse simultáneamente y dejar que el sistema operativo se encargue de su ejecución. La implementación de hilos múltiples de procesamiento en Linux es conocida como pthreads, o POSIX threads. Esta implementación es utilizada en la mayoría de los sistemas UNIX actuales. Algunas de las rutinas principales de esta biblioteca se describen a continuación. 2.3.1. pthread_create() La función pthread_create() se encarga de crear un nuevo hilo de procesamiento y hacer que el sistema operativo programe su ejecución. La sintaxis de esta función se muestra a continuación: #include <pthread.h> int pthread_create(pthread_t *thread, pthread_attr_t *attr, void *(*start_routine)(void *), void *arg); donde thread es la variable que apunta al nuevo hilo creado, con los atributos attr. Este nuevo hilo ejecutará la función start_routine la cual debe retornar un puntero nulo. Además, esta función puede recibir un argumento arg, el cual debe convertirse a un puntero nulo antes de ser enviado. 10 2.3.2. pthread_exit() Esta función es llamada por las funciones ejecutadas dentro de un hilo de procesamiento para indicar su finalización. La sintaxis de pthread_exit() se muestra a continuación: #include <pthread.h> void pthread_exit(void *retval); La variable retval es el valor de retorno de la función. Esta función es equivalente a la función exit() utilizada en las aplicaciones de un solo hilo. 2.3.3. pthread_join() La rutina pthread_join() detiene la ejecución del hilo actual hasta que otro hilo finalice. La sintaxis de esta función es la siguiente: #include <pthread.h> int pthread_join(pthread_t th, void **thread_return); donde th es el hilo por el cual se esperará. El valor de retorno de este hilo (retornado por la función pthread_exit()) es almacenado en la variable apuntada por thread_return. 2.3.4. pthead_mutex_init() Esta función inicializa una variable mutex. Esta variable permite controlar el acceso a un recurso compartido, evitando que dos hilos puedan manipularlo al mismo tiempo. La sintaxis de esta función es la siguiente: #include <pthread.h> int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutex_attr_t *mutexattr); donde mutex es la nueva variable de control de acceso y mutexattr contiene los atributos de esta nueva variable. 11 2.3.5. pthread_mutex_lock() La función pthread_mutex_lock() coloca un bloqueo sobre una variable mutex. Esta operación es llevada a cabo por un hilo cuando desea acceder a alguno de los recursos compartidos. Si la variable ya está bloqueada el recurso está siendo utilizado por otro hilo y la ejecución del hilo actual se detendrá hasta que la variable sea desbloqueada. Una variante de esta función es la rutina pthread_mutex_trylock(), la cual no detendrá la ejecución del hilo si la variable se encuentra bloqueada. La sintaxis de pthread_mutex_lock se muestra a continuación: #include <pthread.h> int pthread_mutex_lock(pthread_mutex_t *mutex); donde mutex es la variable de control a bloquear. 2.3.6. pthread_mutex_unlock() Esta rutina se encarga de desbloquear una variable mutex, con el fin de permitir que otros hilos puedan tener acceso al recurso compartido. La sintaxis de esta función se muestra a continuación: #include <pthread.h> int pthread_mutex_unlock(pthread_mutex_t *mutex); 2.3.7. pthread_mutex_destroy() La rutina pthread_mutex_destroy() se encarga de liberar los recursos ocupados por una variable mutex, una vez que su utilización ha finalizado. La sintaxis de esta rutina es la siguiente: #include <pthread.h> int pthread_mutex_destroy(pthread_mutex_t *mutex); 12 2.4. El sistema Video4Linux Video4Linux es un sistema de acceso a dispositivos de video que agrupa a varios controladores bajo una interfaz de programación común. Esto permite desarrollar programas con una interfaz de acceso al hardware única, que funcionan con una gran cantidad de dispositivos. Además, el programador puede concentrarse en los aspectos funcionales de su programa sin preocuparse por las rutinas de bajo nivel del controlador. Existe una amplia gama de dispositivos soportados por Video4Linux, incluyendo tarjetas de captura de video, sintonizadores de televisión, cámaras USB y de puerto paralelo, además de decodificadores de teletexto. Video4Linux (V4L) fue introducido al final del ciclo de desarrollo del kernel 2.1.x. A partir del kernel 2.5.x se introdujo Video4Linux 2 (V4L2). Esta nueva generación corrige muchos de los problemas de diseño de la versión anterior, además de que presenta nuevas características tales como funciones para el manejo de streaming de video y métodos optimizados de acceso de memoria. Si bien es cierto la recomendación es utilizar V4L2, aún muchos controladores no soportan esta nueva especificación. 2.4.1. La interfaz de programación de Video4Linux Los dispositivos de captura de video V4L son accesibles a través del archivo /dev/video[n], donde [n] es un número entre 0 y 63 que corresponde al identificador del dispositivo y se asigna de forma sucesiva al registrar el dispositivo en el kernel. Una vez abierto este archivo es posible acceder a la mayoría de las funciones del dispositivo utilizando la llamada al sistema ioctl(). Los controles disponibles son los siguientes: VIDIOCGCAP: Permite obtener la información básica del dispositivo, tal como el tipo de dispositivo, el número de canales y la resolución máxima. Recibe como argumento una estructura del tipo video_capability, en la cual se almacena la información retornada por la llamada. La información contenida por esta estructura se muestra en el cuadro 2.1. VIDIOCSFBUF: Permite fijar los parámetros para la escritura directa al framebuffer de la tarjeta de video. No todos los dispositivos soportan esta característica. Además, el 13 Cuadro 2.1 Información contenida por la estructura video_capability. Campo name[32] type channels audios maxwidth maxheight minwidth minheight Descripción Nombre del dispositivo Tipo de dispositivo Número de canales Número de dispositivos de audio Ancho máximo posible del cuadro capturado Altura máxima posible del cuadro capturado Ancho mínimo posible del cuadro capturado Altura mínima posible del cuadro capturado acceso a framebuffer no es posible en algunos sistemas. Esta llamada recibe como argumento una estructura del tipo video_buffer, la cual contiene los parámetros de acceso al framebuffer. El contenido de esta estructura se muestra en el cuadro 2.2. Cuadro 2.2 Información contenida por la estructura video_buffer. Campo void *base int height int width int depth int bytesperline Descripción Dirección física base del buffer Altura del buffer Ancho del buffer Profundidad del buffer Número de bytes entre el inicio de dos líneas adyacentes VIDIOCGWIN: Obtiene la información del área de captura. Recibe como argumento una estructura del tipo video_window, en la cual se escribe la información. El contenido de esta estructura se describe en el cuadro 2.3. VIDIOCSWIN: Permite definir la información del área de captura requerida. Recibe como argumento una estructura video_window la cual debe contener los parámetros deseados. 14 Cuadro 2.3 información contenida por la estructura video_window. Campo x y width height chromakey flags clips clipcount Descripción Coordenada x del área de captura Coordenada y del área de captura Ancho del área de captura Altura del área de captura Valor RGB32 del chroma Banderas de captura adicionales Lista de rectángulos que se requiere extraer (sólo VIDIOCSWIN) Número de rectángulos que se desea extraer (sólo VIDIOCSWIN) VIDIOCCAPTURE: Permite activar y desactivar la captura en modo overlay2 . VDIOCGCHAN: Retorna la información de los canales del dispositivo. Almacena la información en la estructura del tipo video_channel que recibe como argumento. Los contenidos se esta estructura se muestran en el cuadro 2.4. Cuadro 2.4 Información contenida por la estructura video_channel. Campo Descripción channel Número del canal name Nombre del canal tuners Número de sintonizadores en el canal flags Propiedades del sintonizador type Tipo de entrada norm Norma que utiliza el canal VIDIOCSCHAN: Selecciona el canal del cual se va a capturar. Recibe un entero con el número del canal como argumento. VIDIOCGPICT: Retorna las características (brillo, saturación, etc.) de la imagen. Almacena la información en la estructura de tipo video_picture que recibe como argumento. Los componentes de esta estructura se muestran en el cuadro 2.5. 2 Transferencia directa desde la memoria del dispositivo de captura hasta la memoria de la tarjeta de video. Este modo incrementa la velocidad y reduce el procesamiento necesario cuando sólo se requiere desplegar el video capturado, sin efectuar ninguna operación intermedia. 15 Cuadro 2.5 Información contenida por la estructura video_picture. Campo brightness hue colour contrast whiteness depth palette Descripción Brillo Tonalidad (sólo imágenes a color) Balance de color (sólo imágenes a color) Contraste de la imagen Nivel de blancura (sólo imágenes en escala de grises) Nivel de profundidad de la imagen Paleta que debe usarse para la imagen VIDIOCSPICT: Permite definir las características de la imagen que será capturada. Recibe como argumento una estructura del tipo video_picture, la cual debe contener los nuevos valores requeridos. VIDIOCGTUNER: Obtiene las características del sintonizador. Escribe la información en la estructura de tipo video_tuner que recibe como argumento. En el cuadro 2.6 puede verse la información almacenada en esta estructura. No todos los canales ni todos los dispositivos tienen sintonizador, sólo los que corresponden a captura de televisión o de radio. Cuadro 2.6 Información contenida por la estructura video_tuner. Campo tuner name rangelow rangehigh flags mode signal Descripción Número del sintonizador Nombre canónico del sintonizador Mínima frecuencia sintonizable Máxima frecuencia sintonizable Banderas que describen el sintonizador Modo de la señal de video Intensidad de la señal, si se conoce (entre 0 y 65535) VIDIOCSTUNER: Permite seleccionar cual sintonizador se va a utilizar. Recibe un entero con el número de sintonizador como argumento. VIDIOCGFREQ: Obtiene la frecuencia en la cual se encuentra ajustado el sintonizador. VIDIOCSFREQ: Permite definir una nueva frecuencia del sintonizador. 16 VIDIOCGMBUF: Permite obtener el tamaño de la memoria del dispositivo, así como la cantidad de cuadros que puede contener. La llamada almacena esta información en la estructura video_mbuf que recibe como argumento, la cual es descrita en el cuadro 2.7. Cuadro 2.7 Información contenida por la estructura video_mbuf. Campo Descripción size Tamaño de la memoria del dispositivo frames Número de cuadros que puede contener la memoria offsets Posición de cada cuadro dentro de la memoria VIDIOCMCAPTURE: Inicia la captura hacia uno de los cuadros de la memoria de video. Recibe como argumento una estructura del tipo video_mmap donde se especifican varios parámetros tales como el cuadro que se desea capturar, el tamaño del cuadro y el formato de la imagen. La descripción de esta estructura se presenta en el cuadro 2.8. Cuadro 2.8 Información contenida por la estructura video_mmap. Campo Descripción frame Número de cuadro que se desea capturar height Altura del cuadro width Ancho del cuadro format Formato de la imagen VIDIOCSYNC: Espera a que finalice la captura de un cuadro, el cual se especifica a través de la estructura del tipo video_mmap que recibe como parámetro. Además de estas llamadas, existen otras que se encargan de controlar las funciones de audio asociadas con algunos de los dispositivos. 2.5. Algoritmos de detección de movimiento El análisis del movimiento es una de las áreas de estudio más importantes en el campo del procesamiento y análisis de imágenes, debido a que existe una gran cantidad de aplicaciones, como por ejemplo el estudio del movimiento en seres vivos o el análisis del tráfico vehicular. En los últimos años se ha dado un auge en la investigación desarrollada en esta área, 17 principalmente debido a los avances en la tecnología informática y al desarrollo de nuevos y eficientes algoritmos. La tarea más básica dentro de esta disciplina consiste en determinar cuando se da movimiento en un escenario específico. Existen varias técnicas que permiten alcanzar este objetivo; la elección de una de ellas depende de los requerimientos específicos de la aplicación, así como de los recursos disponibles. La forma más intuitiva y sencilla de analizar el movimiento consiste en evaluar las diferencias entre dos imágenes de una secuencia. Estas diferencias pueden estudiarse analíticamente sustrayendo una imagen de la otra, de acuerdo con la ecuación 2.1. O(x, y) = I1 (x, y) − I2 (x, y) (2.1) En este método se utilizan imágenes en escala de grises, debido a que sólo es necesario evaluar los cambios en la intensidad de los pixeles. Al sustraer una imagen de la otra se remueven de la imagen resultante todas las características que no han cambiado, y se resaltan aquellas que sí. Si la iluminación y la geometría del escenario son consistentes entre ambas imágenes, las únicas diferencias en los pixeles donde no ha ocurrido ningún cambio se deben a variaciones estadísticas, provocadas por ruido electrónico o de la cámara[6]. De acuerdo con esto, el movimiento provoca cambios en los niveles de grises entre las imágenes de una secuencia. En el caso ideal, cualquier cambio en uno o más pixeles puede considerarse como movimiento. Sin embargo, en los sistemas reales existen muchos factores que pueden producir estos cambios. Algunos de estos factores son los cambios en el nivel de iluminación y el ruido, tal como se mencionó anteriormente. Si bien es posible utilizar técnicas de filtrado para reducir su influencia, no es posible eliminarla completamente. Además, en muchas ocasiones, no todos los eventos de movimiento que se presentan en una escena son de interés. Por ejemplo, una cortina movida por el viento probablemente no sea de interés cuando lo que se desea es detectar la presencia de una persona en movimiento en un escenario. Debido a estas razones, se utilizan algunos valores de umbral cuando se lleva a cabo la detección. Sea D(x, y) la diferencia, pixel por pixel de dos imágenes, I1 (x, y) y I2 (x, y), definida de acuerdo a la ecuación 2.2 0 si | I (x, y) − I (x, y) |≤ ² 2 1 D(x, y) = 1 si | I2 (x, y) − I1 (x, y) |> ² (2.2) 18 donde ² es el valor de umbral de sensibilidad. Esto significa que la diferencia de intensidad en un pixel tiene que estar por encima de cierto valor para que esta diferencia sea significativa. Esto permite eliminar la influencia del ruido que consiste en cambios pequeños de intensidad. Un evento de movimiento se da cuando la ecuación 2.3 es válida: X D(x, y) > δ (2.3) donde δ es el valor de umbral de evento. De esta forma, la cantidad de pixeles con diferencias significativas debe estar por encima de este valor de umbral para que se considere que se produjo movimiento. Existen otros métodos más complejos para llevar a cabo la detección de movimiento, sin embargo, el método descrito anteriormente proporciona buenos resultados cuando se desea realizar la detección en un escenario estacionario, lo que lo hace apropiado para una gran cantidad de aplicaciones. Además, este algoritmo es significativamente más rápido que los otros, lo que es muy importante en una aplicación de tiempo real como esta. 2.6. Reducción de ruido en las imágenes Como se explicó en la sección anterior, el ruido presente en las imágenes puede afectar significativamente el desempeño de los algoritmos de detección de movimiento. Debido a esto, se acostumbra aplicar algún filtro para reducir los niveles de ruido antes de llevar a cabo la sustracción. Existen muchas técnicas de filtrado que permiten reducir el ruido de una imagen. La elección de una técnica en particular depende de los requerimientos particulares de cada aplicación. Sin embargo existe un filtro muy utilizado, debido a su efectividad y su simplicidad. Este filtro es el filtro paso bajos, o filtro de media. 2.6.1. Filtro de media El filtro de media asigna a cada pixel un promedio calculado con base en su propio valor y el valor de los pixeles cercanos. Esto permite atenuar las transiciones rápidas de intensidad que se dan de un pixel a otro; debido a esto, este filtro también es conocido como filtro paso bajos, dado que solamente conserva las transiciones suaves (o frecuencias bajas). Debido a que la mayoría del ruido aleatorio existente en las imágenes presenta estas características, 19 este filtro es bastante efectivo para reducirlo. El funcionamiento del filtro de media puede expresarse matemáticamente de la siguiente forma: +m X ∗ Px,y = Wi,j · Px+i,y+j i,j=−m +m X (2.4) Wi,j i,j=−m donde P es la matriz de pixeles de la imagen, P ∗ es la matriz resultante una vez aplicado el filtro y W es una matriz cuadrada de dimensión 2m + 1, la cual contiene los pesos que se asignarán a cada uno de los pixeles del vecindario del pixel que está siendo calculado. El tamaño de este vecindario es 3 × 3, 5 × 5 o 7 × 7, pero también podrían utilizarse vecindarios más grandes si se desea obtener un filtrado mejor. También es posible utilizar regiones no cuadradas en esta clase de filtrado[6]. La forma más sencilla de este filtro consiste en una matriz W 3 × 3 en la que todos los pesos son 1: W = 1 1 1 1 1 1 1 1 1 También es posible escoger valores diferentes para la matriz W , de manera que cada uno de los pixeles tiene un peso diferente en el cálculo del promedio. Generalmente se trata de que la matriz tenga una distribución similar a la curva Gausiana, con los pesos mayores asignados al pixel que está siendo calculado y a los pixeles cercanos, y los menores a los pixeles más alejados. Este filtro presenta problemas en los bordes de la imagen, debido a que la región que rodea a los pixeles no es simétrica. Aunque es posible utilizar un vecindario asimétrico en estos casos, la mayoría de las ocasiones estos pixeles se deja sin procesar y se les asigna un valor de cero (color negro). CAPÍTULO 3: Descripción general del sistema de captura de video y detección de movimiento 3.1. Estructura de la aplicación y aspectos generales La aplicación desarrollada está dividida en dos partes principales: un servidor, que se encarga de capturar del dispositivo de video y llevar a cabo las rutinas de detección de movimiento, y un cliente, a través del cual se realiza la visualización en tiempo real del video capturado por el servidor, y que permite controlar algunos de los parámetros del servidor. La comunicación entre ambas partes se lleva a cabo utilizando el conjunto de protocolos TCP/IP, a través de una interfaz de red. Esto permite implementar una aplicación distribuida, en la cual, la captura y el procesamiento puedan llevarse a cabo en una computadora y la visualización y el control puedan ejecutarse de forma remota desde otro equipo. Figura 3.1 Partes principales de la aplicación Tanto el servidor como el cliente fueron desarrollados en lenguaje C, utilizando funciones de la biblioteca estándar de Linux (conocida como libc). La aplicación fue desarrollada utilizado múltiples hilos de procesamiento utilizando la implementación POSIX (conocida como pthreads). Esto permite ejecutar en paralelo varias de las rutinas, como es el caso de la captura del video y de la detección de movimiento en el servidor. 20 21 3.2. Estructura del servidor El servidor está dividido en cuatro partes, cada una de las cuales es ejecutada en un hilo de procesamiento diferente. Estas partes son: Inicialización y comunicación con el cliente: Es el primer hilo que se ejecuta y el que se encarga de crear y administrar a los otros. Se encarga de inicializar el dispositivo de video y de crear un socket TCP, el cual espera conexiones del cliente. A través de esta rutina se controlan todos los procesos del servidor, de acuerdo con las instrucciones enviadas por el cliente. Captura de video: Se encarga de leer las imágenes del dispositivo de video y colocarlas en un área de memoria, la cual es accesible por las otras rutinas del programa. Puede iniciarse y detenerse bajo demanda, según las instrucciones enviadas por el cliente. Detección de movimiento: Analiza la secuencia de imágenes obtenida por el hilo de captura con el fin de detectar movimiento en el video. En caso de que esto ocurra se encarga de almacenar el video en el disco duro, y notificar al cliente que se dio un evento de movimiento. También es activado o desactivado bajo demanda por el cliente a través del canal de instrucciones TCP. Envío del video: Crea un socket UDP y envía el video para su despliegue en tiempo real en el cliente. Es creado por el cliente una vez iniciada la captura y puede detenerse cuando no se desee recibir más video. 3.2.1. Inicialización y comunicación con el cliente Como se mencionó anteriormente, éste es el hilo principal del programa, debido a que se encarga de administrar los distintos componentes de acuerdo con las instrucciones recibidas del cliente. Este hilo se ejecuta al iniciar el programa, y todos los demás son creados desde aquí. Una vez creado, su primera función es la inicialización del dispositivo de video. Posteriormente, crea un socket TCP y lo registra para que espere conexiones en un puerto determinado. Cuando se establece una conexión con el cliente ejecuta las instrucciones que este envía y una vez finalizada la sesión espera a que se establezca de nuevo una conexión. En la figura 3.2 se muestra el diagrama de flujo de esta rutina. 22 Figura 3.2 Diagrama de flujo del hilo de inicialización y comunicación con el cliente. 3.2.2. Captura de video Esta rutina se encarga de reservar un sector de memoria con el fin de proporcionar un buffer circular para almacenar los cuadros capturados del dispositivo de video. Este buffer permite a los otros hilos acceder los últimos cuadros capturados, con el fin de llevar a cabo el procesamiento necesario para la detección de movimiento y de enviar los cuadros al cliente para su despliegue en tiempo real. Una vez hecho esto, se mapea la memoria del dispositivo de video y se inicia el ciclo que copia secuencialmente cada uno de los cuadros de la memoria de video al buffer. Este ciclo se repite hasta que se reciba una orden del cliente. En la figura 3.3 se muestra el diagrama de flujo de la rutina de captura de video. 23 Figura 3.3 Diagrama de flujo del hilo de captura de video. 3.2.3. Detección de movimiento El hilo de detección de movimiento se encarga de determinar cuando se produce algún evento de movimiento en el escenario observado y de almacenar estos eventos en el disco duro. La detección se lleva a cabo analizando las diferencias que existen entre un cuadro y otro. Si las diferencias se encuentran por encima de cierto valor y se mantienen por algún tiempo se considera que se está dando un evento de movimiento y se graban los cuadros capturados al disco duro. Una vez que el evento finaliza, las imágenes almacenadas se combinan en un archivo de video y se notifica al usuario el registro del evento. El diagrama de flujo de la rutina de detección de movimiento se muestra en la figura 3.4. 3.2.4. Envío del video La rutina de envío de video se encarga de crear, bajo pedido del cliente, un socket UDP para enviar los cuadros de video capturados para su visualización en tiempo real. Una vez 24 Figura 3.4 Diagrama de flujo del hilo de detección de movimiento. creado el socket espera por un mensaje del cliente que le informe que está listo para recibir el video. Una vez recibido este mensaje inicia un ciclo en el cual toma un cuadro del buffer, lo fragmenta en paquetes de 4096 bytes con el fin de cumplir con los requerimientos de capacidad del protocolo UDP y lo envía al cliente. Esto se repite hasta que el cliente solicite que se detenga el Envío, lo que provoca que el ciclo finalice y que el hilo de procesamiento se detenga. En la figura 3.5 se muestra el diagrama de flujo para esta rutina. 25 Figura 3.5 Diagrama de flujo del hilo de envío del video. 3.3. Estructura del cliente La aplicación cliente consiste en una interfaz gráfica de usuario que permite visualizar en tiempo real el video capturado por el servidor. Además, es posible manipular muchas de las funciones del servidor, como por ejemplo iniciar o detener la captura de video. El cliente además es notificado por el servidor cada vez que se registra un evento del movimiento. La interfaz gráfica de usuario se implementó utilizando la biblioteca GTK 2.4 para crear las ventanas y demás elementos visuales, así como para manejar la interacción con el usuario y con el administrador de ventanas. Al igual que el servidor, este programa también se dividió en múltiples hilos de procesamiento, con el fin de ejecutar en forma paralela las rutinas correspondientes a la recepción de los cuadros de video y lo referente a la visualización y la interacción con el usuario. En la figura 3.6 pueden verse las partes principales del programa, así como su interacción con el servidor. 26 Figura 3.6 Estructura de la aplicación cliente. 3.3.1. Interfaz gráfica de usuario y mensajería TCP La rutina principal del programa cliente se encarga de crear e inicializar la interfaz gráfica, así como de establecer la comunicación con el servidor a través de un socket TCP. Una vez iniciado el programa, este envía mensajes al servidor para que inicie la captura y la transmisión del video. Además solicita al servidor las características de los cuadros de video con el fin de poder reconstruirlos adecuadamente a partir de los datos recibidos a través del socket UDP. Una vez llevada a cabo la inicialización, la rutina principal lanza el hilo que se va a encargar de recibir los paquetes UDP que contienen el video. Por último, la rutina configura y despliega los elementos de la interfaz gráfica y llama a la rutina de manejo de eventos GTK, la cual se encarga de la interacción con el usuario y con el manejador de ventanas. Este hilo de procesamiento (y el programa completo) finaliza cuando la ventana es destruida (el usuario presiona el botón de cerrar de la ventana) o cuando el usuario selecciona en el menú la acción correspondiente. En la figura 3.7 puede verse el diagrama de flujo de este hilo de procesamiento. 3.3.2. Recepción de los cuadros de video La recepción de los cuadros de video para su despliegue en tiempo real es llevada a cabo por un hilo de procesamiento independiente que crea un socket UDP, envía un mensaje al servidor indicando que está listo para recibir la información y posteriormente inicia un ciclo en el cual recibe la ráfaga de paquetes que contiene los cuadros de video. Como se indicó anteriormente, el servidor envía cada cuadro fragmentado en varios paquetes de 4096 bytes debido a los requerimientos del protocolo UDP, por lo que la rutina de recepción debe reconstruir cada imagen. Para esto, se calcula el número de paquetes por cuadro con base en las características del video capturado solicitadas al servidor a través del canal TCP. Además, 27 Figura 3.7 Diagrama de flujo del hilo encargado de la interfaz gráfica y la mensajería TCP en el cliente cada vez que el servidor finaliza el envío de un cuadro envía un paquete de sincronización de 4 bytes, que permite a la rutina de recepción verificar si ha recibido el número correcto de paquetes de datos. Si hay alguna inconsistencia, el cuadro es desechado y se espera el siguiente. Cuando un cuadro es recibido satisfactoriamente, este es colocado en una posición de memoria que es leída periódicamente por el hilo encargado de la interfaz gráfica. De esta forma, el usuario puede ver un flujo constante de video. En la figura 3.8 se muestra el diagrama de flujo para este hilo de procesamiento. 28 Figura 3.8 Diagrama de flujo del hilo de recepción de video. CAPÍTULO 4: Rutinas de captura de video 4.1. Inicialización del sistema de video El primer paso necesario para acceder el dispositivo de video consiste en abrir el dispositivo para lectura y escritura, y asignarlo a un descriptor de archivo. Este descriptor de archivo se utiliza durante el resto del programa para referirse al dispositivo y efectuar todas las operaciones necesarias. if((fd = open(video_dev, O_RDWR)) == -1) { fprintf(stderr, "Error abriendo el dispositivo %s\n", video_dev); return -1; } En el fragmento de código mostrado, fd es el descriptor de archivo asignado al dispositivo de video. La variable video_dev que se pasa como argumento a open() contiene una cadena de carácteres con el nombre del dispositivo de video que se desea abrir (por ejemplo /dev/video0). Una vez abierto el dispositivo se utiliza la llamada al sistema ioctl() para obtener las de las características del dispositivo, así como para definir algunos parámetros necesarios para la captura. /* Obtención de las capacidades de captura */ if(ioctl(fd, VIDIOCGCAP, &cap) == -1) { fprintf(stderr, "Error al realizar VIDIOCGCAP\n"); return -1; } /* Información de los Buffers */ if((ioctl(fd, VIDIOCGMBUF, &buf_info)) == -1) { printf("Error adquiriendo los buffers\n"); return -1; } 29 30 La llamada a VIDIOCGCAP solicita al dispositivo las características del dispositivo, las cuales son almacenadas en la variable cap —del tipo video_capability— que se pasa como argumento a ioctl(). Estas características incluyen el nombre del dispositivo y el número de canales, así como los tamaños máximos y mínimos posibles para el cuadro a capturar (ver cuadro 2.1). Posteriormente, la llamada a VIDIOCGMBUF se encarga de obtener la información de la memoria del dispositivo. Esta información es almacenada en la variable buf_info, una estructura del tipo video_mmap, y es utilizada por la función mmap() para asignar este bloque de memoria a una posición direccionable por el sistema operativo. VIDIOCGMBUF permite obtener además la cantidad de cuadros que puede almacenar la memoria de video, que puede ir desde un solo cuadro hasta 32 cuadros, dependiendo del dispositivo. map = mmap(0, buf_info.size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if(map == NULL) { printf("Error en el mapeo de la memoria\n"); return -1; } De esta forma, la memoria del dispositivo es accesible a través del puntero map. Esto proporciona un método de captura que resulta más eficiente que efectuar llamadas a read() sobre el dispositivo. El último paso que se debe llevar a cabo antes de iniciar la captura consiste en definir cual será el canal del cual se va a capturar la imagen. Esto se realiza mediante la llamada a VIDIOCSCHAN, la cual toma como argumento un entero con el canal que se desea. int set_chan(int fd, int chan) { if(ioctl(fd, VIDIOCSCHAN, chan) == -1) return -1; else return 0; } 31 4.2. Ciclo de captura Tal como se mencionó en el capítulo 3, el ciclo de captura es llevado a cabo por un hilo de ejecución independiente. Esto es implementado utilizado la biblioteca pthreads, la cual forma parte de la biblioteca estándar del lenguaje C para Linux. pthread_t capture_thread; ... pthread_create(&capture_thread, &attr, capture_loop, NULL); De esta forma se crea un nuevo hilo que llama a la función capture_loop. Una vez que inicia la ejecución de la función se reserva memoria para el buffer circular que va a contener los cuadros capturados. El tamaño del buffer depende del número de cuadros que se requiera almacenar, así como del tamaño de cada cuadro. framesize = video_buf.width * video_buf.height * 3; ... main_buf = (char *)malloc(BUF_FRAME_NUM * framesize); Para llevar a cabo la captura de un cuadro se deben llevar a cabo dos llamadas, mediante ioctl(), al dispositivo de video. La primera llamada, conocida como VIDIOCMCAPTURE, se encarga de iniciar la captura de uno de los cuadros de la memoria de video. La otra llamada, VIDIOCSYNC, se encarga de sincronizar un cuadro capturado, y no retorna hasta que la captura del cuadro finalice. De esta forma, es posible preparar uno de los cuadros mientras se sincroniza otro, lo cual aumenta la eficiencia del proceso de captura. La forma más sencilla de implementar este método consiste en utilizar dos cuadros y sincronizar uno de ellos mientras el otro se prepara, sin embargo es posible llevar esto más allá y utilizar la totalidad de la memoria de video. Tal como se mencionó en la sección anterior, el número de cuadros con que se cuenta depende del dispositivo utilizado. El algoritmo necesario para realizar la captura utilizando n cuadros se muestra en la figura 4.1. Al realizar la lectura utilizando este método es posible obtener la máxima velocidad posible de captura permitida por el dispositivo. 32 Figura 4.1 Diagrama del algoritmo de captura de video utilizado. 33 Tal como se muestra en el diagrama de flujo de la figura 4.1, el primer paso consiste en preparar los cuadros iniciando la captura en cada uno de ellos. El cuadro sobre el cual se va a efectuar la acción se define utilizando el miembro frame, de la estructura del tipo video_mbuf. for(i = 0; i < buf_info.frames; i++) { video_buf.frame = i; ioctl(vfd, VIDIOCMCAPTURE, &video_buf); } Una vez hecho esto, se entra en el ciclo de captura, el cual recorrerá todos los cuadros de la memoria del dispositivo secuencialmente. En este caso, una vez que el cuadro se encuentra disponible, es copiado a la siguiente posición disponible del buffer principal del programa, con el fin de hacerlo disponible para el resto de las rutinas. 34 framecounter = 0; ... while(capture) { /* Si llegó al fin de la memoria del dispositivo */ if(i == buf_info.frames) i = 0; video_buf.frame = i; ioctl(vfd, VIDIOCSYNC, &video_buf); // Sincroniza ioctl(vfd, VIDIOCMCAPTURE, &video_buf); // Prepara para la siguiente // lectura /* Si llegó al final del buffer vuelve a empezar desde el inicio */ if(framecounter == BUF_FRAME_NUM) { framecounter = 0; ... } /* Copia el cuadro al buffer pricipal */ memcpy(main_buf + framecounter * framesize, map + buf_info.offsets[i], framesize); /* Incrementa ambos contadores */ i++; framecounter++; } Este ciclo se va a ejecutar hasta que el valor de la variable global capture sea 0. Esto le permite al hilo principal del programa iniciar o detener la captura de acuerdo con las solicitudes del cliente. Una vez que se sale del ciclo de captura, se libera la memoria utilizada por el buffer de video y se sale del hilo de ejecución. 35 free(main_buf); pthread_exit(NULL); Cuando el hilo principal ordena que se detenga la captura, debe asegurarse que el hilo sea destruido correctamente, con el fin de que se liberen todos los recursos correspondientes. Esto lo hace mediante una llamada a la función pthread_join(). Esta función detiene el flujo normal hasta que el hilo retorne. if(capture) { capture = 0; pthread_join(capture_thread, NULL); printf("Captura detenida\n"); } CAPÍTULO 5: Rutinas de comunicación con el cliente y transmisión de video sobre la red 5.1. Creación del socket TCP en el servidor Como se mencionó en el capítulo 3, el hilo de ejecución principal del programa, que se encarga de crear y administrar los otros hilos de ejecución, crea un socket TCP, una vez inicializado el dispositivo de video, con el fin de responder a las solicitudes del cliente. Esto brinda la posibilidad de controlar remotamente las funciones realizadas por el sistema de captura, y además monitorear su estado. El código necesario para crear el socket y esperar por conexiones del cliente se muestra a continuación: /* Creación del socket */ if((sd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { fprintf(stderr, "Problemas creando el socket\n"); pthread_exit((void *)-1); } /* Almacenamiento de los parámetros de conexión en la estructura sockaddr_in */ bzero((char *)&server, sizeof(struct sockaddr_in)); server.sin_family = AF_INET; server.sin_port = htons(PORT); server.sin_addr.s_addr = htonl(INADDR_ANY); /* Enlace del socket */ if(bind(sd, (struct sockaddr *)&server, sizeof(server)) == -1) { fprintf(stderr, "Error al enlazar el socket\n"); pthread_exit((void *)-1); } /* El socket espera conexiones */ listen(sd, 5); 36 37 /* Ciclo infinito de atención de conexiones */ while(1) { /* Acepta la conexión entrante */ client_len = sizeof(client); if((sd2 = accept(sd, (struct sockaddr *)&client, &client_len)) == -1) { fprintf(stderr, "No se puede aceptar la conexion\n"); pthread_exit((void *)-1); } ... close(sd2); } En primer lugar se crea el socket utilizando la llamada al sistema socket(). La combinación de los parámetros AF_INET y SOCK_STREAM indican al sistema operativo que se desea utilizar el protocolo TCP. Esta llamada retorna un descriptor de socket, el cual se utilizará para referirse al socket en el resto de la rutina. A continuación se almacenan los parámetros de la conexión, tales como el puerto, el protocolo y la dirección en la cual se esperaran las conexiones en la estructura sockaddr_in. Esta estructura se pasa como argumento a la llamada bind(), la cual se encarga de enlazar el socket. Esto provoca que el sistema operativo reserve el puerto para el proceso que lo solicitó, evitando que otro proceso pueda utilizarlo. Una vez llevado a cabo el enlace, se utiliza la llamada listen() para indicar al sistema que el programa va a manejar todas las conexiones entrantes por el socket sd. El segundo argumento de listen() corresponde al número de conexiones que el sistema operativo debe colocar en cola, en caso de que el programa no pueda atenderlas. En este caso, se pueden colocar en espera un máximo de 5 conexiones. Si existe una sexta solicitud, esta será rechazada por el sistema operativo. Posteriormente, el programa inicia un ciclo infinito para atender consecutivamente las conexiones entrantes. La llamada accept() hace que el programa duerma hasta que haya una solicitud de conexión. Cuando esta se presenta se crea un nuevo descriptor exclusivo para atender esta conexión (esto debido a que el envío de los datos al cliente se lleva a cabo a través de un puerto diferente, asignado de manera aleatoria por el sistema operativo). Una 38 vez finalizada la comunicación con el cliente, este descriptor es cerrado, y el programa vuelve a dormir, hasta que se de una nueva conexión. 5.2. Envío y recepción de datos El envío y la recepción de los datos se realizan utilizando las llamadas read() y write(), debido a que el descriptor del socket puede verse como si fuera un descriptor de archivos. De esta forma pueden enviarse y recibirse datos fácilmente, sin tener que recurrir a rutinas de bajo nivel, y sin necesidad de conocer a profundidad el funcionamiento del protocolo TCP/IP. El sistema operativo es el que se encarga de fragmentar la información en paquetes y de colocar los encabezados necesarios para su transmisión. Una vez establecida la conexión, el servidor envía un mensaje inicial al cliente, con el fin de indicarle que está listo para recibir peticiones. En este caso, el mensaje enviado es simplemente el nombre y la versión del programa: char out_buf[BUF_SIZE]; ... /* Envía mensaje de bienvenida */ strcpy(out_buf,"Telescreen v0.3"); write(sd2, out_buf, BUF_SIZE); Una vez enviado este mensaje, se inicia un ciclo infinito en el cual espera que el cliente haga una petición y la procesa. En este ciclo se llama a read() con el fin de leer el mensaje enviado por el cliente: char *buf_ptr; int bytes_left, n_size, n_read; while(1) { /* Para que el ciclo no ocupe todo el procesador */ usleep(100); /* Lectura de los datos enviados por el cliente */ bytes_left = BUF_SIZE; buf_ptr = in_buf; 39 while((n_read = read(sd2, buf_ptr, bytes_left)) > 0) { buf_ptr += n_read; bytes_left -= n_read; } /* Si no hay datos disponibles reinicia el ciclo */ if(n_read < 0) continue; ... Una vez que un mensaje es leído, este es identificado y procesado. Los mensajes implementados actualmente se muestran en el cuadro 5.1. Cuadro 5.1 Mensajes implementados para la comunicación entre el cliente y el servidor Mensaje CAPTURE NOCAPTURE STREAM NOSTREAM MOTION NOMOTION VIDEOPROP CLOSE Acción Inicia el hilo de captura Detiene la ejecución del hilo de captura Inicia el hilo encargado de enviar el video capturado al cliente Detiene el envío del video al cliente Inicia el hilo encargado de llevar a cabo la detección de movimiento Detiene la detección de movimiento en el video capturado Solicita el envío de las propiedades del cuadro de video capturado Cierra la conexión con el servidor La mayoría de los mensajes se encargan de iniciar o detener los hilos de procesamiento de las distintas partes que componen el sistema. A manera de ejemplo se muestra a continuación el código para manejar los mensajes CAPTURE y NOCAPTURE: if((strcmp(in_buf,"CAPTURE")) == 0) { if(!capture) { // Inicio de la captura capture = 1; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE); pthread_create(&capture_thread, &attr, capture_loop, NULL); } 40 strcpy(out_buf, "OK"); write(sd2, out_buf, BUF_SIZE); } else if((strcmp(in_buf, "NOCAPTURE")) == 0) { if(capture) { capture = 0; pthread_join(capture_thread, NULL); } strcpy(out_buf, "OK"); write(sd2, out_buf, BUF_SIZE); } // Fin de la captura En el caso de CAPTURE, se asigna 1 a la variable global capture, y se crea el hilo de procesamiento que llama a la función capture_loop(). Posteriormente se envía el mensaje «OK» al cliente, indicándole que el comando ha sido ejecutado satisfactoriamente. En lo que respecta a NOCAPTURE, la variable capture se pone en 0 lo que provoca que la función capture_loop() finalice. Por último se espera que el hilo de procesamiento finalice con la función pthread_join() y se envía el mensaje de confirmación al cliente. Los otros mensajes son implementados de forma similar, con excepción de VIDEOPROP. En el caso de este mensaje, el servidor debe enviar los datos del cuadro capturado, con el fin de que el programa cliente pueda desplegarlo. Estos datos son enviados en la estructura video_prop: /* Propiedades del cuadro enviado */ struct video_prop { int width; // Ancho int height; // Altura }; El código que atiende esta solicitud se muestra a continuación: /* Envío de las propiedades del cuadro capturado */ } else if((strcmp(in_buf, "VIDEOPROP")) == 0) { struct video_prop prop; char *prop_ptr = &prop; prop.width = video_buf.width; 41 prop.height = video_buf.height; write(sd2, prop_ptr, sizeof(struct video_prop)); printf("Propiedades de video enviadas\n"); strcpy(out_buf, "OK"); write(sd2, out_buf, BUF_SIZE); } 5.3. Envío del video capturado al cliente 5.3.1. Creación del socket UDP Como se mencionó en el capítulo 3, el envío de los cuadros de video capturados se lleva a cabo utilizando el protocolo UDP. Debido a esto, el primer paso para enviar los cuadros consiste en crear un socket que utilice este protocolo: /* Creación del socket UDP */ if((sd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) { fprintf(stderr, "Problemas creando el socket\n"); pthread_exit((void *)-1); } /* Información de la conexión */ bzero((char *)&server, sizeof(server)); server.sin_family = AF_INET; server.sin_port = htons(PORT); server.sin_addr.s_addr = htonl(INADDR_ANY); /* Enlace del socket */ if(bind(sd, (struct sockaddr *)&server, sizeof(server)) == -1) { fprintf(stderr, "Problemas al enlazar el socket\n"); pthread_exit((void *)-1); } /* Espera que el cliente solicite los datos */ client_len = sizeof(client); if((n = recvfrom(sd, buf, BUF_SIZE, 0, (struct sockaddr *)&client, &client_len)) < 0) { 42 fprintf(stderr, "Problemas recibiendo datagramas del cliente\n"); pthread_exit((void *)-1); } El proceso de creación del socket UDP es muy similar a la del socket TCP. La principal diferencia se da al utilizar la llamada socket(), a la cual se le pasa como parámetro la constante SOCK_DGRAM. Además, debido a que UDP es un protocolo no orientado a conexión, no se utilizan las llamadas listen() ni accept(). Una vez enlazado el socket ya se pueden enviar o recibir datos. Para llevar a cabo la comunicación es necesario únicamente utilizar las llamadas recvfrom() para el envío y sendto() para la recepción. En este caso, el servidor espera que el cliente envíe un paquete que le informe que esta listo para recibir el video. 5.3.2. Fragmentación y envío de los cuadros de video Una vez creado el socket, el siguiente paso consiste en el envío secuencial de los cuadros adquiridos por el hilo de captura. Debido a que el tamaño máximo de la carga útil de un paquete UDP es de aproximadamente 5200 bytes, cada cuadro de video debe fragmentarse en varias porciones para su envío. La rutina de envío se encarga de calcular el número de fragmentos necesarios de acuerdo al tamaño del cuadro, y de enviar la secuencia de fragmentos. Además, con el fin de facilitar la reconstrucción del cuadro por parte del cliente, se inserta un paquete de sincronización entre cada cuadro. Esto permite que el cliente pueda determinar el final de un cuadro y determinar si se perdieron fragmentos durante la transmisión. El código necesario para enviar los cuadros de video se incluye a continuación: while(stream) { /* Envía el cuadro anterior al que está siendo capturado */ if(framecounter) fc = framecounter - 1; else fc = BUF_FRAME_NUM; if(prev_fc == fc) continue; 43 prev_fc = fc; int fp = main_buf + framesize * fc; /* Se divide el frame en paquetes UDP de 4096 bytes */ int n_packets = framesize / 4096; int i; for(i = 0; i <= n_packets; i++) { if(sendto(sd, fp + i*4096, 4096, 0, (struct sockaddr *)&client, client_len) == -1) printf("Error enviando el paquete #%d\n", i); /* Para que no se pierdan paquetes en interfaces muy rápidas (ej. loopback) */ usleep(10); } /* Envío del paquete de sincronización */ int sync = n_packets; // Valor arbitrario, lo que importa es el tamaño if(sendto(sd, (char *)&sync, sizeof(int), 0, (struct sockaddr *)&client, client_len) == -1) printf("Error enviando paquete de sincronización\n"); counter++; } En este caso, el tamaño de cada paquete se establece en 4096 bytes. El paquete de sincronización es un entero, que tiene un tamaño de 4 bytes. De esta forma, el cliente puede determinar cual es el paquete de sincronización con base en la cantidad de bytes recibidos. CAPÍTULO 6: Rutinas de detección de movimiento y escritura a disco duro 6.1. Algoritmo de detección de movimiento El algoritmo utilizado para llevar a cabo la detección de movimiento consiste en tomar un cuadro, restarle el cuadro anterior y evaluar el resultado. Si la cantidad de pixeles diferentes entre un cuadro y el otro está por encima de cierto valor de umbral, puede considerarse que alguno de los elementos del cuadro cambió de posición, y por lo tanto se presentó un evento de movimiento. Si bien es cierto este algoritmo es uno de los más sencillos, funciona bastante bien para la aplicación implementada, dado se pretende realizar la detección en un escenario estacionario, donde las únicas perturbaciones corresponden a los eventos que se desean registrar. Uno de los principales problemas de este algoritmo es que es muy susceptible al ruido que se presenta durante la captura, principalmente con cámaras de baja calidad. Debido a esto es necesario aplicar un filtro a las imágenes con el fin de reducir este ruido y obtener mejores resultados. En la próxima sección se explica cual fue el filtro utilizado en este sistema, y como se llevó a cabo su implementación. El código que se encarga de realizar la detección de movimiento entre dos cuadros se muestra a continuación: /* Toma el cuadro anterior al cuadro que está siendo capturado */ fc = framecounter - 1; if(fc < 0) fc = 0; if(fc == prevfc) continue; else prevfc = fc; ... /* Convierte el cuadro actual y el anterior a escala de grises */ 44 45 grayscale(main_buf + fc * framesize, framesize, curr); grayscale(main_buf + (fc?(fc - 1):0) * framesize, framesize, prev); /* Aplica filtro a ambos cuadros */ mean_filter(curr, video_buf.width, video_buf.height); mean_filter(prev, video_buf.width, video_buf.height); /* Resta ambos cuadros y almacena la diferencia en dif */ int i; for(i = 0; i < framesize / 3; i++) { dif[i] = abs(curr[i] - prev[i]); /* Si hay diferencia entre los pixeles incrementa el contador */ if(dif[i] > ith) ndif++; } /* Calcula el porcentaje de pixeles diferentes */ pdif = (float)ndif/((float)framesize/3)*100; if(pdif > threshold) /* Hay movimiento */ ... La rutina toma los dos cuadros anteriores al que está siendo capturado y los convierte a escala de grises. Posteriormente aplica un filtro a ambos cuadros con el fin de reducir el ruido presente. Se trabaja en escala de grises con el fin de facilitar el procesamiento y aumentar la velocidad. Además, para detectar los cambios de posición sólo es necesario evaluar los cambios de intensidad de los pixeles, y no la diferencia de colores. La conversión a escala de grises se realiza utilizando la siguiente ecuación: I = 0, 30R + 0, 59G + 0, 11B (6.1) Donde I representa el valor de intensidad del pixel (escala de grises), R representa el valor de rojo del pixel de la imagen original, G el valor de verde y B el valor de azul. Una vez hecho esto, se realiza la resta de los cuadros, pixel por pixel. El resultado se almacena en el arreglo dif. Si la diferencia entre dos pixeles es mayor que cierto nivel de intensidad entonces, se considera el pixel como diferente y se incrementa la variable ndif, la 46 cual almacena la cantidad de pixeles diferentes. La razón por la cual no se considera cualquier diferencia entre los pixeles es que existen muchas oscilaciones pequeñas de intensidad entre dos cuadros, debido principalmente al ruido, que no son corregidas por el filtro. Por último, una vez analizada toda la imagen, se calcula el porcentaje de pixeles diferentes y se compara con el valor de umbral, almacenado en la variable threshold. Si este porcentaje es mayor que el valor de umbral, se considera que hubo movimiento entre los cuadros. Se utilizan valores porcentuales con el fin de simplificar la manipulación de las variables. De esta forma no es necesario conocer la cantidad de pixeles que componen una imagen. 6.2. Filtrado de las imágenes Como se mencionó anteriormente, el ruido afecta de manera significativa la detección de movimiento. Debido a esto, antes de realizar la resta de los cuadros se aplica un filtro paso bajos a las imágenes con el fin de reducir el nivel de ruido presente. El filtro paso bajos se encarga de atenuar los cambios drásticos de intensidad que existan entre un pixel y los pixeles cercanos. Esto se logra asignando al pixel el valor promedio de los pixeles que lo rodean, tal como se explica en el capítulo 2. En este caso, el filtro implementado es un filtro de media, que asigna a cada pixel el mismo peso, y utiliza la matriz de pesos que se muestra a continuación: W = 1/9 1/9 1/9 1/9 1/9 1/9 1/9 1/9 1/9 El código necesario para implementar el filtro se muestra a continuación: void mean_filter(unsigned char *frame, int x, int y) { int i, j; unsigned char *new_frame; /* Matriz de pesos */ float weights[9] = {0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11}; 47 /* Almacenamiento temporal para la imagen procesada */ new_frame = malloc(x * y); /* Aplica el filtro pixel por pixel for(i = 1; i < (y - 1); i++) for(j = 1; j < (x - 1); j++) { new_frame[i*x+j] = weights[0] * weights[1] * weights[2] * weights[3] * weights[4] * weights[5] * weights[6] * weights[7] * weights[8] * */ frame[(i-1)*x+(j-1)] + frame[(i-1)*x+j] + frame[(i-1)*x+(j+1)] + frame[i*x+(j-1)] + frame[i*x+j] + frame[i*x+(j+1)] + frame[(i+1)*x+(j-1)] + frame[(i+1)*x+j] + frame[(i+1)*x+(j+1)]; } /* Coloca pixeles negros en el borde de la imagen */ for(i = 0; i < y; i++) { new_frame[i*x] = 0; new_frame[i*x+(x-1)] = 0; } for(i = 0; i < x; i++) { new_frame[i] = 0; new_frame[(y-1)*x+i] = 0; } /* copia la imagen procesada sobre la imagen original y libera la memoria temporal */ memcpy(frame, new_frame, x * y); free(new_frame); } El filtro es aplicado a todos los pixeles, con excepción de los bordes, a los cuales se les asigna un 0 (color negro). Aunque esto trae consigo pérdida de información, el área de los bordes no es muy significativa en comparación con el área total de la imagen. Además, al asignar el mismo valor en todas las imágenes, la rutina de detección de movimiento no se ve afectada. 48 6.3. Determinación de eventos de movimiento Una diferencia entre dos cuadros por encima del nivel de umbral no es suficiente criterio para asegurar que se dio un evento de movimiento. Esta diferencia pudo ser provocada por un cambio repentino en la iluminación de la escena o por ruido que no pudo ser eliminado por el filtro. Es necesario que las diferencias entre los cuadros se mantenga por algún tiempo para que puedan considerarse un evento de movimiento real. Debido a esto se diseñó un algoritmo que determina cuando se presenta un evento real de movimiento. El algoritmo define un tiempo inicial durante el cual deben mantenerse las diferencias entre los cuadros para que se declare un evento de movimiento. Además se define un tiempo al final del evento durante el cual no debe haber movimiento para que el evento finalice. Si se presenta movimiento durante este periodo se reinicia el temporizador y el evento continua. La implementación de este algoritmo se llevó a cabo mediante una máquina de estados, cuyo diagrama se muestra en la figura 6.1. Los estados definidos para esta máquina se muestran en la tabla 6.1. Cuadro 6.1 Estados necesarios para el algoritmo de detección de eventos de movimiento Estado IDLE MSTARTED MNORMAL MSTOPED Significado En espera, no se ha detectado movimiento En el periodo de tiempo inicial En un evento de movimiento En el periodo de tiempo final Los cuadros que deben almacenarse al disco duro son los que se capturan en los estados MSTARTED, MNORMAL y MSTOPED. Además, a cada evento de movimiento se le añaden al inicio varios cuadros capturados antes de que empiece el movimiento, con el fin de que el usuario pueda ver como se encontraba el escenario antes de que se diera la perturbación. El fragmento de código que implementa la máquina de estados se muestra a continuación. Las partes que se encargan del almacenamiento han sido removidas, debido a que lo que interesa en este caso es la estructura de la máquina de estados. Las rutinas de almacenamiento serán explicadas posteriormente. 49 Figura 6.1 Diagrama de la máquina de estados encargada de detectar los eventos de movimiento. 50 /* Maquina de estados para almacenar los eventos capturados al disco */ switch(estado) { case IDLE: /* Si hay movimiento */ if(pdif > threshold) { estado = MSTARTED; gettimeofday(&start_time, &tz); } break; case MSTARTED: gettimeofday(&temp_time, &tz); /* Almacenamiento de los cuadros */ ... /* Si se cumplió el tiempo inicial */ if((temp_time.tv_sec - start_time.tv_sec)*1000 + (temp_time.tv_usec - temp_time.tv_usec)/1000 >= mstart_time) { if(pdif > threshold) { // Si hay movimiento estado = MNORMAL; printf("Evento de movimiento iniciado...\n"); /* Lanza el hilo de almacenamiento al disco duro */ ... } else { // Si no hay movimiento estado = IDLE; /* Destruye los cuadros almacenados en memoria */ ... } } break; case MNORMAL: 51 /* Almacenamiento del cuadro */ ... if(pdif < threshold) { // Si no hay movimiento estado = MSTOPED; gettimeofday(&stop_time, &tz); } break; case MSTOPED: /* Almacenamiento del cuadro */ ... if(pdif > threshold) // Si hay movimiento estado = MNORMAL; else { // Si no hay movimiento gettimeofday(&temp_time, &tz); /* Si se cumplió el tiempo final */ if((temp_time.tv_sec - stop_time.tv_sec)*1000 + (temp_time.tv_usec - temp_time.tv_usec)/1000 >= mstop_time) { ... printf("Evento de movimiento finalizado.\n"); } } break; } La temporización se lleva a cabo mediante la llamada al sistema gettimeofday(), que retorna la cantidad de segundos y de microsegundos desde epoch1 . Esta llamada proporciona una mayor precisión que time(), la cual solamente retorna segundos. 1 00:00:00 UTC del 1 de enero de 1970. Esta fecha se utiliza como referencia para el sistema de manejo de tiempo en la mayoría de los sistemas UNIX. 52 6.4. Rutinas de escritura a disco duro El almacenamiento a disco duro de los eventos de movimiento se lleva a cabo almacenando cada cuadro individualmente, como una imagen en formato JPEG. Esta imagen se genera utilizando la biblioteca de manipulación de imágenes imagemagick2 . Posteriormente se utiliza el programa mencoder3 para crear un archivo de video MPEG4 a partir de estas imágenes individuales. Debido al retardo que trae consigo la escritura a disco duro, los cuadros capturados son colocados en un buffer temporal en la memoria, y no son escritos inmediatamente. La escritura es llevada a cabo por un hilo de procesamiento diferente, que se ejecuta de manera paralela al hilo de detección de movimiento. Otra razón para utilizar almacenamiento temporal en memoria es que es necesario almacenar cuadros que no necesariamente van a formar parte de un evento. Esto se da cuando el mecanismo de detección se encuentra en el estado MSTARTED (ver sección anterior). En este caso, es posible que se detenga el movimiento antes de que se cumpla el tiempo inicial, lo que cancelaría el evento de movimiento. Es más sencillo y eficiente borrar estos cuadros de la memoria que del disco duro. La estructura de almacenamiento utilizada para esta memoria temporal consiste de una lista enlazada. Esto permite una mayor eficiencia, dado que permite agregar y eliminar elementos del de manera dinámica. En esta lista, cada elemento contiene un puntero a un cuadro de video, y además un puntero al siguiente elemento de la lista. En el caso del elemento final, este puntero se encuentra indefinido (apunta a NULL). Cada celda de la lista es implementada mediante una estructura: /* Lista enlazada para almacenar los cuadros */ typedef struct le { unsigned char *frame; // Puntero al cuadro struct le *next; // Puntero a la siguiente celda } list; Como se mencionó en la sección anterior, en cada evento de movimiento se almacenan, además de los cuadros propios del evento, varios cuadros anteriores al inicio del movimiento. Estos cuadros se toman de un buffer temporal y son colocados en la lista enlazada la primera vez que se entra al estado MSTARTED. 2 3 http://www.imagemagick.org/ http://www.mplayerhq.hu/ 53 /* Si no existe la lista enlazada */ if(start == NULL) { /* Crea la primera celda de la lista */ start = (list *)malloc(sizeof(list)); start->frame = (char *)malloc(framesize); memcpy(start->frame, nomotion + nmc * framesize, framesize); start->next = NULL; current = start; /* Copia los cuadros previos a la lista */ int i; for(i = nmc + 1; i < nmsize + nmc; i++) { temp = (list *)malloc(sizeof(list)); temp->frame = (char *)malloc(framesize); if(i >= nmsize) memcpy(temp->frame, nomotion + (i - nmsize) * framesize, framesize); else memcpy(temp->frame, nomotion + i * framesize, framesize); temp->next = NULL; current->next = temp; current = temp; } } La variable start es un puntero al primer elemento de la lista enlazada. Al inicializar este puntero se crea la primera celda, en la cual se almacena el cuadro más antiguo del buffer nomotion. Este buffer es un arreglo circular de tamaño nmsize en el cual se almacenan los cuadros que están siendo procesados. El último cuadro almacenado se encuentra en la posición nmc − 1, por lo que el cuadro más antiguo se encuentra en la posición nmc. Posteriormente se almacenan el resto de los cuadros. Para cada uno de ellos se crea una celda nueva utilizando la variable temporal temp. Posteriormente se hace que el puntero current, el cual indicaba la última celda apunte hacia este nuevo elemento. La rutina permanecerá en el estado MSTARTED hasta que se cumpla el tiempo inicial. Durante este periodo, todas las imágenes capturadas deben agregarse a la lista: temp = (list *)malloc(sizeof(list)); temp->frame = (char *)malloc(framesize); 54 memcpy(temp->frame, main_buf + fc * framesize, framesize); temp->next = NULL; current->next = temp; current = temp; Al igual que antes, se crea la nueva celda utilizando el puntero temp, y luego se mueve current hacia este nuevo elemento. Este mismo procedimiento se utiliza en los estados MNORMAL y MSTOPED para agregar los nuevos cuadros capturados a las lista. Si se llega al estado MNORMAL, hay un evento de movimiento real, lo que significa que los cuadros almacenados en la lista deben ser escritos a disco duro. Para esto se crea un nuevo hilo de procesamiento que se encargará de realizar la escritura. Este hilo ejecuta la función save_frames(), que recibe un puntero al primer elemento de la lista que se desea salvar como argumento. estado = MNORMAL; /* Lanza el hilo de almacenamiento al disco duro */ pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE); pthread_create(&save_thread, &attr, save_frames, (void *)start); pthread_attr_destroy(&attr); La función save_frames() crea un directorio para almacenar las imágenes, el cual tiene como nombre la fecha y la hora a la cual inicia el evento. Posteriormente la rutina recorre la lista, empezando por el primer elemento, y guarda cada uno de los cuadros en el disco duro. Una vez que un cuadro es almacenado, la memoria donde se encontraba es liberada y la celda es destruida. void *save_frames(void *list_start) { char timestamp[30]; char filename[50]; list *start = (list *)list_start; list *temp = NULL; 55 int i = 0; /* Determina la hora y la fecha actual */ time_t tptr; tptr = time(NULL); struct tm *tsptr; tsptr = localtime(&tptr); strftime(timestamp, 30, "%Y%m%d-%H%M%S", tsptr); /* Crea el directorio para almacenar las imágenes */ mkdir(timestamp, 0777); /* Bloquea el mutex */ pthread_mutex_lock(&mutexsave); while(1) { /* Guarda el cuadro */ snprintf(filename, 50, "%s/%04d.jpg", timestamp, i); save_image(start->frame, filename, video_buf.width, video_buf.height, "BGR"); /* Si llegó al final de la lista */ while(start->next == NULL) { /* Si no se van a agregar cuadros a la lista */ if(save_end) { /* libera la memoria */ free(start->frame); free(start); /* Desbloquea el mutex */ pthread_mutex_unlock(&mutexsave); /* Genera el archivo de video con mencoder */ printf("Generando archivo de video...\n"); char mencoder_line[150]; snprintf(mencoder_line, 150, "mencoder \"mf://%s/*.jpg\" -mf fps=%f:w=%d:h=%d:type=jpeg -ovc divx4 -o %s/%s.avi", timestamp, fps, video_buf.width, video_buf.height, timestamp, timestamp); system(mencoder_line); 56 /* Finaliza el hilo de ejecución */ pthread_exit(NULL); } /* Espera que se añada otro cuadro a la lista */ usleep(100); continue; } /* mueve start y libera la memoria */ temp = start; start = temp->next; free(temp->frame); free(temp); i++; } } Una de las cosas más importantes de esta función es la utilización de una variable mutex, con el fin de controlar el acceso a los recursos compartidos. En este caso, el mutex se utiliza para evitar que dos hilos de procesamiento guarden imágenes a la vez. Esto fue necesario debido a que la biblioteca imagemagick presentó problemas cuando se daba esta situación. Al utilizar el mutex, el primer hilo que se ejecute coloca un bloqueo sobre la variable utilizando la función pthread_mutex_lock(). Si otro hilo trata de ejecutar esta función sobre el mutex bloqueado será detenida hasta que el bloqueo sea eliminado. Una vez que la función termina de escribir las imágenes, esta elimina el bloqueo con la función pthread_mutex_unlock(), permitiendo que los otros hilos puedan llevar a cabo la escritura. La escritura de las imágenes es llevada a cabo por la función save_image(). Como se mencionó anteriormente, se utiliza la biblioteca imagemagick para llevar a cabo la creación y la escritura de la imagen. /* Crea una imagen y la salva al disco duro */ void save_image(unsigned char *data, char *filename, int width, int height, char *colorspace) { ExceptionInfo exception; Image *image; ImageInfo *image_info; 57 ... InitializeMagick((char *)NULL); GetExceptionInfo(&exception); image_info = CloneImageInfo((ImageInfo *) NULL); image = ConstituteImage(width, height, colorspace, CharPixel, data, &exception); (void)strcpy(image->filename, filename); WriteImage(image_info, image); DestroyImage(image); DestroyExceptionInfo(&exception); DestroyImageInfo(image_info); DestroyMagick(); } CAPÍTULO 7: Rutinas de comunicación y despliegue de video en el cliente 7.1. Envío de mensajes al servidor El envío de mensajes al servidor se lleva a cabo utilizando el protocolo TCP. Una vez iniciado el programa cliente, este llama a la función open_conn(), la cual se encarga de crear el socket y realizar la conexión al servidor, dejando al sistema listo para el envío de los mensajes. int open_conn(char *addr, int port) { struct hostent *hp; struct sockaddr_in server; char *buf_ptr, in_buf[BUF_SIZE]; int bytes_left; int n_read; /* Creación del socket */ if((sd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { fprintf(stderr, "No se puede abrir el socket\n"); return -1; } /* Información de la conexión */ memset((char *)&server, 0, sizeof(struct sockaddr_in)); server.sin_family = AF_INET; server.sin_port = htons(port); if((hp = gethostbyname(addr)) == NULL) { fprintf(stderr, "No se puede resolver el nombre del servidor\n"); return -1; } memcpy((char *)&server.sin_addr.s_addr, hp->h_addr, hp->h_length); /* Conexión con el servidor */ if(connect(sd, (struct sockaddr *)&server, sizeof(server)) == -1) { fprintf(stderr, "No se puede establecer la conexión 58 59 con el servidor\n"); return -1; } ... return 0; } El código es similar al estudiado en el capítulo 5, sin embargo presenta algunas diferencias importantes. En primer lugar, es necesario crear una estructura sockaddr_in con la información del servidor. La dirección del servidor se obtiene utilizando la función gethostbyname(), la cual recurre a las herramientas de resolución de nombres del sistema operativo. Otra diferencia con respecto al servidor es que utilizan las llamadas bind() o listen(), debido a que no es necesario esperar por conexiones. El cliente utiliza la llamada connect() para establecer la conexión con el servidor. Una vez establecido el enlace, es posible enviar o recibir mensajes, utilizando read() y write(). El envío de los mensajes es llevado a cabo por la función send_tcp_msg(): int send_tcp_msg(char *msg, void *retdata, int size) { char *buf_ptr, in_buf[BUF_SIZE], out_buf[BUF_SIZE]; int bytes_left, n_read; /* Envía el mensaje */ strcpy(out_buf, msg); write(sd, out_buf, BUF_SIZE); /* Recibe los datos, si hay algo que recibir */ if(retdata != NULL) { bytes_left = size; printf("bytes_left = %d\n", bytes_left); buf_ptr = retdata; while((n_read = read(sd, buf_ptr, bytes_left)) > 0) { buf_ptr += n_read; bytes_left -= n_read; } 60 } /* Recepción del mensaje de confirmación */ bytes_left = BUF_SIZE; buf_ptr = in_buf; while((n_read = read(sd, buf_ptr, bytes_left)) > 0) { buf_ptr += n_read; bytes_left -= n_read; } /* Verifica que el comando haya sido ejecutado satisfactoriamente */ if(strcmp(in_buf, "OK") == 0) return 0; else return -1; } Esta función envía el mensaje al servidor. Si el servidor debe retornar algo (como en el caso de VIDEOPROP), los datos son leídos y almacenados en la variable retdata. Por último, la función espera que el servidor envíe un mensaje de confirmación y con base en el contenido de este mensaje determina si el comando fue ejecutado de manera satisfactoria. Por ejemplo, a continuación se muestra el código necesario para enviar los mensajes CAPTURE y VIDEOPROP: /* Solicitud de inicio de captura */ if((msg_status = send_tcp_msg("CAPTURE", NULL, 0)) != 0) { fprintf(stderr, "Problemas solicitando el inicio de la captura\n"); exit(EXIT_FAILURE); } /* Petición de envío de las propiedades de video */ if((msg_status = send_tcp_msg("VIDEOPROP", (void *)&prop, sizeof(struct video_prop))) != 0) { fprintf(stderr, "Error obteniendo propiedades de video\n"); exit(EXIT_FAILURE); } 61 En el caso de CAPTURE, el comando no retornará datos, por lo tanto se envía NULL como segundo argumento. En VIDEOPROP, el comando retornará una estructura, del tipo video_prop, por lo tanto se le pasa a la función la variable prop para que almacene los datos, así como el tamaño de esta variable. 7.2. Recepción de los cuadros de video La recepción del video es realizada por un hilo de procesamiento diferente, el cual es creado después de solicitar al servidor el envío de los datos. /* Solicita al servidor el envío de los cuadros */ if((msg_status = send_tcp_msg("STREAM", NULL, 0)) != 0) { fprintf(stderr, "Error obteniendo el frame\n"); exit(EXIT_FAILURE); } /* Datos de la conexión */ strmdata.addr = "localhost"; strmdata.port = 24001; strmdata.width = prop.width; strmdata.height = prop.height; /* Crea el hilo de recepción de video */ pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE); pthread_create(&stream_thread, &attr, start_vid_stream, (void *)&strmdata); printf("Stream Iniciado\n"); Este hilo ejecuta la función start_vid_stream(), la cual recibe como argumento la variable strmdata, una estructura del tipo stream_data: /* Información de la conexión UDP */ struct stream_data { char *addr; // Dirección del servidor int port; // Puerto de conexión int width; // Ancho del cuadro 62 int height; }; // Altura del cuadro Una vez dentro de start_vid_stream(), primer paso para leer estos datos consiste en crear el socket correspondiente. /* Creación del socket UDP */ if((sdu = socket(AF_INET, SOCK_DGRAM, 0)) == -1) { fprintf(stderr, "Error creando el socket UDP\n"); exit(-1); } /* Información del servidor */ memset((char *)&server, 0, sizeof(server)); server.sin_family = AF_INET; server.sin_port = htons(sdata->port); if((hp = gethostbyname(sdata->addr)) == NULL) { fprintf(stderr, "Error resolviendo la dirección del servidor\n"); exit(-1); } memcpy((char *)&server.sin_addr.s_addr, hp->h_addr, hp->h_length); /* Información del cliente */ memset((char *)&client, 0, sizeof(client)); client.sin_family = AF_INET; client.sin_port = htons(0); client.sin_addr.s_addr = htonl(INADDR_ANY); /* Enlace del socket */ if(bind(sdu, (struct sockaddr *)&client, sizeof(client)) == -1) { fprintf(stderr, "Error enlazando el socket\n"); exit(-1); } 63 Una vez creado el socket, el cliente envía un paquete al servidor indicándole que está listo para recibir los datos: server_len = sizeof(server); strcpy(buf, "telescreen"); if(sendto(sdu, buf, BUF_SIZE, 0, (struct sockaddr *)&server, server_len) == -1) { fprintf(stderr, "Problemas enviando datagrama al servidor\n"); exit(-1); } El siguiente paso consiste en iniciar un ciclo en el cual se leerán los datos enviados por el servidor. Tal como se mencionó en el capítulo 5, cada cuadro debe fragmentarse en segmentos de 4096 bytes debido a las limitaciones del paquete UDP. El cliente debe leer cada uno de estos paquetes y reconstruir el cuadro para su despliegue: /* Número de paquetes */ int n_packets = framesize / 4096; /* Cantidad de bytes sobrantes */ int leftover = framesize % 4096; int pc = 0; int p_size; char buf2[4096]; /* Reserva memoria para los cuadros */ temp_frame = malloc(framesize); frame = malloc(framesize); /* Recibe los paquetes UDP con el cuadro */ while(1) { p_size = recvfrom(sdu, buf2, 4096, 0, (struct sockaddr *)&server, &server_len); if(p_size < 0) { // Error en la recepción fprintf(stderr, "Error recibiendo el paquete %d\n", pc); continue; } else if(p_size == sizeof(int)) { // Paquete de sincronización 64 if(pc == n_packets + 1) { memcpy(frame, temp_frame, framesize); } else { fprintf(stderr, "Imagen con información incorrecta\n"); } pc = 0; } else if(p_size == 4096) { // Paquete de datos if(pc == n_packets) memcpy(temp_frame + pc*4096, buf2, leftover); else memcpy(temp_frame + pc*4096, buf2, 4096); pc++; } } La rutina lee los paquetes que recibe a través del socket utilizando la llamada recvfrom(). Si el tamaño del paquete es incorrecto lo descarta y lee el paquete siguiente. Cuando recibe el paquete de sincronización verifica que número de paquetes recibidos sea correcto, o sea, que la imagen este completa y la copia en la posición apuntada por la variable global frame. Esta posición es leída por las rutinas de la interfaz gráfica para llevar a cabo el despliegue. Si el número de paquetes es incorrecta, la imagen completa se descarta y se continúa con la siguiente. 7.3. Despliegue de la imagen Como se explicó en el capítulo 3, se utilizó la biblioteca GTK 2.4 para crear la interfaz gráfica de usuario, así como para desplegar los cuadros de video recibidos del cliente. Las imágenes se muestran utilizando el widget image. Este widget permite desplegar una imagen dentro de una ventana, ya sea leída desde un archivo o contenida en un objeto GdkPixbuf. Estos objetos están diseñados para contener y administrar imágenes, y pueden ser inicializados a partir de un arreglo de bytes que contenga los pixeles de la imagen. En el cliente, la función refresh() se encarga de crear el GdkPixbuf a partir de la información contenida en frame. Posteriormente crea un widget image a partir de este objeto y lo despliega. 65 /* Despliega la imagen recibida */ void refresh(void) { if(frame != NULL) { /* Crea el objeto GdkPixbuf */ video_pixbuf = gdk_pixbuf_new_from_data((const guchar *)frame, GDK_COLORSPACE_RGB, FALSE, 8, prop.width, prop.height, prop.width * 3, NULL, NULL); /* Inicializa image a partir de video_pixbuf */ gtk_image_set_from_pixbuf(image, video_pixbuf); /* Despliega image */ gtk_widget_show(image); } } Esta rutina es llamada periódicamente por el ciclo de manejo de la interfaz gráfica. Esto se logra registrándola mediante la función g_timeout_add(): g_timeout_add(10, refresh, NULL); Esto le indica al ciclo principal de GTK que ejecute la rutina cada 10 milisegundos. CAPÍTULO 8: Resultados 8.1. Captura de video Se llevaron a cabo pruebas para determinar la velocidad de captura (tasa de cuadros por segundo) máxima de la aplicación. Estas pruebas se realizaron utilizando dos dispositivos: 1. Cámara USB marca Genius, modelo VideoCAM Express V2, controlador V4L spca50x. 2. Tarjeta de adquisición de video marca Avermedia, modelo TVPhone 98, controlador V4L bt848. Los resultados obtenidos en las pruebas de velocidad, al realizar la captura al buffer principal de la aplicación se muestran en el cuadro 8.1 Cuadro 8.1 Cuadros por segundo capturados. Dispositivo 1 2 Tasa (cuadros por segundo) 8,89 29.97 En ambos casos se alcanzó la tasa de captura máxima permitida por el dispositivo. La cámara USB es una cámara de baja calidad, por lo tanto no es posible capturar a una velocidad alta. Por otro lado, la tarjeta de captura permite una tasa máxima que va de acuerdo a los estándares de video profesional (alrededor de 30 cuadros por segundo). 8.2. Reducción de ruido Como se mencionó en la sección 6.2, se aplicó un filtro de media para atenuar el ruido presente en las imágenes capturadas y reducir su efecto en las rutinas de detección de movimiento. En la figura 8.1 se muestra una imagen capturada con el dispositivo 1 antes y después de aplicar el filtro de media para la reducción de ruido. En los recuadros se muestran dos casos particulares de pixeles con ruido. Puede verse como en ambos casos, este ruido ha sido atenuado de forma significativa, lo que permite eliminarlo al aplicar el umbral de sensibilidad durante de determinación de la cantidad de 66 67 (a) Antes de aplicar el filtro (b) Después de aplicar el filtro Figura 8.1 Aplicación del filtro de media para reducir el ruido en las imágenes capturadas pixeles con cambios. También puede verse como la imagen ha sido suavizada al aplicar el filtro. Esto es una característica que se presenta siempre que se aplica este tipo de filtros, debido a que se atenúan las transiciones fuertes (como es el caso de los bordes de los objetos) presentes en la imagen. Este suavizado no afecta el desempeño de la rutina de detección de movimiento, debido a que el efecto se presenta por igual en todas las imágenes y no altera la comparación. 8.3. Determinación de eventos de movimiento Otra de las partes de la aplicación que fue sometida a pruebas fue la rutina de determinación de eventos de movimiento. Estas pruebas se llevaron a cabo en distintos escenarios, con distintas condiciones de iluminación para evaluar la respuesta del sistema en un conjunto amplio de situaciones. En todas las pruebas el objeto en movimiento fue una persona, debido a que la aplicación principal de este programa es la vigilancia. 68 En todos los casos, fue posible detectar el movimiento y determinar de forma adecuada la presencia del evento, una vez ajustados los distintos parámetros del sistema: Umbral de sensibilidad Umbral de evento Tiempo inicial Tiempo final Al alterar estos valores es posible ajustar el sistema para que responda satisfactoriamente en distintas situaciones, siempre que las condiciones de luz permitan que la cámara capture una imagen nítida. En la figura 8.2 se muestra un gráfico del porcentaje de pixeles con diferencia por cuadro analizado, durante un evento de movimiento. Este evento consistió de una persona caminando a velocidad normal frente a la cámara. La distancia entre la persona y la cámara fue de aproximadamente 3 m y el escenario presentaba una iluminación normal (luz de día). Los parámetros del sistema de detección durante este evento se muestran en el cuadro 8.2. Cuadro 8.2 Parámetros del sistema de detección de eventos de movimiento utilizados durante la prueba Parámetro Umbral de sensibilidad Umbral de evento Tiempo inicial Tiempo final Valor 8 5% 50 ms 5s El evento que se registró durante la prueba tuvo una duración de 8 segundos, incluyendo el tiempo inicial y el final, así como los cuadros capturados antes que iniciara el evento. El movimiento real se mantiene por aproximadamente 1,5 s. Otra prueba que se realizó fue la respuesta del sistema ante un cambio rápido de iluminación. La figura 8.3 muestra la respuesta del sistema al encender y apagar la luz de una habitación. Los parámetros utilizados durante esta prueba son los mismos que se utilizaron en la prueba anterior (cuadro 8.2). En este caso, el sistema no consideró estas diferencias como un evento de movimiento, lo que muestra la importancia del tiempo inicial para eliminar las falsas alarmas que se pueden producir por alteraciones de esta clase. 69 35 r rrr r Porcentaje de pixeles con diferencias 30 25 r r 20 15 r r 10 r Umbral de evento 5 r r r rr 0 rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr 0 50 100 150 200 Cuadro analizado 250 Figura 8.2 Porcentaje de pixeles con diferencias durante un evento de movimiento 70 60 r Porcentaje de pixeles con diferencias r 50 40 30 r r 20 10 Umbral de evento r r 0 rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr 0 50 100 150 200 250 Cuadro analizado Figura 8.3 Respuesta del sistema ante un cambio rápido en la iluminación CAPÍTULO 9: Conclusiones y Recomendaciones 9.1. Conclusiones La utilización de múltiples hilos de ejecución (threads) permite que cada uno de los procesos se dedique a una tarea específica. Cada hilo trabaja de forma paralela a los otros, sin ser afectado significativamente por los demás, lo que trae consigo un mejor desempeño y facilita el desarrollo de los sistemas. La utilización de múltiples hilos de procesamiento permite además explotar al máximo sistemas de multiprocesamiento, en los cuales es posible obtener un procesamiento paralelo real. Se logró capturar video a la máxima tasa de cuadros por segundo al utilizar todos los cuadros de la memoria del dispositivo de video. Esta técnica permite capturar uno de los cuadros al mismo tiempo que los otros están siendo preparados, lo cual aumenta significativamente la eficiencia en la captura. Debido a la gran cantidad de ruido presente en las imágenes adquiridas, principalmente en el caso de las cámaras web fue necesario aplicar un filtro de media con el fin de reducir esta fuente de error. El filtro funcionó adecuadamente, reduciendo significativamente el ruido aleatorio presente en los cuadros capturados. La modificación de los valores de umbral durante el proceso de detección de movimiento permite ajustar la sensibilidad del sistema de una manera precisa, reduciendo la cantidad de eventos falsos, y permitiendo adaptar el sistema a una gran cantidad de escenarios. Se obtuvieron buenos resultados al utilizar el algoritmo de determinación de eventos de movimiento. La variación de los parámetros de tiempo permitió eliminar la influencia de los cambios repentinos en la intensidad de luz y de otros fenómenos que podían ocasionar el registro de un evento falso. La utilización de un hilo de procesamiento independiente para realizar la escritura de los cuadros capturados al disco duro redujo la influencia del retraso en la escritura a este medio sobre el desempeño del sistema. 71 72 Se obtuvieron buenos resultados al utilizar el protocolo UDP para la transmisión hacia el cliente de los cuadros de video capturados. Aunque hay una mayor pérdida de paquetes, este protocolo brinda una latencia menor, lo cual es fundamental en una aplicación de transmisión en tiempo real. La interfaz gráfica de usuario implementada permite visualizar en tiempo real el video adquirido por el sistema. Esta interfaz puede ser modificada para incorporar funciones adicionales y extender su capacidad. Esta aplicación constituye una plataforma de desarrollo que puede ser utilizada como componente de otros proyectos, debido a que sus características permiten adaptarla a una gran cantidad de aplicaciones. 9.2. Recomendaciones Es necesario realizar algún tipo de compresión en los datos que son enviados al cliente sobre la red. En la actualidad los cuadros son enviados tal como son capturados, lo cual hace que el volumen de datos sea muy grande. Esto hace que sea difícil utilizar la característica de video en tiempo real cuando la red es lenta o tiene mucho tráfico. Entre los métodos de compresión que se podrían utilizar se encuentran el sistema de compresión de video MPEG4 o los algoritmos de compresión LZO. Una forma de mejorar el desempeño durante el registro de los eventos de movimiento sería crear el archivo de video que contiene el evento en la memoria utilizando funciones de alguna biblioteca de codificación, en vez de escribir cada uno de los cuadros capturados y generar el archivo de video al final. El diseño del programa podría ser modificado para que sea posible la inclusión de otros filtros o rutinas de procesamiento, no sólo al realizar la detección de movimiento sino también durante la captura del video. Esto permitiría adaptar el sistema para otras aplicaciones, tales como rastreo de objetos o procesos de control de calidad. BIBLIOGRAFÍA [1] Baxes, G. «Digital Image Processing. Principles and Applications», primera edición, John Wiley & Sons, Estados Unidos, 1994. [2] Jähne, B. «Digital Image Processing. Concepts, Algorithms and Scientific Applications.», cuarta edición, Springer, Alemania, 1997. [3] Johnson, M. «Linux Information Sheet», The Linux Documentation Project. http://www.tldp.org/HOWTO/INFO-SHEET.html, 1998. [4] Kernighan, B. y Ritchie, D. «The C Programming Language», segunda edición. Prentice Hall, Estados Unidos, 1988. [5] Ondrik, M. et al. «Motion Detection Tecnologies», Canberra, http://www.canberra.com/pdf/Literature/motion.pdf, 1998. [6] Russ, J. «The Image Processing Handbook», CRC Press, Estados Unidos, 1992. [7] Wall, M. et al. «Linux Programming Unleashed», primera edición, Sams, Estados Unidos, 1999. [8] León-García, A. y Widjaja, I. «Redes de Comunicación. Conceptos Fundamentales y Arquitecturas Básicas», McGraw-Hill, España, 2002. [9] Wikipedia. «Kernel (Computer Science)», Wikipedia, http://en.wikipedia.org/wiki/kernel_(computer_science), julio, 2005. [10] Wikipedia. «POSIX», Wikipedia, http://en.wikipedia.org/wiki/POSIX, julio, 2005. [11] — «Video4Linux API». http://linux.bytesex.org/v4l2/API.html, marzo, 2005. [12] — «POSIX Threads Programming», http://www.llnl.gov/computing/tutorials/pthreads/, junio, 2005. 73 APÉNDICE A: Código fuente de la aplicación servidor global.h: Declaraciones y definiciones globales 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 /* * Telescreen v0.3 * Programa de captura de video y detección de movimiento utilizando V4L * global.h: Declaraciones y Definiciones globales * Copyright (C) 2005 Andrés Díaz Soto <[email protected]> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. * */ #include #include #include #include #include #include <linux/videodev.h> <stdio.h> <pthread.h> <assert.h> <sys/types.h> <sys/stat.h> #ifndef GLOBAL_H #define GLOBAL_H /* Variables Globales */ unsigned char *main_buf; // Buffer principal unsigned char *map; // Mapeo de la memoria del dispositivo struct video_capability cap; // Caracaterísticas del dispositivo de video struct video_mbuf buf_info; // Información de la memoria de video struct video_mmap video_buf; // Características de los cuadros de la memoria de video int vfd; // Descriptor de archivo del dispositivo de video int capture; // Estado del hilo de captura int stream; // Estado del hilo de envío de video int motion; // Estado del hilo de detección de movimiento int framecounter; // Contador de cuadro capturado int framesize; // Tamaño del cuadro (3bytes/pixel) float fps; // Cuadros por segundo #endif /* GLOBAL_H */ main.c: Rutina principal del servidor 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 /* * * * * * * * * * * * * * * Telescreen v0.3 Programa de captura de video y detección de movimiento utilizando V4L main.c: Implementación de la rutina principal Copyright (C) 2005 Andrés Díaz Soto <[email protected]> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 74 75 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. * */ #include "global.h" #include "video.h" #include "sockets.h" int main(int argc, char **argv) { /* Inicialización de variables globales */ main_buf = NULL; map = NULL; vfd = -1; capture = 0; stream = 0; motion = 0; printf("Telescreen v0.3\n"); /* Inicializa el dispositivo de video */ vfd = init_video(VIDEO_DEV); /* Verifica la existencia del dispositivo */ assert(vfd != -1); /* Define el canal */ set_chan(vfd, CHAN); /* Define propiedades de captura */ set_video_prop(cap.maxwidth, cap.maxheight, VIDEO_PALETTE_RGB24); /* Inicia el servidor */ tcpsocket(); } video.h: Declaraciones y definiciones para las rutinas de inicialización y captura de video 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 /* * Telescreen v0.3 * Programa de captura de video y detección de movimiento utilizando V4L * video.h: Declaraciones y definiciones para las rutinas de inicialización y * captura de video * Copyright (C) 2005 Andrés Díaz Soto <[email protected]> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. * */ #include #include #include #include #include #include #include <stdlib.h> <string.h> <fcntl.h> <sys/time.h> <sys/ioctl.h> <sys/mman.h> "global.h" 76 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 #ifndef VIDEO_H #define VIDEO_H /* Definiciones */ #define VIDEO_DEV "/dev/video0" #define CHAN 0 #define BUF_FRAME_NUM 50 // Dispositivo de video // Canal // Número de cuadros del buffer principal /* Declaraciones de las funciones */ /* init_video: Inicialización del dispositivo de video */ int init_video(char *video_dev); /* set_chan: Definición del canal de captura */ int set_chan(int fd, int chan); /* set_video_prop: Definición de las propiedades de captura */ int set_video_prop(int width, int height, int format); /* capture_loop: Ciclo de captura de video */ void *capture_loop(void *null); #endif /* VIDEO_H */ video.c: Implementación de las rutinas de inicialización y captura de video 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 /* * Telescreen v0.3 * Programa de captura de video y detección de movimiento utilizando V4L * video.c: Implementación de las rutinas de inicialización y captura de video * Copyright (C) 2005 Andrés Díaz Soto <[email protected]> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. * */ #include "video.h" /* init_video: Inicialización del dispositivo de video */ int init_video(char *video_dev) { int fd; /* Apertura del dispositivo */ if((fd = open(video_dev, O_RDWR)) == -1) { fprintf(stderr, "Error abriendo el dispositivo %s\n", video_dev); return -1; } /* Obtención de las capacidades de captura */ if(ioctl(fd, VIDIOCGCAP, &cap) == -1) { fprintf(stderr, "Error al realizar VIDIOCGCAP\n"); return -1; } /* Información de los Buffers */ if((ioctl(fd, VIDIOCGMBUF, &buf_info)) == -1) { printf("Error adquiriendo los buffers\n"); return -1; } 77 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 printf("Tamanno del buffer del dispositivo: %d\n", buf_info.size); map = mmap(0, buf_info.size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if(map == NULL) { printf("Error en el mapeo de la memoria\n"); return -1; } return fd; } /* set_chan: Definición del canal de captura */ int set_chan(int fd, int chan) { if(ioctl(fd, VIDIOCSCHAN, chan) == -1) return -1; else return 0; } /* set_video_prop: Definición de las propiedades de captura */ int set_video_prop(int width, int height, int format) { video_buf.width = width; video_buf.height = height; video_buf.format = format; return 0; } /* capture_loop: Ciclo de captura de video */ void *capture_loop(void *null) { struct timeval time1, time2; struct timezone tz; int i = 0; framesize = video_buf.width * video_buf.height * 3; /* Reserva de memoria para el buffer */ printf("Tamanno del buffer principal: %d\n", framesize * BUF_FRAME_NUM); main_buf = (char *)malloc(BUF_FRAME_NUM * framesize); framecounter = 0; gettimeofday(&time1, &tz); /* Ciclo de captura - Usando todos los buffers */ /* Prepara todos los cuadros para captura */ for(i = 0; i < buf_info.frames; i++) { video_buf.frame = i; ioctl(vfd, VIDIOCMCAPTURE, &video_buf); } i = 0; /* Para calcular la tasa de captura (fps) */ gettimeofday(&time1, &tz); /* Loop infinito de captura */ while(capture) { /* Si llegó al fin de la memoria del dispositivo */ if(i == buf_info.frames) i = 0; video_buf.frame = i; ioctl(vfd, VIDIOCSYNC, &video_buf); ioctl(vfd, VIDIOCMCAPTURE, &video_buf); // Sincroniza // Prepara para la siguiente lectura /* Si llegó al final del buffer vuelve a empezar desde el incio */ if(framecounter == BUF_FRAME_NUM) { framecounter = 0; gettimeofday(&time2, &tz); /* Calcula la tasa de captura */ fps = BUF_FRAME_NUM / ((float)(time2.tv_sec-time1.tv_sec) + (float)(time2.tv_usec-time1.tv_usec) / 1000000.0); printf("Tasa de captura: %f fps\n", fps); gettimeofday(&time1, &tz); 78 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 } /* Copia el cuadro al buffer principal */ memcpy(main_buf + framecounter * framesize, map + buf_info.offsets[i], framesize); /* Incrementa ambos contadores */ i++; framecounter++; } pthread_exit(NULL); } motion.h: Declaraciones y definiciones para las rutinas de detección de movimiento y escritura a disco duro 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 /* * Telescreen v0.3 * Programa de captura de video y detección de movimiento utilizando V4L * motion.h: Declaraciones y definiciones para las rutinas de detección de movimiento y * escritura a disco duro * Copyright (C) 2005 Andrés Díaz Soto <[email protected]> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. * */ #include #include #include #include #include #include "global.h" <string.h> <stdlib.h> <unistd.h> <sys/time.h> <magick/api.h> #ifndef MOTION_H #define MOTION_H /* Valores de umbral Estos valores varían según las condiciones del escenario */ #define THRESHOLD 5 // Umbral de evento #define ITH 8 // Umbral de sensibilidad #define NMSIZE 20 /* Estados de captura */ #define #define #define #define IDLE 0 // No se ha detecctado movimiento MSTARTED 1 // Tiempo inicial MNORMAL 2 // Evento de movimiento MSTOPED 3 // Tiempo final /* Lista enlazada para almacenar los cuadros */ typedef struct le { unsigned char *frame; // Puntero al cuadro struct le *next; // Puntero a la siguiente celda } list; /* Declaraciones de las funciones */ /* motion_detect: Rutina de detección de movimiento y determinación de eventos */ 79 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 void *motion_detect(void *null); /* mean_filter: Filtro de media para la reducción de ruido */ void mean_filter(unsigned char *frame, int x, int y); /* grayscale: Conversión de la imagen a escala de grises (intensidad) */ void grayscale(unsigned char *frame, int size, char *new_frame); /* save_frames: Almacenamiento de las imágenes a disco duro y generación del archivo de video */ void *save_frames(void *list_start); /* save_image: Almacenamiento de una imágen a disco duro utilizando ImageMagick */ void save_image(unsigned char *data, char *filename, int width, int height, char *colorspace); #endif /* MOTION_H */ motion.c: Implementación de las rutinas de detección de movimiento y escritura a disco duro 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 /* * Telescreen v0.3 * Programa de captura de video y detección de movimiento utilizando V4L * motion.c: Implementación las rutinas de detección de movimiento y * escritura a disco duro * Copyright (C) 2005 Andrés Díaz Soto <[email protected]> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. * */ #include "motion.h" /* Variables globales */ static int nmsize; static pthread_mutex_t mutexsave; static int save_end; void *motion_detect(void *null) { int fc = -1, prevfc = -1; unsigned char *dif, *curr, *prev; int ndif; int save = 1; float threshold = THRESHOLD; int ith = ITH; float pdif; unsigned char *nomotion; nmsize = NMSIZE; int nmc = 0; int estado = IDLE; int mstart_time, mstop_time, seqn; struct timeval start_time, temp_time, stop_time; struct timezone tz; pthread_t save_thread; pthread_attr_t attr; 80 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 list *start = NULL, *current = NULL, *temp = NULL; save_end = 0; seqn = nmsize; mstart_time = 50; mstop_time = 5000; dif = malloc(framesize / 3); curr = malloc(framesize / 3); prev = malloc(framesize / 3); nomotion = malloc(nmsize * framesize); FILE *stats = fopen("stats", "w"); int sframe = 0; FILE *estados = fopen("estados", "w"); pthread_mutex_init(&mutexsave, NULL); while(motion) { /* Toma el cuadro anterior al que está siendo caoturado */ fc = framecounter - 1; if(fc < 0) fc = 0; if(fc == prevfc) continue; else prevfc = fc; ndif = 0; memcpy(nomotion + nmc * framesize, main_buf + fc * framesize, framesize); nmc++; if(nmc == nmsize) nmc = 0; /* Convierte el cuadro actual y el anterior a escala de grises */ grayscale(main_buf + fc * framesize, framesize, curr); grayscale(main_buf + (fc?(fc - 1):0) * framesize, framesize, prev); //DEBUG if(save) { save_image(curr, "current_gray.jpg", video_buf.width, video_buf.height, "I"); save_image(prev, "previous_gray.jpg", video_buf.width, video_buf.height, "I"); } /* Aplica filtro a ambos cuadros */ mean_filter(curr, video_buf.width, video_buf.height); mean_filter(prev, video_buf.width, video_buf.height); //DEBUG if(save) { save_image(curr, "current.jpg", video_buf.width, video_buf.height, "I"); save_image(prev, "previous.jpg", video_buf.width, video_buf.height, "I"); save = 0; } /* Resta ambos cuadros y almacena la diferencia en dif */ int i; for(i = 0; i < framesize / 3; i++) { dif[i] = abs(curr[i] - prev[i]); if(dif[i] > ith) ndif++; } /* Calcula el porcentaje de pixeles diferentes */ pdif = (float)ndif/((float)framesize/3)*100; fprintf(stats, "%d\t%f\n", sframe, pdif); sframe++; /* Maquina de estados para almacenar los eventos capturados al disco */ switch(estado) { case IDLE: fprintf(estados, "Estado: IDLE, fc = %d\n", fc); if(pdif > threshold) { estado = MSTARTED; gettimeofday(&start_time, &tz); } 81 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 break; case MSTARTED: fprintf(estados, "Estado: MSTARTED, fc = %d, ", fc); gettimeofday(&temp_time, &tz); /* Almacenamiento de los cuadros */ if(start == NULL) { // Si no existe la lista enlazada fprintf(estados, "start = NULL"); /* Crea la primera celda de la lista */ start = (list *)malloc(sizeof(list)); start->frame = (char *)malloc(framesize); memcpy(start->frame, nomotion + nmc * framesize, framesize); start->next = NULL; current = start; /* Copia los cuadros previos a la lista */ int i; for(i = nmc + 1; i < nmsize + nmc; i++) { temp = (list *)malloc(sizeof(list)); temp->frame = (char *)malloc(framesize); if(i >= nmsize) memcpy(temp->frame, nomotion + (i - nmsize) * framesize, framesize); else memcpy(temp->frame, nomotion + i * framesize, framesize); temp->next = NULL; current->next = temp; current = temp; } } else { temp = (list *)malloc(sizeof(list)); temp->frame = (char *)malloc(framesize); memcpy(temp->frame, main_buf + fc * framesize, framesize); temp->next = NULL; current->next = temp; current = temp; } /* Si se cumplió el tiempo inicial */ if((temp_time.tv_sec - start_time.tv_sec)*1000 + (temp_time.tv_usec - temp_time.tv_usec)/1000 >= mstart_time) { if(pdif > threshold) { // Si hay movimiento estado = MNORMAL; printf("Evento de movimiento iniciado...\n"); /* Lanza el hilo de almacenamiento al disco duro */ pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE); pthread_create(&save_thread, &attr, save_frames, (void *)start); pthread_attr_destroy(&attr); } else { // Si no hay movimiento estado = IDLE; seqn = nmsize; /* Destruye los cuadros almacenados en memoria */ while(start != NULL) { temp = start; start = temp->next; free(temp->frame); free(temp); } temp = NULL; current = NULL; } } fprintf(estados, "\n"); break; case MNORMAL: fprintf(estados, "Estado: MNORMAL, fc = %d\n", fc); /* Almacenamiento del cuadro */ temp = (list *)malloc(sizeof(list)); temp->frame = (char *)malloc(framesize); memcpy(temp->frame, main_buf + fc * framesize, framesize); temp->next = NULL; current->next = temp; current = temp; if(pdif < threshold) { // Si no hay movimiento estado = MSTOPED; gettimeofday(&stop_time, &tz); } break; case MSTOPED: fprintf(estados, "Estado: MSTOPED, fc = %d\n", fc); /* Almacenamiento del cuadro */ temp = (list *)malloc(sizeof(list)); temp->frame = (char *)malloc(framesize); memcpy(temp->frame, main_buf + fc * framesize, framesize); temp->next = NULL; current->next = temp; current = temp; if(pdif > threshold) // Si hay moviento 82 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 estado = MNORMAL; else { // Si no hay movimiento gettimeofday(&temp_time, &tz); /* Si se cumplió el tiempo final */ if((temp_time.tv_sec - stop_time.tv_sec)*1000 + (temp_time.tv_usec - temp_time.tv_usec)/1000 >= mstop_time) { estado = IDLE; /* Inicializa los punteros de la lista */ start = NULL; current = NULL; temp = NULL; save_end = 1; printf("Evento de movimiento finalizado.\n"); } } break; } } /* Libera memoria */ free(dif); free(curr); free(prev); fclose(stats); fclose(estados); pthread_mutex_destroy(&mutexsave); pthread_exit(NULL); } /* mean_filter: Filtro de media para la reducción de ruido */ void mean_filter(unsigned char *frame, int x, int y) { int i, j; unsigned char *new_frame; /* Matriz de pesos */ float weights[9] = {0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11}; /* Almacenamiento temporal para la imagen procesada */ new_frame = malloc(x * y); /* Aplica el filtro pixel por pixel for(i = 1; i < (y - 1); i++) for(j = 1; j < (x - 1); j++) { new_frame[i*x+j] = weights[0] * weights[1] * weights[2] * weights[3] * weights[4] * weights[5] * weights[6] * weights[7] * weights[8] * */ frame[(i-1)*x+(j-1)] + frame[(i-1)*x+j] + frame[(i-1)*x+(j+1)] + frame[i*x+(j-1)] + frame[i*x+j] + frame[i*x+(j+1)] + frame[(i+1)*x+(j-1)] + frame[(i+1)*x+j] + frame[(i+1)*x+(j+1)]; } /* Coloca pixeles negros en el borde de la imagen */ for(i = 0; i < y; i++) { new_frame[i*x] = 0; new_frame[i*x+(x-1)] = 0; } for(i = 0; i < x; i++) { new_frame[i] = 0; new_frame[(y-1)*x+i] = 0; } /* Copia la imagen procesada sobre la imagen actual y libera la memoria temporal */ memcpy(frame, new_frame, x * y); free(new_frame); } /* grayscale: Conversión de la imagen a escala de grises (intensidad) */ void grayscale(unsigned char *frame, int size, char *new_frame) { int i, j; float temp; for(i = 0, j = 0; i < size; i += 3, j++) { temp = 0.11*frame[i] + 0.59*frame[i+1] + 0.3*frame[i+2]; if(temp >= 255) new_frame[j] = 254; else new_frame[j] = temp; 83 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 } } /* save_frames: Almacenamiento de las imágenes a disco duro y generación del archivo de video */ void *save_frames(void *list_start) { char timestamp[30]; char filename[50]; list *start = (list *)list_start; list *temp = NULL; int i = 0; /* Determina la hora y la fecha actual */ time_t tptr; tptr = time(NULL); struct tm *tsptr; tsptr = localtime(&tptr); strftime(timestamp, 30, "%Y%m%d-%H%M%S", tsptr); /* Crea el direcctorio para almacenar las imágenes */ mkdir(timestamp, 0777); /* Bloquea el mutex */ pthread_mutex_lock(&mutexsave); while(1) { /* Guarda el cuadro */ snprintf(filename, 50, "%s/%04d.jpg", timestamp, i); save_image(start->frame, filename, video_buf.width, video_buf.height, "BGR"); /* Si llegó al final de la lista */ while(start->next == NULL) { /* Si no se van a agregar cuadros a la lista */ if(save_end) { /* Libera la memoria */ free(start->frame); free(start); /* Desbloquea el mutex */ pthread_mutex_unlock(&mutexsave); /* Genera el archivo de video con memcoder */ printf("Generando archivo de video...\n"); char mencoder_line[150]; snprintf(mencoder_line, 150, "mencoder \"mf://%s/*.jpg\" -mf fps=%f:w=%d:h=%d:type=jpeg -ovc divx4 -o %s/%s.avi", timestamp, fps, video_buf.width, video_buf.height, timestamp, timestamp); system(mencoder_line); /* Finaliza el hilo de ejecución */ pthread_exit(NULL); } /* Espera a que se añada otro cuadro a la lista */ usleep(100); continue; } /* mueve start y libera la memoria */ temp = start; start = temp->next; free(temp->frame); free(temp); i++; } } /* save_image: Almacenamiento de una imágen a disco duro utilizando ImageMagick */ void save_image(unsigned char *data, char *filename, int width, int height, char *colorspace) { ExceptionInfo exception; Image *image; ImageInfo *image_info; /* Bug? de imagemagick: 255 -> 0 si la imagen está en valores de intensidad*/ if(strcmp(colorspace, "I") == 0) { int i; for(i = 0; i < width * height; i++) if(data[i] == 255) data[i] = 254; } InitializeMagick((char *)NULL); 84 395 396 397 398 399 400 401 402 403 404 405 406 407 408 GetExceptionInfo(&exception); image_info = CloneImageInfo((ImageInfo *) NULL); image = ConstituteImage(width, height, colorspace, CharPixel, data, &exception); (void)strcpy(image->filename, filename); WriteImage(image_info, image); DestroyImage(image); DestroyExceptionInfo(&exception); DestroyImageInfo(image_info); DestroyMagick(); } sockets.h: Declaraciones y definiciones para las rutinas de comunicación con el cliente y transmisión de video 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 /* * Telescreen v0.3 * Programa de captura de video y detección de movimiento utilizando V4L * sockets.h: Declaraciones y definiciones para las rutinas de comunicación con * el cliente y transmisión de video * Copyright (C) 2005 Andrés Díaz Soto <[email protected]> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. * */ #include #include #include #include #include #include #include <sys/socket.h> <netinet/in.h> <string.h> <unistd.h> "global.h" "video.h" "motion.h" #ifndef SOCKETS_H #define SOCKETS_H /* Definiciones */ #define PORT 24001 // Puerto del servidor #define BUF_SIZE 256 // Tamaño del buffer de datos /* Propiedades del cuadro enviado */ struct video_prop { int width; // Ancho int height; // Altura }; /* Declaración de funciones */ /* tcpsocket: Servidor TCP para el hilo principal */ void tcpsocket(void); /* udpsocket: Envío de los cuadros de video al cliente */ void *udpsocket(void *); #endif /* SOCKETS_H */ 85 sockets.c: Implementación de las rutinas de comunicación con el cliente y transmisión de video 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 /* * Telescreen v0.3 * Programa de captura de video y detección de movimiento utilizando V4L * sockets.c: Implementación de las rutinas de comunicación con * el cliente y transmisión de video * Copyright (C) 2005 Andrés Díaz Soto <[email protected]> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. * */ #include "sockets.h" /* tcpsocket: Servidor TCP para el hilo principal */ void tcpsocket(void) { int sd, sd2, client_len; struct sockaddr_in server, client; char *buf_ptr, out_buf[BUF_SIZE], in_buf[BUF_SIZE]; pthread_t capture_thread, udp_thread, motion_thread; pthread_attr_t attr; /* Creación del socket */ if((sd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { fprintf(stderr, "Problemas creando el socket\n"); pthread_exit((void *)-1); } /* Almacenamiento de los parámetros de conexión en la estructura sockaddr_in */ memset((char *)&server, 0, sizeof(struct sockaddr_in)); server.sin_family = AF_INET; server.sin_port = htons(PORT); server.sin_addr.s_addr = htonl(INADDR_ANY); /* Enlace del socket */ if(bind(sd, (struct sockaddr *)&server, sizeof(server)) == -1) { fprintf(stderr, "Error al enlazar el socket\n"); pthread_exit((void *)-1); } /* El socket espera conexiones */ listen(sd, 5); /* Ciclo infinito de atención de conexiones */ while(1) { /* Acepta la conexión entrante */ client_len = sizeof(client); if((sd2 = accept(sd, (struct sockaddr *)&client, &client_len)) == -1) { fprintf(stderr, "No se puede aceptar la conexion\n"); pthread_exit((void *)-1); } /* Envía mensaje de bienvenida */ strcpy(out_buf,"Telescreen v0.1"); write(sd2, out_buf, BUF_SIZE); int n_read, bytes_left; while(1) { /* Para que el ciclo no ocupe todo el procesador */ usleep(100); /* Lectura de los datos enviados por el cliente */ bytes_left = BUF_SIZE; buf_ptr = in_buf; 86 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 while((n_read = read(sd2, buf_ptr, bytes_left)) > 0) { buf_ptr += n_read; bytes_left -= n_read; } if(n_read < 0) continue; if((strcmp(in_buf,"CAPTURE")) == 0) { // Inicio de la captura if(!capture) { capture = 1; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE); pthread_create(&capture_thread, &attr, capture_loop, NULL); printf("Captura Iniciada\n"); } strcpy(out_buf, "OK"); write(sd2, out_buf, BUF_SIZE); } else if((strcmp(in_buf, "NOCAPTURE")) == 0) { // Fin de la captura if(capture) { capture = 0; pthread_join(capture_thread, NULL); printf("Captura detenida\n"); } strcpy(out_buf, "OK"); write(sd2, out_buf, BUF_SIZE); } else if((strcmp(in_buf, "VIDEOPROP")) == 0) { // Envío de las propiedades del video capturado struct video_prop prop; char *prop_ptr = (char *)&prop; prop.width = video_buf.width; prop.height = video_buf.height; write(sd2, prop_ptr, sizeof(struct video_prop)); printf("Propiedades de video enviadas\n"); strcpy(out_buf, "OK"); write(sd2, out_buf, BUF_SIZE); } else if(strcmp(in_buf, "STREAM") == 0) { // Inicio del envío del video al cliente if(!stream) { stream = 1; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE); pthread_create(&udp_thread, &attr, udpsocket, NULL); printf("Transmisión del video iniciada\n"); } strcpy(out_buf, "OK"); write(sd2, out_buf, BUF_SIZE); } else if(strcmp(in_buf, "NOSTREAM") == 0) { // Fin del envío del video if(stream) { stream = 0; pthread_join(udp_thread, NULL); } strcpy(out_buf, "OK"); write(sd2, out_buf, BUF_SIZE); } else if(strcmp(in_buf, "MOTION") == 0) { // Inicio de la detección de movimiento if(!motion) { motion = 1; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE); pthread_create(&motion_thread, &attr, motion_detect, NULL); printf("Detección de movimiento iniciada\n"); } strcpy(out_buf, "OK"); write(sd2, out_buf, BUF_SIZE); } else if(strcmp(in_buf, "NOMOTION") == 0) { // Fin de la detección de movimiento if(motion) { motion = 0; pthread_join(motion_thread, NULL); } strcpy(out_buf, "OK"); write(sd2, out_buf, BUF_SIZE); } else if(strcmp(in_buf, "CLOSE") == 0) { // Finalización de la conexión printf("Conexión finalizada\n"); strcpy(out_buf, "OK"); write(sd2, out_buf, BUF_SIZE); break; } else { // Comando desconocido printf("Comando desconocido\n"); strcpy(out_buf, "UNKNOWN"); write(sd2, out_buf, BUF_SIZE); } } printf("Conexión con el cliente finalizada.\n"); close(sd2); } 87 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 close(sd); } /* udpsocket: Envío de los cuadros de video al cliente */ void *udpsocket(void *null) { int sd, client_len; char buf[BUF_SIZE]; struct sockaddr_in server, client; int fc = 0, prev_fc = -1; int counter = 0; /* Creación del socket UDP */ if((sd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) { fprintf(stderr, "Problemas creando el socket\n"); pthread_exit((void *)-1); } /* Información de la conexión */ memset((char *)&server, 0, sizeof(server)); server.sin_family = AF_INET; server.sin_port = htons(PORT); server.sin_addr.s_addr = htonl(INADDR_ANY); /* Enlace del socket */ if(bind(sd, (struct sockaddr *)&server, sizeof(server)) == -1) { fprintf(stderr, "Problemas al enlazar el socket\n"); pthread_exit((void *)-1); } int n; /* Espera que el cliente solicite los datos */ client_len = sizeof(client); if((n = recvfrom(sd, buf, BUF_SIZE, 0, (struct sockaddr *)&client, &client_len)) < 0) { fprintf(stderr, "Problemas recibiendo datagramas del ciente\n"); pthread_exit((void *)-1); } while(stream) { /* Envía el cuadro anterior al que está siendo capturado */ if(framecounter) fc = framecounter - 1; else fc = BUF_FRAME_NUM; if(prev_fc == fc) continue; prev_fc = fc; unsigned char *fp = main_buf + framesize * fc; /* Se divide el frame en paquetes UDP de 4096 bytes */ int n_packets = framesize / 4096; int i; for(i = 0; i <= n_packets; i++) { if(sendto(sd, (void *)(fp + i*4096), 4096, 0, (struct sockaddr *)&client, client_len) == -1) printf("Error enviando el paquete #%d\n", i); /* Para que no se pierdan paquetes en interfaces muy rápidas (ej. loopback) */ usleep(10); } /* Envío del paquete de sincronización */ int sync = n_packets; // Valor arbitrario, lo que importa es el tamaño if(sendto(sd, (char *)&sync, sizeof(int), 0, (struct sockaddr *)&client, client_len) == -1) printf("Error enviando paquete de sincronización\n"); counter++; } close(sd); pthread_exit(NULL); } APÉNDICE B: Código fuente de la aplicación cliente main.c: Rutina principal del cliente 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 /* * Telescreen v0.3 * Programa de captura de video y detección de movimiento utilizando V4L * main.c: Rutina principal del cliente * Copyright (C) 2005 Andrés Díaz Soto <[email protected]> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. * */ #include #include #include #include #include static static static extern <stdio.h> <stdlib.h> <gtk/gtk.h> <pthread.h> "net.h" GtkWidget *image; GdkPixbuf *video_pixbuf; struct video_prop prop; unsigned char *frame; /* refresh: Despliega la imagen recibida */ void refresh(void) { printf("En refresh()\n"); if(frame != NULL) { /* Crea el objeto GdkPixbuf */ video_pixbuf = gdk_pixbuf_new_from_data((const guchar *)frame, GDK_COLORSPACE_RGB, FALSE, 8, prop.width, prop.height, prop.width * 3, NULL, NULL); /* Inicializa image a partir de video_pixbuf */ gtk_image_set_from_pixbuf(image, video_pixbuf); /* Despliega image */ gtk_widget_show(image); } } int main(int argc, char *argv[]) { GtkWidget *window; int msg_status = -1; char *frame = NULL; struct stream_data strmdata; pthread_t stream_thread; pthread_attr_t attr; int trc; /* Inicialización GTK */ gtk_init(&argc, &argv); window = gtk_window_new(GTK_WINDOW_TOPLEVEL); /* Manejo de señales GTK */ g_signal_connect(G_OBJECT(window), "delete_event", G_CALLBACK(gtk_widget_destroy), NULL); g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(gtk_main_quit), NULL); /* Establecimiento de la conexión */ if(open_conn("localhost", 24001) != 0) { fprintf(stderr, "Error iniciando la conexión con el servidor.\n"); exit(EXIT_FAILURE); } 88 89 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 /* Solicitud de inicio de captura */ if((msg_status = send_tcp_msg("CAPTURE", NULL, 0)) != 0) { fprintf(stderr, "Problemas solicitando el inicio de la captura\n"); exit(EXIT_FAILURE); } /* Petición de envío de las propiedades de video */ if((msg_status = send_tcp_msg("VIDEOPROP", (void *)&prop, sizeof(struct video_prop))) != 0) { fprintf(stderr, "Error obteniendo propiedades de video\n"); exit(EXIT_FAILURE); } sleep(5); /* Solicita el inicio de la detección de movimiento */ if((msg_status = send_tcp_msg("MOTION", NULL, 0)) != 0) { fprintf(stderr, "Error iniciando detección de movimiento\n"); exit(EXIT_FAILURE); } /* Solicita al servidor el envío de los cuadros */ if((msg_status = send_tcp_msg("STREAM", NULL, 0)) != 0) { fprintf(stderr, "Error obteniendo el frame\n"); exit(EXIT_FAILURE); } /* Datos de la conexión UDP */ strmdata.addr = "localhost"; strmdata.port = 24001; strmdata.width = prop.width; strmdata.height = prop.height; /* Crea el hilo de recepción de video */ pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE); pthread_create(&stream_thread, &attr, start_vid_stream, (void *)&strmdata); printf("Stream Iniciado\n"); /* Inicialización widgets GTK */ image = gtk_image_new(); gtk_container_add(GTK_CONTAINER(window), image); gtk_widget_show(window); gtk_widget_show(image); /* refresh se ejecuta cada 10 ms */ g_timeout_add(10, refresh, NULL); /* Ciclo principal GTK */ gtk_main(); /* Al finalizar el programa */ if((msg_status = send_tcp_msg("NOSTREAM", NULL, 0)) != 0) { fprintf(stderr, "Error iniciando detección de movimiento\n"); exit(EXIT_FAILURE); } if((msg_status = send_tcp_msg("NOMOTION", NULL, 0)) != 0) { fprintf(stderr, "Error iniciando detección de movimiento\n"); exit(EXIT_FAILURE); } if((msg_status = send_tcp_msg("NOCAPTURE", NULL, 0)) != 0) { fprintf(stderr, "Error iniciando detección de movimiento\n"); exit(EXIT_FAILURE); } if((msg_status = send_tcp_msg("CLOSE", NULL, 0)) != 0) { fprintf(stderr, "Error cerrando la conexión\n"); exit(EXIT_FAILURE); } close_conn(); return 0; } 90 net.h: Declaraciones y definiciones de las rutinas para la comunicación y la recepción del video en el cliente 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 /* * Telescreen v0.3 * Programa de captura de video y detección de movimiento utilizando V4L * net.h: Declaraciones y definiciones para la comunicación y la recepción * de video en el cliente * Copyright (C) 2005 Andrés Díaz Soto <[email protected]> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. * */ #include #include #include #include #include #include #include #include #include <stdio.h> <sys/types.h> <sys/socket.h> <netinet/in.h> <netdb.h> <fcntl.h> <unistd.h> <stdlib.h> <string.h> #ifndef NET_H #define NET_H /* Definiciones */ #define BUF_SIZE 256 // Tamaño del buffer para el envío de datos /* Propiedades del cuadro recibido */ struct video_prop { int width; // Ancho int height; // Altura }; /* Información de la conexión UDP */ struct stream_data { char *addr; // Dirección del servidor int port; // Puerto de conexión int width; // Ancho del cuadro int height; // Altura del cuadro }; /* Declaración de funciones */ /* open_conn: Inicia la conexión con el servidor */ int open_conn(char *addr, int port); /* close_conn: Finaliza la conexión con el servidor */ int close_conn(void); /* send_tcp_msg: Envía mensaje al servidor */ int send_tcp_msg(char *msg, void *retdata, int size); /* start_vid_stream: Inicia recepción de los cuadros de video */ void *start_vid_stream(void *strmdata); #endif /* NET_H */ 91 net.c: Implementación de las rutinas para la comunicación y la recepción del video en el cliente 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 /* * Telescreen v0.3 * Programa de captura de video y detección de movimiento utilizando V4L * net.c: Implementación de las rutinas para la comunicación y la recepción * de video en el cliente * Copyright (C) 2005 Andrés Díaz Soto <[email protected]> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. * */ #include "net.h" static int sd; unsigned char *frame; /* open_conn: Inicia la conexión con el servidor */ int open_conn(char *addr, int port) { struct hostent *hp; struct sockaddr_in server; char *buf_ptr, in_buf[BUF_SIZE]; int bytes_left; int n_read; /* Creación del socket */ if((sd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { fprintf(stderr, "No se puede abrir el socket\n"); return -1; } /* Información de la conexión */ memset((char *)&server, 0, sizeof(struct sockaddr_in)); server.sin_family = AF_INET; server.sin_port = htons(port); if((hp = gethostbyname(addr)) == NULL) { fprintf(stderr, "No se puede resolver el nombre del servidor\n"); return -1; } memcpy((char *)&server.sin_addr.s_addr, hp->h_addr, hp->h_length); /* Conexión con el servidor */ if(connect(sd, (struct sockaddr *)&server, sizeof(server)) == -1) { fprintf(stderr, "No se puede establecer la conexión con el servidor\n"); return -1; } printf("Conexión establecida con %s\n", hp->h_name); /* Recepción del mensaje de bienvenida */ bytes_left = BUF_SIZE; buf_ptr = in_buf; while((n_read = read(sd, buf_ptr, bytes_left)) > 0) { buf_ptr += n_read; bytes_left -= n_read; } return 0; } /* close_conn: Finaliza la conexión con el servidor */ int close_conn(void) { close(sd); printf("Conexión finalizada.\n"); 92 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 return 0; } /* send_tcp_msg: Envía mensaje al servidor */ int send_tcp_msg(char *msg, void *retdata, int size) { char *buf_ptr, in_buf[BUF_SIZE], out_buf[BUF_SIZE]; int bytes_left, n_read; /* Envía el mensaje */ strcpy(out_buf, msg); write(sd, out_buf, BUF_SIZE); /* Recepción de datos */ if(retdata != NULL) { bytes_left = size; buf_ptr = retdata; while((n_read = read(sd, buf_ptr, bytes_left)) > 0) { buf_ptr += n_read; bytes_left -= n_read; } } /* Recepción del mensaje de confirmación */ bytes_left = BUF_SIZE; buf_ptr = in_buf; while((n_read = read(sd, buf_ptr, bytes_left)) > 0) { buf_ptr += n_read; bytes_left -= n_read; } /* Verifica que el comando haya sido ejecutado satisfactoriamente */ if(strcmp(in_buf, "OK") == 0) return 0; else return -1; } /* start_vid_stream: Inicia recepción de los cuadros de video */ void *start_vid_stream(void *strmdata) { struct sockaddr_in server, client; struct hostent *hp; int sdu, server_len, n; char buf[BUF_SIZE]; unsigned char *temp_frame; struct stream_data *sdata = (struct stream_data *)strmdata; int framesize = sdata->width * sdata->height * 3; /* Creación del socket UDP */ if((sdu = socket(AF_INET, SOCK_DGRAM, 0)) == -1) { fprintf(stderr, "Error creando el socket UDP\n"); exit(-1); } /* Información del servidor */ memset((char *)&server, 0, sizeof(server)); server.sin_family = AF_INET; server.sin_port = htons(sdata->port); if((hp = gethostbyname(sdata->addr)) == NULL) { fprintf(stderr, "Error resolviendo la dirección del servidor\n"); exit(-1); } memcpy((char *)&server.sin_addr.s_addr, hp->h_addr, hp->h_length); /* Información del cliente */ memset((char *)&client, 0, sizeof(client)); client.sin_family = AF_INET; client.sin_port = htons(0); client.sin_addr.s_addr = htonl(INADDR_ANY); /* Enlace del socket */ if(bind(sdu, (struct sockaddr *)&client, sizeof(client)) == -1) { fprintf(stderr, "Error enlazando el socket\n"); exit(-1); } sleep(1); /* Espera a que el UDP en el servidor esté listo */ server_len = sizeof(server); strcpy(buf, "2+2=5"); if(sendto(sdu, buf, BUF_SIZE, 0, (struct sockaddr *)&server, server_len) == -1) { fprintf(stderr, "Problemas enviando datagrama al servidor\n"); 93 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 exit(-1); } /* Número de paquetes */ int n_packets = framesize / 4096; /* Cantidad de bytes sobrantes */ int leftover = framesize % 4096; int pc = 0; int p_size; char buf2[4096]; /* Reserva memoria para los cuadros */ temp_frame = malloc(framesize); frame = malloc(framesize); /* Recibe los paquetes UDP con el cuadro */ while(1) { p_size = recvfrom(sdu, buf2, 4096, 0, (struct sockaddr *)&server, &server_len); if(p_size < 0) { // Si hay error en la recepción fprintf(stderr, "Error recibiendo el paquete %d\n", pc); continue; } else if(p_size == sizeof(int)) { // Paquete de sincronización if(pc == n_packets + 1) { memcpy(frame, temp_frame, framesize); } else { fprintf(stderr, "Imagen con información incorrecta\n"); } pc = 0; } else if(p_size == 4096) { // Paquete con datos if(pc == n_packets) memcpy(temp_frame + pc*4096, buf2, leftover); else memcpy(temp_frame + pc*4096, buf2, 4096); pc++; } } /* Libera memoria */ free(temp_frame); free(frame); close(sdu); pthread_exit(NULL); }