Publicado el Deja un comentario

Primeros pasos con OpenCV

En este post, vamos a hablar sobre algunos conceptos fundamentales para iniciarse a OpenCV y al procesamiento de imágenes. ¡Comencemos!

¿Qué es una imagen bajo una perspectiva computacional?

Todos podríamos dar una definición más o menos buena de lo que es una imagen. Pero, ¿de qué forma puede un ordenador representar una imagen?

Imágenes en escala de grises

Comencemos con el ejemplo más sencillo, el de una imagen monocanal, como puede ser una imagen en escala de grises. Este tipo de imágenes, a nivel computacional, se representan como una matriz de números, sin más complicaciones. Estas matrices son bidimensionales, ya que una dimensión será el ancho y, la otra dimensión, será el alto. En cada una de las posiciones de esa matriz, hay un número que representa el nivel de intensidad de gris de la imagen en ese punto concreto.

Generalmente, la matriz de una imagen suele estar formada por números enteros o números decimales. Con frecuencia, el nivel de intensidad de cada píxel suele representarse con una secuencia binaria de 1 byte, es decir, 8 bits. Esto significa que cada píxel de la imagen puede tomar 28 valores diferentes, lo que hace un total de 256 niveles de gris representables. Cuando trabajamos con números enteros, el rango de valores estará entre el 0 y el 255, donde 0 representa el nivel de intensidad mínimo (es decir, el negro) y el 255 representa el máximo nivel de intensidad posible (es decir, el blanco). Entre 0 y 255 se encuentra una gama de grises. Comúnmente, estos 256 diferentes valores son más que suficientes para poder mostrar imágenes que tengan una apariencia realista bajo nuestra percepción.

Sin embargo, es muy común que se utilice números decimales para representar el nivel de gris de un punto determinado en una imagen. En lugar de tener un rango entre 0 y 255, este rango se sitúa entre 0 y 1. Entonces, ¿cómo es posible representar los niveles de gris de una imagen con un valor entre 0 y 1? Lo que se hace es dividir este intervalo real en 256 particiones. De esta forma, cada partición tendría un tamaño de 1/255 = 0.0039 (se divide entre 255 para que el valor decimal 1 se corresponda con el nivel de intensidad 255).

De este modo, el valor de intensidad 0 estaría representado igualmente por 0, el valor de intensidad 1 estaría representado por el número 0.0039, el valor de intensidad 2 estaría representado por 0.0078.. y así sucesivamente. Hay ocasiones en las que incluso se establece un rango de valores entre el -1 y el 1. Y te preguntarás, ¿por qué se escogen los rangos de números reales? Esto es debido, especialmente, a que esos rangos tienen algunas propiedades estadísticas interesantes (nos permite disponer de unas muestras de datos normalizadas). Para ciertos algoritmos, podría ser indispensable que los datos de entrada estén normalizados y de ahí esta relevancia.

Imágenes en color

Una vez visto el caso para las imágenes en escala de grises, la extrapolación de estas ideas al dominio de las imágenes en color es muy sencilla. Si las imágenes en escala de grises están representadas por una matriz bidimensional, las imágenes en color se representan con una matriz tridimensional. En este caso, además de tener las dimensiones de ancho y alto, existe una tercera, que se corresponde con el canal de color. Generalmente, estas imágenes se encuentran representadas por un modelo de color que se conoce por sus siglas RGB (red, green, blue). Cada píxel de la imagen está representado por tres componentes. Cada una de esas componentes indica, respectivamente, la cantidad de rojo, azul y verde que contiene el punto.

Por decirlo de alguna forma, cada imagen RGB está representada por tres matrices bidimensionales donde la primera representa el canal rojo, la segunda el canal azul y, la tercer, el canal verde. Cada componente de color está representado por una secuencia de 1 byte, es decir, 8 bits, lo cual hace un total de 32 bits que permiten representar 232 = 4294967296 colores diferentes. Esta gama de colores es suficiente para que las imágenes tengan un resultado con apariencia realista.

En ocasiones, se utiliza el modelo de color RGBA que incluye un cuarto canal, donde se representa la opacidad del píxel. Suponiendo un rango entre 0 y 255, el valor 0 representa un píxel totalmente transparente y, el 255, representa un píxel totalmente opaco. Existe una gama de opacidades entre estos valores extremos. Este tipo de imágenes, por tanto, permite representar el mismo número de colores diferentes que el modelo RGB y, además, permite representar 256 niveles distintos de opacidad, para cada píxel. Generalmente se comienza hablando sobre el modelo de color RGB, pero también existen otros como el HSV, el HSL o el LUV, por ejemplo, donde cada canal representa una cualidad del color concreta.

Trabajar con imágenes en OpenCV: primeros pasos

Una vez que hemos visto la teoría de cómo se representa una imagen, pasemos directamente a la implementación práctica de las funcionalidades básicas. El primer paso, antes de hacer cualquier cosa con una imagen, es leerla y cargarla en memoria. Pues esto es lo que vamos a hacer a continuación.

La lectura de imágenes en OpenCV es muy sencilla. Simplemente, se debe utilizar la función «imread()» disponible en la librería especificando la ruta del fichero donde se encuentra almacenada la imagen. Por ejemplo, vamos a leer el logo de la página y vamos a jugar un poquito con él. Está almacenado en un fichero que se llama «input.png» y está situado en la misma carpeta donde tenemos el script. Después, vamos a mirar qué tamaño tiene la matriz obtenida al leer dicha imagen.

A la salida tenemos «(500, 500, 3)». ¿Qué quiere decir esto? Quiere decir que la imagen tiene una resolución de 500 x 500 y tres canales de color. Como por defecto OpenCV lee en RGB y no hemos especificado ningún argumento al leer la imagen, este será el modelo de color utilizado. Sin embargo, ten en cuenta una cosa que, por alguna razón, OpenCV representa los canales con el orden contrario, así que en lugar de tener un RGB va a ser un BGR. Es decir, que el primer canal de color se corresponderá con el azul, el segundo con el verde y el tercero con el rojo. Es un poco lioso, per decidieron implementarlo así.

¿Qué podemos hacer con esta image? Lo cierto es que podemos hacer todo lo que se nos ocurra, pero vamos a por lo más básico. Por ejemplo, ¿cómo hago para coger solo un canal de color? Esto es muy sencillo, sabiendo manejar los índices de Python. En el código de más abajo se puede ver un ejemplo de código extrayendo, separadamente, los tres canales de color.

Para visualizar las imágenes de los tres canales, se puede utilizar la función «imwrite» de la librería OpenCV, que guarda el contenido de una matriz en un fichero. Ese fichero se llamará como le especifiquemos en el primer argumento. Los resultados que se obtienen son los que se pueden observar a continuación.

Si más tarde miramos el tamaño de la matriz de la imagen para un único canal (con la secuencia «print(np.shape(blue_channel))» para el canal azul, por ejemplo), obtendremos para los tres casos «(500, 500)». Es decir, como resultado tendremos una imagen monocanal en escala de grises que, a su vez, representa a un único canal de la imagen original en color. Evidentemente, de esta forma tendríamos una imagen en escala de grises por cada canal de color, en lugar de tener toda la información integrada en una única imagen.

Existen algunas técnicas para convertir una imagen en color a escala de grises integrando toda la información. El método más simple es quizá el de promediado. Es decir, si R, G y B representan la matriz de la imagen para cada canal de color y Gray la imagen en escala de grises final, la operación a llevar a cabo sería Gray = (R + G + B)/3. Pero esta técnica presenta un problema y es que nuestra capacidad de percepción nos permite distinguir una mayor cantidad de gamas de verde que de rojo y azul, por ejemplo. Esto significa que, de alguna forma, debemos darle más peso al color verde a la hora de obtener la representación en escala de grises de la imagen original en color.

Para ello, en lugar de utilizar una media aritmética normal, se realiza una media ponderada, donde se le da un peso determinado a cada canal de color. Este peso que se le da a cada uno de los canales se ha obtenido por medio de estudios científicos, resultando en la siguiente expresión (teniendo la misma notación que antes): Gray = 0.3 * R + 0.59 * G + 0.11 * B. Por lo general, las librerías de procesamiento de imágenes deberían incluir métodos para la conversión directa entre RGB y escala de grises o viceversa. Del mismo modo, también suelen disponer de métodos que permiten cambiar entre diferentes espacios de color (por ejemplo, entre RGB y HSV).

A modo de ejemplo, vamos a ilustrar diferentes formas de hacer lo mismo… aunque solo sea por una cuestión de gimnasia mental, para que así te vayas acostumbrando a manejar la librería y el lenguaje con más soltura. Comenzamos por la estrategia de hacer las operaciones de forma manual. Suponiendo que continuamos con las variables que habíamos obtenido con el código anterior, se haría de la siguiente forma:

Para visualizar los resultados, podemos utilizar las funciones que nos permiten guardar imágenes en un fichero. En este caso, utilizamos el método «imwrite()» de la librería de OpenCV.

Los resultados que se obtienen después de haber ejecutado este código son los siguientes.

Como ves, en esta sección te hemos enseñado a utilizar las funciones más básicas para trabajar con imágenes en OpenCV. Además, hemos realizado algunas operaciones muy básicas con ellas y hemos aprendido cosas fundamentales sobre las imágenes digitales. Vamos poco a poco incrementando el conocimiento sobre este interesante ámbito. ¡Nos vemos en el próximo post!

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *