Como criei um filtro que não corrompe a imagem mesmo depois de um milhão de execuções - parte 2

imagem

imagem

Na primeira parte deste post, falei sobre como o uso repetido de filtros halfpel padrão cria imagens distorcidas e, em seguida, mostrei um novo filtro que não possui esse problema.

Estava um pouco mais embaçado e isso não agradaria a todos. No entanto, era melhor do que suas alternativas - na verdade, esse filtro foi usado na versão original do Bink 2 . Devido à carga de trabalho constante, nunca consegui voltar a ele e examiná-lo com mais detalhes.

Mas agora que encontrei tempo para retornar a esse filtro e escrever um artigo sobre o assunto, devo finalmente fazer uma pergunta: existe um filtro menos desfocado que ainda retenha a propriedade de "estabilidade infinita"?

Aviso de spoiler: a resposta correta é "provavelmente não" e "definitivamente existe". Mas antes de entendermos por que existem duas respostas para essa pergunta e o que elas significam, vamos preparar melhor uma bancada de testes.

Ajuste de deslocamento


Quando trabalhei inicialmente nesse problema, não fazia ideia do que estava procurando. Eu nem sabia que existia um filtro de meio-tom "infinitamente estável", por isso não criei um sistema em sua pesquisa. Eu estava apenas procurando por algo que resistisse às “muitas” iterações de filtro sem distorção da imagem. Todas as imagens da primeira parte refletem essa metodologia: a imagem é deslocada da direita para a esquerda meio pixel por vez, ou seja, se você aplicar o filtro 100 vezes, a imagem resultante será deslocada em 50 pixels.

Agora que sabemos o que realmente estamos procurando, podemos ser um pouco mais precisos. Aplicando o filtro halfpel duas vezes, mudamos a imagem exatamente para um pixel. Ou seja, se simplesmente movermos a imagem um pixel para trás , ela permanecerá no mesmo espaço. Graças a isso, o teste ficará muito mais bonito, porque não apenas poderemos aplicar o filtro várias vezes, sem medo de que a imagem "rasteje" para fora da tela, mas também encontraremos a diferença da imagem nas versões anteriores e com a original.

Isso nos permitirá testar os filtros automaticamente. Simplesmente aplicamos o filtro várias vezes e vemos uma de duas coisas: convergência para uma imagem inalterada, indicando que o filtro é infinitamente estável ou um desvio significativamente grande da imagem original, indicando que o filtro está "quebrado". Para esses testes, escolhi o erro médio por canal 64 (de 255) ou o erro máximo em qualquer um dos canais até o total 255 como "significativamente grande". Se alguma dessas condições for verdadeira, assumiremos que o filtro "quebrou" "

Testando novamente os filtros da primeira parte


Então, agora entendemos melhor como testar esses filtros, então vamos dar uma nova olhada nos filtros da primeira parte. Vamos começar com um bilinear, o que, é claro, não é muito interessante:


Esta é uma imagem após 244 iterações. Como você pode esperar, a imagem "quebra" gradualmente devido à média constante de pixels. Mas mesmo ele atinge gradualmente o limite do erro médio.

Aqui está o h.264:


Para quebrar a imagem, 78 iterações são suficientes para ele. O filtro HEVC com 8 amostras se comporta um pouco melhor, mas ainda quebra após 150 iterações:


Lanczos com 6 quebras de amostra após 166 iterações:


São todos os nossos filtros quebrados. Tudo o que resta é o meu filtro inteiro:


Como esperado, ele não foi o único a quebrar. Ele converge para uma imagem infinitamente estável após 208 iterações.

O que sabemos é bastante notável aqui: pelo menos para uma ampla variedade de imagens de teste, este filtro é infinitamente estável , ou seja, nunca criará um artefato, não importa quantas vezes seja usado.

Isso nos leva de volta à pergunta original: ele é realmente o melhor? E você já sabe as respostas, porque no começo do artigo eu também escrevi: "provavelmente não" e "definitivamente, sim".

Vamos primeiro olhar para a parte “provavelmente não” primeiro.

Filtros de número inteiro


Então, na primeira parte do post, mencionei que o núcleo do filtro que encontrei era "o melhor dos detectados", e essa é sua peculiaridade. E aqui está o recurso:

Quando eu estava procurando esse filtro, na verdade não estava procurando o melhor filtro. Eu estava procurando o melhor filtro que pode ser expresso com um número muito pequeno de turnos inteiros, adições e subtrações . Pode parecer estranho, mas não se apresse.

Você deve ter notado que, quando mostrei os coeficientes de h.264, HEVC e o filtro bilinear, assim como meu filtro, eu os escrevi como numeradores inteiros sobre denominadores inteiros, assim:

MyKernel[] = {1.0/32.0, -4.0/32.0, 19.0/32.0, 19.0/32.0, -4.0/32.0, 1.0/32.0}; 

Mas no caso do windowed sinc, fiz diferente e escrevi assim:

 LanczosKernel[] = {0.02446, -0.13587, 0.61141, 0.61141, -0.13587, 0.02446}; 

A razão para isso é que o sinc em janela é realmente deduzido de uma função matemática contínua que não tem nada a ver com frações inteiras comuns. Ao usar esse filtro, são usados ​​números de ponto flutuante (com a maior precisão possível) que correspondem aos valores da função sinc. Se você se esforçar para aplicá-las com precisão, não deverá arredondá-las para frações comuns, pois isso adicionará um erro.

Os codecs de vídeo tradicionalmente não podem se dar ao luxo de executar essas operações. Operações de ponto flutuante em tarefas “pesadas” como compensação de movimento são simplesmente impossíveis de usar em equipamentos especializados ou de baixa potência. Isso é especialmente verdadeiro se estivermos falando de codecs padrão do setor que devem ser executados em uma ampla gama de dispositivos, incluindo chips embarcados de baixo custo e baixo custo.

Além disso, mesmo se você executá-los na CPU, os conjuntos de instruções modernos são baseados no SIMD, ou seja, operações inteiras na CPU ainda podem ser executadas mais rapidamente: você pode ajustar dois números inteiros de 16 bits no espaço de um flutuador de 32 bits, basicamente duplicando o desempenho das operações, portanto, se considerarmos o número exato de ciclos por operação, um ponto flutuante nem sempre é a opção mais rápida.

Agora você vê por que esse recurso era importante. Como eu precisava apenas de operações inteiras simples de 16 bits, procurei os kernels que podem ser expressos como pequenos números inteiros sobre divisores na potência de dois a 64 e não mais. Este é um conjunto de filtros muito mais limitado do que se eu estivesse considerando um conjunto de 6 coeficientes de ponto flutuante.

Da mesma forma, por razões de eficiência, não considerei nenhum outro número de amostras. A única opção era 6 ou menos, então nem testei versões com 8 ou 10 amostras.

Assim, chegamos à primeira resposta: "provavelmente não". Se aderirmos a essas restrições, provavelmente não encontraremos um filtro melhor que possa ser aplicado um número infinito de vezes sem degradação. O núcleo do filtro da primeira parte é provavelmente o melhor que podemos encontrar, embora deva admitir que não posso provar exaustivamente.

Mas e se não precisarmos aderir a essas restrições?

Versão de ponto flutuante


Se nos livrarmos das limitações específicas da versão original do Bink 2 (que agora está bastante desatualizada - muitas revisões foram lançadas) e usarmos coeficientes arbitrários de ponto flutuante, como podemos melhorar os resultados?

Bem, como sabemos que meu núcleo inteiro nunca se degrada e sabemos que Lanczos é mais nítido, mas se degrada, é lógico que podemos encontrar um lugar entre os dois conjuntos de coeficientes onde a degradação começa. Então, eu escrevi um programa que me ajudou a encontrar esse ponto em particular, e aqui está o kernel que encontrei:

 MyFloatKernel6[] = {0.027617, -0.130815, 0.603198, 0.603198, -0.130815, 0.027617}; 

Este kernel requer 272 iterações para convergir, mas é infinitamente estável e parece muito mais nítido que meu filtro inteiro:


De fato, é quase indistinguível do original:


Quase ... mas não exatamente. Se você observar atentamente, ainda poderá ver embaçamento e atenuação em áreas de alto contraste. A maneira mais fácil de ver isso é nos olhos de um "dinossauro" laranja e em áreas de muita luz atrás do bambu.

Ou seja, um filtro de ponto flutuante de 6 amostras é definitivamente melhor, mas não é perfeito. Ainda pode ser melhorado?

Aumentar a largura do filtro


Inicialmente, um filtro com 6 amostras foi selecionado pelos mesmos motivos que as frações com números inteiros pequenos: eu estava procurando por um filtro extremamente eficiente. Mas agora estamos pesquisando e já passamos para números de ponto flutuante. Por que não considerar um filtro mais amplo?

Combinando nosso filtro inteiro de 6 amostras com o Lanczos de 6 amostras, obtivemos um filtro muito bom. Por que não combinamos com o Lanczos de 8 amostras?

O Lanczos de 8 amostras é assim:

 Lanczos8[] = {-0.01263, 0.05976, -0.16601, 0.61888, 0.61888, -0.16601, 0.05976, -0.01263}; 

Como o Lanczos de 6 amostras, é muito instável e entra em colapso após 178 iterações:


Se procurarmos um filtro melhor entre um filtro inteiro de 6 amostras e um Lanczos de 8 amostras, encontraremos este notável filtro de 8 amostras:

 MyFloatKernel8[] = {-0.010547, 0.052344, -0.156641, 0.614844, 0.614844, -0.156641, 0.052344, -0.010547}; 

Como um filtro infinitamente estável, ele tem um desempenho incrível. Ele converge após 202 iterações (a convergência é mais rápida que meus dois filtros) e é tão parecido com o original que é difícil distinguir qual deles é qual:


Aqui está o original para referência novamente:


Comparado ao meu filtro inteiro original, há uma melhoria significativa.

Como funcionam os filtros infinitamente estáveis?


Eu estava indo para terminar este post algo como isto:

"Não sei exatamente como tudo funciona. Em outras áreas em que trabalhei com as transformações infinitamente aplicáveis, sei como a matemática de fronteira é realizada e a análise útil é criada. Em primeiro lugar, trata-se da análise da superfície limite para superfícies de subdivisão, onde são calculados os autovalores e autovetores da matriz de subdivisão, após os quais é possível levar com precisão o limite em um grau infinito. Mas não tenho experiência em realizar essa análise para filtros halfpel, porque eles não deixam os pixels no lugar, mas os deslocam para o lado ".

Esse era o meu plano. Porém, entre a redação da primeira e da segunda partes, enviei os resultados do filtro aprimorado para Fabien Giessen e Charles Bloom . Não é de surpreender que eles soubessem a matemática necessária para o estudo analítico desse problema. Descobriu-se que, para os filtros, existe realmente uma análise de autovalores e vetores, mas não é bem assim.

Mas pode ser facilmente executado - na verdade, é incorporado aos programas CAM como um processo trivial de uma etapa e podemos realmente observar os autovalores de filtros. Ele não nos fornece respostas completas, porque aqui o fato de arredondar (ou truncar) para 8 bits (ou 10 bits ou 12 bits) após cada filtragem é importante, porque o truncamento afeta o método de acumular erros em comparação com a álgebra infinitamente precisa.

Infelizmente, como essa não é minha área de especialização, não posso nem obter uma visão geral detalhada dessa análise. Perguntei a Fabien e Charles se eles poderiam escrever posts com as boas informações que me enviaram pelo correio (ambos têm blogs técnicos - o ryg blog e o cbloom rants ), e Fabien escreveu uma excelente série de artigos sobre os fundamentos matemáticos dos filtros estáveis . Se você está interessado na estrutura teórica do que está acontecendo nos meus dois posts, recomendo a leitura desta série!

Source: https://habr.com/ru/post/pt468629/


All Articles