Por qué funciona: Unidad 1.3: La imagen como datos
Encontrar, medir y filtrar contornos en OpenCV
Encuentra el contorno de cada blob de una máscara con cv2.findContours, mide su área y su redondez y quédate con los que tienen forma de bola. En la mesa de billar pasan 11 de 14 blobs y el taco se rechaza con redondez 0,12.
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.
Un contorno es el borde de un blob blanco en una máscara, y OpenCV puede medir el tamaño y la forma de cada borde, que es como se distinguen las bolas del taco.

Dónde ya lo has visto: Analyze Particles en ImageJ y Fiji, que cuenta células o granos con un filtro de tamaño y de circularidad; las herramientas de blobs del software de inspección por visión artificial que cuentan piezas en una cinta transportadora; las apps que cuentan monedas.
TL;DR. Cuando una máscara tiene un blob blanco por objeto, OpenCV puede trazar el borde de cada blob y medirlo, así que los objetos se pueden conservar o descartar por tamaño y forma. En la mesa de billar se encuentran 14 contornos; 11 son lo bastante redondos para ser una bola, y el taco, con redondez 0,12 en una escala donde un círculo marca 1, no es uno de ellos.
El problema
La lección 3 dejó una máscara limpia con 14 blobs y tomó prestadas tres líneas para encontrar la mesa. Esta lección explica esas líneas y luego convierte los blobs en una lista de bolas para el proyecto: dónde está cada una y si es redonda.
La mesa es el contorno más grande
cv2.findContours traza el borde de cada región blanca de una máscara y devuelve cada uno como una lista de puntos [1]. La máscara del paño de la lección 2 tiene una región grande, la mesa:
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))
cs, _ = cv2.findContours(cloth, cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
biggest = max(cs, key=cv2.contourArea)
hull = cv2.convexHull(biggest)
print(len(cs), "contours; biggest", cv2.contourArea(biggest))
print("hull corners:", len(hull))4 contours; biggest 571460.0 hull corners: 14
| Llamada | Qué hace |
|---|---|
cv2.findContours(mask, mode, method) | los bordes de las regiones blancas y su anidamiento |
cv2.RETR_EXTERNAL | solo los bordes exteriores; los huecos dentro de una región se ignoran |
cv2.CHAIN_APPROX_SIMPLE | guarda solo los puntos de esquina de los tramos rectos |
cv2.contourArea(c) | el área dentro de un borde, en píxeles |
cv2.convexHull(c) | el borde convexo más pequeño a su alrededor, como una liga elástica |
El borde del paño tiene muescas donde una bola se apoya en la banda y donde las troneras recortan la mesa. La envolvente convexa pasa por encima de las muescas, así que una bola pegada a la banda queda dentro de la mesa en lugar de en su orilla [1] [3]. Rellenar la envolvente y meterla hacia adentro el ancho de la banda da la máscara de la mesa:
table = np.zeros_like(cloth)
cv2.fillPoly(table, [hull], 255)
table = cv2.erode(table, np.ones((19, 19), np.uint8))
objects = cv2.bitwise_and(cv2.bitwise_not(cloth), table)
k5, k7 = np.ones((5, 5), np.uint8), np.ones((7, 7), np.uint8)
objects = cv2.morphologyEx(objects, cv2.MORPH_OPEN, k5)
objects = cv2.morphologyEx(objects, cv2.MORPH_CLOSE, k7)
cs, _ = cv2.findContours(objects, cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
print(len(cs), "contours on the table")14 contours on the table
Mide cada uno
Una bola vista desde cualquier ángulo es un disco, y un disco es la forma más redonda que hay. Un número para “redondo” sale de comparar el área con el perímetro: un círculo de radio r tiene área πr² y perímetro 2πr, así que 4π × área / perímetro² vale exactamente 1 para un círculo y menos para cualquier otra cosa. Un taco largo y delgado marca cerca de 0.
def roundness(c):
# 1.0 para un círculo, cerca de 0 para un palo
area = cv2.contourArea(c)
return 4 * np.pi * area / cv2.arcLength(c, True) ** 2
for c in sorted(cs, key=cv2.contourArea, reverse=True):
a = cv2.contourArea(c)
print(f"area {a:6.0f} roundness {roundness(c):.2f}")area 20810 roundness 0.12 area 4588 roundness 0.23 area 1773 roundness 0.82 area 1526 roundness 0.77 area 1444 roundness 0.66 area 1213 roundness 0.18 area 1165 roundness 0.63 area 1056 roundness 0.76 area 921 roundness 0.83 area 823 roundness 0.86 area 671 roundness 0.69 area 661 roundness 0.83 area 128 roundness 0.65 area 47 roundness 0.62
cv2.arcLength(c, True) es el perímetro, y True significa que el borde es cerrado [2]. El contorno más grande, de unos 20.800 píxeles y redondez 0,12, es el taco. Los dos siguientes, con 0,23 y 0,18, son grupos: las dos bolas amarillas con la tronera detrás, y la bola roja del fondo con una tira de banda. Las bolas sueltas quedan entre 0,66 y 0,86, y las bolas naranja y negra, que se tocan, forman un solo blob con 0,63. Los dos más pequeños son los pedazos blancos de la bola de franja verde, cuya franja verde es del color del paño.
Quédate con los redondos
out = img.copy()
balls = []
for c in cs:
if cv2.contourArea(c) >= 45 and roundness(c) > 0.55:
(x, y), r = cv2.minEnclosingCircle(c)
balls.append((round(x), round(y)))
cv2.circle(out, (int(x), int(y)), int(r) + 4, (0, 255, 0), 2)
print(len(balls), "balls:", balls)
cv2.imshow("round blobs", out)11 balls: [(792, 316), (425, 301), (572, 263), (347, 253), (896, 221), (593, 197), (507, 162), (688, 144), (624, 118), (608, 122), (412, 110)]
cv2.minEnclosingCircle devuelve el círculo más pequeño alrededor de un contorno, lo que le da a cada bola un centro y un radio [2]. Pasan once blobs. A ojo, ocho son bolas sueltas, uno es la naranja y la negra contadas como una, y dos son los dos pedazos blancos de la bola de franja verde. La bola verde que está al lado ni siquiera aparece, porque es del color del paño (la lección 1 leyó su tono en 82). La lección 5 compara el resultado con un conteo hecho a mano en las tres fotos y corrige lo que se puede.
Para probar una foto de tu propia mesa, pulsa “Usar tu imagen” en la primera celda de esta página y luego Ejecutar en esta. Un paño de otro color necesita su propio rango de tono, leído como en la lección 1.
Rómpelo
Filtra solo por tamaño y todo lo bastante grande pasa:
big = [c for c in cs if cv2.contourArea(c) >= 45]
print("kept by area alone:", len(big))
cv2.imshow("all blobs", cv2.drawContours(img.copy(), big, -1, (0, 0, 255), 2))kept by area alone: 14
Pasan los 14, incluidos el taco y los dos grupos. El tamaño dice cuánto hay de algo; solo la forma dice qué es.
Tu turno
El taco es el contorno más grande sobre la mesa. Guarda su redondez en r.
r = None # reemplaza esta línea
print(r)Por qué funciona
Un contorno es el borde de una región conectada de píxeles blancos, y OpenCV lo encuentra recorriendo la orilla de cada región [3]. El área y el perímetro son dos de los descriptores básicos de la forma de una región [4], y la redondez de arriba los combina de modo que el resultado no depende de lo grande que se vea el objeto en la foto, lo que importa cuando una bola cerca de la cámara ocupa tres veces el área de una del fondo. La envolvente convexa es a su vez una construcción morfológica estándar [4]. La unidad de Fundamentos sobre imágenes binarias (2.2) está planeada; hasta que esté escrita, Gonzalez y Woods es la referencia.
Errores comunes
- Objetos negros sobre fondo blanco.
findContourstraza regiones blancas. Invierte la máscara antes [3]. RETR_TREEoRETR_LISTcuando solo quieres objetos. También devuelven los bordes de los huecos dentro de cada objeto, que terminan contados como objetos.RETR_EXTERNALdevuelve solo los exteriores.- Filtrar solo por área. La celda Rómpelo se queda con el taco. Agrega una prueba de forma.
- Un umbral de redondez cerca de 1. El borde de una bola real es dentado a escala de píxel y a menudo lo recorta un reflejo o su sombra; aquí las bolas sueltas marcan de 0,66 a 0,86. Deja margen.
Referencias
[1] OpenCV. Contour Features (Python tutorial, doc/py_tutorials/py_imgproc/py_contours/py_contour_features/py_contour_features.markdown). OpenCV 4.11.0, commit 31b0eee. github.com/opencv/opencv
[2] OpenCV. imgproc.hpp, documentation of arcLength and minEnclosingCircle (modules/imgproc/include/opencv2/imgproc.hpp). OpenCV 4.11.0, commit 31b0eee. github.com/opencv/opencv
[3] OpenCV. Contours: Getting Started (Python tutorial, doc/py_tutorials/py_imgproc/py_contours/py_contours_begin/py_contours_begin.markdown). OpenCV 4.11.0, commit 31b0eee. github.com/opencv/opencv
[4] Gonzalez, R. C., & Woods, R. E. (2018). Digital Image Processing (4th ed.), section 12.4 “Region Feature Descriptors”, Some Basic Descriptors, p. 982, and section 9.5 “Some Basic Morphological Algorithms”, Convex Hull, p. 715. Pearson.
[5] MarkBuckawicki. Billiards table 2.JPG (27 Feb 2014). Wikimedia Commons, CC0 1.0. commons.wikimedia.org