Kodowanie arytmetyczne, range coder
Transkrypt
Kodowanie arytmetyczne, range coder
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ć ⎡ log2(offset znalezionego ciągu) ⎤, (funkcja ⎡ log2(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 ⎡ log2(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 .. 2n – 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 2shift. #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 2shift. 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) — 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). struct qsmodel m; 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 2lg_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 2lg_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 2lg_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(log2(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; }