Grafika Komputerowa - Barwy I Histogram

You might also like

Download as pdf or txt
Download as pdf or txt
You are on page 1of 1

Gra ka Komputerowa GRK GRK english Zasady zaliczenia Grading terms

Barwy i histogram
Wprowadzenie
Niniejsze ćwiczenie ma na celu omówienie metod zapisu barwy w pikselach obrazów rastrowych. Aby je wykonać wczytamy najpierw zdjęcie do
naszego programu, a następnie przeanalizujemy barwy poszczególnych pikseli na kilka różnych sposobów.

Wyświetlenie obrazu
Zacznijmy więc dzisiejsze ćwiczenie. Pierwszym krokiem będzie wczytanie obrazu i wyświetlenie go w odpowiednim miejscu. Do wczytania obrazu
użyjemy funkcji loadImage(). Funkcja ta zostanie uruchomiona w wydarzeniu preload(). Jest ono uruchamiane zanim program zacznie rysować.
Dzięki umieszczeniu wczytywania obrazów w tym miejscu, mamy pewność, że zostanie on w pełni wczytany, zanim go spróbujemy wyświetlić.

Aby go narysować, korzystamy z prostej funkcji image(). Bierze ona przynajmniej 3 argumenty: obraz, jaki chcemy narysować oraz współrzędne x i y
miejsca na canvas'ie, gdzie ma się znajdować górny lewy róg narysowanego obrazu. Następujący program wczytuje i wyświetla obraz, jakiego
będziemy używać podczas tego ćwiczenia:

<script src="//cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.7/p5.js"></script>
<script type="text/javascript">
function preload() {
img = loadImage("https://raw.githubusercontent.com/scikit-image/scikit-
image/master/skimage/data/astronaut.png");
}
function setup() {
createCanvas(512,512);
image(img, 0, 0);
}
</script>

Model kolorów RGB


(zadanie na ocenę 3)
Celem tego zadania będzie podział obrazu na jego 3 składowe: R, G i B oraz narysowanie ich obok siebie, w następujący sposób:

Zacznijmy od stworzenia dodatkowych zdjęć do przechowywania 3 wariantów oryginalnego zdjęcia astronautki, które nazwiemy img_r, img_g i
img_b. Do stworzenia pustego obrazu użyjemy funkcji createImage:

img_r=createImage(256,256);

Użyj powyższego polecenia do stworzenia każdego z trzech wariantów zdjęć. Następnie umieść je wszystkie bezpośrednio pod miejscem w kodzie,
gdzie znajduje się polecenie do wczytywania oryginalnego zdjęcia.

Następnie musimy przeskalować oryginalne zdjęcie o połowę. Chodzi nam o to, żeby na canvas'ie o rozmiarze 512x512 zmieścić 4 obrazy, tak jak na
obrazku powyżej. Oryginalnie obraz ma rozmiar 512x512 pikseli - chcemy go przeskalować na rozmiar 256x256. Użyjemy do tego funkcji resize():

img.resize(256,256);

Umieść powyższe polecenie wewnątrz funkcji setup() po utworzeniu canvas'u poleceniem createCanvas. Po obejrzeniu jak wszystko działa, usuń na
razie rysowanie tego obrazu na ekranie.

Żeby zmodyfikować zawartość obrazu, musimy mieć dostęp do tablicy pikseli. Żeby to osiągnąć musimy skorzystać z dwóch dodatkowych poleceń.
Funkcja loadPixels (wywołana na obiekcie obrazu) służy do skopiowania zawartości zdjęcia do tablicy pixels. Zanim się podejmiemy modyfikacji tej
tablicy, musimy najpierw wywołać funkcję loadPixels! Po dokonaniu zmian w tablicy pikseli, należy wywołać funkcję updatePixels żeby
zaktualizować te zmiany na samym obrazie.

Tablica pixels jest zorganizowana jako jedna długa tablica wartości 1-bajtowych ułożonych po kolei (dokumentacja tutaj):
każda grupa 4 kolejnych wartości to jeden piksel, składający się z wartości R,G,B i A (określające alpha albo przezroczystość)
w następnej kolejności mamy grupy width pikseli (czyli grup 4-elementowych) składających się na jeden wiersz
na końcu, powyższe wiersze się układają w height elementów, tworzących pełny obraz
dla ekranów z technologią Retina, sprawa jest trochę bardziej skomplikowana, gdyż ilość pikseli w każdym wierszu i ilość wierszy w obrazie jest
zwiększona d-krotnie (gdzie d można odczytać z funkcji pixelDensity) - w związku z tym ilość pikseli w obrazie Retina jest d*d razy większa niż na
normalnym ekranie

Żeby przeiterować wszystkie piksele obrazu na zwykłym monitorze, należy użyć następującej procedury:

img.loadPixels();
for(x=0;x<img.width;x++)
for(y=0;y<img.height;y++) {
pos=4*(y*img.width+x);
img.pixels[pos] //to jest wartość dla R
img.pixels[pos+1] //to jest wartość dla G
img.pixels[pos+2] //to jest wartość dla B
img.pixels[pos+3] //to jest wartość dla A
}
img.updatePixels();

W przypadku ekranów Retina, procedura jest nieco bardziej złożona:

img.loadPixels();
d=pixelDensity();
for(x=0;x<img.width;x++)
for(y=0;y<img.height;y++)
for(dx=0;dx<d;dx++)
for(dy=0;dy<d;dy++) {
pos=4*((y*d+dy)*img.width*d+(x*d+dx));
img.pixels[pos] //to jest wartość dla R
img.pixels[pos+1] //to jest wartość dla G
img.pixels[pos+2] //to jest wartość dla B
img.pixels[pos+3] //to jest wartość dla A
}
img.updatePixels();

Skopiuj wartości r, g i b, do odpowiednich kanałów odpowiednich pikseli w zdjęciach img_r, img_g i img_b. Nie zapomnij też ustawić wartość alpha
każdego piksela w tych zdjęciach na 255! Domyślnie całe zdjęcie jest ustawione na 0, więc nawet jeśli coś na nim narysujemy, bez aktualizacji
wartości alpha, to otrzymamy zupełnie przezroczysty obraz. Wszystkie te operacje można zrobić w jednej pętli.

Po wypełnieniu zdjęć odpowiednimi wartościami pikseli, na zewnątrz pętli for należy uruchomić polecenie updatePixels(), żeby przenieść zawartość
bufora pikseli do właściwego obrazu. Polecenie to należy uruchomić dla wszystkich zdjęć, w których zmieniono zawartość.

Na sam koniec należy narysować wszystkie obrazy na canvas'ie. Używamy do tego już wspomnianego wyżej polecenia image(). Zdjęcie ze
składową R należy umieścić w punkcie (0,0), G w punkcie (256,0), a B w punkcie (0,256).

Czwarty obraz powinien wyglądać tak jak oryginalny, ale żeby zademonstrować działanie modelu RGB, stworzymy kolejny obraz, który będzie sumą 3
składowych R,G i B. W funkcji preload() stwórz jeszcze jeden obraz o nazwie img_sum. Na końcu metody setup wykonaj trzy operacje dodawania
metodą blend() (dokumentacja pod tym linkiem):

img_sum.blend(img_r,0,0,256,256,0,0,256,256,ADD);

Dodaj w ten sposób wszystkie 3 składowe do obrazu sumy i narysuj ten obraz w ostatnim miejscu, na współrzędnych (256,256).

Model kolorów HSV


Model HSV jest odpowiednikiem modelu RGB - każdy kolor z przestrzeni RGB można wyrazić wartościami z przestrzeni HSV. Model ten jest jednak
wykorzystywany, ponieważ jest bardziej intuicyjny i łatwiejszy w codziennym użyciu. W przypadku RGB łatwo zapamiętać kilka podstawowych barw,
ale trudno zmienić np. kolor niebieski na pomarańczowy - w przestrzeni RGB często trudno jest przejść z jednego koloru na inny, jaki nas interesuje.
Model HSV jest o wiele wygodniejszy, ponieważ parametry tego modelu są bardziej zbliżone do konkretnego przypadku użycia, z jakim się
spotykamy w grafice.

Wartość H określa odcień barwy (ang. hue) - np. niebieski, żółty albo różowy. Wartość S określa saturację, albo bardziej formalnie chromatyczność
barwy - jest to jakby jaskrawość koloru, np. kolor czerwony może być albo bardzo jaskrawy albo mniej jaskrawy, czyli bardziej szary. Wartość
saturacji równa 0 tworzy kolor szary, niezależnie od odcienia koloru (wartości H). Wartość V (ang. value) to po prostu intensywność koloru w skali od
czarnego do najmocniejszego. Ponieważ mamy do czynienia z addytywnym modelem barwy, a więc najłatwiej porównywalnym do światła, wartość
V to ilość tego światła występująca w danym kolorze. Inna nazwa tego samego parametru to "brightness" czyli jasność. Model HSV jest często
nazywany HSB (to jest dokładnie ten sam model).

Jest jeszcze inny podobny model, o nazwie HSL. Model ten jako ostatniego parametru używa wartości o nazwie "lightness". Jest ona bardzo
podobna do "brightness", ale ma jednak trochę inną definicję matematyczną. Wartość ta również definiuje jasność jako wartość od czerni do
największej intensywności danej barwy, ale tu nie przestaje i modeluje jasność danej barwy jeszcze dalej, aż do bieli. Różnicę miedzy HSV i HSL
najłatwiej można zrozumieć używając tych obrazków:

Spróbujmy teraz odtworzyć ten model barw ręcznie. Najłatwiej będzie zacząć od ostatniej wartości, czyli w pierwszej kolejności obliczymy V (albo
L), potem S, a na końcu najtrudniejszy będzie H.

Value/Brightness/Lightness

(zadanie na ocenę 3.5)


Zacznijmy od zmodyfikowania poprzedniego zadania. Zamieńmy img_r na img_h, img_g na img_s i img_b na img_v. Skasujmy zdjęcie img_sum. Na
samym końcu funkcji setup, skasujmy trzy linie dodawania składowych r,g,b do img_sum i w ostatniej linii zamieńmy img_sum po prostu na img. We
wszystkich pozostałych miejscach w funkcji setup, zamieńmy img_{r,g,b} na img_{h,s,v}. Tylko w pętli for, usuńmy wszsytkie linie ustawiające
wartości do zdjęć img_{h,s,v} i zostawmy je na razie puste. Powinniśmy teraz otrzymać wynik, w którym widać tylko oryginalny obraz w dolnym
prawym rogu (pozostałe trzy są puste, więc i przezroczyste).

Żeby poniższe wzory były poprawne, należy najpierw znormalizować wartość r, g i b do zakresu (0..1), dzieląc je przez 255:

r=img.pixels[pos]/255;
g=img.pixels[pos+1]/255;
b=img.pixels[pos+2]/255;

Do wyliczenia wartości V, warto najpierw policzyć 2 pomocnicze zmienne (bezpośrednio po ekstrakcji wartości r,g,b powyżej): maksymalną
składową RGB oraz minimalną.

cmax = Math.max(r,g,b);
cmin = Math.min(r,g,b);

Żeby określić V, wystarczy przypisać do niej wartość cmax:

v=cmax;

Potem ustawmy tę wartość jako jasność (w odcieniach szarości) piksela na zdjęciu img_v, ale pomnożoną przez 255 (zakres odcieni szarości). Żeby
sobie ułatwić to zadanie, zamiast manipulować tablicą pixels, użyjmy polecenia set():

img_v.set(x,y,255*v);

Żeby to polecenie zadziałało, musimy najpierw wyliczyć współrzędne x i y na podstawie indeksu piksela i. Można to zrobić umieszczając przed
powyższym poleceniem dwa następujące obliczenia:

x=(i/4)%256;//indeks kolumny wewnątrz wiersza


y=(i/4)/256;//indeks wiersza

Po uruchomieniu tak napisanego programu, powinniśmy otrzymać następujące zdjęcie. Zauważ, że wartość jasności rzeczywiście odpowiada
intensywności koloru w danym punkcie.

Zamiast V, możemy obliczyć L w bardzo podobny sposób. Lightness jest zdefiniowana jako średnia arytmetyczna między cmin i cmax:

l=(cmax+cmin)/2;

Jeśli zamiast v do obrazu img_v podstawimy zmienną l, otrzymamy bardzo podobny obraz, ale trochę bardziej przyciemniony. Do zaliczenia
ćwiczenia można wybrać dowolną z tych dwóch metod.

Saturation (zadanie na ocenę 4)


Chromatyczność jest zdefiniowana po prostu jako różnica między maksymalną a minimalną składową RGB:

c = cmax-cmin;

Niestety wartość ta nie jest zbyt wydajna do reprezentowania barwy w przestrzeni 2D - większość przestrzeni jest niewykorzystana, ponieważ wiele
różnych ustawień par wartości V i C nie ma odpowiednika w barwach RGB. Rozwiązaniem tego problemu jest przeskalowanie wartości
chromatyczności względem jasności, co rozciąga przestrzeń w sposób pokazany na zdjęciu poniżej. Analogiczne rozwiązanie jest stosowane w
przypadku kodowania HSL.

Taka wartość chromatyczności jest nazywana saturacją. W przypadku modelu HSV wyliczamy ją za pomocą tego wzoru:

s=c/cmax;

Trzeba jednak uwzględnić jeden wyjątek - jeśli wartość cmax jest równa 0, to wartość s również ustawiamy na 0 (żeby uniknąć dzielenia przez 0).

W przypadku modelu HSL wzór jest analogiczny (z takim samym wyjątkiem):

s=c/(1-Math.abs(2*l-1));

Po narysowaniu zdjęcia saturacji, powinniśmy otrzymać taki wynik. Zauważ, że na tym zdjęciu obszary białe i szare mają ciemniejszy odcień, a te o
jaskrawym kolorze są białe.

Hue

(zadanie na ocenę 4.5)


Policzenie odcienia koloru jest najtrudniejsze z tych trzech wartości. Żeby określić odcień, musimy go najpierw zaklasyfikować do jednego z 3
odcinków pełnego zakresu odcieni. Potem używamy równań liniowych żeby rzutować pozycję z przestrzeni sześcianu RGB na sześciokąt w
przestrzeni chrominancji:

Można to osiągnąć używając następujących wzorów (te same wzory używamy zarówno w przypadku przestrzeni HSV, jak i HSL):

if(c==0)
h=0;
else if(v==r)
h=((g-b)/c)%6;
else if(v==g)
h=((b-r)/c)+2;
else /*v==b*/
h=((r-g)/c)+4;

Potem należy jeszcze podzielić h przez 6, co da nam wartość od (0..1):

h/=6;

Na końcu warto sprawdzić, czy otrzymana wartość h jest mniejsza od 0 i jeśli tak jest, to dodać do niej 1 (czyli zaimplementować zawijanie wartości
ujemnych):

if(h<0) h+=1;

Tak wyliczona wartość, po podstawieniu do odpowiedniego zdjęcia wygeneruje następujący wynik. Zauważ, że obszary o jednolitej barwie na
oryginalnym zdjęciu (np. tło gwiazd na fladze, paski flagi, kombinezon, twarz i włosy astronautki) mają jednolitą jasność (w odcieniach szarości) na
zdjęciu zawierającym składową H, niezależnie od jasności i jaskrawości tej barwy.

Histogram
(zadanie na ocenę 5)
Informacja o barwie jest niezwykle istotna w grafice komputerowej i istnieje mnóstwo metod wykorzystujących tę wiedzę do analizy i przetwarzania
obrazów. Tu zrobimy tylko jeden, niewielki przykład. Histogram to metoda analizy statystycznej polegająca na oszacowaniu rozkładu wartości
określonej zmiennej. W tym zadaniu policzymy rozkład jasności pikseli w obrazie. Metoda ta umożliwi nam ocenę, czy obraz został prawidłowo
naświetlony, czy jest przypadkiem prze- lub niedoświetlony.

Liczenie histogramu jest całkiem proste. Weźmy następujący zbiór liczb:

{1,4,2,3,2,1,2,6,6,4,3,3,2,2}

Histogram mówi nam, ile jakich liczb mamy w zbiorze:

{ 1 => 2, 2 => 5, 3 => 3, 4 => 2, 5 => 0, 6 => 2}

Liczby te są najczęściej prezentowane jako wartość procentowa względem liczby elementów w danym zbiorze:

{ 1 => ~14%, 2 => ~36%, 3 => ~21%, 4 => ~14%, 5 => 0%, 6 => ~14%}

W przypadku obrazów, histogram będzie nam mówił, ile pikseli w danym obrazie ma dokładnie taką samą wartość, np. ile jest pikseli białych, ile
czarnych, ile szarych, itd.

Zacznijmy od programu napisanego w akapicie zatytułowanym Wyświetlanie obrazu powyżej. Żeby zmienić obraz na mapę szarości użyjemy filtru:

img.filter('gray');

Usuńmy teraz polecenie do wyświetlania obrazu (ostatnie w metodzie setup) i zmieńmy rozmiar canvas'u na 256x256 - będziemy rysować histogram
jasności pikseli, a te mogą przyjmować tylko 256 różnych wartości.

Zanim policzymy histogram, musimy najpierw stworzyć tablicę do przechowywania jego wartości. W Javascrip'cie tablicę tworzymy podając ilość
elementów w argumencie konstruktora:

var tablica = new Array(10);

Dodatkowo chcemy, żeby była ona na początku wypełniona zerami. Do tego możemy użyć prostej metody fill. Stwórz tablicę o nazwie histogram
zawierającą 256 elementów i wypełnij ją zerami.

Teraz, w pętli przeiteruj wszystkie piksele zdjęcia (nie zapomnij również o poleceniu loadPixels) i dla każdego piksela zwiększ o jeden wartość w
tablicy, w miejscu odpowiadającym wartości danego piksela. Żeby poprawnie odczytać wartość piksela na zdjęciu w odcieniach szarości, możesz
użyć dowolnej wartości R, G albo B - wszystkie 3 będą zawsze dokładnie takie same. Możesz użyć zdjęcia 256x256 albo 512x512, aczkolwiek
większe zdjęcie da trochę bardziej precyzyjny wynik.

Po obliczeniu histogramu, należy go wyświetlić na ekranie. Najpierw skasujmy cały obraz metodą background, a potem w pętli przeiterujmy
poszczególne elementy tablicy histogramu i narysujmy paski długością odpowiadające wartościom w tablicy. Najłatwiej do tego użyć metody line
(ustawiając wcześniej stroke na kolor inny niż kolor tła).

Ostatni efekt powinien wyglądać mniej więcej tak:

Żeby odpowiednio wyskalować wysokość słupków, warto je najpierw podzielić przez maksymalną wartość w tablicy (tutaj wskazówka), a potem
pomnożyć przez wysokość canvas'a (czyli 256). Można tę wartość jeszcze ewentualnie dodatkowo przeskalować (pomnożyć lub podzielić przez
jakąś wartość), żeby poradzić sobie z bardzo odstającą ilością niektórych wartości pikseli (na tym zdjęciu jest przykładowo bardzo dużo pikseli o
kolorze czarnym).

Zauważ, jak wygląda histogram dla zdjęć o różnej jakości ekspozycji. Po lewej mamy zdjęcie niedoświetlone, a po prawej prześwietlone:

Mała uwaga: jeśli chcesz używać zdjęć z innych adresów URL niż serwer, na którym znajduje się Twój program, to może się zdarzyć, że program ten
nie zadziała ze względu na CORS. Najłatwiej rozwiązać ten problem, wrzucając zarówno kod, jak i obrazki na ten sam serwer (z tego powodu nie
można tego używać na stronie JS.do). Innym rozwiązaniem jest wgranie obrazów na usługę, która do udostępniania zdjęć ma wpisany nagłówek
Access-Control-Allow-Origin "*" (np. github.com) albo konfiguracja własnego serwera WWW w celu dodania tego nagłówka.

You might also like