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.