Há alguns anos, escrevi uma implementação muito simples de compressão de imagem fractal para o trabalho dos alunos e publiquei o código no
github .
Para minha surpresa, o repositório acabou sendo bastante popular, então decidi atualizar o código e escrever um artigo explicando ele e a teoria.
Teoria
Esta parte é bastante teórica e, se você estiver interessado apenas na documentação do código, poderá ignorá-la.
Mapeamentos de compactação
Vamos
(E,d) É o
espaço métrico completo e
f:E rightarrowE - mapeamento de
E em
E .
Dizemos que
f é um
mapeamento compressivo, se existir
0<s<1 tal que:
forallx,y inE,d(f(x),f(y)) leqsd(x,y)
Com base nisso,
f denotará um mapeamento de compactação com uma taxa de compactação
s .
Existem dois teoremas importantes nos mapeamentos de contratação:
o teorema de ponto fixo de Banach e
o teorema da colagem .
Teorema do ponto fixo :
f tem um ponto fixo exclusivo
x0 .
Mostrar provaPrimeiro, provamos que a sequência
(un) definido como
$ inline $ \ left \ {\ begin {alignat *} {2} u_0 & = x \\ u_ {n + 1} & = f (u_n) \ end {alignat *} \ right. $ inline $ é convergente para todos
x emE .
Para todos
m<n in mathbbN :
\ begin {alignat *} {2} d (u_m, u_n) & = d (f ^ m (x), f ^ n (x)) \\ & \ leq s ^ md (x, f ^ {nm} (x)) \ text {dado que f \ text {é um mapa de contração} \\ & \ leq s ^ m \ left (\ sum_ {i = 0} ^ {nm-1} {d (f ^ i (x ), f ^ {i + 1} (x))} \ right) \ text {da desigualdade do triângulo} \\ & \ leq s ^ m \ left (\ sum_ {i = 0} ^ {nm-1} {s ^ id (x, f (x))} \ right) \ text {visto que f \ text {é um mapa de contração} \\ & = s ^ m \ left (\ frac {1 - s ^ {nm}} {1 - s} d (x, f (x)) \ right) \\ & \ leq \ frac {s ^ m} {1 - s} d (x, f (x)) \ underset {m \ rightarrow \ infty} {\ rightarrow} 0 \ end {alignat *}
\ begin {alignat *} {2} d (u_m, u_n) & = d (f ^ m (x), f ^ n (x)) \\ & \ leq s ^ md (x, f ^ {nm} (x)) \ text {dado que f \ text {é um mapa de contração} \\ & \ leq s ^ m \ left (\ sum_ {i = 0} ^ {nm-1} {d (f ^ i (x ), f ^ {i + 1} (x))} \ right) \ text {da desigualdade do triângulo} \\ & \ leq s ^ m \ left (\ sum_ {i = 0} ^ {nm-1} {s ^ id (x, f (x))} \ right) \ text {visto que f \ text {é um mapa de contração} \\ & = s ^ m \ left (\ frac {1 - s ^ {nm}} {1 - s} d (x, f (x)) \ right) \\ & \ leq \ frac {s ^ m} {1 - s} d (x, f (x)) \ underset {m \ rightarrow \ infty} {\ rightarrow} 0 \ end {alignat *}
Portanto,
(un) é
uma sequência de Cauchy e
E é espaço completo, o que significa
(un) é convergente. Deixe ela ser o limite
x0 .
Além disso, como o mapa de contração é
contínuo como um mapa de Lipschitz , ele também é contínuo, ou seja,
f(un) rightarrowf(x0) . Portanto, se
n tende ao infinito em
un+1=f(un) nós temos
x0=f(x0) . Isso é
x0 é um ponto fixo
f .
Nós mostramos que
f tem um ponto fixo. Vamos mostrar, por contradição, que é único. Vamos
y0 - outro ponto fixo. Então:
d(x0,y0)=d(f(x0),f(y0)) leqsd(x0,y0)<d(x0,y0)
Houve uma contradição.
Além disso, indicaremos como
x0 ponto fixo
f .
Teorema da colagem : se
d(x,f(x))< epsilon então
d(x,x0)< frac epsilon1−s .
Mostrar provaNa prova anterior, mostramos que d(um,un) leq fracsm1−sd(x,f(x))= fracsm1−s epsilon .
Se consertarmos m em 0 então nós temos d(x,un) leq frac epsilon1−s .
Ao se esforçar n até o infinito, obtemos o resultado desejado.
O segundo teorema nos diz que se encontrarmos um mapa de contração
f tal que
f(x) perto de
x , podemos ter certeza de que o ponto fixo
f também perto de
x .
Este resultado será a base do nosso trabalho futuro. E, de fato, em vez de salvar a imagem, basta salvar a exibição compressiva, cujo ponto fixo está próximo da imagem.
Telas de compressão para imagens
Nesta parte, mostrarei como criar essas telas compressivas para que o ponto fixo fique próximo da imagem.
Primeiro, vamos definir o conjunto de imagens e a distância. Vamos escolher
E=[0,1]h vezesw .
E Há muitas matrizes com
h em linhas
w colunas e com coeficientes no intervalo
[0,1] . Então vamos levar
d(x,y)= left( sumhi=1 sumwj=1(xij−yij)2 right)0,5 .
d É a distância obtida a partir da
norma Frobenius .
Vamos
x emE É a imagem que queremos compactar.
Vamos dividir a imagem em blocos duas vezes:
- Primeiro, dividimos a imagem em blocos finitos ou com intervalos R1,...,RL . Esses blocos são separados e cobrem a imagem inteira.
- Em seguida, dividimos a imagem em blocos de fontes ou domínios D1,...,DK . Esses blocos não são necessariamente separados e não cobrem necessariamente a imagem inteira.
Por exemplo, podemos segmentar a imagem da seguinte maneira:
Então, para cada bloco de intervalo
Rl nós selecionamos um bloco de domínio
Dkl e mapeamento
fl:[0,1]Dkl rightarrow[0,1]Rl .
Em seguida, podemos definir uma função
f como:
f(x)ij=fl(xDkl)ij textif(i,j) emRl
Aprovação : se
fl estão contratando mapeamentos, então e
f mapeamento também compressivo.
Mostrar provaVamos
x,y emE e suponha que todos
fl são mapeamentos de compactação com uma taxa de compactação
sl . Então temos o seguinte:
\ begin {alignat *} {2} d (f (x), f (y)) ^ 2 & = \ sum_ {i = 1} ^ {h} {\ sum_ {j = 1} ^ {w} { (f (x) _ {ij} -f (y) _ {ij}) ^ 2}} \ text {por definição} d \\ & = \ sum_ {l = 1} ^ L {\ sum _ {(i, j) \ in R_l} {(f (x) _ {ij} -f (y) _ {ij}) ^ 2}} \ text {desde} (R_l) \ text {é uma partição} \\ & = \ sum_ {l = 1} ^ L {\ sum _ {(i, j) \ em R_l} {(f_l (x_ {D_ {k_l}}) _ {ij} -f_l (y_ {D_ {k_l}}) _ {ij }) ^ 2}} \ text {por definição} f \\ & = \ sum_ {l = 1} ^ L {d (f_l (x_ {D_ {k_l}}), f_l (y_ {D_ {k_l}}) ) ^ 2} \ text {por definição} d \\ & \ leq \ sum_ {l = 1} ^ L {s_l ^ 2d (x_ {D_ {k_l}}, y_ {D_ {k_l}}) ^ 2} \ texto {desde} (f_l) \ texto {são mapeamentos de contração} \\ & \ leq \ underset {l} {\ max} {s_l ^ 2} \ sum_ {l = 1} ^ L {d (x_ {D_ {k_l }}, y_ {D_ {k_l}}) ^ 2} \\ & = \ underset {l} {\ max} {s_l ^ 2} \ sum_ {l = 1} ^ L {\ sum _ {(i, j) \ in R_l} {(x_ {ij} -y_ {ij}) ^ 2}} \ text {por definição} d \\ & = \ underset {l} {\ max} {s_l ^ 2} \ sum_ {i = 1} ^ {h} {\ sum_ {j = 1} ^ {w} {(x_ {ij} -y_ {ij}) ^ 2}} \ text {desde} (R_l) \ texto {é uma partição} \\ & = \ underset {l} {\ max} {s_l ^ 2} d (x, y) ^ 2 \ text {por definição} d \\ \ end {alignat *}
Resta uma pergunta que precisa ser respondida: como escolher
Dkl e
fl ?
O teorema da colagem oferece uma maneira de escolhê-los: se
xRl está perto de
f(xDkl) para todos
l então
x está perto de
f(x) e pelo teorema da colagem
x e
x0 também estão próximos.
Portanto, somos independentes para todos
l podemos construir um conjunto de mapeamentos de compressão de cada
Dk em
Rl e escolha o melhor. Na próxima seção, mostramos todos os detalhes desta operação.
Implementação
Em cada seção, copiarei fragmentos de código interessantes, e o script inteiro pode ser encontrado
aqui .
Partições
Eu escolhi uma abordagem muito simples. Os blocos de origem e os blocos de folhas segmentam a imagem em uma grade, conforme mostrado na imagem acima.
O tamanho dos blocos é igual à potência de dois e isso simplifica bastante o trabalho. Os blocos de origem são 8 por 8 e os blocos finais são 4 por 4.
Existem esquemas de particionamento mais complexos. Por exemplo, podemos usar a árvore quadtree para dividir as áreas com muitos detalhes com mais força.
Conversões
Nesta seção, mostrarei como criar mapeamentos de compactação a partir de
Dk em
Rl .
Lembre-se de que queremos gerar esse mapeamento
fl para
f(xDk) estava perto de
xRl . Ou seja, quanto mais mapeamentos gerarmos, maior a probabilidade de encontrar um bom.
No entanto, a qualidade da compactação depende do número de bits necessário para armazenar
fl . Ou seja, se muitas funções forem muito grandes, a compactação será ruim. É necessário um compromisso aqui.
Eu decidi que
fl ficará assim:
fl(xDk)=s vezesgira theta(flipd(reduz(xDk)))+b
onde
reduzir - esta é uma função para passar dos blocos 8 a 8 para os blocos 4 a 4,
flip e
rodar - transformações afins,
s altera o contraste e
b - brilho.
A função
reduce
reduz o tamanho da imagem calculando a média dos arredores:
def reduce(img, factor): result = np.zeros((img.shape[0] // factor, img.shape[1] // factor)) for i in range(result.shape[0]): for j in range(result.shape[1]): result[i,j] = np.mean(img[i*factor:(i+1)*factor,j*factor:(j+1)*factor]) return result
A função de
rotate
gira a imagem em um determinado ângulo:
def rotate(img, angle): return ndimage.rotate(img, angle, reshape=False)
Para manter a forma do ângulo da imagem
theta só pode levar valores
\ {0 ^ {\ circ}, 90 ^ {\ circ}, 180 ^ {\ circ}, 270 ^ {\ circ} \} .
A função
flip
espelha a imagem se a
direction
for -1 e não reflete se o valor for 1:
def flip(img, direction): return img[::direction,:]
A conversão completa é realizada pela função
apply_transformation
:
def apply_transformation(img, direction, angle, contrast=1.0, brightness=0.0): return contrast*rotate(flip(img, direction), angle) + brightness
Precisamos de 1 bit para lembrar se o espelhamento é necessário e 2 bits para o ângulo de rotação. Além disso, se salvarmos
s e
b Usando 8 bits para cada valor, precisaremos de apenas 11 bits para salvar a conversão.
Além disso, devemos verificar se essas funções são mapeamentos de contração. A prova disso é um pouco chata, e não é realmente o que precisamos. Talvez mais tarde eu o adicione como um apêndice ao artigo.
Compressão
O algoritmo de compactação é simples. Primeiro, geramos todas as transformações afins possíveis de todos os blocos de origem usando a função
generate_all_transformed_blocks
:
def generate_all_transformed_blocks(img, source_size, destination_size, step): factor = source_size // destination_size transformed_blocks = [] for k in range((img.shape[0] - source_size) // step + 1): for l in range((img.shape[1] - source_size) // step + 1):
Em seguida, para cada bloco final, verificamos todos os blocos de origem transformados gerados anteriormente. Para cada um, otimizamos o contraste e o brilho usando o método
find_contrast_and_brightness2
e, se a conversão testada for a melhor de todas as encontradas até agora, salve-a:
def compress(img, source_size, destination_size, step): transformations = [] transformed_blocks = generate_all_transformed_blocks(img, source_size, destination_size, step) for i in range(img.shape[0] // destination_size): transformations.append([]) for j in range(img.shape[1] // destination_size): print(i, j) transformations[i].append(None) min_d = float('inf')
Para encontrar o melhor contraste e brilho, o método
find_contrast_and_brightness2
simplesmente resolve o problema dos mínimos quadrados:
def find_contrast_and_brightness2(D, S):
Desembalar
O algoritmo de descompressão é ainda mais simples. Começamos com uma imagem completamente aleatória e depois aplicamos uma imagem do aperto várias vezes
f :
def decompress(transformations, source_size, destination_size, step, nb_iter=8): factor = source_size // destination_size height = len(transformations) * destination_size width = len(transformations[0]) * destination_size iterations = [np.random.randint(0, 256, (height, width))] cur_img = np.zeros((height, width)) for i_iter in range(nb_iter): print(i_iter) for i in range(len(transformations)): for j in range(len(transformations[i])):
Esse algoritmo funciona porque o mapa de compactação possui um ponto fixo exclusivo e, independentemente da imagem de origem que escolhermos, nos esforçaremos por isso.
Acho que chegou a hora de um pequeno exemplo. Vou tentar comprimir e descompactar a imagem do macaco:
A função
test_greyscale
carrega a imagem, compacta, descompacta e mostra cada iteração da descompactação:
Nada mal para uma implementação tão simples!
Imagens RGB
Uma abordagem muito ingênua para compactar imagens RGB é compactar todos os três canais individualmente:
def compress_rgb(img, source_size, destination_size, step): img_r, img_g, img_b = extract_rgb(img) return [compress(img_r, source_size, destination_size, step), \ compress(img_g, source_size, destination_size, step), \ compress(img_b, source_size, destination_size, step)]
E para descompactar, basta descompactar os dados de três canais separadamente e coletá-los em três canais de imagem:
def decompress_rgb(transformations, source_size, destination_size, step, nb_iter=8): img_r = decompress(transformations[0], source_size, destination_size, step, nb_iter)[-1] img_g = decompress(transformations[1], source_size, destination_size, step, nb_iter)[-1] img_b = decompress(transformations[2], source_size, destination_size, step, nb_iter)[-1] return assemble_rbg(img_r, img_g, img_b)
Outra solução muito simples é usar a mesma tela de compactação para todos os três canais, porque eles geralmente são muito semelhantes.
Se você quiser verificar como isso funciona, execute a função
test_rgb
:
Artefatos apareceram. Esse método provavelmente é ingênuo demais para produzir bons resultados.
Para onde ir a seguir
Se você quiser saber mais sobre a compactação de imagem fractal, recomendo que você leia o artigo
Técnicas de compactação de imagem Fractal e Wavelet de Stephen Welsted. É fácil de ler e explica técnicas mais sofisticadas.