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;
}