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!