Professional Documents
Culture Documents
Grafika Komputerowa - Barwy I Histogram
Grafika Komputerowa - Barwy I Histogram
Grafika Komputerowa - Barwy I Histogram
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>
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();
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).
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
Ż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);
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:
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.
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).
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
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;
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.
{1,4,2,3,2,1,2,6,6,4,3,3,2,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:
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).
Ż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.