Na morfologia matemática, as operações fundamentais — erosão, dilatação e esqueletização — são realizadas em imagens binárias. Elas dependem de um elemento estruturante (ou kernel), uma pequena matriz que define a vizinhança ou o padrão de operação.
Considere um elemento estruturante S e uma imagem binária I. O elemento estruturante percorre a imagem, e a operação é definida com base na interação entre S e a vizinhança local dos pixels de I.
1. Fundamentos da Teoria dos Conjuntos
Para formalizar as operações, definimos os seguintes conceitos:
- Elemento: Um ponto a pertencente à região de uma imagem X é dito um elemento de X (a ∈ X).
- Contido (⊆): Uma imagem B está contida em X se todos os seus elementos pertencem a X.
- Acerto (hit): B acerta X se existe pelo menos um ponto que pertence simultaneamente a B e a X (B ∩ X ≠ ∅).
- Complemento (Xc): Conjunto de todos os pontos que não estão na região de X.
- Conjunto Simétrico (Bv): Obtido invertendo as coordenadas (x, y) dos pontos de B para (-x, -y).
- Translação (Ba): Conjunto resultante de deslocar todos os pontos de B por um vetor a.
2. Erosão
A erosão E de uma imagem X por um elemento estruturante B é definida como o conjunto de todos os pontos a para os quais a translação Ba está inteiramente contida em X:
E(X) = X ⊖ B = { a | B_a ⊆ X }
A operação efetivamente "desgasta" as bordas dos objetos na imagem, rmeovendo pixels em suas fronteiras. O resultado depende se B é simétrico. Se não for, a erosão com B e com Bv produzirá resultados diferentes.
Algoritmicamente, para uma imagem binária (0 para fundo, 255 para objeto), um pixel de objeto (0) é mantido como 0 apenas se todos os pixels sob o elemento estruturante também forem 0; caso contrário, ele se torna 255.
3. Dilatação
A dilatação D é a operação dual da erosão. Ela é definida como o conjunto de todos os pontos a para os quais a translação Ba acerta X:
D(X) = X ⊕ B = { a | B_a ∩ X ≠ ∅ }
A dilatação "expande" as regiões dos objetos, adicionando pixels em suas bordas. Um pixel de fundo (255) é convertido para objeto (0) se pelo menos um pixel sob o elemento estruturante for um pixel de objeto (0).
4. Operações Compostas: Abertura e Fechamento
- Abertura: Composição de erosão seguida de dilatação usando o mesmo elemento estruturante B.
Abertura(X) = D(E(X)). Suaviza contornos, quebra istmos finos e elimina pequenas projeções. - Fechamento: Composição de dilatação seguida de erosão usando o mesmo B.
Fechamento(X) = E(D(X)). Suaviza contornos, funde pequenas quebras e preenche pequenos buracos.
Se o elemento estruturante B não for simétrico, a abertura deve ser realizada como D(E(X)), onde D usa Bv para evitar deslocamento indesejado.
5. Esqueletização (Thinning)
A esqueletização tem como objetivo reduzir uma forma binária a um esqueleto de um pixel de largura (eixo medial), preservando sua topologia. Um algoritmo comum usa uma tabela de consulta baseada nos 8 vizinhos de um pixel para determinar se ele pode ser removido sem desconectar a estrutura ou alterar sua forma fundamental.
Regras para remoção (um pixel de objeto P é removido se):
- Não é um ponto interno (todos os vizinhos são objeto).
- Não é um ponto isolado.
- Não é uma ponta de linha.
- Sua remoção não aumenta o número de componentes conectados.
Uma tabela pré-computada de 256 entradas (uma para cada combinação dos 8 vizinhos) codifica essas regras. O processo é iterativo: a imagem é escaneada repetidamente (alternando entre escaneamento horizontal e vertical para evitar viés), e pixels removíveis são apagados, até que nenhuma alteração ocorra em uma pasagem completa.
6. Implementação Conceitual em C
Os exemplos a seguir demonstram uma implementação simplificada para imagens de 8 bits. Note que manipulam diretamente o buffer de dados da imagem.
// Tabela de consulta para esqueletização (fragmento)
// Os bits dos vizinhos (norte, sul, leste, oeste, etc.) são usados como índice.
const unsigned char tabela_remocao[256] = {
0, 0, 1, 1, 0, 0, 1, 1, /* ... preenchida com 0s e 1s ... */
// ... 256 entradas no total ...
};
// Função genérica para aplicar erosão/dilatação com um kernel 3x3
void aplicar_morfologia(unsigned char *dados_saida, const unsigned char *dados_entrada,
int largura, int altura, int eh_dilatacao)
{
// Iteração sobre todos os pixels, excluindo as bordas
for (int linha = 1; linha < altura - 1; linha++) {
for (int coluna = 1; coluna < largura - 1; coluna++) {
int indice_saida = linha * largura + coluna;
int centro = dados_entrada[indice_saida];
if (eh_dilatacao) {
// Dilatação: se o centro for fundo (255), verifica se algum vizinho é objeto (0)
if (centro == 255) {
int vizinho_objeto = 0;
// Verifica os 8 vizinhos (simplificado)
for (int dy = -1; dy <= 1 && !vizinho_objeto; dy++) {
for (int dx = -1; dx <= 1 && !vizinho_objeto; dx++) {
int vizinho = dados_entrada[(linha + dy) * largura + (coluna + dx)];
if (vizinho == 0) vizinho_objeto = 1;
}
}
dados_saida[indice_saida] = vizinho_objeto ? 0 : 255;
} else {
dados_saida[indice_saida] = 0; // Pixel de objeto permanece
}
} else {
// Erosão: se o centro for objeto (0), verifica se todos os vizinhos são objeto
if (centro == 0) {
int todos_objeto = 1;
for (int dy = -1; dy <= 1 && todos_objeto; dy++) {
for (int dx = -1; dx <= 1 && todos_objeto; dx++) {
int vizinho = dados_entrada[(linha + dy) * largura + (coluna + dx)];
if (vizinho != 0) todos_objeto = 0;
}
}
dados_saida[indice_saida] = todos_objeto ? 0 : 255;
} else {
dados_saida[indice_saida] = 255; // Pixel de fundo permanece
}
}
}
}
}
// Função para esqueletização iterativa
void aplicar_esqueletizacao(unsigned char *dados, int largura, int altura)
{
int houve_mudanca = 1;
unsigned char *buffer_temp = malloc(largura * altura);
while (houve_mudanca) {
houve_mudanca = 0;
memcpy(buffer_temp, dados, largura * altura); // Cópia de trabalho
// Fase 1: Scan Horizontal (simplificado)
for (int linha = 1; linha < altura - 1; linha++) {
for (int coluna = 1; coluna < largura - 1; coluna++) {
if (buffer_temp[linha * largura + coluna] == 0) {
// Calcula índice na tabela de consulta baseado nos vizinhos
int idx = (buffer_temp[(linha-1)*largura + (coluna-1)] & 1)
| ((buffer_temp[(linha-1)*largura + coluna] & 1) << 1)
| ((buffer_temp[(linha-1)*largura + (coluna+1)] & 1) << 2)
| ((buffer_temp[linha*largura + (coluna-1)] & 1) << 3)
| ((buffer_temp[linha*largura + (coluna+1)] & 1) << 4)
| ((buffer_temp[(linha+1)*largura + (coluna-1)] & 1) << 5)
| ((buffer_temp[(linha+1)*largura + coluna] & 1) << 6)
| ((buffer_temp[(linha+1)*largura + (coluna+1)] & 1) << 7);
if (tabela_remocao[idx]) {
dados[linha * largura + coluna] = 255; // Remove o pixel
houve_mudanca = 1;
}
}
}
}
// Fase 2: Scan Vertical seria implementada de forma análoga
}
free(buffer_temp);
}