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.
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.
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.

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.
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))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].
kernel = np.ones((5, 5), np.uint8)
eroded = cv2.erode(objects, kernel)
dilated = cv2.dilate(objects, kernel)
print("eroded:", blobs(eroded), " dilated:", blobs(dilated))eroded: 31 dilated: 24
| Llamada | Qué 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].
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)opened: 15 then closed: 14
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:
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)blobs: 14
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:
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)blobs: 5
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.
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