Informe - Escuela de Ingeniería Eléctrica
Transcripción
Informe - Escuela de Ingeniería Eléctrica
Universidad de Costa Rica Facultad de Ingeniería Escuela de Ingeniería Eléctrica IE – 0502 Proyecto Eléctrico Sistema de Seguimiento de objetos utilizando una cámara de video y el sistema operativo GNU/Linux Por: Luis Federico Gómez Salazar Ciudad Universitaria Rodrigo Facio Julio del 2006 Sistema de Seguimiento de Objetos utilizando una cámara de video y el sistema operativo GNU/Linux Por: Luis Federico Gómez Salazar 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. Andrés Díaz Soto Profesor lector ii DEDICATORIA A mis padres y hermano que siempre han estado conmigo… iii RECONOCIMIENTOS Agradezco al profesor guía, Federico Ruiz, así como a los profesores lectores de este proyecto, Francisco Siles y Andrés Díaz ya que durante todo el semestre estuvieron anuentes a consultas y brindaron información vital para la realización de este trabajo. Agradezco también a compañeros como Marco Álvarez y Mario Segura que también fueron de gran ayuda en algunos problemas que se presentaron durante el desarrollo de este proyecto. iv ÍNDICE GENERAL DEDICATORIA ............................................................................................................ iii RECONOCIMIENTOS ................................................................................................ iv INDICE DE FIGURAS ................................................................................................. vi ÍNDICE DE CUADROS .............................................................................................. vii NOMENCLATURA .................................................................................................... viii RESUMEN ..................................................................................................................... ix CAPITULO 1 Introducción.......................................................................................... 1 1.1 Justificación ...................................................................................................... 1 1.2 Objetivos........................................................................................................... 2 1.2.1 Objetivo general ....................................................................................... 2 1.2.2 Objetivos específicos................................................................................ 2 1.3 Metodología...................................................................................................... 2 CAPITULO 2 Desarrollo teórico ................................................................................. 4 2.1 El lenguaje de Programación C++.................................................................... 4 2.2 GNU/Linux....................................................................................................... 5 2.2.1 El sistema Video4Linux ........................................................................... 5 2.2.1.1 Interfaz de programación de V4L para dispositivos de video .............. 6 2.3 La interfaz GTKmm ......................................................................................... 8 2.3.1 Programación de ventanas ........................................................................ 9 2.3.2 Programación con múltiples hilos de procesamiento ............................. 10 2.4 Procesamiento de Imágenes ........................................................................... 10 2.4.1 Representación vectorial de colores ....................................................... 11 2.4.2 Filtros...................................................................................................... 11 2.4.2.1 Filtrado de ruido ................................................................................. 13 2.4.3 Umbralización ........................................................................................ 13 2.4.3.1 Histogramas ........................................................................................ 14 2.4.3.2 Método se Otsu................................................................................... 14 2.4.3.3 Método de Kittler e Illingworth.......................................................... 16 2.5 Centroides....................................................................................................... 17 2.6 Histéresis ........................................................................................................ 19 2.7 El Robot Stäubli RX90 ................................................................................... 20 CAPITULO 3 Descripción del Sistema ..................................................................... 22 3.1 Estructura del sistema..................................................................................... 22 3.2 El programa principal ..................................................................................... 23 3.3 El ciclo infinito ............................................................................................... 24 CAPITULO 4 Algoritmos de acceso a dispositivo y procesamiento de Imagen ....... 27 4.1 Acceso al dispositivo ...................................................................................... 27 4.2 Procesamiento de imagen ............................................................................... 28 CAPITULO 5 Algoritmos de cálculo del centroide y decisión de movimiento ........ 35 5.1 Cálculo del centroide ...................................................................................... 35 5.2 Decisión de Movimiento ................................................................................ 36 5.3 Despliegue de imágenes ................................................................................. 41 CAPITULO 6 Resultados........................................................................................... 43 CAPITULO 7 Conclusiones y recomendaciones....................................................... 46 7.1 Conclusiones................................................................................................... 46 7.2 Recomendaciones ........................................................................................... 47 BIBLIOGRAFÍA .......................................................................................................... 48 APÉNDICES ................................................................................................................. 49 v INDICE DE FIGURAS Figura 2.1: Capas de abstracción de un computador ........................................................ 5 Figura 2.2: Representación vectorial de colores............................................................. 11 Figura 2.3: Manipulación de imágenes por máscaras matriciales .................................. 12 Figura 2.4: Máscara matricial......................................................................................... 13 Figura 2.5: Histograma ................................................................................................... 14 Figura 2.6: Curva de histéresis ....................................................................................... 19 Figura 2.7: Robot Stäubli RX90 ..................................................................................... 20 Figura 3.1: Estructura del sistema incluyendo el robot .................................................. 22 Figura 3.2: Estructura de la clase Imagen ...................................................................... 23 Figura 3.3: Estructura de la clase Acceso....................................................................... 23 Figura 3.4: Diagrama de flujo del ciclo infinito ............................................................. 24 Figura 4.1: Imagen capturada por la cámara de video.................................................... 29 Figura 4.2: Imagen resultante luego de aplicar la transformación a escala de grises.... 30 Figura 4.3: Indefinición en los bordes del cuadro. ......................................................... 31 Figura 4.4: Imagen resultante luego de aplicar el filtro.................................................. 32 Figura 4.5: Imagen resultante luego de aplicar la umbralización................................... 34 Figura 5.1: Segmentación del cuadro capturado ............................................................ 36 Figura 5.2: Movimiento del objeto dentro del cuadro .................................................... 37 Figura 5.3: Diagrama de flujo de la función de decisión de movimiento ...................... 37 Figura 5.4: Segmentación de la imagen en subcuadros.................................................. 39 Figura 6.1: Objeto identificado, alto contraste ............................................................... 43 Figura 6.2: Objeto identificado, bajo contraste .............................................................. 44 Figura 6.3: Escenario que induce a errores..................................................................... 45 Figura 6.4: Objeto no identificado.................................................................................. 45 vi ÍNDICE DE CUADROS Cuadro 2.1: Dispositivos disponibles controlados por V4L............................................. 6 Cuadro 2.2: Información contenida por la estructura video_capability. .......................... 6 Cuadro 2.3: Información contenida por la estructura video_buffer.................................. 7 Cuadro 2.4: Información contenida por la estructura video_window............................... 7 Cuadro 2.5: Información contenida por la estructura video_channel............................... 7 Cuadro 2.6: Información contenida en la estructura video_mbuf..................................... 8 Cuadro 2.7: Información contenida en la estructura video_mmap ................................... 8 Cuadro 2.8: Paquetes necesarios para la utilización de GTKmm..................................... 9 vii NOMENCLATURA GNU/Linux: Es la denominación para el sistema operativo que utiliza el kernel Linux en conjunto con las aplicaciones de sistema creadas por el proyecto GNU. Comúnmente este sistema operativo es denominado simplemente Linux. UNIX: UNIX® (o Unix) es un sistema operativo portable, multitarea y multiusuario; desarrollado en principio por un grupo de empleados de los laboratorios Bell de AT&T, entre los que figuran Ken Thompson, Dennis Ritchie y Douglas McIlroy. En 1970 por vez primera se habla oficialmente del sistema UNIX. GTKmm: Gimp Tool Kit es una interfaz de programación de ventanas para sistemas Linux. GTKmm es la denominación para el recubrimiento a C++. POSIX: Acrónimo de Portable operating system interface, Unix based (Sistema operativo portable basado en UNIX). Una familia de estándares de llamadas al sistema definidos por el IEEE y especificados formalmente en el IEEE 1003, intenta estandarizar las interfaces de los sistemas operativos para que las aplicaciones se ejecuten en distintas plataformas. pthreads: Abreviatura para hilos POSIX y una biblioteca que proporciona funciones POSIX para crear y manipular hilos de programación. La biblioteca se encuentra con mayor frecuencia en sistemas Linux y Unix, sin embargo existe también una versión para Windows. Kernel: También conocido como núcleo, es la parte fundamental de un sistema operativo. Se encarga de gestionar los recursos del sistema. viii RESUMEN Este proyecto tiene como objetivo crear un sistema de seguimiento de objetos que determine la posición de un objeto y señale el movimiento adecuado que debe brindársele al dispositivo de captura para mantener al objeto enfocado en todo momento. Este trabajo pretende quedar implementado para luego utilizarlo conjuntamente con el robot Staubli RX90, acoplar la cámara sobre éste y poder enviarle comandos de forma automática para ejecutar el seguimiento del objeto deseado. Este sistema se implementó utilizando el lenguaje de programación C++ y varias Interfaces de programación de aplicaciones (API) que pueden ser utilizadas sobre el sistema operativo GNU/Linux. Para acceder al dispositivo de video se utilizó el API Video4Linux el cual provee de una interfaz útil para poder acceder a las imágenes capturadas por la cámara de video. Además para el desarrollo de la aplicación gráfica se utilizó la interfaz GTKmm la cual constituye el hilo principal del programa y sobre el cual corren el resto de procesos. La aplicación desarrollada permite identificar un objeto de cualquier forma geométrica siempre y cuando éste sea lo suficientemente contrastante del fondo sobre el cual se desenvuelve. Una vez identificado el objeto se determina la posición de éste con respecto al espacio formado por todo el cuadro capturado por la cámara en algún instante de tiempo. Posteriormente se muestra en la aplicación gráfica un texto que indica el segmento del cuadro en el cual se encuentra el objeto así como una señalización en forma de flechas que indica hacia donde se debe dar movimiento a la cámara para mantener el objeto dentro del rango establecido. El sistema trabajó satisfactoriamente brindando las señalizaciones adecuadas para dar movimiento a la cámara así como señales de otros eventos por ejemplo, cuando no se registra algún objeto o cuando éste se encuentra dentro del espacio del cuadro establecido y por lo tanto no se debe señalar movimiento. ix CAPITULO 1 Introducción 1.1 Justificación Actualmente la utilización de robots para realizar distintas tareas de manufactura, ensamble y muchas otras aplicaciones representa una de las ramas más estudiadas de la tecnología. Ahora no solamente se busca que un operario pueda, mediante comandos o simplemente botones, manipular un mecanismo complejo como lo es un robot, sino que este realice su tarea de forma automatizada. Para ello se desarrollan programas para hacer que el robot realice una tarea específica. Por otro lado, el desarrollo de algoritmos capaces de detectar los movimientos de un objeto, constituyen otra área ampliamente estudiada. Para ello se deben tomar en cuenta diversos factores que afectan el reconocimiento de un objeto sobre otros que estén a su lado o formen parte del fondo visible. Estos factores serán considerados dependiendo de los objetivos buscados para hacerle frente al problema. Para la realización de este proyecto se decidió trabajar con el sistema operativo GNU/Linux, ya que es un sistema muy robusto que cuenta con características especiales de estabilidad y seguridad, además de que es libre. Por otro lado se utilizará una Interfaz de Programación de Aplicaciones (API) llamada Video4Linux, el cual posibilita la adquisición y procesamiento de video. Gracias a la filosofía de software libre de la cual gozan los sistemas Linux, el desarrollo de un gran número de programas y aplicaciones de gran calidad ha sido posible. Esto pues todos los programadores constituyen una enorme comunidad que busca el desarrollo de un producto de alta eficiencia. Este proyecto pretende ser la base para futuros estudios dentro de la Escuela de Ingeniería Eléctrica que puedan utilizarse para aplicaciones de seguridad o investigación y desarrollo. Así como una plataforma para crear un sistema más complejo utilizando el robot Staubli RX90. 1 1.2 Objetivos 1.2.1 Objetivo general Crear un sistema de seguimiento de objetos utilizando una cámara de video y una interfaz gráfica. 1.2.2 Objetivos específicos 1. Crear un algoritmo capaz de detectar la posición de objetos en el espacio. 2. Crear un algoritmo que indique el movimiento de la cámara con el fin de seguir al objeto detectado. 3. Crear una interfaz gráfica para la demostración de estos algoritmos que servirá como guía para controlar el robot Staubli RX90 en futuros proyectos. 1.3 Metodología • Se definen los objetivos del proyecto para determinar las herramientas necesarias para su desarrollo. • Se hace un análisis bibliográfico para determinar estudios relacionados con los objetivos que contempla el proyecto. • Aprender el lenguaje de C++. Determinando las características principales de este, así como su sintaxis. • Investigar sobre el sistema operativo GNU/Linux así como del sistema V4L, los cuales serán piezas fundamentales en el desarrollo de proyecto. • Se investiga sobre la interfaz GTKmm, necesaria para crear una aplicación gráfica que demuestre la funcionalidad de los objetivos. • Se investiga sobre las técnicas utilizadas para el procesamiento de imágenes. • Se utiliza un algoritmo para obtener imágenes secuenciales desde una cámara de video. • Se crea un algoritmo para procesar cada imagen y así poder distinguir un objeto en particular. • Se crea un algoritmo para localizar un objeto dentro del espacio del cuadro capturado por la cámara de video. • En el caso de los pasos relacionados con la creación de algoritmos, se deben 2 desarrollar de forma modular, es decir, ir probando cada parte del código para observar su funcionalidad y finalmente acoplarlo como un todo. • Se crea una aplicación gráfica en donde se visualice el resultado de la implementación del sistema. • Se realiza las pruebas necesarias al sistema bajo diferentes escenarios para comprobar su desempeño. 3 CAPITULO 2 Desarrollo teórico 2.1 El lenguaje de Programación C++ C++ es un lenguaje de Programación Orientada a Objetos (POO), a diferencia de su antecesor C, el cual es un lenguaje estructurado. Fue desarrollado a finales de los 70 por Bjarne Stroustoup bajo la denominación de C con clases, que finalmente adoptó el nombre de C++. Entre las características que tiene la programación orientada a objetos se encuentran: a) Todo es un objeto: Se debe pensar un objeto, como una variable que almacena datos pero que a su vez se le puede solicitar que haga operaciones sobre él mismo. La idea es tomar cada componente conceptual del problema como un objeto en el programa. b) Un programa es un lote de objetos «diciéndose» unos a los otros que hacer mediante mensajes: Para hacer una solicitud a un objeto se le debe «enviar un mensaje». Es decir, un mensaje es una llamada a una función que pertenece a un objeto particular. c) Cada objeto tiene su memoria construida de otros objetos: Es decir, se crea un objeto, a partir de un paquete que contiene otros objetos existentes. De aquí, que objetos simples realizan tareas sencillas pero que son agrupados para tener un objeto mucho más poderoso capaz de realizar trabajos más complejos. d) Todo objeto tiene un tipo: Cada objeto es una instancia de una clase, en donde la clase es el tipo. La característica más sobresaliente de C++ es el uso de clases, las cuales a diferencia de las estructuras de C pueden involucrar funciones las cuales brindan lo que podría definirse como el comportamiento de un objeto. e) Todos los objetos de un tipo particular pueden recibir los mismos mensajes: He aquí uno de los conceptos poderosos de C++. Si un objeto contiene otros objetos que realizan ciertas tareas, estos se pueden manipular con base en el objeto de mayor jerarquía. 1 Este tipo de programación ofrece como resultado un código más ordenado, es decir, no solo más fácil de escribir sino que también más fácil de leer. La inclusión de las clases en este lenguaje provee la capacidad de modularizar el problema y de esta forma, dividir las tareas de una forma adecuada entre los diferentes objetos. Además, se genera un código totalmente reutilizable gracias al concepto de objetos. 1 Información tomada de la referencia [6] 4 2.2 GNU/Linux GNU es un proyecto emprendido desde 1984 que pretendía desarrollar un sistema operativo «desde cero» basado en UNIX, bajo la filosofía de software libre. Este concepto permite la libre publicación del código fuente elaborado durante el desarrollo para que éste pueda ser utilizado y modificado por otras personas. De esta manera, múltiples programadores con la experiencia necesaria podrían realizar cambios, mejoras o ajustes al código base, formando un código más robusto que pudiera satisfacer infinidad de necesidades. Gracias a ello el proyecto GNU desarrolló gran cantidad de herramientas necesarias para obtener un sistema funcional, tales como aplicaciones, intérpretes y compiladores. Sin embargo, fue hasta en 1990 cuando pudieron completar el sistema con la inclusión del kernel Linux, creado por el finlandés Linus Torvalds. El kernel es la pieza fundamental del sistema operativo, la cual asegura el control de acceso a los dispositivos electrónicos, es decir, el hardware. El kernel manipula memoria, organiza la ejecución de los procesos, permite ejecución concurrente de procesos y provee de una capa de abstracción entre el hardware y el software. Programas Shell Kernel Hardware Espacio Usuario Espacio del Kernel Figura 2.1: Capas de abstracción de un computador El shell es una consola de comandos que es muy poderosa en los sistemas Linux, y es capaz de realizar numerosas y útiles tareas dentro del sistema operativo. Linux ofrece todas la interfaces de programación comunes de los sistemas estándar Unix, además de sus utilidades. Además de emplear eficientemente los recursos de hardware y ser un sistema sumamente enfocado en la seguridad. Es por ello que fue el sistema escogido para la realización de este proyecto. El compilador elegido para el código generado fue g++. gcc es un compilador integrado del proyecto GNU para C, C++, Objective C y Fortran; es capaz de recibir un programa fuente en cualquiera de estos lenguajes y generar un programa ejecutable binario en el lenguaje de la máquina donde ha de correr. La sigla gcc significa "GNU Compiler Collection". Originalmente significaba "GNU C Compiler"; todavía se usa gcc para designar una compilación en C. g++ refiere a una compilación en C++. 2.2.1 El sistema Video4Linux Video4Linux es una Interfaz de Programación de Aplicaciones o API (application programming interface). Esto quiere decir que haciendo uso de este sistema es posible manipular mediante un lenguaje de alto nivel dispositivos de audio y video. 5 Estos dispositivos son accedidos mediante un archivo, al igual que la mayoría de las operaciones realizadas sobre un sistema de tipo Unix/Linux. Cuadro 2.1: Dispositivos disponibles controlados por V4L Nombre del Dispositivo /dev/video /dev/radio /dev/vtx /dev/vbi Rango Función 0-63 64-127 192-233 224-239 Interfaz de captura de Video Dispositivos de Radio (AM/FM) Interfaz de Teletexto Información VBI (Intercast/teletext) 2.2.1.1 Interfaz de programación de V4L para dispositivos de video 2 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 principales utilizados en este proyecto 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 2.2. Cuadro 2.2: Información contenida por la estructura video_capability. Campo name [32] type channels audios maxwidth maxheight minwidth minheight Descripción Nombre del dispositivo Tipo del 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 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. 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 la tabla 2.3. 2 Información tomada de la referencia [2] 6 Cuadro 2.3: Información contenida por la estructura video_buffer Campo Descripción void *base Dirección física base del buffer int height Altura del buffer int width Ancho del buffer int depth Profundidad del buffer int bytesperline 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 la tabla 2.4. 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. VIDIOCCAPTURE: Permite activar y desactivar la captura en modo overlay. El cual es una 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 operación intermedia. Cuadro 2.4: 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) 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 la tabla 2.5. Cuadro 2.5: Información contenida por la estructura video_channel Campo channel name tuners flags type norm Descripción Número del canal Nombre del canal Número de sintonizadores en el canal Propiedades del sintonizador Tipo de entrada Norma que utiliza el canal 7 VIDIOCSCHAN: Selecciona el canal del cual se va a capturar. Recibe un entero con el número del canal como argumento. 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 tabla 2.6. Cuadro 2.6: Información contenida en la estructura video_mbuf Campo size frames offsets Descripción Tamaño de la memoria del dispositivo Número de cuadros que puede contener la memoria 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 la tabla 2.7. Cuadro 2.7: Información contenida en la estructura video_mmap Campo frame height width format Descripción Número de cuadro que se desea capturar Altura del cuadro Ancho del cuadro 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.3 La interfaz GTKmm GTK es la abreviatura de Gimp Tool Kit, la cual es una biblioteca desarrollada para la creación de interfaces gráficas. Con ella se pueden crear ventanas, botones y una infinidad de objetos denominados Widgets, que son los objetos más simples de los cuales hace uso esta interfaz. GTK fue creada primeramente para C, sin embargo, es un API orientado a objetos, de modo que ha sido extendido a lenguajes como Perl y C++. En el caso de C++ existe un recubrimiento, llamado gtkmm, que es el que se utilizará en este proyecto. Este provee la gran mayoría de las características de GTK y es una manera adecuada de crear la aplicación gráfica requerida en un lenguaje como C++. Para instalar gtkmm se deben instalar sus dependencias también, tal y como se muestra en la tabla 2.10. 8 Cuadro 2.8: Paquetes necesarios para la utilización de GTKmm Nombre de la aplicación libsigc++ 2.0 pkg-config ATK Pango GTKmm 2.4 2.3.1 Nombre del paquete a instalar libsigc++-dev pkg-config libatk1.0-dev libpango1.0-dev libgtkmm-2.4-dev Programación de ventanas Siempre que se vaya a programar con GTKmm es necesario incluir el archivo de cabecera o header, gtkmm.h, el cual declarará todas las variables, funciones y estructuras necesarias para una correcta definición de las mismas. Como ejemplo de la utilización de esta interfaz, para la creación de una ventana de 200x200 pixeles se puede compilar el siguiente código: 3 #include <gtkmm.h> int main(int argc, char *argv[]) { Gtk::Main kit(argc, argv); Gtk::Window mi_ventana; Gtk::Main::run(mi_ventana); return 0; } La línea : Gtk::Main kit(argc, argv); crea un objeto Gtk::Main. Esto es necesario en todas las aplicaciones de gtkmm. El constructor para este objeto inicializa gtkmm, y revisa los argumentos pasados a la aplicación por medio de la línea de comandos, buscando por opciones standard como –display. Esta línea se debe colocar en la función principal del programa, main(), para una correcta inicialización de la interfaz. Las siguientes dos líneas de código crean y despliegan una ventana: Gtk::Window mi_ventana; Esta línea muestra la ventana y entra al lazo principal de gtkmm, el cual finaliza cuando la ventana es cerrada. Gtk::Main::run(mi_ventana); 3 Ejemplo tomado de la referencia [10]. 9 Si el código se guarda como ventana.cc se debe compilar de la siguiente manera: $ g++ ventana.cc –o ventana `pkg-config gtkmm-2.4 –cflags – libs` El tipo de comillas utilizado hacen que el shell ejecute el comando dentro de ellas, y que use la salida de comandos como parte de la línea de comandos. La opción –cflags causa que pkg-config ponga en la salida una lista de directorios incluidos para que el compilador los busque. La opción –libs pide la lista de librerías para que el compilador las enlace y los directorios para encontrarlas. Otros widgets, importantes son los contenedores, los cuales se utilizan para empaquetar más de un widget por ventana. De esta manera es posible tener una ventana con varios botones, cuadros de diálogo, imágenes y todos los demás objetos existentes dentro de esta interfaz. 2.3.2 Programación con múltiples hilos de procesamiento La programación utilizando múltiples hilos de procesamiento es un esquema que permite el desarrollo de aplicaciones que ejecuten varios flujos de procesamiento dentro de un mismo proceso principal. Esto se utiliza para que cada uno de los hilos se ejecute de forma independiente, sin tener que esperar a que finalice otro proceso. Además, cada uno de los hilos tiene acceso a las distintas variables y objetos dentro del programa. La implementación de estos hilos o threads en Linux se conoce como POSIX threads o pthreads, sin embargo la interfaz GTKmm posee su propio soporte para hilos de procesamiento, así que es necesario utilizar estos para lograr el comportamiento deseado de la aplicación. Para crear un hilo y que este inmediatamente llame a una función en particular se debe proceder de la siguiente manera: Glib::Thread *hiloprinc = Glib::Thread::create((sigc::mem_fun (*this,&acceso::principal)),false); donde se observa que se debe crear un puntero tipo Glib::Thread y asignarle la llamada a la función, en este caso la función principal() perteneciente a la clase llamada acceso. El argumento false le indica al hilo que desaparezca una vez que la función retorne. 2.4 Procesamiento de Imágenes La manera básica de representar una imagen digital a color en la memoria de una computadora es un mapa de bits (bitmap). Un bitmap está constituido por filas de píxeles, los cuales tienen un valor particular que determina el color aparente. Este valor está dado por tres números dando la descomposición del color en los tres colores primarios: rojo, verde y azul. Es lo que se conoce como representación RGB (red-green-blue). Cualquier color visible por el ojo humano puede ser representado de esta manera. Esta descomposición de un color en tres colores primarios está 10 cuantificada por un numero entre 0 y 255, por ejemplo el color blanco que es la suma de todos los colores seria (R,G,B)=(255,255,255). Así entonces, una imagen es un arreglo bidimensional de píxeles, codificados en 3 bytes que representan los tres colores primarios. Esto permite que la imagen pueda contener 256*256*256=16.8 millones de colores diferentes. El ámbito 0-255 es aceptado por dos muy buenas razones. Una de ellas es que el ojo humano no es lo suficientemente sensible para diferenciar más de 256 niveles de intensidad. Es decir, una imagen no se mejorará, para efectos visuales humanos, si se codificara con más de 256 niveles. La otra razón es la conveniencia para el almacenamiento en la computadora ya que un byte (8 bits) puede contener 28 = 256 valores. 2.4.1 Representación vectorial de colores Ya que como se comentó, cualquier color puede ser representado como una combinación de tres colores primarios, se puede hacer una representación vectorial que ilustre los componentes que contiene este modelo. Figura 2.2: Representación vectorial de colores 4 La visualización de la dependencia de colores que muestra este modelo produce resultados directos muy útiles para el desarrollo de este proyecto. Por ejemplo, para cuantificar la diferencia entre dos colores basta realizar una sencilla operación algebraica. Si se quiere determinar la diferencia entre el color C1 = (R1, G1, B1) y el color C2 = (R2, G2, B2) se procede así: D (C1, C 2) = 2.4.2 (R1 − R 2)2 + (G1 − G 2)2 + (B1 − B 2)2 (1) Filtros Desde el punto de vista matemático, si se tiene una señal y se desea realizar una variación en esta, se puede hacer con una convolución entre dos funciones que representan a la entrada x(t) y la respuesta del filtro a un impulso h(t), de la siguiente manera: 4 Imagen tomada de la referencia [12] 11 +∞ y (t ) = h(t ) * x(t ) = ∑ h(t − α )g x (α )dα (2) −∞ Esta misma ecuación pero para tiempo discreto, sería: +∞ y[k] = h[k] * x[k] = ∑ h[k - i]g x [i] (3) −∞ Por lo tanto si se desea saber como filtrar una imagen digital, se debe convolucionar el mapa de píxeles de entrada, con el mapa de píxeles respuesta a un impulso del filtro de la imagen. La función impulso en una imagen de dos dimensiones es un cuadro donde todos los píxeles tienen valor 0 a excepción de un solo píxel que tiene valor de 1. Cuando esta función delta (impulso) pasa a través de una función lineal se obtendrá la función H(k). Entonces al generar una convolución entre estas funciones se obtiene una ecuación matricial parecida a la ecuación (3) de la siguiente forma: y[r, c] = M −1 M −1 1 ∑ ∑ h[j, i]g x [r - j, c - 1] ∑ h[i, j] j =0 i =0 (4) i, j Esta convolución tiene a y como el mapa de píxeles obtenido a la salida del filtro y x como el mapa de entrada, a través de un filtro con respuesta a un impulso h (con ancho y altura igual a M). Ahora esta función se puede representar de una manera más sencilla, donde lo que se hace es tomar el mapa de píxeles de una imagen y multiplicarlo por una máscara matricial y una constante de peso, obteniendo así un mapa de píxeles con las características deseadas. Esto se muestra de una manera más simple en la figura 2.1. Figura 2.3: Manipulación de imágenes por máscaras matriciales 5 5 Figura tomada de la referencia [9] 12 Entonces, algunos de los métodos para aplicar filtros a una imagen digital consisten en máscaras aplicadas a los píxeles para compararlos con los píxeles en una vecindad y así redefinir el valor del píxel analizado. 2.4.2.1 Filtrado de ruido Un filtro de ruido es utilizado para eliminar las posibles corrupciones en los píxeles debido a interferencia externa. Una de las maneras de filtrar la imagen consiste en multiplicarla por una máscara matricial y un valor de peso, que es la suma total de valores de la matriz. +m Pxy = * ∑W i, j =− m i, j ⋅ Px +i , y + j (5) +m ∑W i , j =− m i, j con P como la matriz de píxeles 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 píxeles del vecindario del píxel que está siendo calculado. La forma más sencilla de este filtro consiste en una matriz de 3 × 3 en la cual todos sus pesos son 1: Figura 2.4: Máscara matricial 2.4.3 Umbralización La umbralización es una técnica mediante la cual una imagen es dividida en dos clases de acuerdo con un valor umbral. La imagen se divide en un grupo de píxeles cuya característica de intensidad sea mayor al valor umbral y en uno cuya intensidad sea menor. Esta técnica de procesamiento parte de un concepto muy simple, el parámetro k llamado umbral es determinado y aplicado a una imagen a[m,n] como sigue: ⎧1 si a[m, n] ≥ k a[m, n] = ⎨ ⎩0 si a[m, n]〈 k (6) De esta manera, la imagen queda determinada por dos únicos valores de intensidad, 1 (blanco) o 0(negro), en donde cualquiera de los dos puede representar al objeto o al fondo. 13 La segmentación es la acción de separar un objeto de interés del fondo de la imagen. Para ello es necesario determinar un umbral adecuado que pueda servir para realizar la segmentación. Hay varias formas de determinar el umbral, una de ellas es seleccionarlo arbitrariamente, sin embargo, no resulta muy eficiente en los casos en los que se tiene una secuencia de imágenes capturadas, en donde el entorno puede variar en cualquier momento. Lo más adecuado es emplear alguno de los métodos existentes de umbralización automática. 2.4.3.1 Histogramas Un histograma es un gráfico que muestra la cantidad de píxeles en una imagen con un mismo valor de intensidad. Los histogramas son utilizados por diversas técnicas para determinar un umbral óptimo. Figura 2.5: Histograma 6 El histograma de una imagen con L niveles de gris (generalmente L=256) puede ser representado por un arreglo unidimensional con L elementos. 2.4.3.2 Método se Otsu 7 La operación de cálculo de umbral en este método, se considera como la distribución de los píxeles de una imagen en dos clases C0 y C1 (por ejemplo objeto y fondo o viceversa), considerando un nivel de umbral k. C0 denota los píxeles con un nivel de gris menor o igual al umbral [0,1,…,k] y C1 representa a los píxeles cuyo nivel es mayor al umbral [k+1,k+2,…,L-1]. A partir de esta distribución es posible calcular las siguientes aproximaciones estadísticas: k ω 0 (k ) = ∑ p( g ) (7) g =0 ω1 (k ) = 6 7 L −1 ∑ p( g ) = 1 − ω g = k +1 (8) 0 Imagen tomada de la referencia [14] Descripción de los métodos tomada de la referencia [5] 14 k μ 0 (k ) = ∑ g ⋅ p( g ) (9) g =0 μ1 ( k ) = L −1 ∑ g ⋅ p( g ) (10) g = k +1 k σ 0 2 (k ) = ∑ ( g − μ 0 (k )) 2 ⋅ p ( g ) (11) g =0 L −1 ∑ ( g − μ (k )) σ 1 2 (k ) = g = k +1 2 1 ⋅ p( g ) (12) donde: p( g ) = h( g ) (13) L −1 ∑ h( g ) g =0 La media total y la varianza total están dadas por: L −1 1 g =0 g =0 μ T (k ) = ∑ g ⋅ p( g ) = ∑ ω g (k ) μ g (k ) (14) L −1 σ T 2 = ∑ ( g − μ T ) 2 ⋅ p( g ) = σ w 2 (k ) + σ B 2 (k ) (15) g =0 donde: 1 σ w 2 (k ) = ∑ ω g (k )σ g 2 (k ) (16) g =0 1 σ B (k ) = ∑ ω g (k )( μ g (k ) − μ T ) 2 2 (17) g =0 son las varianzas dentro de la clase y entre clases respectivamente. Un umbral óptimo puede ser determinado al minimizar alguna de las siguientes funciones criterio (equivalentes), respecto al umbral k: λ (k ) = κ (k ) = σ B 2 (k ) σ W 2 (k ) (18) σT 2 (19) σ W 2 (k ) 15 σ B 2 (k ) η (k ) = σT 2 (20) Siendo ésta última la más simple. Por lo tanto, el umbral óptimo k es calculado de forma secuencial hasta satisfacer la siguiente relación: k = arg min k {η (k )} (21) k∈[0 , L −1] 2.4.3.3 Método de Kittler e Illingworth También llamado método del error mínimo. En este método el histograma es visto como un estimado de la función de densidad de probabilidad p(g) de la población mezclada consistente de los niveles de gris de los píxeles del objeto y del fondo. Se asume para los dos componentes de la mezcla p(g|0) y p(g|1), una distribución normal con parámetros para las medias μ 0 , μ1 y desviaciones estándar σ 0 , σ 1 y probabilidades a priori P0 y P1 , es decir: 1 p( g ) = ∑ Pi ⋅ p( g | i ) (22) i =0 donde, ⎛ g − μi exp⎜⎜ − 2 2π σ i ⎝ 2σ i 1 p( g | i) = ⎞ ⎟ ⎟ ⎠ (23) El valor de umbral puede ser hallado al resolver la siguiente ecuación cuadrática: ( g − μ 0 )2 σ0 + ln σ 0 − 2 ln P0 = 2 2 ( g − μ 1 )2 σ1 2 + ln σ 1 − 2 ln P1 2 (24) Los parámetros asociados con la densidad mixta p(g) son usualmente desconocidos. Sin embargo, pueden ser estimados a partir del histograma: k P0 (k ) = ∑ h( g ) (25) g =0 P1 (k ) = 255 ∑ h( g ) (26) g = k +1 k μ 0 (k ) = ∑ g ⋅ h( g ) g =0 (27) P0 (k ) 16 255 μ1 ( k ) = ∑ g ⋅ h( g ) g = k +1 (28) P1 (k ) k σ 0 2 (k ) = ∑ (g − μ g =0 ( k ) ) ⋅ h( g ) 2 0 255 σ 1 (k ) = 2 (29) P0 (k ) ∑ (g − μ (k ) ) 1 g = k +1 2 ⋅ h( g ) (30) P1 (k ) Estos parámetros deben ser sustituidos en la siguiente función criterio: J ( k ) = 1 + 2[P0 (k ) ln σ 0 + P1 ( k ) ln σ 1 ] − 2[P0 ( k ) ln P0 (k ) + P1 (k ) ln P1 (k )] (31) Y el valor umbral k se determinaría al minimizar la función anterior: k = arg min k {J (k )} (32) k∈[0 , L −1] Existen dos formas de implementar el algoritmo anterior. Una de ellas es seguir un esquema secuencial en donde se evalúa la función para todos los valores de gris posibles y se elige el que la minimice. Sin embargo puede que se elija como valor umbral alguno de los valores de los extremos, produciéndose un resultado no deseado. La otra forma es hacerlo en forma iterativa y no tiene este problema. Para implementarla se debe escoger un valor umbral inicial arbitrario y calcular los parámetros antes mencionados a partir del histograma. Luego se resuelve para g la siguiente ecuación: ⎡ 1 ⎡ μ (k ) μ (k ) ⎤ 1 ⎤ − 2 ⎥ − 2 g ⎢ 0 2 − 12 ⎥ + g2⎢ 2 σ 1 (k ) ⎦⎥ ⎣⎢σ 0 (k ) σ 1 (k ) ⎦⎥ ⎣⎢ σ 0 ⎡ μ 0 2 ( k ) μ1 2 ( k ) ⎤ − 2 ⎥ + 2[ln σ 0 (k ) − ln σ 1 (k )] − 2[ln P0 (k ) − ln P1 (k )] = 0 ⎢ 2 σ 1 (k ) ⎦⎥ ⎣⎢ σ 0 (33) Si el nuevo valor de umbral es igual al anterior, se finaliza el algoritmo, de lo contrario se recalculan los parámetros y se itera. 2.5 Centroides Para determinar matemáticamente el centro de gravedad de un cuerpo cualquiera deberá escribirse una ecuación que establezca que el momento respecto a un punto resultante P de las fuerzas de gravedad es igual a la suma de los momentos respecto al mismo punto 17 de las fuerzas de gravedad dP que actúan sobre cada partícula individual. Si se designa por G el centro de gravedad, sus coordenadas por x, y, z y el peso total por P, el principio de los momentos da: x= ∫ xdP P y= ∫ ydP P z= ∫ zdP P (34) El numerador de cada expresión representa la suma de momentos, y el producto de P por la coordenada de G correspondiente representa el momento de la suma. La tercera ecuación se obtiene girando 90 grados al cuerpo y al sistema de referencia respecto a un eje horizontal, de manera que el eje z será horizontal Siempre que la densidad μ de un cuerpo tenga el mismo valor en todos sus puntos, será un factor constante existente en los numeradores y denominadores de las ecuaciones 5 y por lo tanto se suprimirá. Las expresiones definen entonces una propiedad puramente geométrica del cuerpo, ya que no hay referencia alguna a sus propiedades físicas. Cuando el cálculo se refiera a una forma a una forma solamente geométrica se utiliza el término centroide. Entonces, si la densidad es la misma en todos los puntos, las posiciones del centroide y del centro de gravedad coinciden. En el caso de una varilla delgada o un elemento de forma lineal de longitud L y peso μ por unidad de longitud, el cuerpo tiende a un segmento rectilíneo, y dP = μdL . Por lo tanto, si μ es constante las ecuaciones 5 se convierten en: x= ∫ xdL L y= ∫ ydL L z= ∫ zdL L (35) Ahora bien, cuando se tiene un cuerpo de espesor pequeño y tiene a una superficie de área A cuyo peso por unidad de área es μ entonces dP = μdA , y si μ es constante, las ecuaciones 5 se transforman en x= ∫ xdA A y= ∫ ydA A z= ∫ zdA A (36) Si finalmente se tiene una superficie alabeada, como la de una bóveda o cáscara, interesan las tres coordenadas. Si es una superficie plana, solo interesan las dos coordenadas de dicho plano. Como se verá posteriormente en el desarrollo del algoritmo, este será el caso utilizado puesto que lo que se tiene es una imagen en dos dimensiones. Finalmente, como es de suponerse, si lo que se tiene es un cuerpo de volumen V y densidad μ el elemento es dP = μdV y entonces las ecuaciones buscadas serian: 18 x= ∫ xdV V y= ∫ ydV V z= ∫ zdV V (37) 2.6 Histéresis La histéresis es una propiedad física que se encuentra presente en una gran cantidad de sistemas. Uno de los más conocidos es el que se presenta en los fenómenos relacionados con magnetismo. Si al magnetizar un material ferromagnético este mantiene la señal magnética una vez que se retira el campo que la ha inducido, se dice que se presenta histéresis magnética. Lo que ocurre es que los dominios magnéticos quedan magnetizados cuando se aplica el campo externo y se puede decir que “recuerdan” su estado. Este es el principio de memoria utilizado en las cintas magnéticas y dispositivos de almacenamiento masivo como discos duros. El campo induce una magnetización que es codificada como un uno o como un cero y que al retirar el campo permanece el valor, para luego poder ser leído. Figura 2.6: Curva de histéresis 8 Al observar la curva 2.4 se puede determinar de una forma más general el concepto de histéresis. Una señal de algún fenómeno físico se representa mediante la curva que indica la flecha hacia arriba, y va expresándose de acuerdo con esta curva, sin embargo, cuando alcanza cierto valor de magnitud llamado umbral, la señal comienza a describirse por medio de la otra curva, la que tiene la flecha hacia abajo. El fenómeno sigue ocurriendo hasta que se alcanza el otro umbral con lo cual el comportamiento del fenómeno se describe nuevamente mediante la primera curva. 8 Imagen tomada de la referencia [13] 19 2.7 El Robot Stäubli RX90 El brazo robot Stäubli RX90 es un robot que consta de seis articulaciones y por ende seis grados de libertad. Las ultimas tres uniones corresponden a lo que se le denomina “Instrumental del brazo” y las primeras tres hacen que se le clasifique al brazo como de geometría ensamblada esférica, ya que los tres grados de libertad que posee se expresan de forma angular, es decir, ningún grado de libertad se expresa de forma lineal, como lo haría por ejemplo un robot de geometría esférica el cual tiene dos ejes de movimiento angular y uno de movimiento lineal. Estos tres ejes angulares que posee se pueden observar en las articulaciones 1, 2, y 3 en la figura . Las restantes tres articulaciones es lo que se denomina “instrumental final del brazo” o “end effector” que es la terminal del brazo que comprende los movimientos más específicos. Figura 2.7: Robot Stäubli RX90 9 Los movimientos se llevan a cabo gracias a los motores internos particulares a cada articulación 9 Imagen tomada de la referencia [3] 20 El Sistema del robot consta de ocho partes: a) El brazo robot: Las partes mecánicas b) La base del brazo: La pieza que lo sujeta con el suelo c) El gabinete: Estante metálico d) El controlador: Hardware que envía las señales a los motores e) El péndulo: Panel de control para usuario del robot f) Computadora industrial: Computadora que se conecta por medio de conexión serial con el controlador del robot y sirve para ejecutar programas de secuencias de movimientos g) Caja de alimentación h) Cables 21 CAPITULO 3 Descripción del Sistema 3.1 Estructura del sistema El sistema completo utilizando el robot Staubli RX90, del cual el presente proyecto forma la primera parte se puede observar en la siguiente figura: Figura 3.1: Estructura del sistema incluyendo el robot El robot Staubli RX90 posee geometría esférica ensamblada, es decir, posee tres extremidades principales que se mueven de forma angular. Este robot tiene lo que se conoce con el nombre de instrumental del brazo, el cual es otro pequeño brazo ubicado al final de la última extremidad principal que también tiene tres grados de libertad y sirve para realizar tareas de mayor precisión. Los movimientos del robot son manejados por el controlador que posee un puerto serie, el cual posibilita el envío de comandos desde el programa que se corre en la computadora. De esta manera, la secuencia de eventos sería: 1. Capturar una imagen mediante la cámara. 2. Acceder a ésta imagen mediante la interfaz Video4Linux. 3. Procesar la imagen para ubicar a determinado objeto dentro del cuadro capturado. 4. Determinar la posición del objeto dentro del cuadro. 5. Enviar un comando al controlador del robot para que éste se mueva de tal forma que el objeto se encuentre debidamente centrado. 6. Repetir el ciclo Este proyecto implementa la primera parte del sistema, la cual comprende los cuatro primeros puntos mencionados. 22 3.2 El programa principal La estructura del programa elaborado consta de dos clases. Una de ellas denominada clase imagen, contiene todos los métodos que se le pueden aplicar a un objeto imagen. Es decir, contiene funciones de procesamiento como conversión a escala de grises, filtrado y umbralización, así como la función para determinar la ubicación del centroide. Además contiene otras funciones que utilizan el API de ImageMagick para poder guardar en archivos algunas imágenes de importancia. Figura 3.2: Estructura de la clase Imagen Por otro lado, se tiene una clase denominada clase acceso de la cual se crea un objeto en el programa principal. Figura 3.3: Estructura de la clase Acceso Esta clase contiene diferentes métodos relacionados con la inicialización del dispositivo de video así como de la aplicación grafica que se muestran como un único bloque en la figura anterior. El método principal() es llamado al presionar un botón en la aplicación gráfica. Al ser llamado, este entra en un ciclo infinito en el cual llama a la función captura y crea un objeto de tipo imagen para luego ir llamando a cada una de sus funciones miembro, tal y como se detalla en la siguiente sección. Posteriormente se llama a la función decision que a su vez llama a fronteraext y fronteraint que en conjunto determinan en cual porción del cuadro capturado se encuentra el centroide del objeto y establecen la señalización adecuada. 23 /* Programa Principal */ #include "acceso.h" #include <gtkmm.h> int main(int argc, char *argv[]) { Gtk::Main kit(argc, argv); if(!Glib::thread_supported()) Glib::thread_init(); acceso X; } Como se observa, en el programa principal se tienen dos líneas de código necesarias para inicializar adecuadamente GTKmm así como el soporte para sus hilos de procesamiento. El constructor de la clase acceso contiene la definición de todos los elementos de la aplicación gráfica, así que cuando se crea el objeto X de tipo acceso se inicializa la ventana y esta queda a la espera de eventos definidos por los botones. Este constructor se encarga además de llamar a las funciones que utilizan el API de V4L para inicializar correctamente el dispositivo de video. 3.3 El ciclo infinito La ventana permanece inalterable hasta que se presiona el botón Iniciar el sistema el cual pone a correr en un hilo secundario la función acceso::principal() la cual entra en el ciclo infinito de captura y procesamiento. Este ciclo se ilustra con su diagrama de flujo mostrado en la figura 3.4 Figura 3.4: Diagrama de flujo del ciclo infinito 24 Se observa, en el código mostrado abajo que se captura un cuadro al entrar en el ciclo pero antes se inicializan ciertas variables como el contador de cuadro cont y la bandera de decisión de movimiento z, la cual es una variable global, declarada así para que la función decision() llamada más abajo pueda hacer uso de ella (ver cap. 5). Posteriormente se crea un objeto de tipo imagen. imagen instancia(cap.maxwidth, cap.maxheight, map); El constructor de la clase imagen que es invocado al crear el objeto, asigna memoria para el arreglo que contendrá la imagen de acuerdo con los parámetros ancho y alto, contenidos en las variables cap.maxwidth y cap.maxheight que le son pasadas como argumentos. Así mismo este constructor llama a la función escalagrises() que toma el puntero map que apunta al primer píxel de la imagen en la memoria del dispositivo, y convierte la imagen a valores de intensidad o escala de grises (ver cap. 4). El siguiente paso, tal y como se puede ver en el diagrama de flujo, es filtrar la imagen para eliminar ruido, y luego umbralizarla para tener una imagen con solo dos valores de intensidad. Luego, ya con una imagen segmentada, se calcula el centroide de lo que es reconocido como un objeto (píxeles con un valor de cero). Si no existe un objeto dentro de la imagen, se coloca un texto en la aplicación, definido por la variable string texto, así como una imagen, ambos ilustrando la condición de objeto no reconocible. Por el contrario, si algún objeto es registrado, se le debe pasar como argumentos a la función de decisión de movimiento (decisión) las coordenadas del centroide del objeto. Esta función determina la posición del objeto con respecto al cuadro capturado y al llamar a las funciones fronteraext y fronteraint establece las señalizaciones gráficas adecuadas que indican la posición del objeto. void acceso::principal() { captura(); //captura un cuadro z=1; int cont=1; //bandera de decision de movimiento //contador de cuadros while(1){ bandera=false; //bandera que será revisada por la función que será llamada //cada cierto tiempo desde el GUI captura(); data = new unsigned char[3*cap.maxwidth*cap.maxheight]; for (int i=0; i<3*cap.maxwidth*cap.maxheight; i++) data[i] = map[i]; imagen instancia(cap.maxwidth, cap.maxheight, map); bandera=true; instancia.filtroruido(); banderaumb=false; double f=130; instancia.umbral(f); banderaumb=true; int *punt=instancia.centroide(); //arreglo que contiene las coordenadas del //centroide 25 if(punt[0]<0){ //si no existe un valor verdadero para la posición del centroide //no hay ningún objeto pixindic = Gdk::Pixbuf::create_from_file("noobj2.png"); texto="No se registra ningun objeto\n"; } else{ decision(punt); } //Se llama a la función que decidirá el movimiento //dándole como argumento la coordenada del centroide } La inclusión de banderas en este código, así como una copia inicial del puntero map a otra posición de memoria evita problemas a la hora de refrescar las imágenes desplegadas. Esto se detalla en el capitulo 5. 26 CAPITULO 4 Algoritmos de acceso a dispositivo y procesamiento de Imagen Como se puede intuir de la descripción del sistema en el capítulo anterior, la primera parte del proceso consiste en acceder el dispositivo de video, en este caso la cámara, para obtener así una secuencia de imágenes que luego se deben procesar una por una para depurarlas, eliminar información innecesaria así como para resaltar la que sea importante para esta aplicación especifica. 4.1 Acceso al dispositivo En esta sección se describen las principales rutinas utilizadas para acceder la cámara de video mediante la interfaz de Video4Linux. 10 Estas rutinas están dentro de la clase llamada acceso, así que son funciones miembro de dicha clase. En el constructor de la clase acceso se debe dar la correcta inicialización del dispositivo. Para ello, se recurre a uno de sus métodos: init_video, el cual recibe como argumento una variable tipo char que es el nombre del archivo correspondiente al dispositivo de video. Este se encarga de, inicializar el dispositivo, obtener las características de video (capacidades de captura), así como de definir el puntero map que apunta al primer píxel capturado de la imagen actual. vfd = init_video("/dev/video0"); assert(vfd!=-1); set_chan(vfd,0); set_video_prop(cap.maxwidth,cap.maxheight,VIDEO_PALETTE_RGB24); Luego se define el canal de captura, mediante el método set_chan, el cual recibe como argumentos el descriptor de archivo que se construyó al llamar a init_video así como un entero que representa el canal mismo. Posteriormente se definen algunas propiedades de la imagen a recibir mediante la función set_video_prop, la cual almacenará los valores del tamaño del cuadro capturado, así como su formato. Los valores del tamaño fueron determinados a partir de la obtención de las características de video, realizada en init_video, y que fueron almacenadas en una estructura de tipo video_capability. El formato se refiere a la combinación de colores utilizada, en este caso RGB de 24 bits (8 bits por color). La captura de la imagen se realiza con el siguiente método, el cual hace uso de los controles de V4L, VIDIOCMCAPTURE y VIDIOCSYNC. void acceso::capture(void) { video_buf.frame = 0; ioctl(vfd, VIDIOCMCAPTURE, &video_buf); 10 Si se quiere una información más detallada se puede consultar la referencia [2]. 27 ioctl(vfd, VIDIOCSYNC, &video_buf); } 4.2 Procesamiento de imagen Una vez que se ha obtenido la información necesaria del dispositivo y se captura un cuadro en un instante determinado, se debe tomar el puntero map antes mencionado, el cual posee la dirección del primer píxel en el cuadro capturado. Se debe colocar cada pixel en una matriz, haciendo una conversión de colores de RGB a escala de grises o grayscale, la cual es conveniente para eliminar información innecesaria en la imagen recibida. Esta rutina, así como la de otros filtros necesarios se encuentra en una clase llamada imagen. El constructor provee la correcta inicialización de la matriz que contendrá el cuadro. Este recibe como argumentos las características de tamaño del cuadro determinadas anteriormente y así provee del tamaño correcto a la matriz frame, haciendo uso de la función new() que es una mejora de C++ con respecto a C de realizar la asignación de memoria dinámica al tiempo de ejecución. Además, se decidió colocar en el constructor la llamada al método escalagrises para transformar la imagen de RGB a escala de grises para que de esta manera, cada vez que se cree el objeto, la matriz frame contenga la imagen ya convertida a escala de grises. También se reserva memoria y se inicializa apropiadamente el arreglo que contendrá el histograma de la imagen. imagen::imagen(int width, int height, unsigned char *pixel) { frame=NULL; cols=width; fils=height; frame = new float[fils*cols]; arreglo= new int[2]; //arreglo que contendrá las coordenadas del centroide escalagrises(pixel); histograma = new unsigned char[255]; for(int i=0;i<=255;i++) histograma[i]=0; //arreglo que contendrá el histograma //el histograma debe ser inicializado con ceros } Este es el método de la clase utilizado para transformar la imagen a formato de escala de grises. void imagen::escalagrises(unsigned char* pixel) float temp=0; int tam; int k=0; unsigned char* gris; 28 tam=cols*fils; gris=new unsigned char[tam]; for (int i=0; i<tam*3; i+=3){ temp = 0.11*pixel[i] + 0.59*pixel[i+1] + 0.3*pixel[i+2]; if(temp >= 255) gris[k] = 254; else gris[k] = static_cast<char>(temp); k++; } El método escalagrises va almacenando en una variable temporal, temp, el resultado de sumar la multiplicación de tres valores por números decimales. Cada uno de estos tres valores representan los tres colores: rojo, verde y azul para determinar el valor del píxel correspondiente. Es decir, cada píxel consta de una combinación de 8 bytes de información para el color rojo, 8 para el azul y también 8 para el verde con lo cual se tiene que cada píxel es representado por 24 bytes. La variable temp se va almacenando en un arreglo llamado graypic para luego ser introducido a la matriz frame. k=0; //Llenado de la matriz a procesar: frame for(int i=0; i<fils;i++){ for(int j=0; j<cols;j++){ frame[i*cols+j]=static_cast<float>(gris[k]/254.0); k++; Si la cámara de video captura una imagen como la mostrada en la figura 4.1, al aplicar la transformación a escala de grises sobre ella se obtiene una imagen como la de la figura 4.2. Figura 4.1: Imagen capturada por la cámara de video 29 Figura 4.2: Imagen resultante luego de aplicar la transformación a escala de grises El siguiente paso a ejecutar es pasar la imagen almacenada en frame, por un filtro que elimine las variaciones abruptas de la imagen. Esto se realiza mediante el método filtroruido, el cual define una máscara de 3x3 que revisa los píxeles vecinos del píxel actual y redefine el valor de este último. void imagen::filtroruido() { float *new_frame; new_frame = new float[fils*cols]; //Se define una nueva matriz float weights[9] = {0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11}; for(int i=1;i<(fils-1);i++) for(int j=1;j<(cols-1);j++){ new_frame[i*cols + j] = weights[0]*frame[(i-1)*cols + (j-1)]+ weights[1]*frame[(i-1)*cols + j]+ weights[2]*frame[(i-1)*cols + (j+1)]+ weights[3]*frame[i*cols + (j-1)]+ weights[4]*frame[i*cols + j]+ weights[5]*frame[i*cols + (j+1)]+ weights[6]*frame[(i+1)*cols + (j-1)]+ weights[7]*frame[(i+1)*cols + j]+ weights[8]*frame[(i+1)*cols + (j+1)]; } Como se observa, primero se define una nueva matriz llamada new_frame sobre la cual se escribirá el cuadro ya filtrado. Un vector de números en punto flotante representa la máscara a utilizar. Luego simplemente se va tomado cada píxel y se multiplican los píxeles vecinos por el valor en el vector para obtener un valor final y asignarlo al píxel analizado. Se observa como en los ciclos anidados no se toman en cuenta la primera ni la última fila, así como la primera ni la última columna. Esto es porque si se aplicara esta máscara en los píxeles de los bordes del cuadro, ocurriría una violación de segmento, ya que no todos los 9 píxeles existirían. Esto queda más claro en la siguiente figura. 30 Figura 4.3: Indefinición en los bordes del cuadro. Como se aprecia en la figura, si se intentara evaluar el píxel en la posición (0, 0) del cuadro, solo habrá cuatro píxeles a evaluar dentro de la imagen, los otros cinco quedan fuera de ella. for(int i=0; i<fils;i++){ new_frame[i*cols] = 1; new_frame[i*cols + (cols-1)] = 1; } for(int i=0; i<cols;i++){ new_frame[i] = 1; new_frame[(fils-1)*cols + i] = 1; } for(int i=0;i<fils;i++) for(int j=0;j<cols;j++) frame[i*cols + j] = new_frame[i*cols + j]; Entonces, una manera de evitar esto es obviar los bordes y definirlos arbitrariamente de un color, en este caso blanco (el cual tiene un valor de uno), y finalmente se copia la matriz new_frame en _frame para así tener siempre una referencia del cuadro. Finalmente en este método se calcula el histograma de la imagen el cual será utilizado por la función de umbralización para el cálculo del valor umbral. for(int i=0; i <fils; i++) for(int j=0; j <cols; j++){ histograma[static_cast<unsigned char>(getpixel(i,j)*255)]++; } El arreglo histograma contiene 256 posiciones, cada una de las cuales representa un valor posible de los píxeles. De este modo, se va sumando uno a cada posición cuando al recorrer la imagen se encuentre un píxel con ese valor. La función getpixel() simplemente recibe las coordenadas del píxel deseado y ésta devuelve su valor. La imagen resultante al aplicar este filtro sobre la imagen de la figura 3.1 se observa en la siguiente figura. 31 Figura 4.4: Imagen resultante luego de aplicar el filtro Ya con la imagen filtrada, para poder realizar la detección del objeto determinado de una forma fácil se procede a calcular un umbral para luego redefinir el valor de cada píxel en la imagen, de esta forma se obtendrá una imagen completamente en blanco y negro. Para el cálculo automático del umbral se utilizó el método de Kittler e Illingworth en la forma iterativa el cual fue explicado en la parte teórica. Este método fue escogido dado que el método de Otsu y el mismo de Kittler e Illingworth en su forma secuancial, necesitan calcular el valor de las constantes y la minimización de la ecuación para todos los valores posibles de intensidad. Esto es ineficiente dado que ésta aplicación requiere que se minimice el tiempo que dura cada ciclo. Además el método de Kittler e Illingworth en forma iterativa demostró ser bastante efectivo en la determinación del umbral, ya que se alcanza el valor deseado con pocas iteraciones y el valor resulta apropiado. Para implementar este algoritmo, primero se debe calcular todos los parámetros necesarios para luego introducirlos en la función de costo. void imagen::umbral(double k) { for(int i=0; i<=k; i++) P0=P0+histograma[i]; //determinacion de P0 for(int i=(k+1); i<=255; i++) P1=P1+histograma[i]; if(P0!=0 && P1!=0){ for(int i=0; i<=k; i++) ut0=ut0+(i*histograma[i]); u0=ut0/P0; //determinacion de P1 for(int i=(k+1); i<=255; i++) ut1=ut1+(i*histograma[i]); u1=ut1/P1; //determinacion de u1 //determinacion de u0(medias) //determinacion for(int i=0; i<=k; i++) st0=st0+(pow((i-u0),2))*histograma[i]; s0=st0/P0; de s0 (desv. Estandar) //determinacion for(int i=(k+1); i<=255; i++) st1=st1+(pow((i-u1),2))*histograma[i]; s1=st1/P1; de s1 32 Como se observa, este método recibe como argumento un valor flotante k, el cual en primera instancia es arbitrario, tal y como se explicó en la parte teórica. En el ciclo infinito se le da inicialmente un valor de 130 (un valor central, ya que son 256 posibilidades) para calcular los parámetros. Luego de este cálculo se procede a resolver la ecuación cuadrática: if(s0!=0 && s1!=0){ //las desv. Estandar no /* Resolución de la ecuación cuadrática */ deben ser cero double a = ((1/s0)-(1/s1)); double b = -2*((u0/s0)-(u1/s1)); double c = (pow(u0,2)/s0)-(pow(u1,2)/s1) + 2*(log(s0/s1)/log(e))-2*(log(P0/P1)/log(e)); double discr = pow(b,2)-(4*a*c); double result = (-b + sqrt(discr))/(2*a); Simplemente se calculan los coeficientes de la ecuación y se utiliza el método general para la resolución de la misma. Si las desviaciones estándar son cero, la población es uniforme y por lo tanto no hay objeto identificable. Este método se itera hasta que el valor de entrada de la función (k) sea igual al valor umbral calculado, result. Si son iguales, se umbraliza la imagen de acuerdo con ese valor, de lo contrario, se llama a la función misma (recursividad) dándole como argumento el valor calculado el cual queda contenido en la variable result. if(k==result){ float umb= static_cast<float>(result/255); for(int i=0; i <fils; i++) for(int j=0; j <cols; j++){ if(frame[i*cols + j]<=umb){ frame[i*cols + j]=0.0001; } else if(frame[i*cols + j]>umb){ frame[i*cols + j]=1; } } save_image(frame, filename3, "I"); //se guarda la imagen en un archivo else{ umbral(result); } } } } Además, si se encuentra el umbral, se salva la imagen en un archivo para poder ser desplegada en la aplicación gráfica y así tener una referencia para comparar con el resultado de la decisión de movimiento. 33 Por otro lado, si ocurriera que alguna de las variables P0, P1, s0, o s1 fuera cero, existiría una indefinición, pues en algunas partes de la ecuación estas variables se encuentran en el denominador. Si alguna de las desviaciones estándar es cero, quiere decir que todos los datos se encuentran distribuidos uniformemente y por lo tanto no hay una diferenciación clara entre un objeto y su fondo. Cuando esto ocurre se decidió dejar la imagen totalmente blanca para que en la determinación del centroide se establezca la condición de objeto no reconocible. Este último formato en que queda la imagen es el más conveniente para poder identificar el objeto sobre su fondo, calcular su centroide y decidir los movimientos necesarios. La imagen resultante luego de la umbralización se muestra en la siguiente figura. Figura 4.5: Imagen resultante luego de aplicar la umbralización Finalmente, se crearon dentro de la clase imagen, un par de métodos más que hacen uso de las funciones de una interfaz llamada ImageMagick, la cual sirve para manipular imágenes. En este caso se utilizaron para guardar en archivos los diferentes cuadros procesados en cada paso y así poder visualizar el efecto de los filtros. Los algoritmos utilizados fueron tomados de la referencia [1]. Mediante ImageMagick es posible manipular imágenes para obtener otras imágenes con características especiales buscadas. Se les puede variar el color, tamaño, atributos de luminosidad, tono y un gran número de operaciones que pueden ser útiles dependiendo de la aplicación específica. 34 CAPITULO 5 Algoritmos de cálculo del centroide y decisión de movimiento 5.1 Cálculo del centroide En el capitulo 2 se comentó que la forma para determinar el centroide de un objeto de área A, consiste en calcular una integral sobre todos sus puntos y luego dividirlos entre el área total del objeto, es decir, entre todos los puntos que lo componen. En el caso general, se recurre a una integral de área, y se debe establecer una relación matemática para relacionar cada punto con alguna referencia, particularmente, el centro de coordenadas. Ahora bien, como para la imagen almacenada luego de ser pasada por los filtros se conoce el estado de cada píxel (su valor y ubicación de la matriz), basta con realizar la sumatoria de las distancias de cada píxel que forma parte del objeto y luego dividirla entre el numero total de píxeles. Esto brinda la posibilidad de analizar objetos sin una forma definida, e igualmente se podrá detectar su centro geométrico. Entonces se creó un método llamado centroide miembro de la clase imagen (para que pueda tener acceso a la matriz frame) que calcule dicho centroide: int *imagen::centroide(){ int n=0; xsum=0; ysum=0; for (int i=0; i<fils; i++){ for(int j=0;j<cols; j++){ if(frame[i*cols + j] < 0.5){ n++; xsum+=j; ysum+=i; } } } if(n!=0){ arreglo[0]=static_cast<int>(xsum/n); arreglo[1]=static_cast<int>(ysum/n); return(arreglo); } else{ cout<<"No se registra ningun objeto"<<endl; } } Se tienen tres enteros: n, el cual contabiliza los píxeles que forman parte del objeto (los píxeles negros) y que por lo tanto provee el área total del objeto, xsum el cual contiene la sumatoria de las distancias desde el eje x hasta cada píxel, y ysum el cual contiene la misma la sumatoria de las distancias a cada píxel desde el eje y. 35 Así entonces, se va revisando cada píxel en la imagen y cada vez que se encuentre un píxel negro se guardan sus coordenadas y se suma una unidad a n. Posteriormente la sumatoria de las coordenadas de cada punto se divide por n, y se convierte a un entero, en caso de que la división diera un número decimal, para así obtener las coordenadas absolutas del centroide. En el caso de que no se encuentre ningún píxel negro, n sería igual a cero y por lo tanto no se realiza la división. Como se observa, éste método devuelve un puntero a un arreglo, el cual contendrá en su posición [0] la coordenada x del centroide y en su posición [1] la coordenada y. 5.2 Decisión de Movimiento En el caso de este proyecto, se utilizará la propiedad de histéresis para decidir las órdenes de movimiento, de acuerdo con la posición del objeto en determinada posición del cuadro capturado. Figura 5.1: Segmentación del cuadro capturado El cuadro capturado para cada instante de tiempo se dividirá en tres secciones, tal y como se muestra en la figura 4.1. Cuadro 1: representa la sección más externa del cuadro. Cuando el centroide del objeto se encuentra dentro de esta sección, se debe dar la indicación de movimiento necesaria para introducir el objeto dentro del cuadro 2. Cuadro 2: este cuadro circunscribe al cuadro uno y representa una zona de transición. Es en este cuadro donde se contempla la condición de histéresis. Si el objeto se encontraba en el cuadro 1 e ingresa al cuadro 2, como indica la figura 4.2B, se debe dar la indicación de movimiento para introducirlo en el cuadro 3. Si por el contrario el objeto se encontraba en el cuadro 3 y posteriormente ingresa al cuadro 2, como se observa en la figura 4.2 A, no se debe dar indicación de movimiento sino hasta cuando haya ingresado nuevamente al cuadro 1. 36 A B Figura 5.2: Movimiento del objeto dentro del cuadro Cuadro 3: es el cuadro objetivo, es decir, cuando el centroide del objeto se encuentra dentro de este cuadro, se encuentra en la posición correcta y por lo tanto no hay que dar ordenes de movimiento. Cada cuadro es la mitad del tamaño del cuadro que lo circunscribe. Es decir, el cuadro 3 es la mitad de tamaño que el cuadro 2, y un cuarto del tamaño del cuadro 1. Figura 5.3: Diagrama de flujo de la función de decisión de movimiento El puntero generado por el método centroide de la clase imagen, es luego utilizado en la clase acceso, mediante el método decision, el cual determinará en cual sector del cuadro capturado se encuentra el centroide del objeto y además mediante la llamada a otros dos métodos llamados fronteraint (FI en el diagrama de flujo) y fronteraext(FE) se decidirá hacia donde se debe dar el movimiento de la cámara. 37 Como se observa, es en este método en donde se expresa la funcionalidad de la histéresis, mediante el uso de la bandera de decisión z. Al inicio del programa la bandera z es definida a 1, entonces el if es verdadero y por tanto se llama al método fronteraext, pasándole como argumentos las coordenadas del centroide. Si este método devuelve un uno indica que el objeto se encuentra más allá de su territorio y por lo tanto se encuentra en el cuadro exterior. Sino es así, se llama a la función fronteraint que revisa si el objeto se encuentra dentro o fuera del límite que establece. En el caso que el objeto no se encontrara en el cuadro exterior ni en el cuadro central es porque se encuentra en el cuadro interior el cual es el cuadro objetivo. En este caso, la bandera z se pone en cero para indicar que el objeto se encuentra en posición correcta y que no debe de indicarse movimiento sino hasta cuando el objeto haya sobrepasado nuevamente la frontera exterior. /* Funcion que decidirá en cual cuadro se encuentra el objeto y llama a otras funciones para establecer la directriz de movimiento */ void acceso::decision(int *arr) { if(z==1){ if(fronteraext(arr[0],arr[1])==1){ cout<<"Cuadro exterior..."<<endl; texto="Cuadro exterior...\n"; } else{ if(fronteraint(arr[0],arr[1])==1){ cout<<"Cuadro central..."<<endl; texto="Cuadro central...\n"; } else{ cout<<"Cuadro interior... posicion correcta!!!"<<endl; texto="Cuadro interior... posicion correcta!!!\n"; pixindic = Gdk::Pixbuf::create_from_file("centro.png"); z=0; } } } else if(z==0){ if(fronteraext(arr[0],arr[1])==1){ cout<<"Cuadro exterior..."<<endl; texto="Cuadro exterior...\n"; z=1; } else{ cout<<"Se encuentra dentro del rango...!!!"<<endl; texto="Se encuentra dentro del rango...!!!\n"; pixindic = Gdk::Pixbuf::create_from_file("centro.png"); z=0; } } } 38 Tanto fronteraext como fronteraint se basan en una subdivisión de cada cuadro en 8 subcuadros más, los cuales definirán la dirección del movimiento. Esto se puede ver más claramente en la figura 4.3. De este modo, si el objeto se encuentra en el cuadro más exterior, se determina en cual de los subcuadros se encuentra para así mostrar la orden de movimiento. Por ejemplo si el centroide se localiza en el subcuadro 5 del cuadro más exterior, la orden correspondiente sería la de mover la cámara hacia abajo y también hacia la izquierda. De esta manera, cuando el objeto se encuentre en el cuadro exterior se redirigirá primeramente hacia el cuadro central y posteriormente hacia el cuadro interior. Como se observa, en el caso de que el objeto se encuentre en la posición correcta, se coloca una imagen que indica no movimiento dentro del objeto pixbuf (de la interfaz GTKmm), el cual será desplegado en la pantalla. Figura 5.4: Segmentación de la imagen en subcuadros /*Cuadro exterior*/ int acceso::cuadroext(int x, int y) { int xsi1=static_cast<int>(a/4); int ysi1=static_cast<int>(b/4); int xsd1=static_cast<int>(a-(a/4)); int ysd1=static_cast<int>(b/4); int xii1=static_cast<int>(a/4); int yii1=static_cast<int>(b-(b/4)); int xid1=static_cast<int>(a-(a/4)); int yid1=static_cast<int>(b-(b/4)); //160 //120 //480 //120 //160 //360 //480 //360 if((x>xsd1)&&(y<ysd1)){ //cout<<"Derecha y Arriba"<<endl; pixbuf3 = Gdk::Pixbuf::create_from_file("arribader.png"); return 1; } if((x>xid1)&&(y>yid1)){ // cout<<"Derecha y Abajo"<<endl; pixbuf3 = Gdk::Pixbuf::create_from_file("abajoder.png"); return 1; } if((x<xsi1)&&(y<ysi1)){ //cout<<"Izquierda y Arriba"<<endl; 39 pixbuf3 = Gdk::Pixbuf::create_from_file("arribaizq.png"); return 1; } if((x<xii1)&&(y>yii1)){ //cout<<"Izquierda y Abajo"<<endl; pixbuf3 = Gdk::Pixbuf::create_from_file("abajoizq.png"); return 1; } if((x<xsd1)&&(x>xsi1)&&(y<ysi1)){ //cout<<"Arriba"<<endl; pixbuf3 = Gdk::Pixbuf::create_from_file("arriba.png"); return 1; } if((x<xid1)&&(x>xii1)&&(y>yii1)){ // cout<<"Abajo"<<endl; pixbuf3 = Gdk::Pixbuf::create_from_file("abajo.png"); return 1; } if((y<yid1)&&(y>ysd1)&&(x>xsd1)){ //cout<<"Derecha"<<endl; pixbuf3 = Gdk::Pixbuf::create_from_file("derecha.png"); return 1; } if((y<yid1)&&(y>ysd1)&&(x<xsi1)){ // cout<<"Izquierda"<<endl; pixbuf3 = Gdk::Pixbuf::create_from_file("izquierda.png"); return 1; } } El método fronteraext devolverá un uno si alguna condición if se cumple, indicando por lo tanto que el centroide se encuentra dentro del cuadro exterior. Además, indicará el movimiento necesario para ubicar al centroide en el cuadro siguiente, colocando la imagen correspondiente en el objeto pixbuf. Este se basa en la delimitación de fronteras para los subcuadros como se puede apreciar en la figura 4.1. Estas fronteras están determinadas por los pares ordenados de las esquinas de los cuadros. Donde por ejemplo xid1 sería la coordenada x de la esquina inferior derecha del cuadro 1. El método fronteraint trabaja exactamente igual, la única diferencia consiste en que los puntos de referencia, es decir, las fronteras, estarán ahora con base en el cuadro interior, en lugar del cuadro central. Ahora bien, cuando la cámara realiza los movimientos adecuados para que el centroide se ubique en el cuadro central, se desactiva la bandera z, para que así en el método decision se ejecute la entrada al otro if el cual evita que se den decisiones de movimiento a menos que el objeto se localice nuevamente en el cuadro más exterior, 40 momento en el cual la bandera z se vuelve a establecer en uno, para así comenzar nuevamente con las directrices de movimiento. Esta característica de histéresis evita que el programa oscile en un ámbito pequeño, es decir, si no se estableciera un cuadro de transición (cuadro central), el programa estaría en todo momento dando órdenes de movimiento siempre que el objeto no se encuentre dentro del cuadro interior, lo cual no sería conveniente por cuestiones de recursos del sistema. 5.3 Despliegue de imágenes Uno de los puntos importantes del proyecto consiste en la correcta visualización de las indicaciones de movimiento así como de la secuencia de imágenes que la cámara de video está capturando una vez que se corre el programa. Para desplegar las imágenes, GTKmm cuenta con objetos llamados pixbufs con los cuales se pueden manipular arreglos de píxeles que estén guardados en un archivo así como arreglos que se encuentren en la memoria del programa. El código para mostrar en la aplicación las señalizaciones y la secuencia de imágenes se muestra a continuación. void acceso::refrescar(void) { pixbuf = Gdk::Pixbuf::create_from_data((const guint8*)data, Gdk::COLORSPACE_RGB, FALSE, 8, cap.maxwidth, cap.maxheight, cap.maxwidth*3); imagen.set(pixbuf); imagen.show(); //imagen imagenindic.set(pixindic); imagenindic.show(); imagenfoto.set(pixfoto); imagenfoto.show(); //señalizaciones //foto en blanco y negro agregartexto(texto); } Como se observa, primero se construye el pixbuf al cual se le da como argumento el puntero data que apunta a la imagen capturada. Luego para poder mostrarse, este pixbuf debe ser introducido en otro objeto de tipo Gtk::Image, mediante el método set() y luego con el método show() se muestra. Los objetos imagen se deben introducir en otros objetos llamados contenedores para poder desplegarse, esto se encuentra definido en el constructor de la clase acceso. Por otro lado, se observa que se tienen tres pixbufs. pixbuf se refiere a la secuencia de imágenes, pixindic contiene la imagen de señalización que se introduce en los métodos fronteraext y fronteraint según la posición del objeto. En pixfoto se introduce, al 41 presionar un botón, la imagen blanco y negro correspondiente a la imagen mostrada actualmente. Finalmente agregartexto() es una rutina creada para poder desplegar mensajes en la aplicación y se le pasa texto como argumento, el cual es una variable string que se define en distintos sectores del programa para mostrar mensajes relevantes. Un punto muy importante acerca de la rutina refrescar() es que debe ser llamada desde el ciclo principal de GTK. Lo que sucede es que no es conveniente tratar de alterar algo en el hilo principal (el de GTK) desde un hilo secundario (el hilo donde corre el resto del programa). Entonces existe una forma de llamar a la rutina refrescar() desde el GUI (Graphical User Interface): sigc::slot<bool> conexion=sigc::bind(sigc::mem_fun(*this,&acceso::timeout),0); sigc::connection llamada=Glib::signal_timeout().connect(conexion, 30); Mediante estas dos líneas de código que deben ser colocadas durante la definición de todos los objetos de la ventana gráfica, se define una llamada a la función timeout() de la clase acceso cada 30 milisegundos. Esta función llama a la función refrescar() dependiendo del estado de una bandera: bool acceso::timeout(int c) { if(bandera==true){ refrescar(); } else if(bandera==false){cout<<endl;} return true; } Esta bandera lo que hace es proteger la imagen para que no sea refrescada en un momento en el cual no está lista aún. Para ello simplemente se establece en false durante el tiempo que tarde la rutina capture() que captura cada cuadro. 42 CAPITULO 6 Resultados El sistema mostró un buen desempeño en distintas pruebas que se le aplicaron. El tiempo de refrescamiento fue de aproximadamente 0.9 segundos. Este tiempo es definido por la duración del ciclo de captura y procesamiento, en el cual se debe realizar todas las operaciones sobre la imagen capturada y además realizar el despliegue de la misma. Esta última operación retarda de forma significativa el proceso, sin embargo no se debe omitir para efectos de éste proyecto debido a que constituye la referencia principal del desempeño del sistema. En las figuras siguientes se muestra el comportamiento del sistema sobre distintos escenarios. Figura 6.1: Objeto identificado, alto contraste Como se observa, se colocó en la aplicación una foto escalada en blanco y negro. Esta es la imagen producto de la umbralización y por lo tanto, es la imagen sobre la cual se calcula el centroide. Al presionar el botón Imagen de referencia la imagen actual es desplegada, una vez aplicada la umbralización. Esta funcionalidad permite que se pueda estar monitoreando el correcto desempeño del sistema ya que de esta manera se tiene acceso a la imagen de referencia. En esta primera imagen se observa como efectivamente el sistema logra determinar la posición del objeto. En este caso hay un contraste alto entre el objeto y el fondo. En la siguiente imagen el contraste disminuye significativamente y aún así el sistema trabaja adecuadamente. 43 Figura 6.2: Objeto identificado, bajo contraste En la siguiente imagen se observa como las esquinas oscuras evitan que se pueda determinar adecuadamente la posición del centroide, ya que las regiones oscuras también formarán parte del objeto. Al examinar éste resultado se infiere que si se detectara más de un objeto, el sistema lo visualizaría como un único objeto, determinando el centroide de éste al identificar todos los puntos de color negro presentes en la imagen. 44 Figura 6.3: Escenario que induce a errores En esta última figura se observa como al no haber suficiente contraste entre el fondo y el objeto, la desviación estándar de la distribución tiende a cero y por ende no existe un objeto claramente reconocible. Figura 6.4: Objeto no identificado 45 CAPITULO 7 Conclusiones y recomendaciones 7.1 Conclusiones - Se creó un algoritmo capaz de segmentar una imagen para poder distinguir un objeto de interés y localizar su posición dentro del cuadro capturado. - Se creó un algoritmo que indica de manera gráfica la posición del objeto y por ende el movimiento necesario que debe brindársele al dispositivo de video para mantener el objeto en el centro del cuadro capturado. - Se implementó una interfaz gráfica para la demostración de estos algoritmos, la cual permite además poder visualizar en algún momento la imagen de referencia que resulta de los procesamientos y mediante la cual se calculará el centroide del objeto. - La programación en el lenguaje C++ resulta en una forma ordenada de escribir un programa. La modularización del problema en clases que realicen distintas tareas representa una manera eficiente y fácil tanto escribir como de leer el código. - La interfaz Video4Linux representa una manera realmente sencilla de poder acceder al dispositivo de video. En este caso particular resultó muy eficaz para poder manipular la información brindada por la cámara de video. - La interfaz GTKmm constituyó un elemento de gran importancia para el desarrollo de la aplicación. Esta interfaz permitió elaborar la ventana gráfica para poder visualizar el comportamiento y resultados del programa. - Fue necesaria la utilización de múltiples hilos de procesamiento, para los cuales GTKmm tiene su propio soporte, para poder ejecutar varios procesos de forma simultanea. - Los algoritmos existentes para la manipulación y procesamiento de imágenes resultan muy eficientes, además son de fácil comprensión e implementación. - El programa elaborado logra identificar un objeto lo suficientemente contrastante para diferenciarse de su fondo, despliega en la pantalla las imágenes capturadas por la cámara, brinda información acerca de la localización del objeto y señalizaciones que indican el movimiento necesario que debe dársele al dispositivo para mantener el objeto centrado. - Se creó una plataforma para que pueda ser utilizada como base en el desarrollo de más proyectos que involucren el tema estudiado así como la inclusión del robot Staubli RX90 para utilizarlo como el mecanismo que brinde el movimiento a la cámara. 46 7.2 Recomendaciones - Se puede mejorar de forma significativa el desempeño del sistema si se utilizara otra cámara de video, ya que para este proyecto se utilizó una de tipo webcam que no es de muy buena calidad. - Se debe tener muy en cuenta las características del espacio físico en donde se van a realizar pruebas ya que la iluminación debe ser adecuada para que no existan elementos que interfieran con la determinación del cuerpo del objeto. - Se puede estudiar con más detalle los métodos de procesamiento de imágenes para reducir el tiempo que dura cada ciclo y así hacer el sistema más eficiente. - Se puede implementar en la aplicación gráfica una forma para que el usuario pueda establecer el tamaño de los cuadros de referencia, ya que para este proyecto se definieron arbitrariamente, tal y como se menciona en el capitulo 5. - En el caso de la implementación conjunta con el robot Staubli se debe tener en cuenta ciertas variables para que este pueda brindar el movimiento adecuado a la cámara de video. El envío de los comandos necesarios para mover el brazo y el resultado del procesamiento de las imágenes que determinan la posición del objeto debe estar sincronizada adecuadamente. - Se deben establecer relaciones adecuadas de distancias y ángulos del objeto con respecto a la lente de la cámara para poder determinar el giro adecuado que se le debe dar a las extremidades del brazo. Esto por cuanto el brazo Staubli es de geometría esférica ensamblada y por tanto sus tres extremidades principales se mueven de forma angular. 47 BIBLIOGRAFÍA 1. Alvarez, M. “Diseño e Implementación de un mensajero automático capaz de recorrer una ruta preestablecida”, Costa Rica, 2005. 2. Díaz, A. “Implementación de un sistema de captura de video y detección de movimiento utilizando el sistema Video4Linux, Costa Rica, 2005. 3. Montes, C. “Envío de video hacia un cliente remoto utilizando el sistema Video4Linux y desarrollo de una aplicación gráfica GTK sobre una plataforma GNU/Linux para el control del brazo robot Staubli RX90”, Costa Rica, 2005. 4. Segura, M. “Estación experimental de redes neuronales artificiales”, Costa Rica, 2006. 5. Siles, F. “Selección de un método de umbralización para segmentar la silueta de una persona a partir de un análisis estadístico”, Costa Rica, 2003 6. Eckel, Bruce. “Thinking in C++”, Prentice Hall, Estados Unidos, 2000. 7. Kernighan, B, Ritchie, D. “El lenguaje de programación C”, Prentice Hall, México, 1991. 8. Merian, J. “Estática”, Editorial Reverté, Barcelona, 1968. 9. Patin, F,. “An introduction to digital image processing”, http://www.gamedev.net/reference/programming/features/imageproc/page2.asp. Octubre, 2005. 10. “Página oficial de GTKmm”, http://www.gtkmm.org 11. “Wikipedia”, http://www.wikipedia.com 12. http://es.wikipedia.org/wiki/Teor%C3%ADa_del_color 13. http://es.wikipedia.org/wiki/Hist%C3%A9resis 14. http://www.ph.tn.tudelft.nl/Courses/FIP/noframes/fip-Segmenta.html 48 APÉNDICES Código Fuente acceso.h SSO v0.7 Sistema de Seguimiento de Objetos acceso.h.: declaraciones de la clase acceso Copyright (C) 2006 Luis Federico Gómez Salazar <[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 "imagen.h" <gtkmm.h> <fcntl.h> <sys/ioctl.h> <sys/mman.h> <linux/videodev.h> #ifndef CLASEACCESO #define CLASEACCESO class acceso{ private: unsigned char* map; struct video_capability cap; struct video_mbuf buf_info; struct video_mmap video_buf; int vfd; int cont; int z; int a; int b; bool bandera, banderaumb; //Puntero que apunta al primer pixel del frame //Caracteristicas del dispositivo de video (cámara) //Información de la memoria de video //Caracteristicas de los cuadros de la memoria de video //Descriptor de archivo del dispositivo de video //contador de cuadros //bandera de decision de movimiento //variables para cap.maxwidth y cap.maxheight solo para //tratarlas con un nombre más corto! //bandera de sincronizacion de refrescamiento /* Objetos de GTKmm */ Gtk::Notebook tabs; //objeto cuaderno (pestañas) Gtk::VBox caja, cajafoto, cajaindic; //contenedores Gtk::VBox cajanom, cajanom1; Glib::RefPtr<Gdk::Pixbuf> pixbuf, pixfoto, pixindic; //objetos pixbuf Glib::RefPtr<Gdk::Pixbuf> pixscal, pixnom, pixnom1; Gtk::Image image, imagefoto, imageindic, imagenom, imagenom1; //objetos imagen Gtk::Table tabla, tablabotones; //tablas para acomodar objetos Gtk::Frame framevideo; Gtk::Table tablavideo; Gtk::Frame framecapturas, frameconsola, framebotones, frameflechas; //frames Gtk::Frame frameinfo1, frameinfo2, frameinfo3; Gtk::Table tablainfo1, tablainfo2, tablainfo3; Gtk::Button biniciar, bcerrar, bumbral; //botones Gtk::TextView consolaview; //Consola de texto Gtk::ScrolledWindow consolaventana; //Ventana que despliega la consola Glib::RefPtr<Gtk::TextBuffer> consolabuffer; //Buffer de la consola public: acceso(); virtual ~acceso(void); int init_video(char*); int set_chan(int, int); int set_video_prop(int, int, int); void captura(void); 49 void principal(void); void decision(int*); int fronteraext(int, int); int fronteraint(int, int); Gtk::Window ventana; void agregartexto(string); void on_click(string); void refrescar(void); bool timeout(int); string texto; unsigned char *data; }; #endif acceso.cc SSO v0.7 Sistema de Seguimiento de Objetos acceso.cc: Implementación de la clase acceso Copyright (C) 2006 Luis Federico Gómez Salazar <[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. // Clase acceso #include #include #include #include #include "acceso.h" <cassert> <vector> <glibmm/thread.h> <glib.h> using namespace std; acceso::acceso() //constructor que llama a las funciones para abrir el dispositivo de video { /* Inicialización del dispositivo */ vfd = init_video("/dev/video0"); assert(vfd != -1); set_chan(vfd,0); set_video_prop(cap.maxwidth, cap.maxheight, VIDEO_PALETTE_RGB24); cout<<"Comienza el programa...\n"<<endl; a=cap.maxwidth; //640 //solo para utilizarlo con un nombre màs corto b=cap.maxheight; //480 texto="Iniciando...\n"; /* Aplicación Gráfica */ tabla.resize(5,10); //Inicializa cada tabla (filas, columnas) tablabotones.resize(5,5); //Inicializa cada tabla de cada frame tablainfo1.resize(2,2); tablainfo2.resize(3,3); tablainfo3.resize(2,2); tablabotones.set_homogeneous(true); tabla.set_homogeneous(true); ventana.set_title("Sistema de Seguimiento de Objetos"); ventana.set_resizable(true); //Podra cambiarse de tamano ventana.set_border_width(1); ventana.maximize(); //Maximiza la ventana framevideo.set_label("Video"); frameconsola.set_label("Localizacion"); framecapturas.set_label("Capturas"); 50 //frames (para segmentar la ventana) framebotones.set_label("Opciones"); frameflechas.set_label("Directriz de movimiento"); Gtk::Label inf, inf1, inf2, inf3; //etiquetas inf.set_label("Este sistema permite identificar un objeto sobre su fondo\nsiempre que exista suficiente contraste.\nLa forma del objeto puede ser cualquiera."); inf1.set_label("El objeto se encuentra debidamente centrado"); inf2.set_label("No hay ningun objeto reconocible"); inf3.set_label("Universidad de Costa Rica\nEscuela de Ingenieria Electrica\nProyecto Electrico\nSistema de Seguimiento de Objetos\nLuis Federico Gomez Salazar\nA22186"); pixnom = Gdk::Pixbuf::create_from_file("centro.png"); imagenom.set(pixnom); cajanom.pack_start(imagenom); pixnom1 = Gdk::Pixbuf::create_from_file("noobj2.png"); imagenom1.set(pixnom1); cajanom1.pack_start(imagenom1); cajanom.show(); tablainfo1.attach(inf,0,1,0,1); tablainfo2.attach(cajanom, 0,1,0,1); tablainfo2.attach(cajanom1, 0,1,2,3); tablainfo2.attach(inf1, 1,2,0,1); tablainfo2.attach(inf2, 1,2,2,3); tablainfo3.attach(inf3, 0,1,0,1); //tablas para acomodar mas de un widget tabs.append_page(frameinfo1, "Informacion"); //anexa los frames a cada tab del cuaderno tabs.append_page(frameinfo2, "Nomenclatura"); tabs.append_page(frameinfo3, "++..."); /* Botones */ biniciar.set_label("Iniciar el sistema"); biniciar.signal_clicked().connect(sigc::bind<string>( sigc::mem_fun(*this, &acceso::on_click), "biniciar") ); bcerrar.set_label("Terminar"); bcerrar.signal_clicked().connect(sigc::bind<string>( sigc::mem_fun(*this, &acceso::on_click), "bcerrar") ); bumbral.set_label("Imagen de referencia"); bumbral.signal_clicked().connect(sigc::bind<string>( sigc::mem_fun(*this, &acceso::on_click), "bumbral") ); /* Ubicacion de los botones y frames dentro de la ventana */ tablabotones.attach(bumbral,0,3,4,5); tablabotones.attach(bcerrar,0,3,1,2); tablabotones.attach(biniciar,0,3,0,1); //adjunta los botones a la tabla Gtk::VSeparator linea; tabla.attach(framevideo,0,7,0,8); tabla.attach(framebotones,7,10,0,3); tabla.attach(frameconsola,0,3,8,10); tabla.attach(framecapturas,7,10,3,8); tabla.attach(frameflechas,3,5,8,10); tabla.attach(linea,5,6,8,10); tabla.attach(tabs,6,10,8,10); //adjunta los frames a la tabla framebotones.add(tablabotones); frameconsola.add(consolaventana); frameinfo1.add(tablainfo1); frameinfo2.add(tablainfo2); frameinfo3.add(tablainfo3); //adjunta la tabla al frame //adjunta la consola de texto al frame /* Inicializa Consola */ consolaventana.add(consolaview); //Agrega la consolaview a la ventana consolaview.set_sensitive(false); //No podra ser editada //Despliega scrollbars si son necesarias consolaventana.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); consolabuffer= consolaview.get_buffer(); //Obtiene el buffer del VIEW consolaview.set_editable(false); //Evita que pueda editarse la ventana consolaview.set_cursor_visible(false); //Evita que pueda que el cursor sea visible consolaview.set_overwrite(false); consolaview.show(); caja.pack_start(image); framevideo.add(caja); caja.show(); //adjunta la imagen al contenedor //adjunta el contenedor al frame //muestra el contenedor cajafoto.pack_start(imagefoto); framecapturas.add(cajafoto); cajafoto.show(); cajaindic.pack_start(imageindic); frameflechas.add(cajaindic); cajaindic.show(); 51 ventana.add(tabla); ventana.show_all_children(); //agrega la tabla principal a la ventana //muestra todos los objetos hijos de ventana /* Llama a una funcion desde la aplicacion cada cierto intervalo de tiempo en ms esto es necesario ya que no es conveniente tratar de modificar algo en el hilo principal desde un hilo secundario */ sigc::slot<bool> conexion = sigc::bind(sigc::mem_fun(*this,&acceso::timeout),0); sigc::connection llamada = Glib::signal_timeout().connect(conexion, 30); Gtk::Main::run(ventana); //Entra al ciclo principal de GTKmm } acceso::~acceso() { //destructor } void acceso::agregartexto(string linea) { consolabuffer->insert_at_cursor(linea); //Agrega una linea a la consola //Agrega al buffer el texto recibido //Asegura que el texto introducido sea visible //Verifica si hay mas texto del que la ventana puede desplegar if ( consolaventana.get_vadjustment()->get_value() >= ( this->consolaventana.get_vadjustment()->get_upper() this->consolaventana.get_vadjustment()->get_page_size() - 1e-12) ) { Glib::RefPtr<Gtk::TextBuffer::Mark> pos; pos = consolabuffer->create_mark(consolabuffer->end()); //Posicion consolabuffer->move_mark(pos, consolabuffer->end()); //Buffer al final consolaview.scroll_to_mark(pos, 0.0); //Mueve el texto a la ventana } } void acceso::principal() { captura(); //captura un cuadro z=1; //bandera de decision de movimiento int cont=1; //contador de cuadros while(1){ bandera=false; //bandera que será revisada por la función que será llamada //cada cierto tiempo desde el GUI captura(); data = new unsigned char[3*cap.maxwidth*cap.maxheight]; //copia map en otra posicion de memoria for (int i=0; i<3*cap.maxwidth*cap.maxheight; i++) data[i] = map[i]; imagen instancia(cap.maxwidth, cap.maxheight, map); //se crea un objeto de tipo imagen bandera=true; //Una vez que se captura un cuadro, se //pone esta bandera en true para desplegar la imagen cout<<"Cuadro: "<<cont++<<endl<<endl; //instancia.colocar(data); instancia.filtroruido(); //Se pasa la imagen por el filtro de ruido //cout<<"Filtro de ruido... hecho!"<<endl; banderaumb=false; double f=130; instancia.umbral(f); banderaumb=true; //Se deja una imagen en blanco y negro //cout<<"Umbral... hecho!"<<endl; int *punt=instancia.centroide(); //determina la posicion del centroide //cout<<"Coordenadas del centroide:"<<endl; //cout<<"x="<<punt[0]<<endl; //cout<<"y="<<punt[1]<<endl; //cout<<"Decision"<<endl; if(punt[0]<0){ pixindic = Gdk::Pixbuf::create_from_file("noobj2.png"); texto="No se registra ningun objeto\n"; } else{ decision(punt); } //Se llama a la función que decidirá el movimiento //dandole como argumento la coordenada del centroide }//llave del while } /* Inicializa el dispositivo */ 52 int acceso::init_video(char *video_dev) { int fd; /* Apertura del dispositivo */ if((fd=open(video_dev, O_RDWR))==-1) { cout<<stderr<<"Error al abrir el dispositivo %s\n"<<video_dev<<endl; return -1; } /* Información de los buffers */ if((ioctl(fd, VIDIOCGMBUF, &buf_info))==-1) { cout<<"Error adquiriendo los buffers (VIDIOCGMBUF)\n"<<endl; return -1; } /* Obtención de las capacidades de captura del dispositivo */ if(ioctl(fd,VIDIOCGCAP, &cap)==-1){ cout<<stderr<<"Error al obtener las capacidades de captura (VIDIOCGCAP)\n"<<endl; return -1; } /* Determinación del puntero map que contendrá la dirección del primer pixel del cuadro */ map = (unsigned char*)mmap(0, buf_info.size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); if(map==NULL){ cout<<"Error en el mapeo de la memoria\n"<<endl; return -1; } return fd; } /* Define el canal */ int acceso::set_chan(int fd, int chan) { if(ioctl(fd, VIDIOCSCHAN, chan)==-1) return -1; else return 0; } /* Establece el tamaño del frame */ int acceso::set_video_prop(int width, int height, int format) { video_buf.width = width; video_buf.height = height; video_buf.format = format; return 0; } /* Captura */ void acceso::captura(void) { video_buf.frame = 0; ioctl(vfd, VIDIOCMCAPTURE, &video_buf); ioctl(vfd, VIDIOCSYNC, &video_buf); } //Lee el frame (cuadro) //sincroniza la obtención de los cuadros /* Funcion que decidirá en cual cuadro se encuentra el objeto y llama a otras funciones para establecer la directriz de movimiento */ void acceso::decision(int *arr) { if(z==1){ if(fronteraext(arr[0],arr[1])==1){ cout<<"Cuadro exterior..."<<endl; texto="Cuadro exterior...\n"; } else{ if(fronteraint(arr[0],arr[1])==1){ cout<<"Cuadro central..."<<endl; texto="Cuadro central...\n"; } else{ cout<<"Cuadro interior... posicion correcta!!!"<<endl; texto="Cuadro interior... posicion correcta!!!\n"; pixindic = Gdk::Pixbuf::create_from_file("centro.png"); z=0; } } } 53 else if(z==0){ if(fronteraext(arr[0],arr[1])==1){ cout<<"Cuadro exterior..."<<endl; texto="Cuadro exterior...\n"; z=1; } else{ cout<<"Se encuentra dentro del rango...!!!"<<endl; texto="Se encuentra dentro del rango...!!!\n"; pixindic = Gdk::Pixbuf::create_from_file("centro.png"); z=0; } } } //cuadro1=exterior //cuadro2=interior /* Estas dos siguientes funciones le asociarán al objeto pixindic la imagen correspondiente de acuerdo con la posición del objeto en el cuadro. Los diferentes puntos van formando los pares ordenados de las esquinas de los cuadros. De esta manera, ysd1 por ejemplo, seria la coordenada "y" de la esquina Superior Derecha del cuadro 1.*/ /*Cuadro exterior*/ int acceso::fronteraext(int xx, int { int xsi1=static_cast<int>(a/4); int ysi1=static_cast<int>(b/4); int xsd1=static_cast<int>(a-(a/4)); int ysd1=static_cast<int>(b/4); int xii1=static_cast<int>(a/4); int yii1=static_cast<int>(b-(b/4)); int xid1=static_cast<int>(a-(a/4)); int yid1=static_cast<int>(b-(b/4)); yy) //160 //120 //480 //120 //160 //360 //480 //360 if((xx>xsd1)&&(yy<ysd1)){ //cout<<"Derecha y Arriba"<<endl; pixindic = Gdk::Pixbuf::create_from_file("arribader.png"); return 1; } if((xx>xid1)&&(yy>yid1)){ // cout<<"Derecha y Abajo"<<endl; pixindic = Gdk::Pixbuf::create_from_file("abajoder.png"); return 1; } if((xx<xsi1)&&(yy<ysi1)){ //cout<<"Izquierda y Arriba"<<endl; pixindic = Gdk::Pixbuf::create_from_file("arribaizq.png"); return 1; } if((xx<xii1)&&(yy>yii1)){ //cout<<"Izquierda y Abajo"<<endl; pixindic = Gdk::Pixbuf::create_from_file("abajoizq.png"); return 1; } if((xx<xsd1)&&(xx>xsi1)&&(yy<ysi1)){ //cout<<"Arriba"<<endl; pixindic = Gdk::Pixbuf::create_from_file("arriba.png"); return 1; } if((xx<xid1)&&(xx>xii1)&&(yy>yii1)){ // cout<<"Abajo"<<endl; pixindic = Gdk::Pixbuf::create_from_file("abajo.png"); return 1; } if((yy<yid1)&&(yy>ysd1)&&(xx>xsd1)){ //cout<<"Derecha"<<endl; pixindic = Gdk::Pixbuf::create_from_file("derecha.png"); return 1; } if((yy<yid1)&&(yy>ysd1)&&(xx<xsi1)){ // cout<<"Izquierda"<<endl; pixindic = Gdk::Pixbuf::create_from_file("izquierda.png"); return 1; } } 54 /*Cuadro interior*/ int acceso::fronteraint(int xx, int yy) { int xsi2=static_cast<int>((a/4)+(a/8)); int ysi2=static_cast<int>((b/4)+(b/8)); int xsd2=static_cast<int>((a/2)+(a/8)); int ysd2=static_cast<int>((b/4)+(b/8)); int xii2=static_cast<int>((a/4)+(a/8)); int yii2=static_cast<int>((b/2)+(b/8)); int xid2=static_cast<int>((a/2)+(a/8)); int yid2=static_cast<int>((b/2)+(b/8)); //240 //180 //400 //180 //240 //360 //400 //360 if((xx>xsd2)&&(yy<ysd2)){ //cout<<"Derecha y Arriba"<<endl; pixindic = Gdk::Pixbuf::create_from_file("arribader.png"); return 1; } if((xx>xid2)&&(yy>yid2)){ //cout<<"Derecha y Abajo"<<endl; pixindic = Gdk::Pixbuf::create_from_file("abajoder.png"); return 1; } if((xx<xsi2)&&(yy<ysd2)){ //cout<<"Izquierda y Arriba"<<endl; pixindic = Gdk::Pixbuf::create_from_file("arribaizq.png"); return 1; } if((xx<xii2)&&(yy>yii2)){ //cout<<"Izquierda y Abajo"<<endl; pixindic = Gdk::Pixbuf::create_from_file("abajoizq.png"); return 1; } if((xx<xsd2)&&(xx>xsi2)&&(yy<ysi2)){ //cout<<"Arriba"<<endl; pixindic = Gdk::Pixbuf::create_from_file("arriba.png"); return 1; } if((xx<xid2)&&(xx>xii2)&&(yy>yii2)){ //cout<<"Abajo"<<endl; pixindic = Gdk::Pixbuf::create_from_file("abajo.png"); return 1; } if((yy<yid2)&&(yy>ysd2)&&(xx>xsd2)){ //cout<<"Derecha"<<endl; pixindic = Gdk::Pixbuf::create_from_file("derecha.png"); return 1; } if((yy<yii2)&&(yy>ysi2)&&(xx<xsi2)){ //cout<<"Izquierda"<<endl; pixindic = Gdk::Pixbuf::create_from_file("izquierda.png"); return 1; } } /* Funciones de los botones de la aplicación */ void acceso::on_click(string boton) { if (boton=="biniciar"){ //Al activar el boton iniciar, se crea un hilo secundario //que llamará al ciclo infinito de captura y procesamiento Glib::Thread *hiloprinc = Glib::Thread::create((sigc::mem_fun (*this,&acceso::principal)),false); } if (boton=="bcerrar"){ exit(0); } if (boton=="bumbral"){ if(banderaumb==true){ pixscal = Gdk::Pixbuf::create_from_file("foto3.png"); pixfoto = pixscal->scale_simple(210, 210, Gdk::INTERP_BILINEAR); } } } 55 bool acceso::timeout(int c) { if(bandera==true){ refrescar(); } else if(bandera==false return true; } //Esta funcion es la que es llamada cada cierto tiempo //desde el ciclo principal de GTK para revisar la bandera && banderaumb==true){cout<<endl;} void acceso::refrescar(void) //Si la bandera es true, se refresca la imagen! { pixbuf = Gdk::Pixbuf::create_from_data((const guint8*)data, Gdk::COLORSPACE_RGB, FALSE, 8, cap.maxwidth, cap.maxheight, cap.maxwidth*3); image.set(pixbuf); image.show(); imageindic.set(pixindic); imageindic.show(); imagefoto.set(pixfoto); imagefoto.show(); agregartexto(texto); } imagen.h SSO v0.7 Sistema de Seguimiento de Objetos imagen.h: declaraciones de la clase imegen Copyright (C) 2006 Luis Federico Gómez Salazar <[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 <iostream> <string> <vector> <Magick++.h> using namespace std; #ifndef CLASEIMAGEN #define CLASEIMAGEN class imagen{ protected: int cols; int fils; int xsum; int ysum; float valpixel; float *frame; //matriz que contiene en todo momento la imagen a analizar int *arreglo; //arreglo que contiene las coordenadas del centroide unsigned char *histograma; double P0; double P1; double u0, ut0, s0, st0; double u1, ut1, s1, st1; double e; //variables necesarias para el metodo de umbral public: imagen(int, int, unsigned char*); virtual ~imagen(void); void escalagrises(unsigned char*); 56 void colocar(unsigned char*); void filtroruido(void); void umbral(double); int* centroide(void); int column(void); int fil(void); float getpixel(int, int); void salvar_imagen(unsigned char*, char* , char*); void salvar_imagen(float*, char*, char*); }; #endif imagen.cc SSO v0.7 Sistema de Seguimiento de Objetos imagen.cc: implementación de la clase imagen Copyright (C) 2006 Luis Federico Gómez Salazar <[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 "imagen.h" #include <math.h> using namespace Magick; imagen::imagen(int width, int height, unsigned char *pixel) { frame=NULL; cols=width; fils=height; frame = new float[fils*cols]; arreglo= new int[2]; //Constructor //Crea la memoria para la matriz que contendrá //los diferentes frames a procesar //arreglo que contendrá las coordenadas del centroide escalagrises(pixel); //Se llama a esta función para que cuando se cree el objeto histograma = new unsigned char[255]; for(int i=0;i<=255;i++) histograma[i]=0; //arreglo que contendrá el histograma de cada imagen e=2.7182818284590452354; } imagen::~imagen(void) { delete []frame; } //Se libera memoria con el destructor void imagen::colocar(unsigned char* pixel) { //Esta función es solo para prueba //y sirve para obtener la imagen pura de la cámara /*Foto0*/ char *filename0; filename0 = "foto0.png"; salvar_imagen(pixel, filename0, "RGB"); } void imagen::escalagrises(unsigned char* pixel) { float temp=0; int tam; int k=0; unsigned char* gris; tam=cols*fils; gris=new unsigned char[tam]; 57 //conversión de la imagen a escala de grises for (int i=0; i<tam*3; i+=3){ temp = 0.3*pixel[i] + 0.59*pixel[i+1] + 0.11*pixel[i+2]; if(temp >= 255) gris[k] = 254; else gris[k] = static_cast<char>(temp); k++; } k=0; //Llenado de la matriz a procesar: frame for(int i=0; i<fils;i++){ for(int j=0; j<cols;j++){ frame[i*cols+j]=static_cast<float>(gris[k]/254.0); k++; } } } void imagen::filtroruido() { float *new_frame; new_frame = new float[fils*cols]; //Se define una nueva matriz float weights[9] = {0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11}; for(int i=1;i<(fils-1);i++) for(int j=1;j<(cols-1);j++){ new_frame[i*cols + j] = weights[0]*frame[(i-1)*cols + (j-1)]+ weights[1]*frame[(i-1)*cols + j]+ weights[2]*frame[(i-1)*cols + (j+1)]+ weights[3]*frame[i*cols + (j-1)]+ weights[4]*frame[i*cols + j]+ weights[5]*frame[i*cols + (j+1)]+ weights[6]*frame[(i+1)*cols + (j-1)]+ weights[7]*frame[(i+1)*cols + j]+ weights[8]*frame[(i+1)*cols + (j+1)]; } for(int i=0; i<fils;i++){ //Se definen los bordes de la imagen de color blanco! new_frame[i*cols] = 1; new_frame[i*cols + (cols-1)] = 1; } for(int i=0; i<cols;i++){ new_frame[i] = 1; new_frame[(fils-1)*cols + i] = 1; } for(int i=0;i<fils;i++) //Se coloca la imagen filtrada en la matriz a analizar:frame for(int j=0;j<cols;j++) frame[i*cols + j] = new_frame[i*cols + j]; for(int i=0; i <fils; i++) //determinacion del histograma for(int j=0; j <cols; j++){ histograma[static_cast<unsigned char>(getpixel(i,j)*255)]++; } delete []new_frame; } void imagen::umbral(double k) { //Se define un umbral para obtener una imagen solamente blanco y negro P0=0, P1=0, ut0=0, ut1=0, u0=0, u1=0, st0=0, st1=0, s0=0, s1=0; char *filename3; string archivo3 = "foto3.png"; filename3 = &archivo3[0]; for(int i=0; i<=k; i++) P0=P0+histograma[i]; //archivo que contendrá la foto //determinacion de P0 //cout<<"hist"<<histograma[3]<<endl; cout<<"P0 = "<<P0<<endl; for(int i=(k+1); i<=255; i++) P1=P1+histograma[i]; cout<<"P1 = "<<P1<<endl; //determinacion de P1 58 if(P0!=0 && P1!=0){ for(int i=0; i<=k; i++) ut0=ut0+(i*histograma[i]); u0=ut0/P0; for(int i=(k+1); i<=255; i++) ut1=ut1+(i*histograma[i]); u1=ut1/P1; //determinacion de u0 //determinacion de u1 for(int i=0; i<=k; i++) //determinacion de s0 st0=st0+(pow((i-u0),2))*histograma[i]; s0=st0/P0; cout<<"s0= "<<s0<<endl; for(int i=(k+1); i<=255; i++) //determinacion de s1 st1=st1+(pow((i-u1),2))*histograma[i]; s1=st1/P1; cout<<"s1= "<<s1<<endl; if(s0!=0 && s1!=0){ /* Resolución de la ecuación cuadrática */ double a = ((1/s0)-(1/s1)); // cout<<"a= "<<a<<endl; double b = -2*((u0/s0)-(u1/s1)); // cout<<"b= "<<b<<endl; double c = (pow(u0,2)/s0)-(pow(u1,2)/s1) + 2*(log(s0/s1)/log(e))-2*(log(P0/P1)/log(e)); // cout<<"c= "<<c<<endl; double discr = pow(b,2)-(4*a*c); // cout<<"discr= "<<discr<<endl; double result = (-b + sqrt(discr))/(2*a); cout<<"result= "<<result<<endl; if(k==result){ //Se umbraliza la imagen contenida en float umb= static_cast<float>(result/255); //frame de acuerdo con el valor for(int i=0; i <fils; i++) //determinado for(int j=0; j <cols; j++){ if(frame[i*cols + j]<=umb){ frame[i*cols + j]=0.0001; } else if(frame[i*cols + j]>umb){ frame[i*cols + j]=1; } } salvar_imagen(frame, filename3, "I"); cout<<"umbral "<<umb<<endl; //se guarda la imagen en un archivo } else{ umbral(result); } } //llave del if interno else{ cout<<"aja"<<endl; for(int i=0; i <fils; i++) for(int j=0; j <cols; j++) frame[i*cols + j]=1; //determinado salvar_imagen(frame, filename3, "I"); } } //llave del if externo else{ cout<<"ajaaa"<<endl; for(int i=0; i <fils; i++) for(int j=0; j <cols; j++) frame[i*cols + j]=1; //determinado salvar_imagen(frame, filename3, "I"); } } int imagen::column(void) { return cols; } int imagen::fil(void) { 59 return fils; } float imagen::getpixel(int i, int j) { //obtiene el valor de un pixel float valpixel; valpixel = frame[i*cols + j]; return valpixel; } int *imagen::centroide(){ //La función centroide calculará el centroide del objeto int n=0; xsum=0; ysum=0; for (int i=0; i<fils; i++){ for(int j=0;j<cols; j++){ if(frame[i*cols + j] < 0.5){ n++; xsum+=j; ysum+=i; } } } if(n!=0){ arreglo[0]=static_cast<int>(xsum/n); arreglo[1]=static_cast<int>(ysum/n); return(arreglo); } else{ cout<<"No se registra ningun objeto"<<endl; } } /* Estas funciones que utilizan ImageMagick sirven para colocar en archivos las imagenes generadas al concluir cada procesamiento */ void imagen::salvar_imagen(unsigned char *data, char *filename, char *colorspace) { MagickLib::ExceptionInfo exception; MagickLib::Image *image; MagickLib::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 < cols * fils; i++) if(data[i] == 255) data[i] = 254; } MagickLib::InitializeMagick((char *)NULL); MagickLib::GetExceptionInfo(&exception); image_info = MagickLib::CloneImageInfo((MagickLib::ImageInfo *) NULL); image = ConstituteImage(cols, fils, colorspace, MagickLib::CharPixel, data, &exception); (void)strcpy(image->filename, filename); MagickLib::WriteImage(image_info, image); MagickLib::DestroyImage(image); MagickLib::DestroyExceptionInfo(&exception); MagickLib::DestroyImageInfo(image_info); MagickLib::DestroyMagick(); } void imagen::salvar_imagen(float *data, char *filename, char *colorspace) { unsigned char* imaprub; imaprub=new unsigned char [cols*fils]; int k=0; for (int i=0; i<fils; i++){ for(int j=0; j<cols; j++){ imaprub[k]=static_cast<unsigned char>(data[i*cols+j]*254.0); k++; } } MagickLib::ExceptionInfo exception; MagickLib::Image *image; MagickLib::ImageInfo *image_info; 60 /* Bug? de imagemagick: 255 -> 0 si la imagen está en valores de intensidad*/ if(strcmp(colorspace, "I") == 0) { int i; for(i = 0; i < cols * fils; i++) if(imaprub[i] == 255) imaprub[i] = 254; } MagickLib::InitializeMagick((char *)NULL); MagickLib::GetExceptionInfo(&exception); image_info = MagickLib::CloneImageInfo((MagickLib::ImageInfo *) NULL); image = ConstituteImage(cols, fils, colorspace, MagickLib::CharPixel, imaprub, &exception); (void)strcpy(image->filename, filename); MagickLib::WriteImage(image_info, image); MagickLib::DestroyImage(image); MagickLib::DestroyExceptionInfo(&exception); MagickLib::DestroyImageInfo(image_info); MagickLib::DestroyMagick(); delete [] imaprub; } programaprincipal.cc SSO v0.7 Sistema de Seguimiento de Objetos programaprincipal.cc: implementación del programa principal Copyright (C) 2006 Luis Federico Gómez Salazar <[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. /* Programa Principal */ #include "acceso.h" #include <gtkmm.h> int main(int argc, char *argv[]) { Gtk::Main kit(argc, argv); //Inicializa GTKmm if(!Glib::thread_supported()) Glib::thread_init(); //Inicializa threads para GTKmm acceso X; //Se instancia un objeto de tipo acceso } 61