Por que funciona: Unidade 1.3: A imagem como dado
Encontrar, medir e filtrar contornos no OpenCV
Encontre o contorno de cada blob de uma máscara com cv2.findContours, meça sua área e circularidade e fique com os que têm forma de bola. Na mesa de sinuca 11 de 14 blobs passam e o taco é rejeitado com circularidade 0,12.
Tradução assistida por máquina do original em inglês. Números, código e referências foram conferidos com ele; o texto não foi revisado por um falante nativo. Ler o original. Os labs interativos e o código estão em inglês.
Um contorno é a borda de um blob branco numa máscara, e o OpenCV consegue medir o tamanho e a forma de cada borda, e é assim que as bolas são separadas do taco.

Onde você já viu isso: o Analyze Particles do ImageJ e do Fiji, que conta células ou grãos com um filtro de tamanho e de circularidade; as ferramentas de blob dos softwares de inspeção por visão de máquina que contam peças numa esteira; os apps que contam moedas.
TL;DR. Quando uma máscara tem um blob branco por objeto, o OpenCV consegue traçar a borda de cada blob e medi-la, e assim os objetos podem ser mantidos ou descartados por tamanho e forma. Na mesa de sinuca são encontrados 14 contornos; 11 são redondos o bastante para ser uma bola, e o taco, com circularidade 0,12 numa escala em que um círculo marca 1, não está entre eles.
O problema
A lição 3 deixou uma máscara limpa com 14 blobs e pegou emprestadas três linhas para achar a mesa. Esta lição explica essas linhas e depois transforma os blobs numa lista de bolas para o projeto: onde cada uma está e se é redonda.
A mesa é o maior contorno
cv2.findContours traça a borda de cada região branca de uma máscara e devolve cada uma como uma lista de pontos [1]. A máscara do pano da lição 2 tem uma região grande, a 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
| Chamada | O que faz |
|---|---|
cv2.findContours(mask, mode, method) | as bordas das regiões brancas e o seu aninhamento |
cv2.RETR_EXTERNAL | só as bordas externas; buracos dentro de uma região são ignorados |
cv2.CHAIN_APPROX_SIMPLE | guarda só os pontos de canto dos trechos retos |
cv2.contourArea(c) | a área dentro de uma borda, em pixels |
cv2.convexHull(c) | a menor borda convexa em volta dela, como um elástico |
A borda do pano tem reentrâncias onde uma bola encosta na tabela e onde as caçapas recortam a mesa. O fecho convexo passa por cima das reentrâncias, então uma bola encostada na borda fica dentro da mesa em vez de na sua beirada [1] [3]. Preencher o fecho e puxá-lo para dentro pela largura da tabela dá a máscara da 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
Meça cada um
Uma bola vista de qualquer ângulo é um disco, e um disco é a forma mais redonda que existe. Um número para “redondo” vem de comparar a área com o perímetro: um círculo de raio r tem área πr² e perímetro 2πr, então 4π × área / perímetro² vale exatamente 1 para um círculo e menos para qualquer outra coisa. Um taco longo e fino marca perto de 0.
def roundness(c):
# 1.0 para um círculo, perto de 0 para um bastão
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) é o perímetro, e True quer dizer que a borda é fechada [2]. O maior contorno, com cerca de 20.800 pixels e circularidade 0,12, é o taco. Os dois seguintes, com 0,23 e 0,18, são aglomerados: as duas bolas amarelas com a caçapa atrás delas, e a bola vermelha no fundo com uma faixa de tabela. As bolas isoladas ficam entre 0,66 e 0,86, e as bolas laranja e preta, que se tocam, formam um blob só, com 0,63. Os dois menores são os pedaços brancos da bola de listra verde, cuja listra verde tem a cor do pano.
Fique com os 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 devolve o menor círculo em volta de um contorno, o que dá a cada bola um centro e um raio [2]. Onze blobs passam. A olho, oito são bolas isoladas, um é a laranja com a preta contadas como uma, e dois são os dois pedaços brancos da bola de listra verde. A bola verde ao lado deles nem aparece, porque tem a cor do pano (a lição 1 leu o matiz dela em 82). A lição 5 compara o resultado com uma contagem feita à mão nas três fotos e corrige o que dá.
Para testar uma foto da sua própria mesa, aperte “Usar sua imagem” na primeira célula desta página e depois Rodar nesta. Um pano de outra cor precisa da sua própria faixa de matiz, lida como na lição 1.
Quebre
Filtre só pelo tamanho e tudo que é grande o bastante passa:
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
Todos os 14 passam, incluindo o taco e os dois aglomerados. O tamanho diz quanto há de alguma coisa; só a forma diz o que ela é.
Sua vez
O taco é o maior contorno sobre a mesa. Guarde a circularidade dele em r.
r = None # troque esta linha
print(r)Por que funciona
Um contorno é a borda de uma região conectada de pixels brancos, e o OpenCV o encontra seguindo a beirada de cada região ao redor [3]. Área e perímetro são dois dos descritores básicos da forma de uma região [4], e a circularidade acima combina os dois de modo que o resultado não depende do tamanho do objeto na foto, o que importa quando uma bola perto da câmera cobre três vezes a área de uma no fundo. O fecho convexo é ele mesmo uma construção morfológica padrão [4]. A unidade de Fundamentos sobre imagens binárias (2.2) está planejada; até ela ser escrita, Gonzalez e Woods é a referência.
Erros comuns
- Objetos pretos sobre fundo branco.
findContourstraça regiões brancas. Inverta a máscara antes [3]. RETR_TREEouRETR_LISTquando você só quer objetos. Eles também devolvem as bordas dos buracos dentro de cada objeto, que acabam contadas como objetos.RETR_EXTERNALdevolve só as externas.- Filtrar só pela área. A célula Quebre fica com o taco. Acrescente um teste de forma.
- Um limiar de circularidade perto de 1. A borda de uma bola real é serrilhada na escala do pixel e muitas vezes cortada por um reflexo ou pela sombra; aqui as bolas isoladas marcam de 0,66 a 0,86. Deixe folga.
Referências
[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