Algorytmy Kompresji Danych Laboratorium Ćwiczenie nr 4: Kodowanie arytmetyczne, range coder 1. Zapoznać się z opisem implementacji kodera entropijnego range coder i modelem danych opracowanym dla tego kodera (patrz załącznik 1) oraz z przykładami ich użycia. 2. Implementację algorytmu LZSS (koder i dekoder), opracowaną na poprzednim ćwiczeniu, rozbudować o opcję arytmetycznego kodowania wyjścia z kodera słownikowego (i dekodowania wejścia dla dekodera). Dane wyprowadzane przez algorytm LZSS (tzn. flagę /litera czy ciąg/, literę /gdy nie znaleziono ciągu/, lub offset znalezionego ciągu oraz jego długość) należy kodować nie kodem binarnym stałej długości, a za pomocą range codera. Modelowanie wyjścia algorytmu LZSS można zrealizować w oparciu o kilka adaptacyjnych modeli danych: dla flag model bezpamięciowy lub model kontekstowy (w takim przypadku kontekstem winna być poprzednia flaga), dla liter (gdy nie znaleziono ciągu) model bezpamięciowy, dla offsetów znalezionych ciągów model bezpamięciowy, szczegóły patrz niżej, dla długości znalezionych ciągów model kontekstowy, kontekstem może być log 2 (offset znalezionego ciągu), (funkcja log 2 (int) patrz załącznik 2). Zatem w programie należy użyć czterech modeli oraz jednego kodera. W zależności od tego, co w danym momencie kodujemy, używać będziemy odpowiedniego modelu. Jeżeli np. kodujemy literę, to modelem dla liter wyznaczamy jej prawdopodobieństwo (i wymagane przez koder łączne prawdopodobieństwo wszystkich mniejszych liter) i kodujemy ją koderem arytmetycznym (jedynym, tzn. tym samym, którym w poprzednim kroku kodowaliśmy flagę na podstawie danych otrzymanych z modelu dla flag). Offset znalezionego ciągu, tj. liczba z zakresu [1.. MAX_OFFSET], to alfabet o dużej liczbie symboli (typowo MAX_OFFSET=32768); między innymi ze względu na problem ZFP (patrz instrukcja do ćwiczenia nr. 2) adaptacyjne modelowanie takiego alfabetu mogłoby nie być efektywne. Można jednak podzielić zakres na dwa podzakresy: małych i często występujących offsetów [1.. X] oraz dużych, występujących rzadziej [X + 1.. MAX_OFFSET]. X jest parametrem, którego wartość należy dobrać. Następnie modelowaniu i kodowaniu podlega alfabet X + 1 symboli, z których pierwsze X traktowane będą jako wartości małych offsetów, a ostania (X + 1) to znacznik dużego offsetu. gdy offset jest w zakresie [1.. X] to kodujemy wartość offsetu i tą wartością aktualizujemy model, gdy offset jest w zakresie [X + 1.. MAX_OFFSET] to kodujemy symbol X + 1, jako znacznik dużego offsetu, następnie musimy dodatkowo wyprowadzić, binarnie na log 2 (MAX_OFFSET X) bitach, liczbę: offset X 1; model aktualizujemy symbolem X + 1. Np.: W naszym wariancie LZSS MAX_OFFSET=32768. Przyjmujemy, że małe offsety to te, nie większe od 250 (X=250), więc model danych dla offsetów będzie zawierał 251 symboli. Symbol 251 będzie znacznikiem dużego offsetu a pozostałe symbole będą wartościami małych offsetów. Kodujemy offsety wyznaczone algorytmem LZSS:
gdy LZSS wygeneruje mały offset, czyli offset jest w zakresie [1.. 250], np. 20, to kodujemy koderem arytmetycznym wprost wartość offsetu (liczbę 20) i tą wartością aktualizujemy model, gdy offset jest duży, czyli jest w przedziale [251.. 32768] i np. wynosi 2000, to kodujemy, jako znacznik dużego offsetu symbol 251, a następnie wyprowadzamy liczbę 1749=2000 251 binarnie, na 15 bitach; model aktualizujemy symbolem 251. Uwagi: Do strumienia wyjściowego kodera arytmetycznego nie można wprost wyprowadzać żadnych danych z pominięciem tego kodera. Zatem aby wyprowadzić n-bitową liczbę do strumienia, należy ją zakodować koderem arytmetycznym jako liczbę z przedziału [0.. 2 n 1] o równomiernym rozkładzie prawdopodobieństwa (liczby 8- i 16-bitowe patrz załącznik 1: makra encode_byte i encode_short, liczby n-bitowe załącznik 2). Zakres wartości offsetu można podzielić na kilka (K) przedziałów, z których pierwszy jest [1.. X] i używać alfabetu X + K 1 symboli. X początkowych symboli używamy do kodowania offsetów z pierwszego przedziału, pozostałych symboli jako znaczników pozostałych przedziałów. Dla pozostałych przedziałów konieczne jest zakodowanie, oprócz znacznika przedziału, pozycji offsetu wewnątrz przedziału zakodowanie binarnie na takiej liczbie bitów, jaka wynika z długości przedziału. Pewne wartości nie wystąpią na wyjściu algorytmu LZSS; minimalna długość znalezionego ciągu jest liczbą większą od 1 i jest zależna od parametrów algorytmu (typowo wynosi 3 lub więcej), co można uwzględnić optymalizując koder. 3. Dla jednego z plików z korpusu Calgary (sugerowane użycie pliku wybranego podczas poprzednich zajęć) dobrać parametry: liczbę małych offsetów (X) oraz szybkość adaptacji modeli danych (parametr rescale z funkcji initqsmodel). Zmierzyć współczynnik i czas kompresji; porównać uzyskany współczynnik z wynikiem kodera LZSS bez kodowania arytmetycznego. Dla pozostałych plików z Calgary również zmierzyć współczynniki i porównać je z współczynnikami uzyskanymi dla LZSS bez kodera arytmetycznego na poprzednim laboratorium. Uzyskane wyniki oraz wnioski zawrzeć w krótkim sprawozdaniu.
Załącznik 1: Opis implementacji range coder Implementacja range codera autorstwa Michaela Schindlera (do ściągnięcia ze strony przedmiotu, lub z http://www.compressconsult.com/ ) zawiera między innymi: koder entropijny (range coder): rangecod.h rangecod.c bezpamięciowy model danych: qsmodel.h qsmodel.c przykłady użycia kodera i modeli statyczny (de)kompresor: simple_c.c simple_d.c adaptacyjny (de)kompresor (model bezpamięciowy): comp.c decomp.c adaptacyjny (de)kompresor z modelem I* rzędu: comp1.c decomp1.c Niniejszy załącznik zawiera krótki opis podstawowych elementów kodera i modelu, bardziej szczegółowa dokumentacja zawarta jest w plikach.h odpowiednich modułów i programach przykładowych. Sposób użycia kodera i dekodera czytelnie ilustruje przykład simple_c.c simple_d.c, użycie modelu we współpracy z koderem/dekoderem przykład comp.c decomp.c. Koder entropijny range coder (pliki rangecod.h rangecod.c) struct rangecoder rc; Moduł kodera wymaga zadeklarowania zmiennej stanu i przekazywania jej jako parametru do wszystkich funkcji modułu. void start_encoding(rangecoder *rc, char c, int initlength); inicjalizacja kodera. c jest pierwszym bajtem (nagłówkiem) skompresowanych danych (ze względów implementacyjnych konieczne jest wyprowadzenie jednego bajta, o dowolnej wartości, przed rozpoczęciem wyprowadzania skompresowanych danych). initlength to liczba już wyprowadzonych bajtów, (0 gdy nie reinicjalizujemy kodera). void encode_freq(rangecoder *rc, freq sy_f, freq lt_f, freq tot_f); zakoduj symbol s, sy_f to liczba wystąpień symbolu s, lt_f to łączna liczba wystąpień symboli mniejszych od s, tot_f to łączna liczba wystąpień wszystkich symboli alfabetu. Uwaga: domyślnym strumieniem do wyprowadzania skompresowanych danych jest stdout (a wejściem dla dekompresji stdin), aby kompresować do pliku należy albo przekierować stdin/stdout (za pomocą funkcji freopen() i setmode(), tak jak to pokazano w przykładzie simple_c.c), albo przedefiniować makra zdefiniowane w rangecod.c, za pomocą których odbywa się całe i/o range codera: #define outbyte(cod,x) putchar(x) #define inbyte(cod) getchar() void encode_shift(rangecoder *rc, freq sy_f, freq lt_f, freq shift); szybsza wersja funkcji encode_freq, do zastosowania, gdy łączna liczba wystąpień wszystkich symboli alfabetu jest potęgą dwójki i wynosi 2 shift. #define encode_byte(rc,b)... #define encode_short(rc,s)... zapis bajta b lub liczby shortint s do strumienia skompresowanych danych (na odpowiednio około 8 i około 16 bitach).
Uwaga: do strumienia, do którego zapisuje range coder nie należy nic wyprowadzać z pominięciem kodera, i stąd te makra. uint4 done_encoding(rangecoder *rc ); zakończenie pracy kodera, wyprowadzenie wszystkich jeszcze nie zapisanych danych. Funkcja zwraca liczbę bajtów wyprowadzonych od inicjalizacji kodera. int start_decoding(rangecoder *rc ); inicjalizacja dekodowania, zwraca EOF w razie błędu, lub nagłówek zakodowanych danych (parametr c ze start_encoding). freq decode_culfreq(rangecoder *rc, freq tot_f ); zwróć łączną liczbę wystąpień symboli mniejszych od tego, który właśnie dekodujemy (dokładniej wartość jej równą lub nieistotnie mniejszą). tot_f to łączna liczba wystąpień wszystkich symboli alfabetu. Dekodowanie symbolu odbywa się w dwóch krokach: na podstawie zwróconej liczby i modelu wyznaczamy, jakiemu symbolowi odpowiada ta liczba, po czym należy uaktualnić stan dekodera wywołując poniższą funkcję. void decode_update(rangecoder *rc, freq sy_f, freq lt_f, freq tot_f); aktualizacja dekodera po zdekodowaniu symbolu, parametry jak w funkcji encode_freq. freq decode_culshift( rangecoder *ac, freq shift ); #define decode_update_shift(rc,f1,f2,shift)... szybsze wersje funkcji decode_culfreq() i decode_update() do zastosowania, gdy łączna liczba wystąpień wszystkich symboli alfabetu jest potęgą dwójki i wynosi 2 shift. unsigned char decode_byte(rangecoder *rc); unsigned short decode_short(rangecoder *rc); odczytanie ze srtumienia skompresowanych danych odpowiednio bajta lub liczby shortint, po odczytaniu nie wywołujemy decode_update. void done_decoding( rangecoder *rc ); zakończenie pracy dekodera. Proste przykłady użycia powyższych funkcji można znaleźć w programach simple_c.c i simple_d.c. Model danych dla range codera (pliki qsmodel.h qsmodel.c) struct qsmodel m; model bezpamięciowy (zawiera m. in. tablice liczników wystąpień symboli), jest argumentem funkcji operujących na modelu, aby zbudować model wyższego rzędu należy utworzy kolekcję modeli bezpamięciowych (patrz comp1.c decomp1.c). void initqsmodel(qsmodel *m, int n, int lg_totf, int rescale, int *init, int compress); inicjalizacja modelu m, n to liczba symboli alfabetu,
lg_totf łączna liczba wystąpień wszystkich symboli alfabetu zwracana przez model na użytek kodera arytmetycznego będzie zawsze wynosiła 2 lg_totf (tzn. inne liczniki będą skalowane tak, aby zachować zadaną łączną liczbę wystąpień), wartość lg_totf powinna by mniejsza od 16, np. wynosić 12, rescale określa co ile uaktualnień modelu przeprowadzać okresowe dzielenie wszystkich liczników przez 2 (faktycznie jest to wartość docelowa, na początku kompresji dzielenie liczników wykonywane jest częściej), wartość rescale powinna być mniejsza od 2 lg_totf+1. Od wartości rescale zależy szybkość adaptacji modelu, init to tablica początkowych wartości liczników symboli lub NULL (wtedy inicjalizacja w sposób domyślny, wszystkie symbole na początku mają takie same wartości liczników), compress wartośc 1 gdy model używamy w kompresji, 0 dla dekompresji. void resetqsmodel(qsmodel *m, int *init); reinicjalizacja modelu. void deleteqsmodel(qsmodel *m ); zwolnienie pamięci przydzielonej dla modelu w initqsmodel(). void qsgetfreq(qsmodel *m, int sym, int *sy_f, int *lt_f ); dla symbolu sym wyznacz liczbę wystąpień tego symbolu (sy_f) oraz łączną liczbę wystąpień symboli mniejszych od sym (lt_f). Uwaga: łączna liczba wystąpień wszystkich symboli alfabetu to 2 lg_totf, gdzie lg_totf jest zadane podczas inicjalizacji modelu. int qsgetsym( qsmodel *m, int lt_f); wyznacz symbol znając łączną liczbę wystąpień symboli mniejszych od niego (lt_f). void qsupdate(qsmodel *m, int sym); aktualizacja modelu, zarejestrowanie kolejnego wystąpienia symbolu sym. Uwaga: model jest skonstruowany w taki sposób, że wyznaczona przez niego łączna liczba wystąpień wszystkich symboli alfabetu jest potęgą dwójki już od momentu inicjalizacji modelu, dzięki czemu możemy korzystać z szybszych wersji funkcji kodowania/dekodowania range codera. Jednak, aby powyższy warunek spełnić i dodatkowo, aby model działał szybko, operacja qsupdate() nie zawsze prowadzi do natychmiastowej aktualizacji struktur, na podstawie których wyznaczane są liczby wystąpień symboli. Zamiast tego, nowe wystąpienia są zliczane w dodatkowej tablicy, z której co jakiś czas są hurtem wprowadzane do właściwej tablicy liczników. Zatem faktycznie aktualizacja modelu nastąpi dla każdego symbolu z którym wywołana jest funkcja qsupdate(), jednak dla niektórych symboli z pewnym opóźnieniem. Przykłady użycia powyższych funkcji i wykorzystania ich do modelowania dla kompresji z użyciem range codera można znaleźć w programach comp.c decomp.c (model bezpamięciowy) oraz comp1.c decomp1.c (model kontekstowy I rzędu). * Istnieją różne definicje rzędu modelu; tutaj przyjmujemy, że rząd modelu równy jest liczbie symboli poprzedzających dany (długości kontekstu tego symbolu) branych pod uwagę przy wyznaczaniu prawdopodobieństwa dla tego symbolu. Zatem budując model rzędu k zakładamy, że ciąg symboli generowanych przez źródło jest dyskretnym łańcuchem Markowa rzędu k.
Załącznik 2: Przydatne funkcje Funkcja sufit(log 2 (val)) int ceil_log_2(int val) /* ceil(log_2(val)) */ { int result; assert(val>0); } if (val==1) return 0; result=1; val-=1; while (val>>=1) result++; return result; Zapis/odczyt liczb n-bitowych range coderem Zapis n-bitowej liczby b do strumienia skompresowanych danych, 0 < n 16. void encode_n_bits(rangecoder *rc, int b, int n) { assert(n>0 && n<=16); assert(b>=0 && b<(1<<n)); } encode_shift(rc,(freq)1,(freq)b,(freq)n); Odczyt liczby n-bitowej ze strumienia skompresowanych danych, 0 < n 16. unsigned short decode_n_bits(rangecoder *rc, int n) { unsigned short tmp; assert(n>0 && n<=16); } tmp = decode_culshift(rc,n); decode_update_shift(rc,1,tmp,n); return tmp;