Ziv-Lempel-Kompression von André Lichei Einführung: Die wichtigsten Ansprüche,die an einen Komprimierungs-Algorithmus gestellt werden, sind: - eine hohe Komprimierungsrate - für alle Typen von Daten ( daher ohne Kenntnis über die Eigenart der Daten ) - hohe Encode- und Decode-Geschwindigkeit - geringe Ansprüche an die Hardware Die Ziv-Lempel Verfahren sind, in der Gruppe der verlustfrei packenden Algorithmen, die, die diesem Ideal noch am nächsten kommen. Die ersten Verfahren von Jacob Ziv und Abraham Lempel sollen in dieser Arbeit vorgestellt werden. Die Grundidee all dieser Verfahren ist es eine Zeichenkette, die schon einmal gesendet/gespeichert wurde und nun wieder auftaucht, durch einen Zeiger auf die Stelle wo sie eher auftauchte oder durch einen Zeiger auf ein Wörterbuch zu ersetzen. Damit soll die Länge der zu codierenden Zeichenkette beträchtlich gekürzt werden. LZ77 Der LZ77 Algorithmus wurde 1977 vorgestellt. Die Idee: Man legt ein Fenster fester Länge über den bisher gesendeten String. Nun versucht man, in diesem Fenster den längsten Teilstring zu finden, der die nächsten Zeichen repräsentiert. Verschlüsselungsprozeß Es wird als erstes ein Fenster F der Festen Länge LF angelegt und mit r Nullen und den ersten q Zeichen des zu codierenden Strings Q gefüllt. Als einen Präfix der Länge LP einer Zeichenkette def. ich die ersten LP Zeichen dieser Zeichenkette. Es wird nun die längste Zeichenkette P gesucht, die einem Präfix der Zeichenkette F(r+1,f) entspricht. Diese gesuchte Zeichenkette muß in F(1,r) beginnen. Sie darf aber über die Grenze r im Fenster F hinausgehen. Bsp.: LF= 10 r=5 q=5 Q: 00101101011101 F(00000 00101) als P kann man zu Bsp. den String angeben der bei F(5) beginnt und die Länge 2 hat. Gespeichert oder gesendet wird jetzt als erstes die Startposition Ps von P, dann dessen Länge Pl und dann das nächste Zeichen Z. Bsp.: Ps=5 P l=2 Z='1' Die Codewortlänge läßt sich recht einfach ermitteln. Die Bitkette, die Ps darstellt, muß r Positionen abbilden können und muß damit eine Länge von Log2(r) aufgerundet haben. PL kann max. der Größe des noch nicht codierten Strings ( die leere Zeichenkette ist eingeschlossen ) im Fenster entsprechen. Es ist für die Implementierung aber zweckmäßig,
das letzte Zeichen nicht zu codieren. Dies brauchen wir noch um es als Z im Codewort zu senden. Wir brauchen also für PL Log2(q-1+1) aufgerundet Bits. Die Anzahl an Bits, die wir für das Senden/Speichern des nächsten Zeichens benötigen, hängt von dem Code, in dem es gesendet wird ab. Mindestens benötigen wir log2(anzahl der Zeichen im Alphabet)- aufgerundet - Bits. Es gibt sich daher eine Codewortlänge von (log2(r)+log2(q)+log2(anzahlderzeichen)). Bsp.: Codewort: (5,2,'1') 1000101 Es ist sinnvoll bei der Position das Zählen bei 0 zu beginnen. Bei der Länge aber bei 1, da die 0 den leeren String symbolisiert. Nun wird das Fenster um Pl+1 Zeichen nach rechts verschoben ( es werden die nächsten Pl+1 Zeichen eingelesen). Bsp.: F(00001 01101) Der Verschlüsselungsprozeß geschieht nun wieder genauso wie oben. Bsp.: nächste Codewort (4,2,1) 0110101 neues Fenster F(01011 01011) nächstes Codewort(1,4,1) 0001001 neues Fenster F(01011 101) nächstes Codewort (2,2,1) 0010101 Der Code zum Schluß (5,2,1) (4,2,1) (1,4,1) (2,2,1) 1000101 0110101 0001001 0010101 28bit 15 Bit Original Der Code ist recht lang, was daran liegt, daß wir eine sehr schlechte Fenstergröße gewählt haben. Zuerst sollte man immer Fenster der Größe 2^n wählen. Dann ist die Wahrscheinlichkeit, ein großes P zu finden bei einem großem r besser, wie auch die max. Länge von P mit wachsendem q steigt, so dass mit größeren Fenstern wesentlich bessere Kompriemierungsraten erzeugt werden können. Entschlüsselung Zur Entschlüsselung benutzt der Decoder ein ähnliches Fenster. Es wird aber nur P und das nächste Zeichen an die Ausgabe gesendet und das Fenster verschoben, so dass die Decodierung sehr schnell geschieht. Bsp.: Code (5,2,1) (4,2,1) (1,4,1) (2,2,1) F(00000 00 1 ) Ausgabe: 00 1 F(00001 01 1 ) Ausgabe: 01 1 F(01011 01011 ) Ausgabe: 0101 1 F(01011 10 1 ) Ausgabe: 101
Anwendung LZSS LZB LZH Bei dem vorgestellten LZ77 Algorithmus fallen zwei Dinge ins Auge. Zum einen muß immer ein P gesendet werden auch wenn Pl=0 ist. Zum anderen wird immer das nächste Zeichen mitgesendet auch wenn es gar nicht nötig ist. Im LZSS wird entweder ein Zeichen oder P gesendet. Um zwischen Zeichen und zu unterscheiden wird ein Extrabit vorangestellt. Es wird immer dann ein Zeichen gesendet, wenn der Code für das gefundene P länger ist als der Präfix selbst. Das Codewort hat nun entweder die Länge 1+log2(ZeichenImAlphabet) oder 1+log2(r)+log2(q). Jetzt ist es auch bei der Pl sinnvoll mit Null das Zählen zu Beginn (leere Zeichenketten werden ja nicht mehr gesendet). Das längste mögliche P darf jetzt eine Länge Pl=q haben, da wir Z nicht mehr senden. erlaubt verschieden große Zeiger Beruht auf LZSS Zeiger und Zeichen werden mit Huffman codiert, Nachteil: sehr langsam; Probleme mit großen Zeiger zip gzip Sehr ähnlich dem LZSS verfahren verwendet aber noch Huffman Bäume für die Zeiger. Andere Verwaltung, um Zeichenketten zu finden. acb Sehr gute Kompresionsrate aber auch sehr kompliziert. LZ 78 Der LZ 78 wurde 1978 vorgestellt. In diesem Algorithmus wird der Datenstrom in Phrasen unterteilt, wobei keine identischen Phrasen entstehen sollen. Der Algorithmus beginnt mit Phrasen von einem Zeichen Länge. Nun sucht er im Datenstrom die kürzeste noch nicht bekannte Phrase. Alle so während der Codierung entstehenden Phrasen werden in einem Wörterbuch gespeichert. Jede neue Phrase besteht genau aus einer schon codierten Phrase und einem Zeichen. ( man kann auch die längste bekannte Phrase suchen und ein Zeichen anhängen ) Bsp.: Zeichenfolge: 1011010100010 Phrasen: 1 0 11 01 010 00 10 13 Bits Somit kann jede neue Phrase als Zeiger auf eine alte Phrase und einem neuen Zeichen kodiert werden.
Bsp.: Wörterbuch: 0 <> 1 1 2 0 3 11 4 01 5 010 6 00 7 10 Gesendet/ gespeichert werden nun Codewörter bestehend aus einem Zeiger, der auf einen Wörterbucheintrag zeigt und einem Zeichen. Um die Codewortlänge zu bestimmen, müssen wir nun die Größe der Zeiger bestimmen. Eine Möglichkeit wäre, alle Phrasen zu finden, zu zählen und eine feste Zeigerlänge zu bestimmen. In unserem Bsp. würde das Codewort folglich aus log2(8)=>3 Bits + 1 Bit für das Zeichen bestehen Bsp.: Zeichenkette codiert. 0001 0000 0011 0101 1000 0100 0010 28 Bits 0,'1' 0,'0' 1,'1' 2,'1' 4,'0' 2,'0' 1,'0' Ein solcher Code ist nicht besonders sinnvoll, da das Wörterbuch am Anfang sehr klein ist, und die Zeiger sehr kurz sind. Für die Codierung des Zeigers reicht somit eine Bitlänge die der Anzahl der bisher aufgetretenen Phrasen entspricht log2(i+1) Bsp.: Zeichenkette codiert 01 00 011 101 1000 0100 0010 22Bits Bei dem bisher vorgestellten Algorithmus kann das Wörterbuch bis in die Unendlichkeit wachsen. In der Praxis wird dem Wörterbuch eine max. Größe vorgegeben. Ist die Größe erreicht wird es einfach gelöscht und von vorne begonnen. In diesem Bsp. hat der Algorithmus die Zeichenkette nicht komprimiert sondern expandiert. Das ist aber ein Problem der Kürze der Zeichenkette. Bei langen Zeichenketten, wo dann auch die Wörterbucheinträge entsprechend lang werden, können recht gute Ergebnisse erzielt werden. Decodierung: Dem Decoder muß das Wörterbuch nicht extra gesendet werden. Jede Phrase die er empfängt speichert er in einem Wörterbuch. So können dann später empfangene Phrasen mit Hilfe des Wörterbuches entschlüsselt werden. Bsp.: Code (0,'1') (0,'0') (1,'1') (2,'1') (4,'0') (2,'0') (1,'0') Wörterbuch: 0 <> erkannter Eintrag Ausgabe Neuer Wörterbucheintrag <> '1' 1 '1' <> '0' 2 '0' '1' '11' 3 '11' '0' '01' 4 '01' '01' '010' 5 '010' '0' '00' 6 '00' '1' '11' 7 '10'
Verwaltung des Wörterbuches: Das Wörterbuch kann man entweder über eine Tabelle oder einen Baum verwalten. Ersteres ist auf der Decoderseite sinnvoll, auf der Encoderseite aber sehr langsam! Ein Bsp. für einen Baum habe ich leider nicht gefunden. Es ist aber nicht sehr schwer zu mindestens eine ganz einfache Variante zu erstellen. Eine Lsg. ist es, einen Baum zu generieren, dessen Knoten jeweils eine Wörterbuchnummer enthalten. Die Äste stehen jeweils für ein Zeichen. Die Wurzel repräsentiert daher die leere Zeichenkette. Geht man den Baum entlang, erhält man eine Zeichenkette. Die Nummer, die man in dem Knoten an ihrem Ende findet, ist der Wörterbucheintrag, unter dem man diese Zeichenkette findet. Ist nun eine Zeichenkette zu codieren, geht man den Baum entlang bis kein Zeichen mehr der zu kodierenden Zeichenkette entspricht. Die Nummer im letzten gefunden Knoten entspricht der Nummer des zu sendenden Wörterbucheintrages. Dann sendet man das nächste Zeichen. An den Knoten fügt man den Ast der dem neuen Zeichen entspricht. In das Blatt trägt man eine neue Nummer ein. Bsp.: Zu Beginn besteht der Baum nur aus der Wurzel. Sie enthält die Nummer 0. Ein Zähler für die schon vorhandenen Wörterbucheinträge steht auf 0. Zu codierende Zeichenkette: 1011010100010 Das Alphabet besteht aus nur zwei Zeichen. Man erhält somit einen Binärbaum. Der linke Ast eines Knotens soll der '1' entsprechen. Der rechte der '0' Baum Start Der Encoder geht die Zeichenkette und den Baum entlang; Stoppt an der Wurzel sendet die 0 und das nächstes Zeichen die '1'.Fügt einen neuen Ast an die Wurzel an und erhöht den Zähler um eins und ordnet dem neu entstandenen Knoten die Nummer 1 zu '1'/ (1) Zeichenkette 011010100010 wie oben Zeichenkette 11010100010 Jetzt stoppt der Encoder bei dem Vergleich am Knoten 1 sendet die 1 und das nächste Zeichen eine '1' Hängt dann einen neuen Ast an und erhöht den Zähler um eins. Dann ordnet er die neue Nummer zu '1'/ (3)
Zeichenkette 010100010 Stopp an Knoten 2 sendet 2,1 hängt Knoten mit Nummer 4 an '1'/ '0'/ (3) (4) Zeichenkette 0100010 Stopp an Knoten 4 sendet 4,hängt Ast mit Nummer 5 an '1'/ '1'/ (3) (4) '0'\ (5) Zeichenkette: 0010 Stopp an Knoten 2 sendet 2,0 hängt Ast mit Nummer 6 an '1'/ (3) (4) (6) '0'\ (5) Zeichenkette: 10 Stopp an Knoten 1 sendet 1,0 hängt Ast mit Nummer 7 an (3) (7) (4) (6) '0'\ (5) Ein solcher Baum läßt sich schnell und einfach erzeugen und ist wesentlich schneller zu durchsuchen als eine Liste. Die max. Anzahl an Söhnen eines jedem Knoten entspricht der Anzahl der Zeichen im Alphabet. Anwendungen: LZW Auch beim LZ78 Algorithmus fällt auf, daß immer ein Extrazeichen mitgesendet wird auch wenn das gar nicht nötig wäre. Im LZW Algorithmus ist das nicht so. Dieser beginnt nicht mit einem leeren Wörterbuch. Das Wörterbuch wird gleich zu Beginn mit allen bekannten Zeichen initialisiert. Nun wird die längste bekannte Zeichenkette gesucht und ihr Index gesendet. Danach wird diese Zeichenkette um das
nächste Zeichen erweitert und unter einem neuen Index in das Wörterbuch aufgenommen. Bsp.: Wörterbuch mit 256 Einträgen (ASCI-Tabelle von 0 bis 255) Eingabe: Erkanntes Muster Neuer Wörterbucheintrag LZWLZ78LZ77 L LZ (256) ZWLZ78LZ77 Z ZW (257) WLZ78LZ77 W WL (258) LZ78LZ77 LZ LZ7(259) 78LZ77 7 78(260) 8LZ77 8 8L(261) LZ77 LZ7 LZ77(262) 7 7 Decodierung Auch der Decoder beginnt mit dem vorinitialisierten Wörterbuch. Empfangene Zeichenketten werden gepuffert und ausgegeben. Von der nächsten Zeichenkette wird das erste Zeichen an die gepufferte Zeichenkette angehangen und diese dann als neuer Wörterbucheintrag gespeichert. Bsp.: L Z W (256) 7 8 (259) 7 Eingabezeichen Puffer Neuer Wörterbucheintrag L Z L LZ (256) W Z ZW (257) LZ W WL (258) 7 LZ LZ7 (259) 8 7 78 (260) LZ7 8 8L (261) 7 LZ7 LZ77(262) Bei genauerer Betrachtung sieht man, daß das Wörterbuch des Decoder dem des Encoder einen Schritt hinterherhinkt. Das kann zu Problemen beim Decodieren führen. Problemfall K[OMEGA]K [OMEGA]K: K ist ein beliebiges Zeichen und [OMEGA] eine Zeichenkette, wobei K[OMEGA] schon im Wörterbuch vermerkt ist. Es wird nun der Index von K[OMEGA] gesendet, und der Eintrag K[OMEGA]K im Wörterbuch vermerkt. Im nächsten Schritt wird genau dieser Eintrag als der längste identifiziert und gesendet. Dieser Wörterbucheintrag ist dem Decoder in diesem Moment aber noch gar nicht bekannt.
Bsp.: APAPAPAPAP Codierung Eingabe: Erkanntes Muster Neuer Wörterbucheintrag 1 APAPAPAPAP A AP(256) 2 PAPAPAPAP P PA(257) 3 APAPAPAP AP APA(258) 4 APAPAP APA APAP(259) 5 PAP PA PAP(260) 6 P P Decodierung: A P (256) (258) (257) P 1 2 3 4 5 6 Eingabezeichen Puffer Neuer Wörterbucheintrag 1 A 2 P A AP (256) 3 AP P PA (257) 4????? AP K[OMEGA]K Fall ist aufgetreten d.h. erstes Zeichen der nächsten Zeichenkette ist erstes Zeichen der vorhergehenden Zeichenkette also A 4 A... AP APA (258) -->4 APA AP APA (258) 5 PA APA APAP (259) 6 P PA PAP (260) Abschließend sei bemerkt, daß in der Standard-Implemetation das Wörterbuch eine Größe von 4096 Einträgen hat. Die Zeiger auf die Einträge sind statisch 12 Bits groß. Ist das Wörterbuch voll, muß der Encoder damit auskommen. LZC LZT Wird von dem Programm Compress unter Unix verwendet. Hier muß das Wörterbuch keine feste Größe haben, es wächst bis zu einer max. Größe, die vom User angegeben wird. Die Zeiger sind von dynamischer Größe und beginnen bei 9 Bit. Ist das Wörterbuch gefüllt überprüft ein Kontrollalgorithmus die Kompresionsrate. Sinkt diese unter einen bestimmten Wert, wird das gesamte Wörterbuch gelöscht und neu aufgebaut. Genauso wie LZC - das Wörterbuch wird aber nicht ganz gelöscht sondern nur wenig genutzte Einträge werden entfernt. LZMW wie LZT; es wird aber nicht nur das erste Zeichen angehängt, um einen neuen Wörterbucheintrag zu bekommen sondern es werden die letzten beiden Wörterbucheinträge verbunden.
Vorteil: Wörterbuch paßt sich schneller an. Einträge werden schnell länger. Nachteil: sehr komplizierte Wörterbuchverwaltung. Zum Abschluß sei noch angemerkt, das es noch eine ganze Reihe anderer Algorithmen gibt, die auf Lempel-Ziv basieren. Sie alle zu behandeln würde den Rahmen dieser Ausarbeitung aber sprengen. Trotzdem ist es bestimmt noch möglich, Verbesserungen der Algorithmen zu finden. Mehrere verschiedene Kompressionsalgorithmen miteinander zu kombinieren ist vermutlich ein guter Ansatz. Quellen: Gabriele Buch, Fehlerrobuste Verfahren zur Quellen- und Kanalcodierung von Binär und ASCII- Quellen. Mark Nelson, Fast String Searching with Suffix Trees. Marek Tomczyk, Lempel-Ziv-Kodierung.