Universidad Politécnica de Valencia
Transcripción
Universidad Politécnica de Valencia
Universidad Politécnica de Valencia Departamento de Comunicaciones Proyecto Final de Carrera Desarrollo de una interfaz gráfica para programa de reconocimiento audiovisual Presentado por: Javier Ferrandis San Cirilo Dirigida por: Dr. Alberto Albiol Colomer València, 24 de Octubre de 2003. Índice 1. Introducción y objetivos 1.1. Contenido . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2. DirectX 2.1. DirectX . . . . . . . . . . . . . . 2.2. DirectShow . . . . . . . . . . . . 2.3. COM, Component Object Model 2.3.1. Objetos COM . . . . . . . 2.3.2. Interfaces y objetos . . . . 2.3.3. GUIDs . . . . . . . . . . . 2.4. Objetos COM en DirectShow . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3. Manejo de funciones multimedia 3.1. Vı́deo . . . . . . . . . . . . . . . . . . . . . . . . . . 3.1.1. Librerı́a VICTOR . . . . . . . . . . . . . . . . 3.1.2. Problemática . . . . . . . . . . . . . . . . . . 3.1.3. Extracción de fotogramas . . . . . . . . . . . 3.1.4. Ejemplo Básico del DirectX . . . . . . . . . . 3.2. Audio . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2.1. Multimedia Streaming . . . . . . . . . . . . . 3.2.2. Extracción de audio . . . . . . . . . . . . . . . 3.2.3. Funciones para el tratamiento del audio . . . . 3.2.4. Ejemplo: Reproducción de Audio con DirectX 3.3. Librerı́a creada . . . . . . . . . . . . . . . . . . . . . 3.3.1. Vı́deo . . . . . . . . . . . . . . . . . . . . . . 3.3.2. Audio . . . . . . . . . . . . . . . . . . . . . . 4. Teorı́a del reconocimiento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 5 . . . . . . . 7 8 9 12 15 17 18 19 . . . . . . . . . . . . . 21 21 22 22 24 37 39 39 43 43 44 46 46 61 67 1 2 ÍNDICE 4.1. Reconocimiento empleando información visual . . . . . . . . . . . 4.2. Reconocimiento empleando información del audio . . . . . . . . . 5. Programación en Visual Basic y Visual 5.1. Creación de DLLs en VC++ . . . . . . 5.2. Llamada a DLLs en VB . . . . . . . . 5.3. Llamada a DLLs en VC++ . . . . . . 5.3.1. Forma directa . . . . . . . . . . 5.3.2. Forma indirecta . . . . . . . . . 5.4. Uso de librerı́as estáticas . . . . . . . . C++ . . . . . . . . . . . . . . . . . . . . . . . . 67 73 . . . . . . 77 77 80 83 83 84 85 6. Entorno gráfico 6.1. FaceDemo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.2. Recognition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.3. ModelGeneration . . . . . . . . . . . . . . . . . . . . . . . . . . . 87 87 91 94 7. Conclusiones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101 A. MATLAB 103 A.1. Compilador de MATLAB . . . . . . . . . . . . . . . . . . . . . . . 103 A.2. Generación de código . . . . . . . . . . . . . . . . . . . . . . . . . 104 A.3. Optimizaciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106 B. Tabla de conversiones entre VB y VC++ 107 Capı́tulo 1 Introducción y objetivos En primer lugar deberemos dar a conocer los objetivos de este proyecto, las motivaciones que hicieron que surgiera, y las metas que se deben cumplir. El tratamiento de vı́deos es el principal objetivo del proyecto, es decir, que se debe conseguir el poder manipular los vı́deos de una manera sencilla y cómoda, tanto para el usuario como para el futuro programador, que podrá aprovechar perfectamente cualquiera de las funciones que se lleven a cabo en este proyecto. Por ello, otro de los objetivos del proyecto es el de crear una librerı́a con todas las funciones que se hayan creado, de forma que, en un futuro, cualquier programador que tenga acceso a esta librerı́a podrá usar las funciones sin necesidad de saber cómo funcionan internamente ni introducirse a fondo en ellas. En primer lugar se nos plantea el problema de elección de la herramienta que vamos a usar durante el desarrollo del proyecto. Deberemos tener claro que necesitamos un lenguaje que nos sea útil para crear la librerı́a de funciones, pero también que sea capaz de crear un entorno gráfico capaz de llevar a cabo todas las funciones creadas. Con lo que respecta al entorno gráfico, tras revisar diversos lenguajes de programación, ası́ como distintas plataformas, se opta por elegir como lenguaje de programación el BASIC (Beginners All-Purpose Symbolic Instruction Code), que ofrece una gran facilidad con respecto a la creación del entorno gráfico del proyecto. Ası́, se elige el Microsoft Visual Basic 6.0 Edición Profesional como plataforma para llevar a cabo todo el entorno gráfico del programa. Este programa ofrece una gran variedad de utilidades para la reproducción de videos, ası́ como para la perfecta representación de imágenes, idea básica para nuestro proyecto. El lenguaje BASIC se ha utilizado a lo largo de la historia de la informática por más programadores que ningún otro lenguaje. Visual Basic ha evolucionado a partir del 3 4 CAPı́TULO 1. INTRODUCCIÓN Y OBJETIVOS lenguaje BASIC original y ahora contiene centenares de instrucciones, funciones y palabras clave, muchas de las cuales están directamente relacionadas con la interfaz gráfica de Windows. Los principiantes pueden crear aplicaciones útiles con sólo aprender unas pocas palabras clave, pero, al mismo tiempo, la eficacia del lenguaje permite a los profesionales acometer cualquier objetivo que pueda alcanzarse mediante cualquier otro lenguaje de programación de Windows. En el proyecto, se empleó este lenguaje de programación hasta que surgieron ciertas dificultades, debidas a que este lenguaje es perfecto para la creación del entorno gráfico de nuestro proyecto, pero no es nada útil a la hora de programar ciertas funciones, normalmente de bajo nivel, que eran necesarias para el desarrollo de las utilidades del trabajo. Por ello, se añade otro lenguaje de programación a nuestro proyecto, que será el C++, y la plataforma que se usa es el Microsoft Visual C++ 6.0. Este lenguaje es el más óptimo para llevar a cabo todas las funcionalidades que más adelante explicaremos, ya que es un potente motor de programación. Pero ahora necesitamos enlazar las dos plataformas, es decir, que las funciones que se crean en el C++ se deben usar después en el Visual Basic, que será donde finalmente se lleven a cabo todas las tareas que el usuario desee. Para hacer esto, se introdujo todas las funciones programadas en C++ en librerı́as dinámicas de Windows (DLL, Dynamic Link Library), que después pueden ser llamadas por el Visual Basic a través de módulos. Cuando más adelante se detalle los aspectos prácticos de la programación, se mostrará algún ejemplo de estos conceptos. Cuando vamos a trabajar con vı́deos, no podemos aplicar las funciones de transformación de imágenes directamente sobre ellos. Los vı́deos llevan una compresión espacial, que reduce el tamaño del fichero resultante, sin mermar mucho la calidad de la imagen. Pero donde de verdad se lleva a cabo una compresión fuerte es en la compresión temporal, que aprovecha la redundancia entre un fotograma y el siguiente y anterior, para no codificar toda la información, sino sólo la que no se repite. Ası́ se consigue una importante reducción de información, sin que se deteriore demasiado la calidad del vı́deo resultante. Por lo tanto, previo a la aplicación de estas funciones, es necesario extraer cada uno de los fotogramas que se van a usar. Para ello, se hace uso del DirectX, y de todas las funcionalidades que tiene para el tratamiento de imágenes, tanto estáticas como en movimiento (vı́deos). En concreto, para llevar a cabo estas funciones de extracción, dentro el DirectX existe un subconjunto, denominado DirectShow, que es el que se ocupa realmente de todo lo relacionado con imágenes. Cabe destacar que ya existen gran cantidad de programas que llevan a cabo lo que nosotros estamos intentando realizar aquı́. El verdadero problema surge cuando queremos integrar todas esas funciones a nivel de programador. Con esto 1.1. Contenido 5 quiero decir que ya existen muchos programas disponibles, incluso gratuitamente, que extraen fotogramas de una secuencia de video, y otros programas que llevan a cabo otras de las funciones que he llevado a cabo en el trabajo; sin embargo, no son integrables de forma eficiente en un entorno de trabajo como el que nosotros buscamos. Recalcar también que no es objetivo de este proyecto el estudiar ni desarrollar técnicas de reconocimiento de personajes, sino simplemente adaptar las técnicas existentes a nuestras necesidades. 1.1. Contenido Pasaremos ahora una breve introducción a lo que podemos encontrar en cada uno de los capı́tulos de esta memoria. En primer lugar veremos los detalles de la tecnologı́a DirectX, y en concreto el DirectShow, en el cual se basan la mayorı́a de funciones creadas en este proyecto. A continuación veremos qué partes concretas de todo el conjunto del DirectX se han implementado en este proyecto, tanto en la parte visual como de audio, añadiendo ejemplos prácticos para un mejor entendimiento. Además, se ha añadido una relación de todas y cada una de las funciones creadas, en forma de manual para los futuros programadores. El siguiente de los capı́tulos nos presenta una breve introducción a las técnicas de reconocimiento visual y de audio utilizadas en los programas creados. Se dedica el siguiente capı́tulo a explicar la forma de programar en cada una de las dos plataformas utilizadas, para poder enlazar las funciones creadas en cada una de ellas. En el último de los capı́tulos, se muestra un detalle de los tres programas creados, tanto a nivel de entorno gráfico, como a nivel de uso por parte del usuario final. Para finalizar, se han añadido dos anexos, dedicándose el primero de ellos al compilador de MATLAB, y el segundo de ellos a la correspondencia de datos entre las dos plataformas usadas en el proyecto. ,fig:DirectX 6 CAPı́TULO 1. INTRODUCCIÓN Y OBJETIVOS Capı́tulo 2 DirectX Como se ha comentado, el manejo de vı́deos a nivel de programación no es tan fácil como podrı́a parecerle a un usuario inexperto en los temas multimedia. En primer lugar, y debido al sistema de compresión que llevan los ficheros multimedia, vemos que no podremos ”trabajar”1 directamente sobre los ficheros de vı́deo, ya que lo que realmente nos interesa es el poder extraer la información de un fotograma en concreto, por lo que se deberá crear algún tipo de función que permita extraer dicho fotograma de la secuencia de vı́deo, para después poder trabajar sobre él. Al decir extraer lo que queremos decir es que se deberán ejecutar los algoritmos necesarios para eliminar las dependencias entre fotogramas, para poder tener ası́ una imagen estática que represente al fotograma correspondiente. A la hora de comprimir las secuencias de vı́deo, existen muy diversas maneras de llevarlo a cabo. Entre todas ellas, prevalecen una serie de estándares, que serán en los que hemos centrado el proyecto. Ası́ mismo, en el proyecto también es de gran importancia el tratamiento del audio. Por ello, también se han estudiado diversas formas de extracción de audio de los ficheros multimedia, y se comprobó que la forma más práctica y funcional era mediante el DirectX. Debido a la importancia del DirectX dentro del proyecto, dedicaremos el primer punto de este capı́tulo a explicar qué es el DirectX y de los componentes de los que está compuesto, entre los cuales destaca el DirectShow , dedicándole por tanto el segundo de los puntos. Dentro de la tecnologı́a DirectX toma gran importancia el COM (Component Object Model ), por lo que será necesario dedicarle el tercero de los puntos, reservando el último de los puntos a detallar como 1 Con trabajar nos referimos a aplicar funciones de tratamiento digital de imágenes. 7 8 CAPı́TULO 2. DIRECTX se relacionan el DirectShow y el COM. 2.1. DirectX r es un conjunto avanzado de interfaces de aplicación a Microsoft DirectX° la programación multimedia (API, Application Programming Interface) desarrolladas expresamente para los sistemas operativos Microsoft Windows. DirectX proporciona una plataforma de desarrollo estándar para los PCs basados en Windows, para que los diseñadores de software puedan acceder a caracterı́sticas concretas del hardware sin tener que escribir código especı́fico. Esta tecnologı́a fue introducida por primera vez en 1995 y es un estándar reconocido para el desarrollo de aplicaciones multimedia dentro del entorno Windows. De forma muy simple, podrı́amos decir que DirectX es una tecnologı́a Windows que permite el tratamiento y representación de gráficos y sonido, usada muy frecuentemente cuando se están ejecutando juegos o cuando se está viendo vı́deo en el PC. En el núcleo del DirectX están su interfaces de aplicación a la programación, o APIs. Las APIs actúan como un tipo de puente entre el hardware y el software para ”hablar”entre ellos. Las APIs DirectX dan a las aplicaciones multimedia acceso a las caracterı́sticas avanzadas del hardware de alto nivel, tal como chips de aceleración de gráficos tridimensionales y tarjetas de sonido. Controlan además las funciones de bajo nivel, incluyendo la aceleración de gráficos dimensionales; dan soporte a los dispositivos de entrada, como el joystick, teclado y micrófono; y controlan además el tratamiento del sonido y su salida. Gracias al DirectX el trabajo con los gráficos 3-D y la creación de efectos de música y audio son mucho más fáciles y productivos. Microsoft DirectX está compuesto por los siguientes componentes: DirectX Graphics combina los componentes DirectDraw y Direct3D de versiones anteriores dentro de una única interfaz de aplicación a la programación (API) que puede usarse para toda la parte de programación con gráficos. Este componente incluye la librerı́a Direct3DX que simplifica las tareas de programación con gráficos. DirectX Audio combina los componentes DirectSound y DirectMusic de versiones anteriores dentro de una única API que puede usarse para toda la parte de programación con audio. DirectInput proporciona soporte para una gran variedad de dispositivos de entrada, incluyendo soporte total para la tecnologı́a force-feedback. 9 2.2. DirectShow DirectPlay proporciona soporte para la programación de juegos multijugador en red. DirectShow nos permite capturar audio y video con una gran calidad, además de reproducción de archivos multimedia. DirectSetup es un API simple que proporciona la instalación de todos los componentes DirectX de una sola vez. En la figura 2.1 podemos ver de forma gráfica cada uno de los componentes de la tecnologı́a DirectX. Figura 2.1: Componentes del DirectX. 2.2. DirectShow Microsoft DirectShow es una arquitectura, incluida dentro del grupo del DirectX, que se creó para ayudar a la manipulación de archivos multimedia dentro de la plataforma Windows. DirectShow proporciona funciones para la captura de multimedia con buena calidad, además de reproducción de streams multimedia (ya sean audio, vı́deo,...). Soporta gran variedad de formatos, incluyendo ASF (Advanced Streaming Format), MPEG (Motion Picture Experts Group), AVI (Audio-Video Interleaved ), MP3 (MPEG Audio Layer-3 ), y archivos WAV. Soporta también la captura usando los dispositivos WDM (Windows Driver Model ) o incluso los más viejos dispositivos de ’Video for Windows’. La forma de trabajar de esta arquitectura es utilizando el hardware de aceleración de vı́deo y 10 CAPı́TULO 2. DIRECTX audio, cuando detecta que está disponible, aunque también existe la posibilidad de trabajar sin estos dispositivos. DirectShow simplifica todas las tareas de tratamiento de multimedia, proporcionando acceso a la arquitectura de control de streams, para que las aplicaciones puedan crear sus propias soluciones propietarias, como por ejemplo el aplicar efectos sonoros a archivos de audio, o dar soporte a cualquier nuevo formato de archivo. Algunos ejemplos de los tipos de aplicaciones que podemos escribir con el DirectShow incluyen reproductores de DVD, aplicaciones de edición de vı́deo, conversores de AVI a ASF, reproductores de MP3, o aplicaciones de captura de vı́deo digital. DirectShow está basado en el COM (Component Object Model ). Para escribir una aplicación en DirectShow, se deberá entender perfectamente la programación COM. De forma muy resumida, podremos decir que el COM es un estándar que contiene cierto número de interfaces, las cuales a su vez contendrán funciones especı́ficas para cada tarea que deseemos llevar a cabo. Ası́, en nuestra aplicación tendremos objetos COM, que representarán a las interfaces, y desde los que podremos llamar a los métodos correspondientes. Explicaremos estos conceptos con detalle en la sección 2.3 (véase figura 2.4 en la página 14 para tener una visión general de estos conceptos). En la mayorı́a de aplicaciones, no se necesitan implementar nuestros propios objetos COM. DirectShow ya proporciona los componentes que necesitamos. Pero si queremos escribir nuestros propios componentes, como ocurre en nuestro caso, deberemos implementarlos usando los objetos COM (véase sección 2.3.1). Arquitectura La arquitectura DirectShow define cómo controlar y procesar los streams de datos multimedia, usando componentes modulares llamados filtros. Los filtros tienen pins de entrada, pins de salida o ambos, y están conectados entre ellos en una configuración llamada gráfico de filtros. Un gráfico de filtros completo está compuesto de un cierto número de filtros, unidos en una sucesión lógica desde la fuente de datos hasta el reproductor. Las aplicaciones usan un objeto COM llamado administrador de gráfico de filtros para ensamblar los filtros dentro del gráfico y mover los datos a través del mismo. Por defecto, el administrador de gráfico de filtros trata los datos automáticamente; por ejemplo, si queremos reproducir un vı́deo, él mismo se encarga de insertar automáticamente el codec 2.2. DirectShow 11 adecuado, si es que es necesario, e inmediatamente conecta la salida de dicho codec a un filtro transformador, que adaptará los datos para pasarlos a un filtro reproductor, que finalmente reproducirá el vı́deo que querı́amos, y todo ello de forma automática, sin tener que dar nosotros ninguna orden en especial. Podemos ver de forma gráfica los filtros y pines de entrada y salida en la figura 2.2. Figura 2.2: Gráfico de filtros para la reproducción de un vı́deo. Por supuesto, si queremos podremos especificar los filtros que queremos que use y las conexiones que deseemos, si es que no queremos usar la configuración por defecto que nos presenta el DirectShow. El administrador de gráfico de filtros proporciona un conjunto de interfaces COM para que las aplicaciones puedan acceder al gráfico de filtros. Las aplicaciones pueden llamar directamente a las interfaces del administrador de gráfico de filtros para controlar los streams multimedia o recibir eventos de filtros; o pueden usar el control ActiveMovie1 para reproducir los archivos multimedia. De esta forma, vemos que tenemos varios caminos para acceder a la tecnologı́a DirectShow: Mediante el COM, tal y como veremos en el punto siguiente, y que será la forma con la que hemos trabajado en nuestro proyecto. Mediante el control ActiveMovie, mucho más fácil e intuitivo, pero con mucha menos movilidad y con muy pocas opciones de configuración. 1 ActiveMovie es un control válido para Visual Basic y para páginas HTML. Proporciona una interfaz de programación fácil de usar, mediante el uso de los eventos, propiedades y métodos del control. 12 CAPı́TULO 2. DIRECTX Mediante el MCI (Media Control Interface), de gran potencia pero muy tedioso y sin demasiadas ventajas al primer punto. En la figura 2.3 representamos estas formas de acceso con mayor claridad. Figura 2.3: Métodos de acceso al DirectShow. 2.3. COM, Component Object Model Desde un punto de vista práctico, un objeto COM es básicamente una caja negra que nuestra aplicación puede usar para llevar a cabo una o más tareas. El COM (Component Object Model ) es un estándar binario1 que define cómo se crean y destruyen los objetos y, lo más importante, cómo interactúan entre ellos. La idea es que distintas aplicaciones con distintos códigos fuente puedan comunicarse entre ellas mediante procesos, siempre que dichas aplicaciones sigan el estándar COM. Normalmente se usa el COM para establecer fácilmente comunicaciones con otros procesos. 1 Se reconoce un estándar binario porque sus componentes se componen básicamente de datos binarios. Es decir, se centran en un entorno de ejecución y no son válidos para todas las plataformas. 2.3. COM, Component Object Model 13 Debido a que el COM es un estándar binario, podremos decir que es un lenguaje independiente. Es decir, que no necesariamente tendremos que usar el C++ para implementar objetos COM. Se podrá usar cualquier lenguaje que soporte tablas de punteros a función1 . Existen ciertos conceptos básicos dentro del COM, que nos ayudarán a entender el funcionamiento de dicho estándar. Pasamos a continuación a dar una breve reseña de dichos conceptos, para pasar después a explicarlos con detalle: Una interfaz COM es una colección de métodos lógicamente relacionados que llevan a cabo una única funcionalidad. Todas las interfaces COM están basadas en la interfaz IUnknown, y todas ellas están identificadas mediante un identificador único de interfaz (IID). Una clase COM es la implementación de una o más interfaces COM. Un objeto COM es una instancia a una clase COM. Un filtro DirectShow, por ejemplo, es un objeto COM. Cada objeto tiene un identificador globalmente único de clase (CLSID). Los GUIDs son números que identifican a las interfaces y objetos COM, y son usados para eliminar las coincidencias de nombres entre distintas aplicaciones. Los CLSID y los IID son subconjuntos de este grupo. La diferencia entre clase COM y objeto COM es mı́nima, por lo que en muchas ocasiones nos referiremos a los objetos COM como clases, y viceversa. En la figura 2.4 vemos una representación gráfica de estos conceptos, en la que observamos que cada clase COM puede tener una o varias interfaces; y que los objetos COM son instancias de las clases COM, obtenidas mediante la función CoCreateInstance. Mediante la llamada a esta función, también obtenemos un puntero a la interfaz que indiquemos como parámetro. Existirán métodos para obtener los punteros a las demás interfaces existentes en el objeto. Cualquier acceso a un objeto COM se llevará a cabo mediante punteros a sus interfaces. Los métodos de la interfaz son puramente virtuales y se almacenan en una tabla llamada ”vtable”. El puntero de interfaz apuntará al principio de la vtable. Una interfaz COM define los tipos de parámetros y la sintaxis para cada uno de esos métodos. Las clases COM proporcionan una implementación para cada método de la interfaz. 1 Las tablas de punteros a función básicamente son arrays, solo que sus componentes no son variables, sino funciones. 14 CAPı́TULO 2. DIRECTX Figura 2.4: Métodos de acceso al DirectShow. Una vez se ha definido un objeto COM y se le ha asignado un CLSID, ya podremos crear instancias de dicho objeto. Existen diferentes formas de crear dicha instancia, pero la más utilizada será el método COM CoCreateInstance. Cuando creamos una instancia del objeto, se nos devolverá un puntero a una de las interfaces del objeto, exactamente a la interfaz que le indiquemos en la función CoCreateInstance. Una vez ya tengamos el puntero inicial a la interfaz principal del objeto, podremos usar el método IUnknown::QueryInterface para averiguar si el objeto soporta alguna otra interfaz especı́fica y, si lo hace, obtener un puntero a dicha interfaz. Para clarificar estos conceptos, pondremos un sencillo ejemplo, en el cuál crearemos un gráfico de filtros, comprobaremos si es factible el añadir un filtro llamado Sample Grabber y, si lo es, lo incluiremos en el gráfico creado. Podemos ver este código de ejemplo en la figura 2.5, en la cual vemos que, después de declarar las variables necesarias, se llama al método CoCreateInstance para obtener una instancia del objeto Gráfico de Filtros. A continuación deberemos crear el filtro, también mediante el método CoCreateInstance. Con esta 2.3. COM, Component Object Model 15 llamada, obtenemos un puntero a la interfaz IBaseFilter, que será el que utilizaremos a continuación, mediante el método QueryInterface, para comprobar si es posible añadir el filtro ”Sample Grabber” a dicho gráfico. Si el valor de retorno es distinto de NULL, significará que el resultado es satisfactorio, por lo que simplemente nos faltará el llamar al método AddFilter de la interfaz IGraphBuilder para insertar el filtro creado dentro del gráfico de filtros. Figura 2.5: Ejemplo de código de creación de objetos COM. En la figura 2.5 también podemos ver un ejemplo de los números que identifican a cada objeto, los CLSID. Vemos que, en las llamadas a la función CoCreateInstance, para cada objeto deberemos indicar un número diferente, correspondiente al identificador de dicho objeto. Igualmente, podemos ver el IID que se le indica para que nos devuelva el puntero a la interfaz deseada. A continuación describiremos con detalle cada uno de estos conceptos. 2.3.1. Objetos COM Como hemos dicho con anterioridad, y desde un punto de vista práctico, un objeto COM es básicamente una caja negra que nuestra aplicación puede usar para llevar a cabo una o más tareas. Los objetos COM (Component Object Model ) son componentes de software reutilizables que se adhieren a la especificación del COM. La adhesión a esta especificación asegura que los objetos COM trabajan bien juntos y pueden ser fácilmente incorporados dentro de las aplicaciones. Los objetos COM son implementados mayoritariamente como DLLs (Dynamic Link Libraries). Tal y como una DLL convencional, los objetos COM exponen métodos que nuestra aplicación 16 CAPı́TULO 2. DIRECTX puede llamar para llevar a cabo cualquiera de las tareas soportadas. Nuestra aplicación interactúa con un objeto COM tal y como lo harı́a con un objeto C++. Sin embargo, hay algunas diferencias significativas: Los objetos COM hacen valer una encapsulación más estricta. No se puede simplemente crear el objeto y llamar a cualquier método público. Los métodos públicos de un objeto COM están agrupados dentro de una o más interfaces. Tı́picamente una interfaz contiene un conjunto determinado de métodos, cada uno de los cuales da acceso a una caracterı́stica del objeto. Por ejemplo, existe una interfaz que nos permite controlar la reproducción de un vı́deo, que tendrá métodos como play, stop, pause,... Cualquier método que no sea parte de una interfaz no podrá usarse. Los objetos COM se crean de manera distinta a los objetos C++. Existen varias formas diferentes de crear un objeto COM, pero todas ellas involucran las técnicas especı́ficas COM. Las APIs de DirectX incluyen gran cantidad de funciones de ayuda y métodos que simplifican la creación de la mayorı́a de los objetos DirectX. Se deben usar las técnicas especı́ficas COM para controlar la vida del objeto. Es decir, que deberemos tener muy en cuenta las veces que creamos y destruimos un objeto COM (para ello utilizaremos una técnica existente llamada Reference Counting, tal y como se verá más adelante). Los objetos COM no necesitan ser cargados explı́citamente, normalmente están contenidos en una DLL. Sin embargo, no se necesita cargar explı́citamente la DLL o enlazar a una librerı́a estática para usar un objeto COM. Cada objeto COM tiene un único identificador registrado que se usa para crear el objeto. COM automáticamente carga la DLL correcta. Como se ha dicho con anterioridad, el COM es una especificación binaria. Los objetos COM pueden ser agregados y ser accedidos por multitud de lenguajes de programación. No necesitamos conocer nada acerca del código fuente de los objetos. Por ejemplo, las aplicaciones en Microsoft Visual Basic normalmente utilizan objetos COM que han sido escritos en C++. Con esto podemos ver que no necesitamos conocer la total compatibilidad que tienen estos objetos COM con cualquier lenguaje de programación que usemos. Reference counting es la técnica mediante la que un objeto (o, estrictamente, una interfaz) decide cuando no va a ser usada más veces y puede destruirse a si misma. Los objetos COM están asignados dinámicamente a múltiples clientes que pueden usarlos simultáneamente. Para evitar memorias activas, el objeto COM debe conservar una cuenta del número de clientes que están usándolo, y destruirse 2.3. COM, Component Object Model 17 cuando ningún cliente lo necesite más. El número de clientes que están usando el objeto se almacena en la cuenta de referencia (reference count). Cada vez que se crea un nuevo puntero a una interfaz de un objeto COM, el cliente que usa dicho objeto debe aumentar la cuenta de referencia llamando a la función AddRef del puntero de interfaz. Cada vez que un cliente destruye un puntero de interfaz a un objeto, deberá primero decrementar la cuenta de referencia mediante la función Release del puntero de interfaz. Ası́, cuando no haya ningún usuario que haga uso de un objeto COM, se destruirá el mismo para gestionar bien el uso de memoria. 2.3.2. Interfaces y objetos Es importante entender la diferencia entre objetos e interfaces. La mayorı́a de las veces, llamamos a un objeto por el nombre de su principal interfaz. Sin embargo, hablando estrictamente, los dos términos no son intercambiables, ya que tienen algunas diferencias importantes. Veremos a continuación las principales diferencias: Un objeto contiene un número indeterminado de interfaces. Para usar un método en particular, no solo se debe crear el objeto, además se debe obtener el puntero a la interfaz correspondiente. Más de un objeto puede ofrecer la misma interfaz. Una interfaz es un grupo de métodos que llevan a cabo un conjunto especificado de operaciones. La definición de interfaz especifica sólo la sintaxis de los métodos y su funcionalidad general. Cualquier objeto COM que necesite soportar un conjunto particular de operaciones puede hacerlo ofreciendo la interfaz correspondiente. Algunas interfaces están muy especializadas y son expuestas sólo por un único objeto. Otras se usan en una gran variedad de circunstancias, ası́ que son ofrecidas por muchos objetos. El caso extremo es la interfaz IUnknown, que debe ser ofrecida por todos los objetos COM 1 . Pondremos ahora un ejemplo práctico para establecer definitivamente la diferencia entre objetos e interfaces COM. El ejemplo más claro lo tenemos en el objeto FilterGraph, que nos creará un gráfico de filtros (ver sección 2.2). Dentro de 1 Si un objeto ofrece una interfaz, ésta debe soportar todos los métodos que se detallan en la definición de interfaz. Es decir, que se puede llamar a cualquier método y estar seguro de que existe. Sin embargo, los detalles de implementación de un método en particular pueden variar de un objeto a otro. Por ejemplo, distintos objetos pueden usar algoritmos distintos para llegar al mismo resultado final. Algunas veces un objeto ofrece una interfaz usada comúnmente, pero necesita soportar sólo un subconjunto de sus métodos. Lo que se hace es no implementar los métodos que no se necesitan, y si se llaman devolverán la constante E NOTIMPL. 18 CAPı́TULO 2. DIRECTX este objeto, nos encontramos con varias interfaces, como por ejemplo la interfaz IGraphBuilder, que será la que nos ofrece objetos para insertar filtros dentro del gráfico; por ser esta la de uso más habitual, se suele llamar a este objeto GraphBuilder, aunque como hemos dicho es un error. Además de esta interfaz, el objeto nos ofrece también la interfaz IMediaControl, que nos ofrecerá métodos para controlar el flujo de datos dentro del gráfico. También nos ofrece la interfaz IMediaEvent, que nos ofrece métodos para obtener información de eventos sucedidos en el gráfico de filtros. Vemos por lo tanto que un objeto puede ofrecer varias interfaces. El estándar COM requiere que una definición de interfaz no cambie una vez ha sido publicada. No se puede, por ejemplo, añadir un nuevo método a una interfaz existente. Para hacerlo, se deberı́a crear una nueva interfaz. No hay ninguna restricción acerca de qué métodos deber estar incluidos en una interfaz, una práctica común es hacer que la próxima interfaz incluya todos los métodos de la interfaz vieja, mas los nuevos métodos. No es demasiado inusual tener varias versiones de una misma interfaz. Generalmente, todas las versiones llevan a cabo esencialmente la misma tarea, pero difieren en los detalles. Frecuentemente, un objeto ofrece varias versiones de la interfaz, con lo que se permite que las viejas aplicaciones sigan usando las mismas interfaces de los objetos viejos, mientras las nuevas aplicaciones pueden aprovecharse de las ventajas de las nuevas interfaces. Normalmente, una familia de interfaces tiene el mismo nombre, añadiendo un entero que referencia la versión. Ası́, se mantiene la total compatibilidad entre una nueva versión de esta tecnologı́a y las versiones ya existentes. 2.3.3. GUIDs GUID son las siglas de Globally Unique IDentifiers, y son una parte clave del modelo de programación COM. En su forma más básica, un GUID es una estructura de 128 bits. Sin embargo, los GUIDs se han creado de tal forma que garantizan que ningún GUID se repita. COM usa GUIDs principalmente por dos propósitos: 1. Para identificar de forma única un objeto COM en particular. Un GUID que se asigna a un objeto COM se llama una clase ID (CLSID). Deberemos usar un CLSID cuando queramos crear una instancia del objeto COM asociado. 2. Para identificar de forma única una interfaz COM. El GUID que se asocia con una interfaz COM en particular se llama un ID de interfaz (IID). Deberemos usar un IID cuando se requiera una interfaz particular de un 2.4. Objetos COM en DirectShow 19 objeto. Un IID de interfaz será lo mismo, sin tener en cuenta que el objeto ofrece la interfaz. 2.4. Objetos COM en DirectShow Los filtros DirectShow, el administrador de gráfico de filtros,... son todos ellos objetos COM. Se ha adoptado un diseño general para la forma en que el DirectShow implementa los objetos COM. Este diseño está disponible para ayudarnos a implementar nuestros propios filtros o cualquier objeto COM que deseemos. Tı́picamente, una clase única C++ implementa una única clase COM. El espacio de trabajo COM de DirectShow requiere que las clases C++ implementen los objetos COM conforme a unas simples reglas básicas. Una de esas reglas es que el programador proporciona una plantilla para cada una de las clases. Dicha plantilla contendrá la información de la clase que es fundamental para el espacio de trabajo. Las plantillas se definen dentro de la DLL usando dos variables globales: g Templates y g cTemplates tal y como se muestra en el ejemplo siguiente: CFactoryTemplate g Templates[]= { {L"My class name", &CLSID MyClass, CMyClass::CreateInstance, CMyClass::Init}, {L"My class name2", &CLSID MyClass2, CMyClass2::CreateInstance} }; int g cTemplates = sizeof(g Templates)/sizeof(g Templates[0]); Los nombres y tipos de esas variables deben aparecer exactamente igual que el ejemplo anterior. A causa de que una DLL puede contener varias clases COM, cada una de ellas requerirá una plantilla diferente. La plantilla está definida en un array y el número de elementos en el array se almacena en otra variable. Cada elemento del array contiene los siguientes campos: Una descripción textual de la clase (usando caracteres anchos, por lo que tendremos anteponer el prefijo ”L”). Un puntero al CLSID (identificador de clase) de la clase. Un puntero a un método estático de la clase que puede crear instancias de la clase (CFactoryTemplate::CreateInstance). 20 CAPı́TULO 2. DIRECTX Un puntero a un método estático de la clase. Se llama a este método cuando la DLL se carga o se descarga y puede trabajar como la antigua inicialización y finalización. Si no se requiere este método, puede ser omitido, inicializándose a NULL, y será ignorado. Un puntero a una estructura AMOVIESETUP FILTER. Se requiere cuando se usan servicios de auto-registro de filtros. El esquema DirectShow COM usa la información que contiene esta plantilla para crear instancias de la clase especı́fica, y para registrar y quitar del registro las clases COM. Capı́tulo 3 Manejo de funciones multimedia En este capı́tulo nos dedicaremos a explicar todos los detalles concernientes a la programación. Ası́, podremos ver las funciones que se han creado para el manejo de los vı́deos, las interfaces y librerı́as de las que se ha hecho uso para llegar a las funciones finales, problemas surgidos conforme la marcha del proyecto,... Se ha dividido el capı́tulo en dos puntos fundamentales: vı́deo y audio. Se ha hecho ası́ porque aunque desde el punto de vista de la programación sean bastante similares, no lo es ası́ en el modelo conceptual que perseguimos con este proyecto. Por lo tanto, encontraremos en primer lugar un punto dedicado al vı́deo, en el que podremos encontrar el conjunto de tecnologı́as utilizadas para su tratamiento. A continuación encontramos un segundo punto dedicado al audio, en el que se mostrarán, al igual que con el vı́deo, las soluciones que se han tomado para su manejo. Además, en ambos puntos, encontraremos ejemplos de fácil implementación que aclararán toda la teorı́a expuesta. En el tercer y último punto se ha llevado a cabo una relación de todas las funciones creadas y que estarán disponibles en la librerı́a, además de su uso y el detalle de sus parámetros. 3.1. Vı́deo En este punto veremos las soluciones que se han adoptado en lo referente a la extracción de fotogramas, es decir, qué interfaces de las que se veı́an en el capı́tulo anterior hemos usado, qué librerı́as adicionales se utilizan,... En primer lugar explicaremos básicamente qué es la librerı́a VICTOR, librerı́a 21 22 CAPı́TULO 3. MANEJO DE FUNCIONES MULTIMEDIA de tratamiento digital de imágenes, de gran utilidad para el tratamiento de los fotogramas extraı́dos. En el punto 3.1.2 se explica la problemática con que nos encontramos a la hora de la extracción de fotogramas; estos problemas provienen de que en el entorno Windows existen dos tipos principales de plataformas, la del Windows 98 y la del Windows NT. En un principio se desarrolló la aplicación en el sistema operativo w98, de forma que cuando se hicieron las pruebas pertinentes en el sistema operativo wXP (perteneciente al grupo de Windows NT), las funciones no funcionaban correctamente, ya que existen diferencias significativas entre ambos. Por ello, se debe optar por soluciones distintas en cada una de las plataformas. En el punto 3.1.3 se explican las interfaces del DirectShow que se han usado, ası́ como la librerı́a utilizada para el manejo de los vı́deos MPEG-2. Por último, en el punto 3.1.4 encontraremos un práctico ejemplo de cómo reproducir un vı́deo utilizando la tecnologı́a DirectShow. 3.1.1. Librerı́a VICTOR Antes de empezar a explicar cualquier cosa concerniente al tratamiento de las imágenes, es imprescindible hacer una referencia a la librerı́a de tratamiento de imágenes llamada VICTOR. Esta librerı́a nos permitirá trabajar con los fotogramas que vayamos extrayendo de los vı́deos, ya que nos proporciona funciones para abrir y guardar imágenes, para convertir de formatos, para manipular las imágenes una vez cargadas en memoria, entre otras muchas aplicaciones. Quizás las funciones que más hayamos utilizado en este proyecto hayan sido las de guardar imágenes y las de abrir imágenes guardadas en disco. Pero además de estas dos funciones, se han utilizado las funciones de diezmado e interpolación de imágenes, las funciones de dibujar lı́neas dando las coordenadas pertinentes,... 3.1.2. Problemática Cuando se llegó al momento de la extracción de fotogramas, se comprobó que todo lo que funcionaba en un sistema operativo, no funcionaba exactamente igual en otro sistema operativo diferente. Esto ocurrı́a porque normalmente se trabajaba en un entorno FAT32, tal como el Windows 95, 98 o Millenium. El problema llegó cuando se pasó a trabajar en un entorno NT, tal como el Windows NT o el XP. Y dado la amplia expansión del sistema operativo Windows XP, se debieron 3.1. Vı́deo 23 realizar funciones para que este proyecto funcionara correctamente, además de con cualquier formato de vı́deo, en cualquier plataforma Windows en la que se trabajara. Por ello, y antes de empezar a explicar cada una de las interfaces que hemos usado para llevar a cabo la extracción de fotogramas, veremos primero qué opciones se eligieron dependiendo del sistema operativo y del formato de vı́deo en concreto. Cuando se trabaja con el formato de vı́deo MPEG-2 no existe ningún tipo de problema con respecto al sistema operativo. Para ello debemos usar la librerı́a que describiremos en el punto 3.1.3, llamada Mpeg2Lib, y que se encarga de extraer los fotogramas mediante las llamadas oportunas a sus funciones, sea cual sea la plataforma en la que nos encontremos. Esta librerı́a soluciona los problemas vistos porque no se basa en funciones del sistema operativo, por lo que es independiente de la plataforma en la que se utilice. Con el formato AVI tampoco tenemos ningún problema, ya que se utiliza la librerı́a AVIFile (tal y como veremos en el punto 3.1.3), que nos proporciona movilidad suficiente para hacer lo que deseemos con los vı́deos AVI. El problema llega cuando trabajamos con MPEG-1, en los que se utilizan distintas interfaces del DirectX SDK. Como veremos en el siguiente punto, la forma más fácil de extraer los fotogramas es mediante la interfaz de DirectShow IMediaDet, la cual se encarga de abrir el stream de vı́deo, extraer el fotograma y guardarlo en un buffer de memoria, para después cerrar el archivo multimedia y poder trabajar con dicho fotograma. Además de ser la forma más fácil, es la que menos recursos utiliza, con la ventaja adicional de que no importa que el vı́deo esté abierto en otras partes (como por ejemplo en nuestra aplicación, en la que está reproduciéndose en la ventana principal). Por ello, en los casos en los que sea posible elegiremos esta solución como la óptima. Este será el caso del MPEG-1, ya que esta interfaz no es válida cuando trabajamos con MPEG-2. Esta interfaz funciona correctamente con estos vı́deos pero sólo cuando estamos en un entorno Windows 98. Cuando trabajamos en el entorno NT (es decir, en Windows XP) esta interfaz no es válida, y no nos extrae el fotograma que deseamos. Simplemente se limita a abrir el vı́deo y extraernos el primer fotograma, sin tener posibilidad de elegir el fotograma que quesiéramos; a cause de este grave problema, se debe encontrar otra solución alternativa. 24 CAPı́TULO 3. MANEJO DE FUNCIONES MULTIMEDIA MPEG-1 MPEG-2 AVI VOB Windows 98 IMediaDet Mpeg2Lib AVIFile Mpeg2Lib Windows XP ISampleGrabber Mpeg2LIb AVIFile Mpeg2Lib Cuadro 3.1: Relación de sistemas operativos e interfaces utilizadas. Dicha solución la encontramos con la interfaz ISampleGrabber (ver punto 3.1.3), la cuál nos realiza las mismas funciones que la interfaz anterior, pero además trabaja perfectamente en un entorno Windows NT o XP. Esta interfaz también nos es válida para vı́deos MPEG-2 en un entorno Windows 98, pero es mucho más compleja y utiliza más recursos que las funciones de la librerı́a que hemos descrito para el MPEG-2. Por lo tanto, y resumiendo, lo que hacemos a la hora de extraer los fotogramas es lo siguiente: 1. Primero comprobamos si es VOB. Si es ası́, utilizamos la librerı́a Mpeg2Lib y obtenemos directamente el fotograma. 2. Si no es VOB, comprobamos si es AVI. Si es ası́, utilizamos la librerı́a AVIFile y obtenemos directamente el fotograma. 3. Si no es VOB ni AVI, comprobamos si es MPEG, y si es ası́ hacemos una segundo comprobación para ver si es MPEG-1 o MPEG-2. Si es MPEG-2 utilizamos la librerı́a MPEG2Lib y obtenemos directamente el fotograma. 4. Si es MPEG-1 o AVI lo que hacemos es comprobar la plataforma en la que estamos trabajando. Si es Windows 98, utilizamos la interfaz IMediaDet, y si estamos en Windows NT o XP, utilizamos la interfaz ISampleGrabber, con lo que ya tendrı́amos el fotograma que deseábamos. 3.1.3. Extracción de fotogramas Veremos en este punto las dos interfaces principales del DirectShow que se han usado en cuanto a la extracción de fotogramas. Ası́, se verá con detalle lo 3.1. Vı́deo 25 que ya se podı́a entrever en el punto anterior. Además, se profundizará también en la librerı́a Mpeg2Lib, que como hemos dicho se encargará del manejo de vı́deos en formato MPEG-2, ası́ como en la librerı́a AVIFile, válidad para los vı́deos en formato AVI. IMediaDet Cuando se nos presenta el problema de extraer fotogramas, la solución la hemos encontrado en el DirectShow, tal y como hemos explicado anteriormente. En concreto, dentro del DirectShow encontramos una interfaz, denominada IMediaDet que nos lleva a cabo perfectamente las opciones de extracción de fotogramas. La interfaz IMediaDet se utiliza en general para obtener información acerca de un fichero multimedia, tal como el número de streams, el tipo de medio (audio, vı́deo, ...), duración total o velocidad de reproducción de cada uno de los streams. Pero además de estas importantes funciones, también contiene métodos para obtener fotogramas especı́ficos de un stream de vı́deo. Figura 3.1: Extracción de un fotograma mediante la interfaz IMediaDet. Ası́, de una forma muy general, explicaremos ahora los pasos que deben seguirse para conseguir extraer los fotogramas (en la figura 3.1 podemos ver un diagrama de bloques del proceso). En primer lugar, se debe inicializar el COM, mediante la función CoInitialize, pudiendo a partir de entonces utilizar cualquiera de las interfaces del COM. A continuación, deberemos inicializar la interfaz, enlazándola con el COM, tal y como se ha explicado en capı́tulos anteriores. Para ello, usaremos la función CoCreateInstance, indicándole que la instancia que queremos crear es de la interfaz IMediaDet, mediante el GUID CLSID MediaDet. 26 CAPı́TULO 3. MANEJO DE FUNCIONES MULTIMEDIA A continuación, deberemos indicarle el nombre y la ruta completa del fichero de vı́deo del que queremos extraer el fotograma. Para ello, usaremos el método put Filename, que nos provee la interfaz. Esta función tiene un matiz, y es que no podemos pasarle como parámetro el tipo de texto al que estamos acostumbrados (char* ), sino que se le debe pasar un tipo de datos llamado CComBSTR, por lo que deberemos hacer la correspondiente translación. Una vez hecho esto, buscaremos el stream de vı́deo, ya que los otros streams no nos interesan de momento. Para ello, obtenemos primero el número de streams que contiene el vı́deo, mediante el método get OutputStreams, y a continuación, mediante un bucle, obtenemos el tipo de datos que contiene cada stream, mediante el método get StreamType, y lo comparamos con el tipo MEDIATYPE Video, de forma que cuando coincidan querrá decir que hemos encontrado el stream de vı́deo. Una vez hemos conseguido el stream de vı́deo, debemos obtener la información necesaria para el consiguiente tratamiento, es decir, el ancho y alto del vı́deo, para poder guardar después espacio en memoria para el fotograma extraı́do. Para ello llamamos al método get StreamVideoType, que nos devuelve una estructura del tipo AM MEDIA TYPE. A continuación mostramos un detalle de esta estructura, con todos los campos que contiene: typedef struct MediaType { GUID majortype; GUID subtype; BOOL bFixedSizeSamples; BOOL bTemporalCompression; ULONG lSampleSize; GUID formattype; IUnknown *pUnk; BYTE *pbFormat; } AM MEDIA TYPE; De todos ellos, nos interesa uno en especial, el pbFormat, que contiene toda la información que nosotros necesitamos. En este caso, ese campo toma la forma de la estructura VIDEOINFOHEADER*, dentro del cuál encontramos el alto del vı́deo, el ancho, entre otros parámetros de utilidad. Mostramos a continuación el detalle de esta estructura, con todos los campos que contiene: 3.1. Vı́deo 27 typedef struct tagVIDEOINFOHEADER { RECT rcSource; RECT rcTarget; DWORD dwBitRate; DWORD dwBitErrorRate; REFERENCE TIME AvgTimePerFrame; BITMAPINFOHEADER bmiHeader; } VIDEOINFOHEADER; Una vez obtenida esta información, liberamos el espacio en memoria que estábamos ocupando, con la función FreeMediaType, y nos ponemos en la labor de la extracción del fotograma. Para ello, deberemos ejecutar un método, el EnterBitmapGrabMode, que nos sitúa la interfaz en un nuevo modo llamado modo de grabación. Además de esto, nos sirve para posicionar el vı́deo en el fotograma que deseamos. Una reseña tendremos que hacer ahora, y es que no podemos indicarle el número de fotograma en concreto, sino la posición en segundos de la que queremos extraer el fotograma. Sin embargo, este es un problema de fácil solución, ya que tenemos el número de fotogramas por segundo, por lo que con una simple operación podremos saber perfectamente la correspondencia entre el fotograma que queremos extraer y el segundo en que está posicionado dicho fotograma. Por último, ejecutaremos el método GetBitmapBits en dos ocasiones: la primera para obtener el tamaño del buffer que debemos reservar para guardar la imagen, y la segunda para obtener el fotograma en concreto, indicando en ambas el ancho y el alto del vı́deo, que habı́amos obtenido con anterioridad. Una vez llevado a cabo estas acciones, ya tenemos en memoria el fotograma deseado, por lo que lo único que se debe hacer ahora es guardarlo en disco, para poder trabajar con él. Para ello usamos la librerı́a de VICTOR, que nos permite guardar y recuperar imágenes de disco. Para finalizar, deberemos finalizar el COM que habı́amos inicializado al principio, con la función CoUninitialize. ISampleGrabber Esta interfaz está basada en el filtro Sample Grabber , y nos permite obtener fotogramas de casi cualquier formato de vı́deo. Podrı́a parecer que es redundante 28 CAPı́TULO 3. MANEJO DE FUNCIONES MULTIMEDIA a todo lo que hemos explicado en el punto anterior, pero debido a las incompatibilidades que existen entre las distintas plataformas en las que se va a ejecutar este programa, esta interfaz se convierte en imprescindible para la extracción de fotogramas. Pasaremos a continuación a explicar con detalle cómo funciona esta interfaz, y los pasos a seguir para conseguir obtener al final el fotograma que consideremos necesario. En la figura 3.2 podemos ver un diagrama de bloques de todas las funciones necesarias para la extracción del fotograma. Figura 3.2: Extracción de un fotograma mediante la interfaz ISampleGrabber. En primer lugar, deberemos inicializar la librerı́a COM, mediante la llamada a la función CoInitialize. Este paso es muy importante, ya que si no lo realizamos no podremos trabajar con el COM, y por lo tanto no será posible llevar a cabo ninguna de las tareas siguientes. A continuación, deberemos crear la interfaz que contendrá todos los filtros necesarios. Para ello, inicializamos la interfaz IGraphBuilder mediante el método CoCreateInstance. Además, mediante la misma función, deberemos inicializar también el filtro Sample Grabber, contenido en la interfaz ISampleGrabber. Seguido, deberemos incluir este filtro en el gráfico principal, tarea que lleva a cabo el método AddFilter. También deberemos añadir el filtro que contiene al vı́deo origen; para ello, llamaremos al método RenderFile, al que simplemente deberemos indicarle el nombre del archivo de vı́deo que queremos abrir, pero no mediante la forma a la que estamos acostumbrados, sino mediante caracteres anchos (del tipo WCHAR), por lo que previamente deberemos convertir la cadena 3.1. Vı́deo 29 de caracteres (normalmente de tipo char* ) que contiene el nombre del vı́deo a este formato (mediante la función MultiByteToWideChar ). Ahora deberemos inicializar tres interfaces que nos serán necesarias en los siguientes pasos. La primera de ellas es la interfaz IMediaControl, que nos servirá para reproducir el gráfico cuando sea necesario. La segunda es la interfaz IMediaEvent, que nos servirá para indicarle al gráfico ciertos estados que debe cumplir. Y por último la interfaz IMediaSeek, la cuál nos servirá para colocarnos en el fotograma que deseemos. Ahora empezaremos a llamar a ciertos métodos de las interfaces anteriores. Primero llamaremos al método SetBufferSamples, el cual especifica que los datos que copie el filtro se guarden en un buffer y no se pierdan una vez hayan pasado. A continuación, mediante el método SetOneShot, indicamos que el gráfico reproduzca un fotograma y pare, para que nos de tiempo a procesar esos datos. Podremos obtener la duración del vı́deo mediante el método GetDuration, lo que nos será útil para comprobar si el fotograma que hemos indicado está dentro del intervalo de fotogramas que contiene el vı́deo. Una vez comprobado esto, pasaremos a posicionar el vı́deo en el fotograma indicado, mediante el método SetPositions, al cual deberemos indicarle la posición inicial y final a reproducir.1 Ya para finalizar ordenaremos al gráfico que se reproduzca, mediante el método Run. Ahora ya tenemos el fotograma en un buffer; lo único que debemos hacer ahora es recoger la información para procesarla después. Para ello, averiguamos primero las dimensiones del fotograma, mediante el método GetConnectedMediaType de la interfaz ISampleGrabber, la cuál nos devolverá una estructura AM MEDIA TYPE (que hemos mostrado con detalle en el punto anterior). Para finalizar, simplemente deberemos guardar memoria para el fotograma, y llamar al método GetCurrentBuffer de la interfaz ISampleGrabber, el cuál nos devolverá, en la dirección de memoria que habı́amos guardado previamente, el fotograma que le habı́amos indicado. No debemos olvidar el finalizar la librerı́a COM, mediante la función CoUninitialize, que además descargará las DLLs que se hayan cargado anteriormente, liberará toda la memoria que se haya almacenado durante la aplicación,... 1 Recalcar que como sólo queremos extraer un fotograma, la posición inicial y la final serán la misma. 30 CAPı́TULO 3. MANEJO DE FUNCIONES MULTIMEDIA MPEG2Lib Existen ocasiones en las que la calidad del MPEG-1 no nos es suficiente para nuestros fines, y debemos utilizar el MPEG-2 con todas las ventajas que ello conlleva. Por ello, también deberemos añadir facilidades para la extracción de fotogramas para este tipo de formato. Todas las interfaces que posee el DirectX no nos sirven para la extracción de fotogramas en el MPEG-2, ya que como máximo lo que nos hacen es extraer el primer fotograma, pero sin posicionarse en el fotograma que deseamos. Por esto mismo, se debió buscar otra opción en el caso de trabajar con este formato de vı́deo. La solución la encontramos en una librerı́a desarrollada por un programador llamado Michael Vinther, el cual integra en su librerı́a una combinación de un programa de libre distribución llamado DVD2AVI y las funciones desarrolladas por el grupo de investigación del MPEG, en concreto el MSSG (MPEG Software Simulation Group). Esta librerı́a nos ofrece opciones para abrir y cerrar vı́deos, ası́ como para abrir directamente un disco (DVD, ...). Además, encontramos opciones para posicionarse en cualquier fotograma del vı́deo, extraer un fotograma en concreto o elegir el formato de salida de dicho fotograma. En concreto, los formatos de salida que soporta la librerı́a son tres: RGB, YUV o escala de grises. En la mayorı́a de casos, trabajaremos con el formato RGB, que es el que realmente nos interesa, aunque es el que más tiempo de decodificación requiere, ya que se debe hacer una transformación lineal de los colores guardados en el fichero MPEG-2. Lo que se ha intentado con esta librerı́a es crear otra mucho más simple, con funciones tales como ExtraerFotograma, o ExtraerGrupoFotogramas. La primera función nos extrae un fotograma como una imagen de VICTOR, simplemente pasándole el nombre del vı́deo y el número de fotograma que queremos sacar. La segunda nos guarda los fotogramas en disco, pasándole como parámetros el nombre del vı́deo, el nombre con que queremos que guarde los fotogramas, y el intervalo que deseamos extraer. Esta librerı́a también nos ofrece opciones para el tratamiento del audio, pero son bastante más complejas que las que se han desarrollado anteriormente y requieren más tiempo computacional, por lo que para el audio trabajaremos siempre con las funciones que describiremos en el punto siguiente, y que se basan en el DirectShow. 31 3.1. Vı́deo A continuación pasaremos a explicar con detalle todas las funciones con las que cuenta esta librerı́a, que básicamente son diez, englobadas en cuatro grandes grupos: apertura y cierre de archivos, información, posicionamiento, y extracción de fotogramas. Apertura y cierre de archivos En el bloque de apertura y cierre de archivos, nos encontramos con tres funciones: OpenMPEG2File OpenMPEG2Disk CloseMPEG2File La función OpenMPEG2File se utiliza para abrir un archivo de vı́deo con formato MPEG2. Si el vı́deo no tiene este formato, esta función fallará. Por esto mismo, podremos usar esta función para diferenciar entre MPEG1 y MPEG2, ya que a simple vista no existe nada que nos indique qué tipo de formato tiene el archivo(ambos tienen la misma extensión (*.mpg)). A esta función simplemente deberemos pasarle tres parámetros: el primero de ellos será el nombre del vı́deo que deseamos abrir, el segundo es el offset, que será siempre 0, y el tercero el tamaño del archivo, que como normalmente será desconocido, deberemos pasarle el valor de -1. El valor de retorno será 0 si la función falla, y otro valor si la función ha conseguido abrir el archivo. La función OpenMPEG2Disk es básicamente igual que la anterior, con el único cambio de que no nos abre un archivo de vı́deo determinado, sino que abrirá un disco (por ejemplo un DVD). Por ello, no deberemos pasarle un nombre de archivo de vı́deo, sino el identificador del disco que queremos abrir. El valor de retorno será el mismo que en el caso anterior. Por último, la función CloseMPEG2File se utilizará para cerrar el archivo que previamente habı́amos abierto con una de las funciones anteriores. Esta función no tiene parámetros, ni nos devolverá ningún valor, ya que internamente ya se encarga de cerrar el archivo que tenı́a abierto , y no existe la posibilidad de que falle. 32 CAPı́TULO 3. MANEJO DE FUNCIONES MULTIMEDIA Información Dentro de este bloque nos encontramos con dos funciones: GetMPEG2FrameInfo GetMPEG2FileInfo La primera de ellas, GetMPEG2FrameInfo, nos ofrece información acerca del fotograma, es decir, nos proporciona el alto y el ancho del fotograma. Esta información es muy importante, ya que cuando vayamos a guardar espacio en memoria para el fotograma, deberemos saber estas cantidades que serán las que nos indiquen el tamaño de bloque que debemos guardar. La función GetMPEG2FileInfo nos da información del archivo de vı́deo, como su tamaño, el fotograma en que estamos en el momento,... De la información que nos proporciona esta función, la que más usaremos será la que nos indica en qué fotograma estamos, ya que para saltar a otro fotograma deberemos conocer en cual de ellos estamos situados exactamente, para indicar después a la función de posicionamiento el parámetro adecuado. Posicionamiento En este bloque nos encontramos con dos funciones: SkipMPEG2Frames MPEG2Seek La función SkipMPEG2Frames, como su propio nombre indica, nos sirve para saltar el número de fotogramas que le indiquemos. Esta función es muy práctica, pero cuando deseamos realizar grandes saltos dentro del vı́deo, resulta bastante ineficiente, ya que lo que hace es ir buscando las cabeceras de los fotogramas para comprobar si es el que realmente deseamos, por lo que se hace especialmente ineficiente cuando trabajamos con vı́deos grandes, que tengan muchos fotogramas. Por lo tanto, esta función deberá usarse cuando trabajemos con vı́deos pequeños y en los que tengamos que desplazarnos pocos fotogramas. La otra función, MPEG2Seek, lo que hace es moverse en el fichero de vı́deo una cierta cantidad de bytes, la que nosotros le indiquemos como parámetro. Después deberemos encargarnos de comprobar si estamos en el fotograma que realmente 3.1. Vı́deo 33 deseábamos o no, haciendo uso de una de las estructuras que proporciona la librerı́a. Por esto, esta función es mucho más eficiente que la anterior cuando deseamos grandes saltos dentro del vı́deo. Extracción En esta sección nos encontramos con tres funciones, aunque no todas ellas realizan la extracción de un fotograma, pero están relacionadas con dicha tarea. Las funciones son las siguientes: SetMPEG2PixelFormat SetRGBScaleFlag GetMPEG2Frame La función SetMPEG2PixelFormat lleva a cabo la función de elegir el tipo de salida que queremos, es decir, si queremos que el fotograma de salida esté en formato RGB, YUV o en escala de grises. Como hemos explicado anteriormente, el formato que más usaremos será el RGB, por ello si no especificamos nada, el formato por defecto será este. La función SetRGBScaleFlag se utiliza para que en el caso que trabajemos con el formato de salida RGB, poder elegir la escala del fotograma de salida, es decir, que podremos ampliarlo o reducirlo a nuestro gusto. Por último, la función GetMPEG2Frame es la que extrae el fotograma realmente. Esta función será la última que llamemos, ya que previamente tenemos que haberle especificado el formato que queremos, el fotograma que realmente queremos extraer,... Como salida, esta función nos devolverá un buffer que contendrá el fotograma deseado. AVIFile Básicamente esta es una librerı́a de Windows, llamada AVIFile Functions and Macros, que encontramos dentro del archivo vfw32.dll y que podremos utilizar simplemente con añadir el archivo vfw.h dentro de los includes de nuestro programa. Antes de continuar, deberemos reseñar que el AVI no es ningún formato de compresión en concreto, sino que utiliza una serie de compresores ya desarrollados, tales como el DivX, para formar los archivos comprimidos. Por lo tanto, 34 CAPı́TULO 3. MANEJO DE FUNCIONES MULTIMEDIA cuando hagamos uso de esta librerı́a, deberemos asegurarnos antes de que en el ordenador se encuentra instalado el compresor correspondiente al vı́deo con el cual queremos trabajar. Las funciones que encontramos en esta librerı́a tratan la información de los ficheros multimedia como uno o más streams de datos. Un fichero AVI puede contener tipos de datos diferentes, tales como una secuencia de vı́deo, una pista de audio en inglés, una pista de audio en español,. . . Usando las funciones de esta librerı́a, una aplicación puede acceder a cada uno de estos componentes separadamente y trabajar con cada uno de ellos independientemente de lo que pase con el otro; es decir, que podremos trabajar con el stream de vı́deo sin importarnos cuantos streams de audio existan, ni si el vı́deo contiene un stream de datos,... Ya que todas estas funciones están contenidas dentro de una dll (Dynamiclink library), cuando vayamos a utilizar cualquiera de ellas se deberá ejecutar una función para que las reconozca: AVIFileInit. Después de inicializar la librerı́a, se podrán usar cualquiera de las funciones para el tratamiento de ficheros AVI. Por supuesto, una vez finalizado el tratamiento del vı́deo y no se vayan a usar más estas funciones, se deberá ejecutar otra función para liberar la librerı́a: AVIFileExit. La librerı́a AVIFile mantiene un cuenta de referencia de las aplicaciones que están usando la librerı́a, pero no sobre las veces que se ha liberado. De tal forma, nuestras aplicaciones deberán llevar cuidado de que cada vez que se inicialice la librerı́a, se cierre adecuadamente. Nos encontraremos con dos tipos de funciones bien diferenciados: Funciones para trabajar con ficheros: Nos encontraremos funciones para abrir y cerrar ficheros, ası́ como funciones para obtener información de dicho fichero, o para escribir datos dentro del fichero. Por ejemplo, las funciones para abrir y cerrar ficheros serán AVIFileOpen y AVIFileRelease, respectivamente. Funciones para trabajar con streams: Nos encontraremos funciones para abrir y cerrar streams, funciones para extraer datos del stream (por ejemplo para extraer un fotograma), funciones para extraer información de un stream (por ejemplo para obtener el ancho y alto de los fotogramas). Por ejemplo, para extraer un fotograma, necesitaremos tres funciones básicas: AVIFileStreamGetOpen, AVIFileStreamGetFrame y AVIFileStreamGetFrameClose, que se encargan de preparar el stream para la extracción, de extraer y guardar el frame, y de cerrar el stream, respectivamente. En la figura 3.3 podemos ver un ejemplo de cómo abrir un vı́deo AVI y obtener 35 3.1. Vı́deo después el número de streams que contiene. En dicha figura vemos algunas de las funciones que encontramos dentro de la librerı́a AVIFile. Figura 3.3: Código de ejemplo de la librerı́a AVIFile. Lo primero que tenemos que hacer es obtener un puntero al fichero multimedia abierto. Para ello, deberemos usar la función AVIFileOpen, a la que pasaremos como parámetros: La dirección del puntero que apuntará al fichero abierto. El nombre del vı́deo a ser abierto, con el path completo donde se encuentra ubicado. A continuación indicaremos el modo como queramos sea tratado el vı́deo, 36 CAPı́TULO 3. MANEJO DE FUNCIONES MULTIMEDIA es decir, si queremos abrirlo como sólo lectura, como es ahora el caso, en modo escritura,. . . La última variable la pondremos siempre al valor de 0L. A continuación, iremos recorriendo el fichero para comprobar cuantos son los streams que contiene. Para ello, usamos el bucle for, indicándole un lı́mite (MAXNUMSTREAMS), el cual consideremos adecuado. Normalmente, no suelen existir más de tres streams dentro del vı́deo, pero previniendo podremos fijar esta variable a diez, por ejemplo. Como vemos, para obtener un puntero al stream usamos la función AVIFileGetStream, a la cual deberemos pasar ciertos parámetros: El puntero que identifica al fichero abierto en la función anterior. Un puntero que identificará al stream abierto en ese momento, ya sea el de vı́deo, el de audio, . . . Indica el tipo de stream a abrir. Ponemos cero para poder abrir cualquier stream, que es lo que queremos. Contador del stream abierto. Nos detendremos cuando lleguemos al máximo número de streams fijado, o cuando no haya más streams, que será lo más normal. Para saber que no existen más streams, deberemos comprobar que el puntero que le hemos pasado a la función anterior para que identifique al stream es igual a NULL, que nos indicará que está vacı́o. A continuación comprobaremos que se han podido abrir los streams, ya sea porque no existı́a ninguno dentro del fichero o porque los datos estén dañados, de tal forma que no se puedan extraer dichos datos. El número de streams a devolver no será la cuenta que llevábamos, sino uno menos, ya que el programa también cuenta la última comprobación en la que vemos que está vacı́o (cuando comprobamos que es igual a NULL). Antes de finalizar el programa deberemos cerrar el fichero, usando la función AVIFileRelease, a la cual deberemos pasarle el puntero que habı́amos obtenido en la primera función que hemos comentado. También deberá abandonarse la 37 3.1. Vı́deo librerı́a, ya que como se ha comentado en capı́tulos anteriores, deberemos llevar cuidado de cerrar siempre las librerı́as después de ser abiertas. De esta forma, como habı́amos abierto la librerı́a al principio de la función para poder hacer uso de las funciones proporcionadas por dicha librerı́a, ahora deberemos cerrarla. Para finalizar se devuelve el número de streams que hemos encontrado dentro del fichero multimedia que habı́amos pasado a la función como parámetro. 3.1.4. Ejemplo Básico del DirectX Dado que hasta ahora se ha hecho una explicación muy teórica acerca del DirectX y de todos sus componentes, nos dedicaremos en este punto a poner un ejemplo práctico, sencillo y fácil de entender sobre cómo se podrı́a extraer una fotograma utilizando la interfaz IMediaDet. Ası́, se ha creado la función ExtraeFotograma, tay y como se muestra en la figura 3.4, a la cual simplemente deberemos pasarle como parámetros el nombre del vı́deo del que queremos extraer el fotograma, y una estructura BITMAPINFOHEADER, tı́pica de Windows, y que recogerá los datos del fotograma. Detallaremos ahora todas las funciones de las que se ha hecho uso en este ejemplo. Como vemos en la figura 3.4, se ha llamado en primer lugar al método CoInitialize, para inicializar el COM, y en último lugar se ha llamado al CoUninitialize, para finalizarlo. Como hemos dicho con anterioridad, estos son dos puntos fundamentales cuando trabajamos con el DirectX y el COM. A continuación, inicializamos la interfaz IMediaDet, para poder después usar los métodos que contiene. Ası́, después de convertir el nombre que se pasa como parámetro, se llama al método put Filename, con el que indicamos el origen de los datos. Una vez tenemos el vı́deo, vamos recorriendo sus streams hasta que encontremos el de vı́deo, utilizando el método get OutputStreams para saber el número total de streams, el método put CurrentStream para ir cambiando el stream, y el método get StreamType para obtener el tipo de stream del que se trata, que compararemos después con el tipo MEDIATYPE Video hasta que encontremos el stream de vı́deo. Con todo esto lo que hemos conseguido es tener activo el stream de vı́deo, si es que existe dentro del archivo pasado como parámetro. Ası́ que ahora lo que nos queda es averiguar el tamaño del buffer que debemos guardar en memoria para almacenar al fotograma, y se llevará a cabo mediante el método GetBitmapBits, al cual si le pasamos como tercer parámetro NULL, nos devolverá en el segundo 38 CAPı́TULO 3. MANEJO DE FUNCIONES MULTIMEDIA Figura 3.4: Código de ejemplo para extraer un fotograma. parámetro el tamaño que buscábamos. Con todo esto, simplemente nos queda guardar espacio en memoria para el fotograma, y volver a llamar al método GetBitmapBits, ahora indicándole como tercer parámetro el puntero a la dirección de memoria reservada. Ası́, después de hacer las comprobaciones pertinentes, lo único que quedará será el insertar en el 39 3.2. Audio parámetro de entrada el fotograma que hemos obtenido, y pasar como valor de retorno un resultado positivo, si es que todo ha funcionado bien. 3.2. Audio El audio también es una parte importante en la realización de nuestro proyecto. Será necesario el poder independizar el audio de nuestro fichero de vı́deo, para poder ası́ hacer el procesado correspondiente. Por ello, se deberán crear funciones para poder obtener el audio y poder guardarlo en un fichero independiente. Para ello, se usan diversas interfaces del DirectShow, tales como el IAudioMediaStream, IAudioStreamSample, IMemoryData y el IAudioData. Todas ellas las encontramos en un apéndice del DirectShow, llamado Multimedia Streaming, en el que se ofrecen facilidades para la reproducción tanto de secuencias de vı́deo como de audio. Con unos pequeños cambios, se ha conseguido adaptar estas interfaces a nuestra finalidad, que era la de poder independizar el stream de audio del stream de vı́deo, guardando dicho audio en un fichero aparte. Empezaremos hablando acerca de la arquitectura del Multimedia Streaming, ası́ como de sus ventajas y de su relación ı́ntima con el COM, para pasar a continuación en el punto 3.2.2 a ver cómo se utiliza esta tecnologı́a en la extracción del audio de los ficheros multimedia. En el punto 3.2.3 veremos con detalle las funciones creadas para el tratamiento del audio, una vez separado del fichero multimedia y listo para su uso; y por últimos, en el punto 3.2.4 veremos un práctico ejemplo de cómo extraer el audio de un fichero multimedia, haciendo uso de las funciones creadas. 3.2.1. Multimedia Streaming Cuando los programadores usan el multimedia streaming en sus aplicaciones, este reduce considerablemente la cantidad de programación especı́fica necesaria. Tı́picamente, una aplicación que debe obtener datos multimedia desde un fichero que debe conocer perfectamente el formato de los datos1 . La aplicación debe manejar la conexión, trasferir los datos, convertir los datos que sea necesario 1 Al igual que hablamos de datos guardados en un fichero, se sobreentiende que se podrá trabajar igualmente con fuentes hardware, como por ejemplo un lector de CD 40 CAPı́TULO 3. MANEJO DE FUNCIONES MULTIMEDIA convertir, y representar dichos datos o almacenarlos en disco, conforme el caso. A causa de que cada formato y cada dispositivo es diferente a cualquier otro, este proceso es frecuentemente complejo y pesado. El multimedia streaming, por otra parte, negocia automáticamente la transferencia y conversión de datos desde la fuente de la aplicación. Las interfaces de streaming proveen un método uniforme y previsible de acceso y control de los datos, que lo hace fácil para una aplicación que deba reproducir los datos, a pesar de su fuente original o de su formato. Jerarquı́a de objetos El diagrama de la figura 3.5 muestra la jerarquı́a básica de objetos usada en Multimedia Streaming. Figura 3.5: Jerarquı́a de objetos en Multimedia Streaming. Hay tres tipos de objeto básicos definidos en la arquitectura de Multimedia Streaming: 1. Un stream multimedia, que soporta la interfaz IMultiMediaStream. 2. Streams de datos, que soportan la interfaz IMediaStream y contienen datos especı́ficos (vı́deo, audio,...). Cada stream multimedia contiene uno o más de estos streams de datos. 3. Streams de muestras, que soportan la interfaz IStreamSample y son creados por un stream de datos. Estos objetos representan una unidad básica de 3.2. Audio 41 trabajo para el stream, y que serán en definitiva las muestras, es decir, que si estamos con un stream de vı́deo, serán fotogramas. Los objetos que soportan la interfaz IMultiMediaStream son los contenedores básicos de los streams de datos multimedia. La interfaz IMultiMediaStream incluye métodos que enumeran los objetos del stream de datos; esos streams son tı́picamente datos de vı́deo y audio, pero pueden incluir datos de cualquier formato, como por ejemplo texto (para los subtı́tulos), o referencias de tiempo SMPTE. La interfaz IMultiMediaStream es un contenedor genérico, de forma que los programadores pueden crear otras versiones de la interfaz que soporten formatos de datos especı́ficos. Los objetos que implemente la interfaz IAMMultiMediaStream, por ejemplo, pueden enumerar y controlar streams de cualquier formato de datos del DirectShow. A causa de que esos streams de datos individualmente tienen un formato especı́fico, soportan al menos dos interfaces diferentes: una genérica y otra especı́fica dependiente de los datos que contiene. Cada stream soporta la interfaz IMediaStream, que proporciona métodos para obtener su formato y un puntero al mismo stream. Cualquier interfaz derivada de la IMultiMediaStraem también soporta la creación de streams de muestras, las unidades básicas de los datos multimedia. Una muestra multimedia es una referencia a un objeto que contiene los datos. Por ejemplo, para un vı́deo, esta muestra será un fotograma. El contenido exacto de la muestra varia, dependiendo del tipo de medio (audio, texto,...). A causa de que una muestra es sólo una referencia al objeto de datos, varias muestras pueden referirse al mismo objeto. La interfaz IStreamSample proporciona métodos que obtienen y modifican caracterı́sticas de la muestra, tal como las posiciones de comienzo y fin, el estado, y el stream asociado. Uso del multimedia streaming Las interfaces multimedia streaming simplifican considerablemente el proceso de manipulación de los datos multimedia, ya que eliminan la dependencia que se produce normalmente con las fuentes hardware o software; además, proporcionan soporte para todos los formatos multimedia de Microsoft DirectX. Lo que se hace es abstraer los datos a un nivel alto, de forma que nosotros sólo hablamos de streams; las aplicaciones pueden incluso mover datos desde un stream a otro sin conocer absolutamente nada sobre el formato de los datos que contiene dicho stream. Para crear un stream multimedia, simplemente deberı́amos seguir los siguientes pasos: 42 CAPı́TULO 3. MANEJO DE FUNCIONES MULTIMEDIA 1. En primer lugar, deberemos crear el objeto stream multimedia. El método para crear e inicializar el stream es especı́fico de la arquitectura que usemos. El DirectShow soporta la interfaz IAMMultiMediaStream, que se usa para inicializar el stream, aunque también existen otras formas de crear el stream, por ejemplo usando el IMultiMediaStream. 2. Después de haber inicializado el objeto stream multimedia, la aplicación usará la función QueryInterface para obtener la interfaz IMultiMediaStream necesaria para el objeto. Deberá usarse esta interfaz para determinar las propiedades del stream y enumerar los streams por sı́ mismos. Por ejemplo, podremos obtener un stream especı́fico llamando al método IMultiMediaStream::GetMediaStream con un especı́fico ID. Los ID usados más comúnmente son el MSPID PrimaryVideo y el MSPID PrimaryAudio. 3. Ahora deberemos llamar al método IUnknown::QueryInterface para obtener una interfaz especı́fica del tipo de stream multimedia; es decir, que si queremos reproducir un stream de vı́deo, con esto obtendremos la interfaz IDirectDrawMediaStream, que se encargará de reproducir dicho vı́deo en una superficie DirectDraw. Las interfaces especı́ficas de cada medio definen métodos adicionales necesarios para obtener todas las ventajas que nos ofrece el formato con el que trabajemos. 4. Crearemos ahora una o más muestras del stream de datos. Cada stream multimedia soporta el método IMediaStream::CreateSharedSample para la creación de muestras. La muestra resultante soporta la interfaz IStreamSample, que proporciona control total sobre la muestra y sus caracterı́sticas. Tı́picamente, las muestras extraı́das soportan un método especı́fico (dependiendo del formato) que es más potente que los métodos de la interfaz IStreamSample. Por ejemplo, la interfaz IDirectDrawMediaStream puede crear muestras que ataquen directamente a una superficie DirectDraw. En otras situaciones, sin embargo, nos interesará trabajar con las muestras sin conocer nada acerca de su formato; para ello, tendrı́amos que usar el método IMediaStream::CreateSharedSample. 5. Después de crear todas las muestras deseadas, empezarı́amos a reproducir el stream, llamando al método IMultiMediaStream::SetState y pasándole como parámetro el correspondiente al comienzo de la reproducción. 6. Por último, deberemos actualizar la muestra en la que nos encontremos, mediante el método IStreamSample::Update. 43 3.2. Audio 3.2.2. Extracción de audio Con todo lo dicho hasta ahora, podemos ver que tenemos perfectamente separada la información de vı́deo de la información de audio, por lo que podemos extraerlos por separado; es decir, que si queremos podremos extraer un fotograma, y si lo que nos interesa es el audio podremos extraerlo y guardarlo en un formato de sonido (como por ejemplo el wav, mp3, ...). Esto es lo que se ha hecho en el proyecto, el poder extraer el audio de un fichero multimedia y guardarlo como un archivo de sonido, en concreto en WAV. Para ello, se ha recurrido al multimedia streaming, para poder extraer el stream de audio, y a una clase llamada CWaveFile, que se encargará de guardar dicho stream de audio en un fichero WAV. No nos adentraremos en profundidad en la explicación de esta clase por no hacer demasiado pesada la lectura de esta memoria, además de no considerar relevante dicha explicación en estos momentos. Lo que se hace básicamente es abrir el fichero de vı́deo, buscar el stream de audio, y cuando se ha encontrado, abrirlo y reproducirlo, mientras que la clase CWaveFile ya se encarga de ir guardando dichos datos en el archivo de sonido. Finalmente deberemos tener cuidado de cerrar todo los que habı́amos abierto: el stream de audio, el fichero multimedia y el fichero de audio. Para hacer más fácil la extracción del audio de los ficheros de vı́deo, lo que se ha hecho es crear una función que directamente se encargue de todo. Dicha función se ha llamado extraeAudio, y que podremos ver en el punto 3.3.2 3.2.3. Funciones para el tratamiento del audio Como ya hemos comentado anteriormente, estas funciones se escribieron en un principio en el lenguaje de programación M, perteneciente a MATLAB, ya que es mucho más sencillo y práctico el tratamiento del audio en el lenguaje M que en el lenguaje C++. Por ello, se ha tenido que hacer una adaptación de dichas funciones al entorno en el que estamos trabajando continuamente, que es el entorno de C++. Para ello, el programa MATLAB ofrece un compilador el cual nos realiza la transformación pertinente, obteniendo como resultado librerı́as estáticas, dinámicas, e incluso el código fuente C++. Para ver con detalle el funcionamiento de dicho compilador, véase apéndice A. Normalmente a estas funciones se le pasarán datos como strings, es decir, 44 CAPı́TULO 3. MANEJO DE FUNCIONES MULTIMEDIA cadenas de caracteres, ya que el programa MATLAB trata todos los parámetros como texto, y por lo tanto los datos que le pasemos a la función deberán ser texto también. Además, estas funciones no retornan ningún valor para comprobar que se ha completado con éxito o ha ocurrido algún tipo de error. Por ello, deberemos comprobar con antelación, antes de llamar a estas funciones, que los parámetros que le pasemos sean ciertamente los que tenemos que pasar (por ejemplo, si el nombre que le pasamos es el de un vı́deo, comprobar con antelación que dicho vı́deo existe realmente). Podremos ver una completa referencia del uso de estas funciones en el punto 3.3.2. 3.2.4. Ejemplo: Reproducción de Audio con DirectX En este punto veremos un ejemplo básico y sencillo acerca de cómo reproducir un archivo de audio, utilizando el DirectX Audio (en concreto el DirectMusic). Veremos paso a paso los puntos básicos en la reproducción, que serán el inicializar una interfaz DirectMusic y después reproducir el archivo WAV. En la figura 3.6 podemos ver el código que necesario para llevar a cabo esta función. En este código, se reproduce un archivo llamado audio.wav situado en el directorio raı́z. Veremos ahora paso a paso todas las funciones necesarias. En primer lugar, y como siempre, deberemos inicializar el COM, mediante el CoInitialize. Para finalizar, no deberemos olvidar el utilizar la función CoUnitialize, para liberar todas las interfaces que hemos estado utilizando. A continuación, inicializaremos las dos interfaces que necesitamos: la interfaz IDirectMusicLoader y la interfaz IDirectMusicInterface. Para ello, y como se explicó, se utiliza la función CoCreateInstance. Una vez inicializadas las interfaces, el próximo paso será inicializar el sintetizador y los dispositivos que se utilizarán para reproducir el sonido. Deberemos usar para ello el método InitAudio, con los parámetros pertinentes y que podemos ver en el código de ejemplo. Ahora simplemente deberemos cargar el archivo de audio, mediante el método LoadObjectFromFile, al que deberemos pasarle como parámetro el nombre del archivo que queremos abrir, pero también con caracteres anchos (WCHAR), tal y como ocurrı́a en el punto 3.1.3. 45 3.2. Audio Figura 3.6: Código de ejemplo del DirectX Audio. Con todo esto ya tenemos el sonido almacenado en un buffer, dispuesto a ser reproducido. Pero antes de reproducirlo, deberemos traspasar los datos al sintetizador, que será el que finalmente se encargue de que se escuche el sonido. Para ello deberemos utilizar el método Download, y a continuación el método PlaySegmentEx, que será el que realmente reproduzca el sonido. Ya para finalizar, deberemos detener la reproducción del sonido, mediante el método Stop; cerrar el fichero que habı́amos abierto con anterioridad, mediante el método CloseDown; y cerrar todas las interfaces que habı́amos abierto, mediante el método Release correspondiente a cada una de las interfaces abiertas. 46 CAPı́TULO 3. MANEJO DE FUNCIONES MULTIMEDIA 3.3. Librerı́a creada Uno de los objetivos del proyecto era el de crear una librerı́a donde se incluyeran funciones para el tratamiento de los ficheros multimedia. Por lo tanto, es de especial interés ofrecer una referencia de todas y cada una de las funciones que estarán disponibles en la librerı́a. Hemos dividido las funciones, al igual que en otros capı́tulos, en los dos grandes bloques del proyecto: audio y vı́deo. La estructura utilizada para la explicación de las funciones es la siguiente: Sinopsis: resumen de la función. Formato: sintaxis a utilizar en la llamada a la función y breve explicación de cada uno de los parámetros que intervienen. include: Fichero a incluir para poder usar la función. Descripción: implementación de la función. 3.3.1. Vı́deo En esta sección nos encontraremos con funciones para extraer fotogramas, para sacar las caras de un fotograma en concreto, para posicionarse dentro del vı́deo,... Veremos a continuación todas las funciones que se creado, de forma que han sido separadas según la función que realizan, viendo primero todas las funciones que se encargan de la extracción de fotogramas, tratamiento de los distintos formatos y por último las funciones necesarias para el tratamiento de los vı́deos VOB. getVideoInfo Sinopsis: Función que nos proporciona información de un vı́deo. Formato: int getVideoInfo (char *fileName, int *width, int *height, double *fps); • char *fileName: El nombre del vı́deo origen. 3.3. Librerı́a creada 47 • int *width: Puntero a una variable donde se guardará el ancho de los fotogramas. • int *height: Puntero a una variable donde se guardará el alto de los fotogramas. • double *fps: Puntero a una variable en la que se guardará el número de fotogramas por segundo. include: video.h Descripción: La función nos proporcionará información acerca de los parámetros principales de un vı́deo, tales como la anchura y altura de sus fotogramas, y el número de fotogramas por segundo. extractMediaDet Sinopsis: Función que extrae y guarda un fotograma del vı́deo indicado (en el caso de que sea MPEG1 o AVI); además extrae la cara dadas las coordenadas y la guarda con el nombre indicado. Formato: int extractMediaDet (char *fileName, char *output, char *face, double second, int xc1, int yc1, int xc2, int yc2); • char *fileName: El nombre del vı́deo origen. • char *output: El nombre de la imagen donde se guardará el frame. • char *face: El nombre de la imagen donde se guardará la cara. • double second: El segundo exacto del fotograma del que queremos extraer. • int xc1: La coordenada x del primer ojo. • int yc1: La coordenada y del primer ojo. • int xc2: La coordenada x del segundo ojo. • int yc2: La coordenada y del segundo ojo. include: video.h Descripción: La función extraerá el fotograma indicado en second del vı́deo fileName, y lo guardará en el archivo output. Para ello, utilizará la interfaz IMediaDet vista en el punto 3.1.3. En el archivo face guardará la selección del fotograma indicada por las coordenadas xc1, yc1, xc2, yc2. El valor de retorno será un entero, y será igual a cero si la función falla, o cualquier otro valor si la función cumple su objetivo. 48 CAPı́TULO 3. MANEJO DE FUNCIONES MULTIMEDIA NOTA: Recordar que esta interfaz nos será válida cuando estemos en la plataforma w98, y para los vı́deos MPEG-1 o AVI. extractSampGrab Sinopsis: Función que extrae y guarda un fotograma del vı́deo indicado (siempre que sea MPEG-1 o AVI); además, extrae la cara dadas las coordenadas y la guarda con el nombre indicado. Formato: HRESULT extractSampGrab (char *fileName, double second, int xc1, int yc1, int xc2, int yc2, char *output, char *face); • char *fileName: El nombre del vı́deo origen. • double second: El segundo exacto del fotograma del que queremos extraer. • int xc1: La coordenada x del primer ojo. • int yc1: La coordenada y del primer ojo. • int xc2: La coordenada x del segundo ojo. • int yc2: La coordenada y del segundo ojo. • char *output: El nombre de la imagen donde se guardará el frame. • char *face: El nombre de la imagen donde se guardará la cara. include: video.h Descripción: La función extraerá el fotograma indicado en second del vı́deo fileName, y lo guardará en el archivo output. Para ello, utilizará la interfaz ISampleGrabber vista en el punto 3.1.3. En el archivo face guardará la selección del fotograma indicada por las coordenadas xc1, yc1, xc2, yc2. El valor de retorno será el tipo HRESULT, y será igual a S OK si la función lleva a cabo su tarea, y el código de error resultante si la función falla. NOTA: Cabe diferenciar esta función y la anterior, ya que básicamente hacen lo mismo, pero cada una debe de ser ejecutada en el entorno correcto, en el caso contrario no funcionará. Esta función funcionará correctamente en todos los entornos, siempre que el vı́deo sea MPEG-1, pero consume muchos más recursos y será preferible utilizar la función anterior en los casos en que trabajemos en w98 y dejar esta función para cuando trabajemos en wXP (ver punto 3.1.3). 3.3. Librerı́a creada 49 extractAvi Sinopsis: Función que extrae y guarda un fotograma del vı́deo indicado, siempre que sea un vı́deo MPEG2. Además, extrae la cara dadas las coordenadas. Formato: int extractAvi(char *fileName, char *output, char *face, int frame, int xc1, int yc1, int xc2, int yc2); • char *fileName: El nombre del vı́deo origen. • char *output: El nombre de la imagen donde se guardará el frame. • char *face: El nombre de la imagen donde se guardará la cara. • int frame: El número de fotograma a extraer. • int xc1: La coordenada x del primer ojo. • int yc1: La coordenada y del primer ojo. • int xc2: La coordenada x del segundo ojo. • int yc2: La coordenada y del segundo ojo. include: video.h Descripción: La función extraerá el fotograma indicado en frame del vı́deo fileName, y lo guardará en el archivo output. Para ello, utilizará la librerı́a AVIFile vista en el punto 3.1.3. En el archivo face se guardará la selección del fotograma indicada por las coordenadas xc1, yc1, xc2, yc2. El valor de retorno será un entero, y será igual a cero si la función falla, o cualquier otro valor si la función cumple su objetivo. Esta función se utilizará para extraer un único fotograma. extractMPEG2 Sinopsis: Función que extrae y guarda un fotograma del vı́deo indicado, siempre que sea un vı́deo MPEG2. Además, extrae la cara dadas las coordenadas. Formato: long extractMPEG2 (char *fileName, long frame, long xc1, long yc1, long xc2, long yc2, double duration, char *output, char *face); • char *fileName: El nombre del vı́deo origen. • long frame: El número de fotograma a extraer. • int xc1: La coordenada x del primer ojo. 50 CAPı́TULO 3. MANEJO DE FUNCIONES MULTIMEDIA • int yc1: La coordenada y del primer ojo. • int xc2: La coordenada x del segundo ojo. • int yc2: La coordenada y del segundo ojo. • double duration: La duración total del vı́deo, en segundos. • char *output: El nombre de la imagen donde se guardará el frame. • char *face: El nombre de la imagen donde se guardará la cara. include: video.h Descripción: La función extraerá el fotograma indicado en frame del vı́deo fileName, y lo guardará en el archivo output. Para ello, utilizará la librerı́a Mpeg2Lib vista en el punto 3.1.3. En el archivo face guardará la selección del fotograma indicada por las coordenadas xc1, yc1, xc2, yc2. El valor de retorno será un entero, y será igual a cero si la función falla, o cualquier otro valor si la función cumple su objetivo. Esta función se utilizará para extraer un único fotograma. Si lo que queremos es extraer un intervalo de fotogramas utilizaremos la función SacaFrame, que veremos más adelante. isVideoMPEG2 Sinopsis: Función muy práctica la cual nos dirá si el vı́deo que le indicamos es MPEG2 o no. Formato: bool isVideoMPEG2 (char *video); • char *video: El nombre del vı́deo del cuál queremos obtener la información. include: video.h Descripción: La función comprobará el vı́deo de entrada, y devolverá verdadero si el vı́deo cumple el formato MPEG2, o por el contrario falso si el vı́deo no lo cumple. Nos será de utilidad cuando tengamos un archivo con extensión mpg y no sepamos exactamente si el formato es MPEG1 o MPEG2. 3.3. Librerı́a creada 51 extractFramesMPEG2 Sinopsis: Función que nos extrae ciertos fotogramas de un vı́deo en formato MPEG2, válida para cuando vayamos a hacer el reconocimiento de un personaje. Formato: int extractFramesMPEG2 (char *fileName, double duration, char *spath); • char *fileName: El nombre del vı́deo origen. • double duration: La duración total del vı́deo, en segundos. • char *spath: El directorio donde queremos se guarden los archivos temporales. include: video.h Descripción: Los fotogramas extraı́dos del vı́deo fileName serán uno de cada diez, empezando por el fotograma número treinta y desechando el último, por si existen errores de inicialización y finalización del vı́deo. El valor de retorno será un entero, y será igual a cero si la función falla, o cualquier otro valor si la función cumple su objetivo. Esta función guardará los fotogramas en el directorio temporal que le hemos indicado (spath), en formato tif, y con el identificativo del fotograma al que corresponde en el nombre de la imagen guardada. El formato de los fotogramas será de 352 pı́xels de ancho por 240 pı́xels de alto. decodermpeg Sinopsis: Función que funciona de forma similar a la anterior, pero válida sólo para vı́deos en formato MPEG1. Formato: int decodermpeg (char *fileName, char *spath); • char *fileName: El nombre del vı́deo origen. • char *spath: El directorio donde queremos se guarden los archivos temporales. include: video.h Descripción: Al igual que la función anterior, esta función nos extrae un fotograma de cada diez del vı́deo fileName, y lo guarda en un formato ppm, quedando indicado en el nombre del archivo el fotograma al que corresponde. Dichos archivos se guardarán en el directorio temporal indicado en 52 CAPı́TULO 3. MANEJO DE FUNCIONES MULTIMEDIA spath. El valor de retorno será un entero, y será igual a cero si la función falla, o cualquier otro valor si la función cumple su objetivo. getFrameMediaDet Sinopsis: Función que nos extrae un fotograma determinado de un vı́deo MPEG-1. Formato: int getFrameMediaDet(char *fileName, ı́nt frame, imgdes *output); • char *fileName: Nombre del vı́deo origen. • int frame: Fotograma que queremos extraer. • imgdes *output: Imagen de VICTOR donde se almacena el fotograma. include: video.h Descripción: Esta función, a partir de un vı́deo MPEG-1, y dentro de la plataforma Windows 98, nos extrae un fotograma en concreto. Para ello, utiliza la interfaz IMediaDet. El resultado será una imagen de VICTOR, que podremos manipular como queramos, ya que se almacena en memoria. El valor de retorno será igual a cero si la función falla, o cualquier otro valor si se extrae el fotograma con éxito. getFrameSampGrab Sinopsis: Función que nos extrae un fotogramaen concreto de un vı́deo MPEG-1. Formato: int getFrameSampGrab(char *fileName, int frame, imgdes *output); • char *fileName: Nombre del vı́deo origen. • int frame: Fotograma que queremos extraer. • imgdes *output: Imagen de VICTOR donde se almacena el fotograma. include: video.h 3.3. Librerı́a creada 53 Descripción: Esta función, a partir de un vı́deo MPEG-1, y dentro de la plataforma Windows XP, nos extrae un fotograma en concreto. Para ello, utiliza la interfaz ISampleGrabber. El resultado será una imagen de VICTOR, que podremos manipular como queramos, ya que se almacena en memoria. El valor de retorno será el tipo HRESULT, y será igual a S OK si se extrae con éxito el fotograma, o el código de error resultante si la función falla. getFrameAvi Sinopsis: Función que nos extrae un fotograma determinado de un vı́deo AVI. Formato: int getFrameAvi(char *fileName, int frame, imgdes *output); • char *fileName: Nombre del vı́deo origen. • int frame: Número del fotograma que queremos extraer. • imgdes *output: Imagen de VICTOR donde se almacena el fotograma. include: video.h Descripción: Esta función, a partir de un vı́deo AVI, nos extrae un fotograma en concreto. Para ello, utiliza la librerı́a AVIFile. El resultado será una imagen de VICTOR, que podremos manipular como queramos, ya que se almacena en memoria. El valor de retorno será igual a cero si la función falla, o cualquier otro valor si se extrae el fotograma con éxito. getFrameMPEG2 Sinopsis: Función que nos extrae un fotograma de un vı́deo MPEG-2 y almacena el resultado en una imagen de VICTOR. Formato: int getFrameMPEG2 (char *fileName, long frame, double duration, imgdes *output); • char *fileName: Nombre del vı́deo origen. • long frame: Número del fotograma que queremos extraer. • double duration: Duración total del vı́deo, en segundos. • imgdes *output: Imagen de VICTOR donde se almacena el fotograma. 54 CAPı́TULO 3. MANEJO DE FUNCIONES MULTIMEDIA include: video.h Descripción: Esta función, a partir de un vı́deo MPEG-2 nos extrae un fotograma en concreto. Para ello, utiliza la librerı́a Mpeg2Lib. Además del número de fotograma a extraer, deberemos pasarle como parámetro la duración total del vı́deo. El resultado será una imagen de VICTOR, que podremos manipular como queramos, ya que se almacena en memoria. El valor de retorno será igual a cero si la función falla, o cualquier otro valor si se extrae el fotograma con éxito. extractIntervalMPEG1 Sinopsis: Función que nos extrae un intervalo de fotogramas de un vı́deo MPEG-1, guardándolos con el formato deseado. Formato: HRESULT extractIntervalMPEG1(char *fileName, int init, int end, char *output); • char *fileName: Nombre del vı́deo origen. • int init: Primer fotograma a extraer. • int end: Último fotograma a extraer. • char *output: Ruta completa y nombre que se le darán a los fotogramas. • char *format: Extensión que tendrán los fotogramas. include: video.h Descripción: Esta función, a partir de un vı́deo MPEG-1, nos extrae un conjunto de fotogramas, delimitados entre init y end, ambos incluidos. Los fotogramas se guardarán en la ruta y nombre indicados en output, seguidos de el número de fotograma al que corresponde, y con la extensión indicada. Por ejemplo, un valor válido para output serı́a el de ”c : /tmp/f otogramas %05d.jpg”, con lo que los fotogramas se guardarı́an con formato JPEG, y con cinco dı́gitos para identificar el número de fotograma (fotogramas00030.jpg, fotogramas00031.jpg,...). El valor de retorno será el tipo HRESULT, y será igual a S OK si la función tiene éxito, o el código de error resultante si la función falla. 3.3. Librerı́a creada 55 extractIntervalMPEG2 Sinopsis: Función que nos extrae un intervalo de fotogramas de un vı́deo MPEG-2, guardándolos con el formato deseado. Formato: int extractIntervalMPEG2(char *fileName, int init, int end, char *output); • char *fileName: Nombre del vı́deo origen. • int init: Primer fotograma a extraer. • int end: Último fotograma a extraer. • char *output: Ruta completa y nombre que se le darán a los fotogramas. • char *format: Extensión que tendrán los fotogramas. include: video.h Descripción: Esta función, a partir de un vı́deo MPEG-2, nos extrae un conjunto de fotogramas, delimitados entre init y end, ambos incluidos. Para ello, se utilizará la librerı́a Mpeg2Lib. Los fotogramas se guardarán en la ruta y nombre indicados, seguidos de el número de fotograma al que corresponde, y con la extensión indicada. Por ejemplo, un valor válido para output serı́a el de ”c : /tmp/f otogramas %05d.jpg”, con lo que los fotogramas se guardarı́an con formato JPEG, y con cinco dı́gitos para identificar el número de fotograma (fotogramas00030.jpg, fotogramas00031.jpg,...). El valor de retorno será igual a cero si la función falla, o cualquier otro valor si se extrae el fotograma con éxito. extractIntervalAVI Sinopsis: Función que nos extrae un intervalo de fotogramas de un vı́deo AVI, guardándolos con el formato deseado. Formato: HRESULT extractIntervalAVI(char *fileName, int init, int end, char *output); • char *fileName: Nombre del vı́deo origen. • int init: Primer fotograma a extraer. • int end: Último fotograma a extraer. • char *output: Ruta completa y nombre que se le darán a los fotogramas. 56 CAPı́TULO 3. MANEJO DE FUNCIONES MULTIMEDIA • char *format: Extensión que tendrán los fotogramas. include: video.h Descripción: Esta función, a partir de un vı́deo AVI, nos extrae un conjunto de fotogramas, delimitados entre init y end, ambos incluidos. Para ello, se utilizará la librerı́a AVIFile. Los fotogramas se guardarán en la ruta y nombre indicados, seguidos de el número de fotograma al que corresponde, y con la extensión indicada. Por ejemplo, un valor válido para output serı́a el de ”c : /tmp/f otogramas %05d.jpg”, con lo que los fotogramas se guardarı́an con formato JPEG, y con cinco dı́gitos para identificar el número de fotograma (fotogramas00030.jpg, fotogramas00031.jpg,...). El valor de retorno será igual a cero si la función falla, o cualquier otro valor si se extrae el fotograma con éxito. AbreVideoMPEG1 Sinopsis: Función que nos abre un vı́deo y lo deja preparado para la búsqueda y extracción de fotogramas. Formato: HRESULT AbreVideoMPEG1 (char *fileName); • char *fileName: Nombre del vı́deo que queremos abrir. include: video.h Descripción: Función que dado un vı́deo en formato MPEG1, inicializa las interfaces necesarias y prepara el vı́deo fileName para la próxima extracción de fotogramas de dicho vı́deo. El valor de retorno será del tipo HRESULT y será igual a S OK si la función tiene éxito, o cualquier otro valor en caso contrario. SacaFrameMPEG1 Sinopsis: Función que nos extrae un fotograma de un vı́deo abierto previamente con la función AbreVideoMPEG2 y lo guarda en disco. Formato: HRESULT SacaFrameMPEG1(double second, imgdes *img); • double second: Segundo en el que se encuentra el fotograma. • imgdes *img: Estructura de VICTOR donde se guardará el fotograma. 3.3. Librerı́a creada 57 include: video.h Descripción: Función que nos extrae un fotograma de un vı́deo abierto previamente con la función AbreVideoMPEG1. Averiguar el segundo en el que se encuentra el fotograma será fácil utilizando la función getVideoInfo, que nos devuelve el número de fotogramas por segundo con el que se reproduce el vı́deo. El valor de retorno será del tipo HRESULT y será igual a S OK si la función tiene éxito, o cualquier otro valor en caso contrario. AbreVideoAvi Sinopsis: Función que nos abre un vı́deo y lo deja preparado para la búsqueda y extracción de fotogramas. Formato: PGETFRAME AbreVideoAvi(char *fileName, PAVISTREAM *stream); • char *fileName: Nombre del vı́deo que queremos abrir. • PAVISTREAM *stream: Variable donde se almacenará el stream de vı́deo con el que vamos a tratar. include: video.h Descripción: Función que dado un vı́deo en formato AVI, inicializa las interfaces necesarias y prepara el vı́deo fileName para la próxima extracción de fotogramas de dicho vı́deo. El valor de retorno será del tipo PGETFRAME y será el que utilizaremos más tarde para la extracción de fotogramas, mediante la función SacaFrameAvi. CierraVideoAvi Sinopsis: Función que se encarga de cerrar cualquier vı́deo abierto con la función AbreVideoAvi. Formato: void CierraVideoAvi(PGETFRAME t, PAVISTREAM stream); • PGETFRAME t: Variable devuelta en la llamada a la función AbreVideoAvi. • PAVISTREAM *stream: Variable devuelta en la llamada a la función AbreVideoAvi. 58 CAPı́TULO 3. MANEJO DE FUNCIONES MULTIMEDIA include: video.h Descripción: A la función le pasaremos como parámetros los que nos ha devuelto la función AbreVideoAvi, y se encargará de liberar todos los recursos guardados para la extracción de fotogramas. SacaFrameAvi Sinopsis: Función que nos extrae un fotograma de un vı́deo abierto previamente con la función AbreVideoAvi y lo almacena en una estructura de VICTOR. Formato: int SacaFrameAvi (PGETFRAME t,int numframe, imgdes *imagen); • PGETFRAME t: Variable devuelta en la llamada a la función AbreVideoAvi. • int numframe: Número de fotograma a extraer. • imgdes *img: Estructura de VICTOR donde se guardará el fotograma. include: video.h Descripción: Función que nos extrae un fotograma de un vı́deo abierto previamente con la función AbreVideoAvi. El valor de retorno será del tipo int y será igual a cero si la función falla, o cualquier otro valor en caso contrario. AbreVideoMPEG2 Sinopsis: Función que nos abre un vı́deo y lo deja preparado para la búsqueda y extracción de fotogramas. Esta función fallará si el vı́deo no tiene formato MPEG2. Formato: long AbreVideoMPEG2 (char *fileName); • char *fileName: Nombre del vı́deo que queremos abrir. include: video.h 3.3. Librerı́a creada 59 Descripción: Función que dado un vı́deo en formato MPEG2, inicializa la librerı́a Mpeg2Lib y prepara el vı́deo fileName para la próxima extracción de fotogramas de dicho vı́deo. El valor de retorno será del tipo long y será igual a cero si la función falla, o tomará el valor del tamaño del vı́deo, valor de utilidad para reproducir el vı́deo. Esta es la primera de las funciones para el tratamiento de los vı́deos VOB, y que nos serán de gran utilidad cuando queramos reproducir un vı́deo VOB en nuestra aplicación, sin tener que utilizar el Windows Media Player. Esta función y todas las siguientes hacen uso de la librerı́a Mpeg2Lib, vista en el punto 3.1.3. CierraVideoMPEG2 Sinopsis: Función que se encarga de cerrar cualquier vı́deo abierto con la función AbreVideoMPEG2. Formato: void CierraVideoMPEG2 (); include: video.h Descripción: La función no tiene parámetros, ya que internamente se mantiene una referencia del vı́deo que está abierto; y no tiene valor de retorno, ya que es imposible que falle. Por lo tanto, deberemos tener cuidado de llamar a la función después de haber llamado a la función AbreVideoMPEG2. Además de cerrar el vı́deo, liberará la librerı́a Mpeg2Lib y todos los recursos guardados. SeekPositionMPEG2 Sinopsis: Función que se sitúa en el fotograma indicado. Formato: int SeekPositionMPEG2 (long position); • long position: Número de bytes a moverse en el vı́deo. include: video.h Descripción: Función que se moverá dentro del vı́deo para situarse en el fotograma que indiquemos. Para ello, se mueve dentro del vı́deo una cierta cantidad de bytes, la que le indiquemos en position; este valor está relacionado con el valor e retorno de la función AbreVideoMPEG2. No hace falta indicarle el nombre del vı́deo porque se mantiene una referencia interna 60 CAPı́TULO 3. MANEJO DE FUNCIONES MULTIMEDIA del vı́deo abierto. Como valor de retorno tendrá un entero, que valdrá cero si la función fallo, o cualquier otro valor si la función cumple su objetivo. Como se explicó con anterioridad, esta forma de moverse dentro del vı́deo es muy rápida, y por lo tanto la respuesta será casi inmediata. SacaFrameMPEG2 Sinopsis: Función que nos extrae un fotograma de un vı́deo abierto previamente con la función AbreVideoMPEG2 y lo guarda en disco. Formato: long SacaFrameMPEG2 (char *output); • char *output: Nombre con el que queremos que se guarde el fotograma. include: video.h Descripción: Función que nos extrae un fotograma de un vı́deo abierto previamente con la función AbreVideoMPEG2, y que lo guarda en disco con el nombre de output. El formato a utilizar para guardar el fotograma será el que le indiquemos en output, y serán válidos los formatos jpg, tif o ras. El valor de retorno será del tipo int, y será igual a cero si la función falla, o devolverá el ancho del fotograma, que nos será de utilidad para su posterior tratamiento. extraeCara Sinopsis: Función que nos extrae un recuadro de una imagen determinada, determinado por ciertas coordenadas. Formato: int extraeCara(char *fileName, double x1, double y1, double x2, double y2, char *output, int &ancho, int &alto); • char *fileName: El nombre de la imagen origen. • double x1: La coordenada x del primer ojo. • double y1: La coordenada y del primer ojo. • double x2: La coordenada x del segundo ojo. • double y2: La coordenada y del segundo ojo. • char *output: El nombre de la imagen donde se guardará la cara. 3.3. Librerı́a creada 61 • int &ancho: Un puntero a un entero, donde se guardará el ancho del fotograma. • int &alto: Un puntero a un entero, donde se guardará el alto del fotograma. include: video.h Descripción: Función que nos extrae la cara de la imagen fileName, dándole las coordenadas de dos puntos, normalmente de los ojos (x1, y1, x2, y2 ). La cara se guardará con el nombre de output, con un tamaño de 50 pı́xels de ancho por 70 pı́xels de alto, y con el formato indicado en output. Esta función nos será de utilidad cuando trabajemos con los vı́deos VOB, de los cuales sacamos un fotograma pero después deseamos obtener la cara, aunque podrı́amos hacer uso de ella en cualquier tipo de vı́deo, ya que depende sólo de la imagen que le pasemos como parámetro. Los valores de las coordenadas que le pasemos como parámetros no serán los reales, sino los relativos al VB. Es decir, que nosotros pasaremos como coordenada la que saquemos del VB dividida por el ancho del objeto que contiene el vı́deo en VB, y después esta función ya se encarga de rectificar este cambio. Se hace de esta forma para tener que realizar operaciones innecesarias. 3.3.2. Audio Tal y como hemos hecho con el vı́deo, detallaremos ahora cada una de las funciones que se han creado para el audio. extraeAudio Sinopsis: Función que nos extrae el audio de un archivo de vı́deo, y lo guarda en formato WAV. Formato: int extraeAudio (char *nombre, char *audio); • char *fileName: El nombre del fichero de vı́deo original, del cual se extraerá el audio. • char *audio: El nombre del fichero de audio de salida, en el cual se guardará, en formato WAV, el audio buscado. include: audio.h 62 CAPı́TULO 3. MANEJO DE FUNCIONES MULTIMEDIA Descripción: Función que nos permite, a partir de un archivo de vı́deo origen, extraer el audio y guardarlo en formato WAV. Como valor de retorno, tendremos un cero si la función ha fallado, o por el contrario, si la función se ha llevado a cabo satisfactoriamente, dicho valor será 1. cepstrum Sinopsis: Función que nos extraerá los coeficientes de un archivo de audio. Formato: void cepstrum (char *audio, char *coeficientes); • char *audio: Un string donde le indiquemos el nombre del archivo de audio original, del que queremos extraer los parámetros. • char *coeficientes: Un string indicando el nombre del fichero en que queremos guardar dichos parámetros. include: audio.h Descripción: Función que se encarga de llevar a cabo el mel cepstrum, es decir, la generación de los coeficientes de un vı́deo en concreto. El fichero de salida (coeficientes), que contendrá los parámetros, tendrá la extensión mcc. Como entrada, como vemos, no tenemos que pasarle el vı́deo del que queremos extraer los parámetros, sino directamente el audio, por lo que previamente a esta función se tendrá que hacer uso de la función extraeAudio para extraer el audio de dicho vı́deo. genera modelo Sinopsis: Función que genera el modelo de audio de un personaje, a partir de uno o varios archivos de coeficientes. Formato: genera modelo coef1, coef2, coef3, ..., modelo • coef1, coef2, ...: El nombre del fichero o ficheros de coeficientes, hasta un máximo de 63 (recordar que estos archivos tienen extensión mcc). • modelo: El nombre del archivo donde queremos se guarde el modelo que se creará (este tendrá la extensión gmm). include: audio.h 3.3. Librerı́a creada 63 Descripción: Esta función es la que se encarga de, a partir de los ficheros de parámetros (o coeficientes) generados por la función anterior, nos crea un fichero que contendrá el modelo del personaje. Por lo tanto, le podremos pasar uno o varios ficheros de coeficientes, y en este punto se nos presenta un problema, y será que el lenguaje C++ acepta funciones que no tengan los parámetros definidos, pero en el punto de enlazar esta función con Visual Basic resulta imposible, por lo que se buscó otra solución. Esta solución consiste en crear un archivo ejecutable, que sı́ acepta varios parámetros como entrada y se puede llamar fácilmente desde el Visual Basic. Ası́, cuando queramos usar esta función, al llamarla se abrirá una ventana de comandos, y se ejecutará dicha función, aunque el usuario prácticamente no notará que se está ejecutando la función en una ventana de comandos, ya que aparecerá minimizada y en cuanto termine se cerrará. verifica modelo Sinopsis: Función que comprueba si un fichero de coeficientes pertenece a un determinado modelo. Formato:int verifica modelo (char *modelo, char *coeficientes, char *salida); • char *modelo: El nombre del fichero que contiene el modelo. • char *coeficientes: El nombre del fichero que contiene los parámetros que queremos verificar. • char *salida: El nombre del fichero donde queremos que se guarde el valor de salida.1 include: audio.h Descripción: Función la cual a partir de modelo y un fichero de coeficientes, nos da un valor para comprobar si realmente dichos coeficientes pertenecen al personaje o no. Posteriormente, si queremos utilizar dicho valor, simplemente tendremos que abrir el fichero de salida de la función, y leer la primera lı́nea, que será dicho valor. 1 El formato de este archivo es de texto. 64 CAPı́TULO 3. MANEJO DE FUNCIONES MULTIMEDIA Remuestreo Sinopsis: Función que remuestrea el audio a una nueva frecuencia de 22050 Hz y lo convierte a mono. Además, nos cortará el audio por la posición inicial y final indicada. Formato: int remuestreo (char *fileName, char *Tin, char *Tout, char *output); • char *fileName: Nombre del fichero que contiene el audio original, en formato WAV. • char *Tin: Tiempo inicial, en segundos. • char *Tout: Tiempo final, en segundos. • char *output: Nombre del fichero destino, que también será WAV, y que contendrá el audio remuestreado a la frecuencia deseada. include: audio.h Descripción:Función que nos remuestrea el audio a una frecuencia de 22050 Hz, que será la que nosotros utilizamos para la generación de coeficientes. Además, nos cortará el archivo de audio de salida por los puntos que nosotros le indiquemos. Esta función se utiliza normalmente de forma interna, pero se ha decidido extraerla también ya que se considera que puede servir de utilidad en algunos casos, como por ejemplo en casos que ya tengamos el audio separado del vı́deo, pero no lo tengamos a la frecuencia deseada. resampleOnly Sinopsis: Función que remuestrea el audio a una nueva frecuencia de 22050 Hz y lo convierte a mono. Formato: void resampleOnly (char *fileName, char *output); • char *fileName: Nombre del audio origen, muestreado a cualquier frecuencia. • char *output: Nombre del fichero de audio destino, que contendrá el audio mono y muestreado a 22050 Hz. include: audio.h 3.3. Librerı́a creada 65 Descripción: Funciona básicamente como la función remuestreo, pero esta función no cortará el audio, sino que realizará la conversión de frecuencia a todo el archivo de audio. También convertirá el audio a mono, si es que originalmente tenı́a más de un canal. 66 CAPı́TULO 3. MANEJO DE FUNCIONES MULTIMEDIA Capı́tulo 4 Teorı́a del reconocimiento La base de este proyecto es el reconocimiento de personajes, tanto a nivel visual como de audio. Por lo tanto, consideramos necesario el incluir los conocimientos teóricos fundamentales en los que se basan las operaciones de reconocimiento. Ası́, y como a lo largo de todo el proyecto, hemos dividido este capı́tulo en dos puntos, dedicando el primero de ellos al reconocimiento visual y el segundo al reconocimiento de audio. Quiero recalcar que no es objetivo del proyecto el estudiar las técnicas de reconocimiento. Por ello, podremos encontrar más información acerca de este tema en [1]. 4.1. Reconocimiento empleando información visual Cuando se trata del reconocimiento de vı́deo, lo que realmente pretendemos es, a partir de una secuencia de vı́deo, averiguar si la cara de la persona que buscamos aparece realmente dentro de dicha secuencia. Para ello, generalmente se debe realizar una búsqueda para encontrar las caras que puedan haber en cada fotograma de la secuencia, para después llevar a cabo el reconocimiento de cada una de las caras encontradas. En la figura 4.1 podemos ver el diagrama de bloques que se ha implementado en este proyecto. En dicho diagrama podemos observar que la entrada será la secuencia en la cuál queremos encontrar a la persona buscada. A continuación aparece el bloque de detección de caras, que será el que nos encuentre todas las 67 68 CAPı́TULO 4. TEORı́A DEL RECONOCIMIENTO Figura 4.1: Diagrama de bloques de un sistema de reconocimiento visual. caras existentes en la secuencia. La salida de dicho bloque se introduce al bloque de reconocimiento de caras, cuya tarea será la de, a partir del modelo de la persona que buscamos, separar todas las caras encontradas en la secuencia en dos tipos: las que pertenecen al personaje buscado y las que no. Una vez reconocidas las caras, pasaremos los resultados a un bloque que combinará todos los resultados obtenidos en cada imagen, y pasaremos al bloque de decisión, el cual nos dirá si realmente se ha encontrado al personaje en la secuencia, o en caso contrario que no aparece en dicha secuencia. Cabe comentar que previo al reconicimiento de caras, es necesaria una normalización para eliminar los cambios en la posición de la cara, en su expresión o las diferentes condiciones de iluminación. De entre todas las técnicas existentes para el reconocimiento de caras, la que se ha utilizado en el proyecto es la de Análisis de Componente Principal (PCA), ya que se ha demostrado que es una de las mejores técnicas existentes en el momento, especialmente en las aplicaciones de identificación de caras. La idea básica de la técnica PCA es reducir considerablemente las dimensiones del espacio en el cuál se van a realizar los cálculos, realizando proyecciones del original dentro de un subespacio de dimensión mucho menor. Después, se usarán dichas proyecciones para representar el original. Este subespacio está representado por un conjunto de vectores ortonormales, denominados eigenfaces. De forma básica, el proceso de reconocimiento de una persona se llevará a cabo mediante los siguientes pasos: Obtención de una cierta cantidad de imágenes fijas que contienen el rostro de la persona buscada. 4.1. Reconocimiento empleando información visual 69 Se proyectan las caras de test sobre el subespacio formado por esas eigenfaces. Se reconstruye la cara del personaje a partir de las eigenfaces obtenidas anteriormente. Se calcula el error de reconocimiento, que será el que realmente nos servirá para decidir si el personaje que aparece en la secuencia es la persona que buscábamos o no. En el proyecto se ha implementado una variante del modelo PCA original, en el que el análisis trabaja sobre un grupo de diferentes vistas de la misma persona a ser reconocida, obteniendo un grupo particular de eigenfaces para cada personaje, llamadas self-eigenfaces. La figura 4.2 muestra un ejemplo donde se obtiene un conjunto de selfeigenfaces a partir de tomas distintas del rostro de la misma persona. En dicha figura podemos ver que se aumenta el conjunto de caras de entrenamiento añadiendo las imágenes espejo de cada una de ellas. Este paso, que se basa en la simetrı́a de la cara, intenta mejorar la calidad de las self-eigenfaces, ya que añade variaciones adicionales a las caras de entrenamiento. Figura 4.2: Ejemplo de caras de entrenamiento y self-eigenfaces. 70 CAPı́TULO 4. TEORı́A DEL RECONOCIMIENTO Pasaremos ahora a la etapa de test, en la cual cada imagen que tiene que ser reconocida se proyecta sobre un grupo diferente de self-eigenfaces. Pongamos que x es un imagen de test representada como un vector columna, y xn es la cara después del proceso de normalización. Entonces, los coeficientes de proyección se usan para aproximar la imagen de test original, y el error de reconstrucción se usa para tomar la decisión final. Los coeficientes de proyección de la cara de test usando las self-eigenfaces de la persona se obtiene haciendo uso de la siguiente ecuación: (4.1) y m = VmT (xn − xm µ) donde xm µ es la cara media de la persona m y Vm es una matriz que contiene las Km eigenfaces principales vkm de la persona m. Una vez hayamos obtenido los coeficientes de proyección, la cara de test original se podrá aproximar mediante la siguiente ecuación: xm n = Km X y m (k).vkm + xm µ (4.2) k=1 Después de reconstruir la imagen de test, el error de reconstrucción ²m se calculará de la siguiente forma: ²m = v u 1 u u 1 t 255 MN M N X 2 |xn (j) − xm n (j)| (4.3) j=1 donde j es el ı́ndice de vector. La idea básica de este modelo es que dada una cara de test, se conseguirá un error de reconstrucción bajo cuando se utilice el grupo de self-eigenfaces del personaje correspondiente, o lo que es lo mismo, que habremos acertado en el reconocimiento. En la figura 4.3 podremos ver un ejemplo de reconocimiento, en el cuál se ha intentado reconocer a un personaje mediante cuatro tomas diferentes de su cara, y utilizando las self-eigenfaces mostradas en la figura 4.2. Podemos observar que se producen dos casos bien identificados: En el primer caso, las caras de test A y B están situadas aproximadamente en la misma posición que las imágenes de entrenamientos que veı́amos en la figura 4.2. Podemos ver que el error de reconstrucción nos será útil para el reconocimiento de caras. En este caso, la cara reconstruida An se parece mucho a la cara normalizada An , y por lo tanto el error de reconstrucción es pequeño. En el caso de la cara B podemos ver que la cara reconstruida B n no se parece a la cara normalizada B n , y por lo tanto el error de reconstrucción será más alto. 4.1. Reconocimiento empleando información visual 71 En el segundo caso, la posición de las caras normalizadas C n y Dn difiere mucho de la posición de las caras en el entrenamiento (ver figura 4.2). Por lo tanto, las caras reconstruidas C n y Dn , que se construyen a partir de combinaciones lineales de las self-eigenfaces de la figura 4.2, son muy diferentes de las caras normalizadas. Por ello, el error de reconstrucción es mucho mayor que el obtenido en el caso B. Otro detalle será que en el caso D el tamaño de la imagen no es el mismo que el de la cara normalizada, que tendrá siempre el mismo tamaño, por lo que se añade una deformación adicional que conlleva el que el error sea mucho mayor. Figura 4.3: Ejemplo de reconocimiento de caras. Para finalizar con este punto, mostraremos los detalles matemáticos del modelo PCA. Supongamos que X = x1 , x2 , ..., xM es un conjunto de vectores Ndimensionales. El valor medio (xµ ) y la covarianza (Σ) de los datos podrán obtenerse de la siguiente forma: M 1 X xm (4.4) xµ = M m=1 y M 1 X ΣN xN = [xm − xµ ][xm − xµ ]T (4.5) M m=1 72 CAPı́TULO 4. TEORı́A DEL RECONOCIMIENTO donde ΣN xN es una matriz simétrica de N xN y caracteriza la dispersión del conjunto de datos. Si suponemos ahora que gi = xi − xµ y que G = [g1 , g2 , ..., gM ], podremos reescribir la ecuación de la matriz de varianza de la siguiente forma: ΣN xN = GGT (4.6) Ası́, tendremos que un vector distinto de cero para el que se cumpla la siguiente ecuación: ΣN xN vk = λk vk (4.7) será un eigenvector de la matriz de covarianza, mientras que λk será su correspondiente eigenvalue. Si suponemos ahora que λ1 , λ2 , ..., λK son los eigenvalues mayores de ΣN xN , entonces la matriz: VN xK = [v1 , v2 , ..., vK ] (4.8) contiene los K eigenvectores dominantes. Esos vectores abarcan un subespacio K dimensional al que nos referiremos como subespacio principal. Cuando el conjunto de datos está formado por caras, esos eigenvectores se llaman eigenfaces. Un vector de entrada x N -dimensional puede ser transformado linealmente dentro de un vector K -dimensional y como sigue: x= K X yk vk + xµ (4.9) k=1 Se ha diseñado la PCA para minimizar el error cuadrático medio de reconstrucción, ², sobre el conjunto de datos de entrenamiento, donde: ²= M 1 X |xm − xm |2 M m=1 (4.10) Sin embargo, en situaciones prácticas los eigenvectores de ΣN xN son difı́ciles de obtener, debido a que la matriz es muy grande. No obstante, los eigenvectores principales pueden estimarse usando una técnica similar a la SVD (Singular Value Descomposition) y que pasamos a resumir a continuación. Sea vj un eigenvector de ΣN xN y sea λj su eigenvalue correspondiente; entonces: GGT vk = λk vk (4.11) 4.2. Reconocimiento empleando información del audio 73 Si multiplicamos ambas partes por GT , y asumiendo que uk = GT vk y que CM xM = GT G, podremos expresar la ecuación anterior de la siguiente forma: CM xM uk = λk uk (4.12) A partir de aquı́, vemos que podemos obtener los eigenvalores de ΣN xN obteniendo los eigenvalores de CM xM . Por lo tanto, como es sabido que el número de elementos de CM xM es mucho menor que el de ΣN xN , se simplifica considerablemente el problema computacional. 4.2. Reconocimiento empleando información del audio En el audio existente en una secuencia de vı́deo también existe multitud de información que puede ser explotada para el reconocimiento de personajes. Por lo tanto, se hará uso del audio para complementar el reconocimiento visual o incluso para llevar a cabo el reconocimiento cuando sólo tengamos un fichero de audio. En la figura 4.4 podemos ver el diagrama de bloques básico para llevar a cabo el reconocimiento mediante el audio. En dicho diagrama podemos observar que el primer paso será una segmentación del audio de entrada, para obtener trozos de audio homogéneos en los que sólo aparezca el personaje hablando, o en el que haya música, ruido o incluso trozos de silencio. Ası́, estos trozos se pasarán a un bloque que reconocerá si realmente dicho sonido es voz o no, para descartar todos los bloques en los que no aparezca el personaje. Por último, se aplican todas las técnicas de reconocimiento de audio para obtener la identidad del personaje que está hablando. En muchos casos, la segmentación del audio de entrada no será tan fácil como se ha descrito, ya que es posible que el personaje esté hablando con una música de fondo, que hablen dos o más personajes al mismo tiempo, o cualquier otro problema que haga que la voz del personaje suene distinta. Para el proyecto se ha asumido que sólo un personaje está hablando a la vez. Además, en el proyecto también se ha asumido que el audio de entrada ya es voz, por lo que dejaremos aparte los dos primeros bloques del diagrama, para 74 CAPı́TULO 4. TEORı́A DEL RECONOCIMIENTO Figura 4.4: Diagrama de bloques en el reconocimiento de audio. centrarnos en el tercero de los bloques, es decir, en el reconocimiento de voz y las técnicas usadas para llevarlo a cabo. Para llevar a cabo el reconocimiento, serán necesarios dos pasos: Extracción de parámetros: A partir de señales de voz del personaje a reconocer, llevaremos a cabo medidas para extraer los rasgos de dicho personaje, que corresponderán a ciertos parámetros de la voz. Comparación de parámetros: Una vez tengamos el modelo del personaje a reconocer, podremos comparar las señales de audio que tengamos para decidir si se ha reconocido al personaje o no. Se ha demostrado que el espectro frecuencial de la voz es muy útil para el reconocimiento, ya que dicho espectro refleja directamente el efecto del tracto vocal del personaje, que es el principal factor psicológico que distingue a la voz de las personas. El conjunto de parámetros que utilizaremos para el reconocimiento de la voz serán los Mel-Frequency Cepstrum Coefficients (MFCC ). Estos parámetros están basados en la variación de los anchos de banda crı́ticos del oı́do humano con la frecuencia, que tendrá filtros espaciados linealmente en bajas frecuencias y logarı́tmicamente a altas frecuencias para capturar las principales caracterı́sticas fonéticas de la voz. Esto se puede comprobar en la escala de frecuencias mel, que usa un espaciado lineal por debajo de los 1000 Hz, y un espaciado logarı́tmico por encima de los 1000 Hz. En la figura 4.5 podemos ver un diagrama de bloques del proceso de extracción de los parámetros MFCC. El único requerimiento es que la señal de entrada esté muestreada a 22050 muestras/s, para que cubra todos los sonido que generan los humanos. Pasaremos ahora a detallar los bloques de la figura 4.5. En primer lugar nos 4.2. Reconocimiento empleando información del audio 75 Figura 4.5: Extracción de parámetros MFCC. encontramos con el Windowing, que nos partirá la señal en trozos para poder ser procesada posteriormente. A continuación cada uno de estos trozos pasará por el bloque de FFT, que convertirá la señal al dominio frecuencial. Ya que la salida de este bloque será compleja, se incluye a continuación un bloque que se quedará con el valor absoluto de dicha señal compleja. Ahora, y tras un bloque de preénfasis, se pasará la señal por un banco de filtros del tipo mel, cuyo aspecto puede verse en la figura 4.6 1 . Para finalizar, simplemente nos quedará aplicar el Cepstrum, que nos convertirá la señal de nuevo al dominio temporal; para ello, se aplica la DCT (Discrete Cosine Transform), cuya salida serán los coeficientes MFCC. Una vez obtenidos los parámetros correspondientes al personaje, pasaremos ahora a la parte de reconocimiento. La técnica usada para el reconocimiento está basada en el cociente de verosimilitud, de forma que dados los coeficientes del segmento de voz a reconocer, se calcula su probabilidad empleando el modelo de la persona buscada. Este modelo se construye empleando la mezcla de componentes gaussianas (GMM, Gaussian Mixture Models). Se ha utilizado este modelo gracias a las ventajas que presenta; la primera de esas ventajas es que tiene la capacidad de representar densidades arbitrarias de los rasgos de los personajes. Otra ventaja es que las decisiones finales de clasificación están basadas en probabilidades, y no en distancias. Además, las componentes individuales gaussianas se interpretan normalmente para representar las clases acústicas dependientes del personaje. 1 Vemos que los filtros tienen una respuesta en frecuencia triangular, y el espaciado depende de una constante del modelo. 76 CAPı́TULO 4. TEORı́A DEL RECONOCIMIENTO Figura 4.6: Banco de filtros mel. Capı́tulo 5 Programación en Visual Basic y Visual C++ Debido a los problemas surgidos con la programación y creación de librerı́as, tanto estáticas como dinámicas, hemos considerado necesario dedicar un punto a explicar las opciones existentes y las soluciones por las que se ha optado en nuestro proyecto. Tal y como se ha dicho en los capı́tulos anteriores, la creación de librerı́as es necesaria y fundamental para la puesta en marcha de nuestro proyecto, ya que son el punto clave para enlazar las dos plataformas de programación. Además, hemos visto que deberemos adaptar las funciones creadas originalmente en MATLAB, para lo cuál también necesitamos el poder trabajar con librerı́as dinámicas. Encontraremos más información de todo lo relacionado con el MATLAB en el anexo A. Ası́, dejando aparte el MATLAB, veremos en los siguientes puntos la creación de librerı́as dinámicas (DLL) en Visual C++ (en adelante VC++), y a continuación podremos ver cómo se usan después esas librerı́as en Visual Basic (en adelante VB) y las diferentes formas de usarlas en VC++. Para finalizar, se explicará el uso de librerı́as estáticas (normalmente con extensión lib). 5.1. Creación de DLLs en VC++ La aplicación con la que programamos en C++ (Microsoft Visual C++) ofrece un amplio rango de posibilidades cuando creamos un nuevo proyecto. Todas ellas tienen su función, pero en nuestro caso la que nos interesa es la opción de ”Win32 77 78 CAPı́TULO 5. PROGRAMACIÓN EN VISUAL BASIC Y VISUAL C++ Dynamic-Link Library”, como podemos ver en la figura 5.1. Figura 5.1: Ventana de nuevo proyecto en Microsoft VC++. Una vez hayamos creado satisfactoriamente nuestro proyecto, eligiendo la opción anterior, empezaremos a escribir nuestro código fuente. Para ello, escribiremos las funciones normalmente, con los parámetros que consideremos necesarios y con el valor de retorno que deseemos. La única consideración que hay que tener será la de añadir la palabra WINAPI entre el tipo de valor de retorno y el nombre de la función. Es decir, que para una función que retorne un entero, y que tenga como parámetros dos caracteres, tendremos que escribir el código siguiente: int WINAPI funcion (char a, char b) { //código } Aparte de todas las funciones que hayamos programado, deberemos añadir otra que será la principal, al igual que ocurre cuando tenemos un proyecto que crea un ejecutable, en el cuál deberá existir una función main. Esta función se llamará DllMain, y tendrá tres parámetros: 5.1. Creación de DLLs en VC++ 79 1. Un identificador del módulo de la DLL. 2. La razón por la que se ha llamado a la librerı́a. 3. Parámetro reservado. Ası́, el aspecto que tomará dicha función será el siguiente: BOOL WINAPI DllMain ( HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) { } En el código de esta función, y dependiendo de la razón por la que se ha llamado a la librerı́a, se introducirá un código u otro. Normalmente, y en todos nuestros casos, no se introduce ningún tipo de código. Y con esto ya tendrı́amos el archivo de código fuente terminado. Ahora deberemos añadir un segundo archivo, con extensión DEF y que se denominará archivo de definiciones. Aquı́ será donde le indiquemos al compilador las funciones que queremos exportar. Para ello, simplemente pondremos en cada lı́nea el nombre de la función que queramos exportar, seguido del sı́mbolo de @ y de un número consecutivo que identificará a la función. Además de los nombres de las funciones, se deberán añadir unos campos de cabecera. Para clarificar esta explicación, pondremos un ejemplo de un archivo DEF: ;Módulo DEF para EJEMPLO.DLL LIBRARY EJEMPLO DESCRIPTION ’Funciones de ejemplo’ EXPORTS funcion1 @1 funcion2 @2 ;Fin del módulo 80 CAPı́TULO 5. PROGRAMACIÓN EN VISUAL BASIC Y VISUAL C++ Podemos ver que en este código de ejemplo la librerı́a dinámica se hubiera llamado ejemplo.dll (la primera y la última lı́nea son comentarios, ya que empiezan por ’;’), y hubiera exportado dos funciones, llamadas funcion1 y funcion2. En la figura 5.2 podemos ver una ventana de entorno de trabajo del programa Visual C++, en el cual se ha creado un proyecto llamado ejemplo, y en el que podemos ver que existen dos ficheros principales: el fichero con extensión cpp, que contendrá todas las declaraciones de funciones, y el fichero con extensión def, que contendrá todas las definiciones de las funciones que se van a exportar, tal y como se ha explicado anteriormente. Figura 5.2: Ejemplo de proyecto en VC++. Una vez llevados a cabo todos estos pasos satisfactoriamente, se le indicará a la aplicación que compile el código, dando como resultado en el directorio en el que se indique el fichero .dll que tenı́amos como objetivo. 5.2. Llamada a DLLs en VB Ahora, una vez tenemos las funciones que hemos desarrollado dentro de una librerı́a dinámica dll, tendremos que enlazar dicha librerı́a con el VB, para poder hacer uso de dichas funciones. Para ello, dentro del programa crearemos un proyecto nuevo, y no elegiremos ninguna opción especial, sino simplemente la opción de EXE estándar, tal y como 5.2. Llamada a DLLs en VB 81 se muestra en la figura 5.3. Figura 5.3: Ventana de nuevo proyecto en Microsoft VB. Con esto, se nos abrirá una nueva ventana de proyecto, donde podemos ir añadiendo los objetos que deseamos para nuestra aplicación. Para llamar a las funciones de la librerı́a, deberemos crear un nuevo módulo, yendo al menú Proyecto y eligiendo el submenú de Agregar módulo. Los módulos se utilizan para realizar declaraciones de funciones comunes al proyecto, o para declarar variables globales. En la figura 5.4 podemos ver un ejemplo de una ventana de proyecto dentro del programa Visual Basic; en dicho proyecto se ha añadido un fichero de formulario, donde crearemos nuestra aplicación gráfica, y un fichero de módulo, en el que realizaremos las declaraciones de funciones externas. Dentro de este módulo, haremos las llamadas a nuestras funciones. Para e- 82 CAPı́TULO 5. PROGRAMACIÓN EN VISUAL BASIC Y VISUAL C++ Figura 5.4: Ejemplo de proyecto en VB. llo, deberemos empezar por la palabra clave Declare, seguida de Function o Sub, según la función devuelva algún parámetro o no, respectivamente. A continuación pondremos el nombre de la función, exactamente igual que el que habı́amos exportado anteriormente1 , seguido de la palabra clave Lib y el nombre de la librerı́a donde se encuentra la función, entre comillas. Ahora le llega el momento a los parámetros, declarándolos del mismo tipo que habı́amos exportado, e indicando delante la palabra clave ByVal si pasamos el parámetro por valor o ByRef, si lo pasamos por referencia. Para finalizar, y si procede, indicaremos el tipo del valor de retorno, anteponiendo la palabra clave As. Vemos a continuación como quedarı́a una declaración de ejemplo: Declare Function f Ejemplo Lib ”ejemplo.dll” (ByVal p1 As Integer, ByVal p2 As Double) As Boolean En este ejemplo hemos declarado la función f Ejemplo, con dos parámetros: un entero y un double. El valor de retorno será booleano, y si no ponemos una ruta especı́fica en el nombre de la librerı́a, como es el caso, queda implı́cito que dicha librerı́a existirá en el mismo directorio en el que se encuentre el fichero ejecutable creado. 1 Es importante que el nombre sea exactamente el mismo, incluso con mayúsculas y minúsculas iguales. 5.3. Llamada a DLLs en VC++ 83 En la figura 5.5 podemos ver un ejemplo de cómo se llamarı́a a la función que hemos declarado anteriormente, llamada f Ejemplo, y a la que debemos pasar dos parámetros y recoger su valor de retorno. Figura 5.5: Ejemplo de código en VB. Deberemos tener en cuenta que los tipos de variables no coinciden entre las dos plataformas, sino que existen una serie de variaciones con algunos tipos. En el apéndice B encontraremos una tabla con todas estas conversiones. 5.3. Llamada a DLLs en VC++ Tendremos dos formas de llamar a funciones incluidas dentro de librerı́as dinámicas: la forma directa, en la que se llamará directamente a la dll, y la forma indirecta, en la que se utilizará un archivo lib y un archivo h. 5.3.1. Forma directa No será muy usual el utilizar la forma directa de llamar a las librerı́as dinámicas, ya que normalmente se utilizará la forma indirecta, pero existen casos en los que es necesario el uso de esta forma directa, por lo que se ha considerado 84 CAPı́TULO 5. PROGRAMACIÓN EN VISUAL BASIC Y VISUAL C++ necesario el incluir una referencia sobre esta forma de llamar a las funciones de una dll. Para utilizar una función que se encuentre incluida dentro de una dll, tendremos que utilizar ciertos métodos que detallamos a continuación. En primer lugar deberemos cargar la librerı́a dentro de nuestro proyecto, mediante la función LoadLibrary, a la cual deberemos pasarle como parámetro la ruta completa donde se encuentre la librerı́a, y que nos devolverá un puntero a una estructura que identifica a dicha librerı́a, del tipo HINSTANCE. Deberemos hacer una declaración, mediante la palabra clave typedef, de los parámetros que tiene la función y del valor de retorno que tiene, tal y como se muestra en la figura 5.6. Como vemos, el tipo que le hemos asignado (LPFNDLLFUNC1) será el que más tarde identifique a esta función, y declararemos una nueva variable de este tipo, que será la que recoja el puntero a dicha función. Para cargar la función, haremos uso del método GetProcAddress, al cuál deberemos pasarle como parámetros el puntero que identifique a la librerı́a, y el nombre de la función que habı́amos exportado, exactamente igual que lo habı́amos definido en el archivo DEF (ver punto 5.1). El valor de retorno de este método será el que hemos declarado con anterioridad. Ahora ya podemos usar la función como si la hubiéramos declarado en el mismo documento, utilizando los parámetros y valor de retorno que habı́amos definido al principio con el typedef. Para finalizar, deberemos liberar la librerı́a que habı́amos cargado, mediante el método FreeLibrary, al cuál deberemos pasarle como parámetro el puntero a la librerı́a. 5.3.2. Forma indirecta Esta será la forma que se utilizará normalmente, ya que es la más intuitiva y la que más fácil resulta a la hora de la programación. Esta forma se basa en que cuando se ha creado una dll, también se ha creado un archivo lib y un archivo h, en los cuales se encuentran las declaraciones de las funciones que se han exportado dentro de la librerı́a dinámica. 5.4. Uso de librerı́as estáticas 85 Figura 5.6: Código de ejemplo: utilizar una DLL en VC++. Por lo tanto, cuando queramos utilizar dichas funciones, simplemente deberemos incluir al archivo lib dentro de los archivos de nuestro proyecto, y hacer una referencia al archivo h 1 dentro del archivo cpp desde el que queramos hacer uso de dichas funciones. Una vez seguidos estos pasos, podremos hacer uso de cualquiera de las funciones que se hayan exportado dentro de la librerı́a dinámica. 5.4. Uso de librerı́as estáticas Pasaremos ahora a detallar el uso de librerı́as estáticas, mucho más fáciles de utilizar en la programación y mucho más intuitivas. En primer lugar, una breve reseña de cómo crear estas librerı́as estáticas. En el menú de nuevo proyecto del VC++, elegiremos la opción de Win32 Static Library, tal y como podemos ver en la figura 5.7. 1 Mediante la instrucción #include”archivo.h”. 86 CAPı́TULO 5. PROGRAMACIÓN EN VISUAL BASIC Y VISUAL C++ Esta vez no deberemos hacer nada en especial, sino simplemente crear un archivo de cabecera, con extensión h, que contenga todas las funciones que queremos exportar. Una vez hayamos compilado el proyecto, obtendremos como salida un archivo con extensión lib, que junto con el archivo de cabecera serán los que tenemos que distribuir para usar las funciones creadas. Figura 5.7: Ventana de nuevo proyecto en VC++. Para hacer uso de las funciones exportadas en librerı́as estáticas tendremos que utilizar el programa VC++, ya que el VB no da opciones para este tipo de librerı́as. Simplemente tendremos que añadir en nuestro archivo una definición, mediante la palabra clave include, seguida del nombre del archivo cabecera que habı́amos creado con antelación. Un ejemplo serı́a el siguiente: #include "libreria.h" Además deberemos incluir a los archivos de nuestro proyecto la librerı́a que queramos usar, ya que en caso contrario la aplicación no encontrará los datos a los que se refiere el archivo de cabecera que le habı́amos indicado. Capı́tulo 6 Entorno gráfico Ya que una de las finalidades era crear un entorno gráfico ameno para el usuario de estas funciones, dedicaremos este capı́tulo a mostrar la forma que tiene dicho entorno, ası́ como la forma básica de usar dicho programa con todas las facilidades que se le han añadido. Deberemos diferenciar dos vertientes básicas del programa: reconocimiento y detección. Cada una de ellas necesitará diferentes opciones y objetos para llevar a cabo su función. Por ejemplo, en el caso de reconocimiento, deberemos tener una ventana donde se reproduzca el vı́deo y donde podamos seleccionar los ojos, para sacar la cara correspondiente y poder después obtener el modelo PCA de dicho personaje. En el caso de detección, también necesitaremos una ventana donde reproducir un vı́deo, pero de carácter meramente informativo, ya que después es el mismo programa el que se encarga de extraer los fotogramas de dicho vı́deo, obtener los candidatos y tomar una decisión sobre si el personaje que aparece en el vı́deo se corresponde o no con el nombre que le indicamos. Dentro de la parte de reconocimiento, se han creado dos programas distintos, cada uno de los cuales lleva a cabo su función. Por lo tanto, tendremos en total tres ejecutables, por lo que dividiremos este capı́tulo en tres puntos, dedicando cada uno de ellos a cada uno de los ejecutables. 6.1. FaceDemo Este programa trata de ofrecer una demostración acerca del reconocimiento de caras, sin contar para nada con el audio. Por ello, se basará en todas las funciones descritas para el vı́deo, y ofrecerá un resultado basado sólo en el vı́deo, por lo que no será tan fiable como si utilizamos ambas componentes. 87 88 CAPı́TULO 6. ENTORNO GRÁFICO Cuando abrimos el programa, la ventana principal toma una forma como la de la figura 6.1. Figura 6.1: Ventana principal del programa FaceDemo. Para empezar, deberemos abrir una secuencia de vı́deo (menú File ⇒Open). Serán válidos todos los formatos de vı́deo, no importa que sean MPEG1, MPEG2 o VOB. Una vez tengamos abierto el vı́deo, se habilitará el botón de Extract and Detect Faces, que llevará a cabo las tareas de extraer los fotogramas y detectar las caras existentes en cada uno de ellos. Cuando termine, se mostrarán a la parte derecha de la ventana los fotogramas con las caras detectadas, representadas por un cuadro blanco. En la figura 6.2 podemos ver cómo quedarı́a el programa en este punto, donde vemos a la parte derecha un fotograma en el que se han detectado dos caras, de las cuales una es un fallo (ha detectado una cara donde en realidad no existe, es una parte de cuello). Una vez hayamos terminado con esa tarea, se habilitará la lista desplegable situada debajo de los botones. En dicha lista elegiremos el personaje que queremos comprobar si está en la secuencia o no. Una vez elegido el personaje, se habilitará el botón de Face Recognition, que llevará a cabo las tareas de reconocimiento del personaje dentro de las caras detectadas. En la figura 6.3 podemos ver que ha reconocido al personaje en una de las caras que habı́a detectado. Podemos comprobarlo porque las caras que se detectan se pinta también con un cuadro, 89 6.1. FaceDemo Figura 6.2: Programa FaceDemo tras detección. pero negro. Figura 6.3: Programa FaceDemo tras reconocimiento. Además de comprobar que se ha reconocido el personaje, podemos ver la fiabilidad que nos da ese reconocimiento. Para ello tenemos la barra existente entre el vı́deo y los fotogramas representados. Se representará en ella el umbral 90 CAPı́TULO 6. ENTORNO GRÁFICO que nosotros fijemos mediante una lı́nea azul y el identificativo de Threshold (umbral). Como vemos, esta cara ha sido reconocida con bastante fiabilidad, ya que el nivel de la barra supera con creces el umbral fijado. Podremos fijar este umbral a nuestro gusto, aunque se ha comprobado que un umbral igual a 0.08 obtiene resultados con una alta fiabilidad. Podremos repetir el proceso con distintos personajes, comprobando las veces que encuentra a dicho personaje en el vı́deo y las veces que se equivoca. Aparte de estas funciones, se han añadido menús adicionales para el fácil y práctico manejo de esta herramienta. Ası́, dentro del menú Persons, tendremos las herramientas para añadir y quitar personas de la lista de personajes a reconocer. Para añadir un personaje a la lista, deberemos ir al menú Persons ⇒Add Person, y en la ventana que nos salga simplemente deberemos indicarle un archivo psn de personajes, que habrá sido creado anteriormente con el programa de creación de modelos. Para borrar un personaje, iremos al menú Persons ⇒Remove Person y se abrirá una ventana como la de la figura 6.4, en la cual elegiremos el personaje que deseamos quitar de la lista. Al aceptar nos pedirá confirmación, y si es positiva nos dará la opción de borrar los archivos de modelos del personaje. Figura 6.4: Ventana para eliminar personaje. Dentro del menú Config, podremos elegir las opciones de visualización de los fotogramas, es decir, que podremos elegir entre ver los fotogramas detectados, los fotogramas detectados y reconocidos juntos,. . . Ası́, dentro de dicho menú, tendremos dos submenús: View Frames y View Next. El primero de ellos lo utilizaremos para, una vez reconocidas las caras, intercambiar la vista entre fotogramas 6.2. Recognition 91 con caras detectadas solo (opción Detected ), o fotogramas con caras detectadas y reconocidas (opción Recognized ). Como ayuda adicional, en la parte inferior izquierda de la ventana, en la barra de estado, se irán mostrando todas las tareas que realiza el programa en cada momento. Ası́, podremos tener una idea perfecta de las tareas necesarias para cada función que queramos ejecutar. 6.2. Recognition En este programa veremos aplicadas las funciones de reconocimiento de personas, tanto a nivel de vı́deo como de audio. Por ello, este programa será mucho más fiable que el anterior, si utilizamos los dos criterios de reconocimiento (audio y vı́deo). También podremos usar este programa para el reconocimiento de audio y de vı́deo por separado, si por alguna razón es este nuestro cometido. Cuando ejecutamos el programa, veremos en primer lugar la ventana de presentación, y tomará el aspecto tal y como puede verse en la figura 6.5. En ella podemos ver los elementos principales de la aplicación. Explicaremos ahora la forma de uso de esta aplicación. En primer lugar deberemos abrir una secuencia de vı́deo, mediante el menú File ⇒Open. Podremos trabajar con cualquier tipo de formato de vı́deo, e incluso podremos trabajar con archivos de sonido del tipo WAV. Una vez abierto el archivo multimedia, deberemos elegir el criterio de reconocimiento a usar. Para ello, dentro del menú Options tendremos tres opciones para elegir: sólo vı́deo, sólo audio, o ambos, vı́deo y audio. Por supuesto, si la entrada es un archivo de vı́deo pero no tiene sonido, sólo estará disponible la opción de sólo vı́deo; igualmente si el archivo de entrada es del tipo WAV, sólo estará disponible la opción de sólo audio. A continuación, deberá pulsarse el botón de Extract-Detect, que procederá a la extracción de los parámetros del archivo de entrada. Si hemos elegido la opción de sólo vı́deo, su función será extraer los fotogramas y detectar las caras dentro de dichos fotogramas. Si hemos elegido la opción de sólo audio, su función será extraer el audio del vı́deo, adaptarlo para que pueda ser utilizado y extraer sus parámetros. Si hemos elegido la opción de utilizar ambos, primero realizará las funciones del vı́deo y a continuación las del audio. 92 CAPı́TULO 6. ENTORNO GRÁFICO Figura 6.5: Ventana principal del programa Recognition. Cuando terminen de ejecutarse las funciones anteriores, se habilitará la lista desplegable situada debajo de los botones, donde podremos indicar el personaje que queremos reconocer dentro del archivo multimedia de entrada. La elección del personaje deberá ser coherente con la elección anterior de utilizar el audio o el vı́deo, ya que deberán existir los modelos pertinentes. Es decir, que si hemos elegido la opción de sólo vı́deo, deberán existir los archivos de PCA para el personaje seleccionado. Si se cumplen los requisitos anteriores, se habilitará el botón de Recognition, que finalmente nos mostrará una pantalla indicándonos si el personaje ha sido reconocido dentro del vı́deo o no. Este reconocimiento dependerá mucho del umbral que hayamos elegido. Para elegir el umbral, deberemos mover la barra de desplazamiento nombrada como Select a threshold. Al mover dicho control, se moverá el umbral situado en la barra vertical situada al lado del vı́deo, que indicará el umbral seleccionado. Si hemos elegido la opción de reconocimiento mediante vı́deo, además de indicarnos si el personaje se encuentra o no en el vı́deo de entrada, se nos mostrará la cara que más se parece al personaje seleccionado. Podemos ver un ejemplo en la figura 6.6. 93 6.2. Recognition Figura 6.6: Programa Recognition tras reconocimiento válido. Figura 6.7: Ventana para eliminar personaje. Además, para facilitar el manejo del programa, se ha permitido la inserción y extracción de personajes de la lista. Ası́, dentro del menú Persons podremos elegir la opción de Add para añadir un personaje a la lista (seleccionando el archivo psn correspondiente al personaje a añadir). Si lo que queremos es borrar a un personaje, elegiremos la opción de Remove, con lo que se nos mostrará una ventana como la de la figura 6.7, en la que podremos elegir el personaje a borrar 94 CAPı́TULO 6. ENTORNO GRÁFICO de la lista; tras pedir una confirmación del borrado, se nos presentará además la opción de borrar los archivos de modelos del personaje. Como ayuda adicional, en la parte inferior izquierda de la ventana, en la barra de estado, se irán mostrando todas las tareas que realiza el programa en cada momento. Ası́, podremos tener una idea perfecta de las tareas necesarias para cada función que queramos ejecutar. 6.3. ModelGeneration Este programa se creó para poder crear modelos de los personajes, para poder después utilizarlos con los anteriores programas de reconocimiento. Podremos crear tanto modelos de audio como de vı́deo. Si generamos los modelos de vı́deo, se generarán los archivos correspondientes a las PCA, y si generamos los modelos de audio, se generará el correspondiente archivo gmm. Figura 6.8: Ventana principal del programa ModelGeneration. Cuando ejecutamos el programa, nos aparecerá una ventana como la de la figura 6.8, donde podemos ver las componentes principales de la aplicación: el objeto Windows Media Player donde se reproducirá el vı́deo, el objeto donde se 6.3. ModelGeneration 95 irán mostrando las caras extraı́das del vı́deo (Selected Faces), y el objeto donde se mostrarán los audios utilizados para generar el modelo de audio (Selected Audios). Para empezar a utilizar el programa, deberemos crear un nuevo personaje, o abrir uno que ya hubiéramos guardado con anterioridad. Para ello, deberemos ir al menú File ⇒New Person e introducir el nombre del personaje nuevo que deseamos crear; si lo que queremos es abrir un personaje ya utilizado anteriormente, iremos al menú File ⇒Open Person y buscaremos el archivo del personaje, que tendrá extensión psn. Una vez tengamos abierto un personaje, podremos elegir entre crear su modelo de vı́deo o de audio. Empezaremos con el de vı́deo, por lo que tendremos que ir al menú Video y elegir el submenú Open Video..., que nos dará opciones para abrir distintos formatos de vı́deo. Cuando tengamos abierto el vı́deo, deberemos ir eligiendo fotogramas de los que queremos extraer la cara. Para extraer la cara, simplemente deberemos de seleccionar los ojos. Para ello, pincharemos con el botón primario del ratón hasta que hayamos acertado en el primer ojo, y para confirmar pincharemos con el botón secundario. Repetiremos esta acción para el segundo ojo con el resultado de que la cara aparecerá en el objeto de Selected Faces, tal y como podemos ver en la figura 6.9. En dicha figura vemos como se han marcado los dos ojos y el resultado ha sido la cara de la parte derecha. Como podemos observar, se nos irá indicando el número de caras que se han extraı́do. Repetiremos el proceso hasta que tengamos un número de caras razonable; para que el modelo sea aceptable, deberemos tener al menos veinte caras extraı́das. Como facilidad adicional, se ha añadido un menú de Zoom, donde podremos cambiar el tamaño de la ventana de reproducción del vı́deo a la mitad (menú 50 % ), o por el contrario reproducir el vı́deo a su tamaño original (menú 100 % ). Esta función será útil cuando reproduzcamos vı́deos de gran tamaño, o tengamos una configuración de pantalla con baja resolución. Antes de proceder a crear el modelo, deberemos elegir las opciones pertinentes para la generación del modelo. Para ello, deberemos ir al menú Video⇒Options..., con lo que se nos abrirá una ventana como la de la figura 6.10. Los campos existentes en dicha ventana significan lo siguiente: Save...: Nos dará la opción de guardar los archivos intermedios o no. La opción de Log Files nos permitirá guardar los archivos de depuración, y la opción de Eigenfaces nos permitirá guardar como imágenes las eigenfaces 96 CAPı́TULO 6. ENTORNO GRÁFICO Figura 6.9: Ejemplo de extracción de caras. generadas. Eigenface Ratio: Valor de la varianza de los valores obtenidos. Mediante pruebas, se ha comprobado que el mejor valor es el de 0,9. Components: Pesos que daremos a las componentes Y, U, V. Por defecto, estos valores deberán tener su valor máximo, es decir, uno. Una vez elegidas todas las opciones deseadas, iremos al menú Video⇒PCA Generation, con lo que se empezarán a crear todos los archivos necesarios para generar las PCA. Este proceso puede tardar varios minutos si el número de caras es grande. Finalizado el proceso, obtendremos, dentro de la carpeta Results (dentro del directorio donde ejecutemos el programa), los correspondientes archivos de modelo del personaje, con extensión pca. Tendremos tres archivos, uno para cada componente (Y, U, V), y serán todos necesarios para su futuro uso en los programas de reconocimiento. Además, si guardamos los cambios hechos en el personaje (menú File⇒Save Person...), este modelo se guardará dentro del directorio correspondiente al personaje. Si en la ventana de opciones hemos elegido la generación de eigenfaces, tendremos ahora una opción para ver los resultados obtenidos. Iremos al menú Video⇒View 6.3. ModelGeneration 97 Figura 6.10: Opciones para la generación de modelos de vı́deo. Results, y se nos abrirá una ventana como la de la figura 6.11, en la que podemos ver los resultados correspondientes a la componente Y. Para poder ver las demás componentes, simplemente deberemos seleccionar la componente que queremos en el cuadro Components. Para que la componente que deseamos ver esté activada, deberemos indicárselo al programa con anterioridad; para ello, iremos al menú de Video⇒Result Options y elegiremos las componentes que deseamos visualizar. Una vez vistas todas las opciones para la generación de modelos de vı́deo, pasaremos ahora a los detalles de la generación de modelos de audio. Al igual que con el vı́deo, necesitaremos crear un nuevo personaje o abrir uno ya existente, mediante los menús File⇒New Person o File⇒Open Person, respectivamente. A continuación, podremos insertar archivos de audio de dos formas diferentes. Una de ellas será seleccionar un archivo de audio en formato WAV. Para ello, iremos al menú Audio⇒Insert new audio, y en la ventana que nos aparezca elegiremos el archivo WAV que deseemos añadir. Ası́, este fichero nos aparecerá en la lista de Selected Audios, con un indicativo del lugar que ocupa en la lista (valor meramente informativo, sin ningún significado adicional). La otra opción para insertar audio será mediante la utilización del audio de un archivo de vı́deo; es decir, que si tenemos a un personaje grabado en una secuencia de vı́deo, en la cual tenemos imagen y audio, podremos aprovechar dicho audio para generar el modelo. Para ello, una vez tengamos un vı́deo abierto, se habilitará el menú Audio⇒Insert this audio, mediante el cual añadiremos el audio 98 CAPı́TULO 6. ENTORNO GRÁFICO Figura 6.11: Visualización de los resultados de la PCA. a la lista de Selected Audios. Cuando elegimos esta opción, podremos indicar el momento inicial y el final que queremos que se utilice para el modelo. Para ello, tenemos los botones de Tin y Tout, mediante los cuales podremos seleccionar el instante inicial y el final, y que se mostrará más tarde en la lista de Selected Audios, al lado del nombre del vı́deo abierto. En la figura 6.12 podemos ver un ejemplo, donde se han añadido tres audios: el primero es un fichero WAV, el segundo todo el audio de un archivo de vı́deo MPEG, y el tercero una parte del vı́deo MPEG abierto en el momento. Una vez seleccionados todos los audios que consideremos necesarios para la creación del modelo, iremos al menú Audio⇒Model Generation, con lo que empezarán a llevarse a cabo todas las funciones necesarias que desembocarán en la creación de un modelo de audio, de extensión gmm, y que se almacenará en el directorio Results, situado en el directorio donde se haya ejecutado el programa. Además, si guardamos los cambios hechos en el personaje (menú File⇒Save Person...), este modelo se guardará dentro del directorio correspondiente al personaje. 6.3. ModelGeneration Figura 6.12: Ejemplo de la inserción de audios. 99 100 CAPı́TULO 6. ENTORNO GRÁFICO Capı́tulo 7 Conclusiones Como conclusiones apuntaremos que se han llevado a cabo con éxito todos los objetivos planteados en este proyecto, ya que además de crear un entorno gráfico amigable para poner en funcionamiento todas las funciones de reconocimiento, se ha creado una librerı́a adicional con un conjunto de funciones de utilidad para el tratamiento de archivos multimedia. Con respecto a la tecnologı́a utilizada, hemos comprobado que el DirectX no ha cumplido con todas las expectativas que se le habı́an atribuido en un principio, basándonos en su documentación. Se ha comprobado que el uso del DirectX sólo es provechoso en el tratamiento de vı́deos MPEG1. Por lo tanto, para la extracción de fotogramas de los demás formatos se ha debido hacer uso de librerı́as externas, como el MPEG2Lib en el caso de los vı́deos MPEG2 y VOB. Además se ha comprobado que las funciones no son válidas en todas las versiones de Windows, ni llevan a cabo las funciones que deberı́an ejecutar, difiriendo mucho de lo se muestra en la documentación. Con respecto a los lenguajes de programación usados, hemos comprobado que poseen una gran potencia cuando se utilizan conjuntamente, ya que podremos crear un entorno gráfico completo con el VB y poder hacer uso de todas las ventajas que posee el VC++. El uso de las DLL facilita en gran medida la comunicación entre plataformas, ya que permite comunicar el MATLAB con el Visual C++, y el Visual C++ con el Visual Basic, con el que realmente creamos el entorno gráfico. Con respecto a los resultados, decir que se ha intentado crear un entorno gráfico cuyo uso fuera lo más fácil posible, con menús y botones de uso intuitivo. También se ha intentado que las librerı́as creadas contengan un conjunto de funciones muy útiles en cuanto al tratamiento de ficheros multimedia, y además que fueran de un uso muy intuitivo. 101 102 CAPı́TULO 7. CONCLUSIONES Apéndice A MATLAB Cuando llegamos a la parte relacionada con el audio, todas las funciones que nos extraen los parámetros, comprueban los modelos,... están programadas r ya que es un lenguaje de programación en el que se pueden en MATLAB °, controlar mucho mejor los archivos de audio, matrices y todo lo necesario para obtener y comprobar los modelos del audio de las personas. Hasta este punto todo perfecto, pero el problema viene cuando tenemos que integrarlo todo, ya que deberemos insertar estas funciones de reconocimiento de audio en el proyecto global, en el que hemos trabajado con BASIC y C++. En este momento aparece el compilador de MATLAB (mcc). Este compilador nos permite pasar el código M, escrito en el lenguaje de programación de MATLAB, al lenguaje C o C++, que será el que realmente nos interese a nosotros para crear librerı́as dinámicas, que después llamaremos desde la interfaz gráfica del programa. A.1. Compilador de MATLAB Los ficheros en C resultantes de la compilación pueden usarse en cualquiera de los tipos de fichero ejecutables, como por ejemplo MEX, ejecutables (exe), o librerı́as. A nosotros nos interesará el crear aplicaciones independientes (standalone applications), que serán usadas después para crear las DLLs necesarias. Las principales razones para compilar los archivos M mediante el compilador de MATLAB son las siguientes: Para crear aplicaciones independientes o librerı́as compartidas en C (DLLs). 103 104 APÉNDICE A. MATLAB Para ”esconder”el código que hemos programado en MATLAB. Para aumentar la velocidad de ejecución de los programas creados. En el primer punto lo que pretendemos es poder crear aplicaciones en MATLAB, que se aprovechen de las ventajas de las funciones matemáticas del MATLAB, pero sin el requerimiento de tener instalado este programa para poder ejecutarlos. Las aplicaciones independientes son la forma más conveniente de aprovechar la potencia del MATLAB y distribuir ejecutables a los usuarios. Con respecto a la ocultación del código creado, nos será de gran utilidad esta herramienta, ya que los ficheros M son ficheros de texto ASCII, que cualquiera puede ver y modificar. Sin embargo, si compilamos ese código, lo que nos resulta es un ejecutable que contiene datos binarios, por lo que es imposible la modificación de los algoritmos que hayamos creado en ese programa, y además también será imposible la copia de dichos algoritmos. El aumento de la velocidad de ejecución de los programas compilados frente a sus equivalentes M se debe a que el código compilado normalmente se ejecuta más rápidamente que el código interpretado, que es lo que hace el MATLAB. Además, en C++ se puede liberar memoria en cualquier momento, si es que se sabe que no va a usarse más; en MATLAB no podremos hacerlo por diversos motivos. Además, los bucles se ejecutarán mucho más rápido en el código compilado que en MATLAB. A.2. Generación de código Aquı́ podremos ver los ficheros que se generan tras la compilación, es decir, los ficheros cabecera, las funciones de interfaz, librerı́as en C o C++,... Cuando se usa el compilador de MATLAB para compilar los ficheros M, se generan los siguientes ficheros: Código C o C++, dependiendo del lenguaje que hayamos especificado (con la opción -L) Fichero de cabecera Fichero de datos (lib, dll, mex,...), dependiendo de la opción elegida (-W) A.2. Generación de código 105 El código C o C++ que genera el compilador y el fichero cabecera son independientes del fichero ejecutable que vayamos a crear después, incluso de la plataforma en la que vaya a ser usado dicho código. El fichero de datos será el que proporcione el código necesario para soportar el tipo ejecutable de salida. Por lo tanto, el fichero de datos es diferente para cada tipo de fichero ejecutable. Como hemos dicho, el compilador de MATLAB nos ofrece una amplia variedad en los formatos de salida, pero el que nos interesa a nosotros y el que hemos empleado en el proyecto es la librerı́a. Con esta opción, tendremos como salida varios ficheros, entre ellos los ficheros C y los ficheros H, uno por cada fichero M que hayamos introducido en la orden de compilación. En estos archivos tendremos todo el código compilado, por si necesitamos llevar a cabo algún tipo de modificación. También tendremos como salida un archivo con estensión exports, el cual nos indicará todas las funciones que tenemos disponible en la librerı́a que se ha creado, y otro archivo con extensión mlib, que también nos aportará información acerca de las funciones que se exportan. Y por último, tendremos la librerı́a en sı́ misma, es decir, un archivo con extensión lib, que será el que después usemos para llamar a las funciones que tenı́amos en el archivo original M. Para ello, deberemos primero cargar la librerı́a mediante unas funciones que ya nos aporta la compilación (generalmente la llamada a esta inicialización será el nombre de la función que tenı́amos en el archivo M, al que añadiremos la palabra libInitialize, sin ningún parámetro y sin ningún valor de retorno), por lo que deberemos incluir en el proyecto el fichero cabecera principal que nos saque la compilación. A partir de entonces ya podremos usar las funciones tal y como las usábamos en MATLAB. Por supuesto, deberemos cerrar la librerı́a cuando terminemos de usarla; para ello, haremos lo mismo que al inicializarla, pero sustituyendo la palabra libInitialize por libTerminate. Pondremos un ejemplo práctico para que quede totalmente clara esta explicación. Supongamos que tenemos una función en MATLAB llamada prueba. Los ficheros que usaremos son el pruebalib.lib y el pruebalib.h. Cuando vayamos a inicializar la librerı́a, llamaremos a la función pruebalibInitialize(). Para usar la función, lo haremos con el nombre mlfPrueba, con los argumentos oportunos. Por último, para cerrar la librerı́a llamaremos a la función pruebalibTerminate(). 106 APÉNDICE A. MATLAB A.3. Optimizaciones El compilador de MATLAB también permite el aplicar optimizaciones en el código fuente del fichero M, lo que desembocará en que el código generado en C/C++ sea mucho más rápido que el código original M, o que el código creado sin ningún tipo de optimización. En general, los distintos tipos de optimizaciones son los siguientes: Manipulación de arrays escalares o no escalares. Contenido de arrays de una o dos dimensiones. Contenido de bucles que empiecen y se incrementen con enteros. Contenido de expresiones condicionales donde los operandos son enteros. Únicamente no elegiremos ninguna optimización en el caso de que estemos depurando nuestro código o en el caso en que se quiera mantener la legibilidad de nuestro código. Generalmente, todas las optimizaciones están activadas por defecto, por lo que aunque no lo sepamos, estaremos usando dichas optimizaciones. Sin embargo, para estar seguros de que realmente están activadas, simplemente deberemos ejecutar la orden de MATLAB siguiente: mcc -O list y directamente se nos mostrará por pantalla todas las optimizaciones disponibles, y si realmente están activadas o no. Si alguna de las optimizaciones no está activada, deberemos activarla, y en el caso de que no deseemos hacerlo deberemos saber muy bien que nuestro programa no va a usar funciones que hagan uso de esas funciones (por ejemplo, si nuestro programa no utiliza ningún tipo de bucle, aunque la optimización de los bucles no esté activada no acarreará ningún tipo de problema). Como opinión personal, recomiendo que estén todas las optimizaciones activadas, ya que ası́ nos evitaremos problemas en el código compilado, con respecto a la velocidad del programa. Apéndice B Tabla de conversiones entre VB y VC++ El intercambio de datos entre el Visual C++ y el Visual Basic, con respecto a las variables, no es inmediato. Existen una serie de cambios que deberemos tener en cuenta. En la tabla se muestran los tipos de datos de C++ más comunes y su equivalente en Visual Basic. Tipos de datos del lenguaje C++ Declarados en Visual Basic como Llamados con ATOM ByVal variable As Integer Una expresión que da como resultado un tipo de datos Integer BOOL ByVal variable As Long Una expresión que da como resultado un tipo de datos Long BYTE ByVal variable As Byte Una expresión que da como resultado un tipo de datos Byte CHAR ByVal variable As Byte Una expresión que da como resultado un tipo de datos Byte 107 108 APÉNDICE B. TABLA DE CONVERSIONES ENTRE VB Y VC++ Tipos de datos del lenguaje C++ Declarados en Visual Basic como Llamados con COLORREF ByVal variable As Long Una expresión que da como resultado un tipo de datos Long DWORD ByVal variable As Long Una expresión que da como resultado un tipo de datos Long HWND, HDC, etc. (controladores de Windows) ByVal variable As Long Una expresión que da como resultado un tipo de datos Long INT, UINT ByVal variable As Long Una expresión que da como resultado un tipo de datos Long LONG ByVal variable As Long Una expresión que da como resultado un tipo de datos Long LPARAM ByVal variable As Long Una expresión que da como resultado un tipo de datos Long LPDWORD variable As Long Una expresión que da como resultado un tipo de datos Long LPINT, LPUINT variable As Long Una expresión que da como resultado un tipo de datos Long 109 Tipos de datos del lenguaje C++ Declarados en Visual Basic como Llamados con LPSTR, LPCSTR ByVal String Una expresión que da como resultado un tipo de datos String LPVOID variable As Long Cualquier variable (se utiliza ByVal si se pasa una cadena) LRESULT ByVal variable As Long Una expresión que da como resultado un tipo de datos Long NULL ByVal variable As Long ByVal Nothing o vbNullString SHORT ByVal variable As Integer Una expresión que da como resultado un tipo de datos Integer VOID Sub procedimiento No aplicable WORD ByVal variable As Integer Una expresión que da como resultado un tipo de datos Integer WPARAM ByVal variable As Long Una expresión que da como resultado un tipo de datos Long variable As 110 APÉNDICE B. TABLA DE CONVERSIONES ENTRE VB Y VC++ Bibliografı́a [1] Alberto Albiol Colomer. Face Detection for Pseudo-Semantic labeling in Video Databases. Tesis Doctoral, Universidad Politécnica Valencia, 2003. [2] DVD2AVI. http://arbor.ee.ntu.edu.tw/ jackei/dvd2avi. [3] Joan L. Mitchell et al. MPEG video compression standard. Digital Multimedia standards series, New York, 1996. [4] MPEG Simulation Software Group. http://www.mpeg.org/MPEG/MSSG. [5] The MathWorks. MATLAB Compiler User’s Guide. Versión 2.1, Septiembre 2000. [6] The MathWorks. MATLAB C++ Math Library User’s Guide. Versión 2.3, Octubre 2001. [7] Microsoft. Microsoft Development Network Library (MSDN). [8] Microsoft. Microsoft DirectX 8.1 SDK (Software Development Kit). [9] Mpeg2Lib. http://logicnet.dk/lib. [10] Brian Siler & Jeff Spotts. Edición Especial Visual Basic 6. Prentice Hall, Madrid, 1999. [11] Beck Zaratian. Microsoft Visual C++ 6.0: Manual del programador. McGraw-Hill / Interamericana de España, 1999. 111