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.

Luis Condados · · 5 min de leitura Iniciante

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.

O contorno da mesa em branco, os contornos redondos em verde e o resto em vermelho.
O contorno da mesa em branco, os contornos redondos em verde e o resto em vermelho.

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.

A foto da sinuca ao lado da mesma foto com o contorno da mesa desenhado em branco, os contornos das bolas em verde e o contorno do taco em vermelho.
Direita: o contorno da mesa (branco), contornos redondos o bastante para ser uma bola (verde) e o resto (vermelho): o taco, as duas bolas amarelas grudadas na caçapa atrás delas e a bola vermelha no fundo grudada numa faixa de tabela. Foto de MarkBuckawicki, CC0 [5].

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:

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))
Saída
4 contours; biggest 571460.0
hull corners: 14
ChamadaO que faz
cv2.findContours(mask, mode, method)as bordas das regiões brancas e o seu aninhamento
cv2.RETR_EXTERNALsó as bordas externas; buracos dentro de uma região são ignorados
cv2.CHAIN_APPROX_SIMPLEguarda 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:

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")
Saída
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.

Python · OpenCV
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}")
Saída
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

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)
Saída
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 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:

Quebre
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))
Saída
kept by area alone: 14
all blobs
all blobs

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.

Sua vez
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. findContours traça regiões brancas. Inverta a máscara antes [3].
  • RETR_TREE ou RETR_LIST quando você só quer objetos. Eles também devolvem as bordas dos buracos dentro de cada objeto, que acabam contadas como objetos. RETR_EXTERNAL devolve 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