Este capítulo apresenta dois temas fundamentais do Processamento Digital de Imagens (PDI): a morfologia matemática e a segmentação de imagens. A morfologia matemática fornece um arcabouço teórico baseado na teoria dos conjuntos para analisar, refinar e quantificar a forma de objetos em imagens binárias e em tons de cinza, por meio de operadores fundamentais como erosão e dilatação. A segmentação, por sua vez, tem como objetivo particionar a imagem em regiões de interesse, separando objetos do fundo e produzindo representações adequadas para análise e interpretação.
O capítulo inicia com a limiarização, uma das técnicas mais importantes de segmentação, introduzindo o método automático de Otsu e revisitando a análise de histogramas por meio da variância interclasses, apresentada no Capítulo 1. Em seguida, são estudados os principais operadores da morfologia matemática, incluindo erosão, dilatação, abertura, fechamento e reconstrução morfológica, que permitem refinar máscaras binárias e preservar estruturas relevantes dos objetos. Por fim, são apresentadas técnicas de segmentação baseada em regiões, como a rotulação de componentes conexos, a transformada de distância e o algoritmo watershed baseado em marcadores, culminando na extração de descritores geométricos e na geração de bounding boxes compatíveis com sistemas modernos de detecção de objetos.
4.1 Objetivos
Ao final deste capítulo, você será capaz de:
Aplicar limiarização: Compreender o critério automático de Otsu por maximização da variância interclasses (\(\sigma_B^2\)) e selecionar estratégias adequadas de pré-processamento para facilitar a segmentação;
Dominar morfologia binária: Compreender e aplicar erosão (\(A\ominus B\)) e dilatação (\(A\oplus B\)) como operadores fundamentais, derivando abertura (\(A\circ B\)), fechamento (\(A\bullet B\)) e operações baseadas em reconstrução morfológica, como mm.clohole e mm.edgeoff;
Aplicar morfologia em tons de cinza: Utilizar gradiente morfológico e filtros top-hat para realce e análise de estruturas locais;
Rotular componentes conexos: Identificar e separar regiões conectadas em imagens binárias por meio de algoritmos de rotulação;
Aplicar transformada de distância: Interpretar e calcular distâncias ao fundo utilizando abordagens morfológicas e métricas geométricas;
Segmentar por regiões: Construir pipelines de segmentação baseados em marcadores utilizando Transformada de Distância e o algoritmo watershed;
Extrair descritores geométricos: Calcular propriedades como área, perímetro, centróide, circularidade e bounding boxes por meio de mm.label0 e extração de contornos;
Relacionar PDI e visão computacional: Compreender como descritores extraídos por segmentação podem ser convertidos para formatos utilizados por detectores modernos, como YOLO.
A limiarização (thresholding) é uma das formas mais simples e eficientes de segmentação de imagens. Seu objetivo é classificar cada pixel em duas classes de intensidade, normalmente associadas a objeto e fundo:
em que \(f(x,y)\) representa a intensidade do pixel na imagem original e \(g(x,y)\) a imagem binária resultante.
A escolha do limiar \(T\) é importante para a qualidade da segmentação. O método de Otsu determina automaticamente o limiar ótimo ao maximizar a variância interclasses\(\sigma_B^2\) e definida por:
\(w_0(T)\) e \(w_1(T)\) são as probabilidades acumuladas das classes fundo e objeto;
\(\mu_0(T)\) e \(\mu_1(T)\) são as médias de intensidade dessas classes;
\(\sigma_B^2(T)\) representa a variância interclasses para um dado limiar \(T\).
O método funciona melhor quando o histograma apresenta dois grupos de intensidades relativamente separados. Para isso, o algoritmo avalia todos os limiares possíveis da imagem — tipicamente no intervalo \([0,255]\) para imagens de 8 bits — e seleciona o valor que maximiza a variância entre classes, denotada por \(\sigma_B^2\):
O método de Otsu produz melhores resultados quando o histograma apresenta dois picos bem definidos (bimodalidade), correspondentes ao fundo e ao objeto. Quanto maior a separação entre esses picos e mais pronunciado o máximo de \(\sigma_B^2\), mais confiável tende a ser o limiar obtido.
Em imagens com iluminação não uniforme ou múltiplas regiões de intensidade, técnicas de limiarização adaptativa — nas quais o limiar é calculado localmente — costumam produzir segmentações mais robustas.
O índice \(B\) em \(\sigma_B^2\) significa between classes (entre classes). Assim, \(\sigma_B^2\) representa a variância entre as classes (between-class variance).
4.2.1 Imagem de Moedas
A imagem utilizada para praticar a segmentação é uma fotografia de coleção de moedas de diferentes países e épocas (Figura 4.1). Crédito: GAZI.MD.AHAD (CC BY-SA 4.0). Ela apresenta objetos circulares com bordas bem definidas, sendo ideal para demonstrar limiarização, operadores morfológicos, transformada de distância, watershed e descritores de forma.
Figura 4.1: Imagem com moedas de vários tipos. Crédito: GAZI.MD.AHAD (CC BY-SA 4.0).
4.2.2 Pré-processamento para o Otsu
A qualidade do método de Otsu depende diretamente de quão bimodal é o histograma da imagem de entrada. A imagem original das moedas apresenta iluminação não uniforme e moedas escuras (cobre oxidado) com intensidades próximas às do fundo escuro, tornando o histograma pouco bimodal.
Para minimizar essas limitações, serão avaliadas duas técnicas clássicas de pré-processamento, apresentadas no capítulo anterior, aplicadas antes da etapa de limiarização. A Tabela 4.1 resume as características de cada abordagem. Essas técnicas buscam aumentar a separação entre objeto e fundo, tornando o histograma mais próximo de uma distribuição bimodal.
Tabela 4.1: Técnicas de pré-processamento avaliadas para melhorar a separação entre moedas e fundo antes da aplicação do método de Otsu.
Técnica
O que faz
Quando usar
CLAHE
Equalização de histograma adaptativa local
Baixo contraste global ou regional
Gaussiano
Suavização por convolução com gaussiana
Ruído de alta frequência (textura do fundo)
A função cv2.createCLAHE(clipLimit, tileGridSize) divide a imagem em blocos e aplica equalização de histograma em cada um deles, limitando a amplificação de ruído por meio do parâmetro clipLimit. Já o filtro Gaussiano (cv2.GaussianBlur) suaviza texturas finas que poderiam criar falsos picos no histograma.
Para comparar objetivamente qual pré-processamento produz a melhor entrada para o método de Otsu, serão apresentados, para cada versão da imagem, a própria imagem, o histograma com o limiar ótimo \(T^*\) destacado, a curva \(\sigma_B^2(T)\) e o resultado da binarização.
NotaCritério de comparação
A versão que apresentar o maior valor de \(\sigma_B^2\) em seu pico fornece a melhor separação entre as classes de fundo e objeto e, consequentemente, a melhor entrada para o método de Otsu (Figura 4.2).
import cv2, ioimport matplotlib.pyplot as pltdef otsu_criterio(img): h=mm.hist(img); p=h/h.sum();# histograma e probabilidades sigma2=np.zeros(len(p)) for T inrange(1,len(p)): # percorre limiares w0,w1=p[:T].sum(),p[T:].sum() # probabilidades das classesif w0*w1==0: continue# evita divisão por zero mu0=(np.arange(T)*p[:T]).sum()/w0 # média fundo mu1=(np.arange(T,len(p))*p[T:]).sum()/w1 # média objeto sigma2[T]=w0*w1*(mu0-mu1)**2# σ²B(T)return sigma2,np.argmax(sigma2) # curva e T ótimodef fig2img(fig): b=io.BytesIO(); fig.savefig(b,format='png',dpi=100) # figura → buffer plt.close(fig); b.seek(0)return np.array(plt.imread(b)) # buffer → arraydef plot_curve(y,T,title,ylabel,color): fig,ax=plt.subplots(figsize=(4,3)) ax.plot(y,color=color) if ylabel=="σ²B"else ax.bar(range(len(y)),y,color=color,width=1) ax.axvline(T,color='red',lw=2,label=f"T*={T}") # limiar ótimo ax.set(xlabel="T"if ylabel=="σ²B"else"Intensidade",ylabel=ylabel) ax.legend(fontsize=8); plt.tight_layout()return fig2img(fig)# ── Pré-processamentos ───────────────────────────────────────────────────────img_clahe = mm.clahe(img_coins_gray, clipLimit=2.0, tiles=8)img_gauss = cv2.GaussianBlur(img_clahe,(5,5),0)imgs0=[("Original",img_coins_gray), ("CLAHE",img_clahe), ("CLAHE+Gauss",img_gauss)]# ── Tabela comparativa ───────────────────────────────────────────────────────print(f"{'Versão':<18}{'T*':>6}{'σ²B pico':>14}")print("-"*40)imgs,titles=[],[]for nome,img in imgs0: sigma2,T=otsu_criterio(img) # calcula σ²B(T)print(f"{nome:<18}{T:>6}{sigma2[T]:>14.4e}") imgs += [ img, # imagem plot_curve(mm.hist(img),T,f"Hist T*={T}","Freq.","steelblue"), plot_curve(sigma2,T,"σ²B(T)","σ²B","darkorange"), mm.threshold(img) # Otsu final ] titles += [nome,f"Hist T*={T}","σ²B(T)",f"Otsu T*={T}"]# ── Exibição final ───────────────────────────────────────────────────────────mm.show(imgs,titles=titles,cols=4,figsize=(12,12),dpi=200)
Versão T* σ²B pico
----------------------------------------
Original 105 1.9437e+03
CLAHE 122 2.4695e+03
CLAHE+Gauss 123 2.4283e+03
Figura 4.2: Comparação dos pré-processamentos: imagem | histograma+T* | σ²B(T) | Otsu. A melhor separação bimodal indica o limiar mais confiável.
4.2.3 Resultado: CLAHE como Melhor Pré-processamento
A análise da Figura 4.2 indica que o CLAHE obteve o maior valor da variância inter-classes (\(\sigma_B^2 \approx 2{,}47 \times 10^3\)), com limiar ótimo \(T^* = 122\). Embora a combinação CLAHE+Gaussiano tenha produzido resultado muito semelhante (\(\sigma_B^2 \approx 2{,}43 \times 10^3\), \(T^* = 123\)), o critério quantitativo do método de Otsu favorece ligeiramente o uso do CLAHE isoladamente.
Em termos visuais, as imagens binarizadas obtidas com CLAHE e CLAHE+Gaussiano são praticamente equivalentes. A diferença entre as duas abordagens torna-se mais evidente na análise dos histogramas e dos valores de \(\sigma_B^2(T)\) do que na inspeção direta das segmentações resultantes. Assim, a escolha do CLAHE baseia-se principalmente na maximização da separação estatística entre as classes de fundo e objeto.
DicaInterpretação dos resultados
Observe que os pré-processamentos com CLAHE e CLAHE+Gaussiano produzem histogramas e limiares ótimos muito próximos (\(T^*=122\) e \(T^*=123\)). Consequentemente, as imagens binarizadas resultantes também são bastante semelhantes. Nesse caso, a decisão não é baseada em diferenças visuais marcantes, mas no critério objetivo do método de Otsu: o maior valor de \(\sigma_B^2\) indica a melhor separação entre as classes.
4.3 Morfologia Matemática
A morfologia matemática é uma teoria baseada em conjuntos utilizada para analisar a forma e a estrutura de objetos em imagens. Diferentemente dos filtros lineares apresentados no Capítulo 3, os operadores morfológicos são não lineares, pois se baseiam em operações de mínimo, máximo e inclusão espacial, em vez de combinações lineares de intensidades. Esses operadores atuam sobre a vizinhança de cada pixel por meio de um elemento estruturante\(\mathbb{B}\), responsável por definir a forma e o tamanho da região analisada.
Em imagens binárias e em tons de cinza com elementos planos, o elemento estruturante transladado para a posição \(x\) é definido espacialmente como:
\[
\mathbb{B}_x = \{ x + b \mid b \in \mathbb{B} \}
\]
Nas regiões de borda da imagem, parte do conjunto \(\mathbb{B}_x\) pode extrapolar o domínio físico da cena (\(\mathbb{E}\)). Para garantir a consistência matemática dos operadores primitivos nessas fronteiras, assume-se teoricamente que o espaço exterior ao domínio da imagem é preenchido com o elemento neutro da operação correspondente (infinito positivo para a erosão e infinito negativo para a dilatação), impedindo que o ambiente externo corrompa as estruturas internas do objeto.
Quando o elemento estruturante associa pesos a seus elementos — isto é, \(b: \mathbb{B} \to \mathbb{Z}\) — ele é denominado função estruturante ou elemento estruturante não plano.
Desenvolvida por Matheron e Serra na década de 1960 para imagens binárias e posteriormente estendida a tons de cinza, a morfologia matemática fundamenta operadores como gradiente morfológico, top-hat, watershed e transformada de distância, todos derivados de dois primitivos: erosão e dilatação(Matheron, 1975; Serra, 1982).
4.3.1 Erosão e Dilatação
Os dois operadores primitivos são definidos de forma unificada para imagens em tons de cinza (\(f: \mathbb{E} \to \mathbb{Z}\)) e, por restrição ao domínio \(\{0,1\}\), também para imagens binárias.
4.3.1.1 Erosão
A Erosão de uma imagem \(f\) por uma função estruturante \(b: \mathbb{B} \to \mathbb{Z}\) é definida formalmente por:
Na prática, a erosão substitui a intensidade do pixel \(x\) pelo mínimo valor resultante da diferença entre a imagem e o elemento estruturante na vizinhança definida pelo domínio \(\mathbb{B}\). Valores positivos nos pesos de \(b(z)\) forçam o resultado local para baixo, “cavando” o relevo da imagem mais profundamente e intensificando a erosão.
No caso plano (onde os pesos são nulos dentro do domínio, ou seja, \(b \equiv 0\)), a expressão simplifica-se para o mínimo local puro:
Em imagens binárias, essa operação equivale a exigir que o conjunto \(\mathbb{B}\), transladado para a coordenada \(x\), esteja completamente contido no objeto \(A\):
\[
A \ominus \mathbb{B} = \{\, z \in \mathbb{E} \mid \mathbb{B}_z \subseteq A \,\}
\]
Efeito Visual:Encolhe objetos e estruturas claras, eliminando protuberâncias, picos brilhantes ou ruídos que sejam geometricamente menores que o domínio \(\mathbb{B}\).
4.3.1.2 Implementação da erosão
A versão didática mm.ero0 implementa o caso particular de erosão com elemento estruturante plano. Para cada pixel \((y,x)\), a função percorre os vizinhos espaciais permitidos por \(B\) e armazena o menor valor encontrado na imagem de entrada \(f\), reproduzindo diretamente a operação de mínimo local descrita na Equação 4.3 para \(b \equiv 0\).
Observe que os valores dos vizinhos são sempre lidos de forma estática da imagem original \(f\); a matriz de saída \(g\) é utilizada exclusivamente para registrar o mínimo acumulado da vizinhança corrente. Dessa forma, o resultado final é invariante em relação à ordem de varredura dos pixels (seja por linhas ou colunas).
A função auxiliar _viz calcula as coordenadas dos vizinhos válidos dentro dos limites físicos da imagem. Nas bordas, a inicialização do acumulador em 255 emula com exatidão o preenchimento por elemento neutro exigido pela teoria. Já a função de interface mm.ero recorre à implementação nativa e otimizada do OpenCV (mm.ero) quando o elemento estruturante é plano, chaveando para a rotina geral mm.ero1 caso o elemento possua pesos topográficos.
O exemplo computacional a seguir ilustra a aplicação de um elemento estruturante em cruz (mm.secross()) em destaque na Figura 4.3, comparando a execução da variante didática em laço (mm.ero0) com o motor computacional do OpenCV (mm.ero).
A função _viz utiliza yield para gerar cada vizinho sob demanda, sem armazenar todos os resultados em memória em uma lista. No experimento abaixo, uma janela de 3000×3000 produz 9 milhões de vizinhos. A implementação baseada em lista consumiu mais de 1 GB de RAM e levou aproximadamente 46 s, enquanto a versão com yield consumiu memória desprezível e executou em 38 s. Em processamento de imagens, geradores são especialmente úteis para percorrer grandes vizinhanças de forma eficiente.
import tracemallocdef lista(n): return [(i, i) for i inrange(n)]def gera(n):for i inrange(n): yield i, ifor nome, f in [("LISTA", lista), ("YIELD", gera)]: tracemalloc.start()sum(x+y for x,y in f(9_000_000)) _, pico = tracemalloc.get_traced_memory() tracemalloc.stop()print(f"{nome}: {pico/1024/1024:.1f} MB")
LISTA: 830.8 MB
YIELD: 0.0 MB
4.3.1.3 Dilatação
A Dilatação de uma imagem \(f\) por uma função estruturante \(b: \mathbb{B} \to \mathbb{Z}\) é definida formalmente por:
Na prática, a dilatação substitui a intensidade do pixel \(x\) pelo maior valor resultante da soma entre a imagem e o elemento estruturante na vizinhança definida. O argumento de inversão espacial (\(x - z\)) indica que a dilatação avalia implicitamente o elemento transposto (refletido) \(\hat{b}\), propriedade fundamental para assegurar a dualidade matemática em relação à erosão.
No caso plano (onde os pesos são nulos dentro do domínio, ou seja, \(b \equiv 0\)), a expressão reduz-se ao máximo local puro:
Em imagens binárias, essa operação equivale a exigir que o conjunto refletido \(\hat{\mathbb{B}}\), transladado para a coordenada \(x\), possua interseção não vazia com o objeto \(A\):
\[
A \oplus \mathbb{B} = \{\, z \in \mathbb{E} \mid \hat{\mathbb{B}}_z \cap A \neq \varnothing \,\}
\]
Efeito Visual:Expande as estruturas claras da imagem, aumentando o preenchimento de objetos, conectando componentes próximos e eliminando canais, fossas escuras ou vales que sejam geometricamente menores que o domínio \(\mathbb{B}\).
4.3.1.4 Implementação da dilatação
A versão didática mm.dil0 implementa o caso particular de dilatação com elemento estruturante plano. Para cada pixel \((y,x)\), a função percorre os vizinhos espaciais permitidos por \(B\) e armazena o maior valor encontrado na imagem de entrada \(f\), reproduzindo diretamente a operação de máximo local para \(b \equiv 0\).
Antes de iniciar a varredura espacial, o elemento estruturante sofre uma reflexão geométrica por meio da instrução np.flip(Bc) para construir explicitamente a matriz transposta \(\hat{B}\) exigida pela teoria. Em máscaras perfeitamente simétricas (como cruzes, quadrados e discos centrados na origem), essa reflexão não altera o arranjo dos pixels; contudo, para elementos assimétricos, tal etapa é estritamente necessária para garantir a equivalência com as definições formais e salvaguardar as leis de dualidade.
Assim como verificado no operador de erosão, os valores dos vizinhos são sempre lidos de forma estática a partir da matriz original \(f\), enquanto a matriz de saída \(g\) atua puramente como o registrador do máximo acumulado da vizinhança. Nas fronteiras da imagem, a inicialização do acumulador em 0 emula com exatidão o preenchimento externo por elemento neutro (\(-\infty\), ou zero em representações de 8 bits), garantindo que as bordas físicas da cena sejam dilatadas em perfeita conformidade com o padrão adotado pelo OpenCV.
O exemplo computacional abaixo ilustra a aplicação prática de um elemento em cruz (mm.secross()), validando a consistência entre a lógica em laços (mm.dil0) e o método nativo industrial (mm.dil).
def dil(f, Bc=np.zeros((3,3),dtype='uint8')):"""Dilatação (OpenCV ou com pesos)."""try: return cv2.dilate(f, Bc)except: return mm.dil1(f, Bc)def dil0(f, Bc=np.zeros((3,3),dtype='uint8')):"""Dilatação plana seguindo rigorosamente a teoria.""" g = np.empty_like(f) Bc = np.flip(Bc) # reflexão explícita: B̂for y inrange(f.shape[0]):for x inrange(f.shape[1]): g[y,x] =0# Inicializa com o valor mínimo para buscar o máximofor vy,vx,bv in mm._viz(f,Bc,y,x):if bv and g[y,x] < f[vy,vx]: g[y,x] = f[vy,vx]return g# Script de Teste e ValidaçãoB = mm.secross()print("Elemento estruturante B:")print(mm.drawImage(B))f = mm.randomImage(5,5)print("Imagem original f:")print(mm.drawImage(f))print("Dilatação Plana Didática (dil0):")print(mm.drawImage(dil0(f, B)))print("Dilatação Otimizada OpenCV (dil):")print(mm.drawImage(dil(f, B)))
NotaNota: O Confronto dos Sinais (\(f(x+z)\) vs \(f(x-z)\))
Compare as definições formais da erosão (Equação 4.3) e da dilatação (Equação 4.4). Considere um elemento estruturante assimétrico à direita \(\mathbb{B}=\{0,1\}\) (origem e um pixel à direita) aplicado na posição \(x=10\).
Devido ao sinal negativo (\(-z\)), avançar no elemento estruturante corresponde a recuar na imagem, fazendo com que a dilatação consulte o pixel à esquerda (\(9\)).
A função _viz, utilizada em morph.py, gera vizinhos por deslocamentos aditivos da forma \(x+z\). Por esse motivo, a implementação de mm.dil0 reflete previamente o elemento estruturante por meio de np.flip(B). Após a reflexão, a varredura baseada em \(x+z\) passa a acessar exatamente os mesmos pontos definidos pela expressão teórica \(f(x-z)\) da dilatação em Equação 4.4.
Para elementos estruturantes simétricos (como discos, quadrados e cruzes centradas), a reflexão não altera a máscara. Já para elementos assimétricos, essa etapa é indispensável para que a implementação reproduza corretamente a definição matemática da dilatação e preserve a dualidade erosão–dilatação.
NotaDualidade erosão–dilatação
Erosão e dilatação são duais pelo complemento. Isso significa que um operador pode ser completamente obtido a partir do outro, desde que se atue sobre o complemento da imagem utilizando o elemento estruturante refletido \(\hat{B}\):
\[
(A \ominus B)^c = A^c \oplus \hat{B} \quad \Longleftrightarrow \quad A \ominus B = (A^c \oplus \hat{B})^c
\]
De forma análoga, a dilatação também pode ser obtida a partir da erosão:
\[
(A \oplus B)^c = A^c \ominus \hat{B} \quad \Longleftrightarrow \quad A \oplus B = (A^c \ominus \hat{B})^c
\]
Em termos práticos, a erosão de um objeto pode ser obtida pela dilatação de seu complemento, seguida da complementação do resultado (e vice-versa). Na implementação do pacote morph.py, as versões didáticas mm.ero0 e mm.dil0 tornam explícita essa estrutura por meio de laços (loops), enquanto mm.ero e mm.dil delegam as operações ao OpenCV visando maior eficiência computacional.
NotaCondições de contorno e imagens finitas
Na morfologia matemática clássica, definida sobre um domínio infinito (tipicamente \(\mathbb{Z}^2\)), essa dualidade é exata. Em imagens digitais, entretanto, trabalha-se com matrizes finitas, e o resultado passa a depender da forma como são tratados os pixels localizados fora da imagem.
Para que as identidades de dualidade permaneçam válidas, o complemento deve ser definido em relação ao mesmo universo e as condições de contorno adotadas para a erosão e para a dilatação devem ser complementares entre si. Por exemplo, se a erosão assume que os pixels externos pertencem ao objeto (\(255\)), então a dilatação aplicada ao complemento deve assumir que esses mesmos pixels externos pertencem ao fundo (\(0\)).
Quando diferentes estratégias de preenchimento são utilizadas (replicação, reflexão, valor constante etc.), a dualidade teórica pode deixar de ser satisfeita exatamente nas regiões próximas às bordas da imagem.
Para ilustrar numericamente os operadores morfológicos e a dualidade erosão–dilatação, a Figura 4.4 apresenta uma imagem binária 10×10 processada com um elemento estruturante em formato de “L”. Na implementação de morph.py, a origem de \(B\) é fixada no centro geométrico da máscara — posição \((1,1)\) para um kernel 3×3 — e deve corresponder a um elemento ativo para que a erosão se comporte corretamente (conforme discutido anteriormente). O elemento estruturante \(B_L\) definido a seguir satisfaz essa condição. A Figura 4.5 complementa a análise com um simulador interativo da erosão, permitindo visualizar o deslocamento do elemento estruturante sobre a imagem e identificar as posições em que ele permanece completamente contido no objeto.
Figura 4.4: Erosão e dilatação em imagem binária 10×10 com elemento estruturante ‘L’ assimétrico 3×3. Validação da dualidade erosão–dilatação.
🪨 Simulador: Erosão MorfológicaA ⊖ B_L · offsets via _viz
Clique numa célula do canvas para mover o kernel B_L ou utilize os controles deslizantes para testar a inclusão de contidos.
Posição X (col)
4
Posição Y (lin)
4
Pixel Erosão
255
🖱️ Clique numa célula para mover o kernel B_L
🟢
Sucesso: contido!
Pixel recebe 1 (255) na imagem erodida.
Controles de Coordenada
4
4
Kernel B_L (3×3)
1
0
0
1
★
0
1
1
0
Figura 4.5: Simulador: Erosão Morfológica (A ⊖ B_L)
4.3.2 Abertura e Fechamento
Combinando erosão e dilatação obtêm-se dois operadores de grande utilidade prática: a abertura e o fechamento, definidos pelas Equações Equação 4.5 e Equação 4.6. Seus principais efeitos são resumidos na Tabela 4.2.
Abertura (opening) — erosão seguida de dilatação pelo mesmo \(B\):
\[
A \circ B = (A \ominus B) \oplus B
\tag{4.5}\]
Fechamento (closing) — dilatação seguida de erosão pelo mesmo \(B\):
\[
A \bullet B = (A \oplus B) \ominus B
\tag{4.6}\]
Na prática, mm.open e mm.close aplicam o mesmo elemento estruturante nas duas etapas. Para elementos estruturantes simétricos (os mais comuns), essa implementação coincide com a definição matemática apresentada acima.
Tabela 4.2: Propriedades de abertura e fechamento.
Operador
Sequência
Efeito principal
Abertura \(A \circ B\)
erosão → dilatação
Remove estruturas incapazes de conter o elemento estruturante; suaviza contornos externos
Fechamento \(A \bullet B\)
dilatação → erosão
Preenche buracos menores que \(B\); suaviza contornos internos
Propriedade importante: ambos são idempotentes. Por exemplo,
\[
(A \circ B) \circ B = A \circ B,
\]
ou seja, após a primeira aplicação, novas aplicações do mesmo operador não alteram mais o resultado.
4.3.2.1 Implementação da abertura e do fechamento
Diferentemente da erosão e da dilatação, abertura e fechamento não introduzem novos mecanismos computacionais. Ambos são obtidos pela composição sequencial dos operadores primitivos já apresentados:
A função mm.open delega a operação para mm.open(f, B), enquanto mm.close utiliza mm.close(f, B), produzindo o mesmo resultado de forma mais eficiente.
A abertura herda da erosão a capacidade de remover estruturas menores que o elemento estruturante e da dilatação a restauração parcial das regiões preservadas. O fechamento realiza o processo inverso: primeiro expande os objetos e depois restaura suas dimensões originais, preenchendo lacunas e buracos menores que o elemento estruturante.
4.3.2.2 Filtro Sequencial Alternado
Na prática, abertura e fechamento são frequentemente aplicados em sequência para remover simultaneamente ruído externo e preencher buracos internos. A função mm.asf (Alternating Sequential Filter) generaliza essa estratégia ao aplicar aberturas e fechamentos alternadamente com elementos estruturantes progressivamente maiores. As sequências disponíveis são apresentadas na Tabela 4.3.
Tabela 4.3: Sequências do filtro sequencial alternado mm.asf.
Sequência
Ordem
Uso típico
'OC'
abertura → fechamento
remove ruído externo antes de preencher pequenos buracos
'CO'
fechamento → abertura
preenche pequenos buracos antes de remover ruído externo
'OCO'
abertura → fechamento → abertura
enfatiza a remoção de ruído externo
'COC'
fechamento → abertura → fechamento
enfatiza o preenchimento de buracos e lacunas
O parâmetro n controla o número de escalas utilizadas pelo filtro. Em cada iteração \(i\), o elemento estruturante é ampliado por soma de Minkowski (mm.sesum(b, i)), produzindo uma sequência de filtros morfológicos cada vez mais abrangentes. Diferentemente de uma única abertura ou fechamento com um elemento estruturante grande, o ASF realiza uma suavização progressiva em múltiplas escalas, preservando melhor a geometria dos objetos relevantes enquanto elimina estruturas menores. A Figura 4.6 apresenta um exemplo de aplicação da abertura, do fechamento e do ASF na imagem das moedas.
Figura 4.6: Abertura, fechamento, composição e filtro sequencial alternado aplicados à binarização Otsu das moedas (pré-processadas por realce de contraste). Elemento estruturante: disco 13×13.
4.3.3 Operadores Geodésicos
Os operadores geodésicos introduzem uma restrição adicional aos operadores morfológicos clássicos por meio de uma imagem de controle denominada máscara\(g\). Em vez de permitir que a erosão ou a dilatação se propaguem livremente pela imagem, o resultado de cada iteração é limitado ponto a ponto pelos valores da máscara, restringindo a evolução da operação às regiões permitidas.
4.3.3.1 Dilatação Geodésica
A dilatação geodésica de uma imagem marcador \(f\) sob uma imagem máscara \(g\), utilizando um elemento estruturante plano \(b\), é definida por:
\[
f \oplus_g b = (f \oplus b) \wedge g,
\tag{4.7}\]
onde \(\wedge\) representa o mínimo ponto a ponto.
Em outras palavras, realiza-se inicialmente uma dilatação convencional sobre o marcador e, em seguida, o resultado é restringido pela máscara \(g\). Dessa forma, a propagação nunca pode ultrapassar as regiões permitidas pela máscara.
A formulação clássica da dilatação geodésica pressupõe que o marcador esteja contido na máscara, isto é, \(f \le g\), garantindo que a evolução da operação permaneça sempre limitada pela máscara.
4.3.3.2 Implementação da dilatação geodésica
A função mm.cdil implementa diretamente esse operador e permite executar múltiplas iterações consecutivas:
def cdil(f, g, b=np.zeros((3,3),dtype='uint8'), n=1):"""Dilatação geodésica do marcador f sob a máscara g.""" y = f.copy()for _ inrange(n): y = np.minimum(mm.dil(y, b), g)return y
A instrução np.minimum(mm.dil(y, b), g) implementa exatamente a definição matemática da dilatação geodésica, isto é, \((y \oplus b)\wedge g\).
Quando \(n=1\), a função executa uma única dilatação geodésica. Para \(n>1\), o resultado de cada etapa torna-se o marcador da etapa seguinte, produzindo uma propagação progressiva controlada pela máscara.
O simulador interativo Figura 4.7 permite acompanhar, passo a passo, a propagação do marcador \(f\) ao longo dos corredores do labirinto. A cada iteração de mm.cdil, a frente de dilatação avança para as células vizinhas livres — aquelas em que \(g = 1\) —, enquanto as paredes (\(g = 0\)) permanecem intransponíveis. O número de passos necessários para que o marcador alcance a saída corresponde exatamente ao comprimento geodésico do caminho mais curto dentro da máscara, evidenciando a conexão direta entre a dilatação geodésica iterada e a noção de distância em grafos.
🗺️ Simulador: Dilatação Geodésica no Labirintoδ_g^(n)(f)
Parede
Caminho livre (g)
Marcador f
Propagação
Saída
Passo 0 — marcador inicial f (entrada)
Figura 4.7: Simulador interativo de dilatação geodésica: o marcador f (verde) propaga-se passo a passo pelos caminhos livres da máscara g, sem atravessar paredes.
4.3.3.3 Erosão Geodésica
De forma dual, a erosão geodésica de uma imagem marcador \(f\) sob uma imagem máscara \(g\) é definida por:
\[
f \ominus_g b = (f \ominus b) \vee g,
\tag{4.8}\]
onde \(\vee\) representa o operador de máximo ponto a ponto.
Nesse caso, a erosão convencional do marcador é seguida por uma restrição inferior imposta pela máscara. Assim, nenhum pixel do resultado pode assumir valor inferior ao correspondente pixel da máscara.
A formulação clássica da erosão geodésica pressupõe a condição dual
\[
f \ge g,
\]
de modo que a máscara atue como limite inferior durante todo o processo.
4.3.3.4 Implementação da erosão geodésica
A função estática mm.cero materializa esse operador:
@staticmethoddef cero(f, g, b=np.zeros((3,3),dtype='uint8'), n=1):"""Erosão geodésica do marcador f sob a máscara g.""" y = f.copy()for _ inrange(n): y = np.maximum(mm.ero(y, b), g)return y
A instrução np.maximum(mm.ero(y, b), g) implementa diretamente a expressão \((y \ominus b)\vee g\).
Assim como na dilatação geodésica, o parâmetro \(n\) define quantas erosões geodésicas sucessivas serão computadas antes de retornar a imagem final.
NotaRelação com a reconstrução morfológica
A reconstrução morfológica apresentada na próxima seção é obtida pela aplicação iterativa da dilatação geodésica (mm.cdil) até que um ponto fixo seja alcançado, isto é, até que nenhuma célula mude de valor entre duas iterações consecutivas. Em outras palavras, a reconstrução consiste em uma sequência de dilatações geodésicas sucessivas que se propagam dentro da máscara até não haver mais alterações.
De forma dual, também é possível definir reconstruções baseadas em erosão geodésica por meio de aplicações sucessivas de mm.cero.
4.3.3.5 Exemplo: propagação em um labirinto via dualidade
A Figura 4.8 ilustra a resolução do problema de conectividade de um labirinto utilizando a dualidade morfológica por meio das funções mm.cero e mm.suprec.
Em vez de propagar um marcador pelos corredores livres através de dilatações geodésicas, o problema é formulado no domínio complementar. Inicialmente, a máscara original é invertida,
\[
g = 1 - g_{orig},
\]
fazendo com que as paredes passem a assumir valor 1 e os corredores valor 0. De forma análoga, o marcador é construído nesse mesmo domínio complementar, contendo um único valor 0 na posição de entrada do labirinto e valor 1 nos demais pixels.
Utilizando o elemento estruturante em cruz (mm.secross()), a erosão geodésica atua sobre o marcador complementado. A cada iteração de mm.cero, a região conectada ao marcador inicial sofre erosões sucessivas, enquanto a máscara impõe um limite inferior que impede a propagação através das paredes do labirinto.
As imagens intermediárias mostram estados da evolução após diferentes números de iterações (n=5, n=12 e n=22). O resultado final é obtido pela reconstrução geodésica por erosão (mm.suprec), que aplica erosões geodésicas sucessivas até atingir um ponto fixo, isto é, uma situação em que nenhuma alteração adicional ocorre entre duas iterações consecutivas.
No domínio complementar, a região reconstruída corresponde exatamente ao conjunto de corredores conectados à entrada do labirinto. Assim, a conectividade entre entrada e saída pode ser determinada diretamente a partir da imagem reconstruída.
import numpy as npimport matplotlib.pyplot as pltimport matplotlib.colors as mcolors# 1. Máscara original g (0 = Parede, 1 = Corredor)g_orig = np.array([ [0, 1, 0, 0, 0, 0, 0, 0, 0, 0], [0, 1, 1, 1, 1, 1, 0, 1, 1, 1], [0, 0, 0, 0, 0, 1, 0, 1, 0, 1], [0, 1, 1, 1, 0, 1, 1, 1, 0, 1], [0, 1, 0, 1, 0, 0, 0, 0, 0, 1], [0, 1, 0, 1, 1, 1, 1, 1, 1, 1], [0, 1, 0, 0, 0, 0, 0, 0, 1, 0], [0, 1, 1, 1, 1, 1, 1, 0, 1, 0], [0, 0, 0, 0, 0, 0, 1, 1, 1, 0], [0, 0, 0, 0, 0, 0, 0, 0, 1, 0]], dtype=np.uint8)# Inversão global no início (Dualidade Morfológica)g =1- g_orig # Agora: 1 = Parede, 0 = Corredorf = np.ones((10, 10), dtype=np.uint8)f[0, 1] =0# Semente injetada como 0 no corredor# Elemento estruturante em cruzB_cruz = mm.secross()# Processamento direto usando mm.cero e mm.suprec no domínio invertidopasso_5 = mm.cero(f, g, B_cruz, n=5)passo_12 = mm.cero(f, g, B_cruz, n=12)passo_22 = mm.cero(f, g, B_cruz, n=22)ponto_fixo = mm.suprec(f, g, B_cruz)# --- CONFIGURAÇÃO DA EXIBIÇÃO GRÁFICA ---titles = ["Máscara (~g)", "Marcador (~f)", "Avanço (n=5)", "Avanço (n=12)", "Avanço (n=22)", "~mm.suprec"]images = [g, f, passo_5, passo_12, passo_22, ponto_fixo]fig, axes = plt.subplots(1, 6, figsize=(16, 4), facecolor='#fcfcfc')# Mapa de cores adaptado para o domínio complementar:# No domínio invertido: 1 = Parede (Azul Escuro)# Onde g == 0 e imagem == 1 = Corredor Livre (Cinza Claro)# Onde g == 0 e imagem == 0 = Onda Geodésica Ativa (Ouro)cmap_pipeline = mcolors.ListedColormap(['#ffc13b', '#e0e0e0', '#1e3d59'])for i, ax inenumerate(axes):if i ==0or i ==1:# Para as condições iniciais (~g e ~f) cmap_init = mcolors.ListedColormap(['#e0e0e0', '#1e3d59']) ax.imshow(images[i], cmap=cmap_init, vmin=0, vmax=1)else:# Renderização baseada nos estados complementares render_step = np.zeros_like(g, dtype=np.uint8) render_step[g ==1] =2# Parede (1 original do mapa de cores) render_step[g ==0] =1# Corredor padrão render_step[images[i] ==0] =0# Onda ativa (0 original do mapa de cores) ax.imshow(render_step, cmap=cmap_pipeline, vmin=0, vmax=2) ax.set_title(titles[i], fontsize=10, fontweight='bold', color='#1e3d59', pad=12) ax.set_xticks(np.arange(-0.5, 10, 1), minor=True) ax.set_yticks(np.arange(-0.5, 10, 1), minor=True) ax.grid(which='minor', color='#ffffff', linestyle='-', linewidth=1) ax.tick_params(which='both', bottom=False, left=False, labelbottom=False, labelleft=False)# Indicadores gráficos de Entrada e Saída ax.plot(1, 0, marker='v', color='#2ecc71', markersize=7, markeredgewidth=1.5) ax.plot(8, 9, marker='o', color='#e74c3c', markersize=6, fillstyle='none', markeredgewidth=2)plt.tight_layout()plt.show()
Figura 4.8: Resolução de labirinto via Operações Complementares: Invertendo a máscara e o marcador no início do pipeline, o fluxo é resolvido diretamente no domínio complementar através de mm.cero e mm.suprec, eliminando re-inversões redundantes.
4.3.4 Reconstrução Morfológica
A reconstrução morfológica propaga uma imagem marcador\(f\) dentro de uma imagem máscara\(g\), garantindo que o resultado nunca ultrapasse os valores de intensidade impostos pela máscara. O operador fundamental que viabiliza essa propagação contida é a dilatação geodésica, definida pela Equação 4.7.
A reconstrução é obtida pela aplicação iterativa dessa dilatação condicionada. Inicialmente, o marcador é limitado pela máscara para estabelecer o estado inicial:
\[
X^{(0)} = f \wedge g,
\]
e as iterações subsequentes são definidas de forma recursiva por:
\[
X^{(k)} = (X^{(k-1)} \oplus b) \wedge g.
\]
A sequência cresce monotonicamente até atingir um ponto fixo, produzindo a reconstrução morfológica por dilatação (também conhecida na literatura como inf-reconstrução):
A subida iterativa é interrompida assim que a estabilidade é alcançada, isto é, quando duas iterações consecutivas produzem matrizes com valores absolutamente idênticos.
4.3.4.1 Implementação da reconstrução morfológica
A rotina didática mm.infrec implementa diretamente o algoritmo iterativo de ponto fixo. Inicialmente, o marcador efetivo inicial \(X^{(0)}\) é determinado pela operação np.minimum(f, g). Para garantir que o laço de verificação execute a primeira passada sem disparar falsas convergências prematuras, a variável de controle da iteração anterior (y1) é inicializada preenchida com um valor sentinela fora do domínio de dados (ou simplesmente com uma matriz que force a primeira execução).
def infrec(f, g, b=np.zeros((3,3), dtype='uint8')):"""Inf-reconstrução: dilata o marcador (f ∧ g) até convergir sob a máscara g.""" y = np.minimum(f, g)# Inicializa y1 com valores impossíveis para forçar a entrada no laço y1 = np.full_like(f, 256, dtype=np.int16) whilenot np.array_equal(y, y1): y1 = y.copy()# Aplica a dilatação geodésica: (y ⊕ b) ∧ g y = np.minimum(mm.dil(y, b), g)return y.astype('uint8')
No interior do laço while, a variável y armazena a estimativa corrente da reconstrução \(X^{(k)}\), enquanto y1 preserva a imagem do estágio imediatamente anterior \(X^{(k-1)}\). A instrução de controle condicional np.minimum(mm.dil(y, b), g) traduz fielmente a dilatação geodésica teórica, onde a expansão morfológica convencional comandada pelo OpenCV é imediatamente “podada” e limitada pelas barreiras de intensidade da máscara \(g\). O laço cessa quando nenhuma modificação de pixel é registrada entre os passos.
4.3.4.2 Vantagens da reconstrução morfológica
A reconstrução morfológica é significativamente mais robusta que a abertura convencional porque é capaz de remover estruturas indesejadas sem distorcer ou alterar a morfologia dos objetos que devem ser preservados.
Enquanto a abertura clássica suaviza cantos, elimina pontas e deforma contornos devido à imposição geométrica rígida do elemento estruturante, a reconstrução geodésica utiliza a máscara para recuperar com exatidão os limites e formatos originais dos objetos que possuem conectividade com o marcador original.
Em termos intuitivos, o marcador atua como uma semente de contágio que se expande progressivamente, mas apenas trafegando pelas regiões permitidas pela máscara. Componentes que não possuem nenhuma intersecção com o marcador jamais serão reconstruídas (sendo eliminadas), enquanto as componentes tocadas pela semente expandem-se até restaurar integralmente a sua geometria original.
Esse comportamento discriminatório e conservativo é ilustrado na Figura 4.9, utilizando o elemento estruturante em cruz apresentado na Figura 4.3.
Figura 4.9: Pipeline de Reconstrução Morfológica por Dilatação Condicionada: a máscara contém dois objetos, o marcador isola apenas o núcleo do objeto principal, e as iterações reconstroem sua forma exata até a convergência.
4.3.5 Preenchimento de Buracos e Remoção de Bordas
Dois operadores baseados em reconstrução morfológica completam o pipeline de limpeza binária. Suas características principais são resumidas na Tabela 4.4.
Preenchimento de buracos (mm.clohole) remove cavidades completamente cercadas pelo objeto, independentemente do tamanho, sem alterar os contornos externos. O procedimento atua no complemento da imagem, utilizando como marcador uma restrição da moldura (frame) ao fundo:
Em termos operacionais, primeiro reconstrói-se o fundo externo e, em seguida, aplica-se a complementação para recuperar os objetos com buracos preenchidos.
Remoção de objetos de borda (mm.edgeoff) elimina todos os objetos que tocam a borda da imagem, preservando apenas os componentes totalmente internos. O marcador é obtido pela interseção entre a moldura (frame) e os objetos da imagem:
\[
\text{edgeoff}(f) = f \setminus R_f^\delta(\text{frame}(f) \wedge f)
\tag{4.11}\]
Tabela 4.4: Comparação entre os operadores clohole e edgeoff.
Operador
Marcador
Máscara
Efeito
mm.clohole
frame restrito ao fundo (\(f^c\))
\(f^c\)
Preenche buracos internos
mm.edgeoff
frame restrito ao objeto (\(f\))
\(f\)
Remove objetos conectados à borda
A evolução passo a passo dessas transformações geodésicas pode ser acompanhada nas figuras a seguir. A Figura 4.10 ilustra o mecanismo de inundação controlada do operador mm.clohole, no qual a reconstrução ocorre a partir do fundo externo e impede a propagação para regiões internas não conectadas ao exterior, resultando no preenchimento consistente das cavidades internas. Em contrapartida, a Figura 4.11 detalha a dinâmica do operador mm.edgeoff, em que apenas os componentes conectados à borda são reconstruídos e posteriormente removidos, preservando exclusivamente os objetos totalmente contidos no interior da imagem.
A conectividade da propagação geodésica é controlada pelo elemento estruturante: mm.sebox() (vizinhança de 8) inclui conexões diagonais, enquanto mm.secross() (vizinhança de 4) as exclui. Consequentemente, a escolha do elemento estruturante afeta quais componentes são alcançados pela reconstrução e, portanto, quais serão preservados ou removidos.
4.3.5.1 Conformidade com a implementação
As definições acima estão diretamente alinhadas com a implementação em morph.py, reproduzida a seguir:
@staticmethoddef clohole(f, b=np.ones((3,3),dtype='uint8')):# marcador restrito ao fundo da imagem marcador = mm.frame(f, border=1) & mm.neg(f)return mm.neg(mm.infrec(marcador, mm.neg(f), b))@staticmethoddef edgeoff(f, b=np.ones((3,3),dtype='uint8')):# marcador restrito aos objetos da imagem marcador = mm.frame(f, border=1) & freturn mm.subm(f, mm.infrec(marcador, f, b))
Essas implementações deixam explícito que ambos os operadores são instâncias diretas de reconstrução morfológica por dilatação geodésica com mm.infrec, diferindo apenas na escolha do marcador e da máscara: clohole atua no complemento da imagem, enquanto edgeoff atua diretamente no domínio dos objetos.
Figura 4.10: Pipeline de preenchimento de buracos (clohole): o marcador vem da borda da imagem, restrito ao complemento \(f^c\). A dilatação geodésica reconstrói o fundo externo; após a complementação, os buracos internos ficam preenchidos.
Figura 4.11: Pipeline de eliminação de estruturas de borda (edgeoff): o marcador captura as raízes conectadas às extremidades, a reconstrução delimita esses elementos e a subtração preserva só os objetos totalmente internos.
4.3.6Pipeline de Limpeza Binária com CLAHE
Com base na análise anterior — na qual o CLAHE produziu o maior valor da variância interclasses (\(\sigma_B^2 \approx 2{,}47 \times 10^3\)), ver Figura 4.2, e o operador mm.clohole mostrou-se eficaz no preenchimento das cavidades internas —, o pipeline final de segmentação, ilustrado na Figura 4.12, é estruturado pelo seguinte fluxo computacional:
Após a etapa de mm.clohole, aplica-se uma segunda abertura morfológica com um elemento estruturante maior (mm.sedisk(33), disco de diâmetro 33). Essa operação remove pequenas regiões residuais e artefatos que possam ter permanecido após a segmentação. Em particular, o preenchimento geodésico pode transformar pequenas cavidades isoladas em componentes conectados ao objeto, tornando conveniente uma etapa adicional de filtragem baseada em tamanho. O diâmetro foi escolhido de modo que as moedas permaneçam capazes de conter o elemento estruturante, enquanto componentes significativamente menores sejam eliminados.
Nesta imagem, nenhuma moeda está conectada à borda da matriz. Consequentemente, a aplicação de mm.edgeoff não altera o resultado obtido após a segunda abertura. Ainda assim, essa etapa é mantida no pipeline por robustez, pois em outras imagens podem existir objetos parcialmente visíveis ou conectados às bordas, que devem ser removidos antes da etapa de análise.
DicaPor que a abertura após o clohole?
O operador mm.clohole preenche todas as cavidades fechadas presentes nos objetos segmentados. Em algumas situações, pequenas regiões indesejadas podem permanecer após essa etapa ou tornar-se conectadas aos objetos principais. A abertura morfológica subsequente remove componentes menores que o elemento estruturante, preservando as moedas devido ao seu tamanho significativamente maior.
# ── Pré-processamento: apenas CLAHE img_clahe0 = mm.clahe(img_coins_gray, 2.0, 8)# ── Etapa 1: Binarização Otsuimg_bin = mm.threshold(img_clahe0)# ── Etapa 2: Abertura — remove ruídos brancos de fundoimg_open = mm.open(img_bin, mm.sedisk(9))# ── Etapa 3: clohole — fecha todos os buracos internosimg_hole = mm.clohole(img_open)# ── Etapa 4: Abertura (kernel grande) — remove artefatos do cloholeimg_limpo = mm.open(img_hole, mm.sedisk(33))# ── Etapa 5: edgeoff — remove objetos que tocam a borda img_final = mm.edgeoff(img_limpo, border=1)mm.show( [img_clahe0, img_bin, img_open, img_hole, img_limpo, img_final], titles=["CLAHE", "Otsu", "Abertura (r=9)","clohole", "Abertura (r=33)", "Final (edgeoff)"], cols=6, figsize=(18, 6))
Figura 4.12: Pipeline completo de segmentação com CLAHE: Otsu → abertura (r=9) → clohole → abertura (r=33) → edgeoff.
4.3.7 Morfologia em Tons de Cinza
Os operadores morfológicos estendem-se naturalmente para imagens em tons de cinza. Nessa formulação, a erosão e a dilatação passam a atuar diretamente sobre os níveis de intensidade da imagem. Para elementos estruturantes planos (\(b \equiv 0\)), a erosão corresponde ao mínimo local e a dilatação ao máximo local dentro da vizinhança definida pelo elemento estruturante.
A interpretação intuitiva é simples: a erosão escurece regiões ao substituir cada pixel pelo menor valor presente em sua vizinhança, enquanto a dilatação clareia regiões ao utilizar o maior valor disponível. A combinação desses operadores permite construir transformações capazes de realçar bordas, remover tendências de iluminação e destacar estruturas locais.
Três operadores derivados são especialmente úteis:
Gradiente morfológico — destaca bordas como a diferença entre dilatação e erosão:
\[
\text{grad}_B(f) = (f \oplus B) - (f \ominus B)
\tag{4.12}\]
Top-hat — destaca estruturas brilhantes menores que o elemento estruturante (diferença entre a imagem original e sua abertura):
\[
\text{top-hat}_B(f) = f - (f \circ B)
\tag{4.13}\]
Black-hat — destaca estruturas escuras menores que o elemento estruturante (diferença entre o fechamento e a imagem original):
\[
\text{black-hat}_B(f) = (f \bullet B) - f
\tag{4.14}\]
O Top-hat extrai detalhes brilhantes que não sobrevivem à abertura, enquanto o Black-hat evidencia detalhes escuros removidos pelo fechamento. Já o gradiente morfológico realça transições abruptas de intensidade, produzindo uma representação semelhante à de um detector de bordas.
Para compreender o mecanismo desses operadores em nível local, a Figura 4.13 apresenta um simulador interativo de morfologia em tons de cinza. O simulador permite editar livremente o elemento estruturante, visualizar seu deslocamento sobre a imagem e acompanhar simultaneamente o perfil unidimensional das intensidades. Dessa forma, torna-se possível observar diretamente como a erosão seleciona mínimos locais, como a dilatação seleciona máximos locais e como o gradiente morfológico emerge da diferença entre esses dois operadores.
O botão localizado no canto superior direito permite alternar entre a visualização original em tons de cinza e uma representação pseudocolorida (colormap) apenas nos três tipos gradientes. A versão colorida facilita a percepção visual das variações de intensidade, tornando mais evidente a ação dos operadores morfológicos sobre máximos, mínimos e transições locais da imagem.
Os operadores apresentados estão disponíveis em morph.py por meio das funções mm.gradm, mm.tophat e mm.blackhat.
🎛️ Simulador Avançado de Morfologia Matemática
🖱️ Mova o mouse para atualizar o perfil 1D da linha correspondente
X (col)
—
Y (lin)
—
f(x,y)
—
valor
—
Elemento B (Clique para Editar)
Abertura (f∘B): dil(ero(f)) Fechamento (f•B): ero(dil(f)) Gradiente: dil(f) − ero(f) Top-hat: f − (f∘B) Black-hat: (f•B) − f
Perfil 1D da linha: Nenhuma (passe o mouse sobre a imagem)
Figura 4.13: Simulador interativo avançado de morfologia com elemento estruturante editável e perfil 1D.
A Figura 4.14 ilustra os efeitos desses operadores sobre a imagem das moedas e seus respectivos histogramas. Observe que a erosão desloca a distribuição para intensidades mais baixas, enquanto a dilatação a desloca para intensidades mais altas. O gradiente concentra valores nas regiões de contorno, e os operadores Top-hat e Black-hat produzem histogramas fortemente concentrados em baixos níveis de intensidade, pois apenas pequenas estruturas locais são realçadas.
import ioimport matplotlib.pyplot as pltimport numpy as npdef fig2img(fig): b = io.BytesIO(); fig.savefig(b, format='png', dpi=100); plt.close(fig); b.seek(0)return (plt.imread(b)[:, :, :3] *255).astype(np.uint8)def plot_hist(img, title): fig, ax = plt.subplots(figsize=(4, 3)) h = mm.hist(img)# CORREÇÃO: range usa len(h) dinamicamente para casar com o retorno da biblioteca ax.bar(range(len(h)), h, color='steelblue', width=1, edgecolor='steelblue') ax.set(xlim=(0, 255)); plt.tight_layout()return fig2img(fig)# 1. Processamento morfológico baseB = mm.sedisk(19)operadores = [ ("Original", img_coins_gray), ("Erosão", mm.ero(img_coins_gray, B)), ("Dilatação", mm.dil(img_coins_gray, B)), ("Gradiente Morf.", mm.gradm(img_coins_gray, B)), ("Top-hat", mm.tophat(img_coins_gray, B)), ("Black-hat", mm.blackhat(img_coins_gray, B))]# 2. Montagem dinâmica do par: [Imagem, Histograma]imgs, titles = [], []for nome, img in operadores: imgs += [img, plot_hist(img, f"Hist. {nome}")] titles += [nome, f"Hist. {nome}"]# 3. Exibição final em grade de duas colunas (Imagem | Histograma)mm.show(imgs, titles=titles, cols=4, figsize=(10, 8))
Figura 4.14: Morfologia em tons de cinza e seus respectivos histogramas. A erosão reduz intensidades locais, a dilatação as amplia, o gradiente destaca bordas, e os operadores Top-hat e Black-hat evidenciam detalhes locais brilhantes e escuros. Elemento estruturante: disco de diâmetro 19.
Os operadores morfológicos apresentados anteriormente serão agora utilizados como ferramentas de refinamento e geração de marcadores para métodos de segmentação mais avançados, apresentados a seguir.
4.4 Segmentação de Imagens: Fundamentação e Taxonomia
A segmentação de imagens consiste em dividir a imagem em regiões associadas a objetos ou estruturas de interesse. Em PDI, ela representa a transição entre o processamento de baixo nível — como filtragem e realce — e etapas de análise mais avançadas, como extração de características, reconhecimento e interpretação da cena.
Formalmente, o objetivo da segmentação consiste em decompor o domínio espacial completo de uma imagem, denotado por \(\Omega\), em uma partição de subconjuntos \(\{R_1, R_2, \ldots, R_n\}\) que satisfaça simultaneamente os critérios de completeza e disjunção:
Além das propriedades de completeza e disjunção expressas na Equação 4.15, cada sub-região \(R_i\) deve constituir um domínio homogêneo segundo um predicado de similaridade definido sobre propriedades locais — intensidade, cor ou textura — e, simultaneamente, ser distinta das regiões adjacentes.
As técnicas de segmentação podem ser organizadas em diferentes famílias. Neste capítulo serão enfatizadas as abordagens resumidas na Tabela 4.5, fundamentadas principalmente em critérios de intensidade, conectividade e proximidade espacial.
Tabela 4.5: Taxonomia simplificada das principais abordagens de segmentação e refinamento estudadas neste capítulo.
Abordagem
Critério de Segmentação
Operadores de Referência
Limiarização
Particionamento do espaço de intensidades
Critério de Otsu, limiarização global e local
Morfologia Matemática
Relações espaciais definidas por elementos/funções estruturantes
Erosão, dilatação, abertura, fechamento e reconstrução
Baseada em Regiões
Homogeneidade local e conectividade espacial
Rotulação de componentes conexos, Transformada de Distância e Watershed
Até este ponto, o desenvolvimento prático concentrou-se na limiarização, por meio da combinação entre equalização adaptativa CLAHE e o método global de Otsu. Essa etapa foi complementada por operadores de reconstrução morfológica baseados em dilatações geodésicas, implementados pelas funções mm.infrec, mm.clohole e mm.edgeoff, produzindo uma máscara binária limpa e adequada para análise.
Contudo, em cenários onde objetos distintos aparecem conectados na máscara binária — seja por contato físico, sobreposição parcial ou por pontes estreitas de pixels produzidas pela segmentação —, a limiarização deixa de ser suficiente para individualizar cada objeto. Nesses casos, múltiplos objetos passam a compor um único componente conexo, dificultando etapas posteriores de medição e interpretação.
Para superar essa limitação, as próximas seções introduzem três ferramentas complementares: a Rotulação de Componentes Conexos, a Transformada de Distância e o algoritmo de segmentação por Watershed baseado em marcadores. Em conjunto, essas técnicas permitem separar objetos adjacentes, identificar regiões individualmente e extrair descritores geométricos consistentes para análise quantitativa.
4.4.1 Rotulação
A rotulação de componentes conexas (connected component labeling) é o operador que atribui um identificador inteiro único a cada conjunto de pixels pertencentes à mesma componente conexa em uma imagem binária.
NotaDefinição formal
Dada uma imagem binária \(f\) e uma relação de conectividade definida por um elemento estruturante \(B\) (tipicamente conectividade-4 ou conectividade-8), o algoritmo de rotulação da Figura 4.15 produz uma imagem \(g\) na qual todos os pixels pertencentes à mesma componente conexa recebem o mesmo rótulo inteiro positivo, enquanto pixels pertencentes a componentes distintas recebem rótulos diferentes.
A conectividade define quais pixels são considerados vizinhos diretos de um pixel \((x,y)\). As definições mais utilizadas são:
Conectividade-4: considera apenas os quatro vizinhos ortogonais (norte, sul, leste e oeste).
Conectividade-8: considera os quatro vizinhos ortogonais e os quatro diagonais, totalizando oito vizinhos.
A escolha da conectividade influencia diretamente a formação das componentes conexas e, consequentemente, o resultado da rotulação, como ilustrado na Figura 4.17. Um exemplo adicional pode ser explorado interativamente no simulador apresentado na Figura 4.16.
A implementação em morph.py disponibiliza duas versões desse operador. A função mm.label0 reproduz explicitamente o algoritmo de flood-fill utilizando uma pilha e permite controlar a conectividade por meio do elemento estruturante adotado. Já mm.label delega a operação à implementação otimizada do OpenCV (mm.label0). Em ambos os casos, o resultado é uma imagem rotulada na qual cada componente conexa recebe um identificador inteiro distinto.
Algoritmo de rotulação por flood-fill com pilha — painel interativo com HTML e SVG
Passo a passo
Cards
Linha do tempo
Código Python
Fluxograma
Flood-fill com pilha
Rotulagem de componentes conexas
1
Criar imagem de saída g, inicializada com zeros, mesma dimensão de f.
2
Inicializar contador de rótulos (cor) cor ← 1.
3
Percorrer f em ordem raster (coordenadas x e y) até encontrar uma semente: pixel ativo (f[x,y] ≠ 0) ainda não rotulado (g[x,y] = 0).
4
Inserir a semente encontrada na pilha pilha ← [[x,y]].
5
Enquanto a pilha contiver coordenadas (while pilha):
Desempilhar pixel atual: i, j ← pilha.pop() e atribuir o rótulo: g[i,j] ← cor.
Buscar vizinhos usando o iterador mm._viz(f,b,i,j). Se o vizinho for ativo no elemento estruturante (bv ≠ 0), ativo na imagem (f[vy,vx] ≠ 0) e não rotulado (g[vy,vx] = 0), empilhá-lo.
6
Pilha vazia ⟹ Toda a componente conexa atual foi explorada e rotulada com sucesso.
7
Incrementar o rótulo para a próxima componente: cor ← cor + 1 e continuar a varredura raster.
A conectividade (4 ou 8 vizinhos) é definida unicamente pela matriz morfológica b passada como parâmetro, alterando os pixels retornados em mm._viz.
01
Inicialização
Criar matriz de rótulos g preenchida com zeros (fundo). Definir rótulo inicial cor ← 1.
02
Varredura Raster
Percorrer a matriz bidimensional linha por linha, localizando pixels pertencentes ao objeto que ainda não possuem rótulo.
03
Semente inicial
Ao achar um pixel válido, inicializar a estrutura LIFO de busca: pilha = [[x, y]].
04
Expansão por Flood-Fill
Enquanto houver elementos na pilha:
Extrair (i, j) via pop() e marcar g[i, j] = cor.
Inspecionar vizinhança geométrica e adicionar novos candidatos à pilha.
05
Próxima Componente
Pilha esvaziada ⟹ Incrementar indexador cor ← cor + 1 para diferenciar o próximo objeto isolado.
01
Alocação Espacial
g ← zeros_like(f) e definição do primeiro identificador: cor ← 1.
02
Varredura Bidimensional
Laços encadeados varrendo as dimensões h e w da imagem.
03
Descoberta de Objeto
Filtro condicional localiza pixel ativo não indexado e cria a pilha semente.
04–05
Preenchimento por Região (Flood-fill)
while pilha
Remover último da pilha (i,j) e aplicar rótulo atual.
Empilhar vizinhos conectados que atendam aos critérios morfológicos de b.
06
Fechamento do Objeto
Pilha vazia determina o fim do isolamento daquela componente.
07
Atualização do Rótulo
Incremento linear: cor ← cor + 1. A varredura raster continua do ponto onde parou.
label0.pyflood-fill com pilha
deflabel0(f, b=np.ones((3,3),dtype='uint8')):
"""Rotulagem por flood-fill com pilha."""
h, w = f.shape
g = np.zeros(f.shape, dtype=int)
cor = 1for x inrange(h):
for y inrange(w):
if f[x,y] and not g[x,y]:
pilha = [[x,y]]
while pilha:
i,j = pilha.pop(); g[i,j] = cor
for vy,vx,bv in mm._viz(f,b,i,j):
if bv and f[vy,vx] and not g[vy,vx]:
pilha.append([vy,vx])
cor += 1return g
1
g = np.zeros(f.shape, dtype=int) — Inicializa a matriz de saída com zeros. Zeros representam o fundo invariável.
2
mm._viz(f, b, i, j) — O iterador morfológico avalia a conectividade. Passando B_cruz a busca expande em 4-vizinhança; passando quadrado (ones) expande em 8-vizinhança.
3
pilha.pop() — Remove o último par de coordenadas inserido, caracterizando um comportamento LIFO de busca em profundidade (DFS) para varrer o objeto de forma contígua.
4
cor += 1 — O incremento ocorre estritamente fora do laço while, garantindo que o mesmo número marque toda a extensão da componente concluída antes de passar para a próxima semente raster.
Figura 4.15: Algoritmo de rotulação por flood-fill com pilha.
O auxiliar _viz itera sobre a janela estruturante b centrada em \((i,j)\), gerando somente os vizinhos válidos dentro dos limites da imagem — a conectividade desejada é inteiramente determinada pela forma de b passada ao algoritmo.
Exemplo didático — efeito da conectividade:
🪙 Simulador: Rotulação de Componentes Conexasflood-fill com pilha
Pixels Ativos
0
Componentes
0
Conectividade
4
Passo Raster
–
🖱️ Clique para ativar/desativar pixels · Arraste para pintar
Conectividade
4 vizinhos ortogonais: N, S, L, O
Visualização
Passo a Passo
–
–
Exemplos Predefinidos
Figura 4.16: Simulador interativo de rotulação de componentes conexas (connected component labeling): visualização da expansão flood-fill, conectividade-4 e conectividade-8.
Figura 4.17: Efeito da conectividade na rotulação: a imagem binária 10×10 contém pixels diagonalmente adjacentes. Com conectividade-4, esses pixels formam componentes distintas; com conectividade-8, fundem-se em uma única componente.
4.4.2 Transformada de Distância
A Transformada de Distância (TD) é um operador que, aplicado a uma imagem binária \(f\), produz uma imagem em níveis de cinza \(D\) na qual cada pixel pertencente ao objeto (\(f(x,y)\neq 0\)) recebe como valor a distância geométrica até o pixel de fundo (\(f(x',y')=0\)) mais próximo:
onde \(d(\cdot,\cdot)\) é uma métrica de distância — tipicamente a distância Euclidiana (\(L_2\)). O resultado é uma representação topográfica dos objetos: pixels localizados no interior assumem valores elevados, enquanto pixels próximos às bordas apresentam baixos valores de distância. Os máximos locais de \(D\) correspondem aos pontos mais afastados da borda do objeto, frequentemente próximos aos seus centros geométricos ou centros de máxima inscrição — propriedade particularmente útil para a geração automática de marcadores no algoritmo watershed.
NotaDefinição formal usando erosões
A TD admite ainda uma interpretação morfológica iterativa, conforme algoritmo da Figura 4.18. Considere uma função estruturante \(b\) cujo valor central é nulo e cujos vizinhos possuem custos negativos associados ao deslocamento. Ao aplicar erosões sucessivas com essa função estruturante particular, os valores dos pixels dos objetos (que devem assumir a distância máxima possível da imagem) são progressivamente reduzidos segundo os custos definidos por \(b\). O valor acumulado dessa propagação passa então a representar a distância ao fundo segundo a métrica induzida pela função estruturante.
Essa interpretação é implementada em mm.dist1(), que acumula erosões sucessivas utilizando a operação mm.ero1(). Já mm.dist() delega o cálculo da distância Euclidiana ao operador otimizado do OpenCV mm.dist(f), onde f é a imagem binária de entrada, distância L2 especifica a métrica Euclidiana (\(L_2\)) e 5 indica o uso de uma máscara 5×5 para aproximar a distância com elevada precisão.
A função dist1 produz uma transformada de distância discreta cuja métrica é determinada pela geometria e pelos pesos da função estruturante utilizada. Por exemplo, utilizando uma função estruturante em cruz com custo unitário para os quatro vizinhos ortogonais, obtém-se a distância de Manhattan (\(L_1\)). Outras escolhas de vizinhança e pesos induzem métricas diferentes. Já mm.dist() calcula uma aproximação eficiente da distância Euclidiana (\(L_2\)).
Por exigir sucessivas erosões sobre toda a imagem, a abordagem dist1 possui custo computacional significativamente maior que mm.dist(), sendo empregada neste livro principalmente para fins didáticos e para evidenciar a relação entre morfologia matemática e transformadas de distância.
Transformada de distância por erosões numéricas sucessivas — painel interativo
Passo a passo
Cards
Linha do tempo
Código Python
Fluxograma
Transformada de distância por erosão numérica
Propagação matemática de distâncias via elemento estruturante com pesos
1
Inicializar a imagem de trabalho fazendo uma cópia da original: g ← f.copy(). Os pixels de fundo (0) servem como fontes de distância nula.
2
Entrar em um laço infinito de erosões com pesos (ponto fixo):
Salvar estado anterior: f ← g.copy().
Erodir: g ← ero1(g, b), aplicando a subtração local de pesos e computando o valor mínimo para cada vizinhança.
3
Verificar convergência: se f for idêntica a g (array_equal), a frente de onda de distâncias se estabilizou. Romper o laço (break).
4
Retornar a matriz modificada g contendo o mapa exato de distâncias.
Nesta abordagem morfológica numérica, não há incremento artificial ou contador. A distância propaga-se de fora para dentro porque a erosão contínua puxa o valor 0 do fundo e o decrementa matematicamente (subtraindo os pesos negativos como -1), fazendo com que os valores escalem radialmente.
Cruz — L₁ (Manhattan)
B_cruz [y,x]
Pesos: Centro=0, Lados=-1, Cantos=-inf
Erosão de Cinzas
f[vy,vx] - bv
Subtrai o peso e busca o valor mínimo local
Convergência
f == g
Para quando nenhum pixel muda de valor
01
Inicialização
Clonar imagem de entrada: g ← f.copy(). O objeto possui intensidade alta (255) e o fundo possui intensidade 0.
02
Mapeamento Local (ero1)
Para cada coordenada (y, x), buscar o mínimo valor da operação f[vy, vx] - bv aplicada à sua vizinhança estruturante.
03
Loop Iterativo
Atualizar sequencialmente: f = g.copy() seguido de g = ero1(g, b). Os valores nulos propagam-se para o interior do objeto.
04
Critério de Parada
Se np.array_equal(f, g), significa que o mapa de distâncias atingiu o equilíbrio estável e a propagação terminou.
01
Cópia de Trabalho
Prepara a matriz inicial `g`.
02
Loop de Erosão de Escala
while True
Guarda estado: f ← g.copy()
Aplica erosão com pesos: g ← ero1(g, b)
03
Estabilização Espacial
Condição de parada acionada assim que np.array_equal(f, g) se torna verdadeiro.
04
Retorno Numérico
Retorna g contendo as distâncias calculadas pela subtração cumulativa dos pesos.
morph_dist.pyErosão numérica iterativa
@staticmethoddefero1(f, b):
g = np.empty_like(f)
for y inrange(f.shape[0]):
for x inrange(f.shape[1]):
g[y,x] = 255for vy,vx,bv in mm._viz(f,b,y,x):
if np.isinf(bv): continue
val = int(f[vy,vx]) - int(bv)
if g[y,x] > val:
g[y,x] = max(0, val)
return g
@staticmethoddefdist1(f, b):
g = f.copy()
whileTrue:
f = g.copy()
g = mm.ero1(g, b)
if np.array_equal(f, g):
breakreturn g
1
g[y,x] = 255 — Inicializa o elemento com o valor máximo antes de computar o operador de mínimo da erosão.
2
f[vy,vx] - bv — Subtrai o peso associado da vizinhança. Como os pesos da cruz externa são negativos (ex: -1), a operação torna-se uma adição matemática (f[vy,vx] - (-1) = f[vy,vx] + 1) propagando a distância a partir das bordas zeradas.
3
np.array_equal(f, g) — Critério de convergência exato por estabilização de ponto fixo.
Figura 4.18: Algoritmo da Transformada de Distância.
A Figura 4.19 apresenta um simulador interativo da TD: é possível posicionar o cursor sobre diferentes pixels do objeto e observar, em tempo real, o valor da distância associado àquela posição, isto é, a distância até o pixel de fundo mais próximo. A Figura 4.20 apresenta um exemplo prático dessa execução em ambiente Python.
🗺️ Simulador: Transformada de Distância (TD)Fronteiras em +∞ (144)
Pixels Ativos
0
Distância Máx.
0
Métrica Atual
L∞ (Chebyshev)
Iteração (k)
–
🖱️ Clique para ativar/desativar pixels · Arraste para pintar
Elemento Estruturante (b)
Clique para alterar os pesos:
Visualização
Propagação Passo a Passo
Exemplos Predefinidos
Figura 4.19: Simulador interativo da Transformada de Distância (TD) iterativa via erosão em tons de cinza. Pixels fora da imagem assumem o valor máximo (144), propagando os custos a partir do fundo interno.
Figura 4.20: Transformada de Distância em imagem binária 10×10. Esquerda: original (foreground = 255). Centro: mm.dist1 iterativa (erosões com cruz). Direita: mm.dist (L2).
A anotação dos valores numéricos diretamente sobre os pixels permite verificar como dist1 propaga as distâncias segundo a métrica induzida pela função estruturante utilizada. No caso do elemento cruz com custo unitário, os valores obtidos correspondem à distância de Manhattan (\(L_1\)). Embora dist1 e mm.dist produzam valores numéricos distintos por adotarem métricas diferentes, ambas as transformadas preservam a estrutura topográfica dos objetos, fazendo com que seus máximos ocorram em regiões centrais semelhantes. Essa propriedade justifica o uso de mm.dist em aplicações práticas, devido à sua elevada eficiência computacional.
4.4.3 Transformada de Distância Euclidiana em quatro passos
O simulador da Figura 4.21 implementa o algoritmo da TDE de Lotufo; Zampirolli (2001) em duas etapas. Na primeira etapa, a função edt1 realiza uma transformação unidimensional vertical de forma sequencial (in-place), percorrendo cada coluna em raster (↓) e anti-raster (↑) para calcular as distâncias na direção vertical (dois passos: Sul e Norte). Na segunda etapa, a função edt2 utiliza esse resultado como entrada e realiza uma propagação horizontal por filas: para cada linha da matriz, duas filas de prioridade, Eq e Wq, são inicializadas percorrendo os índices de coluna em sentidos opostos (Eq de W-1 até 1, Wq de 2 até W), de modo que cada pixel atualizado enfileira imediatamente seus vizinhos para reprocessamento dentro da mesma rodada (mais dois passos: Leste e Oeste). Esse mecanismo de fila permite aplicar erosões sucessivas com pesos ímpares crescentes (b = 1, 3, 5, ..., incrementado a cada iteração do laço externo) sem que a propagação fique presa em valores desatualizados, já que cada rodada resolve a cadeia de dependências horizontal por completo antes do próximo incremento de b. A convergência dessa propagação produz a Transformada de Distância Euclidiana em toda a matriz, combinando a informação vertical obtida em edt1 com a propagação horizontal em fila realizada em edt2.
Usa a mesma estrutura raster/anti-raster descrita no artigo para propagação de distâncias exatas (exemplo do artigo, pág. 103).
Passo Atual
–
Fase
–
b Atual
–
Vel.:
Figura 4.21: Simulador interativo 2D (4x4) com sincronização estrita de filas de propagação horizontal (b) para obter a convergência exata descrita no artigo.
4.4.3.1 Transformada de Distância Geodésica
A transformada de distância geodésica associa a cada pixel a menor distância até um marcador, sob a restrição imposta por uma máscara. Dessa forma, a propagação ocorre exclusivamente pelos pixels permitidos, preservando a conectividade do domínio.
A Figura 4.22 ilustra esse processo em um labirinto: (a) a máscara g; (b) a distância geodésica D1 calculada a partir da entrada; (c) a distância D2 calculada a partir da saída; e (d) o caminho mínimo obtido a partir dessas duas transformadas.
O caminho ótimo é determinado pela soma das distâncias (D1 + D2). Os pixels pertencentes à trajetória mínima são aqueles para os quais essa soma assume seu menor valor, definindo uma conexão entre entrada e saída com comprimento geodésico mínimo.
Esse princípio permite resolver labirintos sem a necessidade de explorar explicitamente todas as possibilidades de percurso. A solução emerge diretamente da propagação de distâncias em um domínio restrito. Essa abordagem é particularmente relevante em labirintos de elevada complexidade, como os construídos a partir de estruturas quasicristalinas e ciclos hamiltonianos descritos por Singh; Lloyd; Flicker (2024). Em Zampirolli et al. (2025), esse mesmo formalismo é empregado para resolver um labirinto complexo; a seguir, o método é ilustrado em uma versão simplificada do problema.
import numpy as np# 1 = corredor, 0 = paredeg = np.array([ [0,1,0,0,0,0,0,0,0,0], [0,1,1,1,1,1,0,1,1,1], [0,0,0,0,0,1,0,1,0,1], [0,1,1,1,0,1,1,1,0,1], [0,1,0,1,0,0,0,0,0,1], [0,1,0,1,1,1,1,1,1,1], [0,1,0,0,0,0,0,0,1,0], [0,1,1,1,1,1,1,0,1,0], [0,0,0,0,0,0,1,1,1,0], [0,0,0,0,0,0,0,0,1,0]], dtype=np.uint8)# marcador da entradaentrada = np.zeros_like(g, dtype=np.uint8)entrada[0,1] =1# marcador da saídasaida = np.zeros_like(g, dtype=np.uint8)saida[9,8] =1# distâncias geodésicasD1 = mm.gdist(g, entrada)D2 = mm.gdist(g, saida)# soma das distânciasS = D1 + D2# menor valor válido da somadmin = np.min(S[S >0])# pixels pertencentes a um caminho ótimocaminho = (S == dmin)print("Distância geodésica mínima:", dmin)mm.show( [g, D1, D2, caminho], titles=["Labirinto","Distância da Entrada","Distância da Saída",f"Menor Caminho\n(d={dmin})" ], cols=4, figsize=(14,4), axis=True)
Distância geodésica mínima: 15
Figura 4.22: Menor caminho geodésico em um labirinto. As distâncias geodésicas são calculadas a partir da entrada e da saída usando mm.gdist. Os pixels cujo somatório das duas distâncias é igual à distância mínima entre os marcadores pertencem a um caminho ótimo.
4.4.4 Segmentação por Watershed
O algoritmo Watershed interpreta uma imagem em níveis de cinza como uma superfície topográfica, na qual valores elevados correspondem a montanhas e valores baixos correspondem a vales ou bacias de drenagem (catchment basins). No contexto da segmentação baseada em marcadores, os máximos da Transformada de Distância são frequentemente utilizados para identificar regiões internas aos objetos, fornecendo sementes confiáveis para o processo de inundação.
A segmentação é então realizada por uma simulação conceitual de inundação progressiva a partir desses marcadores. À medida que as bacias associadas a diferentes sementes se expandem, regiões vizinhas eventualmente entram em contato. Nesse instante são construídas barreiras virtuais, denominadas watershed lines, que passam a delimitar os objetos da cena. Esse mecanismo permite separar objetos adjacentes ou parcialmente sobrepostos, mesmo quando eles formam uma única componente conexa após a limiarização.
A implementação didática apresentada neste capítulo explora inicialmente o conceito de crescimento de regiões (region growing) confinado por uma máscara binária, conforme detalhado no algoritmo interativo da Figura 4.23.
NotaVersão didática versus implementação clássica
A função mm.watershed0 não implementa o algoritmo watershed clássico. Seu objetivo é ilustrar, de forma simplificada, a propagação de marcadores por crescimento de regiões (region growing), permitindo visualizar como diferentes sementes competem pela ocupação do espaço disponível. O crescimento é delimitado por uma máscara binária de suporte e monitorado por um controle de estagnação, gerando um resultado semelhante a uma partição de Voronoi restrita à geometria dos objetos de entrada.
Já a função mm.watershed utiliza a implementação otimizada do OpenCV (mm.watershed), que realiza a inundação sobre uma superfície topográfica definida pela imagem de entrada. Nesse caso, a propagação dos marcadores é influenciada pelos valores dos pixels, fazendo com que as linhas de separação se formem naturalmente sobre as cristas do relevo.
Algoritmo Didático do Watershed Limitado por Máscara — painel interativo
Passo a passo
Cards
Linha do tempo
Código Python
Fluxograma
Crescimento de Regiões Confinado por Máscara
Inundação concorrente com restrição geométrica de suporte e sincronização síncrona por malha
1
Rotular os marcadores sementes em f via mm.label0(f, b), instanciar a malha dinâmica g ← f.copy() e binarizar a mask.
2
Enquanto houver pixels não rotulados (while True), reiniciar o controle de atividade mudou ← False e varrer a imagem:
Identificar se a coordenada atual é um vazio contido no escopo: g[x,y] == 0 and mask[x,y].
Avaliar a vizinhança estrutural em mm._viz(f, b, x, y) baseada no estado síncrono estável f.
Se um vizinho possuir rótulo dominante (g[x,y] < f[vy,vx]), a célula em g absorve esse identificador e marca-se mudou ← True.
3
Verificar ponto fixo: caso uma varredura completa não expanda nenhuma fronteira (not mudou), interrompe-se o laço (break).
4
Atualizar o estado de referência de forma síncrona para a próxima iteração: f ← g.copy().
5
Se op == 'region', retornar o mapa de bacias g; caso contrário, extrair as cristas divisórias via mm.gradm(g).
A sincronização f = g.copy() ao final de cada ciclo impede o crescimento assimétrico ou dependente da ordem da varredura raster (propagação em estilo Jacobi).
Escopo Geométrico
mask[x,y] > 0
Restrição binária rígida impedindo o avanço periférico de rótulos.
Estabilização
if not mudou: break
Evita loops infinitos interrompendo ao saturar o domínio da máscara.
Mapeamento Jacobi
f = g.copy()
Sincronização em bloco após inspeção de todas as coordenadas.
01
Inicialização
Geração dos identificadores iniciais pelo mapeamento de componentes conexas e binarização da máscara de suporte.
02
Expansão Concorrente
Varredura 2D inspecionando vazios internos autorizados. A malha de trabalho g absorve os rótulos lidos da referência estável f.
03
Ponto Fixo Local
A flag mudou monitora mudanças estruturais. Se nenhuma frente avançar, o laço de inundação é finalizado via break.
04
Sincronização e Saída
Atualização em bloco do estado referencial. A saída pode ser moldada como partições regionais ou linhas de cristas (linhas de watershed).
01
Condicionamento Prévio
Rotulagem preliminar de marcadores e isolamento booleano do domínio.
02
Laço Síncrono Iterativo
while True
Redefinição de flag: mudou = False
Crescimento condicional: se g[x,y] == 0 e estiver na máscara, expande lendo f
Controle de estabilidade: if not mudou: break
Atualização síncrona: f = g.copy()
03
Extração Topológica
Retorno condicional das bacias preenchidas ou cálculo morfológico do gradiente de transição.
mm_watershed.pyAlgoritmo com Restrição de Máscara
defwatershed0(f, mask=None, b=np.zeros((3,3),dtype='uint8'), op='region'):
f = mm.label0(f, b)
g = f.copy()
mask = np.ones_like(f) if mask isNoneelse (mask > 0)
whileTrue:
mudou = Falsefor x inrange(f.shape[0]):
for y inrange(f.shape[1]):
if g[x,y] == 0and mask[x,y]:
for vy,vx,bv in mm._viz(f, b, x, y):
if bv and g[x,y] < f[vy,vx]:
g[x,y] = f[vy,vx]
mudou = Trueifnot mudou:
break
f = g.copy()
return g if op == 'region'else mm.gradm(g, mm.secross())
1
mask = (mask > 0) — Converte a imagem de suporte informada para um mapa Booleano indexável.
2
g[x,y] == 0 and mask[x,y] — Filtro ativo: pixels fora da máscara (fundo zero) são ignorados de imediato, confinando as frentes de expansão.
3
if not mudou: break — Mecanismo de escape. Quando todos os espaços internos permitidos forem preenchidos ou estabilizados contra a barreira, o laço aborta de forma limpa.
Figura 4.23: Algoritmo Didático de Watershed por Crescimento de Regiões Limitado por Máscara.
A Figura 4.24 apresenta um simulador iterativo que ilustra a propagação dos marcadores pela região de interesse. Cada marcador atua como uma fonte de inundação que expande sua área de influência até encontrar regiões provenientes de outras sementes. No algoritmo do OpenCV, os pixels pertencentes às linhas divisoras são identificados pelo valor -1, representando as fronteiras entre bacias adjacentes.
💧 Simulador: Segmentação por WatershedPropagação com Elemento Estruturante (b)
Área (Máscara)
0
Marcadores
0
Preenchimento
0%
Iteração (k)
–
🖱️ Arraste para desenhar/apagar a máscara ou as sementes
Ferramentas
Elemento Estruturante (b)
Visualização
Inundação Passo a Passo
Exemplos Iniciais
Figura 4.24: Simulador interativo do Algoritmo Watershed por propagação morfológica. Desenhe a máscara, posicione os marcadores e ajuste o Elemento Estruturante para observar a inundação. Quando as bacias se encontram simultaneamente, o empate é resolvido assumindo uma das regiões de forma aleatória.
Pipeline Morfológico do Watershed
O watershed baseado em marcadores normalmente integra um fluxo mais amplo de segmentação. Em imagens reais, etapas de pré-processamento são frequentemente necessárias para melhorar o contraste, reduzir ruídos e gerar marcadores confiáveis. Esse fluxo completo está resumido na Tabela 4.6.
Tabela 4.6: Pipeline completo do watershed baseado em marcadores para imagens reais.
Etapa
Operação
Finalidade
1
CLAHE + Suavização
Realce de contraste e redução de ruído
2
Limiarização
Separação inicial entre objeto e fundo
3
Abertura/Fechamento
Remoção de ruídos e pequenas imperfeições
4
Dilatação da Máscara
Identificação do Fundo Certo (Sure Background)
5
Transformada de Distância + Limiar
Identificação do Objeto Certo (Sure Foreground)
6
Região Incerta
Diferença entre Fundo Certo e Objeto Certo
7
mm.watershed
Propagação dos marcadores pela região incerta
Para enfatizar exclusivamente os conceitos de Transformada de Distância, marcadores e inundação topográfica, o exemplo da Figura 4.25 utiliza uma imagem binária sintética e adota um fluxo simplificado, resumido na Tabela 4.7.
Tabela 4.7: Pipeline simplificado utilizado no exemplo didático da Figura 4.25.
Etapa
Operação
Finalidade
1
Transformada de Distância
Construção da superfície topográfica
2
Limiar da TD
Extração dos marcadores (Sure Foreground)
3
Dilatação
Determinação do Fundo Certo (Sure Background)
4
Região Incerta
Diferença entre fundo e marcadores
5
mm.watershed
Propagação dos marcadores e geração das fronteiras
Figura 4.25: Pipelinewatershed delimitado por máscara em imagem binária 20×20.
Aplicação em moedas sobrepostas:
img_base = img_coins_gray# 1. Simular moedas sobrepostas/conectadas (usando a máscara binária base)img_sobrepostas = mm.dil(img_final, mm.sebox(40))# 2. Abertura morfológica para limpar ruídosopening = mm.open(img_sobrepostas, mm.sebox(2))# 3. Transformada de Distânciadist = mm.dist(opening)dist_vis=(255*(dist/dist.max())).astype(np.uint8) if dist.max() >0else dist.astype(np.uint8)# 4. Picos seguros (Marcadores das moedas)picos = (dist >0.5* dist.max()).astype(np.uint8) *255# 5. Execução do Watershed (Ajustado para usar a nova assinatura)# Passamos 'opening' direto em mask, pois ela delimita o escopo de expansão das moedasws_region = mm.watershed(picos, mask=opening, op='region')ws_line = mm.watershed(picos, mask=opening, op='line')# 6. Contagem de objetos (Ignora fundo 0)labels = np.unique(ws_region)labels = labels[labels >0]print(f"Objetos detectados: {len(labels)}")# 7. Anotação Finalimg_annotated = cv2.cvtColor(img_base, cv2.COLOR_GRAY2BGR)for idx, label_id inenumerate(labels): mask_reg = (ws_region == label_id).astype(np.uint8)if mask_reg.sum() <500: continue cy, cx = np.mean(np.where(mask_reg), axis=1).astype(int) cv2.putText(img_annotated, str(idx +1), (cx -25, cy +20), cv2.FONT_HERSHEY_SIMPLEX, 3.2, (0, 255, 0), 8, cv2.LINE_AA)# Desenha as linhas de separação em vermelhomask_ann = mm.dil(ws_line, np.ones((11, 11), np.uint8))img_annotated[mask_ann >0 ] = [255, 0, 0]# Exibiçãomm.show( [img_base, img_sobrepostas, dist_vis, picos, ws_region, img_annotated], titles=["Original", "Sobrepostas", "Distância", "Marcadores", "Watershed", "Anotado"], cols=3, rows=2, figsize=(12, 8))
Objetos detectados: 12
Figura 4.26: PipelineWatershed para separação de moedas sobrepostas: da máscara binária dilatada até os contornos finais anotados sobre a imagem original.
4.5 Extração de Componentes e Descritores de Forma
Após a segmentação e o refinamento morfológico, o próximo passo consiste em identificar individualmente cada objeto presente na imagem e extrair suas propriedades geométricas. Essa etapa é fundamental para tarefas de medição, classificação e reconhecimento de padrões.
Um componente conexo é um conjunto maximal de pixels pertencentes ao objeto que permanecem mutuamente conectados segundo uma relação de conectividade previamente definida (4- ou 8-conectividade). Após a rotulação, cada componente recebe um identificador único, permitindo que suas características sejam analisadas individualmente.
O OpenCV oferece duas abordagens complementares para essa análise, resumidas na Tabela 4.8.
Tabela 4.8: Comparação entre as abordagens baseadas em componentes conexos e em contornos.
connectedComponentsWithStats
findContours
Retorna
rótulo por pixel e estatísticas por componente
sequência de pontos que descreve a borda
Descritores diretos
área, bounding box e centróide
perímetro, forma e hierarquia
Objetos em contato
tende a fundir regiões conectadas
tende a produzir um único contorno externo
Uso típico
contagem, filtragem e rotulação
análise geométrica e descritores de forma
4.5.1 Rotulação e Estatísticas de Componentes
A função mm.label0 realiza simultaneamente a rotulação dos componentes conexos e a extração de descritores básicos de cada região. O operador retorna:
uma imagem de rótulos (labels);
estatísticas geométricas (stats);
coordenadas dos centróides (centroids).
As estatísticas incluem área, largura, altura e posição da bounding box mínima alinhada aos eixos da imagem.
A Figura 4.27 ilustra a aplicação desse operador após o pipeline de segmentação das moedas. Cada componente conexo recebe uma cor distinta e sua área é anotada diretamente sobre a imagem.
# 1. Rotulagem e estatísticasn, labels, stats, centroids = cv2.connectedComponentsWithStats( img_final, connectivity=8)# 2. Coloração dos componentes — paleta FIXA (gerada uma vez com# np.random.seed(4)) para a trilha C++ reproduzir cor a cor sem# depender do gerador do numpy. Se n exceder len(PALETA), cicla.PALETA = np.array([ [0, 0, 0], [224, 82, 193], [233, 155, 73], [190, 247, 244], [103, 94, 179], [51, 154, 59], [137, 96, 232], [250, 243, 205], [100, 141, 208], [228, 187, 163], [202, 108, 214], [131, 105, 104], [86, 153, 81]], dtype=np.uint8)colors = PALETA[np.arange(n) %len(PALETA)].copy()colors[0] = [0, 0, 0] # fundo pretoimg_colored = colors[labels]img_annotated = img_colored.copy()# 3. Tabela de descritoresprint(f"Componentes detectados (excluindo fundo): {n -1}")print(f"\n{'ID':>4}{'Área':>8}{'cx':>6}{'cy':>6}{'w':>6}{'h':>6}")print("-"*42)for i inrange(1, n): area = stats[i, cv2.CC_STAT_AREA] cx, cy =int(centroids[i, 0]), int(centroids[i, 1]) w, h = stats[i, cv2.CC_STAT_WIDTH], stats[i, cv2.CC_STAT_HEIGHT]print(f"{i:>4}{area:>8}{cx:>6}{cy:>6}{w:>6}{h:>6}")for cor, esp in [((0,0,0), 10), ((255,0,0), 5)]: cv2.putText(img_annotated, f"{i}: {area}", (cx -150, cy +18), cv2.FONT_HERSHEY_SIMPLEX, 2.0, cor, esp, cv2.LINE_AA)mm.show( [img_coins_gray, img_final, img_colored, img_annotated], titles=["Original", "Segmentação Final", "Componentes Conexos", "Áreas Anotadas"], cols=4, figsize=(18, 6))
Figura 4.27: Componentes conexos extraídos após o pipeline CLAHE → Otsu → limpeza morfológica. Cada objeto é colorido com cor distinta e anotado com sua área em pixels.
4.5.2 Descritores de Forma
Enquanto connectedComponentsWithStats opera sobre regiões, extração de contornos atua diretamente sobre suas fronteiras. O operador retorna, para cada objeto, uma sequência ordenada de pontos que descreve seu contorno.
A partir dessa representação é possível calcular descritores geométricos que não são fornecidos diretamente pela rotulação de componentes:
Área (área do contorno)
Perímetro (perímetro);
Circularidade (\(C = \frac{4\pi A}{P^2}\), onde \(A\) é a área e \(P\) o perímetro);
Aproximação poligonal (aproximação poligonal);
Fecho convexo (convex hull);
Hierarquia de contornos, permitindo representar relações pai-filho entre regiões e seus buracos internos.
A circularidade assume valor máximo igual a 1 para um círculo perfeito e diminui à medida que o objeto se torna mais alongado ou apresenta irregularidades em sua borda.
A Figura 4.28 apresenta os contornos extraídos das moedas segmentadas, juntamente com os valores de circularidade calculados para cada objeto.
Figura 4.28: Contornos extraídos com findContours. Cada moeda é anotada com sua circularidade — valores próximos de 1 confirmam forma circular.
4.5.3 Conexão com Detecção de Objetos Moderna
Os descritores extraídos nas seções anteriores — especialmente bounding boxes, centróides, áreas e medidas de forma — estabelecem uma ponte natural entre a segmentação morfológica clássica e os sistemas modernos de detecção de objetos. Embora as técnicas estudadas neste capítulo utilizem operações sobre pixels e regiões segmentadas, muitas das representações produzidas são diretamente compatíveis com os formatos empregados em modelos contemporâneos de visão computacional.
Detectores baseados em aprendizado profundo, como a família YOLO (You Only Look Once) (Redmon et al., 2016), operam diretamente sobre imagens coloridas e produzem, para cada objeto detectado, uma bounding box descrita pelo centro \((cx,cy)\) e pelas dimensões \((w,h)\), além de uma classe e uma pontuação de confiança. Essa representação compartilha a mesma estrutura geométrica básica obtida por connectedComponentsWithStats, embora seja produzida por um modelo aprendido e não por segmentação explícita.
A Figura 4.29 ilustra como as bounding boxes obtidas por morfologia podem ser exportadas no formato YOLO para compor conjuntos de dados utilizados no treinamento ou na avaliação de detectores.
# Recalcula rótulos/estatísticas a partir da segmentação finaln, labels, stats, centroids = cv2.connectedComponentsWithStats( img_final, connectivity=8)H_img, W_img = img_coins_gray.shapeimg_bbox = cv2.cvtColor(img_coins_gray, cv2.COLOR_GRAY2BGR)CLASSE =0# 0 = moeda (única categoria neste exemplo)print(f"{'cls':>4}{'cx_n':>8}{'cy_n':>8}{'w_n':>8}{'h_n':>8} ← formato YOLO")print("-"*54)yolo_linhas = []for i inrange(1, n): x0 = stats[i, cv2.CC_STAT_LEFT] y0 = stats[i, cv2.CC_STAT_TOP] w = stats[i, cv2.CC_STAT_WIDTH] h = stats[i, cv2.CC_STAT_HEIGHT] cx_n = (x0 + w /2) / W_img cy_n = (y0 + h /2) / H_img w_n = w / W_img h_n = h / H_img yolo_linhas.append(f"{CLASSE}{cx_n:.4f}{cy_n:.4f}{w_n:.4f}{h_n:.4f}")print(f"{CLASSE:>4}{cx_n:>8.4f}{cy_n:>8.4f}{w_n:>8.4f}{h_n:>8.4f}") cv2.rectangle(img_bbox, (x0, y0), (x0 + w, y0 + h), (0, 255, 0), 4) cv2.putText(img_bbox, f"moeda", (x0 +8, y0 +60), cv2.FONT_HERSHEY_SIMPLEX, 3.8, (255, 0, 0), 5, cv2.LINE_AA)# Exportar arquivo de anotação no formato YOLOwithopen("moedas.txt", "w") as f: f.write("\n".join(yolo_linhas))print("\nAnotação salva em moedas.txt")mm.show( [img_coins_gray, img_final, img_bbox], titles=["Original", "Segmentação Final", "Bounding Boxes (formato YOLO)"], cols=3, figsize=(18, 6))
Figura 4.29: Bounding boxes derivadas dos componentes conexos sobrepostas à imagem original. As anotações são exportadas no formato YOLO (classe cx cy w h), com coordenadas normalizadas para o intervalo [0,1].
O formato YOLO armazena cada objeto em uma linha contendo cinco campos:
onde \((cx,cy)\) representa o centro da bounding box e \((w,h)\) suas dimensões. Todos os valores geométricos são normalizados para o intervalo \([0,1]\) em relação à largura e à altura da imagem. A classe é um identificador inteiro associado a uma categoria definida pelo conjunto de dados (por exemplo, 0 → moeda). Quando há múltiplas categorias — como moeda de ouro (0), moeda de prata (1) e disco plástico (2) — basta atribuir o identificador correspondente a cada objeto antes da exportação, mantendo exatamente o mesmo formato de anotação. No exemplo anterior, essas informações foram armazenadas no arquivo moedas.txt.
O fluxo apresentado neste capítulo — segmentação → rotulação → extração de bounding boxes — corresponde conceitualmente à etapa de anotação (labeling) empregada na construção de conjuntos de treinamento para detectores modernos. Ferramentas especializadas, como Label Studio e Roboflow, automatizam esse processo em imagens complexas, mas a lógica fundamental permanece a mesma: associar a cada objeto uma região de interesse e uma classe. Em cenários controlados, com fundo uniforme e objetos bem separados, técnicas morfológicas podem inclusive gerar anotações automaticamente ou servir como ponto de partida para a rotulação manual, reduzindo significativamente o esforço de construção do conjunto de dados. Em aplicações reais mais complexas, entretanto, a validação humana continua sendo necessária para garantir a qualidade das anotações.
NotaAvaliação: IoU (Intersection over Union)
Uma forma simples de avaliar a qualidade de uma segmentação consiste em compará-la com uma máscara de referência (ground truth). A métrica mais utilizada para essa finalidade é a IoU (Intersection over Union):
onde \(A\) representa a segmentação produzida pelo algoritmo e \(B\) a segmentação de referência.
O valor da IoU varia entre 0 e 1. Quanto maior o valor, maior a sobreposição entre as máscaras. Uma IoU igual a 1 indica correspondência perfeita entre a segmentação obtida e a referência.
A mesma métrica também é amplamente utilizada em detecção de objetos, sendo aplicada às bounding boxes previstas e anotadas. Nessa área, valores de IoU superiores a 0,5 são frequentemente adotados como critério mínimo para considerar uma detecção correta.
DicaAlém da Morfologia
As técnicas estudadas neste capítulo segmentam objetos explorando conectividade espacial, operações morfológicas e relevo topográfico. Existem, entretanto, abordagens alternativas baseadas em agrupamento de características, como o algoritmo k-means, modelos de mistura Gaussiana (GMM) e métodos mais recentes baseados em aprendizado profundo. Essas técnicas serão retomadas na Parte II do livro, dedicada à Visão Computacional.
4.6 Resumo
Neste capítulo foram apresentadas as principais técnicas de segmentação e morfologia matemática, concluindo o estudo do PDI no domínio espacial.
Pré-processamento e limiarização: A combinação entre equalização adaptativa CLAHE e o método de Otsu mostrou-se eficaz para induzir separação bimodal no histograma e simplificar a binarização de imagens com iluminação não uniforme.
Erosão e dilatação: Operadores morfológicos fundamentais baseados na busca por mínimos e máximos locais em uma vizinhança definida pelo elemento estruturante \(B\). São operadores duais pelo complemento e foram implementados por meio das funções mm.ero e mm.dil.
Abertura e fechamento: Composições de erosão e dilatação que permitem remover ruídos, suavizar contornos e preencher pequenas lacunas, preservando a estrutura global dos objetos.
Reconstrução morfológica: Processo geodésico iterativo que propaga um marcador dentro dos limites impostos por uma máscara, constituindo a base de operadores como mm.clohole e mm.edgeoff.
Pipeline de limpeza binária: Fluxo consolidado composto por CLAHE → Otsu → abertura → mm.clohole → abertura restrita → mm.edgeoff, produzindo máscaras adequadas para análise quantitativa.
Morfologia em tons de cinza: Extensão algébrica baseada em mínimos e máximos ponderados, viabilizando operadores como gradiente morfológico e filtros top-hat para realce de estruturas locais.
Transformada de Distância e Watershed: A Transformada de Distância permitiu gerar marcadores automáticos para o algoritmo watershed, possibilitando a separação de objetos adjacentes ou parcialmente sobrepostos.
Componentes conexos e descritores: A rotulação de regiões (mm.label0) e a extração de contornos (extração de contornos) permitiram calcular descritores geométricos como área, centróide, perímetro, circularidade e bounding boxes.
Conexão com Visão Computacional Moderna: As bounding boxes extraídas por morfologia foram exportadas no formato YOLO, evidenciando a ligação entre técnicas clássicas de segmentação e sistemas modernos de detecção de objetos.
O Capítulo 5 introduzirá técnicas de processamento no domínio da frequência, abordando a Transformada de Fourier, filtragem espectral e os fundamentos da compressão de imagens, incluindo DCT, JPEG e wavelets.
4.7 🤖 Uso do Gemini Notebook como Tutor Complementar
Nesta edição, o uso do Gemini Notebook é incentivado como ferramenta complementar de aprendizagem. Baseado em inteligência artificial, o sistema utiliza exclusivamente os documentos fornecidos pelo autor como fonte de conhecimento, produzindo respostas alinhadas ao conteúdo e à abordagem adotada ao longo deste capítulo.
O projeto deste capítulo no Gemini Notebook foi construído apenas com o texto em português e os exemplos de código em Python. Se você está estudando pela edição em inglês ou francês, ou acompanhando a trilha em C++, as respostas do tutor podem não corresponder exatamente à versão que você está lendo.
⚠️ Aviso sobre Conteúdo Gerado por IA
Embora seja uma ferramenta valiosa de apoio aos estudos, o Gemini Notebook pode eventualmente produzir respostas incompletas, imprecisas ou incorretas. Recomenda-se validar as informações consultando o material do capítulo, livros, artigos científicos e outras fontes acadêmicas confiáveis. Sempre que possível, execute e experimente os exemplos práticos apresentados ao longo do texto para consolidar a compreensão dos conceitos.
4.8 Lista de Exercícios
(10%) Implemente manualmente o critério de Otsu sem utilizar mm.threshold. Calcule a variância interclasses \(\sigma_B^2(T)\) para todos os limiares \(T \in [0,255]\) usando mm.hist, identifique o limiar ótimo \(T^*\) e compare o resultado com o valor obtido pelo OpenCV. Plote \(\sigma_B^2\) em função de \(T\) e destaque o ponto de máximo.
(15%) Aplique limiarização adaptativa com blocos de tamanho 11, 31 e 51 a uma imagem contendo iluminação não uniforme. Compare os resultados com a limiarização global de Otsu e discuta as vantagens e limitações de cada abordagem.
(15%) Execute o pipeline watershed da imagem de moedas variando o limiar aplicado à Transformada de Distância (\(0.3\), \(0.5\) e \(0.7\) vezes o valor máximo). Explique como esse parâmetro influencia a geração dos marcadores, a separação de objetos adjacentes e a ocorrência de sobre-segmentação.
(15%) Utilizando mm.drawImg, construa uma demonstração visual passo a passo da erosão de uma imagem binária 7×7 com elemento estruturante quadrado 3×3. Para cada posição analisada, indique se o elemento estruturante está completamente contido no objeto e justifique o valor atribuído ao pixel de saída.
(15%) Demonstre experimentalmente a dualidade entre erosão e dilatação verificando a identidade \((A \ominus B)^c = A^c \oplus \hat{B}\) utilizando mm.ero, mm.dil e mm.bnot. Calcule a diferença pixel a pixel entre os dois lados da equação e apresente o resultado utilizando mm.histImg ou visualização equivalente.
(15%) Implemente manualmente o gradiente morfológico utilizando apenas mm.ero e mm.dil, comparando o resultado com mm.gradm(img, B). Avalie o efeito de diferentes elementos estruturantes (quadrado 3×3, disco 5×5 e linha 1×9) sobre a detecção de bordas.
(15%) Construa um pipeline completo para contagem e classificação de moedas por tamanho (pequena, média e grande) utilizando área e circularidade como descritores. Gere uma máscara de referência (ground truth) manualmente e calcule a métrica IoU (Intersection over Union) para avaliar a qualidade da segmentação. Apresente os resultados em tabela e por meio de visualizações produzidas com mm.show.
Referências do Capítulo
A fundamentação teórica deste capítulo baseia-se nas seguintes obras:
Gonzalez; Woods (2018) para os conceitos de segmentação, limiarização de Otsu, Transformada de Distância, watershed, morfologia matemática e descritores de forma.
Matheron (1975) e Serra (1982) para a fundamentação teórica original, formulação algébrica e desenvolvimento da Morfologia Matemática.
Szeliski (2022) para segmentação baseada em regiões, rotulação de componentes conexos, watershed baseado em marcadores e avaliação de segmentação por meio da métrica IoU.
Bradski; Kaehler (2008) para a utilização prática da biblioteca OpenCV, incluindo funções como mm.dist, mm.watershed, mm.label0 e extração de contornos.
Redmon et al. (2016) para a introdução aos detectores modernos da família YOLO e sua relação com descritores geométricos como bounding boxes extraídas por segmentação.
Singh; Lloyd; Flicker (2024) para a construção de labirintos complexos baseados em ciclos hamiltonianos sobre mosaicos quasicristalinos, utilizados como exemplo de aplicação da Transformada de Distância Geodésica e de algoritmos de busca de caminhos.
Zampirolli et al. (2025) para a implementação dos operadores morfológicos, transformadas geodésicas e resolução de labirintos por propagação de distâncias em domínios restritos.
4.9 💻 Parte Prática com Exercícios de Programação
Esta lista transforma os conceitos do Capítulo 4 em uma trilha prática de segmentação e morfologia matemática. Os EPs começam com limiarização e avançam até rotulação e descritores de componentes, sempre com matrizes pequenas para que cada pixel possa ser conferido à mão.
ImportanteRegra comum dos EPs morfológicos
Nas operações com vizinhança, não faça padding. Para cada pixel, avalie apenas as posições do elemento estruturante que caem dentro do domínio da imagem. Essa é a mesma ideia das implementações didáticas em morph.py, como mm.dil0, mm.ero0, mm.dil1 e mm.label0: a vizinhança é recortada pelo domínio válido da imagem.
🎯 Objetivo deste Caderno
O caderno permite desenvolver, validar, organizar e testar soluções de Exercícios de Programação (EPs) em ambientes interativos, como o Colab, com os mesmos casos de teste do Moodle, copiando para lá apenas na hora de registrar a nota oficial.
Download
Baixe morph.py e testsuite.py executando a célula abaixo:
Para avaliar os testes, execute TestSuite("EP04_01.extensão").run() numa nova célula, trocando a extensão pela da linguagem usada (.py, .java, .c, .cpp, .js ou .r). O sistema baixa os casos de teste do GitHub, executa o programa e calcula a nota automaticamente.
Para testar código Python diretamente, sem salvar arquivo, use run_code(codigo) passando o código como string numa variável codigo:
codigo ="""from morph import mm# ... seu código aqui ..."""TestSuite("EP04_01").run_code(codigo)
4.9.1 EP04_01 🎚️ Limiarização Global por Limiar Fixo
Em scanners de documentos e em sistemas de leitura de código de barras, a primeira etapa do processamento é sempre separar o que é “objeto” (tinta, texto, barras) do que é “fundo” (papel, embalagem). A limiarização global faz exatamente isso: compara cada pixel a um único limiar \(T\) e decide, em tempo real, se ele pertence à classe clara ou à classe escura. É o operador de segmentação mais simples — e mesmo assim, está por trás de boa parte dos pipelines industriais de inspeção visual. Ver na Figura 4.30 uma simulação deste EP.
4.9.1.1 📋 Diretrizes de Implementação
Dimensões: Ler os inteiros \(L\) (linhas) e \(C\) (colunas).
Limiar: Ler o inteiro \(T\) (limiar de decisão).
Dados: Ler os valores inteiros da matriz original linha a linha.
Mapeamento: Para cada pixel \(p\), calcular o novo valor pela equação:
\[
p' =
\begin{cases}
255, & \text{se } p > T \\
0, & \text{se } p \le T
\end{cases}
\] 5. Saída: Exibir a matriz binarizada com dimensões \(L \times C\).
4.9.1.2 📌 Restrições Computacionais
Binarização: A saída contém apenas os valores \(0\) ou \(255\).
Comparação estrita: O critério usa \(> T\) (pixels iguais a \(T\) tornam-se fundo).
Tipo: O resultado final deve ser inteiro.
Observação: Este EP segue a convenção da OpenCV (cv2.THRESL_BINARY): apenas pixels com valor maior que\(T\) tornam-se brancos (255); pixels com valor igual a\(T\) permanecem pretos (0).
4.9.1.3 🧠 Fundamentação Teórica
Parâmetro
Tipo
Impacto Visual
\(T\) pequeno
Inteiro
A maioria dos pixels torna-se branca
\(T\) grande
Inteiro
A maioria dos pixels torna-se preta
\(T\) bem escolhido
Inteiro
Separa nitidamente objeto e fundo
4.9.1.4 📦 Especificação de Entrada e Saída (VPL)
Entrada:
Linha 1: Inteiro \(L\).
Linha 2: Inteiro \(C\).
Linha 3: Inteiro \(T\).
Linhas seguintes: Elementos inteiros da matriz original.
Saída:
Matriz binarizada em \(L\) linhas e \(C\) colunas, valores \(0\) ou \(255\) separados por espaço.
4.9.1.5 📌 Exemplos
Entrada
Saída
Observação
2
4
100
0 99 100 180
255 30 120 80
0 0 0 255
255 0 255 0
\(T=100\): apenas pixels com valor maior que 100 tornam-se brancos;
por isso, 99 e 100 tornam-se pretos.
1
3
0
0 50 255
0 255 255
\(T=0\): apenas pixels com valor estritamente maior que 0 tornam-se brancos.
👆 Clique numa célula da Entrada Original para escurecer o píxel (−30) e clique com o botão direito para clarear (+30). Ajuste o limiar T para binarização.
128
Entrada Original (Clicável)
Resultado Binarizado (p')
Fórmula aplicada: (p > 128) ? 255 : 0
Figura 4.30: Simulador EP04_01: Limiarização Global por Limiar Fixo (p’ = (p > T) ? 255 : 0)
%%writefile EP04_01.py# Código Python
Writing EP04_01.py
TestSuite("EP04_01.py").run()
✔️ EP04_01.cases já existe em casos/
📋 7 caso(s) carregado(s) de casos/EP04_01.cases
🔍 Testando Python: EP04_01.py
⚠️ EP04_01.py: Arquivo sem conteúdo (menos de 3 linhas). Testes ignorados.
4.9.2 EP04_02 📊 Limiarização Automática de Otsu
Escolher manualmente o limiar \(T\) funciona quando a iluminação é estável, mas em microscopia digital e em inspeção de lâminas de sangue, cada amostra tem um contraste diferente — um limiar fixo falharia de imagem para imagem. O método de Otsu resolve isso encontrando, sozinho, o limiar que maximiza a separação estatística entre as duas classes de pixels, tornando a segmentação automática e adaptativa. Ver na Figura 4.31 uma simulação deste EP.
4.9.2.1 📋 Diretrizes de Implementação
Dimensões: Ler os inteiros \(L\) (linhas) e \(C\) (colunas).
Dados: Ler os valores inteiros da matriz original linha a linha.
Histograma: Construir o histograma \(h[i]\), \(i=0,\dots,255\), contando quantos pixels têm valor \(i\).
Busca do limiar: Para cada candidato \(T\) de \(1\) a \(255\), calcular a variância entre classes: \[
\sigma_B^2(T) = \frac{n_0 \cdot n_1}{N^2}\,(m_0 - m_1)^2
\] onde \(n_0,n_1\) são as quantidades de pixels com valor \(<T\) e \(\geq T\), \(m_0,m_1\) são suas médias, e \(N=L\times C\).
Escolha: O limiar ótimo \(T^*\) é o que maximiza \(\sigma_B^2(T)\) (em caso de empate, manter o primeiro encontrado).
Aplicação: Binarizar a imagem usando T*, aplicando: \[
p' =
\begin{cases}
255, & \text{se } p > T^* \\
0, & \text{se } p \le T^*
\end{cases}
\]
4.9.2.2 📌 Restrições Computacionais
Candidatos válidos: Ignorar \(T\) que deixe \(n_0=0\) ou \(n_1=0\) (classe vazia).
Empate: Sempre manter o primeiro\(T\) que atingiu o valor máximo de \(\sigma_B^2\).
Tipo:\(T^*\) e a matriz de saída devem ser inteiros.
Convenção OpenCV: A binarização segue cv2.THRESL_BINARY; pixels com valor exatamente igual a \(T^*\) tornam-se pretos.
4.9.2.3 🧠 Fundamentação Teórica
Conceito
Significado
Impacto
\(\sigma_B^2(T)\) alta
Classes bem separadas em \(T\)
\(T\) é um bom candidato a limiar
Histograma bimodal
Dois “morros” distintos
Otsu encontra o vale entre eles
Histograma unimodal
Um único “morro”
Otsu ainda escolhe algum\(T\), mas a segmentação é pouco confiável
4.9.2.4 📦 Especificação de Entrada e Saída (VPL)
Entrada:
Linha 1: Inteiro \(L\).
Linha 2: Inteiro \(C\).
Linhas seguintes: Elementos inteiros da matriz original.
Saída:
Matriz binarizada em \(L\) linhas e \(C\) colunas, valores \(0\) ou \(255\).
👆 Clique esquerdo escurece (−25) e clique com o botão direito clareia (+25) os pixels da entrada. Observe o limiar ótimo T* ajustar-se dinamicamente ao histograma.
T* = −
Entrada Original (Clicável)
Resultado Otsu (p')
Figura 4.31: Simulador EP04_02: Limiarização Automática de Otsu (T* = argmax σ²_B(T))
%%writefile EP04_02.py# Código Python
Writing EP04_02.py
TestSuite("EP04_02.py").run()
✔️ EP04_02.cases já existe em casos/
📋 5 caso(s) carregado(s) de casos/EP04_02.cases
🔍 Testando Python: EP04_02.py
⚠️ EP04_02.py: Arquivo sem conteúdo (menos de 3 linhas). Testes ignorados.
4.9.3 EP04_03 🌱 Dilatação Binária Plana (mm.dil0)
Em microscopia de partículas e em OCR de placas de carro desgastadas, traços finos ou descontínuos precisam ser “engrossados” para que o reconhecimento funcione. A dilatação morfológica faz exatamente isso: expande regiões claras usando um elemento estruturante \(B\) — a mesma operação implementada em morph.py como mm.dil0(f, B), usada quando \(B\) é plano (sem pesos, só \(0\)/\(1\)). Ver na Figura 4.32 uma simulação deste EP.
4.9.3.1 📋 Diretrizes de Implementação
Dimensões da imagem: Ler os inteiros \(L\) (linhas) e \(C\) (colunas) de \(f\).
Dimensões de \(B\): Ler os inteiros \(L_B\) (linhas) e \(C_B\) (colunas) do elemento estruturante.
Elemento estruturante: Ler a matriz \(B\) com valores \(0\) ou \(1\), linha a linha.
Dados: Ler a matriz \(f\) (a imagem original), linha a linha.
Reflexão: Construir \(B_{ref}\), a versão de \(B\) refletida em \(180°\) (linhas e colunas invertidas) — exatamente como faz mm.dil0 internamente.
Vizinhança sem padding: Para cada pixel \((y,x)\), percorrer as posições \((by,bx)\) de \(B_{ref}\) centradas em \((y,x)\), usando o deslocamento \[
v_y = y + by + o_y,\quad v_x = x + bx + o_x,\quad o_y=-\tfrac{L_B}{2}+0{,}5,\quad o_x=-\tfrac{C_B}{2}+0{,}5
\]Descartar todo \((v_y,v_x)\) fora de \([0,L)\times[0,C)\) — não preencher com zeros.
Mapeamento: Calcular cada pixel de saída como o máximo entre \(f(y,x)\) e todos os \(f(v_y,v_x)\) válidos cuja posição correspondente em \(B_{ref}\) vale \(1\): \[
g(y,x) = \max\Big(f(y,x),\ \max_{\substack{(v_y,v_x)\ \text{válido}\\ B_{ref}(by,bx)=1}} f(v_y,v_x)\Big)
\]
Saída: Exibir a matriz \(g\) com dimensões \(L \times C\).
4.9.3.2 📌 Restrições Computacionais
Sem padding: Jamais inventar vizinhos fora da imagem; usar apenas os que existem de fato.
Reflexão obrigatória:\(B\) deve ser refletido antes de aplicado (é o que diferencia mm.dil0 de uma simples busca por máximo).
Robustez de borda: Se nenhuma posição válida de \(B_{ref}=1\) cair dentro do domínio para um dado pixel, ele mantém seu valor original.
4.9.3.3 🧠 Fundamentação Teórica
Conceito
Significado
Impacto Visual
Dilatação
\(g \geq f\) sempre (extensiva)
Regiões claras crescem, buracos escuros encolhem
\(B\) maior
Vizinhança mais ampla
Crescimento mais agressivo
Reflexão de \(B\)
\(B_{ref}(y,x) = B(-y,-x)\)
Garante a definição formal de Minkowski da dilatação
4.9.3.4 📦 Especificação de Entrada e Saída (VPL)
Entrada:
Linha 1: Inteiro \(L\).
Linha 2: Inteiro \(C\).
Linha 3: Inteiro \(L_B\).
Linha 4: Inteiro \(C_B\).
Próximas \(L_B\) linhas: elementos inteiros (\(0\) ou \(1\)) da matriz \(B\).
Próximas \(L\) linhas: elementos inteiros da matriz \(f\).
Saída:
Matriz \(g\) em \(L\) linhas e \(C\) colunas, valores inteiros separados por espaço.
4.9.3.5 📌 Exemplos
Entrada
Saída
Observação
3
3
3
3
0 1 0
1 1 1
0 1 0
0 0 0
0 9 0
0 0 0
0 9 0
9 9 9
0 9 0
\(B\) em cruz simétrico: ponto isolado se expande em cruz
1
4
1
3
1 1 1
10 200 5 80
200 200 200 80
\(B\) horizontal: cada pixel “puxa” o máximo dos vizinhos da linha
🌱 Simulador EP04_03: Dilatação Plana (mm.dil0)g = f ⊕ B
Alterne o elemento estruturante B (ou selecione os presets) e clique nas células da imagem original f para acender ou apagar pixels.
Elemento Estruturante B (Clique para Alternar 0/1)
Imagem Original f (5×5)
Dilatada g (f ⊕ B)
g(y,x) = max sobre vizinhos válidos de B refletido
Figura 4.32: Simulador EP04_03: Dilatação Binária Plana (g = f ⊕ B)
%%writefile EP04_03.py# Código Python
Writing EP04_03.py
TestSuite("EP04_03.py").run()
✔️ EP04_03.cases já existe em casos/
📋 5 caso(s) carregado(s) de casos/EP04_03.cases
🔍 Testando Python: EP04_03.py
⚠️ EP04_03.py: Arquivo sem conteúdo (menos de 3 linhas). Testes ignorados.
4.9.4 EP04_04 🪨 Erosão Binária Plana (mm.ero0)
Se a dilatação engrossa, a erosão afina. Em sistemas de contagem de células, ela é usada para separar células que se tocam: ao “comer” as bordas de cada região, conexões finas entre objetos desaparecem antes mesmo de qualquer contagem ser feita. Em morph.py, essa é a operação mm.ero0(f, B) — a dual exata da dilatação, e a única das duas que não reflete o elemento estruturante. Ver na Figura 4.33 uma simulação deste EP.
4.9.4.1 📋 Diretrizes de Implementação
Dimensões da imagem: Ler os inteiros \(L\) (linhas) e \(C\) (colunas) de \(f\).
Dimensões de \(B\): Ler os inteiros \(L_B\) (linhas) e \(C_B\) (colunas) do elemento estruturante.
Elemento estruturante: Ler a matriz \(B\) com valores \(0\) ou \(1\), linha a linha.
Dados: Ler a matriz \(f\) (a imagem original), linha a linha.
Vizinhança sem padding (sem reflexão!): Para cada pixel \((y,x)\), percorrer as posições \((by,bx)\) de \(B\)na ordem original (sem refletir), usando o mesmo deslocamento do EP04_03: \[
v_y = y + by + o_y,\quad v_x = x + bx + o_x,\quad o_y=-\tfrac{L_B}{2}+0{,}5,\quad o_x=-\tfrac{C_B}{2}+0{,}5
\]
Descartar todo \((v_y,v_x)\) fora de \([0,L)\times[0,C)\). 6. Mapeamento: Calcular cada pixel de saída como o mínimo entre \(f(y,x)\) e todos os \(f(v_y,v_x)\) válidos cuja posição correspondente em \(B\) vale \(1\): \[
g(y,x) = \min\Big(f(y,x),\ \min_{\substack{(v_y,v_x)\ \text{válido}\\ B(by,bx)=1}} f(v_y,v_x)\Big)
\] 7. Saída: Exibir a matriz \(g\) com dimensões \(L \times C\).
4.9.4.2 📌 Restrições Computacionais
Sem reflexão: Diferente da dilatação, \(B\) é usado exatamente como lido — refletir aqui seria um erro conceitual grave.
Sem padding: Vizinhos fora da imagem são simplesmente ignorados, nunca tratados como \(0\).
Robustez de borda: Se nenhuma posição válida de \(B=1\) cair dentro do domínio, o pixel mantém seu valor original.
4.9.4.3 🧠 Fundamentação Teórica
Conceito
Significado
Impacto Visual
Erosão
\(g \leq f\) sempre (anti-extensiva)
Regiões claras encolhem, ruído pontual desaparece
Dualidade
\(\text{ero}(f,B) = -\text{dil}(-f, B_{ref})\)
Erosão e dilatação são “espelhos” matemáticos
\(B\) maior
Erosão mais agressiva
Objetos finos somem completamente
4.9.4.4 📦 Especificação de Entrada e Saída (VPL)
Entrada:
Linha 1: Inteiro \(L\).
Linha 2: Inteiro \(C\).
Linha 3: Inteiro \(L_B\).
Linha 4: Inteiro \(C_B\).
Próximas \(L_B\) linhas: elementos inteiros (\(0\) ou \(1\)) da matriz \(B\).
Próximas \(L\) linhas: elementos inteiros da matriz \(f\).
Saída:
Matriz \(g\) em \(L\) linhas e \(C\) colunas, valores inteiros separados por espaço.
4.9.4.5 📌 Exemplos
Entrada
Saída
Observação
3
3
3
3
0 1 0
1 1 1
0 1 0
9 9 9
9 0 9
9 9 9
9 0 9
0 0 0
9 0 9
O “buraco” central (0) se propaga em cruz
1
4
1
3
1 1 1
10 200 5 80
10 5 5 80
\(B\) horizontal: cada pixel “puxa” o mínimo dos vizinhos da linha
🪨 Simulador EP04_04: Erosão Plana (mm.ero0)g = f ⊖ B
Alterne o elemento estruturante B (ou selecione os presets) e clique nas células da imagem original f para acender ou apagar pixels.
Elemento Estruturante B (Clique para Alternar 0/1)
Imagem Original f (5×5)
Erodida g (f ⊖ B)
g(y,x) = min sobre vizinhos válidos de B (sem refletir)
Figura 4.33: Simulador EP04_04: Erosão Binária Plana (g = f ⊖ B)
%%writefile EP04_04.py# Código Python
Writing EP04_04.py
TestSuite("EP04_04.py").run()
✔️ EP04_04.cases já existe em casos/
📋 5 caso(s) carregado(s) de casos/EP04_04.cases
🔍 Testando Python: EP04_04.py
⚠️ EP04_04.py: Arquivo sem conteúdo (menos de 3 linhas). Testes ignorados.
4.9.5 EP04_05 🧹 Abertura Morfológica (Remoção de Ruído)
Imagens capturadas por sensores de baixo custo, como os de drones agrícolas, costumam vir salpicadas de pequenos pontos de ruído — pixels isolados que não representam nada real. Aplicar erosão seguida de dilatação com o mesmo elemento estruturante produz a abertura: ela “limpa” pontos e protuberâncias finas, mas devolve ao objeto principal praticamente seu tamanho original. É a combinação clássica usada em pré-processamento de imagens de satélite antes de qualquer contagem de área plantada. Ver na Figura 4.34 uma simulação deste EP.
4.9.5.1 📋 Diretrizes de Implementação
Dimensões da imagem: Ler os inteiros \(L\) (linhas) e \(C\) (colunas) de \(f\).
Dimensões de \(B\): Ler os inteiros \(L_B\) (linhas) e \(C_B\) (colunas) do elemento estruturante.
Elemento estruturante: Ler a matriz \(B\) com valores \(0\) ou \(1\), linha a linha.
Dados: Ler a matriz binária \(f\) (valores \(0\) ou \(1\)), linha a linha.
Erosão: Calcular \(e = f \ominus B\), usando exatamente o algoritmo do EP04_04 (sem refletir \(B\), sem padding).
Dilatação: Calcular \(g = e \oplus B\), usando exatamente o algoritmo do EP04_03 (refletindo \(B\), sem padding) — mas agora aplicado sobre \(e\), não sobre \(f\).
Saída: Exibir a matriz resultante \(g\) (a abertura de \(f\) por \(B\)) com dimensões \(L \times C\).
4.9.5.2 📌 Restrições Computacionais
Ordem fixa: É sempre erosão primeiro, depois dilatação — a ordem inversa define outro operador (fechamento, do próximo EP).
Mesmo \(B\): O elemento estruturante usado na erosão e na dilatação deve ser idêntico.
Pontos isolados e a protuberância fina desaparecem; o quadrado central sobrevive
🧹 Simulador EP04_05: Abertura Morfológicag = (f ⊖ B) ⊕ B
Clique nas células de f original para acender ou apagar pixels (crie seu próprio ruído de fundo!) e ajuste o tamanho do elemento estruturante B.
3×3
f Original (Clicável)
e = f ⊖ B (Erosão)
g = e ⊕ B (Abertura)
Figura 4.34: Simulador EP04_05: Abertura Morfológica (g = (f ⊖ B) ⊕ B)
%%writefile EP04_05.py# Código Python
Writing EP04_05.py
TestSuite("EP04_05.py").run()
✔️ EP04_05.cases já existe em casos/
📋 5 caso(s) carregado(s) de casos/EP04_05.cases
🔍 Testando Python: EP04_05.py
⚠️ EP04_05.py: Arquivo sem conteúdo (menos de 3 linhas). Testes ignorados.
4.9.6 EP04_06 🧩 Fechamento Morfológico (Preenchimento de Falhas)
Em digitalização de impressões digitais, sulcos da pele às vezes ficam interrompidos por sujeira ou ressecamento, criando pequenas falhas na curva contínua que deveria existir. O fechamento — dilatação seguida de erosão com o mesmo elemento estruturante — é o operador dual da abertura: ele preenche buracos pequenos e reentrâncias estreitas, sem alterar significativamente o contorno externo do objeto. É a etapa padrão antes de extrair o esqueleto de uma impressão digital. Ver na Figura 4.35 uma simulação deste EP.
4.9.6.1 📋 Diretrizes de Implementação
Dimensões da imagem: Ler os inteiros \(L\) (linhas) e \(C\) (colunas) de \(f\).
Dimensões de \(B\): Ler os inteiros \(L_B\) (linhas) e \(C_B\) (colunas) do elemento estruturante.
Elemento estruturante: Ler a matriz \(B\) com valores \(0\) ou \(1\), linha a linha.
Dados: Ler a matriz binária \(f\) (valores \(0\) ou \(1\)), linha a linha.
Dilatação: Calcular \(d = f \oplus B\), usando exatamente o algoritmo do EP04_03 (refletindo \(B\), sem padding).
Erosão: Calcular \(g = d \ominus B\), usando exatamente o algoritmo do EP04_04 (sem refletir \(B\), sem padding) — agora aplicado sobre \(d\), não sobre \(f\).
Saída: Exibir a matriz resultante \(g\) (o fechamento de \(f\) por \(B\)) com dimensões \(L \times C\).
4.9.6.2 📌 Restrições Computacionais
Ordem fixa: É sempre dilatação primeiro, depois erosão — a ordem inversa é a abertura do EP04_05.
Mesmo \(B\): O elemento estruturante usado na dilatação e na erosão deve ser idêntico.
Os dois buracos internos não-adjacentes são totalmente preenchidos
🧩 Simulador EP04_06: Fechamento Morfológicog = (f ⊕ B) ⊖ B
Clique nas células de f original para acender ou apagar pixels (preencha buracos internos!) e ajuste o tamanho do elemento estruturante B.
3×3
f Original (Clicável)
d = f ⊕ B (Dilatação)
g = d ⊖ B (Fechamento)
Figura 4.35: Simulador EP04_06: Fechamento Morfológico (g = (f ⊕ B) ⊖ B)
%%writefile EP04_06.py# Código Python
Writing EP04_06.py
TestSuite("EP04_06.py").run()
✔️ EP04_06.cases já existe em casos/
📋 5 caso(s) carregado(s) de casos/EP04_06.cases
🔍 Testando Python: EP04_06.py
⚠️ EP04_06.py: Arquivo sem conteúdo (menos de 3 linhas). Testes ignorados.
4.9.7 EP04_07 ⛰️ Dilatação e Erosão com Pesos (mm.dil1 / mm.ero1)
Até agora, o elemento estruturante só dizia “este vizinho conta” ou “não conta” — mas em modelos digitais de elevação (usados em SIG e em planejamento de drenagem urbana), cada vizinho deveria ter um peso diferente dependendo da distância ou da direção do relevo. As versões ponderadas da dilatação e da erosão, implementadas em morph.py como mm.dil1(f, b) e mm.ero1(f, b), somam (ou subtraem) o peso de cada vizinho antes de tomar o máximo (ou mínimo) — generalizando tudo o que foi feito nos EPs anteriores. Ver na Figura 4.36 uma simulação deste EP.
4.9.7.1 📋 Diretrizes de Implementação
Dimensões da imagem: Ler os inteiros \(L\) (linhas) e \(C\) (colunas) de \(f\).
Dimensões de \(b\): Ler os inteiros \(L_B\) (linhas) e \(C_B\) (colunas) do elemento estruturante ponderado.
Pesos: Ler a matriz \(b\) de pesos inteiros (podem ser negativos, zero ou positivos), linha a linha.
Dados: Ler a matriz \(f\) (a imagem original), linha a linha.
Vizinhança sem padding: Para cada pixel \((y,x)\), percorrer todas as posições \((by,bx)\) de \(b\) (não apenas onde valeria \(1\) — aqui todo peso participa), usando o mesmo deslocamento dos EPs anteriores: \[
v_y = y + by + o_y,\quad v_x = x + bx + o_x,\quad o_y=-\tfrac{L_B}{2}+0{,}5,\quad o_x=-\tfrac{C_B}{2}+0{,}5
\]Descartar todo \((v_y,v_x)\) fora de \([0,L)\times[0,C)\).
Erosão ponderada: Calcular, usando o mesmo \(b\) e sem refletir: \[
g_{ero}(y,x) = \min\Big(f(y,x),\ \min_{(v_y,v_x)\ \text{válido}} \big(f(v_y,v_x) - b(by,bx)\big)\Big)
\]
Saída: Exibir primeiro a matriz \(g_{dil}\) completa, e depois a matriz \(g_{ero}\) completa.
4.9.7.2 📌 Restrições Computacionais
Nenhuma das duas reflete \(b\) — a versão ponderada não usa reflexão, mesmo na dilatação (diferente de mm.dil0).
Todos os pesos participam: Não existe aqui o filtro “\(B=1\)”; mesmo peso \(0\) entra na conta.
Sem padding: vizinhos fora da imagem são ignorados, nunca virtualmente preenchidos.
Tipo: A saída pode conter valores negativos ou maiores que \(255\) — não há clipping neste EP.
Dica: Para remover mensagens de overflow ao ultrapassar limites do tipo uint8, incluir no início do código:
import warningswarnings.filterwarnings("ignore")
4.9.7.3 🧠 Fundamentação Teórica
Conceito
Significado
Impacto Visual
Peso positivo
“Puxa” o valor do vizinho para cima na dilatação
Simula relevo que sobe naquela direção
Peso negativo
Reduz a contribuição do vizinho
Simula distância ou atenuação direcional
Dualidade ponderada
\(\text{ero1}(f,b) = -\text{dil1}(-f,b)\)
A simetria entre as duas operações se mantém mesmo com pesos
4.9.7.4 📦 Especificação de Entrada e Saída (VPL)
Entrada:
Linha 1: Inteiro \(L\).
Linha 2: Inteiro \(C\).
Linha 3: Inteiro \(L_B\).
Linha 4: Inteiro \(C_B\).
Próximas \(L_B\) linhas: elementos inteiros (podem ser negativos) da matriz \(b\).
Próximas \(L\) linhas: elementos inteiros da matriz \(f\).
Saída:
Primeiro a matriz \(g_{dil}\) em \(L\) linhas e \(C\) colunas.
Em seguida a matriz \(g_{ero}\) em \(L\) linhas e \(C\) colunas.
Peso central \(2\) acelera o crescimento na dilatação e o encolhimento na erosão
⛰️ Simulador EP04_07: Pesos no Elemento Estruturantedil1 / ero1
Ajuste os pesos do elemento estruturante b com os controles deslizantes e observe o efeito da dilatação e erosão com pesos sobre a matriz f.
Pesos b (Ajuste os Sliders por Célula)
f Original
dil1(f, b) (Dilatação)
ero1(f, b) (Erosão)
Figura 4.36: Simulador EP04_07: Dilatação e Erosão com Pesos (mm.dil1 / mm.ero1)
%%writefile EP04_07.py# Código Python
Writing EP04_07.py
TestSuite("EP04_07.py").run()
✔️ EP04_07.cases já existe em casos/
📋 3 caso(s) carregado(s) de casos/EP04_07.cases
🔍 Testando Python: EP04_07.py
⚠️ EP04_07.py: Arquivo sem conteúdo (menos de 3 linhas). Testes ignorados.
4.9.8 EP04_08 🌋 Gradiente Morfológico, Top-hat e Black-hat
Em inspeção automática de placas de circuito, três perguntas aparecem o tempo todo: onde estão as bordas dos componentes? Quais detalhes claros e pequenos (como pontos de solda) se destacam do fundo? Quais reentrâncias escuras (como fissuras) o fundo esconde? Um único par erosão/dilatação responde às três: o gradiente morfológico evidencia contornos, o top-hat revela picos estreitos, e o black-hat revela vales estreitos — três ferramentas, uma só vizinhança. Ver na Figura 4.37 uma simulação deste EP.
4.9.8.1 📋 Diretrizes de Implementação
Dimensões da imagem: Ler os inteiros \(L\) (linhas) e \(C\) (colunas) de \(f\).
Dimensões de \(B\): Ler os inteiros \(L_B\) (linhas) e \(C_B\) (colunas) do elemento estruturante.
Elemento estruturante: Ler a matriz \(B\) com valores \(0\) ou \(1\), linha a linha.
Dados: Ler a matriz \(f\) (a imagem original, em tons de cinza), linha a linha.
Operadores de base: Calcular, exatamente como nos EPs 04_03 a 04_06:
(gradiente: halo \(3\times3=70\) em torno de \((2,2)\) e halo \(3\times3=8\) em torno de \((6,6)\), resto \(0\))
(top-hat: único \(70\) em \((2,2)\), resto \(0\))
(black-hat: único \(8\) em \((6,6)\), resto \(0\))
Adicione picos ou vales na matriz f e observe o comportamento simultâneo dos operadores de gradiente, top-hat e black-hat.
f (Entrada)
Gradiente
Top-hat
Black-hat
Figura 4.37: Simulador EP04_08: Gradiente Morfológico, Top-hat e Black-hat
%%writefile EP04_08.py# Código Python
Writing EP04_08.py
TestSuite("EP04_08.py").run()
✔️ EP04_08.cases já existe em casos/
📋 4 caso(s) carregado(s) de casos/EP04_08.cases
🔍 Testando Python: EP04_08.py
⚠️ EP04_08.py: Arquivo sem conteúdo (menos de 3 linhas). Testes ignorados.
4.9.9 EP04_09 🗺️ Transformada de Distância e o “Miolo” do Objeto
Em robótica móvel, ao planejar uma rota dentro de um corredor, o robô quer saber não apenas onde existe espaço livre, mas também quão longe cada ponto livre está da parede mais próxima. Os caminhos mais seguros tendem a passar pelo “miolo” do corredor, longe dos obstáculos.
A transformada de distância morfológica atribui a cada pixel um valor que representa sua distância até a borda mais próxima, segundo a métrica definida pelo elemento estruturante. Pixels próximos à borda recebem valores baixos, enquanto pixels mais internos recebem valores maiores. O pixel de valor máximo corresponde à região mais protegida do objeto, frequentemente associada ao seu centro morfológico.
Dimensões da imagem: ler os inteiros \(L\) (linhas) e \(C\) (colunas) da imagem \(f\).
Dimensões de \(B\): ler os inteiros \(L_B\) (linhas) e \(C_B\) (colunas) do elemento estruturante.
Elemento estruturante: ler a matriz \(b\), contendo valor \(0\) no centro e valores negativos nas demais posições.
Imagem: ler a matriz binária \(f\) (valores \(0\) ou \(1\)), linha a linha.
Preparação: multiplicar a imagem por \(L\times C\), garantindo que os pixels internos tenham valor inicial suficientemente alto para a propagação das distâncias.
Transformada de distância: calcular a matriz de distâncias utilizando o método mm.dist1(f,b).
Saída: exibir a matriz resultante da transformada de distância.
4.9.9.2 📌 Restrições Computacionais
Utilizar a implementação de erosão ponderada fornecida pela biblioteca.
O elemento estruturante pode conter valores negativos arbitrários.
A transformada deve ser obtida pela aplicação iterativa de erosões ponderadas até atingir um ponto fixo.
⚠️ Nota Crucial sobre Leitura de Matrizes: Como o elemento estruturante pode conter valores inteiros negativos (por exemplo, -1 e -99), não utilize a função mm.readImg para ler a matriz \(b\). Essa função converte os dados para o tipo uint8, provocando underflow e corrompendo os valores negativos. Leia as \(L_B\) linhas de \(b\) manualmente utilizando o tipo padrão int. A imagem \(f\) pode continuar sendo lida normalmente por mm.readImg.
4.9.9.3 🧠 Fundamentação Teórica
Conceito
Significado
Impacto Visual
\(\text{dist}(y,x)\)
Distância morfológica até a borda mais próxima segundo a métrica definida por \(b\)
Pixels mais internos recebem valores maiores
Valor máximo
Pixel mais distante da borda
Aproxima o centro morfológico do objeto
Elemento estruturante ponderado
Define os custos de deslocamento entre pixels vizinhos
Determina a métrica de distância utilizada
Objetos finos
Regiões estreitas do objeto
Produzem valores baixos de distância
4.9.9.4 📦 Especificação de Entrada e Saída (VPL)
Entrada:
Linha 1: inteiro \(L\).
Linha 2: inteiro \(C\).
Linha 3: inteiro \(L_B\).
Linha 4: inteiro \(C_B\).
Próximas \(L_B\) linhas: elementos inteiros da matriz \(b\).
Próximas \(L\) linhas: elementos binários (\(0\) ou \(1\)) da matriz \(f\).
⚠️ Nota de implementação: Os elementos da matriz \(f\) (0 ou 1) devem ser multiplicados por 255 para gerar uma imagem binária adequada (\(0\) e \(255\)) antes de aplicar a Transformada de Distância (TD).
Saída:
Matriz da transformada de distância em \(L\) linhas e \(C\) colunas.
Nota: o valor -99 atua como uma aproximação prática de \(-\infty\), impedindo a propagação pelas diagonais. Dessa forma, apenas os vizinhos horizontal e vertical contribuem para a distância, produzindo a distância de Manhattan.
🗺️ Simulador EP04_09: Transformada de DistânciaCamadas de Erosão
Clique nas células para desenhar o seu próprio objeto ou selecione uma forma predefinida para calcular o mapa de distâncias em cascata.
Mapa de Distâncias Calculado
Figura 4.38: Simulador EP04_09: Transformada de Distância (Camadas de Erosão)
%%writefile EP04_09.py# Código Python
Writing EP04_09.py
TestSuite("EP04_09.py").run()
✔️ EP04_09.cases já existe em casos/
📋 4 caso(s) carregado(s) de casos/EP04_09.cases
🔍 Testando Python: EP04_09.py
⚠️ EP04_09.py: Arquivo sem conteúdo (menos de 3 linhas). Testes ignorados.
4.9.10 EP04_10 🪙 Separação de Blobs, Rotulação e Descritores
Em uma linha de produção de moedas, é comum que peças encostem umas nas outras na esteira, formando uma única mancha conectada na imagem — uma contagem ingênua erraria o total. A solução clássica combina operações morfológicas e análise de conectividade: primeiro uma erosão reduz ou rompe conexões frágeis entre objetos, e depois a rotulação de componentes conectados separa cada objeto em uma região distinta. Por fim, descritores geométricos (área e caixa delimitadora) resumem cada componente encontrado.
Dimensões da imagem: ler os inteiros \(L\) (linhas) e \(C\) (colunas) de \(f\).
Dimensões de \(B\): ler os inteiros \(L_B\) (linhas) e \(C_B\) (colunas) do elemento estruturante.
Elemento estruturante: ler a matriz \(B\), contendo valores \(0\) ou \(1\), linha a linha.
Dados: ler a matriz binária \(f\) (valores \(0\) ou \(1\)), linha a linha.
Separação: calcular \[
f_{ero} = f \ominus B
\] usando erosão binária plana (como no EP04_04), eliminando conexões frágeis entre objetos.
Rotulação: sobre \(f_{ero}\), identificar componentes conectados usando conectividade definida pela vizinhança \(B\). A rotulação deve seguir varredura raster: ao encontrar um pixel \(1\) ainda não rotulado, atribuir um novo rótulo inteiro crescente a partir de 1 e propagar esse rótulo a toda a região conectada.
Ajuste a espessura da ponte entre as moedas e observe como a erosão morfológica separa os objetos para contagem e extração de descritores (área e caixa delimitadora).
1 px
f Original (Ligadas)
Após Erosão + Rótulos
Figura 4.39: Simulador EP04_10: Separação de Blobs, Rotulação e Descritores
%%writefile EP04_10.py# Código Python
Writing EP04_10.py
TestSuite("EP04_10.py").run()
✔️ EP04_10.cases já existe em casos/
📋 4 caso(s) carregado(s) de casos/EP04_10.cases
🔍 Testando Python: EP04_10.py
⚠️ EP04_10.py: Arquivo sem conteúdo (menos de 3 linhas). Testes ignorados.
BRADSKI, Gary; KAEHLER, Adrian. Learning OpenCV: Computer vision with the OpenCV library. [S.l.]: " O’Reilly Media, Inc.", 2008.
GONZALEZ, R. C.; WOODS, R. E. Digital Image Processing. 4th. ed. New York: Pearson, 2018.