Il y a quelques années, j'ai écrit une implémentation très simple de la compression d'image fractale pour le travail des étudiants et publié le code sur
github .
À ma grande surprise, le référentiel s'est avéré être très populaire, j'ai donc décidé de mettre à jour le code et d'écrire un article l'expliquant ainsi que la théorie.
Théorie
Cette partie est assez théorique, et si vous n'êtes intéressé que par la documentation du code, vous pouvez la sauter.
Mappages de compression
Soit
(E,d) Est l'
espace métrique complet , et
f:E rightarrowE - cartographie à partir de
E sur
E .
Nous disons que
f est un
mappage compressif s'il existe

de telle sorte que:
forallx,y inE,d(f(x),f(y)) leqsd(x,y)
Sur cette base,
f dénotera un mappage de compression avec un taux de compression
s .
Il existe deux théorèmes importants sur les contrats de cartographie:
le théorème du point fixe de Banach et
le théorème du collage .
Théorème du point fixe :
f a un point fixe unique
x0 .
Montrer la preuveNous prouvons d'abord que la séquence
(un) définir comme
$ inline $ \ left \ {\ begin {alignat *} {2} u_0 & = x \\ u_ {n + 1} & = f (u_n) \ end {alignat *} \ right. $ inline $ est convergent pour tous
x inE .
Pour tous
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 {puisque} f \ text {est une carte de contraction} \\ & \ leq s ^ m \ left (\ sum_ {i = 0} ^ {nm-1} {d (f ^ i (x ), f ^ {i + 1} (x))} \ right) \ text {de l'inégalité du triangle} \\ & \ leq s ^ m \ left (\ sum_ {i = 0} ^ {nm-1} {s ^ id (x, f (x))} \ right) \ text {puisque} f \ text {est une carte de contraction} \\ & = 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 {puisque} f \ text {est une carte de contraction} \\ & \ leq s ^ m \ left (\ sum_ {i = 0} ^ {nm-1} {d (f ^ i (x ), f ^ {i + 1} (x))} \ right) \ text {de l'inégalité du triangle} \\ & \ leq s ^ m \ left (\ sum_ {i = 0} ^ {nm-1} {s ^ id (x, f (x))} \ right) \ text {puisque} f \ text {est une carte de contraction} \\ & = 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 *}
Par conséquent
(un) est
une séquence de Cauchy , et
E est plein d'espace, ce qui signifie
(un) est convergent. Laissez-la être la limite
x0 .
De plus, comme la carte de contraction est
continue comme une carte de Lipschitz , elle est également continue, c'est-à-dire
f(un) rightarrowf(x0) . Par conséquent, si
n tend à l'infini dans
un+1=f(un) nous obtenons
x0=f(x0) . C’est
x0 est un point fixe
f .
Nous avons montré que
f a un point fixe. Montrons par contradiction qu'il est unique. Soit
y0 - un autre point fixe. Ensuite:
d(x0,y0)=d(f(x0),f(y0)) leqsd(x0,y0)<d(x0,y0)
Il y avait une contradiction.
De plus, nous désignerons comme
x0 point fixe
f .
Théorème du collage : si
d(x,f(x))< epsilon alors
d(x,x0)< frac epsilon1−s .
Montrer la preuveDans la preuve précédente, nous avons montré que d(um,un) leq fracsm1−sd(x,f(x))= fracsm1−s epsilon .
Si nous réparons m dans 0 alors nous obtenons d(x,un) leq frac epsilon1−s .
Lorsque vous vous efforcez n à l'infini on obtient le résultat souhaité.
Le deuxième théorème nous dit que si nous trouvons une carte de contraction
f tel que
f(x) près de
x , alors nous pouvons être sûrs que le point fixe
f aussi proche de
x .
Ce résultat sera le fondement de nos travaux futurs. Et en fait, au lieu de sauvegarder l'image, il nous suffit de sauvegarder l'affichage compressif, dont le point fixe est proche de l'image.
Écrans de compression pour les images
Dans cette partie, je vais montrer comment créer de tels affichages compressifs afin que le point fixe soit proche de l'image.
Tout d'abord, définissons l'ensemble d'images et la distance. Nous choisirons
E=[0,1]h foisw .
E Est-ce que beaucoup de matrices avec
h en rangées
w colonnes et avec des coefficients dans l'intervalle
[0,1] . Ensuite, nous prendrons
d(x,y)= left( sumhi=1 sumwj=1(xij−yij)2 droite)0,5 .
d Est la distance obtenue à partir de la
norme Frobenius .
Soit
x inE Est-ce l'image que nous voulons compresser.
Nous allons diviser l'image en blocs deux fois:
- Tout d'abord, nous divisons l'image en blocs finis ou à intervalles R1,...,RL . Ces blocs sont séparés et couvrent toute l'image.
- Ensuite, nous divisons l'image en blocs de sources ou de domaines D1,...,DK . Ces blocs ne sont pas nécessairement séparés et ne couvrent pas nécessairement l'image entière.
Par exemple, nous pouvons segmenter l'image comme suit:
Puis pour chaque bloc d'intervalle
Rl nous sélectionnons un bloc de domaine
Dkl et cartographie
fl:[0,1]Dkl rightarrow[0,1]Rl .
Ensuite, nous pouvons définir une fonction
f comme:
f(x)ij=fl(xDkl)ij textif(i,j) inRl
Approbation : si
fl contractent des mappages, puis et
f également cartographie compressive.
Montrer la preuveSoit
x,y inE et supposons que tous
fl sont des mappages de compression avec un taux de compression
sl . Ensuite, nous obtenons ce qui suit:
\ 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 {par définition} d \\ & = \ sum_ {l = 1} ^ L {\ sum _ {(i, j) \ dans R_l} {(f (x) _ {ij} -f (y) _ {ij}) ^ 2}} \ text {puisque} (R_l) \ text {est une partition} \\ & = \ sum_ {l = 1} ^ L {\ sum _ {(i, j) \ in R_l} {(f_l (x_ {D_ {k_l}}) _ {ij} -f_l (y_ {D_ {k_l}}) _ {ij }) ^ 2}} \ text {par définition} f \\ & = \ sum_ {l = 1} ^ L {d (f_l (x_ {D_ {k_l}}), f_l (y_ {D_ {k_l}}) ) ^ 2} \ text {par définition} d \\ & \ leq \ sum_ {l = 1} ^ L {s_l ^ 2d (x_ {D_ {k_l}}, y_ {D_ {k_l}}) ^ 2} \ text {since} (f_l) \ text {sont des mappages de contraction} \\ & \ 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 {par définition} d \\ & = \ underset {l} {\ max} {s_l ^ 2} \ sum_ {i = 1} ^ {h} {\ sum_ {j = 1} ^ {w} {(x_ {ij} -y_ {ij}) ^ 2}} \ text {since} (R_l) \ text {est une partition} \\ & = \ underset {l} {\ max} {s_l ^ 2} d (x, y) ^ 2 \ text {par définition} d \\ \ end {alignat *}
\ 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 {par définition} d \\ & = \ sum_ {l = 1} ^ L {\ sum _ {(i, j) \ dans R_l} {(f (x) _ {ij} -f (y) _ {ij}) ^ 2}} \ text {puisque} (R_l) \ text {est une partition} \\ & = \ sum_ {l = 1} ^ L {\ sum _ {(i, j) \ in R_l} {(f_l (x_ {D_ {k_l}}) _ {ij} -f_l (y_ {D_ {k_l}}) _ {ij }) ^ 2}} \ text {par définition} f \\ & = \ sum_ {l = 1} ^ L {d (f_l (x_ {D_ {k_l}}), f_l (y_ {D_ {k_l}}) ) ^ 2} \ text {par définition} d \\ & \ leq \ sum_ {l = 1} ^ L {s_l ^ 2d (x_ {D_ {k_l}}, y_ {D_ {k_l}}) ^ 2} \ text {since} (f_l) \ text {sont des mappages de contraction} \\ & \ 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 {par définition} d \\ & = \ underset {l} {\ max} {s_l ^ 2} \ sum_ {i = 1} ^ {h} {\ sum_ {j = 1} ^ {w} {(x_ {ij} -y_ {ij}) ^ 2}} \ text {since} (R_l) \ text {est une partition} \\ & = \ underset {l} {\ max} {s_l ^ 2} d (x, y) ^ 2 \ text {par définition} d \\ \ end {alignat *}
Il reste une question à laquelle il faut répondre: comment choisir
Dkl et
fl ?
Le théorème du collage offre un moyen de les choisir: si
xRl est proche de
f(xDkl) pour tous
l alors
x est proche de
f(x) et par le théorème du collage
x et
x0 sont également proches.
Nous sommes donc indépendants pour tout le monde
l nous pouvons construire un ensemble de mappages de compression de chaque
Dk sur
Rl et choisissez le meilleur. Dans la section suivante, nous montrons tous les détails de cette opération.
Implémentation
Dans chaque section, je vais copier des fragments de code intéressants, et le script complet peut être trouvé
ici .
Cloisons
J'ai choisi une approche très simple. Les blocs source et les blocs feuilles segmentent l'image sur une grille, comme illustré dans l'image ci-dessus.
La taille des blocs est égale aux puissances de deux, ce qui simplifie considérablement le travail. Les blocs source sont 8 par 8 et les blocs de fin sont 4 par 4.
Il existe des schémas de partitionnement plus complexes. Par exemple, nous pouvons utiliser l'arbre à quatre arbres pour diviser plus fortement les zones avec beaucoup de détails.
Conversions
Dans cette section, je vais montrer comment créer des mappages de compression à partir de
Dk sur
Rl .
N'oubliez pas que nous voulons générer une telle cartographie
fl à
f(xDk) était proche de
xRl . Autrement dit, plus nous générons de mappages, plus il est probable qu'il en trouve un bon.
Cependant, la qualité de la compression dépend du nombre de bits requis pour stocker
fl . Autrement dit, si de nombreuses fonctions sont trop grandes, la compression sera mauvaise. Un compromis est nécessaire ici.
J'ai décidé que
fl ressemblera à ceci:
fl(xDk)=s foistourner theta(flipd(réduire(xDk))))+b
où
réduire - il s'agit d'une fonction permettant de passer des blocs 8 à 8 aux blocs 4 à 4,
flip et
fairepivoter - transformations affines,
s change le contraste, et
b - luminosité.
La fonction de
reduce
réduit la taille de l'image en faisant la moyenne de l'environnement:
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
La fonction de rotation fait pivoter l'image d'un angle donné:
def rotate(img, angle): return ndimage.rotate(img, angle, reshape=False)
Pour conserver la forme de l'angle de l'image
theta ne peut prendre que des valeurs
\ {0 ^ {\ circ}, 90 ^ {\ circ}, 180 ^ {\ circ}, 270 ^ {\ circ} \}\ {0 ^ {\ circ}, 90 ^ {\ circ}, 180 ^ {\ circ}, 270 ^ {\ circ} \} .
La fonction d'
flip
reflète l'image si la
direction
est -1 et ne reflète pas si la valeur est 1:
def flip(img, direction): return img[::direction,:]
La conversion complète est effectuée par la fonction
apply_transformation
:
def apply_transformation(img, direction, angle, contrast=1.0, brightness=0.0): return contrast*rotate(flip(img, direction), angle) + brightness
Nous avons besoin d'un bit pour se souvenir si la mise en miroir est nécessaire et de 2 bits pour l'angle de rotation. De plus, si nous économisons
s et
b En utilisant 8 bits pour chaque valeur, nous n'aurons besoin que de 11 bits pour enregistrer la conversion.
De plus, nous devons vérifier si ces fonctions sont des mappages de contraction. La preuve en est un peu ennuyeuse, et pas vraiment ce dont nous avons besoin. Peut-être que je l'ajouterai plus tard en annexe à l'article.
La compression
L'algorithme de compression est simple. Tout d'abord, nous générons toutes les transformations affines possibles de tous les blocs source en utilisant la fonction
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):
Ensuite, pour chaque bloc final, nous vérifions tous les blocs source transformés générés précédemment. Pour chacun, nous optimisons le contraste et la luminosité à l'aide de la méthode
find_contrast_and_brightness2
, et si la conversion testée est la meilleure trouvée jusqu'à présent, alors enregistrez-la:
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')
Pour trouver le meilleur contraste et luminosité, la méthode
find_contrast_and_brightness2
résout simplement le problème des moindres carrés:
def find_contrast_and_brightness2(D, S):
Déballage
L'algorithme de décompression est encore plus simple. Nous commençons avec une image complètement aléatoire, puis appliquons une image compressée plusieurs fois
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])):
Cet algorithme fonctionne parce que la carte de compression a un point fixe unique, et quelle que soit l'image source que nous choisissons, nous nous efforcerons de le faire.
Je pense que le moment est venu pour un petit exemple. Je vais essayer de compresser et de décompresser l'image du singe:
La fonction
test_greyscale
charge l'image, la compresse, la décompresse et affiche chaque itération de la décompression:
Pas mal du tout pour une implémentation aussi simple!
Images RVB
Une approche très naïve de la compression d'images RVB consiste à compresser les trois canaux individuellement:
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)]
Et pour le déballage, nous déballons simplement les données de trois canaux séparément et les collectons dans trois canaux d'image:
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)
Une autre solution très simple consiste à utiliser le même affichage de compression pour les trois canaux, car ils sont souvent très similaires.
Si vous souhaitez vérifier comment cela fonctionne, exécutez la fonction
test_rgb
:
Des artefacts sont apparus. Cette méthode est probablement trop naïve pour produire de bons résultats.
Où aller ensuite
Si vous voulez en savoir plus sur la compression d'images fractales, je peux vous recommander de lire l'article
Techniques de compression d'images fractales et ondelettes de Stephen Welsted. Il est facile à lire et explique des techniques plus sophistiquées.