Diseno de Bibliotecas de Entrada y Salida de Imagenes Extensibles
Las técnicas del procesamiento de imágenes gráficas por computador, han tenido aplicaciones útiles en campos tan diversos como la visión por ordenador, los sistemas de información geográfica, la proyección de imágenes médicas, entre otras. Estas aplicaciones proveen a los investigadores nuevas maneras de visualizar sus datos y de profundizar en su estructura. Su diseño también plantea importantes desafíos a los Ingenieros de Software . Tal desafío se refiere a la entrada y salida de información: Cómo pueden estos sistemas de software ser diseñados de modo tal, que trabajen con los datos almacenados en una gran variedad de diversos formatos en un disco dado?
Al principio, la solución puede parecerse bastante fácil: decídase sobre algunos formatos utilizar, y el código de algunas rutinas para manejar esos formatos. Este acercamiento puede ser válido para las aplicaciones que son pensadas para el uso de un solo grupo que trabaja en un problema muy específico. Sin embargo, estos posibles escenarios no se presentan frecuentemente. En los diversos grupos de investigación, diseminados través de sus respectivas instituciones alrededor del mundo, cada equipo ha desarrollado a menudo un genuino interés en su propios formatos de archivos. Aún cuando todos estos equipos decidieran un formato de archivo común, ellos perderían la capacidad de utilizar sus sistemas de datos existentes al tratar con un nuevo código base, a menos que programen las rutinas necesarias para su conversión.
Todos estos factores hacen que este acercamiento simplista sea un tanto inadecuado. Por lo tanto, es necesario desarrollar un framework para la entrada y salida de información, específicamente cuando se trata de imágenes. En consecuencia, hemos desarrollado un framework, como parte del proyecto “INSIGHT: El Kit de Herramientas para el Proceso de la Imagen Visible del Cuerpo Humano", financiado por el National Library El diseño de nuestro framework para la entrada y salida de imágenes, y los detalles de nuestra implementación en C++ son presentados en este artículo.
Un buen diseño para una biblioteca de entrada-salida de imagen (ImageI/O) debería tener en cuenta los siguientes criterios:
Extensibilidad para el soporte de los nuevos tipos de archivos que pueden ser agregados por terceros sin tener que modificar el código existente. No es posible predecir qué formatos adicionales de la imagen se presentarán en el futuro, ni tampoco es factible quienes proporcionarán el soporte adecuado para todos los formatos existentes. Por lo tanto, es esencial que la entrada y salida de la imagen del Framework sea “abierta en la extensión, pero cerrada a la modificación" [ 5 ]. Esta meta se resuelve con el uso de un modelo de diseño compuesto conocido como la fábrica de los objetos “enchufables” (pluggable object factory), y definiendo una interfaz estándar a través de uno de los superclass abstractos. La fábrica del objeto proporciona a un mecanismo para intercambiar fácilmente una clase (o familia de clases) con otros, a condición de que los dos se deriven de la misma superclass (base). La fábrica de los objetos “enchufables” amplía esta noción permitiendo que estos objetos se enchufen o sean agregados, en nuevas subclases, como un “plug-in” en framework .
Transparencia - idealmente, los programadores de aplicaciones deberían poder pasar un nombre de archivo a una clase apropiada y después tener acceso a los datos de la imagen guardados en el archivo, sin tener que preocuparse de los detalles de un formato determinado. La transparencia se alcanza a través de dos mecanismos. Primero, presentan características comunes a todos los formatos de imagen donde se “descomponen en factores" , y donde los miembros de estos superclass abstractos, se derivan en subclases de formatos específicos. De esta manera, un programador de aplicaciones puede trabajar con cualquier imagen utilizada a través de una interfaz común. El segundo mecanismo, es con la fábrica del objeto enchufable la cual se puede utilizar automáticamente al instantiar (crear) la subclase correcta que maneja un archivo dado. Los detalles de este mecanismo son dados mas adelante.
Hay dos conceptos claves que el lector debe tener presente antes de proceder. Primero, hemos separado la noción de una imagen, donde la que es guardada en un disco es distinta al de una imagen en memoria. Ya que las aplicaciones deben trabajar con imágenes en memoria, utilizamos una clase intermedia, llamada FilterIOToImage, para convertir entre ambas representaciones. En segundo lugar, hemos utilizado un modelo del diseño del software conocido como la fábrica de los objetos enchufables cuyos “plug-ins” sirven para crear dinámicamente, la subclase apropiada que ocupa un archivo en un formato determinado. Siempre será necesaria tener un soporte para un nuevo formato de archivo, donde la subclase de ImageIO se encarga de manejar los detalles de la implementación del nuevo formato. Además, una fábrica que puede producir casos de esta nueva subclase también es creada y registrada en un diccionario principal. Este diccionario principal se puede entonces utilizar para seleccionar, durante el tiempo de ejecución, la subclase apropiada de ImageIO al instantiar o crear un nombre de archivo determinado. A continuación presentamos los detalles de la jerarquía de los tipos de clases usados para poner este framework en ejecución.
Estructura de la Clase 
Cuadro 1. Jerarquía de la
Clase
En el diagrama superior, cada clase se muestra como un rectángulo. Se ponen en letra itálica los nombres abstractos de la clase, pero no en los nombres concretos de la subclase. Las líneas que terminan con puntas de flecha indican las relaciones intrínsecas ("es una"), mientras que las otras líneas que terminan con puntas de diamantes indican relaciones de agregación ("tiene una").
ImageIO es el corazón de la biblioteca. Esta clase abstracta maneja las imágenes guardadas en la unidad del disco, y define la interfaz que los programadores de aplicaciones pueden utilizar para interactuar con la biblioteca, el cual proporciona los métodos para cargar y guardar las imágenes, así como leer cualquier información pertinente acerca de un archivo (dimensiones de la imagen, profundidad de color, etc.). Nótese que además del valor por defecto de los métodos Load() y de Save(), que nos permiten definir versiones especializadas para utilizar estos métodos, por ejemplo, formatos de imágenes bidimensionales (2D) para así ensamblar volúmenes tridimensionales (3D), y extraer una sola rebanada o un rango de rebanadas de una imagen tridimensional.
class ImageIO {
public:
private: void* imageData; class ImageIOJpeg { string
GetSupportedFileExtensions() const { return ".jpg"; }
);
virtual void Load() = 0;
virtual void Save() = 0;
virtual void Save2DSlice(int sliceNumber) = 0;
virtual void Assemble3DVolume() = 0;
/* This method returns the file extensions
corresponding to the filetypes that this class can handle */
virtual string GetSupportedFileExtensions() const = 0;
public:
El mienbro imageData es aquel puntero (pointer) que sirve para anular, de modo que se puedan salvar los datos de las imágenes de cualquier tipo. El puntero sirve para cargar y guardar los métodos según el tipo de datos apropiado. Por ejemplo, ImageIOJpeg trata los datos sin caracteres, porque ése es el tipo de datos de imágenes para los tonos grises del formato JPEG
Factory <Key, Object> es la base de la clase “enchufable” de la fábrica del objeto. La fábrica del objeto enchufable [ 2 ] es esencialmente una composición de varios modelos de diseño ya descritos [ 4 ]: la Fábrica Abstracta, nos proporciona un mecanismo para la creacion de clases de una jerarquía; el Simpleton, nos asegura de que exista solamente un caso de una clase determinada y que sea accesible globalmente; y que se deriva en el Prototipo, que nos sirve para especificar la fábrica abstracta de los objetos plug-ins de otras como las fábricas concretas. Esta fábrica mantiene un diccionario, que asocia las palabras claves con cada objeto (definiciones). Se creanparámetros (templados) segun el tipo de clave y el tipo de objeto.
template <class
Key, class Object>
class Factory<Key, Object> {
public:
static Object* Create (const Key k) {
Factory<Key, Object>* f =
(dictionary->find(k))->second;
if (!f) return NULL
else return f->MakeObject();
}
protected:
Factory(const Key k) {
static bool dictionaryInitialized = false;
if (!dictionaryInitialized) {
dictionary = new Dictionary;
dictionaryInitialized = true;
}
dictionary->insert(std::make_pair(k, this);
}
virtual Object* MakeObject() const {
/* implemented by subclasses. return instance of
object that subclass is responsible for */
}
private:
typedef Factory<Object>* FactoryPtr;
typedef std::map<Key, FactoryPtr> Dictionary;
static Dictionary* dictionary;
};
El miembro público Create() es el único método visible para el programador de aplicaciones. Su unico parámetro es una letra o clave, que se utiliza para poner en un índice en el diccionario interno. Si la clave se encuentra en el diccionario, la fábrica correspondiente crea entonces un objeto. El diccionario es la variable estática de este miembro. En consecuencia, un objeto del diccionario se comparte entre todos los casos concretos de la fábrica del objeto.
El constructor para la Fábrica Abstracta
toma un clave como parámetro, y los inserta en el diccionario, al asociar esta
clave y la subclase (Fábrica Concreta) que invoca este constructor (cuyo codigo
en C++ provee informacion para referirse a si mismo). Nos referiremos al
proceso de registro de las fábricas
concretas que se enumeran en el diccionario principal.Observese que el uso de la variable boleana estática, como
el dictionaryInitialized
dentro del constructor. Se utiliza esta variable
mientras el indicador asegura de que el diccionario se está inicializado
solamente una vez, momentos antes de que la primera fábrica concreta intenta
registrarse a si mismo. Esto es necesario para respetar el estricto orden de
inicializaciones a través de los diversos fuentes de archivos ordenados,porque
varía entre las distintas plataformas y los compiladores, de lo contrario no se
podría saberse anticipadamente. En consecuencia, siempre va a ser necesario de
que este proceso se efectue para asegurarse de que el diccionario esté
inicializado antes de realizar el registro.
Entonces el FactoryIO<string, ImageIO> de la Factory class,o clase Fabrica que toma la ventaja del hecho de que cada archivo tendrá una extensión única para identificar el formato (ej. bmp, jpg, png, etc.), y por lo tanto pueden ser usados como claves.
De la clase FactoryIO se derivan alternadamente, una fábrica concreta separada para cada formato, y donde cada uno de estas fábricas solamente es responsable de crear casos de su clase correspondiente a la ImageIO. Más adelante, daremos un ejemplo de nuestro código para cargar archivos de imágenes en formato JPEG.
class FactoryIOJpeg
: public FactoryIO {
public:
FactoryIOJpeg() :
FactoryIO(myObject.GetSupportedFileExtensions()) {}
static const ImageIOJpeg myObject;
ImageIO* MakeObject() const { return new ImageIOJpeg; }
static const FactoryIOJpeg registerMyself;
Hay dos puntos claves que hay que observar en la implementación de esta fábrica concreta. Primero, cada fábrica concreta contiene un caso estático de sí mismo, usado solamente para los propósitos de registrar la fábrica en el diccionario. En segundo lugar, cada fábrica concreta también tiene un caso estático del objeto de la clase ImageIO que es responsable de su duplicación. Esto se utiliza simplemente para determinar los valores dominantes o claves para utilizarlos en el proceso de registro.
Image<PixelType>es la clase que utilizamos para mantener una imagen en la memoria. Sus detalles como el diseño y la implementación no son importantes para esta discusión. El lector simplemente debe observar que la clase está dada por los parámetros o variables del tipo de datos que los pixeles tienen (pixel’s datatype), con los cuales la aplicación pueda funcionar. Éste no es necesariamente igual que el tipo de datos de la imagen guardada en el disco.
El componente final de este framework es la clase FilterIOToImage. <pixeltype>Hay que recordar que debemos distinguir entre las imágenes que residen en disco y las que residen en la memoria del ordenador, ya que ambas clases son excluyentes entre sí, debemos utilizar una clase intermediaria del filtro para traducir reciprocamente los datos de la imagen entre ambas, y es donde la clase FilterIOToImage realiza exactamente esa función.
class
FilterIOToImage<PixelType> {
public:
FilterIOToImage(const char* fileName,
Image<PixelType>* image);
FilterIOToImage(Image<PixelType>* image, const char* fileName);
virtual void CopyPixelsToIO();
virtual void CopyPixelsToImage;
protected:
ImageIO* myIO;
Image<PixelType>* myImage;
static const Factory<ImageIO> myIOFactory;
};
La clase
FilterIOToImagecontiene una referencia a un
objeto de ImageIO y una referencia a un objeto de Image. Observese que los
objetos FilterIOToImage e Image, están ambas parametrizadas en el PixelType. De
manera de que el anterior tenga conocimiento del último parámetro deltemplate.
Como los nombres indican,
CopyPixelsToIO() y CopyPixelsToImage() estos traducen datos de la imagen a
partir de una representación a la otra. Derivando de la clase intermedia
FilterIOToImage, esta traducción puede ser cualquier cosa desde un simple
escrito hasta las operaciones más complejas tales como conversión de los tonos
de grises. Notese que también hemos definido dos constructores que toman los
nombres de los archivo e imágenes como parámetros. El primer constructor se
encarga de cargar la imagen y transfiriendo los pixeles a la imagen pasada como
parámetro, mientras que el segundo constructor transfiere los pixeles desde
la imagen para después guardarlos al disco. Ambos constructores crean el
objeto apropiado de ImageIO, analizando la extensión del nombre del archivo (fileName)
y después procede a
cargar o guardar la imagen usando este objeto. Definiendo a estos
constructores, podemos presentar una simple interface para la biblioteca y que
es apropiada para la mayoría de tareas de la entrada y salida de imágenes
(véase el ejemplo abajo). Sin embargo, el constructor del por defecto (no mostrado)
está también disponible para los programadores así que ellos puedan tener
control total sobre este proceso de entrada y salida.
En muchos casos, el hacer la subclase ImageIO implica el interconectar con bibliotecas escritas en C por terceras personas. Este será el caso para los formatos comúnmente usados en imágenes, pues las buenas bibliotecas de bajo-nivel en C existen para casi todos, y tiene muy poco sentido reescribir estas rutinas. Siempre y cuando, que tengamos este código existente escritos en nuestras clases de ImageIO, para que efectivamente podamos envolver la interface proporcionada por estas bibliotecas de terceros. Usando la directiva externa de "C", podemos mezclar los códigos de C y C++, que a través del reordenamiento de la biblioteca de terceros junto con nuestras propias clases en C++ , y así crear nuevas bibliotecas apropiadas para su uso en este framework.
Es más fácil compilar bibliotecas nuevas con el reordenador temporal o compile time linking. Sin embargo, sugerimos el uso de las bibliotecas de conexión dinámicas (DLL) en lugar de otros. Los DLL o Dynamic Link Libraries son bibliotecas que se escriben y se ordenan para ser cargadas en el tiempo real de ejecución de una aplicación. Por lo tanto, pueden ser cargadas y ser descargadas a pedido. Además, si más de un programa solicita una biblioteca dada, entonces una sola copia de esta biblioteca se carga una vez en la memoria y los programas múltiples comparten esta sola copia de la biblioteca dando por resultado una huella más pequeña en la memoria para estas aplicaciones. Es posible definir una Clase de alto nivel que ignore los los detalles específicos de la plataforma al cargar e importar los símbolos de las bibliotecas de conexión dinámicas (DLL). Con esta Clase en su lugar, es posible colocar las bibliotecas nuevas que utilizan tipos de archivos adicionales en un solo directorio que se exploran al inicio del programa. Este pequeño cambio elimina la necesidad de de reiniciar las aplicaciones existentes al agregar el soporte para los nuevos tipos de imagen. Además, si se encuentra un tipo de archivo sin soporte, en vez de volvera anularse a través del puntero NULL para indicar esta condición, podemos explorar un camino adicional de la biblioteca en tiempo real para ver si existe un DLL que utilice de hecho, este tipo de archivo. De ser posible, el DLL puede ser importado y la fábrica del objeto para este tipo de la imagen será ragistrada dinámicamente en el diccionario permitiendo que la aplicación repita la solicitacion de un objeto para manejar este tipo de archivo.
El programa de muestra siguiente ilustra el interfaz simplificado presentado al programador de la aplicación:
void main () {
FilterIOToImage<float>* loadFilter,
saveFilter;
Image<float>* image = NULL;
loadFilter = new FilterIOToImage<float>("input.jpg", image);
// Start processing image
...
// Done processing image. Save result
saveFilter = new FilterIOToImage<float>(image, "output.bmp");
En este primer ejemplo simplemente se carga una imagen para procesarla y después guardarla de nuevo en el disco con formato distinto. Ya que los archivos del JPEG y del bmp son intrínsecamente bidimensionales (2-D), la carga por defecto y los métodos de almacenamiento son adecuados para la imagen que esta siendo procesada, y por lo tanto puede ser usada la interface simplificada de la biblioteca. La carga y el guardado de la imagen son procesos muy intuitivos y requieren una sola declaración para cada operación.
Este ejemplo siguiente, ilustra la interface estándar disponible para el programador, para cuando se necesita un mayor control de los procesos:
void main () {
FilterIOToImage<float>* loadFilter,
saveFilter;
Image<float>* image;
const FactoryIO ioFactory;
ImageIO* loadIO, saveIO;
char inputFileName[] = "inputSlice*.jpg";
char outputFileName[] = "outputSlice.bmp";
loadIO = ioFactory.Create(ParseFileExtension(inputFileName));
loadIO->SetFileName(inputFileName);
loadIO->Assemble3DVolume();
loadFilter = new FilterIOToImage<float>(loadIO, image);
loadFilter->CopyPixelsToImage();
// Start processing image
...
// Done processing image. Save result
saveIO = ioFactory.Create(ParseFileExtension(outputFileName));
saveIO->SetFileName(outputFileName);
saveFilter = new FilterIOToImage<float>(saveIO, image);
saveFilter->CopyPixelsToIO();
saveIO->Save2DSlice(3);
En este caso, el programa primero ensambla un volumen tridimensional, a partir de una serie numerica de rebanadas que estan secuencialmente guardadas como imágenes en formato JPEG. Después de procesarlos, una sola rebanada de ese volumen se guarda en disco como archivo en formato bmp. Aunque algunas líneas adicionales del código son requeridos para crear los filtros y para transferir los datos de la imagen entre las dos representaciones, el framework subyacente sigue siendo razonablemente transparente y cómodo.
La gran desventaja de este framework tiene que ver con la eficiencia.La clase ImageIO confía a menudo en una biblioteca de terceros para manejar los detalles de un formato determinado, y tal biblioteca mantendrá probablemente un almacenador intermedio interno (internal buffer) para guardar la imagen. Entonces, la ImageIO alternadamente copiará este almacenador intermedio en su propio Buffer del imageData. Finalmente, el FilterIOToImage copiará y traducirá los pixeles sobre el objeto Image. Tres copias de los mismos datos se utilizan antes de que sea finalmente usado por la aplicación. Esto puede convertirse en un problema al manejar imágenes muy grandes. Si la aplicación requiere mas memoria, entonces las operaciones de entrada-salida pueden llegar a ser muy lentas. Afortunadamente, este lastre se compensa, debido a que es una operación de entrada-salida relativamente poco frecuente; y generalmente, una imagen se carga al iniciarse un programa, y entonces queda guardado en el disco una vez se acaba el proceso. Además, podemos eliminar una de estas copias si modificamos el diseño de la clase de ImageIO para utilizar un puntero vacío externo (NULL pointer)como buffer o almacenador intermedio, en vez de usar uno interno propio.
Con respecto de la advertencia a la clasificación de bytes: el asunto del llamado "pequeño-endian vs. el grande-endian” [ 1 ]. Algunas configuraciones arquitectónicas del ordenador tratan con el bit que está más a la izquierda de un byte (o byte de un Word) como el más significativo, mientras que otras configuraciones de ordenadores tratan con el bit o byte que está más a la derecha. Por lo tanto, es necesario realizar una conversión entre ordenar usando el archivo y ordenar usando la memoria de computadora. Este tipo de desperfecto dará lugar que las imágenes que aparezcan correctamente en un tipo de máquina. Hemos dejado este asunto que se resuelva a través de las bibliotecas de imágenes de terceros, que interactuan con la ImageIO de nuestro Framework, y la experiencia nos ha mostrado eso, y que por lo menos para los tipos de imágenes que hemos encontrado, tales como las bibliotecas que realizan una apropiada conversión.
En resumen, las ventajas que este framework proporciona tanto en la forma de su extensibilidad como en la facilidad de mantención, compensan con creces las limitaciones descritas más arriba.Los soportes para los nuevos tipos de imágenes se pueden agregar a través de terceros, y son integradas durante la ejecución de las aplicaciones por los DLL.Las clases de I/O se guardan totalmente apartados del resto de la aplicación, para poder así reutilizarlas en otros programas.La biblioteca se escribe en código totalmente en C++, que se ejecutan en Windows y varias variantes del sistema Unix con solamente una recompilación requerida. Finalmente, la interface que hemos definido en la base de la clase ImageIO agrega flexibilidad permitiendo a formatos de imágenes bidimensionales(2D) ser utilizado en un sentido tridimensional (3D), y viceversa..
Este trabajo cuenta con el apoyo del NLM (contrato N01-LM-0-3501) como parte de los esfuerzos en el desarrollo de “INSIGHT: El Kit de Herramientas para el Proceso de la Imagen Visible del Cuerpo Humano", (Insight: The Visible Human Image Processing Toolkit).
Biografia
Parag Chandra es un estudiante graduado, ayudante de investigación de la Universidad de Carolina del Norte (E.U.A.) en donde él está persiguiendo su Master en Informática y está ayudando en la investigación en el registro de imágenes médicas. Recientemente graduado del Georgia Tech, tras 6 años de experiencia como Ingeniero de Software en el uso de tecnologías para los ámbitos industrial y académicos.Actualmente se encuentra en vías de graduarse durante la primavera del 2001.
Dr. Luis Ibáñez recibió en 1989 el grado de Licenciado en Ciencias Física en la Universidad Industrial de Santander (Colombia). Durante el año 2000 obtuvo el grado de Doctor en el Procesamiento de Señales en la Universite de Rennes I (Francia) . Actualmente es profesor ayudante de investigación en la Universidad de Carolina del Norte. Sus intereses incluyen el procesamiento de imágenes médicas, la cirugía con imágenes tele-dirigidasy la morfogénesis biológica.
Last Modified: