Por que funciona: Unidade 1.3: A imagem como dado
Erosão, dilatação, abertura e fechamento de uma máscara no OpenCV
Limpe uma máscara de cor com morfologia: a erosão encolhe as áreas brancas, a dilatação as aumenta, a abertura remove pontinhos e o fechamento preenche buracos. Na mesa de sinuca a máscara vai de 59 blobs para 14.
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.
Uma máscara que acabou de sair de um teste de cor vem cheia de pontinhos e furinhos, e a morfologia a limpa encolhendo e aumentando as áreas brancas.

Onde você já viu isso: Selecionar › Encolher e Selecionar › Aumentar no GIMP, e Contrair e Expandir no Photoshop, que fazem erosão e dilatação de uma seleção; a remoção de pontinhos nos apps de scanner de documentos; a limpeza de um chroma key de tela verde.
TL;DR. Encolher as áreas brancas de uma máscara em alguns pixels e depois fazê-las crescer de volta apaga tudo que é menor que o encolhimento, e fazer o contrário preenche buracos pequenos. Na mesa de sinuca isso leva a máscara do que não é pano de 59 blobs brancos separados para 14, um por objeto, com as menores bolas intactas.
O problema
A lição 2 fez uma máscara de tudo que não é pano, o segundo passo para achar todas as bolas na mesa. Dentro da mesa ela chega perto de um blob branco por bola, mas não exatamente: pontinhos onde o pano tem um fio gasto, bolas partidas em pedaços pelas listras e uma faixa irregular ao longo das tabelas. A próxima lição conta blobs, então cada pontinho contaria como uma bola.
Conte os blobs
A primeira célula reconstrói a máscara e fica só com a mesa. Três das suas linhas acham o contorno da mesa; a lição 4 as 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))
# o contorno da mesa: a lição 4 explica estas três linhas
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
Uma máscara é um array de 0s e 255s como qualquer outra imagem (An image is an array, em inglês). cv2.connectedComponents dá a cada região branca separada o seu próprio número e devolve quantos números usou, contando o fundo preto como um; daí o - 1.
Erosão e dilatação
As duas deslizam um quadrado pequeno, o kernel, sobre a máscara. A erosão mantém um pixel branco só se todos os pixels sob o kernel forem brancos, então as áreas brancas perdem uma borda da largura de metade do kernel. A dilatação deixa um pixel branco se qualquer pixel sob o kernel for branco, então as áreas brancas ganham uma [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
| Chamada | O que faz com as áreas brancas |
|---|---|
cv2.erode(mask, kernel) | encolhe; tudo que é mais fino que o kernel desaparece |
cv2.dilate(mask, kernel) | aumenta; vãos mais estreitos que o kernel se fecham |
np.ones((5, 5), np.uint8) | um kernel quadrado 5×5 |
A erosão corta a contagem mais ou menos pela metade, porque os pontinhos somem, mas cada bola também ficou menor. A dilatação também reduz a contagem, pelo motivo oposto: pedaços vizinhos se juntaram num só.
Abra, depois feche
A abertura é uma erosão seguida de uma dilatação com o mesmo kernel: os pontinhos morrem na erosão e não voltam, e tudo que é maior cresce de volta até mais ou menos o tamanho que tinha. O fechamento é o inverso, uma dilatação e depois uma erosão, que preenche buracos e junta pedaços que quase se tocam [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 viram 15 depois da abertura e 14 depois do fechamento. Os tamanhos dos kernels são os dois ajustes que importam: o kernel da abertura tem de ser menor que a menor bola, e o kernel do fechamento maior que os vãos dentro de uma bola.
Puxe a mesa para dentro
O contorno da mesa passa por cima das tabelas, então uma faixa de tabela e as caçapas atrás das bolas perto da borda ainda ficam dentro dele. Fazer a erosão da própria mesa, com um kernel bem maior, puxa a borda dela para dentro em cerca de 9 pixels:
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
A contagem continua em 14, por coincidência: uma faixa de 3.575 pixels ao longo da tabela do fundo desaparece, e o blob na borda esquerda que continha as duas bolas amarelas, a caçapa atrás delas e um pedaço de tabela encolhe de 6.737 pixels para 4.815 e solta um pedaço separado. Esta é a máscara que o resto do módulo usa.
Quebre
O kernel da abertura decide o que conta como pontinho. Deixe-o maior que as bolas do fundo:
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
Sobram cinco blobs. As bolas no fundo da mesa aparecem menores na foto que as bolas perto da câmera, então elas vão primeiro, junto com os pontinhos. A lição 5 conta o que os ajustes finais do módulo acham e perdem em três fotos.
Sua vez
Ache o maior tamanho ímpar de kernel cuja abertura ainda mantém a bola vermelha em x = 494, y = 92.
size = 5 # mude isto
k = np.ones((size, size), np.uint8)
opened = cv2.morphologyEx(objects, cv2.MORPH_OPEN, k)
print(blobs(opened), opened[92, 494])Por que funciona
Erosão e dilatação são as duas operações básicas da morfologia matemática, e a abertura e o fechamento são definidos a partir delas [2]. A abertura remove toda região branca dentro da qual o kernel não cabe, e é por isso que o tamanho do kernel funciona como um limiar de tamanho. O fechamento faz o mesmo com as regiões pretas, então preenche buracos menores que o kernel. A trilha de Fundamentos trata de imagens binárias na unidade 2.2, que está planejada; até ela ser escrita, o capítulo 9 de Gonzalez e Woods é a referência [2].
Erros comuns
- Um kernel maior que o menor objeto. A abertura o apaga, como a célula Quebre mostra. Dimensione o kernel pela menor coisa que você precisa manter.
- Abertura e fechamento na ordem errada. Fechar primeiro junta os pontinhos às bolas antes que a abertura possa removê-los. Limpe o ruído primeiro, depois preencha os buracos.
- Objetos pretos sobre branco. A morfologia no OpenCV age sobre o branco. Para limpar objetos pretos, inverta a máscara antes ou troque erosão por dilatação.
Referências
[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