Conteo de objetos con umbralización simple en Python | Minicurso OpenCV – Parte 8

Por Administrador

Bienvenidos al octavo artículo del minicurso de Visión por Computador con OpenCV en Python. En el artículo anterior hablamos sobre la umbralización simple, una técnica que, aunque es sencilla, puede llegar a ser muy poderosa cuando trabajamos en entornos controlados. En esta ocasión veremos como usarla para resolver un problema práctico real: Contar objetos automáticamente en una imagen.

Al final también revisaremos lo que debes tener en cuenta siempre que utilices esta técnica en tus propios proyectos. Así que… ¡vamos con la programación!

¿Qué aprenderás en este artículo?

  • Cómo preparar una imagen para contar objetos.
  • Cómo aplicar umbralización simple.
  • Cómo eliminar ruido usando transformaciones morfológicas.
  • Cómo detectar contornos con cv2.findContours().
  • Cómo dibujar contornos y mostrar el número total de objetos.
  • Qué condiciones debe cumplir una imagen para que este método funcione bien.

Analizando la imagen de entrada para el conteo de objetos con OpenCV

En esta ocasión vamos a trabajar con la siguiente imagen, que contiene varios limones sobre un fondo oscuro:

Al observar la imagen, podemos ver que cumple con lo que mencionamos en el artículo anterior:

  • Los objetos se diferencian claramente del fondo.
  • Hay iluminación uniforme.
  • No hay sombras fuertes.

Nuestro objetivo será contar los 19 limones presentes en la imagen, a través de la umbralización simple.

¡Vamos con el código!

Preparar la imagen de entrada para realizar el conteo de objetos mediante umbralización simple

Vamos a empezar con el código:

import cv2
import numpy as np

# Leer la imagen, convertirla a escala de grises y suavizarla
image = cv2.imread("01_image.jpg")
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
gblur = cv2.GaussianBlur(gray, (5, 5), 0)

# Aplicar umbralización simple
_, binary = cv2.threshold(gblur, 80, 255, cv2.THRESH_BINARY)

Línea 1 y 2: Importamos OpenCV y NumPy.

Línea 5: Leemos la imagen de entrada con cv2.imread().

Línea 6: Recordemos que OpenCV lee una imagen por defecto en BGR, así que para pasarla a escala de grises usaremos la función cv2.cvtColor(), indicamos la imagen que vamos a transformar y luego especificamos cv2.COLOR_BGR2GRAY.

Línea 7: Aplicamos suavizado a la imagen, y para ello usamos la función cv2.GaussianBlur(). Esto nos ayudará a eliminar pequeñas variaciones que podrían afectar a la umbralización.

Línea 10: Aplicamos umbralización simple con cv2.threshold(). Para ello hemos establecido los siguientes valores para sus parámetros:

  • gblur, imagen en escala de grises suavizada.
  • 80, umbral entre 0 y 255 elegido luego de prueba y error.
  • 255, valor asociado a los píxeles que superen el umbral.
  • cv2.THRESH_BINARY, tipo de umbralización.

Veamos las imágenes resultantes hasta ahora:

Tenemos a la imagen de entrada, la imagen en escala de grises y la imagen binaria. Ahora trabajaremos con esta última, ya que si prestamos atención existen pequeñas regiones blancas adicionales.

Transformaciones morfológicas sobre la imagen binaria obtenida luego de umbralización

Analicemos la imagen binaria:

Aunque la imagen binaria se ve bastante bien, podemos notar pequeñas zonas blancas cerca de los objetos principales. Estas se originan por pequeñas sombras o variaciones de iluminación, por lo que hay que tener cuidado, ya que afectarían al conteo de objetos. Por esta razón es importante eliminarlas. ¿Cómo lo haremos? A través de las transformaciones morfológicas.

Las transformaciones morfológicas son operaciones que se aplican principalmente sobre imágenes binarias (blanco y negro) para modificar la estructura o forma de los objetos presentes en la imagen.

En primer lugar aplicamos erosión, que hace que las regiones blancas se encojan, eliminando las pequeñas áreas de ruido. Como en el ejemplo a continuación:

Apliquemos erosión sobre la imagen binaria:

# Transformaciones morfológicas
kernel = np.ones((5, 5), np.uint8)
binary_2 = cv2.erode(binary, kernel, iterations=2)

Línea 13: En primer lugar con ayuda de NumPy creamos un kernel, que no es más que una pequeña matriz que recorre una imagen aplicando operaciones. Esta matriz tiene 5 filas por 5 columnas, pero puedes usar otros tamaños para experimentar, siempre y cuando sean impares, para que tengan un píxel central.

Linea 14: Ahora se aplica erosión con cv2.erode(). Entonces especificamos:

  • binary, imagen binaria donde se aplica la erosión.
  • kernel, la pequeña matriz que construimos en la línea 13.
  • iterations, el número de veces que se aplicará esta operación sobre la imagen.

Veamos un antes y un después de la imagen binaria:

La imagen de la izquierda es la imagen binaria obtenida luego de la umbralización, mientras que a la derecha tenemos la imagen binaria luego de la erosión. Podemos ver que las pequeñas áreas blancas se han ido, y se han mantenido unicamente las de los objetos.

Ahora aplicaremos dilatación, que se encarga de engrosar las áreas blancas. Entonces les devolveremos su tamaño, y ya no aparecerán las áreas eliminadas por la erosión. Veamos un ejemplo:

Entonces aplicamos la dilatación sobre la imagen binaria obtenida luego de la erosión:

binary_3 = cv2.dilate(binary_2, kernel, iterations=2)

Línea 15: Para aplicar dilatación usamos cv2.dilate(), y especificamos los parámetros:

  • binary_2, imagen binaria donde se aplicó la erosión.
  • kernel, la pequeña matriz que construimos en la línea 13.
  • iterations, el número de veces que se aplicará esta operación sobre la imagen.

Veamos la imagen binaria resultante:

Encontrar contornos usando cv2.findContours()

Cuando hablamos de detectar contornos nos referimos a identificar los bordes que delimitan cada una de las regiones blancas de la imagen binaria. Para llevarlo a cabo usamos cv2.findContours().

# Encontrar contornos
cnts, _ = cv2.findContours(binary_3.copy(),
                           cv2.RETR_EXTERNAL,
                           cv2.CHAIN_APPROX_SIMPLE)
print(cnts)

Línea 17 a 20: Especificamos:

  • binary_3.copy(), se usa una copia de la imagen binaria ya que la función puede modificar la imagen.
  • cv2.RETR_EXTERNAL, para extraer solo los contornos externos.
  • cv2.CHAIN_APPROX_SIMPLE, para obtener solo los puntos necesarios para representar el contorno, en lugar de guardar todos los puntos del borde, lo cual reduce el uso de memoria y mejora el rendimiento, sin perder información relevante para nuestro caso.

Línea 21: Al imprimir cnts, obtendremos la lista de contornos encontrados.

(array([[[532, 790]],

       [[531, 791]],

       [[528, 791]],

       [[527, 792]],...

Dibujar los contornos encontrados y el número de objetos

# Dibujar contornos
print(len(cnts))
cv2.drawContours(image, cnts, -1, (0, 255, 0), 3)

# Visualizar el número de contornos detectados
cv2.putText(image, str(len(cnts)), (30, 50),
            cv2.FONT_HERSHEY_SIMPLEX, 1.5, (0, 255, 0), 3)

# Visualizar resultados
cv2.imshow("Imagen original", image)
#cv2.imshow("Escala de grises", gray)
#cv2.imshow("Filtro Gaussiano", gblur)
#cv2.imshow('THRESH_BINARY', binary)
#cv2.imshow('binary_2', binary_2)
cv2.imshow('binary_3', binary_3)

cv2.waitKey(0)
cv2.destroyAllWindows()

Línea 24: Usamos print(len(cnts)) para obtener el número total de objetos encontrados, de donde obtenemos 19, que corresponden al número de limones.

Línea 25: Ahora visualizaremos los contornos con ayuda de cv2.drawContours(), especificamos:

  • image, imagen donde se dibujan los contornos.
  • cnts, la lista de contornos.
  • -1, indicamos que se dibujen todos los contornos encontrados.
  • (0, 255, 0), los contornos se dibujan el color verde.
  • 3, grosor de línea.

Línea 28 y 29: Con ayuda de cv2.putText() ubicamos el número total de objetos encontrados en la sección superior izquierda de la imagen. Para ello especificamos:

  • image, imagen donde se dibuja el texto.
  • str(len(cnts)), corresponde al texto que se mostrará.
  • (30, 50), son las coordenadas (x, y) donde se colocará el texto.
  • cv2.FONT_HERSHEY_SIMPLEX, tipo de fuente.
  • 1.5, tamaño de la fuente
  • (0, 255, 0), color del texto en BGR.
  • 3, grosor de línea.

Línea 32 a 40: Se visualizan las imágenes hasta que se presione una tecla. Una vez que eso pase, se cierran las ventanas de visualización.

Veamos el resultado:

Se puede usar la misma programación en otras imágenes en donde el entorno y objetos compartan características:

¡A tomar en cuenta al momento de contar objetos usando umbralización simple!

A más de las condiciones del entorno, en proyectos donde contamos el número total de objetos usando umbralización simple, es importante que los objetos no estén demasiado cerca entre sí.

Si los objetos están muy juntos o se tocan, es muy probable que en la imagen binaria terminen formando una sola área blanca. En ese caso, OpenCV detectará un solo contorno y el conteo será incorrecto.

Por ello, siempre que trabajes con umbralización simple, es fundamental revisar primero la imagen binaria y asegurarte de que cada objeto esté claramente separado del resto. Si esto no se cumple, será necesario ajustar el umbral, aplicar otras transformaciones morfológicas o incluso considerar técnicas más avanzadas.

Gracias por haber llegado hasta aquí. Espero que este contenido te haya sido útil. Si te quedaste con alguna duda o te gustaría que profundicemos en algún punto, puedes dejarlo en los comentarios.

Nos vemos en el siguiente artículo. 😊