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!

Publicado el Deja un comentario

¿Cómo aplicar un filtro de suavizado con OpenCV en Python?

Una de las partes más importantes a la hora de trabajar con una imagen, para posteriormente aplicar algoritmos de visión artificial es el preprocesamiento. Las imágenes del mundo real poseen ruido, bien sea por los dispositivos de captura, por la digitalización o transmisión de los datos. Para la eliminación o atenuación de este ruido, se utilizan diversas estrategias de preprocesamiento.

Una de ellas es la de suavizar la imagen, para eliminar la aparición de ciertos valores aleatorios que puedan representar ese ruido antes mencionado. Para el suavizado de una imagen, se aplica un filtro conocido como «filtro de paso bajo», es decir, que deja pasar las frecuencias bajas y elimina las frecuencias altas.

¿Qué significa esto? Las frecuencias altas representan bordes, ya que son zonas de la imagen que representan transiciones. Por su parte, todo lo que no sea una transición, es decir, una zona que tiende a un color bastante homógeneo, no será borde y, por tanto, se corresponderá con las frecuencias bajas de la imagen. Sabiendo esto, es fácil deducir que un filtro de paso bajo eliminará los bordes y, de esta forma, difuminará la imagen.

¿Cómo se suaviza una imagen?

Para el suavizado de una imagen, se puede seguir el procedimiento de convertir la imagen al dominio de la frecuencia por medio de la Transformada de Fourier, aplicar un filtro de paso bajo que solo acepte frecuencias debajo de un determinado umbral y, más tarde, volver a convertir esa señal ya procesada al dominio espacial (el dominio original de la imagen), nuevamente.

El método más seguido en la actualidad consiste en utilizar filtros convolucionales para tal fin, que ofrecen la ventaja de que el procesamiento se realiza directamente sobre el dominio no transformado de la imagen (el dominio espacial), evitando la aplicación de la Transformada de Fourier y la aplicación posterior de la Transformada Inversa de Fourier para regresar al dominio espacial.

Los filtros de suavizado se basan en ese proceso de filtrado convolucional utilizando unos kernels con unas características específicas. Generalmente, estos kernels realizan algún tipo de promediado, que puede ser ponderado o no. Pero vamos poco a poco.

Filtro de medias

En el filtro de medias, todos los coeficientes del kernel convolucional tienen valores positivos. De esta forma, conseguimos una integración de los valores de una determinada vecindad. Por ejemplo, supongamos que tenemos un kernel 3 x 3 con todos sus coeficientes a 1, como el que se puede ver en la imagen de abajo.

Con un kernel como el que aquí se describe, no solo se tomaría el valor central (indicado con color rojo) si no también esa vecindad local de 3 x 3 que el propio kernel indica. Por tanto, el resultado final (es decir, el valor que se le asignará al píxel que se está procesando), será la suma de los nueve valores de la vecindad (de ahí el concepto de la integración de los valores).

Además, para garantizar que el valor de salida del filtro esté dentro del rango de niveles de gris de la imagen original (generalmente en el rango [0, 255]), se debe realizar un promedio (de ahí que, además de aplicar el kernel, se divida el resultado entre 9, para este caso).

Filtro de medias en OpenCV

Realizar un filtro de medias en OpenCV es un procedimiento muy sencillo. Como puedes ver a continuación, se puede limitar a cuatro líneas.

En primer lugar, se realiza la importación de la librería. En segundo lugar, se lee la imagen con la función «cv2.imread()». Sobre esta imagen, ahora se podrá realizar un suavizado con un filtro de medias, haciendo uso de la función «cv2.blur()».

Esta función recibe dos parámetros: la imagen de entrada que se desea suavizar y el tamaño del kernel, de 10 x 10 para este ejemplo. Ya que se trata de simple filtro de medias en este caso, todos los coeficientes del kernel valdrán 1. Una vez aplicado, se almacena en un fichero de imagen o se muestra por pantalla con «cv2.imshow()». En este caso, hemos decidido almacenar todo en un fichero PNG con la función «cv2.imwrite()».

El resultado que se obtiene es algo como lo que se puede ver a continuación.

Para ajustar el nivel de suavizado, se debe variar el valor del tamaño del kernel. Este sería un ejemplo con un kernel de tamaño mayor, en este caso, de 20 x 20.

Como se puede observar, al aplicar un filtro de medias con un tamño de kernel mayor (20 x 20, en este caso), el difuminado de la imagen es mucho más notable.

Filtro gaussiano

El filtro gaussiano es un caso particular del filtro de medias. Realmente, consiste en realizar un filtro de medias ponderado. ¿Qué quiere decir esto? Para entenderlo, hay que tener en cuenta que, en el filtro de medias normal, todos los píxeles de la vecindad tienen el mismo peso. Pero, razonadamente, sabemos que no todos los píxeles deberían tener el mismo peso, ya que los píxeles más lejanos dentro de la vecindad influyen menos sobre el píxel central que los más cercanos.

Una alternativa para atender a estas ideas es utilizar un kernel cuyos coeficientes sean los valores de una función gaussiana. Esta función gaussiana calculará un valor en función de la posición en la que el coeficiente se encuentre dentro del kernel. De forma natural, estaremos ejecutando un suavizado con un filtro de medias ponderado.

Filtro gaussiano en OpenCV

Realizar un filtrado gaussiano en OpenCV también es un procedimiento muy sencillo. Igual que en el caso anterior, se importa la librería correspondiente, se lee la imagen, se aplica la función correspondiente y se almacena en un fichero y se visualiza.

En este caso, se hace uso de la función «cv2.GaussianBlur()» que recibe como parámetros la imagen sobre la que se desea aplicar el suavizado, el tamaño del kernel y el valor de sigma de la función gaussiana. Generalmente, se suele dejar el valor a 0 ya que, de este forma, la propia función obtiene un valor de sigma automáticamente a partir del tamaño del kernel.

Después de aplicar el filtro correspondiente, el resultado obtenido se puede observar a continuación.

Es posible que no se aprecie demasiado las diferencias entre ambas imágenes porque el tamño del kernel del suavizado es demasiado pequeño. Del mismo modo que ocurre con el filtro de medias, para obtener un difuminado mayor es preciso aumentar el tamaño del kernel. Más abajo, se puede observar el mismo ejemplo pero utilizando, en este caso, un filtro gaussiano de tamaño 51 x 51.

En este caso, con un tamaño de kernel mucho mayor (51 x 51) el difuminado obtenido es considerablemente más notorio.

Es importante tener en cuenta que, debido a cuestiones internas de la función utilizada, los kernels deben tener un tamaño impar. Por ello, no se podrá realizar un suavizado gaussiano con un kernel 4 x 4 o con un kernel de 50 x 50. En caso contrario, la propia función devolverá un error similar a este:

cv2.error: OpenCV(4.4.0) /tmp/pip-req-build-99ib2vsi/opencv/modules/imgproc/src/smooth.dispatch.cpp:296: error: (-215:Assertion failed) ksize.width > 0 && ksize.width % 2 == 1 && ksize.height > 0 && ksize.height % 2 == 1 in function ‘createGaussianKernels’

Filtro de medianas

Es ampliamente conocido en el mundo de las matemáticas que la media es un estadístico muy influenciado por los valores atípicos que pueden aparecer en una muestra. Es decir, supongamos que tenemos un conjunto de valores del estilo.

[1.70, 1.90, 2.10, 3.50, 30.90, 2.90, 1.70]

Claramente, se observa que hay un valor que desentona dentro de esa distribución, el 30.9, por lo que representa un valor atípico dentro del conjunto de datos especificado. La media de estos valores será aproximadamente 6.39. Evidentemente, este valor no es representativo del conjunto de datos, ya que los datos «normales» (todos los que no son atípicos, en este caso) no se aproximan a ese supuesto valor medio.

Si eliminásemos ese valor atípico, la media sería de 2.30, lo cual ya parece más coherente con el conjunto de datos. Este concepto, conocido como la media recortada, es perfectamente aplicable al ámbito del suavizado de imágenes. Un concepto similar es el de la mediana, que colocaría los valores anteriores en orden…

[1.70, 1.70, 1.90, 2.10, 2.90, 3.50, 30.90]

Y, en este caso, como el tamaño de la lista es impar, asumiríamos el valor central (que deja mismo número de valores a derecha e izquierda) como la mediana. Por tanto, para este caso, sería el 2.10 lo cual, nuevamente, es un valor más coherente. En resumen, si fuera de interés, utilizar un suavizado que sea robusto a valores atípicos, los filtros de medianas son la alternativa que se suele utilizar.

Filtro de medianas en OpenCV

Por último, el filtro de medianas también es muy sencillo de aplicar en OpenCV. Para ello, se aplica el mismo procedimiento que en los casos anteriores, haciendo uso de la función «cv2.medianBlur()».

Esta función recibe dos parámetros. En primer lugar, la imagen sobre la que se quiere realizar el suavizado. En segundo lugar, se debe especificar el tamaño del kernel para el filtro. En este caso, el tamaño del kernel también debe estar especificado por un valor positivo impar por cuestiones internas de la función.

Una vez que se aplican los procesos correspondientes, se obtiene imágenes con resultados como los que se pueden observar a continuación. Una vez más, cuanto mayor sea el tamaño del kernel mayor será el nivel de difuminado del filtro de medianas.

Imagen original.
Imagen después de un filtrado de medianas de tamaño 51.
Imagen después de un filtrado de medianas de tamaño 11.
Imagen después de un filtrado de medianas de tamaño 5.
Publicado el Deja un comentario

¿Cómo umbralizar una imagen con OpenCV en Python?

La umbralización en OpenCV utilizando Python es una tarea que se puede llevar a cabo de una forma muy sencilla. La librería de OpenCV nos ofrece una serie de opciones para realizar esta binarización, que mostraremos una por una a continuación.

Binarización simple

Debemos recordar que la binarización simple consiste en umbralizar los valores de gris utilizando, para ello, un valor seleccionado manualmente, lo cual da lugar a una imagen binaria.

En las imágenes binarias, se suele utilizar el valor de gris más intenso (es decir, más próximo a blanco) para colorear las zonas que se corresponden con los objetos de interés. Por su parte, el valor de gris más oscuro (más próximo al negro) se utilizará para colorear el fondo, es decir, todo aquello que no sea el objeto de interés. De hecho, lo más común es utilizar el blanco (valor de 255) para las zonas de interés y el negro (valor de 0) para el fondo.

Dicho esto, pasemos al ejemplo que nos interesa. Supongamos que se desea segmentar todos los objetos que se pueden ver en la siguiente imagen.

Ejemplo de imagen para su umbralización con OpenCV

Parece bastante aceptable que una umbralización podría ser suficiente para poder segmentar esos objetos de interés con una enorme precisión. Para ello, se puede utilizar el código que se muestra a continuación.

Estos son los pasos a seguir a la hora de realizar una umbralización simple sobre una imagen RGB, junto a su código correspondiente, explicándolo de una forma más detallada.

1. Importar librerías y leer la imagen de entrada

Se importan todas las librerías necesarias para el proceso. Para este caso, las librerías que se utilizan son OpenCV para el trabajo con las imágenes, Numpy para ciertas tareas matemáticas y Matplotlib para la graficación del histograma. A continuación, con el método «imread» de la librería de OpenCV, se leerá la imagen de entrada a una matriz de tres dimensiones (ancho, alto y canal de color RGB).

2. Obtención del histograma (opcional)

Opcionalmente, se puede graficar el histograma de la imagen para obtener alguna conclusión por medio de la exploración visual.

Para este ejemplo, se puede obtener un histograma como el de arriba. En este caso, se decide obtener el histograma del canal verde, pero se podría obtener para cualquiera de los otros dos canales.

3. Obtener una imagen de un solo canal para la umbralización

Antes de umbralizar la imagen, dado que es RGB, se debe tomar una decisión. Es necesario tener una entrada de un solo canal, para lo cual existen principalmente dos opciones.

La primera consiste en convertir la imagen en color a escala de grises.

La segunda consiste en quedarse con un solo canal (R, G ó B, es decir, el canal rojo, el verde o el azul) de la imagen original.

Existe también una tercera opción, muy similar a la de asumir un único canal de la imagen RGB pero, en este caso, utilizando otro modelo de color diferente como HSL o HSV, por ejemplo.

Para este caso, se ha decidido coger únicamente el canal «1» que, dado que en OpenCV las imágenes se representan con un modelo BGR (en lugar de RGB), se corresponde con el canal verde (canal G).

4. Llevar a cabo la umbralización

Sobre el canal seleccionado, se considera un valor umbral concreto que se especifica manualmente. Con frecuencia, esta selección se hará a base de prueba y error, pero la visualización del histograma puede aportar información a priori de cuál puede ser el valor idóneo.

La función «cv2.threshold» utiliza cuatro parámetros de entrada:

  • La imagen que se desea umbralizar (de un solo canal).
  • El valor umbral.
  • El valor de nivel de gris que queremos ponerle a los píxeles de la clase «1», es decir, a los que superen el valor umbral antes especificado. Generalmente, este valor debería ser 255.
  • El tipo de umbralización que se desea llevar a cabo. Existen varias opciones.

OPCIONES DE LA FUNCIÓN «CV2.THRESHOLD» PARA UMBRALIZAR

cv2.THRESH_BINARY: umbralización estándar. Todos aquellos píxeles que superen el valor umbral será considerado como píxeles de objeto (y, para el ejemplo que estamos tratando, estos píxeles tendrán un valor de 255) y todo lo demás se corresponderá con el fondo (tendrá un valor de 0).

Teniendo ImRes(x, y) como la imagen resultante de la umbralización, Im(x, y) como la imagen que se desea umbralizar, OBJVAL como el valor de los píxeles que superan el umbral y T como el valor umbral, se tiene la siguiente expresión:

    \[ ImRes(x, y)= \left\{ \begin{array}{lcc}              OBJVAL &   si  & Im(x, y) > T \\              \\ 0 &  si & Im(x, y) \leq T  \\              \end{array}    \right. \]


cv2.THRESH_BINARY_INV: el procedimiento se ejecuta de la misma forma que en el caso anterior pero, en este caso, a la inversa. De este modo, todo lo que sobrepase el valor umbral tendrá un valor de 0 y, lo que se quede por debajo de dicho umbral, recibirá un valor de 255.

Teniendo ImRes(x, y) como la imagen resultante de la umbralización, Im(x, y) como la imagen que se desea umbralizar, OBJVAL como el valor de los píxeles que superan el umbral y T como el valor umbral, se tiene la siguiente expresión:

    \[ ImRes(x, y)= \left\{ \begin{array}{lcc}              OBJVAL &   si  & Im(x, y) < T \\              \\ 0 & si & Im(x, y) \geq T  \\              \end{array}    \right. \]


cv2.THRESH_TRUNC: en este caso, los píxeles que no sobrepasen el umbral conservarán su valor en la imagen resultante y, aquellos que sí lo superen, aparecerán en la imagen resultante con el valor umbral. En otras palabras, esta funcionalidad hace que ningún píxel de la imagen sobrepase el valor umbral en la imagen resultante. Bajo este punto de vista, no se está realizando propiamente una binarización, ya que la imagen resultante seguirá estando en escala de grises.

Teniendo ImRes(x, y) como la imagen resultante de la umbralización, Im(x, y) como la imagen que se desea umbralizar, OBJVAL como el valor de los píxeles que superan el umbral y T como el valor umbral, se tiene la siguiente expresión:

    \[ ImRes(x, y)= \left\{ \begin{array}{lcc}              T &   si  & Im(x, y) > T \\              \\ Im(x, y) & si & Im(x, y) \leq T  \\              \end{array}    \right. \]


cv2.THRESH_TOZERO: en este caso, tampoco se obtiene una imagen binaria. Todo lo que supere el valor umbral, conservará es valor en la imagen resultante, mientras que los píxeles que no lo superen se pondrán a cero.

Teniendo ImRes(x, y) como la imagen resultante de la umbralización, Im(x, y) como la imagen que se desea umbralizar, OBJVAL como el valor de los píxeles que superan el umbral y T como el valor umbral, se tiene la siguiente expresión:

    \[ ImRes(x, y)= \left\{ \begin{array}{lcc}              Im(x, y) &   si  & Im(x, y) > T \\              \\ 0 &  si & Im(x, y) \leq T  \\              \end{array}    \right. \]


cv2.THRESH_TOZERO_INV: esta umbralización funciona de la misma forma que el caso anterior, pero a la inversa. Todo aquello que supere el valor umbral se pondrá a cero y, los píxeles restantes, conservarán su valor en la imagen resultante.

Teniendo ImRes(x, y) como la imagen resultante de la umbralización, Im(x, y) como la imagen que se desea umbralizar, OBJVAL como el valor de los píxeles que superan el umbral y T como el valor umbral, se tiene la siguiente expresión:

    \[ ImRes(x, y)= \left\{ \begin{array}{lcc}              0 &   si  & Im(x, y) > T \\              \\ Im(x, y) &  si & Im(x, y) \leq T  \\              \end{array}    \right. \]


Cabe destacar que esta imagen devuelve una dupla donde el primer elemento es el valor umbral con el que se ha realizado la umbralización, que será el mismo que has puesto como parámetro, en este caso (aunque no tenga mucho sentido, más adelante verás que, con la umbralización de Otsu, es importante que la función lo devuelva). El segundo elemento de esa dupla será la propia imagen ya umbralizada.

5. Visualización de la imagen resultante

Una vez escogido el valor umbral y después de haber aplicado la umbralización correspondiente, la imagen binaria resultante se almacena en un fichero de imagen con el formato que deseemos, comúnmente «.jpeg», «.jpg» o «.png».

Por supuesto, también tendrías la opción de mostrarlo en una ventana interactiva con la función que se muestra más abajo.

6. Discutir los resultados del método

El resultado que se obtiene para el ejemplo se puede observar a continuación. Para este caso, se ha considerado una umbralización simple invertida (flag «cv2.BINARY_THRESH_INV») ya que, asumiendo el canal verde, se observa que los niveles de gris de los objetos se encuentran en los valores bajos y, los píxeles de fondo, tienen una gran luminosidad.

Por lo general, se observa que un valor umbral de 220 sobre el canal verde de esta imagen, es suficiente para hacer una segmentación bastante precisa. Un objeto para el que la segmentación no es del todo precisa se puede observar en la imagen de a continuación, marcada en rojo.

Binarización con el algoritmo de Otsu

En este caso, en lugar de escoger de forma manual el valor umbral, se utiliza el método de Otsu, el cual es capaz de obtener dicho valor de forma automática. De este modo, el procedimiento a seguir es muy similar al de la binarización simple. De hecho, puede utilizarse exactamente la misma función «cv2.threshold», solo habrá que incluir el flag «cv2.THRESH_OTSU» a mayores del de «cv2.BINARY_THRESH_INV», como se indica en el código de a continuación.

Como se indicaba antes, el hecho de que la función de umbralización devuelva el umbral, cobra sentido. Ya que es el método quien selecciona el valor concreto, es útil conocerlo a posteriori. Esto se puede hacer sencillamente con un «print(ret)», ya que es un simple escalar.

Particularmente, para este ejemplo, el método de Otsu parece funcionar peor, ya que segmenta menos píxeles de algunos objetos. Se ha utilizado de nuevo el canal verde pero, en este caso, el umbral seleccionado por el método ha sido 149.

Hasta aquí el post de hoy. Si tienes alguna duda o sugerencia, no dudes en dejar tus comentarios. ¡Hasta la próxima entrada!