Detecção de Bordas e Transformações Morfológicas em Imagens com OpenCV

Extraindo algumas features clássicas de uma imagem

Luísa Mendes Heise
Turing Talks

--

Texto escrito por Luísa Mendes Heise e Guilherme Salustiano

No Turing Talks de hoje nós vamos falar sobre um assunto bem famoso na área de visão computacional. O assunto é detecção de bordas e transformações morfológicas em imagens. Para que você, caro leitor, consiga acompanhar esse texto sem maiores dificuldades, precisamos que conheça o conceito de convolução em imagens e também um pouco de python. Caso sinta que não cumpre esses requisitos, relaxe, temos textos sobre esses temas aqui no Turing Talks.

Detecção de Bordas

Detectar bordas é uma tarefa, muitas vezes nada trivial, mas é algo útil, porque nos ajuda a segmentar objetos de uma forma muito direta em algumas situações.

Em desenhos animados, as bordas nos ajudam a facilmente segmentar os personagens e objetos

O que é uma borda?

Intuitivamente, podemos dizer que uma borda é um “lugar geométrico” que delimita um objeto qualquer. Infelizmente, essa definição é pouco precisa e pouco útil na maioria dos problemas. E por quê?

Bom, porque nós queremos usar a lógica inversa: encontrar as bordas e, só depois, delimitar o objeto, ou mesmo que não queiramos delimitar o objeto, ter de fazer isso de antemão só para encontrar as bordas não seria eficiente. Então precisamos encontrar outra forma de definir uma borda para o computador.

E como é essa outra forma?

Resumidamente, essa outra forma é encontrar os pontos em que a intensidade luminosa muda de forma abrupta.

Por exemplo, nessa foto, temos um pedaço preto que, de repente, fica branco. É justamente essa mudança na cor que nos faz identificar uma borda.

Nesse caso, nós temos de novo a mudança de cores do preto para o branco. Entretanto não identificamos borda porque essa mudança não é “abrupta”.

Intuitivamente, já deve ter ficado claro que a borda tem a ver com essa mudança “rápida”. Mas para definirmos alguma forma de o computador aplicar isso, precisamos ser um pouco mais precisos do que isso.

Se você já estudou cálculo na vida, a palavra “mudança rápida” deve ter acendido uma luzinha na sua cabeça. Exato, vamos falar de gradiente.

O gradiente é um vetor (você pode imaginar como uma setinha) que aponta para a direção que uma função tem o maior aumento.

Se em um ponto esse gradiente (mais precisamente o módulo dele) é muito grande quer dizer que ali há uma taxa de variação grande no valor da função.

Tá, mas nós não estamos falando de uma função aqui: é uma imagem. Como você vai utilizar a noção de gradiente em uma imagem?

O que fazemos é utilizar alguns tipos específicos de kernel em uma convolução, esses kernels medem essa variações, fazendo aproximações numéricas para os gradientes.

Operador de Sobel

O operador de Sobel calcula separadamente os gradientes nos eixos x e y, por meio das seguintes matrizes 3x3 e ele faz isso usando simples convoluções das seguintes matrizes:

A ideia aqui é fazer a diferença entre os pixels da direita e da esquerda, no caso do gradiente horizontal, assim detectar mudanças abruptas. Fica mais fácil entender com um exemplo, vamos conferir abaixo:

Como você pode observar, ao encontrar uma diferença entre os pixels um lado acaba se sobressaindo ao outro gerando um maior valor na nossa convolução. Também é interessante notar que ele se dá muito bem com bordas horizontais mas não consegue diferenciar bem as verticais, por isso precisamos das duas matrizes.

bordas observadas pelos diferentes eixos

Combinado então esses dois resultados, por meio da distância euclidiana simples temos o seguinte resultado

É interessante notar também que por termos a intensidade de cada eixo podemos plotar também o ângulo da borda usando o arco tangente.

Aqui a cor representa o ângulo do vetor gradiente. Créditos: Computerphile

Sendo as fórmulas principais:

Com o OpenCV podemos facilmente aplicar esse filtro:

Detector de borda de Canny

Os operadores de Sobel já ajudam e muito a indicar bordas, mas nem só de bordas vive um limitador de corpos. No caso dos cartoons apresentados a borda delimitam exatamente a divisão entre o rosto, o fundo e a camiseta. Em casos mais complexos, como fotos de máquinas ou pessoas, os operadores podem marcam qualquer mudança, como rugas no ferro ou pequenos amassados na camisa como bordas, que não são efetivas para separar os componentes da imagem.

Imagem cheia de rugas e imperfeições que dificultam achar as bordas

Para isso foi criado o algoritmo de canny, que pode ser simplificado em 4 passos:

  1. Aplicar um filtro gaussiano para reduzir o ruído da imagem
  2. Achar os gradientes de intensidade da imagem
  3. Reduzir a largura das bordas
  4. Aplicar histereses e remover todas que não estão ligadas a bordas fortes

Parece bastante coisa mas vamos detalhar e entender mais um pouquinho de cada um desses passos.

Passo 1: Filtro gaussiano

Lembra do ruído? Pequenos amassados da camisa que são detectados como bordas? A ideia de passar um filtro gaussiano antes é “esparramar” esse ruído ao redor diminuindo assim a sua presença. No caso de bordas fortes esse filtro não será suficiente para o fazer desaparecer.

Passo 2: Achar os gradientes de intensidade

Como já foi detalhado acima, costumamos usar o operador de sobel para calcular o gradiente.

Passo 3: Reduzir a largura de borda

Como forma de simplificar o resultado final costumamos reduzir a borda pra minima possivel. Fazemos isso começando de uma borda e a partir do seu sentido, retirado dos gradientes pelo arctg, procuramos traçar seus paralelos.

Crédito: Computerphile

A partir daí teremos algo como uma distribuição normal da intensidade das bordas, então pegamos o meio disso e deixamos como a borda principal (que resume todas as outras)

Passo 4: Histerese, tolerância é a chave

Aqui nós vamos decidir o que é realmente borda e o que é ruído. Uma primeira abordagem é definir um limite, acima dele tudo é uma borda verdadeira e abaixo dele apagamos tudo. O problema é que fica difícil definir um limite aceitável para todos.

Partimos então para a abordagem realmente utilizada, definimos dois limites, um superior onde certamente a borda é verdadeira, um inferior onde certamente é falsa, e o “talvez”, a região entre esses dois limites que apenas é considerada como borda se estiver ligada a uma borda certamente verdadeira.

Créditos: Computerphille

Juntando tudo isso temos nosso algoritmo.

Você não precisa se preocupar em como vai fazer tudo isso, o OpenCV já tem um método pronto para você.

Operações Morfológicas

Operações morfológicas são uma classe de operações não lineares que nos permitem remover ruído de uma imagem e extrair a forma/ estrutura da imagem. No geral, essas transformações são aplicadas em imagens em escala de cinza ou binárias. É muito comum aplicá-las após a extração de bordas.

Na essência o que vamos fazer é uma operação lógica com os nossos pixels. Baseadas num chamado “elemento estruturante”, que vai nos dizer o que considerar nessa operação lógica.

Essas operações lógicas, para o escopo desse texto, são E e OU. Também, para o escopo deste texto, vamos considerar imagens binárias, que só possuem valores de 0 e/ou 255.

Elemento estruturante

O elemento estruturante será nossa máscara para aplicação das operações lógicas. Esse elemento, portanto, é uma matriz, com valores (no nosso exemplo 0 ou 255). Os valores em 255(no nosso exemplo inicial) sendo os que vamos considerar para a operação lógica. Ele tem uma posição central, chamada origem, na qual vamos fazer ou não uma mudança a depender do resultado da nossa operação lógica.

Exemplo de elemento estruturante

De forma mais clara, podemos exemplificar uma operação OU com elemento estruturante:

  1. Percorra a imagem com o elemento estruturante, sobrepondo os valores dele com os da imagem original
  2. Caso algum dos nossos 255 se sobreponha a outro 255, dizemos que a operação OU é verdadeira, caso nenhum valor bata, a operação é falsa.
  3. Se a operação lógica for verdadeira, mude o valor do pixel sobreposto com a posição central (origem) para o valor 255.

O elemento estruturante pode possuir diferentes formas, a depender do efeito desejado na imagem.

Caso você ainda não tenha entendido, fique tranquilo, caro leitor, com as explicações de dilatação e erosão, as coisas devem ficar mais fáceis.

Dilatação

A dilatação é uma operação morfológica que utiliza o operador OU no elemento estruturante. Vamos fazer um exemplo:

Vamos supor que temos essa imagem e esse elemento estruturante (em formato de cruz).

Vamos começar a nossa operação no canto superior esquerdo.

Podemos verificar que na primeira sobreposição, nenhum dos valores da imagem (todos 0 são diferentes de 255) bateu com os valores do elemento estruturante. Dessa forma, obtemos um falso e não fazemos nada.

Andando uma posição para o lado, vemos que o valor de baixo da cruz do elemento estruturante se sobrepõe a um 255, logo a operação de OU é verdadeira, porque pelo menos uma das sobreposições foi satisfeita. Assim, mudamos o valor do pixel sobreposto à origem para 255.

Seguindo mais alguns passos nessa operação:

Até que, por fim, teríamos o seguinte resultado:

Como podemos ver, a parte branca da imagem “dilatou”. No openCV, dilatando os valores brancos com um elemento estruturante retangular, temos o seguinte:

Erosão

Como vimos, a dilatação pode aumentar bordas e preencher buracos. A erosão faz o oposto disso, ela remove detalhes finos.

Para fazer isso, nós vamos utilizar a seguinte regra: a cor do pixel central (sobreposto à origem do elemento estruturante) só poderá ter a cor igual à da origem se todos os pixels analisados forem um match com o elemento estruturante.

Vamos partir da nossa imagem dilatada e utilizando o mesmo elemento estruturante, vamos ver o que aconteceria.

Nessa primeira sobreposição, vemos que não são todos os pixels que dão match com o elemento estruturante. Assim, o elemento central não pode ser 255. Como ele já é 0, não fazemos nada.

Nesse caso, novamente, os valores não batem integralmente. Como o pixel sobreposto à origem era de 255, mudamos seu valor para 0.

Mais alguns passos e temos:

Por fim, obtemos o seguinte resultado:

No OpenCV, podemos aplicar uma erosão com um elemento estruturante retangular da seguinte forma:

Opening

Opening é um nome dado ao processo de erosão seguido de dilatação (utilizando o mesmo elemento estruturante). É útil na remoção de ruído da imagem.

Closing

O Closing é o contrário do opening, ou seja, é uma dilatação seguida por erosão. Ele pode ser utilizado para fechar pequenos buracos dentro dos objetos em primeiro plano ou pequenos pontos pretos na imagem.

Gradiente Morfológico

Essa é a diferença entre a dilatação e a erosão de uma imagem. A dilatação “engorda” as bordas, enquanto que a erosão “apaga” elas, assim a diferença dos dois serve como uma forma de aproximar o gradiente numericamente, tal como faziam os kernels visto na primeira parte deste texto.

Mais alguma coisa sobre operações morfológicas…

Além das apresentadas aqui, existem outras operações morfológicas. O OpenCV também nos permite criar elementos estruturantes customizados, como em formato de elipse.

Conclusão

Parabéns por ter chegado até aqui. Esses conceitos podem ser difíceis de digerir, mas são importantes para muitas aplicações. Queremos deixar linkado aqui algumas referências muito úteis:

Se você gostou do texto e acha que te ajudou, deixe os claps, siga o Turing Talks no Medium e siga as páginas do Grupo Turing no Facebook e no Instagram, até a próxima!

--

--