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.

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.

El contorno de la mesa en blanco, los contornos redondos en verde y el resto en rojo.
El contorno de la mesa en blanco, los contornos redondos en verde y el resto en rojo.

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.

La foto del billar junto a la misma foto con el contorno de la mesa dibujado en blanco, los contornos de las bolas en verde y el contorno del taco en rojo.
Derecha: el contorno de la mesa (blanco), los contornos lo bastante redondos para ser una bola (verde) y el resto (rojo): el taco, las dos bolas amarillas pegadas a la tronera que tienen detrás y la bola roja del fondo pegada a una tira de banda. Foto de MarkBuckawicki, CC0 [5].

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:

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))
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))
Salida
4 contours; biggest 571460.0
hull corners: 14
LlamadaQué hace
cv2.findContours(mask, mode, method)los bordes de las regiones blancas y su anidamiento
cv2.RETR_EXTERNALsolo los bordes exteriores; los huecos dentro de una región se ignoran
cv2.CHAIN_APPROX_SIMPLEguarda 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:

Python · OpenCV
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")
Salida
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.

Python · OpenCV
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}")
Salida
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

Python · OpenCV
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)
Salida
11 balls: [(792, 316), (425, 301), (572, 263), (347, 253), (896, 221), (593, 197), (507, 162), (688, 144), (624, 118), (608, 122), (412, 110)]
round blobs
round blobs

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:

Rómpelo
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))
Salida
kept by area alone: 14
all blobs
all blobs

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.

Tu turno
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. findContours traza regiones blancas. Invierte la máscara antes [3].
  • RETR_TREE o RETR_LIST cuando solo quieres objetos. También devuelven los bordes de los huecos dentro de cada objeto, que terminan contados como objetos. RETR_EXTERNAL devuelve 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