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.