Labor (Ruby)
image: title.webp kind: labor language: Ruby / Python languages: [ruby, python] duration: 2 Doppelstunden

Komprimieren mit Huffman

Im Morsecode ist E ein einziger kurzer Ton und Q ein langes Lang-lang-kurz-lang, weil E so viel häufiger vorkommt. Huffman-Codes machen dasselbe mit Bytes, aber so, dass man keine Pausen braucht, um zu wissen, wo ein Code aufhört. Du baust den Baum, aus dem die Codes kommen, in Ruby oder Python, codierst und decodierst damit und findest heraus, wie JPEG seine Huffman-Tabellen mit ein paar Bytes speichert.

1. Kurze Codes für häufige Zeichen

Jedes Byte hat 8 Bits, ob es ein »e« ist, in einem deutschen Text etwa jeder sechste Buchstabe, oder ein »q«, das kaum vorkommt. Das ist bequem, aber verschwenderisch. Samuel Morse und Alfred Vail haben das schon in den 1830er Jahren besser gemacht: Im Morsecode ist E ein einziger kurzer Ton (·), T ein langer (−), und seltene Buchstaben wie Q (−−·−) sind lang. Im Mittel wird eine Nachricht so kürzer.

Der Morsecode braucht dafür Pausen zwischen den Buchstaben: Ohne sie könnte ·· ein I sein oder zweimal E. Ein Computer soll ohne Pausen auskommen. Das geht, wenn kein Code der Anfang eines anderen ist. Dann heißt die Codetabelle präfixfrei: Wer Bit für Bit liest, weiß in dem Moment, in dem ein Code vollständig ist, dass er zu Ende ist.

Zeichen Code
A 0
B 10
R 110
C 1110
D 1111

Mit dieser Tabelle ist 0101100 eindeutig: 0 ist A, dann 10 ist B, 110 ist R, 0 ist A. Kein Code beginnt mit einem anderen, also gibt es keine zweite Möglichkeit.

Aufgabe 1Präfixfrei

Welche dieser Tabellen ist präfixfrei, lässt sich also ohne Pausen eindeutig lesen?

Präfixfrei

2. Der Baum

Wie findet man die besten Codes, also die, mit denen ein Text so wenige Bits wie möglich braucht? Diese Frage stellte Robert Fano 1951 seinen Studierenden am MIT als Hausarbeit, statt einer Prüfung. Er selbst kannte nur ein Verfahren, das gute, aber nicht immer die besten Codes findet. David Huffman fand nach Monaten eins, das immer die besten findet, und es ist erstaunlich einfach:

  1. Zähle, wie oft jedes Byte vorkommt. Jedes Byte ist ein Baum aus einem einzigen Blatt, und seine Häufigkeit ist das Gewicht des Baums.
  2. Nimm die beiden leichtesten Bäume und häng sie unter einen neuen Knoten. Sein Gewicht ist die Summe ihrer Gewichte.
  3. Wiederhole Schritt 2, bis nur noch ein Baum übrig ist.

Den Code eines Bytes liest du auf dem Weg von oben zu seinem Blatt ab: links eine 0, rechts eine 1. Seltene Bytes werden früh zusammengefasst und landen tief unten, mit langen Codes; häufige kommen spät dazu und bleiben oben, mit kurzen. Und weil jedes Byte an einem Blatt hängt und nie auf dem Weg zu einem anderen liegt, ist die Tabelle von selbst präfixfrei.

Probier es aus, mit dem Text unten oder einem eigenen. Rot umrandet sind die beiden Bäume, die als nächste zusammenkommen.

Haben zwei Bäume dasselbe Gewicht, ist es egal, welchen man nimmt. Die Codes sehen dann anders aus, manche sind sogar anders lang, aber der Text braucht insgesamt genau gleich viele Bits.

Vermutung 2Von Hand

Bau den Huffman-Baum für ABRACADABRA auf Papier, bevor du ihn oben im Schrittmacher nachbaust: A kommt 5-mal vor, B und R je 2-mal, C und D je einmal. Wie viele Bits braucht der ganze Text mit diesen Codes?

Tipp

Fass zuerst C und D zusammen (Gewicht 2). Dann gibt es drei Bäume mit Gewicht 2. Am Ende zählst du für jedes Zeichen Anzahl mal Länge seines Codes zusammen.

3. Einrichten

Lege im Terminal (StrgJ) einen Ordner an und öffne ihn mit StrgK StrgO:

mkdir /workspace/huffman

Lege darin die Datei huffman.rb an:

# Huffman-Codes: häufige Bytes bekommen kurze Codes, seltene lange.
#
# Ein Baum ist entweder ein Blatt, das Byte selbst (eine Zahl), oder ein
# Knoten, ein Array [links, rechts]. Links steht für 0, rechts für 1.

# Wie oft jedes Byte vorkommt: {65 => 5, 66 => 2, ...}
def frequencies(data)
end

# Immer die beiden leichtesten Bäume zusammenfassen, bis einer übrig ist.
def build_tree(counts)
end

# Der Code jedes Bytes als String aus 0 und 1: {65 => '0', 66 => '10', ...}
def code_table(tree)
end

# Der ganze Text als String aus 0 und 1
def encode(data, table)
end

# Aus den Bits und dem Baum wieder die Bytes
def decode(bits, tree)
end

p frequencies('ABRACADABRA')

Lege darin die Datei huffman.py an:

# Huffman-Codes: häufige Bytes bekommen kurze Codes, seltene lange.
#
# Ein Baum ist entweder ein Blatt, das Byte selbst (eine Zahl), oder ein
# Knoten, ein Tupel (links, rechts). Links steht für 0, rechts für 1.


def frequencies(data):
    """Wie oft jedes Byte vorkommt: {65: 5, 66: 2, ...}"""


def build_tree(counts):
    """Immer die beiden leichtesten Bäume zusammenfassen, bis einer übrig ist."""


def code_table(tree):
    """Der Code jedes Bytes als String aus 0 und 1: {65: '0', 66: '10', ...}"""


def encode(data, table):
    """Der ganze Text als String aus 0 und 1"""


def decode(bits, tree):
    """Aus den Bits und dem Baum wieder die Bytes"""


print(frequencies(b'ABRACADABRA'))

b'ABRACADABRA' sind Bytes, keine Zeichen; alle Methoden bekommen und liefern Bytes.

Die Bits schreiben wir der Einfachheit halber als String aus den Zeichen 0 und 1. Echte Programme packen je 8 davon in ein Byte, wie GIF und JPEG es tun; gezählt wird hier trotzdem in Bits.

4. Zählen

Schreib frequencies. Es gibt einen Hash zurück, der für jedes Byte des Texts sagt, wie oft es vorkommt.

Schreib frequencies. Es gibt ein dict zurück, das für jedes Byte des Texts sagt, wie oft es vorkommt.

  • data.each_byte { |byte| … } geht die Bytes durch, als Zahlen.
  • Hash.new(0) ist ein Hash, der für jeden Schlüssel, den es noch nicht gibt, 0 liefert. Damit kannst du gleich counts[byte] += 1 schreiben.
  • for byte in data: geht die Bytes durch, als Zahlen.
  • counts.get(byte, 0) liefert 0, wenn es das Byte noch nicht gibt. Damit kannst du gleich counts[byte] = counts.get(byte, 0) + 1 schreiben.
Aufgabe 3Zählen

Schreib frequencies in huffman.rb.

Tipp

Mit counts = Hash.new(0) liefert counts[byte] für jedes neue Byte 0, und counts[byte] += 1 zählt.

Aufgabe 3Zählen

Schreib frequencies in huffman.py.

Tipp

Mit counts[byte] = counts.get(byte, 0) + 1 zählst du, auch wenn es das Byte noch nicht gibt.

5. Den Baum bauen

Schreib build_tree nach den drei Schritten von oben. Ein Blatt ist das Byte selbst, eine Zahl; ein Knoten ist ein Array aus seinen beiden Kindern. Für »ABRACADABRA« könnte der Baum so aussehen: [65, [[67, 68], [66, 82]]].

Schreib build_tree nach den drei Schritten von oben. Ein Blatt ist das Byte selbst, eine Zahl; ein Knoten ist ein Tupel aus seinen beiden Kindern. Für »ABRACADABRA« könnte der Baum so aussehen: (65, ((67, 68), (66, 82))).

  • Beim Bauen brauchst du zu jedem Baum sein Gewicht. Halte deshalb eine Liste von Paaren [weight, tree], am Anfang eines für jedes Byte.
  • sort_by! { |weight, _| weight } sortiert die Liste nach dem Gewicht, shift nimmt das erste Element heraus. Zweimal shift nach dem Sortieren gibt dir die beiden leichtesten.
  • Am Ende ist ein einziges Paar übrig, und sein Baum ist das Ergebnis.
  • Beim Bauen brauchst du zu jedem Baum sein Gewicht. Halte deshalb eine Liste von Paaren (weight, tree), am Anfang eines für jedes Byte.
  • trees.sort(key=lambda pair: pair[0]) sortiert die Liste nach dem Gewicht, trees.pop(0) nimmt das erste Element heraus. Zweimal pop(0) nach dem Sortieren gibt dir die beiden leichtesten.
  • Am Ende ist ein einziges Paar übrig, und sein Baum ist das Ergebnis.

Dann code_table: Es geht den Baum von oben durch und merkt sich den Weg. Am einfachsten ist das mit einer Methode, die sich selbst aufruft, für das linke Kind mit einer 0 mehr im Weg, für das rechte mit einer 1. Kommt sie an ein Blatt, also an eine Zahl (tree.is_a?(Integer)), ist der Weg bis dorthin der Code dieses Bytes. Eine Methode darf dafür weitere Parameter mit Standardwerten haben, etwa def code_table(tree, prefix = '', table = {}).

Dann code_table: Es geht den Baum von oben durch und merkt sich den Weg. Am einfachsten ist das mit einer Funktion, die sich selbst aufruft, für das linke Kind mit einer 0 mehr im Weg, für das rechte mit einer 1. Kommt sie an ein Blatt, also an eine Zahl (isinstance(tree, int)), ist der Weg bis dorthin der Code dieses Bytes. Eine Funktion darf dafür weitere Parameter mit Standardwerten haben, etwa def code_table(tree, prefix=''); table.update(…) nimmt die Einträge eines anderen dict dazu.

Ein Sonderfall: Besteht der Text aus einem einzigen Byte, immer wieder, ist der Baum nur ein Blatt, und der Weg dorthin ist leer. Gib diesem Byte dann den Code 0.

Aufgabe 4Der Baum und die Codes

Schreib build_tree und code_table. Weil es bei Gleichstand mehrere richtige Bäume gibt, prüft die Aufgabe nicht die Codes selbst, sondern ob sie präfixfrei sind und ob der Text mit ihnen so wenige Bits braucht wie möglich.

Tipp

Eine Liste von Paaren: trees = counts.map { |byte, count| [count, byte] }. Solange mehr als eines darin ist: sortieren, die beiden ersten mit shift herausnehmen, ein neues Paar aus der Summe der Gewichte und [left, right] hinten anhängen.

Aufgabe 4Der Baum und die Codes

Schreib build_tree und code_table. Weil es bei Gleichstand mehrere richtige Bäume gibt, prüft die Aufgabe nicht die Codes selbst, sondern ob sie präfixfrei sind und ob der Text mit ihnen so wenige Bits braucht wie möglich.

Tipp

Eine Liste von Paaren: trees = [(count, byte) for byte, count in counts.items()]. Solange mehr als eines darin ist: sortieren, die beiden ersten mit pop(0) herausnehmen, ein neues Paar aus der Summe der Gewichte und (left, right) hinten anhängen.

6. Codieren und decodieren

encode ist kurz: Für jedes Byte der Code aus der Tabelle, alle hintereinander. join hängt eine Liste von Strings aneinander.

encode ist kurz: Für jedes Byte der Code aus der Tabelle, alle hintereinander. ''.join(…) hängt Strings aneinander.

decode liest die Bits einzeln und geht dabei im Baum nach unten, bei 0 nach links (node[0]), bei 1 nach rechts (node[1]). Kommt es an ein Blatt, hat es ein Byte gefunden, merkt es sich und beginnt wieder oben. Wie bei LZW macht pack('C*') am Schluss aus den Zahlen einen String. Denk auch hier an den Baum, der nur ein Blatt ist.

decode liest die Bits einzeln und geht dabei im Baum nach unten, bei 0 nach links (node[0]), bei 1 nach rechts (node[1]). Kommt es an ein Blatt, hat es ein Byte gefunden, merkt es sich und beginnt wieder oben. Wie bei LZW macht bytes(…) am Schluss aus den Zahlen Bytes. Denk auch hier an den Baum, der nur ein Blatt ist.

Aufgabe 5Codieren und decodieren

Schreib encode und decode.

Tipp

Beim Decodieren merkst du dir, wo du im Baum gerade bist: node = tree am Anfang, dann für jedes Bit node = node[0] oder node = node[1]. Ist node eine Zahl, ist ein Byte fertig.

Aufgabe 5Codieren und decodieren

Schreib encode und decode.

Tipp

Beim Decodieren merkst du dir, wo du im Baum gerade bist: node = tree am Anfang, dann für jedes Bit node = node[0] oder node = node[1]. Ist node eine Zahl, ist ein Byte fertig.

7. Wie gut ist das?

Lade die Spezifikation von GIF in den Ordner, einen Text von 1990 mit 83.003 Bytes, und ein Foto als JPEG:

wget https://workspace.hackschule.de/lzw/spec-gif89a.txt
wget https://workspace.hackschule.de/jpeg/44-baseline.jpg

File.binread('spec-gif89a.txt') liest sie Byte für Byte. Lass dein Programm ausgeben, wie viele Bits encode daraus macht, und probier gleich, ob decode die Datei wieder genau herausgibt.

open('spec-gif89a.txt', 'rb').read() liest sie als Bytes; das b in 'rb' ist wichtig. Lass dein Programm ausgeben, wie viele Bits encode daraus macht, und probier gleich, ob decode die Datei wieder genau herausgibt.

Aufgabe 6Die Spezifikation von GIF

Wie viele Bytes braucht spec-gif89a.txt mit Huffman, also die Zahl der Bits durch 8, aufgerundet?

Tipp

encode(data, table).length ist die Zahl der Bits.

Aufgabe 6Die Spezifikation von GIF

Wie viele Bytes braucht spec-gif89a.txt mit Huffman, also die Zahl der Bits durch 8, aufgerundet?

Tipp

len(encode(data, table)) ist die Zahl der Bits.

spec-gif89a.txt Bytes
Original 83.003
Huffman dein Ergebnis
LZW, mit wachsender Breite der Codes 28.813
gzip -9 20.271

Huffman sieht jedes Byte einzeln an: Es weiß, dass »e« häufig ist, aber nicht, dass nach »sc« fast immer ein »h« kommt. LZW findet genau solche Folgen und schlägt Huffman bei diesem Text deutlich. Am besten ist, beides zu verbinden: gzip sucht erst Wiederholungen, mit LZ77, einem Verwandten von LZW, und codiert das Ergebnis dann mit Huffman. JPEG macht es ähnlich: Erst werden die Zahlen eines Bildes so umgerechnet, dass viele Nullen entstehen, dann codiert Huffman, was übrig bleibt.

Aufgabe 7Ein Foto

Codiere auch 44-baseline.jpg mit deinem Huffman. Er spart dort nur gut 50 von 143.206 Bytes. Warum?

Ein Foto

8. Die Tabelle mitschicken

Wer decodieren will, braucht die Codes. Anders als bei LZW lassen sie sich nicht beim Lesen erraten; die Tabelle muss mit in die Datei. Den ganzen Baum zu speichern, wäre aufwendig. Es geht mit viel weniger: Es genügt, für jedes Byte die Länge seines Codes zu speichern, wenn beide Seiten die Codes nach derselben festen Regel daraus berechnen. Solche Codes heißen kanonisch:

  1. Sortiere die Bytes nach der Länge ihres Codes, bei gleicher Länge nach dem Byte.
  2. Das erste Byte bekommt lauter Nullen, so viele, wie sein Code lang ist.
  3. Jedes weitere bekommt den Code des vorigen plus 1. Ist sein Code länger als der vorige, kommen hinten Nullen dazu, bis die Länge stimmt.

Aus den Längen A: 2, B: 2, C: 2, D: 3, E: 3 werden so die Codes A = 00, B = 01, C = 10, D = 110 (10 + 1 = 11, mit einer Null dahinter), E = 111. Die Längen sind dieselben wie vorher, also braucht der Text genauso viele Bits, nur die Codes selbst haben sich geändert.

Genau so speichert JPEG seine Huffman-Tabellen im DHT-Segment: 16 Zahlen, wie viele Codes es mit 1, 2, … 16 Bits gibt, dann die Symbole in ihrer Reihenfolge. Daraus berechnet der Decoder die Codes. Ein Code darf bei JPEG höchstens 16 Bits haben; ein Encoder, dessen Baum tiefer wird, muss ihn so umbauen, dass kein Code länger ist.

Schreib canonical(lengths): Es bekommt einen Hash mit der Länge jedes Bytes und gibt einen Hash mit den kanonischen Codes zurück, als Strings aus 0 und 1. code.to_s(2) schreibt eine Zahl als Binärzahl, rjust(length, '0') füllt vorn mit Nullen auf, und << schiebt eine Zahl nach links, hängt also hinten Nullen an: 0b10 << 1 ist 0b100.

Schreib canonical(lengths): Es bekommt ein dict mit der Länge jedes Bytes und gibt ein dict mit den kanonischen Codes zurück, als Strings aus 0 und 1. format(code, 'b') schreibt eine Zahl als Binärzahl, rjust(length, '0') füllt vorn mit Nullen auf, und << schiebt eine Zahl nach links, hängt also hinten Nullen an: 0b10 << 1 ist 0b100. sorted(lengths.items(), key=lambda item: (item[1], item[0])) sortiert nach Länge und dann nach Byte.

Aufgabe 8Kanonische Codes

Schreib canonical(lengths). Es bekommt einen Hash mit der Länge des Codes für jedes Byte und gibt die kanonischen Codes zurück.

Tipp

Merk dir den Code als Zahl und die Länge des vorigen. Für jedes Byte, sortiert mit sort_by { |byte, length| [length, byte] }: erst code <<= length - previous, dann den Code mit code.to_s(2).rjust(length, '0') eintragen, dann code += 1.

Aufgabe 8Kanonische Codes

Schreib canonical(lengths). Es bekommt einen Hash mit der Länge des Codes für jedes Byte und gibt die kanonischen Codes zurück.

Tipp

Merk dir den Code als Zahl und die Länge des vorigen. Für jedes Byte, sortiert nach (length, byte): erst code <<= length - previous, dann den Code mit format(code, 'b').rjust(length, '0') eintragen, dann code += 1.

9. Weiter geht's

  • JPEG: In Eine JPEG-Datei decodieren liest du die Huffman-Tabellen aus einem DHT-Segment und decodierst damit Bit für Bit die Koeffizienten eines Fotos.
  • Bits packen: Schreib encode so um, dass es echte Bytes liefert, mit [bits].pack('B*'), und decode passend dazu. Wie erkennt decode, wo die Bits zu Ende sind, wenn das letzte Byte nicht voll ist?
  • Bits packen: Schreib encode so um, dass es echte Bytes liefert, mit int(bits[i:i + 8].ljust(8, '0'), 2) für je 8 Bits, und decode passend dazu. Wie erkennt decode, wo die Bits zu Ende sind, wenn das letzte Byte nicht voll ist?
  • Bruchteile von Bits: Ein Byte, das 90 % eines Texts ausmacht, bekommt bei Huffman mindestens ein ganzes Bit, obwohl ihm nach der Rechnung von Claude Shannon gut ein Siebtel Bit genügen würde. Die arithmetische Codierung kommt dem näher; such heraus, wie sie funktioniert.