← Voltar à Escrita

É possível recuperar uma imagem desfocada?

Inverter uma convolução em Julia: a explicação completa, equações e experiências de reconstrução.

  • julia
  • imagem
  • convolução
  • álgebra-linear
Read in English →

Publicado originalmente no Medium em 2020. Republicado aqui na íntegra, com as imagens guardadas localmente e os créditos originais.

Estava a ver um vídeo de Grant Sanderson (também conhecido como 3Blue1Brown) sobre convoluções, processamento de imagem e a linguagem de programação Julia (uma linguagem fantástica!) quando alguém fez esta pergunta.

Vê o vídeo de Grant Sanderson sobre convoluções e Julia. Recomendo-o vivamente!

Ele respondeu (corretamente) que não achava que fosse possível, porque estaríamos a perder informação. No entanto, pensei um pouco mais na questão e percebi que seria possível se a imagem de saída tivesse o mesmo tamanho que a imagem de entrada. Dessa forma, a saída teria píxeis — e informação — suficientes para recuperar a imagem original.

A convolução como filtro

Comecemos por explicar o que é uma convolução e como pode ser usada para desfocar uma imagem. Para uma explicação aprofundada, sugiro o vídeo acima; para uma versão rápida, continua por aqui. Uma convolução é uma operação matemática que, quando aplicada a imagens, pode ser entendida como um filtro.

GIF da aplicação de uma operação de convolução a uma imagem

Imagem do GitHub: vdumoulin/conv_arithmetic.

Nesta animação vemos um exemplo da convolução de uma imagem com um filtro ou kernel. A imagem original é a matriz azul, o kernel é a matriz azul-escura que desliza sobre ela e a saída é a matriz azul-esverdeada.

A convolução obtém-se multiplicando os elementos sobrepostos do kernel e da imagem e somando os produtos. As equações seguintes podem ajudar: dada uma imagem x e um kernel k, o resultado da convolução será y,

convolução de x 4 por 4 com k 3 por 3, resultando em y 2 por 2

com

Expressões para cada elemento de y como soma de produtos de x e k

Se já sabias como funcionavam as convoluções em imagens, talvez este sistema de equações não seja demasiado assustador. Se não sabias, não te preocupes: não tens de o memorizar, esse é o trabalho do programa. Uma representação útil consiste em interpretar a convolução como uma multiplicação de matrizes, que não é muito difícil de escrever a partir das equações anteriores:

Convolução de x e k na forma matricial

equivalente à equação matricial

A 4 por 16 multiplicada por x 16 por 1 é igual a y 4 por 1

Com esta representação, parece que, conhecendo A e y, poderíamos calcular x resolvendo a equação acima. Contudo, como A tem mais colunas do que linhas, o sistema é indeterminado, o que significa que não conseguimos obter uma única solução.

Manter a entrada e a saída com o mesmo tamanho

Comecei por dizer que, para ser possível inverter uma convolução, a entrada e a saída teriam de ter o mesmo tamanho. Na forma matricial, isto corresponderia a A ser quadrada — ter o mesmo número de linhas e colunas —, permitindo invertê-la e calcular x como

x é igual a A inversa multiplicada por y

Neste momento, a entrada é 4×4 e a saída é 2×2. Como podemos obter uma saída com o mesmo tamanho da entrada? Uma possibilidade é adicionar preenchimento (padding) à imagem de entrada — por exemplo, zero-padding,

convolução de x 4 por 4 com k 3 por 3 e zero-padding de 1, resultando em y 4 por 4

Desta forma, a saída seria 4×4, tal como a entrada original. Em detalhe, neste caso simples de uma convolução com padding, as dimensões da saída podem ser calculadas como

tamanho da saída igual ao tamanho da entrada mais duas vezes o padding, menos o tamanho do kernel, mais um

Se quisermos que a entrada e a saída tenham o mesmo tamanho, então o padding tem de ser

padding igual ao tamanho do kernel menos um, a dividir por dois

o que impõe uma condição importante: o tamanho do kernel tem de ser ímpar, uma vez que o padding é um valor inteiro.

Esta convolução também pode ser representada como uma multiplicação de matrizes semelhante à anterior, mas poupo-te à leitura porque as dimensões seriam muito maiores. Bastaria escrever as equações da convolução associadas a cada entrada de y e organizá-las numa multiplicação matricial como a anterior.

A 16 por 16 multiplicada por x 16 por 1 é igual a y 16 por 1

Repara que, apesar de a entrada com padding ser 6×6, correspondendo a 36 elementos, apenas 4×4 desses elementos são variáveis únicas e desconhecidas. Assim, o x da equação pode ter apenas 16×1 elementos, em vez de 36×1.

Para calcular x e inverter a convolução, basta conhecer A e y. Para construir A, é preciso conhecer o kernel usado na convolução e o tipo de padding aplicado.

Uma experiência em Julia

Como é que isto pode ser usado?

Uma imagem pode ser desfocada através de uma convolução. Por exemplo, um desfoque gaussiano é obtido convoluindo uma imagem com um kernel ou filtro que segue uma distribuição gaussiana, com o maior valor no centro e valores cuja soma é 1. Um exemplo seria o kernel

Exemplo de um kernel gaussiano 3 por 3

Fiz uma implementação simples deste problema em Julia para confirmar que é, de facto, possível inverter a convolução. Podes consultar e experimentar o código na ligação abaixo.

gonced8/reverse-convolution

Comecei por desfocar uma imagem com um desfoque gaussiano. Apliquei uma convolução à imagem original com um kernel gaussiano e usei replicate padding: os valores fora da imagem original são definidos como o valor mais próximo da margem, em vez de zero.

Imagem original à esquerda e imagem desfocada à direita

Imagem original à esquerda e imagem desfocada à direita.

Como conhecia o kernel utilizado, consegui construir a matriz A e resolver a equação em ordem a x. O resultado foi o esperado: a imagem reconstruída é exatamente igual à original.

Imagem desfocada e reconstrução usando o kernel e o padding conhecidos

Imagem desfocada à esquerda e imagem reconstruída à direita.

Podes consultar a implementação acima e confirmar que a imagem original não foi usada na reconstrução.

Quando o kernel ou o padding estão errados

Esta reconstrução a 100% só é possível porque conhecemos o kernel e o padding usados. O que acontece se usarmos um kernel que não é exatamente igual ao usado para desfocar a imagem original?

Imagem desfocada à esquerda e reconstruída à direita; a reconstrução continua desfocada e contém artefactos

Imagem desfocada à esquerda e reconstruída à direita quando não se usa o kernel exato.

E se assumirmos zero-padding quando foi usado replicate padding?

Imagem desfocada à esquerda e reconstruída à direita; a reconstrução é ruído psicadélico e não se assemelha à original

Imagem desfocada à esquerda e reconstruída à direita quando não se assume o padding exato.

Como vemos, se não conhecermos o kernel e o padding usados, não conseguimos reconstruir a imagem original. Neste sentido, o processo pode quase ser visto como um problema de encriptação: se conhecermos a “chave”, conseguimos reconstruir a mensagem original sem perdas nem ruído adicional.

Reconstruir a imagem original também é uma tarefa muito dispendiosa, porque a matriz A cresce muito depressa com o tamanho da imagem. Se a imagem original tiver 4×4 píxeis, A terá 16×16 elementos — o número de elementos cresce com N².

Espero que tenhas gostado desta breve explicação e que a tenhas achado interessante. Eu achei, e foi uma excelente forma de aprender mais sobre Julia, convoluções, processamento de imagem e álgebra linear. Se tiveres alguma pergunta ou encontrares algum erro, partilha-o comigo.

Como nota de despedida, acho que ainda não precisamos de nos preocupar com alguém inverter o desfoque das nossas fotografias.

Imagem desfocada à esquerda e reconstruída à direita; a imagem reconstruída mostra um macaco

Imagem desfocada à esquerda e reconstruída à direita.

Imagem desfocada e reconstruída de uma mulher chamada Lena

Imagem de destaque.

Nota sobre esta republicação

O texto original acima foi preservado de 2020. Uma matriz quadrada não é necessariamente invertível: a recuperação também exige um operador não singular e o condicionamento numérico é relevante. A condição de o kernel ser ímpar pressupõe padding inteiro simétrico e stride unitário. Aqui, NN representa o número de píxeis da imagem, pelo que uma matriz de convolução densa N×NN \times N armazena N2N^2 elementos. A analogia histórica com encriptação é apenas intuitiva, não uma garantia de segurança.