Por qué funciona: Unidad 1.3: La imagen como datos

Erosión, dilatación, apertura y cierre de una máscara en OpenCV

Limpia una máscara de color con morfología: la erosión encoge las áreas blancas, la dilatación las agranda, la apertura quita puntitos y el cierre rellena huecos. En la mesa de billar la máscara pasa de 59 blobs a 14.

Luis Condados · · 5 min de lectura Principiante

Traducción asistida por máquina del original en inglés. Los números, el código y las referencias se comprobaron contra él; el texto no fue revisado por un hablante nativo. Leer el original. Los labs interactivos y el código están en inglés.

La máscara de lo que no es paño de la mesa de billar tras la apertura y el cierre: un blob blanco por objeto.
La máscara de lo que no es paño de la mesa de billar tras la apertura y el cierre: un blob blanco por objeto.

Una máscara recién salida de una prueba de color viene llena de puntitos y agujeritos, y la morfología la limpia encogiendo y agrandando las áreas blancas.

Dos máscaras de la mesa de billar lado a lado: a la izquierda, bolas blancas con bordes irregulares y puntitos dispersos; a la derecha, las mismas bolas como blobs redondos y limpios.
Izquierda: lo que no es paño, dentro de la mesa. Derecha: la misma máscara abierta con un cuadrado de 5×5 y luego cerrada con uno de 7×7. Foto de MarkBuckawicki, CC0 [3].

Dónde ya lo has visto: Seleccionar › Encoger y Seleccionar › Agrandar en GIMP, y Contraer y Expandir en Photoshop, que aplican erosión y dilatación a una selección; la eliminación de puntitos en las apps de escáner de documentos; la limpieza de un chroma key de pantalla verde.

TL;DR. Encoger las áreas blancas de una máscara unos cuantos píxeles y volver a agrandarlas borra todo lo que es más pequeño que el encogimiento, y hacerlo al revés rellena huecos pequeños. En la mesa de billar eso lleva la máscara de lo que no es paño de 59 blobs blancos separados a 14, uno por objeto, con las bolas más pequeñas intactas.

El problema

La lección 2 hizo una máscara de todo lo que no es paño, el segundo paso para encontrar todas las bolas de la mesa. Dentro de la mesa se acerca a un blob blanco por bola, pero no del todo: puntitos donde el paño tiene un hilo gastado, bolas partidas en pedazos por sus franjas y una tira irregular a lo largo de las bandas. La siguiente lección cuenta blobs, así que cada puntito contaría como una bola.

Cuenta los blobs

La primera celda reconstruye la máscara y se queda solo con la mesa. Tres de sus líneas encuentran el contorno de la mesa; la lección 4 las explica.

Python · OpenCV
import cv2
import numpy as np

img = cv2.imread("pool.webp")
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
cloth = cv2.inRange(hsv, (75, 80, 40), (100, 255, 255))
# el contorno de la mesa: la lección 4 explica estas tres líneas
cs, _ = cv2.findContours(cloth, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
table = np.zeros_like(cloth)
cv2.fillPoly(table, [cv2.convexHull(max(cs, key=cv2.contourArea))], 255)
objects = cv2.bitwise_and(cv2.bitwise_not(cloth), table)

def blobs(mask):
    return cv2.connectedComponents(mask)[0] - 1
print("blobs:", blobs(objects))
Salida
blobs: 59

Una máscara es un array de 0s y 255s como cualquier otra imagen (An image is an array, en inglés). cv2.connectedComponents le da a cada región blanca separada su propio número y devuelve cuántos números usó, contando el fondo negro como uno; de ahí el - 1.

Erosión y dilatación

Las dos deslizan un cuadrado pequeño, el kernel, sobre la máscara. La erosión deja un píxel en blanco solo si todos los píxeles bajo el kernel son blancos, así que las áreas blancas pierden un borde tan ancho como la mitad del kernel. La dilatación pone un píxel en blanco si cualquier píxel bajo el kernel es blanco, así que las áreas blancas ganan uno [1] [2].

Python · OpenCV
kernel = np.ones((5, 5), np.uint8)
eroded = cv2.erode(objects, kernel)
dilated = cv2.dilate(objects, kernel)
print("eroded:", blobs(eroded), " dilated:", blobs(dilated))
Salida
eroded: 31  dilated: 24
LlamadaQué les hace a las áreas blancas
cv2.erode(mask, kernel)las encoge; todo lo más delgado que el kernel desaparece
cv2.dilate(mask, kernel)las agranda; los huecos más estrechos que el kernel se cierran
np.ones((5, 5), np.uint8)un kernel cuadrado de 5×5

La erosión reduce el conteo más o menos a la mitad, porque los puntitos desaparecen, pero cada bola también quedó más pequeña. La dilatación también reduce el conteo, por la razón opuesta: pedazos cercanos se fundieron en uno.

Abre y luego cierra

La apertura es una erosión seguida de una dilatación con el mismo kernel: los puntitos mueren en la erosión y no vuelven, y todo lo más grande vuelve a crecer hasta más o menos su tamaño anterior. El cierre es lo contrario, una dilatación y luego una erosión, que rellena huecos y une pedazos que casi se tocan [1] [2].

Python · OpenCV
opened = cv2.morphologyEx(objects, cv2.MORPH_OPEN, kernel)
k7 = np.ones((7, 7), np.uint8)
closed = cv2.morphologyEx(opened, cv2.MORPH_CLOSE, k7)
print("opened:", blobs(opened), " then closed:", blobs(closed))
cv2.imshow("opened, then closed", closed)
Salida
opened: 15  then closed: 14
opened, then closed
opened, then closed

59 blobs se vuelven 15 después de la apertura y 14 después del cierre. Los tamaños de los kernels son los dos ajustes que importan: el kernel de la apertura tiene que ser más pequeño que la bola más pequeña, y el del cierre más grande que los huecos dentro de una bola.

Mete la mesa hacia adentro

El contorno de la mesa pasa por encima de las bandas, así que una tira de banda y las troneras detrás de las bolas cercanas al borde siguen dentro de él. Aplicar erosión a la propia mesa, con un kernel mucho más grande, mete su borde unos 9 píxeles hacia adentro:

Python · OpenCV
table = cv2.erode(table, np.ones((19, 19), np.uint8))
objects = cv2.bitwise_and(cv2.bitwise_not(cloth), table)
opened = cv2.morphologyEx(objects, cv2.MORPH_OPEN, kernel)
closed = cv2.morphologyEx(opened, cv2.MORPH_CLOSE, k7)
print("blobs:", blobs(closed))
cv2.imshow("with the table pulled in", closed)
Salida
blobs: 14
with the table pulled in
with the table pulled in

El conteo se queda en 14, por coincidencia: una tira de 3.575 píxeles a lo largo de la banda del fondo desaparece, y el blob del borde izquierdo que contenía las dos bolas amarillas, la tronera detrás de ellas y un pedazo de banda se encoge de 6.737 píxeles a 4.815 y suelta un pedazo aparte. Esta es la máscara que usa el resto del módulo.

Rómpelo

El kernel de la apertura decide qué cuenta como puntito. Hazlo más grande que las bolas del fondo:

Rómpelo
big = np.ones((25, 25), np.uint8)
too_much = cv2.morphologyEx(objects, cv2.MORPH_OPEN, big)
print("blobs:", blobs(too_much))
cv2.imshow("opened with 25 x 25", too_much)
Salida
blobs: 5
opened with 25 x 25
opened with 25 x 25

Quedan cinco blobs. Las bolas del fondo de la mesa se ven más pequeñas en la foto que las bolas cercanas a la cámara, así que se van primero, junto con los puntitos. La lección 5 cuenta lo que los ajustes finales del módulo encuentran y pierden en tres fotos.

Tu turno

Encuentra el tamaño impar de kernel más grande cuya apertura todavía conserva la bola roja en x = 494, y = 92.

Tu turno
size = 5  # cambia esto
k = np.ones((size, size), np.uint8)
opened = cv2.morphologyEx(objects, cv2.MORPH_OPEN, k)
print(blobs(opened), opened[92, 494])

Por qué funciona

La erosión y la dilatación son las dos operaciones básicas de la morfología matemática, y la apertura y el cierre se definen a partir de ellas [2]. La apertura elimina toda región blanca dentro de la cual el kernel no cabe, y por eso el tamaño del kernel funciona como un umbral de tamaño. El cierre hace lo mismo con las regiones negras, así que rellena huecos más pequeños que el kernel. La ruta de Fundamentos trata las imágenes binarias en la unidad 2.2, que está planeada; hasta que esté escrita, el capítulo 9 de Gonzalez y Woods es la referencia [2].

Errores comunes

  • Un kernel más grande que el objeto más pequeño. La apertura lo borra, como muestra la celda Rómpelo. Elige el tamaño del kernel a partir de lo más pequeño que necesitas conservar.
  • Apertura y cierre en el orden equivocado. Cerrar primero une los puntitos a las bolas antes de que la apertura pueda quitarlos. Limpia el ruido primero y luego rellena los huecos.
  • Objetos negros sobre blanco. En OpenCV la morfología actúa sobre el blanco. Para limpiar objetos negros, invierte la máscara antes o intercambia erosión y dilatación.

Referencias

[1] OpenCV. Morphological Transformations (Python tutorial, doc/py_tutorials/py_imgproc/py_morphological_ops/py_morphological_ops.markdown). OpenCV 4.11.0, commit 31b0eee. github.com/opencv/opencv

[2] Gonzalez, R. C., & Woods, R. E. (2018). Digital Image Processing (4th ed.), section 9.2 “Erosion and Dilation”, p. 696, and section 9.3 “Opening and Closing”, p. 702. Pearson.

[3] MarkBuckawicki. Billiards table 2.JPG (27 Feb 2014). Wikimedia Commons, CC0 1.0. commons.wikimedia.org