Vor ein paar Jahren habe ich eine sehr einfache Implementierung der fraktalen Bildkomprimierung fĂŒr Studenten geschrieben und den Code auf 
Github gepostet.
Zu meiner Ăberraschung stellte sich heraus, dass das Repository sehr beliebt war, und ich entschied mich, den Code zu aktualisieren und einen Artikel zu schreiben, der ihn und die Theorie erklĂ€rt.
Theorie
Dieser Teil ist ziemlich theoretisch, und wenn Sie nur an der Codedokumentation interessiert sind, können Sie ihn ĂŒberspringen.
Komprimierungszuordnungen
Lassen 
(E,d) Ist der 
gesamte metrische Raum und 
f:E rightarrowE - Zuordnung von 
E auf 
E .
Wir sagen das 
f ist eine 
komprimierte Zuordnung, falls vorhanden 
0<s<1 so dass:
 fĂŒrallex,y inE,d(f(x),f(y)) leqsd(x,y)
Darauf aufbauend 
f bezeichnet eine Komprimierungszuordnung mit einem KomprimierungsverhÀltnis 
s .
Es gibt zwei wichtige SĂ€tze zu Kontraktionsabbildungen: 
den Banach-Fixpunktsatz und 
den Collagensatz .
Fixpunktsatz : 
f hat einen eindeutigen Fixpunkt 
x0 .
Beweise zeigenZunÀchst beweisen wir, dass die Reihenfolge 
(un) eingestellt als
 $ inline $ \ left \ {\ begin {alignat *} {2} u_0 & = x \\ u_ {n + 1} & = f (u_n) \ end {alignat *} \ right. $ inline $ ist fĂŒr alle konvergierend 
x inE .
FĂŒr alle 
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 {da} f \ text {eine Kontraktionskarte ist} \\ & \ leq s ^ m \ left (\ sum_ {i = 0} ^ {nm-1} {d (f ^ i (x ), f ^ {i + 1} (x)} \ right) \ text {aus der Dreiecksungleichung} \\ & \ leq s ^ m \ left (\ sum_ {i = 0} ^ {nm-1} {s ^ id (x, f (x))} \ right) \ text {da} f \ text {eine Kontraktionskarte ist} \\ & = s ^ m \ left (\ frac {1 - s ^ {nm}} {1 - s} d (x, f (x)) \ rechts) \\ & \ leq \ frac {s ^ m} {1 - s} d (x, f (x)) \ Underset {m \ rechtspfeil \ infty} {\ rightarrow} 0 \ end {alignat *}
Deshalb 
(un) ist 
eine Cauchy - Sequenz , und 
E ist voller Raum, was bedeutet 
(un) konvergiert. Lass sie die Grenze sein 
x0 .
Da darĂŒber hinaus die Kontraktionskarte 
als Lipschitzkarte kontinuierlich ist, ist sie auch kontinuierlich, d.h. 
f(un) rightarrowf(x0) . Deshalb, wenn 
n neigt zur Unendlichkeit 
un+1=f(un) wir bekommen 
x0=f(x0) . Also 
x0 ist ein fester Punkt 
f .
Das haben wir gezeigt 
f hat einen festen Punkt. Zeigen wir im Widerspruch, dass es einzigartig ist. Lassen 
y0 - Ein weiterer Fixpunkt. Dann:
d(x0,y0)=d(f(x0),f(y0)) leqsd(x0,y0)<d(x0,y0)
Es gab einen Widerspruch.
 Weiter werden wir als bezeichnen 
x0 fester Punkt 
f .
Collagensatz : wenn 
d(x,f(x))< epsilon dann 
d(x,x0)< frac epsilon1âs .
Beweise zeigenIm vorherigen Beweis haben wir das gezeigt d(um,un) leq fracsm1âsd(x,f(x))= fracsm1âs epsilon .
Wenn wir es reparieren m in 0 dann bekommen wir d(x,un) leq frac epsilon1âs .
Beim Streben n bis unendlich bekommen wir das gewĂŒnschte ergebnis.
 Der zweite Satz sagt uns, dass wir eine Kontraktionskarte finden 
f so dass 
f(x) in der NĂ€he von 
x Dann können wir sicher sein, dass der Fixpunkt 
f auch in der NĂ€he von 
x .
Dieses Ergebnis wird die Grundlage fĂŒr unsere zukĂŒnftige Arbeit sein. Anstatt das Bild zu speichern, reicht es fĂŒr uns, die komprimierte Anzeige zu speichern, deren fester Punkt nahe am Bild liegt.
Komprimierungsanzeigen fĂŒr Bilder
In diesem Teil werde ich zeigen, wie man solche Druckanzeigen erstellt, damit der Fixpunkt nahe am Bild liegt.
Stellen wir zuerst den Bildsatz und die Entfernung ein. Wir werden wÀhlen 
E=[0,1]h timesw . 
E Ist eine Menge Matrizen mit 
h in Reihen 
w Spalten und mit Koeffizienten im Intervall 
[0,1] . Dann nehmen wir 
d(x,y)= left( sumhi=1 sumwj=1(xijâyij)2 right)0.5 . 
d Ist der Abstand von der 
Frobenius-Norm erhalten .
Lassen 
x inE Ist das Bild, das wir komprimieren möchten.
Wir werden das Bild zweimal in Blöcke aufteilen:
- ZunÀchst unterteilen wir das Bild in endliche oder Intervallblöcke R1,...,RL . Diese Blöcke sind getrennt und bedecken das gesamte Bild.
- Dann teilen wir das Bild in Blöcke von Quellen oder DomÀnen D1,...,DK . Diese Blöcke sind nicht notwendigerweise getrennt und bedecken nicht notwendigerweise das gesamte Bild.
Zum Beispiel können wir das Bild wie folgt segmentieren:
Dann fĂŒr jeden Intervallblock 
Rl Wir wÀhlen einen Domain-Block 
Dkl und Zuordnung 
fl:[0,1]Dkl rightarrow[0,1]Rl .
Als nÀchstes können wir eine Funktion definieren 
f wie:
f(x)ij=fl(xDkl)ij textif(i,j) inRl
Genehmigung : wenn 
fl sind Vertragszuordnungen, dann und 
f auch komprimiertes Mapping.
Beweise zeigenLassen 
x,y inE und nehme an, dass alle 
fl sind Kompressionszuordnungen mit einem KompressionsverhÀltnis 
sl . Dann bekommen wir folgendes:
\ 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 {per definitionem} d \\ & = \ sum_ {l = 1} ^ L {\ sum _ {(i, j) \ in R_l} {(f (x) _ {ij} -f (y) _ {ij}) ^ 2}} \ text {da} (R_l) \ text {eine Partition ist} \\ & = \ 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 {per definitionem} f \\ & = \ sum_ {l = 1} ^ L {d (f_l (x_ {D_ {k_l}}), f_l (y_ {D_ {k_l}}) ) ^ 2} \ text {per definitionem} d \\ & \ leq \ sum_ {l = 1} ^ L {s_l ^ 2d (x_ {D_ {k_l}}, y_ {D_ {k_l}}) ^ 2} \ text {since} (f_l) \ text {sind Kontraktionszuordnungen} \\ & \ 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 {per definitionem} 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 {ist eine Partition} \\ & = \ underset {l} {\ max} {s_l ^ 2} d (x, y) ^ 2 \ text {per definitionem} d \\ \ end {alignat *}
 Es bleibt noch eine Frage zu beantworten: Wie soll man wÀhlen? 
Dkl und 
fl ?
Der Collagensatz bietet eine Möglichkeit, sie auszuwÀhlen: if 
xRl ist in der NĂ€he von 
f(xDkl) fĂŒr alle 
l dann 
x ist in der NĂ€he von 
f(x) und nach dem Collagensatz 
x und 
x0 sind auch nah.
Wir sind also fĂŒr alle unabhĂ€ngig 
l wir können aus jedem einen Satz von Quetschzuordnungen konstruieren 
Dk auf 
Rl und wÀhle das Beste. Im nÀchsten Abschnitt zeigen wir alle Details dieser Operation.
Implementierung
In jedem Abschnitt werden interessante Codefragmente kopiert. Das gesamte Skript finden Sie 
hier .
Partitionen
Ich habe einen sehr einfachen Ansatz gewÀhlt. Quellblöcke und Blattblöcke segmentieren das Bild in einem Raster, wie im obigen Bild gezeigt.
Die GröĂe der Blöcke entspricht der Potenz von zwei, was die Arbeit erheblich vereinfacht. Quellblöcke sind 8 mal 8 und Endblöcke sind 4 mal 4.
Es gibt komplexere Partitionsschemata. Zum Beispiel können wir den Quadtree-Baum verwenden, um Bereiche mit vielen Details stÀrker aufzuteilen.
Conversions
In diesem Abschnitt werde ich zeigen, wie Sie aus Komprimierungszuordnungen erstellen 
Dk auf 
Rl .
Denken Sie daran, dass wir ein solches Mapping generieren möchten 
fl zu 
f(xDk) war in der NĂ€he von 
xRl . Das heiĂt, je mehr Zuordnungen wir generieren, desto wahrscheinlicher ist es, eine gute zu finden.
Die QualitÀt der Komprimierung hÀngt jedoch von der Anzahl der zum Speichern erforderlichen Bits ab 
fl . Das heiĂt, wenn viele Funktionen zu groĂ sind, ist die Komprimierung schlecht. Hier ist ein Kompromiss erforderlich.
Ich habe das entschieden 
fl wird so aussehen:
fl(xDk)=s maldrehen theta(flipd(verkleinern(xDk)))+b
wo 
reduzieren - Dies ist eine Funktion zum Verschieben von den Blöcken 8 bis 8 zu den Blöcken 4 bis 4. 
flip und 
drehen - affine Transformationen, 
s Àndert den Kontrast und 
b - Helligkeit.
Die 
reduce reduziert die GröĂe des Bildes, indem die Umgebung gemittelt wird:
 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 
Die 
rotate dreht das Bild um einen bestimmten Winkel:
 def rotate(img, angle): return ndimage.rotate(img, angle, reshape=False) 
Beibehaltung der Form des Bildwinkels 
 theta kann nur Werte annehmen 
\ {0 ^ {\ circ}, 90 ^ {\ circ}, 180 ^ {\ circ}, 270 ^ {\ circ} \} .
Die 
flip Funktion spiegelt das Bild, wenn die 
direction -1 ist, und spiegelt nicht, wenn der Wert 1 ist:
 def flip(img, direction): return img[::direction,:] 
Die vollstÀndige Konvertierung wird von der Funktion 
apply_transformation :
 def apply_transformation(img, direction, angle, contrast=1.0, brightness=0.0): return contrast*rotate(flip(img, direction), angle) + brightness 
Wir benötigen 1 Bit, um uns zu erinnern, ob eine Spiegelung erforderlich ist, und 2 Bit fĂŒr den Drehwinkel. AuĂerdem, wenn wir sparen 
s und 
b Bei Verwendung von 8 Bits fĂŒr jeden Wert werden nur 11 Bits benötigt, um die Konvertierung zu speichern.
AuĂerdem sollten wir prĂŒfen, ob es sich bei diesen Funktionen um Kontraktionszuordnungen handelt. Der Beweis dafĂŒr ist etwas langweilig und nicht wirklich das, was wir brauchen. Vielleicht fĂŒge ich es spĂ€ter als Anhang zum Artikel hinzu.
Kompression
Der Komprimierungsalgorithmus ist einfach. ZunÀchst generieren wir alle möglichen affinen Transformationen aller Quellblöcke mit der Funktion 
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):  
Dann prĂŒfen wir fĂŒr jeden letzten Block alle zuvor erzeugten transformierten Quellblöcke. FĂŒr jedes 
find_contrast_and_brightness2 optimieren wir Kontrast und Helligkeit mit der Methode 
find_contrast_and_brightness2 Wenn die getestete Konvertierung die bisher beste ist, speichern Sie sie:
 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')  
Um den besten Kontrast und die beste Helligkeit zu finden, löst die Methode 
find_contrast_and_brightness2 einfach das Problem der kleinsten Quadrate:
 def find_contrast_and_brightness2(D, S):  
Auspacken
Der Dekomprimierungsalgorithmus ist noch einfacher. Wir beginnen mit einem völlig zufÀlligen Bild und wenden dann mehrmals ein Quetschbild an 
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])):  
Dieser Algorithmus funktioniert, weil die Komprimierungszuordnung einen eindeutigen festen Punkt hat und wir unabhÀngig davon, welches Quellbild wir auswÀhlen, danach streben werden.
Ich denke, es ist Zeit fĂŒr ein kleines Beispiel. Ich werde versuchen, das Affenbild zu komprimieren und zu entpacken:
Die Funktion 
test_greyscale lÀdt das Bild, komprimiert es, dekomprimiert es und zeigt jede Iteration der Dekomprimierung an:
Gar nicht so schlecht fĂŒr eine so einfache Implementierung!
RGB-Bilder
Ein sehr naiver Ansatz zum Komprimieren von RGB-Bildern besteht darin, alle drei KanÀle einzeln zu komprimieren:
 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)] 
Und zum Auspacken packen wir einfach die Daten von drei KanÀlen getrennt aus und sammeln sie in drei BildkanÀlen:
 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) 
Eine andere sehr einfache Lösung besteht darin, fĂŒr alle drei KanĂ€le dieselbe Komprimierungsanzeige zu verwenden, da sie sich hĂ€ufig sehr Ă€hnlich sind.
Wenn Sie ĂŒberprĂŒfen möchten, wie dies funktioniert, fĂŒhren Sie die Funktion 
test_rgb aus:
Artefakte erschienen. Diese Methode ist wahrscheinlich zu naiv, um gute Ergebnisse zu erzielen.
Wohin als nÀchstes?
Wenn Sie mehr ĂŒber die fraktale Bildkomprimierung erfahren möchten, kann ich Ihnen den Artikel 
Fractal and Wavelet Image Compression Techniques von Stephen Welsted empfehlen. Es ist leicht zu lesen und erklÀrt anspruchsvollere Techniken.