Kompresja falkowa w środowisku NVIDIA CUDA

Podobne dokumenty
Założenia i obszar zastosowań. JPEG - algorytm kodowania obrazu. Geneza algorytmu KOMPRESJA OBRAZÓW STATYCZNYCH - ALGORYTM JPEG

2. Próbkowanie Sygnały okresowe (16). Trygonometryczny szereg Fouriera (17). Częstotliwość Nyquista (20).

Kodowanie transformacyjne. Plan 1. Zasada 2. Rodzaje transformacji 3. Standard JPEG

mgr inż. Grzegorz Kraszewski SYSTEMY MULTIMEDIALNE wykład 4, strona 1. GOLOMBA I RICE'A

Kompresja Kodowanie arytmetyczne. Dariusz Sobczuk

Według raportu ISO z 1988 roku algorytm JPEG składa się z następujących kroków: 0.5, = V i, j. /Q i, j

Kodowanie i kompresja Streszczenie Studia Licencjackie Wykład 11,

Kompresja danych DKDA (7)

Praca dyplomowa magisterska

Transformata Fouriera

Programowanie Współbieżne

dr inż. Jarosław Forenc

Podstawy Informatyki Systemy sterowane przepływem argumentów

Przedmowa 11 Ważniejsze oznaczenia 14 Spis skrótów i akronimów 15 Wstęp 21 W.1. Obraz naturalny i cyfrowe przetwarzanie obrazów 21 W.2.

Programowanie procesorów graficznych GPGPU

Cyfrowe przetwarzanie i kompresja danych

Kodowanie podpasmowe. Plan 1. Zasada 2. Filtry cyfrowe 3. Podstawowy algorytm 4. Zastosowania

Kwantyzacja wektorowa. Kodowanie różnicowe.

Kompresja dźwięku w standardzie MPEG-1

Temat: Algorytm kompresji plików metodą Huffmana

Teoria przetwarzania A/C i C/A.

Metody numeryczne Technika obliczeniowa i symulacyjna Sem. 2, EiT, 2014/2015

Wydajność systemów a organizacja pamięci, czyli dlaczego jednak nie jest aż tak źle. Krzysztof Banaś, Obliczenia wysokiej wydajności.

Fundamentals of Data Compression

Przetwarzanie i transmisja danych multimedialnych. Wykład 7 Transformaty i kodowanie. Przemysław Sękalski.

Algorytm. a programowanie -

Porównanie wydajności CUDA i OpenCL na przykładzie równoległego algorytmu wyznaczania wartości funkcji celu dla problemu gniazdowego

Technika audio część 2

KOMPRESJA OBRAZÓW STATYCZNYCH - ALGORYTM JPEG

0 + 0 = 0, = 1, = 1, = 0.

ZADANIE 1. Rozwiązanie:

LABORATORIUM PROCESORY SYGNAŁOWE W AUTOMATYCE PRZEMYSŁOWEJ. Zasady arytmetyki stałoprzecinkowej oraz operacji arytmetycznych w formatach Q

Kodowanie i kompresja Streszczenie Studia dzienne Wykład 9,

Programowanie kart graficznych

Wybrane metody kompresji obrazów

5. Rozwiązywanie układów równań liniowych

Budowa Mikrokomputera

Python: JPEG. Zadanie. 1. Wczytanie obrazka

Podstawy programowania. Wykład Funkcje. Krzysztof Banaś Podstawy programowania 1

Spis treści. I. Skuteczne. Od autora... Obliczenia inżynierskie i naukowe... Ostrzeżenia...XVII

Podstawy informatyki. Informatyka stosowana - studia niestacjonarne. Grzegorz Smyk

ARYTMETYKA BINARNA. Dziesiątkowy system pozycyjny nie jest jedynym sposobem kodowania liczb z jakim mamy na co dzień do czynienia.

teoria informacji Kanały komunikacyjne, kody korygujące Mariusz Różycki 25 sierpnia 2015

LABORATORIUM AKUSTYKI MUZYCZNEJ. Ćw. nr 12. Analiza falkowa dźwięków instrumentów muzycznych. 1. PODSTAWY TEORETYCZNE ANALIZY FALKOWEJ.

INFORMATYKA W SZKOLE. Podyplomowe Studia Pedagogiczne. Dr inż. Grażyna KRUPIŃSKA. D-10 pokój 227

Przekształcenia widmowe Transformata Fouriera. Adam Wojciechowski

Przygotowanie kilku wersji kodu zgodnie z wymogami wersji zadania,

Wprowadzenie do programowania w środowisku CUDA. Środowisko CUDA

Transformaty. Kodowanie transformujace

CUDA Median Filter filtr medianowy wykorzystujący bibliotekę CUDA sprawozdanie z projektu

Rozpoznawanie obrazu. Teraz opiszemy jak działa robot.

Niezwykłe tablice Poznane typy danych pozwalają przechowywać pojedyncze liczby. Dzięki tablicom zgromadzimy wiele wartości w jednym miejscu.

Programowanie procesorów graficznych NVIDIA (rdzenie CUDA) Wykład nr 1

BIBLIOTEKA PROGRAMU R - BIOPS. Narzędzia Informatyczne w Badaniach Naukowych Katarzyna Bernat

Kody Tunstalla. Kodowanie arytmetyczne

Kodowanie i kompresja Tomasz Jurdziński Studia Wieczorowe Wykład Kody liniowe - kodowanie w oparciu o macierz parzystości

6. Algorytmy ochrony przed zagłodzeniem dla systemów Linux i Windows NT.

Cyfrowe przetwarzanie i kompresja danych. dr inż.. Wojciech Zając

Analiza ilościowa w przetwarzaniu równoległym

Systemy liczbowe. 1. Przedstawić w postaci sumy wag poszczególnych cyfr liczbę rzeczywistą R = (10).

przetworzonego sygnału

2 Arytmetyka. d r 2 r + d r 1 2 r 1...d d 0 2 0,

Stan wysoki (H) i stan niski (L)

Dla człowieka naturalnym sposobem liczenia jest korzystanie z systemu dziesiętnego, dla komputera natomiast korzystanie z zapisu dwójkowego

Skalowalność obliczeń równoległych. Krzysztof Banaś Obliczenia Wysokiej Wydajności 1

Definicje. Algorytm to:

PAMIĘCI. Część 1. Przygotował: Ryszard Kijanka

Kod uzupełnień do dwóch jest najczęściej stosowanym systemem zapisu liczb ujemnych wśród systemów binarnych.

Samodzielnie wykonaj następujące operacje: 13 / 2 = 30 / 5 = 73 / 15 = 15 / 23 = 13 % 2 = 30 % 5 = 73 % 15 = 15 % 23 =

Kodowanie transformujace. Kompresja danych. Tomasz Jurdziński. Wykład 11: Transformaty i JPEG

Programowanie współbieżne Wykład 2. Iwona Kochańska

Przetwornik analogowo-cyfrowy

12. Wprowadzenie Sygnały techniki cyfrowej Systemy liczbowe. Matematyka: Elektronika:

PROGRAMOWANIE WSPÓŁCZESNYCH ARCHITEKTUR KOMPUTEROWYCH DR INŻ. KRZYSZTOF ROJEK

Metody numeryczne Wykład 4

Kodowanie informacji

Wydajność obliczeń a architektura procesorów. Krzysztof Banaś Obliczenia Wysokiej Wydajności 1

Wprowadzenie do architektury komputerów systemy liczbowe, operacje arytmetyczne i logiczne

Numeryczna algebra liniowa

Aproksymacja funkcji a regresja symboliczna

Transformacja Fouriera i biblioteka CUFFT 3.0

Budowa komputera Komputer computer computare

Zastosowanie kompresji w kryptografii Piotr Piotrowski

Obliczenia iteracyjne

Budowa i zasada działania komputera. dr Artur Bartoszewski

Język C zajęcia nr 11. Funkcje

Lab 9 Podstawy Programowania

Wskaźniki a tablice Wskaźniki i tablice są ze sobą w języku C++ ściśle związane. Aby się o tym przekonać wykonajmy cwiczenie.

Metody optymalizacji soft-procesorów NIOS

SIMR 2016/2017, Analiza 2, wykład 1, Przestrzeń wektorowa

FFT i dyskretny splot. Aplikacje w DSP

Zastosowanie falek w przetwarzaniu obrazów

3. Opracować program kodowania/dekodowania pliku tekstowego. Algorytm kodowania:

Arytmetyka komputera. Na podstawie podręcznika Urządzenia techniki komputerowej Tomasza Marciniuka. Opracował: Kamil Kowalski klasa III TI

Mathcad c.d. - Macierze, wykresy 3D, rozwiązywanie równań, pochodne i całki, animacje

1. Nagłówek funkcji: int funkcja(void); wskazuje na to, że ta funkcja. 2. Schemat blokowy przedstawia algorytm obliczania

Wstęp do informatyki- wykład 2

Przetwarzanie Sygnałów. Zastosowanie Transformaty Falkowej w nadzorowaniu

Klasyfikacja metod kompresji

JĘZYKI PROGRAMOWANIA Z PROGRAMOWANIEM OBIEKTOWYM. Wykład 6

Transkrypt:

Politechnika Warszawska Wydział Elektroniki i Technik Informacyjnych Instytut Informatyki Rok akademicki 2010/2011 Praca dyplomowa inżynierska Stanisław Ogórkis Kompresja falkowa w środowisku NVIDIA CUDA Opiekun pracy: mgr inż. Julian Myrcha Ocena....................................................................................................... Podpis Przewodniczącego Komisji Egzaminu Dyplomowego

Specjalność: Informatyka Inżynieria systemów informatycznych Data urodzenia: 15 lutego 1988 r. Data rozpoczęcia studiów: 1 października 2007 r. Życiorys Nazywam się Stanisław Ogórkis i urodziłem się 15 lutego 1988 r. w Suwałkach. Po ukończeniu gimnazjum, kontynuowałem naukę w I L.O. im. Marii Konopnickiej w Suwałkach. W szkole średniej uczęszczałem do klasy o profilu matematyczono-fizycznym. W październiku 2007 r. rozpocząłem studia na Wydziale Elektroniki i Technik Informacyjnych Politechniki Warszawskiej..................................... podpis studenta Egzamin dyplomowy Złożył egzamin dyplomowy w dn...................................................... Z wynikiem.......................................................................... Ogólny wynik studiów................................................................ Dodatkowe wnioski i uwagi Komisji..........................................................................................................................................

Streszczenie Głównym celem pracy jest zaimplementowanie kompresora obrazów i sekwencji wideo wykorzystującego transformatę falkową. Ponadto wykorzystana jest technologia NVIDIA CUDA pozwalająca na wykonywanie obliczeń na procesorze karty graficznej. Zaprezentowane są algorytmy związane z transformatą falkową, kwantyzacją i kodowaniem arytmetycznym. W pracy przedstawione są podstawy platformy NVIDIA CUDA. Szczególny nacisk położony jest na możliwości realizacji części algorytmów na procesorze graficznym. Wykonany koder jest zweryfikowany pod kątem efektywności implementacji GPU. Słowa kluczowe: transformata falkowa, falki, kompresja stratna, kwantyzacja, cuda. Abstract Title: Wavelet compression in NVIDIA CUDA. The main goal of this thesis is to implement image and video sequence wavelet compressor. The compressor is implemented using NVIDA CUDA technology which allows computing on GPU. The algorithms for wavelet transform, quantization and arithmetic coding are presented. The thesis describes basics of the NVIDA CUDA architecture. The particular emphasis is placed on the feasibility of the algorithms on the GPU. Created encoder is verified for GPU implementation effectiveness. Key words: wavelet transform, wavelets, lossy compression, quantization, cuda.

Spis treści 1 Wstęp...................................... 3 1.1 Zdefiniowanie problemu........................ 3 1.2 Cele pracy................................ 4 1.3 Zawartość pracy............................. 4 2 Transformata falkowa.............................. 5 2.1 Informacje ogólne............................ 5 2.2 Funkcja falkowa............................. 5 2.3 Realizacja dyskretnej transformaty falkowej przy użyciu filtrów... 6 2.4 Przykład obliczenia transformaty i transformaty odwrotnej..... 7 2.5 Współczynniki filtrów falkowych.................... 9 2.6 Transformata sygnałów wielowymiarowych.............. 11 2.7 Algorytm konwolucji.......................... 12 3 Metody kwantyzacji............................... 14 3.1 Skalarna kwantyzacja równomierna.................. 14 3.2 Skalarna kwantyzacja nierównomierna................ 14 3.3 Skalarna kwantyzacja równomierna z rozszerzonym przedziałem zerowym.................................. 15 4 Kodowanie arytmetyczne............................ 17 4.1 Kodowanie arytmetyczne oparte o arytmetykę rzeczywistoliczbową. 17 4.2 Przykład................................. 18 4.3 Kodowanie arytmetyczne oparte o arytmetykę całkowitoliczbową.. 19 4.4 Koder adaptacyjny........................... 21 5 Technologia NVIDIA CUDA.......................... 22 5.1 Efektywność obliczeniowa procesorów graficznych.......... 22 5.2 Model programowania......................... 24 5.3 Funkcje kerneli............................. 24 5.4 Hierarchia wątków........................... 25 5.5 Hierarchia pamięci........................... 26 6 Opis proponowanego rozwiązania....................... 29 6.1 Wymagania dotyczące aplikacji.................... 29 6.2 Funkcje aplikacji............................ 29 1

6.3 Architektura rozwiązania........................ 31 7 Implementacja projektu............................ 39 7.1 Platforma sprzętowa.......................... 39 7.2 Narzędzia i biblioteki.......................... 40 7.3 Algorytm obliczania transformaty falkowej na procesorze GPU... 41 7.4 Mechanizmy pomiaru czasu...................... 44 8 Weryfikacja................................... 46 8.1 Zbiór testowy.............................. 46 8.2 Precyzja obliczeń............................ 48 8.3 Efektywność kompresji......................... 48 8.4 Szybkość działania........................... 53 9 Podsumowanie................................. 59 2

1 Wstęp 1.1 Zdefiniowanie problemu Uzyskanie możliwie najkrótszej reprezentacji danych jest głównym kryterium stosowanym przy ocenie algorytmów kompresji. Wraz z rozwojem technik multimedialnych oraz wykorzystania komputerów w medycynie oraz inżynierii, wzrosło zapotrzebowanie na skuteczne kompresowanie obrazów i sekwencji wideo. Metody bezstratne umożliwiają dla typowych obrazów uzyskiwanie współczynników kompresji do około 4 : 1. Aby uzyskać krótszą reprezentację wykorzystujemy algorytmy, które nie gwarantują jednakowego odtworzenia skompresowanych danych. Wówczas poza kryterium stopnia kompresji i szybkości działania ważna staje się jakość odtworzonych danych. Przez lata powstał szereg metod stratnej kompresji danych. Ich główna idea opiera się na odrzuceniu tych informacji, które są najmniej istotne dla odbiorcy. Dzięki temu w kolejnych krokach możemy skuteczniej kompresować dane przy użyciu standardowych algorytmów kompresji bezstratnej. Powszechnie stosowane algorytmy wykorzystują transformaty, które zmieniają dziedzinę danych na taką gdzie energia sygnału koncentruje się w mniejszej liczbie współczynników niż ma to miejsce w pierwotnej reprezentacji [2] [10]. Na przykład, po zastosowaniu transformaty kosinusowej dla bloków 8x8 obrazu w standarcie JPEG, energia sygnału kumuluje się lewej górnej części macierzy współczynników. Kwantyzacja uzyskanych współczynników jest krokiem, który decyduje o jakości odtworzonych danych. W tym kroku selektywnie rezygnujemy z części danych, aby uzyskać uproszczoną reprezentację, która lepiej poddaje się kompresji bezstratnej. Najnowsze algorytmy kompresji (JPEG2000, REDCODE) jako transformatę wykorzystują funkcje falkowe. Pierwszą funkcję falkową opisał na początku XX wieku Haar, ale wstępne próby ich zastosowania do kompresji pojawiły się dopiero po pracach Ingrid Daubechies [4] oraz Stéphane Mallat [9]. Transformata falkowa umożliwia wielokrotnie powtarzaną dekompozycję danych na składowe wysoko i niskoczęstotliwościowe. Dzięki temu na niewielkiej liczbie współczynników możemy skoncentrować większość energii sygnału związanej z pasmem niskoczęstotliwościowym. Równie ważną cechą jest separowalność funkcji falkowych, która umożliwia liczenie transformat wielowymiarowych przez odpowiednie złożenie transformat jednowymiarowych. Wyznaczenie współczynników transformaty dla przypadku danych więcej niż dwuwymiarowych wymaga znacznych nakładów obliczeniowych. Przy ciągłym rozwoju grafiki komputerowej, współczesne akceleratory graficzne posiadające setki rdzeni uzyskały większą moc obliczeniową niż jednostki centralne. Udostępnione na początku 2007 roku API NVIDIA CUDA umożliwia programowanie karty graficznej do wykonywania obliczeń (GPU Computing). Kompresja falkowa bardzo dobrze nadaje się do implementacji na GPU dzięki możliwości zrównoleglenia obliczeń. 3

1.2 Cele pracy Celem niniejszej pracy jest realizacja kompresji obrazów oraz sekwencji wideo przy użyciu metod falkowych. Szczególny nacisk został położony na porównanie zwykłej implementacji z implementacją bazującą na równoległych obliczeniach zrealizowanych na procesorze karty graficznej. Sprawdzona została również efektywność kompresji różnych algorytmów kwantyzacji i funkcji falkowych. Praca zawiera kompletny opis algorytmów wykorzystanych w poszczególnych krokach kompresji. Przedstawiłem też kilka przykładów pozwalających na zrozumienie działania poszczególnych metod. Umieściłem również krótki opis technologii NVIDIA CUDA. Dzięki temu praca może służyć jako materiał dydaktyczny dla osób chcących zapoznać się z zastosowaniem metod falkowych w kompresji, jak również z wykonywaniem obliczeń na GPU. 1.3 Zawartość pracy Rozdział drugi zawiera wstęp teoretyczny dotyczący transformaty falkowej. Przedstawiłem w nim algorytmy obliczenia transformaty wraz z przykładem. W rozdziale trzecim przedstawione są różne metody kwantyzacji skalarnej. Rozdział czwarty opisuje metodę kompresji bezstratnej jaką jest kodowanie arytmetyczne i zawiera przykład kodowania i dekodowania wybranego ciągu. Rozdział piąty jest w całości poświęcony technologii NVIDIA CUDA, skupiając się na elementach wykorzystanych w implementacji. W rozdziale szóstym znajduje się opis proponowanego rozwiązania, a w rozdziale siódmym opis implementacji. Rozdział ósmy przedstawia wyniki testów wykonanej aplikacji. Ostatni, dziewiąty rozdział zawiera podsumowanie całej pracy. 4

2 Transformata falkowa 2.1 Informacje ogólne Transformaty wykorzystywane w przetwarzaniu sygnałów wykorzystują iloczyn skalarny badanego sygnału przez część nazywaną jądrem przekształcenia. Wykonanie takiego działania na sygnale pozwala uzyskać jego reprezentację w nowej dziedzinie, która może być korzystniejsza dla jego reprezentacji bądź analizy. Transformata falkowa, podobnie jak transformata Fouriera należy do przekształceń całkowych. Podstawowy wzór opisujący transformatę falkową funkcji ciągłej x(t) ma postać: X(a, b) = 1 a ( ) t b x(t)ψ dt (1) a gdzie współczynnik a to czynnik skalujący, b to przesunięcie, ψ to sprzężenie zespolone falki bazowej nazywanej falką matką (mother wavelet). Wykorzystując współczynniki a i b jesteśmy w stanie wygenerować całą rodzinę falek umożliwiając reprezentację badanej funkcji na różnym poziomie szczegółowości. Do odtworzenia pierwotnego ciągłego sygnału wykorzystujemy transformatę odwrotną o postaci: x(t) = 0 gdzie ψ jest funkcją odwrotną do ψ. 2.2 Funkcja falkowa 1 a 2 X(a, b) 1 a ψ ( ) t b dbda (2) a W najbardziej znanej transformacie Fouriera jako jądro wykorzystywane są funkcje sinusoidalne. Transformata falkowa nie ma ściśle określonej funkcji jądra [1]. Może być nią dowolna funkcja spełniająca następujące warunki: 1. Jej wartość średnia ma mieć wartość 0 ψ(x)dx = 0 2. Musi mieć skończone pasmo przenoszenia, tj. poza określonym zakresem jej wartość jest równa 0 3. Musi mieć odpowiadającą funkcję skalującą ϕ(t) 5

2.3 Realizacja dyskretnej transformaty falkowej przy użyciu filtrów Na potrzeby cyfrowego przetwarzania sygnałów wykorzystywana jest dyskretna transformata falkowa (DWT - Discrete Wavelet Transform) [2] [10] [19]. Mnożenie we wzorach 1 i 2 przekłada się na obliczenie splotu ciągów, które w dziedzinie cyfrowego przetwarzania sygnałów wyrażane są przez operację filtracji. Aby efektywnie zaimplementować transformatę falkową należy najpierw wyznaczyć zestawy filtrów. Dzięki właściwościom funkcji falkowych możemy wygenerować całą rodzinę funkcji falkowych do reprezentacji sygnału na różnym poziomie szczegółowości. Dla danej funkcji falkowej należy wyliczyć współczynniki filtru górno i dolnoprzepustowego (oznaczane odpowiednio H 0 i H 1 ) oraz filtrów syntezy (F 0 i F 1 ). Sygnał poddawany jest filtracji i uzyskujemy na wyjściu filtra dolno przepustowego składowe niskoczęstotliwościowe (y L ), a na wyjściu filtru górno przepustowego składowe wysokoczęstotliwościowe (y H ). y L [n] = (x H 1 )[n] y H [n] = (x H 0 )[n] (3) W wyniku tego działania dostajemy jednak dwa wektory współczynników o długości równej wektorowi wejściowemu. Tą nadmiarowość redukujemy usuwając co drugą próbkę z tych wektorów (symbolicznie oznaczane jest to przez 2) i mnożąc przez 2. Sygnał poddany takiej filtracji podzielony jest na dwa pasma i aby wrócić do jego poprzedniej postaci należy najpierw przywrócić pierwotną ilość próbek. Wykonuje się to poprzez wstawienie zer w miejsca poprzednio usuniętych wartości (oznaczane przez 2). Jako że próbki nieusunięte zostały pomnożone przez 2 moc sygnału jest zachowana. Tak otrzymane wartości poddawane są filtracji odwrotnej przez bank filtrów F 0 i F 1 i sumowane. Rysunek 1: Bank filtrów analizy i syntezy Na rysunku 1 przedstawiony został bank filtrów realizujący dekompozycję, a następnie syntezę danego sygnału x. W pracach [1], [19] zostały szczegółowo podane wyprowadzenia 6

wzorów jakie mają spełniać zestawy filtrów H 0 H 1 F 0 F 1, aby sygnał po przejściu przez nie został identycznie zrekonstruowany. Mają one postać: H 0 (z)f 0 (z) + H 1 (z)f 1 (z) = 2 H 0 ( z)f 0 (z) + H 1 ( z)f 1 (z) = 0 Wyznaczając odpowiednie wartości współczynników filtrów jesteśmy w stanie efektywnie implementować algorytmy transformaty falkowej. Dekompozycja sygnału może być kontynuowana w podpaśmie najniżej częstotliwości. Na rysunku 2 przedstawiono trzypoziomowy schemat dekompozycji wraz z podziałem pasma częstotliwości. Obliczanie transformaty dla poziomu drugiego odbywa się w dolnym paśmie częstotliwości, który stanowi połowę długości wejściowego sygnału. Rysunek 2: Podział pasma częstotliwości w banku filtrów analizy Współczynniki transformaty falkowej z poszczególnych poziomów dekompozycji pokrywają całe spektrum częstotliwości. Nie tracimy więc żadnej informacji w wyniku wykonania tego przekształcenia. Współczynniki zwrócone przez filtr górnoprzepustowy nazywamy współczynnikami detali, a współczynniki zwrócone przez filtr dolnoprzepustowy współczynnikami uśrednionymi. 2.4 Przykład obliczenia transformaty i transformaty odwrotnej Obliczanie transformaty oraz transformaty odwrotnej zostanie przedstawione przy wykorzystaniu falki Haara. Transformacie zostanie poddany ośmioelementowy ciąg liczbowy. Wykonana zostanie trzypoziomowa dekompozycja sygnału wejściowego. Najpierw przedstawię postać falki Haara. 7

Falka Haara Falka Haara jest najstarszą i najprostszą funkcją falkową. Wartość średnia funkcji ψ(x) wynosi 0. Ma ona również ograniczony nośnik (poza określonym zakresem jej wartość jest równa 0) i posiada odpowiednią funkcję skalującą. Spełnione są więc wszystkie warunki stawiane funkcji falkowej. Podstawowy wzór opisujący falkę Haara ma postać: 1 dla 0 t < 1/2 ψ(x) = 1 dla 1 t < 1 2 0 dla pozostałych t a funkcję skalującą ϕ(t): 0 dla 0 t < 1 ϕ(t) = 1 dla pozostałych t Rysunek 3: Falka Haara Obliczenie współczynników Załóżmy, że dany mamy ośmio elementowy ciąg: 4 2 5 5 7 5 1 3 W pierwszym kroku obliczymy współczynniki uśrednione zmniejszając jednocześnie ich liczbę o dwa (operator 2). Dla ośmio elementowego ciągu potrzebować będziemy więc czterech współczynników uśrednionych. Obliczenie średniej wykonujemy parami. Pierwszy współczynnik ma wartość 3 ((4+2)/2), drugi 5 ((5+5)/2), itd. Współczynniki uśrednione wynoszą: 8

3 5 6 2 Współczynniki detali w dziedzinie Haara zawierają utraconą w wyniku uśrednienia informację. Pierwszy współczynnik ma wartość 1, ponieważ współczynnik uśredniony (3) jest o 1 mniejszy od współczynnika 4 i o 1 większy od współczynnika 2. Widzimy więc, że z jednego współczynnika uśrednionego i jednego detali jesteśmy w stanie zrekonstruować dwie wartości wejściowe. Nie tracimy zatem w wyniku tego przekształcenia żadnej informacji. Współczynniki detali wynoszą: 1 0 1 1 Obliczanie transformaty będziemy kontynuować w dolnym podpaśmie częstotliwości, czyli na obliczonych współczynnikach detali. W poniższej tabeli znajdują się wyniki pełnej dekompozycji sygnału: Poziom Współczynniki uśrednione Współczynniki detali 1 [3 5 6 2] [1 0 1 1] 2 [4 4 ] [ 1 2] 3 [4] [0] W przedstawiony sposób obliczyliśmy współczynniki transformaty falkowej przy wykorzystaniu banku filtrów. Rekonstrukcja wejściowego ciągu polega na rekursywnym dodawaniu i odejmowaniu współczynników detali do współczynników uśrednionych na poszczególnych poziomach dekompozycji. Ostatecznie obliczone współczynniki mają wartości: 4 0 1 2 1 0 1 1 Warto od razu zauważyć korzyść wynikającą z przejścia do bazy współczynników Haara. Energia sygnału kumuluje się w współczynnikach pasma dolnoprzepustowego, a współczynniki detali mają bardzo małą amplitudę i ich wartości są bliskie zeru. Wiele z tych współczynników można przybliżyć do zera wprowadzając jedynie niewielki błąd przy rekonstrukcji. Tak przedstawiony ciąg lepiej poddaje sie kompresji przy użyciu koderów entropijnych. 2.5 Współczynniki filtrów falkowych W przedstawionym wcześniej przykładzie użyłem najprostszej falki, czyli falki Haara. Do kompresji sygnałów wykorzystuje się zazwyczaj bardziej skomplikowane funkcje falkowe umożliwiające uzyskanie mniejszych zniekształceń po wykonaniu kwantyzacji współczynników. Filtry mają wówczas większą długość przez co zwiększa się koszt obliczenia pojedynczego współczynnika. 9

W mojej aplikacji wykorzystałem filtry ortogonalne Haar (haar) oraz Daubechies o długościach 4 i 6 (daub4 i daub6 ). Filtry ortogonalne Daubechies o różnych długościach konstruuje się według specjalnego wzoru przedstawionego m.in w pracy [4]. Filtr o długości 2 sprowadza się do filtru Haara. Filtry ortogonalne cechują się m.in. tym, że współczynniki banków analizy i syntezy są identyczne. Zaimplementowałem również filtry biortogonalne CDF97 (Cohen-Daubechies- Feauveau) oraz antonini (z pracy [10]). Filtry biortogonalne mają znaczenie lepsze cechy jeśli chodzi o kompresję. Filtr CDF97 jest wykorzystywany m.in. w standardzie JPEG2000. Filtry biortogonalne zapewniają rekonstrukcję sygnału. Banki filtrów analizy i syntezy nie mają jednak identycznych współczynników. W tabeli 1 znajdują się współczynniki banków filtrów dla falek ortogonalnych Haar, Daub4 i Daub6, natomiast w tabelach 2 i 3 współczynniki dla falki CDF97 i antonini. Ortogonalność zapewnia to, że do opisania falki wystarczą dwa zestawy współczynników. W przypadku falek biortogonalnych potrzeba czterech zestawów współczynników. Filtry falki CDF97 mają centrum w środkowym współczynniku (ma to znaczenie przy obliczaniu splotu z ciągiem wejściowym). Szczegółowy opis takich własności funkcji falkowych jak ortogonalność/biortogonalność, liczba momentów zanikających (vanishing moments), czy ostrość (sharpness) opisana jest w pracy [9]. Indeks współczynnika haar low haar high daub4 low daub4 high daub6 low daub6 high 0 1 1 0.6830127-0.830127 0.47046721 0.04981750 1 1-1 1.1830127-0.3169873 1.14111692 0.12083221 2 0.3169873 1.1830127 0.65036500-0.19093442 3-0.1830127-0.6830127-0.19093442-0.6503650 4-0.12083221 1.14111692 5 0.0498175-0.47046721 Tabela 1: Współczynniki filtrów falek ortogonalnych Haar, Daub4 i Daub6 Indeks współczynnika analysis low analysis high synthesis low synthesis high -4 0.026748757411 0 0 0.026748757411-3 -0.016864118443 0.091271763114-0.091271763114 0.016864118443-2 -0.078223266529-0.057543526229-0.057543526229-0.078223266529-1 0.266864118443-0.591271763114 0.591271763114-0.266864118443 0 0.602949018236 1.11508705 1.11508705 0.602949018236 1 0.266864118443-0.591271763114 0.591271763114-0.266864118443 2-0.078223266529-0.057543526229-0.057543526229-0.078223266529 3-0.016864118443 0.091271763114-0.091271763114 0.016864118443 4 0.026748757411 0 0 0.026748757411 Tabela 2: Współczynniki filtrów falki biortogonalnej CDF97 10

Indeks współczynnika analysis low analysis high synthesis low synthesis high -4 8.28284032 0 0-12.28284032-3 4.482956738-19.543487188-19.543487188-8.482956738-2 -4.007083083 9.06053045-13.06053045 2.007083083-1 9.258873244 10.364926289 10.364926289 9.258873244 0 22.178753243-22.433261231 20.433261231-24.178753243 1 9.258873244 10.364926289 10.364926289 9.258873244 2-4.007083083 9.06053045-13.06053045 2.007083083 3-8.482956738-19.543487188-19.543487188-8.482956738 4 8.28284032 0 0-12.28284032 Tabela 3: Współczynniki filtrów falki biortogonalnej antonini 2.6 Transformata sygnałów wielowymiarowych Transformatę sygnału wielowymiarowego możemy uzyskać przez odpowiednie złożenie transformat jednowymiarowych. Dla kompresji szczególne znaczenie mają takie metody które koncentrują podobne współczynniki wokół siebie. W artykule [7] opisane zostały dwie podstawowe metody dekompozycji sygnału dwuwymiarowego (obraz), aby uzyskać jak najkorzystniejsze właściwości dla kompresji. Standard JPEG2000 wykorzystuje algorytm dekompozycji Mallata, który dzieli obraz na podpasma, w których znajdują się odpowiednio składowe niskoczęstotliwościowe (tło) i wysokoczęstotliwościowe (krawędzie). Na rysunku 4 przedstawiony jest sposób dekompozycji obrazu przedstawionego jako macierz X o wymiarach n x n. Rysunek 4: Dekompozycja obrazu przy użyciu algorytmu Mallata Na wejście banku filtrów na początku podawane są wektory wierszy i dokonywana jest na nich transformata jednowymiarowa. Następnie usuwamy co drugą kolumnę. W kolejnym kroku wykonujemy analogiczne działanie na wektorach kolumn. Wynik działania takiej procedury pokazany jest na rysunku 5. Dekompozycję można kontynuować w podpaśmie A, aby jeszcze zmniejszyć ilość współczynników pasma niskoczęstotliwościowego. 11

Rysunek 5: Schematyczny sposób działania dekompozycji wielorozdzielczej obrazu: a) obraz orginalny; b) obraz poddany dekompozycji jednopoziomowej c) przykład działania na obrazie Lena; W wyniku działania tego schematu w podpaśmie A uzyskujemy obraz w niższej skali, w podpaśmie B krawędzie pionowe, w C krawędzie poziome, a w D krawędzie ukośne. Kontynuując dekompozycję jesteśmy w stanie uzyskać silnie upakowaną reprezentację sygnału. Większość współczynników ma wartości zerowe bądź bardzo bliskie zeru. Po kroku kwantyzacji uzyskamy w pasmach wysokoczęstotliwościowych bardzo długie ciągi zer które skutecznie będą poddawały się kompresji bezstratnej. Transformata falkowa bardzo dobrze nadaje się do reprezentacji typowych obrazów, które składają się głównie z obszarów tła, czyli gradientów (składowe niskoczęstotliwościowe) oraz niewielkiej ilości obszarów reprezentujących krawędzie (składowe wysokoczęstotliwościowe). 2.7 Algorytm konwolucji Obliczenie transformaty oraz transformaty odwrotnej wymaga obliczenia wyjścia filtrów analizy i syntezy. Często stosowanym rozwiązaniem jest algorytm konwolucji. Dla ciągu x i filtru h wyjście y ma postać: y[n] = (x h)[n] = + k= = x[k]h[n k] (4) Przedstawione wcześniej wzory (3) na dyskretną transformatę przyjmują postać: y L [n] = (x H 1 )[n] = y H [n] = (x H 0 )[n] = + k= + k= = x[k]h 1 [2n k] = x[k]h 0 [2n k] (5) Zastosowano tutaj od razu operator 2 wyrzucania co drugiej próbki. 1 Takie zastosowanie algorytmu konwolucji doprowadza jednak do kilku problemów. Przede wszystkim 1 czynnik H[n k] został zamieniony na H[2n k] 12

w praktycznej realizacji dysponujemy sygnałami oraz filtrami o skończonej liczbie współczynników. Moglibyśmy oczywiście uznać, że poza zadanym zakresem wartości te są równe zero (zero padding). W wyniku tego działania uzyskamy jednak wyjście filtru o długości N +K 1 (N - długość sygnału wejściowego, K - liczba współczynników filtru). Usunięcie K 1 nadmiarowych współczynników uniemożliwiłoby prawidłową rekonstrukcję sygnału. Lepszym rozwiązaniem jest periodyczne rozszerzenie sygnału na granicach (periodic extension). Na rysunku 6 pokazany został schemat rozszerzenia periodycznego. Po wykonaniu filtracji na tak roszerzonym sygnale o okresie N, otrzymamy wyjście filtru o okresie N. Wiedząc, że rozszerzaliśmy sygnał periodycznie całą informację o wyjściu filtru możemy zapisać wykorzystując N współczynników. Periodyczne rozszerzanie sygnału x o długości N wyraża zależność: x[n] dla 0 n < N x[n] = x[ n] dla n < 0 x[n n] dla n N Rysunek 6: Rozszerzanie periodyczne oraz roszerzanie symetryczne Przedstawiony schemat rozszerzania sygnału ma jednak kilka wad, które rozwiązuje rozszerzanie symetryczne (symmetric extension). Rozszerzanie periodyczne jest ograniczone do sygnałów o długości podzielnej przez dwa. Tak więc dla transformaty falkowej o D poziomach dekompozycji, sygnał musi mieć długość będącą wielokrotnością liczby 2 D. Dodatkowo na granicy sygnału pojawia się nieciągłość, która powoduje większą wartość współczynników wysokich częstotliwości, co skutkuje większym kosztem kodowania entropijnego. Rozwiązanie tych problemów zapewnia rozszerzanie symetryczne (rysunek 6). Działa na sygnałach dowolnej wielkości i usuwa nieciągłość na granicach. Ogólnym celem jest uzyskanie symetrycznego sygnału wyjściowego. Schemat rozszerzania jest jednak bardziej skomplikowany i zależny od zastosowanych banków filtrów. Szczegółowy opis działania rozszerzania symetrycznego znajduje się w pracy [3]. 13

3 Metody kwantyzacji W wyniku obliczenia transformaty falkowej otrzymujemy współczynniki o wartościach ciągłych. Aby możliwe było ich skuteczne skompresowanie koderem bezstratnym należy najpierw je skwantyzować. Krok ten jest krokiem stratnym powodującym zniekształcenia zrekonstruowanego sygnału. Dobór odpowiedniego algorytmu kwantyzacji umożliwia znaczącą redukcję alfabetu występujących symboli. Najczęściej stosowaną miarą zniekształceń spowodowanych kwantyzacją jest miara błędu średniokwadratowego MSE (ang. Mean Square Error). Dla sygnału X o długości n wyraża się ona wyrażana wzorem: MSE = 1 (x(i) x (i)) 2 (6) n i Warto zauważyć że ta miara przy jej minimalizacji dąży do globalnego zmniejszenia błędu. Korzystne więc może być uzyskanie mniejszych błędów dla dużej liczby współczynników kosztem uzyskania znacznych błędów dla niewielkiej liczby współczynników. 3.1 Skalarna kwantyzacja równomierna Podstawowym schematem kwantyzacji jest kwantyzacja równomierna (ang. Uniform Quantization). Stosuje się ją dla sygnału o wartościach pomiędzy (f min, f max ). Cały zakres jest dzielony na L równych przedziałów o długości Q nazywanych przedziałem kwantyzacji (rysunek 7): Q = (f max f min )/L (7) Wartość leżąca w danym przedziale kwantyzacji jest odwzorowywana na środkową wartość tego przedziału. W wyniku kwantyzacji uzyskujemy indeks przedziału kwantyzacji: Q i (f) = f f min Q (8) Aby przeprowadzić dekwantyzację należy znać wartości f min i f max oraz indeksy przedziałów kwantyzacji dla poszczególnych symboli: Q(f) = Q i (f)q + Q/2 + f min (9) 3.2 Skalarna kwantyzacja nierównomierna Przedstawiona wcześniej metoda kwantyzacji jest optymalna jedynie dla sygnałów o rozkładzie równomiernym. Aby zminimalizować błąd średniokwadratowy (6), należy 14

Rysunek 7: Podział na przedziały kwantyzacji w kwantyzatorze równomiernym wykorzystać schemat, w którym przedziały kwantyzacji nie są równej wielkości. Tam gdzie występuje skupienie wartości należy zmniejszyć przedziały kwantyzacji (rysunek 8). Rysunek 8: Podział na przedziały kwantyzacji w kwantyzatorze nierównomiernym Nieliniowy charakter przekształcenia wymaga, aby przesłać informację o optymalnych przedziałach kwantyzacji do dekodera. Takie rozwiązanie zwiększa ilość potrzebnych bitów, więc nie jest stosowane w kompresji. Wykorzystywane są specjalne modele, które pozwalają na nieliniowe odwzorowanie przedziałów kwantyzacji bez wysłania dodatkowych informacji. 3.3 Skalarna kwantyzacja równomierna z rozszerzonym przedziałem zerowym Kwantyzacja równomierna z rozszerzonym przedziałem zerowym jest szczególnie efektywna przy kwantyzacji współczynników transformaty falkowej. W pasmach wysokich częstotliwości wiele współczynników ma wartości bardzo bliskie zeru. Dzięki niewielkiemu 15

rozszerzeniu przedziału zerowego przy niewielkim wzroście błędu średniokwadratowego znacząco zwiększamy ilość współczynników o wartości 0 (rysunek 9). Taki kwantyzator został opisany w części drugiej standartu JPEG2000 ([12]). Oznaczmy przez przedział kwantyzacji dla kwantyzatora równomiernego. Wówczas wartość skwantyzowana q dla elementu f wynosi: 0 dla f nz q = (10) sign(f) f +nz dla f nz W przedstawionym wzorze parametr nz odpowiada za szerokość przedziału zerowego i musi być przesłany do dekodera. Szerokość przedziału zerowego wynosi więc 2(1 nz). W standardzie JPEG2000 jako potencjalnie korzystną wartość wskazuje się nz 0.25 czyli 1.5 ([12]). Dekwantyzację wykonuje się wykorzystując wzór: 0 dla q = 0 f = (11) sign(q)( q nz + 0.5) dla q 0 Rysunek 9: Podział na przedziały kwantyzacji w kwantyzatorze równomiernym z rozszerzonym przedziałem zerowym 16

4 Kodowanie arytmetyczne Uzyskany w wyniku kwantyzacji strumień symboli kodowany jest koderem entropijnym. Metoda Huffmana wykorzystuje do zakodowania symbolu całkowitą liczbę bitów. Takie działanie powoduje jednak, że metoda ta jest nieoptymalna. Przytaczając zamieszczony w [11] przykład dla symbolu o prawdopodobieństwie 1/3 optymalna długość słowa kodowego wynosi około 1.6 bitów. Koder Huffmana musi jednak przypisać dla niego dwa bity przez co nie jest on optymalny. Różnica ta staje się jeszcze bardziej widoczna w przypadku kompresji obrazów, gdy jeden z symboli stanowi np. 90 procent wszystkich wystąpień. Wówczas optymalna długość słowa kodowego wynosi 0.15 bitów. Przypisując symbolowi jeden bit uzyskujemy ponad sześciokrotnie gorszy wynik niż wynosi optimum. 4.1 Kodowanie arytmetyczne oparte o arytmetykę rzeczywistoliczbową Kodowanie arytmetyczne to zupełnie inne podejście do kompresji ciągu symboli. Nie wykonujemy tutaj odwzorowania każdego symbolu na zakodowany ciąg bitów. Dla całego strumienia przyporządkowujemy jedną liczbę ułamkową nazywaną liczbą kodową. Wyznaczanie liczby kodowej odbywa się poprzez sukcesywne zacieśnianie przedziału kodowego wraz z kodowaniem kolejnych symboli. Ostatecznie liczbą kodową przekazywaną do dekodera jest dowolna liczba ułamkowa wybrana z przedziału kodowego po zakodowaniu wszystkich elementów. W dalszej części przedstawiony zostanie podstawowy algorytm kompresji i dekompresji kodera arytmetycznego opartego na liczbach rzeczywistych oraz prosty przykład. Algorytm kompresji: 1. Określenie prawdopodobieństwa występowania symboli. 2. Podzielenie przedziału [0, 1) na podprzedziały w zależności od częstości występowania symbolu. Kolejność w jakiej symbole są przypisywane do przedziałów nie ma znaczenia, ważne, aby była taka sama w koderze i dekoderze. Podprzedziały muszą pokrywać cały przedział początkowy i nie mogą na siebie zachodzić. 3. Czytanie symboli ze strumienia i zmiana przedziału kodowego. Po wczytaniu każdego z symboli wykonujemy aktualizację górnej (high) i dolnej (low) wartości przedziału kodowego korzystając z zależności: r an g e = h i g h low ; high = low + range high range ( symbol ) ; low = low + range low range ( symbol ) ; 17

gdzie funkcja high range zwraca górną granicę, a low range zwraca dolną granicę podprzedziału danego symbolu. 4. Wybranie liczby kodowej z pomiędzy otrzymanego przedziału. Dodatkowo aby możliwe było zdekodowanie ciągu do dekodera, musi być przekazana informacja o prawdopodobieństwach występowania symboli oraz o liczbie zakodowanych symboli. Algorytm dekompresji: 1. Pobranie informacji o prawdopodobieństwie występowania poszczególnych symboli oraz o liczbie symboli. Dodatkowo należy pobrać zakodowaną liczbę kodową. 2. Podzielenie przedziału [0, 1) analogicznie jak w punkcie 2 algorytmu kompresji. 3. Dekodowanie symbolu odbywa się poprzez określenie przedziału, w który wpada liczba kodowa. Po zdekodowaniu należy usunąć wpływ odczytanego symbolu na liczbę kodową korzystając z zależności: code number = ( code number low range ( symbol ) ) / ( high range ( symbol ) low range ( symbol ) ) ; 4. Jeśli nie odczytano wszystkich symboli powrót do punktu 3. 4.2 Przykład Załóżmy, że mamy do czynienia z czteroelementowym alfabetem a, b, c, d o prawdopodobieństwach występowania kolejno (0.5, 0.2, 0.2, 0.1 ). Po określeniu prawdopodobieństw dokonujemy podziału przedziału początkowego. Przyjmujemy leksykalną kolejność występowania symboli: symbol prawd. zakres a 0.5 0.0 p < 0.5 b 0.2 0.5 p < 0.7 c 0.2 0.7 p < 0.9 d 0.1 0.9 p < 1.0 Znając podział zakresu możemy przystąpić do kodowania ciągu symboli. W poniższej tabeli przedstawiono dolną i górna granicę przedziału kodowego po zakodowaniu poszczególnych symboli przykładowego ciągu wejściowego a, c, a, a, b, a, d, b. 18

symbol dolny przedział górny przedział 0.0 1.0 a 0.0 0.5 c 0.35 0.45 a 0.35 0.4 a 0.35 0.375 b 0.3625 0.3675 a 0.3625 0.2650 d 0.36475 0.3650 b 0.364875 0.364925 Po zakodowaniu wybieramy liczbę kodową z końcowego zakresu [0.364875, 0.364925). Przyjmijmy więc, że będzie to liczba 0.36488. Liczba ta wraz z informacją o prawdopodobieństwach symboli i ich liczbie jednoznacznie reprezentuje wejściowy ciąg. W poniższej tabeli przedstawiony został proces dekodowania. Po sprawdzeniu, w którym przedziale znajduje się liczba i wypisaniu symbolu na wyjście usuwany jest wpływ tego symbolu na liczbę kodową zgodnie z punktem trzecim algorytmu dekompresji. Liczba kodowa Symbol wyjściowy dolny przedział górny przedział a 0.36488 0.0 0.5 c 0.72976 0.7 0.9 a 0.1488 0.0 0.5 a 0.2976 0.0 0.5 b 0.5952 0.5 0.7 a 0.476 0.0 0.5 d 0.952 0.9 1.0 b 0.52 0.5 0.7 STOP 0.1 - - Podsumowując przedstawiony przykład należy zauważyć, że proces kodowania polega na sukcesywnym zawężaniu przedziału kodowego. W zależności od tego jakie prawdopodobieństwo ma kodowany symbol, przedział kodowy zawęża się w mniejszym bądź większym stopniu. Im szerszy uzyskamy przedział, tym mniej miejsc po przecinku zajmie na końcu liczba kodowa. 4.3 Kodowanie arytmetyczne oparte o arytmetykę całkowitoliczbową Przedstawiony wcześniej schemat kodowania arytmetycznego opierał się na arytmetyce rzeczywistoliczbowej. Nie da go się jednak zrealizować w praktycznych implementacjach 19

ze względu na ograniczoną długość rejestrów procesorów. Po zakodowaniu zaledwie kilkunastu symboli dochodziłoby do ich przepełnienia. Możliwe jest jednak zaimplementowanie kodera arytmetycznego przy wykorzystaniu arytmetyki całkowitoliczbowej wykorzystującej proste operacje bitowe na rejestrach. Bity reprezentujące liczbę kodową są wysuwane na wyjście w miarę kodowania kolejnych elementów. Dzięki temu możliwe jest w praktyce kodowanie dowolnie długich ciągów. Aby możliwe było reprezentowanie prawdopodobieństw w rejestrach całkowitoliczbowych, należy przyjąć że dolną granicę 0.0000 reprezentują wszystkie bity ustawione na zero, a górna granicę 0.9999 reprezentują wszystkie bity ustawione na jeden. Przez wszystkie bity rozumiane są tu wszystkie bity reprezentujące ułamek (implementacja używa 31 bitów z 32 bitowego rejestru). Warto również zauważyć, iż w obu liczbach część całkowita wynosi 0, więc można ją pominąć. Koder posiada zatem dwa rejestry dół i góra zainicjalizowane przez wartości 0000 i FFFF. Teraz w miarę kodowania kolejnych symboli wysuwane będą bity rejestru dół w momencie, gdy najbardziej znaczące bity obu rejestrów będą sobie równe. Zmiany będą zachodziły już tylko na mniej znaczących bitach, więc można bezpiecznie je wysunąć. Rejestr dół jest uzupełniany zerami natomiast rejestr góra jest uzupełniany jedynkami. Koder musi również zabezpieczyć się przed sytuacją, w której wysuwanie zostanie zatrzymane przez niedomiar. Kodowanie kolejnych symboli zacieśnia przedział kodowy, lecz różnica na najbardziej znaczących bitach może nie wystąpić od razu. Załóżmy, że posiadamy czterobitowe rejestry. Wartość rejestru dół wynosi 0110, a wartość rejestru góra wynosi 1001. Teraz w miarę procesu kodowania może dojść do sytuacji gdy po zakodowaniu kolejnego symbolu wartości te będą wynosić odpowiednio 0111 i 1000. W tym momencie najbardziej znaczące bity będą różne, uniemożliwiając wysunięcie. Rozwiązaniem tego problemu jest wcześniejsza detekcja możliwości wystąpienia tego problemu. Rozpatrując poprzedni przykład niedomiar występuje wtedy, gdy rejestr dół ma postać 01XX, a rejestr góra 10XX. Po wykryciu niedomiaru należy przesunąć w lewo wszystkie bity obu rejestrów oprócz najbardziej znaczących. Dekoder wczytuje sekwencyjne kolejne bity zakodowanej liczby kodowej do rejestru kod. Musimy również posiadać dwa rejestry dół i góra analogiczne do tych w koderze. W dekoderze trzeba wykonywać te same operacje na tych rejestrach jak w koderze, włączając wykrywanie niedomiaru. Po określeniu aktualnego prawdopodobieństwa dokonujemy odpowiednich przesunięć na wszystkich trzech rejestrach. Kompletny algorytm kodowania arytmetycznego w arytmetyce całkowitoliczbowej wraz w wyjaśnieniami można znaleźć w książkach [16] [11]. 20

4.4 Koder adaptacyjny Przedstawiony wcześniej algorytm kompresji posiada pewne wady. Pierwszą z nich jest konieczność wstępnego określenia prawdopodobieństwa występowania symboli. W takim przypadku algorytm musi być dwuprzebiegowy. Najpierw zliczana jest liczność poszczególnych symboli, a następnie wykonywane jest kodowanie. Tablica występowania symboli musi być dodatkowo przesłana do dekodera, zmniejszając efektywność kompresji. Warto zmniejszyć dynamikę elementów tablicy poprzez skalowanie, np. jeśli do zliczania liczności poszczególnych symboli wykorzystywaliśmy 32 bitowe liczby można je przeskalować i zapisać na 8 bitach. W ten sposób można w oszczędny sposób przesłać informację do dekodera o prawdopodobieństwach, nie wpływając znacząco na efektywność kompresji symboli. Lepszym rozwiązaniem jest zastąpienie statycznego modelu statystycznego modelem dynamicznym. Model ten jest aktualizowany w miarę kodowania kolejnych symboli, dopasowując się do charakterystyki kodowanych danych. Dekoder działa w analogiczny sposób. Po odkodowaniu danego symbolu, aktualizowane jest jego prawdopodobieństwo. Nie jest więc konieczne dwukrotne iterowanie przez dane. Dzięki temu nie trzeba również przesyłać informacji o prawdopodobieństwie występowania symboli. Zanim model dynamiczny dostosuje się do charakterystyki danych, kodowanie będzie mniej efektywne. Po zakodowaniu pewnej liczby symboli prawdopodobieństwa już się ustalą i kodowanie będzie efektywne. Dodatkowo model adaptacyjny dostosowuje się do charakterystyki lokalnej danych. Takie rozwiązanie pozwala uzyskać w większości przypadków lepszy stopień kompresji niż rozwiązanie z modelem statycznym. 21

5 Technologia NVIDIA CUDA W tym rozdziale zostały przedstawione podstawy technologii NVIDIA CUDA (Compute Unified Device Architecture). Szczególny nacisk został położony na elementy wykorzystane przy implementacji. Rozdział został przygotowany w oparciu o oficjalną dokumentację [14] oraz książkę [5]. Specyficzne konstrukcje kompilatora CUDA zostały wyjaśnione na prostych przykładach. 5.1 Efektywność obliczeniowa procesorów graficznych Jednostki obliczeniowe procesorów graficznych były rozwijane przez szereg lat, aby sprostać zapotrzebowaniu na renderowanie wysokiej jakości grafiki 3D. Przez ten czas wyewoluowały one w wysoce równoległe procesory posiadające znaczącą moc obliczeniową. Udostępniony przez firmę NVIDIA interfejs CUDA umożliwia wykorzystanie procesora graficznego do obliczeń nie związanych z renderowaniem grafiki. Procesory graficzne posiadają dużą liczbę prostszych rdzeni pozwalających na uzyskanie wysokiej efektywności obliczeń. Zwykłe jednostki obliczeniowe CPU zmniejszyły tempo wzrostu taktowania na rzecz wzrostu liczby rdzeni. Rysunek 10: Teoretyczna przepustowość obliczeń zmiennoprzecinkowych na liczbach pojedynczej precyzji [14] 22

Jako porównanie weźmy pod uwagę rodzinę procesorów Intel Core i7. Procesor ten posiada cztery rdzenie wspierające technologię hyperthreading, przez co może wykonywać równolegle osiem wątków. Procesor graficzny karty NVIDIA GeForceGTX 580 posiada 512 rdzeni pozwalając na równoczesne uruchomienie kilku tysięcy wątków. Osiąga on blisko 10 razy większą teoretyczną przepustowość obliczeń zmiennoprzecinkowych na liczbach pojedynczej precyzji (rysunek 10). Tak duża rozbieżność w wydajności bierze się z fundamentalnych różnic w architekturze obu rozwiązań (rysunek 11). Procesor CPU jest przystosowany do sekwencyjnego wykonywania instrukcji. Istnieją w nim mechanizmy, które sprawiają, że instrukcje wewnątrz procesora wykonywane są poza kolejnością mimo wrażenia sekwencyjnego wykonania (na rysunku blok CONTROL). Kolejną ważną cechą jest rozbudowany mechanizm dużej pamięci podręcznej zmniejszającej opóźnienia przy dostępnie do pamięci głównej. Procesor graficzny ze względu na swój uproszczony model pamięci ma kilkukrotnie większą przepustowość od głównej pamięci DRAM procesora. Renderowanie wysokiej jakości grafiki 3D wymaga wykonania ogromnych ilości obliczeń zmiennoprzecinkowych na każdą ramkę obrazu. Zajmuje się tym duża liczba wątków o uproszczonej logice wykonania. Mała pamięć podręczna pozwala zmniejszyć zużycie przepustowości pamięci DRAM, bez wprowadzania kosztownej logiki związanej z zarządzaniem nią, przeciwnie niż ma to miejsce w pamięci podręcznej procesora CPU. Rysunek 11: Architektury procesorów CPU i GPU [14] Procesor graficzny składa się z szeregu multiprocesorów strumieniowych (SM - streaming multiprocessor). Multiprocesory składają się grupy procesorów strumieniowych (SP - streaming processor), które mają wspólną pamięć podręczną. Ilości multiprocesorów i procesorów są zróżnicowane w zależności od danego procesora. Procesor graficzny działa na pamięci wewnętrznej karty graficznej. Pamięć karty graficznej będziemy nazywać pamięcią urządzenia (device memory), a zwykłą pamięć RAM pamięcią hosta (host memory). Szczegóły dotyczące architektury sprzętowej kart wspierających technologię CUDA 23

można znaleźć w książce [5]. W dalszej części przedstawię abstrakcje modelu programowania pozwalające na programowanie jednostki GPU bez znajomości sprzętowych szczegółów implementacyjnych. 5.2 Model programowania Z perspektywy programisty system składa się z hosta, który jest jest zwykłym procesorem, oraz z urządzeń (devices), które są wysoce wielowątkowymi procesorami graficznymi. Dzięki dużej liczbie jednostek obliczeniowych w procesorze graficznym, możliwe są równoczesne obliczenia na dużej ilości danych. W technologii CUDA kluczowe jest znalezienie tych fragmentów kodu, które wymagają intensywnych obliczeń arytmetycznych i można je zrównoleglić. Program wykorzystujący CUDA składa się z kodu wykonywanego na hoście oraz na urządzeniu. Do kompilacji kodu wykorzystywanego zarówno przez hosta jak i urządzenie wykorzystywany jest specjalny kompilator NVIDIA C (nvcc). Kod hosta to zwykły kod ANSI C i jest uruchamiany jak normalny proces. Kod urządzenia to ANSI C z pewnymi rozszerzeniami opisanymi w dalszej części rozdziału. 5.3 Funkcje kerneli Funkcje kerneli nazywane w skrócie kernelami (kernels), pozwalają na definiowanie kodu wykonywanego przez wątki w urządzeniu. Każdy z kerneli jest wykonywany równolegle przez osobne wątki, przeciwnie niż ma to miejsce w zwykłych funkcjach C. Aby zdefiniować kernel, należy przed deklaracją funkcji umieścić specyfikator global. Wywołanie kernela z odpowiednią liczbą wątków odbywa się poprzez wykorzystanie specjalnej składni <<<>>> (execution syntax). Całość musi być skompilowana kompilatorem nvcc. Poniżej znajduje się przykład kernela podnoszącego zadany ciąg liczb do kwadratu. Normalnie rozwiązanie takiego zadania opierałby się na sekwencyjnym obliczeniu kwadratu każdej liczby w pętli. Aby wykorzystać wielowątkowe możliwości procesora graficznego, każdy z elementów będzie obliczany oddzielnie. Indeks elementu w tablicy wyznaczany jest na podstawie unikalnego identyfikatora wątku (thread ID). Pobierany jest on z wbudowanej zmiennej threadidx. W wywołaniu kernela używamy specjalnej składni podając ilość wątków którą chcemy uruchomić. // d e f i n i c j a k e r n e l a g l o b a l void square ( float data ) { int index = threadidx. x ; data [ index ] = data [ index ] data [ index ] ; } int main ( ) {... 24

} // wywolanie k e r n e l a z N watkami // data d e v i c e memory square <<<1, N>>>(data ) ; 5.4 Hierarchia wątków Wątki mogą być indeksowane w trzech wymiarach x, y, z przy użyciu specjalnej zmiennej threadidx. Tworzą one wówczas odpowiednio jedno, dwu, lub trójwymiarowy blok wątków (thread block). Takie grupowanie pozwala w łatwy sposób wywoływać obliczenia na wektorach, macierzach i danych wolumetrycznych. Każdy z uruchomionych w ramach bloku wątków ma unikalne wartości indeksów x, y oraz z. Istnieje limit określający liczbę wątków w ramach bloku i obecnie wynosi on 512. Wątki danego bloku są przetwarzane na jednym multiprocesorze. Dzielą więc one zasoby ograniczonej pamięci podręcznej. Mają możliwość współpracy ze sobą poprzez wykorzystanie wspólnej pamięci podręcznej jak również synchronizację wykonania. Bloki są grupowane w jedno, bądź dwuwymiarowe kraty (grid). Maksymalny wymiar kraty zależy od urządzenia. Tak przygotowana krata wątków może liczyć od tysięcy do milionów prostych wątków. Schemat hierarchii wątków przedstawiony jest na rysunku 12. Rysunek 12: Schemat hierarchii wątków [5] 25

Poprawmy więc teraz wcześniejszy przykład tak, aby możliwe było wykorzystanie go dla tablicy dłuższych niż 512 elementów. Znając już pojęcie bloków, można wyjaśnić dokładnie, składnie wywołania kernela. Pierwszym argumentem jest liczba bloków, natomiast drugim jest ilość wątków na blok. Dla uproszczenia załóżmy, że długość tablicy jest wielokrotnością ilości wątków na blok: // i l o s c watkow na b l o k const int THREADS PER BLOCK = 512; } g l o b a l void square ( float data ) { int index = blockidx. x THREADS PER BLOCK + threadidx. x ; data [ i ] = data [ i ] data [ i ] ; int main ( ) {... // wywolanie k e r n e l a z N watkami int blocknum = N / THREADS PER BLOCK; square <<<blocknum, THREADS PER BLOCK>>>(data ) ; } Do uzyskania indeksu bloku w kracie użyliśmy specjalnej zmiennej blockidx. Jeśli chcemy wykorzystać kernel do przetwarzania macierzy bądź danych wolumetrycznych jako argumenty należy przekazać struktury dim3. Posiadają one trzy wartości x, y, z, którymi można określić wymiar bloku i kraty wątków. 5.5 Hierarchia pamięci Host i urządzenie mają oddzielne przestrzenie pamięci. Tak więc przed wykonaniem obliczeń należy zaalokować pamięć i przenieść dane z pamięci hosta do pamięci globalnej urządzenia (global memory). Po wywołaniu kernela pobieramy z powrotem wyniki do pamięci hosta i zwalniamy zaalokowaną pamięć urządzenia. Poniżej znajduje się uzupełniona o alokację i przesyłanie pamięci procedura main. Do alokacji pamięci na urządzeniu wykorzystywana jest funkcja cudamalloc, działająca analogicznie do funkcji z standardowej biblioteki C. Analogicznie działa również funkcja zwalniająca pamięć, cudafree. Funkcja cudamemcpy zajmuje się kopiowaniem pamięci do i z urządzenia. To, w którą stronę zachodzi przesyłanie danych, zależy od czwartego argumentu wywołania będącego enumeracją cudamemcpykind. int main ( ) { float hostmemory ; 26

... float devicememory ; cudamalloc ( ( void ) devicememory, N s i z e ( float ) ) ; cudamemcpy( devicememory, hostmemory, N s i z e ( float ), cudamemcpyhosttodevice ) ;... square <<<blocknum, THREADS PER BLOCK>>>(deviceMemory ) ; } cudamemcpy( hostmemory, devicememory, N s i z e ( float ), cudamemcpydevicetohost ) ; cudafree ( devicememory ) ; Po przesłaniu danych do pamięci urządzenia, wątki pobierają swoją porcję danych z pamięci globalnej przy użyciu zmiennych threadidx oraz blockidx. Takie rozwiązanie nie wykorzystuje jednak pełnej wydajności procesora graficznego. Wątek ma dostęp do kilku innych rodzajów pamięci, które można wykorzystać dla uzyskania większej szybkości wykonania. Rysunek 13 pokazuje wszystkie rodzaje pamięci i ich związek z hierarchią wątków. Host przy użyciu funkcji cudamemcpy ma możliwość zapisu do pamięci globalnej (global memory) oraz do pamięci stałych (constant memory). Pamięć globalna została już przedstawiona na przykładach i służy do przekazywania danych i wyników obliczeń. Wątki mogą ją odczytywać i zapisywać. Pamięć stałych zapewnia krótszy czas dostępu i wysoką przepustowość przy założeniu tylko odczytu przez wątki. Ma ona również ograniczoną wielkość (64KB). Każdy z wątków posiada swoją pamięć lokalną oraz rejestry. Pamięć tego typu znajduje się bezpośrednio na procesorze. Jest ona dostępna jedynie dla tego wątku i zapewnia najlepszą wydajność. Wykorzystuje się ją do przechowywania często używanych zmiennych prywatnych dla danego wątku. Wątki będące w jednym bloku mają dostęp do wspólnej pamięci współdzielonej (shared memory). Ma ona znacznie krótszy czas dostępu niż pamięć główna i ograniczoną wielkość (16KB lub 48KB na multiprocesor). Można ją sobie wyobrazić jako ręcznie zarządzaną pamięć podręczną w ramach bloku. Wykorzystuje się ją zwłaszcza w przypadku dostępu do tych samych danych wejściowych przez wiele wątków, bądź dostępu do obliczeń pośrednich. Przedstawione rodzaje pamięci mają swój czas życia. Pamięć globalna i stałych jest dostępna przez czas działania aplikacji. Pamięci lokalne wątków oraz pamięć współdzielona ma czas życia równy czasowi życia bloku. Po wykonaniu bloku, pamięć jest zwalniana umożliwiając uruchomienie kolejnych bloków wątków. 27

Rysunek 13: Schemat hierarchii pamięci w procesorze graficznym [5] Do określenia pewnych typów pamięci, API określa odpowiednie specyfikatory: pamięć współdzielona - shared pamięć globalna - pamięć stałych - device constant Wykorzystanie pamięci współdzielonej, stałych i lokalnej pozwala ograniczyć użycie przepustowości pamięci globalnej, która jest wąskim gardłem w aplikacji wykorzystujących CUDA. Należy mieć jednak na uwadze, że wymienione rodzaje pamięci mają swoje limity. Przy zwiększaniu ich wykorzystania, możemy doprowadzić do zmniejszenia ilości wątków wykonywanych na danym multiprocesorze. 28

6 Opis proponowanego rozwiązania W tym rozdziale przedstawiony jest opis stworzonego przeze mnie narzędzia do kompresji sekwencji wideo i obrazów. Zaimplementowałem kompresor działający na procesorze CPU oraz na karcie graficznej wykorzystującej CUDA. Na początku przedstawię wymagania postawione aplikacji. Następnie opiszę funkcje udostępnione przez kompresor. Na końcu opiszę architekturę rozwiązania z wyszczególnieniem tych fragmentów aplikacji, które udało się zaimplementować przy użyciu procesora karty graficznej. 6.1 Wymagania dotyczące aplikacji Głównym celem zaprojektowanej aplikacji jest stworzenie narzędzia do kompresji sekwencji wideo przy wykorzystaniu metod falkowych i technologii NVIDIA CUDA. Aby możliwe było łatwe porównanie skuteczności kompresji z innymi rozwiązaniami, stworzony przeze mnie kompresor rozszerzyłem o możliwość kompresji obrazów. 2 Aplikacja musi umożliwiać kompresję w dwóch trybach, tj. przy wykorzystaniu CPU oraz przy wykorzystaniu CPU i procesora graficznego. W program powinny być wbudowane mechanizmy liczące czas wykonania poszczególnych jego części. Posiadając te dane można określić efektywność implementacji CUDA w stosunku do implementacji CPU, co jest głównym celem pracy. Kompresor powinien umożliwiać odczyt i zapis szeroko wykorzystywanych formatów plików. Zakodowane pliki w własnym formacie muszą posiadać wszelkie dane niezbędne do ich poprawnego odkodowania bez względu na wybrane parametry kodera. Ważne jest również poprawne dekodowanie plików zakodowanych na różnych platformach sprzętowych. 6.2 Funkcje aplikacji Interfejs Aplikacja udostępnia swoje funkcje poprzez argumenty wywołania. Komunikacja zwrotna odbywa się poprzez komunikaty. W argumentach wywołania możliwe jest określenie stopnia szczegółowości wyświetlanych informacji. W przypadku wystąpienia błędu jest wyświetlony odpowiedni komunikat oraz zwrócony kod błędu. Kompresja obrazów Aplikacja umożliwia kompresję obrazów bitmapowych. Możliwa jest kompresja obrazów w odcieniach szarości lub kolorowych. Obsługiwane formaty odczytu i zapisu plików bitmapowych to: pgm, bmp, jpg, tiff. Ze względu na zastosowany algorytm 2 W większości prac z dziedziny kompresji stratnej można, np. znaleźć wyniki dla standardowego obrazu testowego Lena. 29

obliczania transformaty falkowej, obraz musi mieć wymiary będące wielokrotnością liczby 2 d, gdzie d to liczba poziomów dekompozycji. Kompresja sekwencji wideo Aplikacja umożliwia kompresję sekwencji wideo. Wymagania dotyczące liczby kolorów oraz wymiarów są takie same jak dla kompresji obrazów. Obsługiwany wejściowy i wyjściowy format pliku to avi w formacie RAW i420 bez dźwięku. Wybór sposobu wykonania obliczeń Domyślnie aplikacja działa przy użyciu implementacji CPU. Możliwe jest jednak określenie by program zastosował implementację wykorzystującą procesor karty graficznej. Wyświetlanie czasu wykonania poszczególnych części programu W aplikację wbudowane są mechanizmy liczące czas wykonania poszczególnych kroków algorytmu kompresji i dekompresji. Określenie stopnia kompresji Aplikacja przy kompresji pobiera parametr określający żądany stopień kompresji. Dla przykładu chcąc uzyskać stopień kompresji 4:1 musimy podać wartość 4. Aplikacja nie obsługuje parametryzacji poprzez żądaną jakość obrazu zrekonstruowanego. Określenie innych parametrów Przekazując odpowiednie argumenty wywołania aplikacja przy kodowaniu pozwala na wybór algorytmów wykorzystywanych w poszczególnych krokach kompresji. Informacje te są zapisywane w zakodowanym pliku na potrzeby dekodera. Parametryzowane są następujące opcje: wykorzystana funkcja falkowa (dostępne filtry: Haar, Daub4, Daub6, CDF97 oraz Antonini), liczba poziomów dekompozycji transformaty falkowej, typ kwantyzatora (dostępny: kwantyzator równomierny, kwantyzator równomierny z rozszerzonym przedziałem zerowym) 30

6.3 Architektura rozwiązania Zaimplementowany przeze mnie kompresor opiera się na standardowym schemacie transformacyjnej kompresji stratnej. Na rysunku 14 znajduje się schemat kompresji obrazów. Kompresja sekwencji wideo przechodzi przez dokładnie te same kroki z pewnymi drobnymi różnicami. Rysunek 14: Schemat kompresji obrazów Formatowanie danych Pierwszym krokiem algorytmu kompresji jest formatowanie danych. Zadany obraz oryginalny musi być odpowiednio przechowywany, aby wykonanie na karcie graficznej było efektywne. Ważne jest, aby kanały kolorowego obrazu były przetwarzane oddzielnie. Wynika to z faktu, że transformata i transformata odwrotna musi być wykonana oddzielnie dla każdego kanału. Procesor karty graficznej przy pobieraniu danych z pamięci globalnej pobiera je od razu w większych blokach 32, 64 lub 128 bitowych. Technika ta nazywa się memory coalescing i ma na celu zmniejszenie liczby żądań do pamięci globalnej. Rysunek 15: Zmiana struktury przechowywania pikseli w pamięci Zakładając, że każdy z wątków oblicza jeden współczynnik transformaty, musiałby by on sięgać po co trzeci element. Miałoby to oczywiście zły wpływ na wydajność ze względu na zbędne pobranie niewykorzystywanych w obliczeniach wartości. Rysunek 15 przedstawia niezbędne w tym kroku ustawienie danych. Oddzielne przetwarzanie kanałów pozwala również zmniejszyć zużycie pamięci globalnej karty graficznej. W danej chwili wykonywane są obliczenia dla tylko jednego kanału. 31

Rysunek 16: Sekwencja obrazów przy przetwarzaniu jest grupowana (w - szerokość; h - wysokość; f - ilość ramek tworzących grupę) Wejściowym formatem dla sekwencji wideo jest sekwencja obrazów. Podział kanałów barw jest analogiczny do zastosowanego przy formatowaniu obrazów. Kompresor w danej chwili pracuje na określonej liczbie ramek (na rysunku 16 parametr f). Transformata falkowa dla sekwencji wideo odbywa się po trzech wymiarach, tak więc konieczne jest zastosowanie odpowiednio dużej liczby ramek, aby możliwe było wykonanie kilkupoziomowej dekompozycji sygnału. Po wykonaniu pozostałych kroków algorytmu na grupie ramek i zapisie zakodowanych danych do strumienia wyjściowego przetwarzanie odbywa się na kolejnej grupie ramek. Transformata kolorów Dla obrazów barwnych wykonywana jest transformata kolorów. Jej głównym celem jest wykorzystanie psychowizualnych cech ludzkiego zmysłu wzroku. Jak powszechnie wiadomo ludzki wzrok jest bardziej czuły na zmiany luminancji niż chrominancji. Powszechnie wykorzystywany model RGB przechowuje jasności w każdej z swoich składowych. Dla celów kompresji korzystniejsze są modele gdzie jasność jest przechowywana na jednej składowej, a barwy na dwóch pozostałych. W zaimplementowanym kompresorze wykorzystałem przejście do modelu Y C b C r, gdzie Y to jasność, a C b i C r to składowe opisujące barwę. Poniżej znajdują się wykorzystane wzory transformacyjne. Y 0 0.299 0.587 0.114 R C b = 128 + 0.169 0.331 0.500 G C r 128 0.500 0.419 0.081 B 32

R 1.000 0.000 1.400 Y G = 1.000 0.343 0.711 (C b 128) B 1.000 1.765 0.000 (C r 128) Algorytm obliczania transformaty kolorów łatwo daje się zrównoleglić i zaimplementować na GPU. Każdy z wątków ma dostęp do składowych RGB piksela. Na podstawie blockidx i threadidx może określić swój indeks. Następnie wykorzystując przedstawione wzory, dokonuje obliczenia wartości Y C b C r i zapisuje je na miejsce wartości RGB. Przy tak prostym dostępie do pamięci nie należy korzystać z pamięci współdzielonej. Każdy wątek korzysta jedynie ze swoich danych. Transformata falkowa Po opcjonalnym wykonaniu transformacji kolorów wykonywana jest transformata falkowa. We wstępie teoretycznym przedstawiona została metoda obliczania współczynników transformaty falkowej, przy użyciu algorytmu konwolucji z periodycznym rozszerzaniem sygnału na granicach. Taki właśnie algorytm został zaimplementowany zarówno na procesorze CPU i GPU. Kompresor pozwala na użycie kilku zdefiniowanych funkcji falkowych. Każda falka składa się z zestawu czterech filtrów. Są to pary nisko i wysokoczęstotliwościowych filtrów analizy i syntezy. Każdy z filtrów posiada ciąg współczynników o danej długości i parametr określający pierwszy współczynnik filtru. Mówi on o ile pozycji w lewo należy przesunąć filtr przy obliczaniu splotu (rysunek 17). Rysunek 17: Konwolucja obrazu dla filtru o czterech współczynnikach bez przesunięcia i z przesunięciem równym jeden Obliczenie splotu wejściowego ciągu x z filtrem h, można zaimplementować przy użyciu dwóch prostych pętli zagnieżdżonych. Obliczenie wykonujemy tylko dla co drugiego elementu ze względu na wyrzucenie w kolejnym kroku co drugiej próbki. Ważne jest periodyczne rozszerzenie sygnału na granicach. W przypadku implementacji na GPU każdy z wątków wykonuje obliczenie jednego współczynnika dla niskich częstotliwości i jednego dla wysokich. Jeśli oba filtry analizy 33

mają podobną długość i przesunięcie, wówczas wykorzystują one te same elementy wejściowe do obliczenia obu współczynników (rysunek 18). Wykonując więc obliczenie obu współczynników, zmniejszamy zużycie przepustowości pamięci. Dodatkowo warto wykorzystać pamięć współdzieloną ze względu na dostęp do tych samych współczynników przez kilka wątków. Rysunek 18: Równoczesne obliczenie współczynników nisko i wysokoczęstotliwościowych Obliczanie transformaty dla sygnałów wielowymiarowych wykonujemy dla każdego wymiaru oddzielnie korzystając z separowalności funkcji falkowej. W zaprojektowanym przeze mnie kompresorze ramki sekwencji wideo grupuję w bloki tworząc trójwymiarową macierz pikseli (rysunek 19). Tak uzyskaną macierz pikseli poddaję transformacie falkowej po każdym z wymiarów. Sposób dekompozycji dla obrazów został już przedstawiony w wstępnie teoretycznym. Rysunek 19: Trójwymiarowa macierz pikseli 34

W wyniku obliczenia transformaty uzyskamy trójwymiarowe podmacierze, w których znajdą się współczynniki poszczególnych podpasm. Tak uzyskane dane są sprowadzane do postaci liniowej. Na rysunku 20 przedstawiony został sposób linearyzacji dla obrazu. Dane w każdym podpaśmie są kopiowane wiersz po wierszu. Poszczególne podpasma są ustawione od pasma najniższych częstotliwości do najwyższych. Dla obliczonej trójwymiarowej macierzy współczynników dane w podmacierzy są kopiowane ramka po ramce, a w ramach ramki, wiersz po wierszu. Wykonanie linearyzacji ułatwia w kolejnych krokach dostęp do współczynników. Dane są tam przetwarzane w ramach podpasm, nie jest więc konieczne złożone indeksowanie. Wystarczy znać wskaźnik do pierwszego elementu podpasma i długość podpasma. Rysunek 20: Sposób linearyzacji obrazu poddanego transformacie Kwantyzacja Po wykonaniu transformaty uzyskujemy zbiór podpasm współczynników o wartościach ciągłych. Aby w kolejnym kroku możliwe było zakodowanie ich koderem arytmetycznym, należy je zdyskretyzować. W tym kroku odpowiednio dobierając liczbę poziomów dekompozycji zmniejszamy jego dynamikę w taki sposób, aby skutecznie poddawał się kompresji koderem arytmetycznym. Wybór liczby poziomów dekompozycji realizuje algorytm optymalizacji R-D (rate-distortion). Każde z podpasm jest kwantyzowane przy użyciu innej liczby poziomów dekompozycji. Wynika to oczywiście z znaczących różnic w dynamice poszczególnych podpasm. W pasmach wysokiej częstotliwości amplituda współczynników jest niewielka w porównaniu z pasmami niskich częstotliwości, gdzie kumuluje się energia sygnału. Liczba poziomów kwantyzacji jest równa 2 b, gdzie b to ilość bitów przeznaczonych na dany współczynnik. W wykonanym koderze zaimplementowałem dwa kwantyzatory: kwantyzator równomierny i kwantyzator równomierny z rozszerzonym przedziałem zerowym. W przypadku kwantyzatora z rozszerzonym przedziałem zerowym sygnał jest uznawany za symetryczny. 35

Oznacza to, że jego amplituda nie wyraża się poprzez różnicę między największym i najmniejszym współczynnikiem, a poprzez dwukrotność wartości bezwzględnej z maksimum wartości bezwzględnej tych wartości (2 max( max, min )). Algorytm kwantyzacji nie został zaimplementowany na GPU ze względów wyjaśnionych w dalszej części pracy. Kodowanie arytmetyczne Po wykonaniu kwantyzacji uzyskujemy w każdym z podpasm całkowite współczynniki mieszczące się na b bitach. W zaprojektowanym kompresorze krok kodowania bezstratnego realizuje adaptacyjny koder arytmetyczny. Model statystyczny jest inicjalizowany równomiernie, tj. każdy symbol uzyskuje to samo prawdopodobieństwo. Wraz z kodowaniem kolejnych symboli model dostosowuje się do charakterystyki danych. Zdecydowałem się na wykorzystanie nowego modelu dla każdego z podpasm. Do wyjaśnienia tej decyzji przedstawię przykład. W wyniku optymalizacji R-D pasmo najniższych częstotliwości zostało skwantyzowane przy użyciu 8 bitów, a pasmo najwyższych częstotliwości przy użyciu 3 bitów. Tak jak zostało wspomniane wcześniej w pasmach wysokich częstotliwości większość współczynników przyjmuje wartości 0. Wykorzystując jeden i ten sam model, kodowanie pasma wysokich częstotliwości byłoby nieefektywne ze względu na to, że model przystosowałby się do pasma niższych częstotliwości. Oczywiście po pewnym czasie przystosowałby się on do charakterystyki danych, ale trwałoby to dłużej niż w przypadku rozpoczęcia kodowania od prawdopodobieństw wynoszących 1/8. Dodatkowo zaadoptowany przeze mnie algorytm optymalizacji R-D uniemożliwiał wykorzystanie tego samego modelu. Sekwencyjny charakter algorytmu kodowania arytmetycznego polegający na przesunięciach bitowych na trzech rejestrach dyskwalifikuje go przy implementacji na procesorze karty graficznej. Uzyskany w wyniku działania algorytmu strumień bitów jest zapisywany do pliku. Jest on poprzedzony odpowiednim nagłówkiem oraz danymi każdego z kwantyzatorów. Optymalizacja R-D W przedstawionym do tej pory schemacie kompresji brakuje informacji o sposobie wyboru liczby poziomów dekompozycji. Kompresor posiada parametr określający żądany poziom kompresji, np. 4 : 1. Żeby uzyskać taki poziom kompresji, należy tak dobrać liczbę poziomów kwantyzacji poszczególnych podpasm, aby po zakodowaniu skwantyzowanych współczynników koderem arytmetycznym osiągnąć żądaną liczbę bitów. Dobraniem liczby poziomów dekompozycji zajmują się algorytmy optymalizacji R-D (rate distorion). Na rysunku 21 przedstawiona jest przykładowa krzywa R-D dla jednego z kwantyzatorów. Wraz z zmniejszaniem kosztu bitowego (rate), zwiększają się zniekształcenia (distortion). Gdyby był wykorzystywany tylko jeden kwantyzator, wystarczyłoby zaczynając od pewnej ustalonej liczby bitów na symbol sprawdzać, czy mieścimy się założonym budżecie bitów. Jeśli nie, to należałoby zmniejszyć liczbę bitów na symbol, co 36

spowodowałoby większe zniekształcenia kwantyzacyjne i równocześnie zmniejszenie liczby potrzebnych bitów. Rysunek 21: Krzywa R-D dla jednego kwantyzatora Ze względu na to, iż koder wykorzystuje wiele kwantyzatorów, problem staje się trudniejszy. Ważne staje się takie dobranie liczby poziomów kwantyzacji w poszczególnych pasmach, aby globalne zniekształcenia były minimalne przy spełnieniu ograniczenia na liczbę wykorzystanych bitów. Silna kwantyzacja w pasmach wysokich częstotliwości pozwala uzyskać niski koszt bitowy przy niewielkich zniekształceniach. Na potrzeby mojego kodera zaadaptowałem algorytm R-D z biblioteki Wavelet Image Compression Construction Kit, 3 który opiera się na artykule [20]. Korzysta on z powszechnie wykorzystywanej metody mnożników Lagrange a. Problem minimalizacji można przedstawić jako minimalizację funkcji D + λr, gdzie λ jest mnożnikiem Lagrange a. Na rysunku 21 zmienna λ określa nachylenie stycznej do krzywej R-D. Optymalizacja przy użyciu mnożników Lagrange a polega na zmianie parametru λ aż do uzyskania żądanego kosztu bitowego. Szczegóły dotyczące algorytmu opisane są w wspomnianej pracy. Wykorzystana metoda optymalizacji wymaga, aby dla każdego z podpasm przygotować krzywe R-D. Miarą zniekształceń D jest przedstawiony wcześniej błąd średniokwadratowy. Aby uzyskać możliwie precyzyjnie żądaną liczbę bitów, zdecydowałem się na obliczanie kosztu bitowego poprzez kodowanie uzyskanych w wyniku kwantyzacji współczynników. Koder arytmetyczny działa wtedy w trybie zliczania bajtów i nie jest wówczas zapisywany strumień wyjściowy. Konieczność wielokrotnego kodowania koderem arytmetycznym skwantyzowanych współczynników uniemożliwiła zaimplementowanie kroku kwantyzacji na GPU. Wynika to z wysokiego kosztu przesyłania danych z pamięci urządzenia do pamięci głównej. 3 http://www.geoffdavis.net/dartmouth/wavelet/wavelet.html 37

Dekompresja Skompresowany plik zawiera w sobie nagłówek określający wszystkie niezbędne do wykonania dekompresji dane. Są to między innymi wymiary, liczba kanałów, liczba poziomów dekompozycji, zastosowana funkcja falkowa, kwantyzator oraz koder arytmetyczny. Dodatkowo przed zakodowanymi współczynnikami znajdują się dane wykorzystywane przez kwantyzatory w procesie dekwantyzacji. Są to wartość minimalna, maksymalna współczynników oraz liczba poziomów kwantyzacji. Po odczytaniu nagłówka następuje zdekodowanie danych zakodowanych koderem arytmetycznym. Ważne jest śledzenie liczby odczytanych elementów, aby przełączyć model statystyczny kodera przy odczycie kolejnego podpasma. Po odczytaniu każdego elementu jest on od razu poddawany dekwantyzacji. Dzięki temu nie jest konieczne wykorzystanie pośredniego bufora dla skwantyzowanych wartości. Przed wykonaniem transformaty odwrotnej należy zdelinearyzować współczynniki falkowe do pierwotnej postaci. Następnie współczynniki falkowe są poddawane odwrotnej transformacie falkowej, która jest wykonywana analogicznie do zwykłej transformaty. Jedyną różnicą jest wykorzystanie par filtrów syntezy zamiast filtrów analizy i wstawienie dodatkowych zer pomiędzy współczynnikami. Jeśli obraz bądź sekwencja wideo była poddana transformacie kolorów, należy wykonać odwrotną transformatę kolorów. Po jej wykonaniu uzyskujemy już pełną rekonstrukcję w pamięci. Ostatnim krokiem jest reorganizacja danych, mająca na celu ustalenie takiego formatu danych, aby możliwy był zapis w innej postaci (np. jeśli wartości poszczególnych barw piksela muszą być obok siebie). Kroki, które musi wykonać dekoder, są przedstawione na rysunku 22. Rysunek 22: Schemat dekompresji obrazów 38

7 Implementacja projektu W tym rozdziale przedstawiony jest opis implementacji projektu. Najpierw opiszę wykorzystywaną platformę sprzętową, niezbędne narzędzia oraz biblioteki. Następnie przedstawię w szczegółach zaimplementowany algorytm obliczania transformaty falkowej na procesorze GPU. Na końcu opiszę wbudowane w aplikację mechanizmy mierzenia czasu wykonania. 7.1 Platforma sprzętowa Ze względu na konieczność porównania implementacji CPU i GPU ważne jest na jakim sprzęcie została ona przetestowana. Na potrzeby implementacji i testów wykorzystano następującą konfigurację sprzętową: CPU AMD Atholon II X3 440 (3x3GHz) Chipset AMD 785 RAM DDR3 2x2GB (1333Mhz) GPU NVIDIA GeForce GTS250 1GB W poniższej tabeli zamieszczam właściwości wykorzystanego akceleratora graficznego pod kątem technologii NVIDIA CUDA: Compute capability * 1.1 Liczba multiprocesorów (MP) 16 Liczba rdzeni w multiprocesorze (SP) 8 Łączna liczba rdzeni 128 Taktowanie zegara 1.46GHz Łączna ilość pamięci globalnej 1073020928 bajtów Ilość pamięci współdzielonej na rdzeń 65535 bajtów Liczba rejestrów na rdzeń 8192 Maksymalna liczba wątków na blok 512 Maksymalna wielkość każdego z wymiarów w bloku 512 x 512 x 64 Maksymalna wielkość każdego z wymiarów w kracie 65535 x 65535 x 1 * Compute capability określa możliwości obliczeniowe procesora graficznego. Główny numer rewizji mówi o architekturze procesora, natomiast numer podrewizji określa stopniowo rozwijane rozszerzenia w ramach tej samej architektury. Szczegóły można znaleźć w dodatku G dokumentacji [14]. Komputer pracował pod kontrolą systemu Ubuntu Linux 10.10 64bit z jądrem w wersji 2.6.35.28. Wersja sterowników NVIDIA to 270.35. 39

7.2 Narzędzia i biblioteki Do implementacji projektu wykorzystałem następujące narzędzia: Cuda Toolkit v4.0 Zawiera zbiór niezbędnych narzędzi (np. kompilator nvcc) i bibliotek. Dołączona jest tu również dokumentacja i przykładowe aplikacje wraz z kodem źródłowym. Scons Narzędzie służące do budowania aplikacji, zastępujące makefile. Dzięki temu, że jest napisane w języku Python bardzo łatwo zwiększa się jego funkcjonalność. Wykorzystałem rozszerzenia pozwalające na kompilację kodu CUDA i automatyczne uruchamianie testów jednostkowych po każdym zbudowaniu aplikacji. valgrind Narzędzie umożliwiające detekcję wycieków pamięci i nieprawidłowych odwołań do pamięci. QtCreator Środowisko programistyczne dla języka C++. Szczególnie przydatną funkcją jest dobrze działający graficzny debugger bazujący na gdb. svn Użyłem systemu kontroli wersji svn. Kod projektu znajduje się w publicznie dostępnym repozytorium pod adresem: http://code.google.com/p/ wavelet-compressor/. Poniżej znajdują się użyte biblioteki oraz zaadaptowany kod wraz z opisem ich wykorzystania w zaimplementowanym projekcie: OpenCV Biblioteka OpenCV (Open Source Computer Vision) została wykorzystana do odczytu i zapisu różnych formatów plików obrazów i sekwencji wideo. googletest Biblioteka do tworzenia testów jednostkowych w C++. Wavelet Image Compression Construction Kit Szkielet aplikacji kompresującej obrazy przy wykorzystaniu transformaty falkowej. Zaadaptowałem stąd kod algorytmu optymalizacji R-D. Koder arytmetyczny z publikacji [6] Wykorzystałem kod kodera arytmetycznego opisanego w wyżej wymienionej pracy. Wprowadzone zostały zmiany umożliwiające zmianę modelu i kodowanie/dekodowanie pojedynczych symboli. 40

7.3 Algorytm obliczania transformaty falkowej na procesorze GPU W rozdziale poświęconym opisowi proponowanego rozwiązania przedstawione zostały główne założenia dotyczące implementacji transformaty falkowej na procesorze karty graficznej. Każdy z wątków będzie zajmował się równocześnie obliczaniem jednego współczynnika wysokich częstotliwości i jednego niskich. Tak więc dla jednowymiarowego sygnału o długości N będziemy potrzebowali N/2 wątków. Dodatkowo dla filtrów o większej długości kilka wątków będzie korzystało z tych samych współczynników (rysunek 23). Rysunek 23: Dostęp do tych samych elementów przy obliczaniu konwolucji z filtrem o ośmiu współczynnikach Dzięki takiemu dostępowi do pamięci warto jest wykorzystać pamięć współdzieloną. Wątki ładują dane do pamięci współdzielonej, a następnie obliczają transformatę korzystając z zgromadzonych tam danych. Jako że każdy z wątków oblicza dwa współczynniki, musi on również załadować dwa współczynniki. Nie można również zapomnieć o tym, że do obliczenia konwolucji potrzebujemy kilku sąsiednich elementów. Na granicach należy pobrać o tyle więcej elementów ile wynosi suma długości najdłuższego filtra i jego przesunięcia. Część wątków uczestniczy tylko w ładowaniu danych do pamięci współdzielonej bez wykonywania obliczeń. Ze względu na małą długość filtrów w porównaniu do liczby wątków nie wpływa to znacząco na efektywność. Rysunek 24: Obliczanie współczynników falkowych z wykorzystaniem pamięci współdzielonej 41

Na rysunku 24 przedstawiony został schemat obliczania współczynników falkowych z wykorzystaniem pamięci współdzielonej. Wykorzystana musi być funkcja syncthreads() synchronizująca wątki w ramach bloku. Wątki nie biorące udziału w obliczaniu współczynników kończą działanie po wywołaniu funkcji syncthreads(). Wszystkie wątki przy liczeniu transformaty wykorzystują współczynniki filtrów dolno i górnoprzepustowego. Naturalnym miejscem do przechowywania tych wartości jest pamięć stałych. Współczynniki filtrów są umieszczane w pamięci stałych przez hosta przed uruchomieniem kerneli. Obliczenie transformaty dla sygnałów wielowymiarowych odbywa się poprzez osobne obliczenie transformaty jednowymiarowej po każdym z wymiarów. Dla obrazu wykonujemy więc najpierw obliczenie transformaty po wierszach, a następnie po kolumnach. Dzięki wykorzystaniu pamięci współdzielonej znacznie uprościła się implementacja poszczególnych kerneli. Każdy z nich ładuje najpierw dane w ramach bloku do pamięci współdzielonej. Na rysunku 25 pokazane jest ładowanie danych w przypadku transformaty obrazu. Należy oczywiście uwzględnić dodatkowe elementy na granicach. Rysunek 25: Załadowanie danych do pamięci współdzielonej w przypadku transformaty falkowej obrazu Po załadowaniu danych do pamięci współdzielonej, możemy zastosować algorytm konwolucji wykorzystujący współczynniki z pamięci współdzielonej i współczynniki filtrów z pamięci stałych. Dzięki wstępnemu załadowaniu danych do tej pamięci nie jest wymagane złożone indeksowanie. Kernele dla transformaty jedno, dwu i trójwymiarowej mają identyczną strukturę. W pierwszym kroku ładują one dane do pamięci współdzielonej, np. w sekwencji wideo ładowana jest kolumna danej ramki. Po zsynchronizowaniu wątków wywoływana jest funkcja obliczająca i-ty współczynnik falkowy na podstawie danych zgromadzonych w pamięci współdzielonej. Następnie otrzymany współczynnik jest zapisywany w pamięci globalnej przeznaczonej na wynik. 42

Transformata odwrotna działa w analogiczny sposób z tą różnicą, że każdy z wątków oblicza tylko jeden współczynnik. Musi on najpierw załadować do pamięci współdzielonej jeden współczynnik wysokich częstotliwości i jeden współczynnik niskich częstotliwości. Między pobrane wartości wstawiane są od razu zera (rysunek 26). Następnie wywoływana jest generyczna funkcja obliczająca transformatę odwrotną na podstawie przekazanych w pamięci współdzielonej danych. Zrekonstruowany element zapisywany jest w pamięci globalnej. Rysunek 26: Obliczenie transformaty odwrotnej z załadowaniem danych pasma niskich i wysokich częstotliwości do pamięci współdzielonej Posiadając zaimplementowane funkcje kerneli należy dla wybranych danych wybrać odpowiednią ilość bloków i wywołać funkcje kerneli. Trzeba również wziąć pod uwagę liczbę poziomów dekompozycji. Poniżej umieściłem kod wywoływany przy obliczaniu transformaty falkowej dla kilku poziomów dekompozycji w obrazie. while ( l e v e l s > 0) { // C a l c u a l t e dimensions of b l o c k f o r row transform... // C a l c u a l t e dwt on d e v i c e f o r each row forwardtransform2drow<<<numblocks, threadsperblock > > >(...); // C a l c u a l t e dimensions of b l o c k f o r row transform... // C a l c u a l t e dwt on d e v i c e f o r each column forwardtransform2dcolumn<<<numblocks, threadsperblock > > >(...); } currwidth = ( currwidth +1) / 2 ; currheight = ( currheight +1) / 2 ; 43

7.4 Mechanizmy pomiaru czasu Aplikacja posiada wbudowane w siebie mechanizmy pomiaru czasu wykonania poszczególnych kroków algorytmu kompresji/dekompresji. Dzięki temu możliwe jest określenie zysku uzyskanego przy implementacji CUDA. Standardowym podejściem przy pomiarze czasu jest wykorzystanie funkcji clock() z biblioteki time.h. Taki zegar posiada jednak niską rozdzielczość przez co nie mógł być wykorzystany w mojej aplikacji. Do pomiaru czasu wykonania przy pomocy zegara CPU można wykorzystać funkcję clock gettime(), która posiada większą rozdzielczość. Należy jednak mieć na uwadze, że część wywołań CUDA jest asynchroniczna (m.in. wywołania kerneli) [13]. Aby możliwy był poprawny pomiar czasu wykonania, należy wówczas wywołać przed zatrzymaniem zegara funkcję cudathreadsynchronize(). Można tego uniknąć wykorzystując CUDA Event API, które pozwala na tworzenie zdarzeń (cudaeventcreate) i zarejestrowanie czasu (cudaeventrecord) poprzez znacznik czasu. Synchronizacja wywołań asynchronicznych nie jest wówczas wymagana. Taki właśnie sposób wykorzystałem w swojej aplikacji. Pomiarowi czasu zostały poddane przedstawione na rysunku 27 kroki algorytmu kompresji. Dla implementacji CUDA uwzględniłem czas konieczny na zaalokowanie pamięci urządzenia oraz na wykonanie transferów. Kwantyzacja i kodowanie arytmetyczne zostały uwzględnione razem, ze względu na sposób implementacji. Współczynniki falkowe są kwantyzowane jeden po drugim i od razu kodowane koderem arytmetycznym. Nie jest więc możliwy oddzielny pomiar czasu wykonania tych kroków. Rysunek 27: Kroki algorytmu kompresji poddane pomiarowi czasu dla implementacji CPU i GPU 44

Rysunek 28: Kroki algorytmu dekompresji poddane pomiarowi czasu dla implementacji CPU i GPU Analogiczny pomiar czasu działania jest zaimplementowany przy algorytmie dekompresji (rysunek 28). Dekodowanie arytmetyczne oraz dekwantyzacja została uwzględniona razem. Zdekodowane koderem arytmetycznym symbole są od razu poddawane dekwantyzacji. Nie jest wykorzystywany żaden pośredni bufor przez co nie jest możliwy oddzielny pomiar czasu. Na obu rysunkach 27 i 28 przedstawiłem również bloki związane z odczytem i zapisem przy wykorzystaniu biblioteki OpenCV. W kroku tym ujęty jest czas wykorzystywany do odpowiedniego sformatowania danych. Przedstawiony pomiar czasu nie obejmuje całości czasu wykonania programu. Nie są w nim uwzględnione, np. czasy związane z wykonaniem parsowania argumentów wywołania. Czas ten jednak jest zaniedbywalnie mały w porównaniu z czasem wykonania poszczególnych kroków kompresji/dekompresji. 45

8 Weryfikacja W tym rozdziale przedstawione są metody weryfikacji rozwiązania wraz z uzyskanymi wynikami. Zaimplementowany kompresor został przetestowany pod względem skuteczności kompresji oraz szybkości działania. Zbadano efektywność kompresji dla poszczególnych funkcji falkowych i kwantyzatorów. Na potrzeby porównania implementacji CUDA zmierzono czas wykonania dla implementacji na procesorze CPU i GPU. Wykorzystano wbudowane w aplikację mechanizmy pomiaru czasu działania poszczególnych kroków algorytmu. Dodatkowo wykonane zostały syntetyczne testy dla transformaty falkowej sprawdzające precyzję obliczeń na procesorze GPU. Każdy z testów został opisany w oddzielnym podrozdziale wraz z krótkim opisem metodyki wykonania testu. Na początku przedstawiam wybrany zbiór testowy obrazów i sekwencji wideo. 8.1 Zbiór testowy lena 512x512 (768kB) lena gray 512x512 (256kB) barbara 512x512 (256kB) 46

pw 1024x1024 (3MB) globe 2048x2048 (12MB) moon 4096x4096 (48MB) klip wideo crysis 2 640x360, 256 klatek (168,75 MB) 512x256, 256 klatek (96 MB) 256x128, 256 klatek (24 MB) 47

8.2 Precyzja obliczeń Wyniki obliczeń wykonywane na procesorze GPU mogą nieco różnić się od wyników na procesorze GPU. Wykorzystywana przeze mnie karta graficzna wspiera liczby zmiennoprzecinkowe pojedynczej precyzji zbliżone do standardu IEEE 754. W tabeli 5 znajdują się wyniki dla wykonanego testu precyzji obliczeń. Przetestowana została transformata i transformata odwrotna dla sygnałów jedno, dwu i trójwymiarowych z zadaną liczbą poziomów dekompozycji. Danymi wejściowymi były losowe liczby całkowite z zakresu [0-255] (tak jak wartości jasności pikseli). Obliczony został całkowity błąd bezwzględny oraz błąd na symbol. Uzyskane wyniki świadczą o niewielkiej rozbieżności precyzji obliczeń na GPU. Dyskretna transformata falkowa jest stabilna numerycznie i niewielkie niedokładności nie wpływają na uzyskane wyniki. Liczba poziomów dekompozycji Wymiary Całkowity błąd Błąd na symbol DWT 1D 16 16777216 250.073 1.49 10 5 DWT 2D 8 1024 x 1024 18.157 1.73 10 5 DWT 3D 5 512 x 512 x 64 306.206 1.83 10 5 IDWT 1D 16 16777216 213.801 1.73 10 5 IDWT 2D 8 1024 x 1024 102.353 9.76 10 5 IDWT 3D 5 512 x 512 x 64 278.142 1.81 10 5 Tabela 5: Wyniki testów porównujących precyzję obliczeń transformaty CPU i GPU 8.3 Efektywność kompresji Efektywność kompresji stratnej jest oceniana pod dwoma kryteriami: uzyskanej liczby bitów (rate) oraz zniekształceń (distortion). Miarę uzyskanej liczby bitów określa się przy pomocy stopnia kompresji wyrażającego stosunek liczby bitów danych oryginalnych do liczby bitów uzyskanych w wyniku kompresji. Często korzysta się również z pojęcia bitrate określającego liczbę bitów przypadających na jeden symbol zakodowanego sygnału. W przypadku kompresji obrazów i sekwencji wideo jest to liczba bitów przypadających na jeden piksel (bpp - bit per pixel). Do opisania zniekształceń wybrałem powszechnie wykorzystywaną miarę PSNR (Peak signal-to-noise ratio). Wyraża ona stosunek maksymalnej możliwej energii sygnału i energii szumu (w tym przypadku zniekształceń). Wykorzystuje się w niej skalę logarytmiczną, ze względu na różną dynamikę sygnałów, dla których używa się tej miary. Do opisania szumu wykorzystywany jest opisany wcześniej błąd średniokwadratowy MSE 6. Maksymalna energia sygnału MAX to maksymalna możliwa wartość jasności piksela (dla 8 bitów na piksel to 255). 48

Wzór opisujący PSNR ma następującą postać: ( ) MAX 2 P SNR = 10 log 10 MSE ( ) MAX = 20 log 10 MSE (12) Dla obrazów barwnych miara PSNR jest również obliczona dla każdego z kanałów osobno. W testach efektywności kompresji najlepiej jednak korzystać z obrazów w odcieniach szarości, ponieważ uzyskujemy możliwość łatwego porównania skuteczności kompresji z innymi rozwiązaniami. Dodatkowo dla obrazów barwnych jest obliczana miara PSNR po sprowadzeniu obrazu do odcieni szarości. Warto zauważyć, że przy braku zniekształceń błąd średniokwadratowy wynosi 0 i miara PSNR dąży do nieskończoności. W pracy [15], można znaleźć wiele innych miar syntetycznych, jak również miar subiektywnych. W wykonanych przeze mnie testach aplikacji ograniczyłem się do najczęściej wykorzystywanej miary PSNR. W koder wbudowane są mechanizmy pomiaru powstałych w wyniku kompresji zniekształceń, według miary PSNR. Liczba bitów jest obliczana przez pomiar długości powstałego pliku wynikowego. Na podstawie uzyskanej liczby bitów określane jest również odchylenie uzyskanego stopnia kompresji od zadanego. Wszystkie te informacje można uzyskać po podaniu odpowiedniego argumentu wywołania programu. Jako pierwsze przedstawię wyniki porównania dwóch zaimplementowanych kwantyzatorów: kwantyzatora równomiernego (UTQ) i kwantyzatora równomiernego z rozszerzonym przedziałem zerowym (DUTQ). Obrazem testowym jest lena w odcieniach szarości. Dla lepszego zobrazowania uzyskanych wyników wykorzystam przedstawione wcześniej krzywe R-D (rysunek 29). Rysunek 29: Wyniki kompresji dla kwantyzatorów UTQ i DUTQ 49

Zgodnie z przewidywaniami, wykorzystanie kwantyzatora z rozszerzonym przedziałem zerowym umożliwiło uzyskanie mniejszych zniekształceń dla wyższych stopni kompresji (0.5 PSNR dla stopnia 64 i ponad 0.8 PSNR dla stopnia 128). Wraz z wzrostem stopnia kompresji silniej kwantyzujemy współczynniki i rozszerzony przedział zerowy kwantyzuje więcej współczynników do zera. Przy niskiej kompresji wykorzystanie tego kwantyzatora nie daje lepszych wyników. Dalsze testy były przeprowadzane przy użyciu kwantyzatora z rozszerzonym przedziałem zerowym, który charakteryzuje się lepszym stopniem kompresji. Kolejnym testem, który przeprowadziłem było określenie optymalnej liczby poziomów dekompozycji przy kompresji obrazów i plików wideo. Do testów wybrałem obraz lena w odcieniach szarości oraz klip testowy o rozmiarze ramki 640x360 pikseli. Testy zostały wykonane przy użyciu banku filtrów antonini dla stopnia kompresji 32 dla obrazu i 32 dla sekwencji wideo. Pomiarowi został również poddany uzyskany stopień kompresji. W tabeli 6 znajdują się uzyskane wyniki. Dla sekwencji wideo zostały uwzględnione uzyskane wyniki dla poszczególnych kanałów. Poziomy Lena Crysis dekomp. Uzyskany PSNR Uzyskany stopień stopień PSNR PSNR-C Y PSNR-C b PSNR-C r 1 37.75 16.636 32.286 30.161 29.899 37.571 36.029 2 32.05 29.128 32.173 33.318 35.110 42.000 43.676 3 32.45 31.176 32.068 43.761 43.816 51.412 53.287 4 32.24 31.529 32.072 31.437 30.848 40.880 47.325 5 32.00 31.544 32.112 30.086 30.317 40.213 46.753 6 32.00 31.544 7 32.02 31.541 8 32.04 31.537 Tabela 6: Wyniki testów porównujących wpływ liczby poziomów dekompozycji na stopień kompresji Przy kompresji obrazu zadowalające wyniki uzyskujemy dopiero po wykonaniu przynajmniej trzy poziomowej dekompozycji. Wartością optymalną jest 5 i taką też przyjąłem jako domyślną w mojej aplikacji. 4 Wraz z zwiększeniem liczby poziomów dekompozycji zwiększa się ilość podpasm, a co za tym idzie ilość kwantyzatorów, których dane trzeba przesłać (szerokość przedziału kwantyzacji itp.). Należy mieć również na uwadze to, że większa liczba poziomów dekompozycji zwiększa koszt obliczenia transformaty falkowej. W przypadku kompresji wideo optymalną wartość uzyskujemy przy trzypoziomowej dekompozycji. Najważniejsze dla odbiorcy dane zawarte są w kanale z jasnością (Y). Dla każdego z 3 kanałów przeznaczona jest inna ilość bitów. Kanał Y uzyskuje 0.8 budżetu bitowego natomiast kanały C b i C r dostają po 0.1 budżetu bitów. Wyższa jakość kompresji dla kanału z chrominancją wynika z cech zastosowanej transformaty kolorów. 4 Inne obrazy mogą mieć inną wartość optymalną jednak będzie ona blisko tej wartości. 50

Kolejną zbadaną przeze mnie kwestią były wybrane filtry falkowe. W tabeli 7 przedstawione są wyniki testów dla zadanych stopni kompresji przy obrazie barbara. Obraz był kompresowany przy użyciu 5 poziomów dekompozycji. Obraz barbara posiada znacznie więcej krawędzi niż obraz lena, w którym przeważały dość łagodne gradienty. Skutkuje to oczywiście niższą skutecznością kompresji. Stopień kompr. haar daub4 cdf97 antonini 2 47.628 48.401 48.985 49.983 4 37.568 39.259 40.666 40.666 8 30.647 32.642 34.584 34.584 16 25.854 28.278 29.801 29.801 32 23.907 24.766 25.291 25.291 Tabela 7: Wyniki testów porównujących wybrane filtry falkowe według miary PSNR Najlepszymi zestawami filtrów okazały się filtry cdf97 i antonini. Uzyskane przez nie wyniki są prawie takie same. Różnice w ilości bitów i uzyskanej miary PSNR są bardzo małe. Jako domyślny filtr falkowy ustaliłem filtr antonini. Rysunek 30 pokazuje fragment uzyskanego w wyniku kompresji obrazu. Rysunek 30: Wyniki ośmiokrotnej kompresji dla różnych filtrów: A - oryginał, B - haar, C - daub04, D - cdf97 51

Stopień kompresji lena lena gray barbara pw globe moon PSNR 45.651 42.357 40.666 49.148 51.270 50.524 4 PSNR-Y 45.991 49.032 51.348 50.011 PSNR-C b 40.981 51.721 53.243 51.885 PSNR-C r 41.657 51.733 55.499 52.700 PSNR 42.582 36.557 34.583 44.483 49.085 49.000 8 PSNR-Y 42.469 44.463 49.256 48.756 PSNR-C b 39.724 49.157 50.525 49.335 PSNR-C r 39.878 49.416 55.100 51.537 PSNR 38.593 33.929 29.801 37.695 41.553 43.416 16 PSNR-Y 38.005 37.691 41.621 43.380 PSNR-C b 38.506 45.975 47.903 46.437 PSNR-C r 38.503 46.223 53.545 48.910 PSNR 35.693 31.544 25.291 32.477 36.036 39.483 32 PSNR-Y 35.096 32.467 36.070 39.480 PSNR-C b 36.585 42.288 45.507 44.390 PSNR-C r 36.350 42.516 51.835 46.851 PSNR 32.331 29.317 23.201 28.851 32.550 36.723 64 PSNR-Y 31.852 28.828 32.569 36.723 PSNR-C b 32.785 38.543 43.448 42.499 PSNR-C r 33.808 38.921 50.156 44.726 PSNR 29.104 26.815 21.103 25.827 30.076 34.055 128 PSNR-Y 29.032 25.749 30.089 33.990 PSNR-C b 28.193 32.673 41.294 41.374 PSNR-C r 32.105 33.430 48.129 42.932 Tabela 8: Wyniki testów kompresji obrazów dla stopni 4, 8, 16, 32, 64 i 128 według miary PSNR. PSNR dla obrazów barwnych został obliczony po przekształceniu na przestrzeń barw Y C b C r, a także po przekształceniu na obraz monochromatyczny. Stopień kompresji crysis 512x256 PSNR 42.677 32 PSNR-Y 42.746 PSNR-C b 50.285 PSNR-C r 52.701 PSNR 38.630 64 PSNR-Y 38.640 PSNR-C b 47.520 PSNR-C r 49.905 PSNR 34.721 128 PSNR-Y 34.701 PSNR-C b 44.127 PSNR-C r 47.426 Tabela 9: Wyniki testów efektywności kompresji sekwencji wideo crysis 2 dla stopni 32, 64 i 128 według miary PSNR. PSNR został obliczony po przekształceniu na przestrzeń barw Y C b C r, a także po przekształceniu na sekwencję obrazów monochromatyczną. 52

8.4 Szybkość działania Dzięki wbudowanym w aplikację mechanizmom pomiaru czasu udało się precyzyjnie określić ile czasu zajmuje wykonanie poszczególnych kroków algorytmu kompresji/dekompresji. Głównym celem było oczywiście porównanie efektywności implementacji CUDA z implementacją CPU. Każdy z testów czasu działania był pięciokrotnie powtarzany, a z zmierzonych czasów była wyciągnięta średnia. Jako pierwszą porównałem efektywność działania dla obrazów, różnych rozmiarów. Wybrałem obrazy barwne, aby określić korzyść z wykorzystania obliczeń GPU. Szczegółowe wyniki przedstawione są w tabelach 10, 11, 12, 13 na końcu podrozdziału. Na wykresach 31 i 32 przedstawiłem czas działania dla kompresji i dekompresji obrazów dla poszczególnych rozmiarów. Dla obrazu o rozmiarze 512x512 pikseli nie ma w praktyce zysku z wykorzystania implementacji GPU. Przy obrazach większego rozmiaru zysk staje się zauważalny. Rysunek 31: Czas kompresji obrazów dla implementacji CPU i CUDA Rysunek 32: Czas dekompresji obrazów dla implementacji CPU i CUDA 53

Jak da się zauważyć zysk przy dekompresji jest znacznie większy niż przy kompresji. Wynika to z tego, iż większą część czasu działania przy kompresji zajmuje wykonanie optymalizacji RD (rysunki 33 i 34). Dlatego pomimo blisko dziesięciokrotnego przyśpieszenia obliczania transformaty falkowej dla obrazu 4096x4096 uzyskałem wzrost wydajności rzędu 40 procent. Dekompresja nie wymaga wykonania optymalizacji RD, więc przyśpieszenie transformaty odwrotnej skutkuje ponad trzykrotnym przyśpieszeniem czasu działania (rysunki 35 i 36). Implementacja transformaty kolorów na procesorze GPU jest średnio kilkunastokrotnie szybsza niż na CPU. Jednak ze względu na znikomy koszt w porównaniu z pozostałymi krokami nie wpływa ona znacząco na całkowitą efektywność kompresora. Należy również zwrócić uwagę na koszty alokacji i transferów danych na urządzeniu. Dla transformaty dwuwymiarowej koszty alokacji i transferów stanowią do 20 procent całego czasu przetwarzania na GPU. Przy dekompresji znaczący czas zaczyna zajmować zapis do innego formatu pliku wykonywany przez bibliotekę OpenCV. Rysunek 33: Czas wykonania kompresji dla implementacji CPU Rysunek 34: Czas wykonania kompresji dla implementacji CUDA 54

Rysunek 35: Czas wykonania dekompresji dla implementacji CPU Rysunek 36: Czas wykonania dekompresji dla implementacji CUDA Dla kompresji i dekompresji sekwencji wideo uzyskałem analogiczne wyniki (rysunki 37 i 38) jak dla obrazów. Szczegółowe wyniki znajdują się w tabelach 14, 15, 16 i 17. Zdecydowaną większość czasu kompresji zajmuje wykonanie optymalizacji RD (dla sekwencji wideo 640x360 ponad 70 procent). Da się również zauważyć mniejszy stosunek czasu obliczenia transformaty na CPU do czasu obliczenia na GPU. Dla transformaty trójwymiarowej wynosi on około trzech. Mniejsza efektywność ma swoje źródło w dostępie do pamięci. Przy transformacie trójwymiarowej dostęp do pamięci zazwyczaj nie odbywa się po kolejnych fragmentach pamięci 5 przez co nie jest wykorzystywana technika memory coalescing. Współczynniki ładowane do pamięci współdzielonej bloku są zazwyczaj fizycznie rozroszone w pamięci głównej. 5 dostęp po kolejnych elementach w pamięci ma miejsce przy transformacie po wierszach 55

Rysunek 37: Czas kompresji wideo dla implementacji CPU i CUDA Rysunek 38: Czas dekompresji wideo dla implementacji CPU i CUDA Rysunek 39: Czas kompresji wideo dla implementacji CUDA Rysunek 40: Czas dekompresji wideo dla implementacji CUDA 56