Kirjoittaja: os
Kirjoitettu: 07.01.2006 – 07.01.2006
Tagit: grafiikka, koodi näytille, vinkki
Gauss-sumennus on yleinen kuvasuodin, jota käytetään laajalti kaikessa signaalinkäsittelyssä tietokonegrafiikasta radioteleskooppikuvien prosessointiin. Sen tuloksena saadaan laadukas sumennus, jonka voimakkuus voidaan säätää tarkasti halutunlaiseksi.
Sumennus tehdään käytännössä sekoittamalla kuvan jokaiseen väripisteeseen arvoja viereisistä pisteistä tietyssä suhteessa. Matemaattisesti tämä on aproksimaatio eräästä integraalista, jota kutsutaan konvoluutiotuloksi.
Nimi Gauss-sumennus tulee siitä, että tässä sumennuksessa sekoitussuhde kullekin pisteelle noudattaa normaalijakaumaa eli Gaussin käyrää y = k * exp(x2/s2), missä k on vakio, x on etäisyys sumennettavasta kuvapisteestä ja keskihajonta, s, vastaa sumennuksen voimakkuutta. Jos käyrän ja x-akselin rajaama ala on yksi, ei kuvan kirkkaus muutu sumennettaessa.
Tehokkain tapa tehdä sumennus on laskea y:n arvot valmiiksi eräänlaiseen maskiin, pieneen taulukkoon siten, että kohta x=0 on taulukon keskellä. Maski voidaan ajatella siirrettävän jokaisen kuvapisteen "päälle", jonka jälkeen tälle pisteelle lasketaan uusi väri viereisten pisteiden eräänlaisena painotettuna keskiarvona maskin osoittamien suhteiden mukaan.
Periaatteessa Gaussin käyrän ala on aina yksi, kun k:n arvo on oikea. Toisaalta y ei millään etäisyydellä ole nolla, joten maskin koon pitäisi olla ääretömän suuri. Käyrä kuitenkin lähestyy nopeasti nollaa, kun |x| kasvaa. Hyvä kompromissi on valita maskin kooksi keskihajonnan jokin pienehkö monikerta, ja varmistaa, että sen alkioiden summa on yksi.
Vaikka kaksiulotteista kuvaa sumennettaessa maskin pitäisi periaateessa olla kaksiulotteinen, voidaan sumennus tasoon piirretyn Gaussin käyrän symmetrisyyden johdosta tehdä myös sumentamalla sekä kuvan rivit että sarakkeet yksiulotteisilla maskeilla, mikä on paljon nopeampaa.
Toivottavasti koodi valottaa asiaa. Koodasin tämän C++:lla, jotta samaa funktiota voi käyttää haluamallaan tavalla ilman SDL-rajapintaa.
Osoitteesta http://olento.dyndns.org/docs/gauss.zip löytyy pieni esimerkkisovellus SDL:lle.
classes.h
#include <math.h> typedef unsigned char BYTE; typedef unsigned short int WORD; typedef unsigned long int DWORD; class RGB48 { // RGB-pikseli 16-bittisillä värikanavilla public: WORD r,g,b; RGB48() { } RGB48(WORD r1, WORD g1, WORD b1) { r=r1; g=g1; b=b1; } void operator+=(RGB48 p) { r+=p.r; g+=p.g; b+=p.b; } RGB48 operator*(WORD k) { return RGB48(((DWORD)r*k)>>16, ((DWORD)g*k)>>16, ((DWORD)b*k)>>16); } }; class Image { // abstrakti kuvaluokka public: virtual bool GetDimensions(int &, int &) = 0; virtual RGB48 GetPixel(int, int) = 0; virtual void SetPixel(int, int, RGB48) = 0; };
gauss.cpp
#include "classes.h" #define GAUSSIAN_KERNEL_RANGE 2 // konvoluutiomaskin koko ("laatu") inline int truncate(int x, int max, int min=0) // rajaa x:n välille [min, max) { if(x<min) return min; if(x>=max) return max-1; return x; } /* Gauss-sumennus sumentaa kuvaa sekoittamalla sen jokaiseen väripisteeseen arvoja viereisistä väripisteistä tietyssä suhteessa. Gauss-sumennuksessa tämä sekoitusjakauma vastaa normaalijakaumaa (Gaussin käyrä), jonka keskihajonta annetaan "stddev"-parametrissa. Ensimmäinen parametri on pointteri abstraksiin kuvaolioon käyttö esim.: GaussianBlur(&(SDLImageWrap(screen)),3.0); */ bool GaussianBlur(Image *img, float stddev) { int xsize, ysize, ksize, k_center; // kuvan ja konvoluutiomaskin mitat float sum, val, *f_kernel; // liukulukumaski RGB48 pixel, *temp_image; // väliaikainen kuva WORD *kernel; // (konvoluutio)maski if(!img->GetDimensions(xsize,ysize)) return 0; // lasketaan maskille sopiva resoluutio k_center = (int)(stddev*GAUSSIAN_KERNEL_RANGE); ksize = 2*k_center+1; temp_image = new RGB48[xsize*ysize]; // varataan muisti f_kernel = new float[ksize]; kernel = new WORD[ksize]; if(!f_kernel || !kernel || !temp_image) return 0; sum = 0; // piirretään maskiin symmetrinen Gaussin käyrä y = k*e^(0.5*x^2/s^2)... for(int i=0; i<=k_center; i++) { val = exp(-i*i*0.5/(stddev*stddev)); if(i) sum += val; sum += val; // ...ja lasketaan sen ala f_kernel[k_center+i] = f_kernel[k_center-i] = val; } /* Kopioidaan liukulukumaskin alkiot 16-bittiseen maskiin siten, että niiden summa (ala) on 1 (eli 0xFFFF). Näin kuva ei kirkastu eikä tummu sumennuksessa. */ for(int i=0; i<ksize; i++) kernel[i] = (int)(0xFFFF * f_kernel[i] / sum); /* Muodostetaan väliaikainen kuva alkuperäisen kuvan rivien ja sekoitusmaskin konvoluutiotulona. Tässä kuvan reunojen väripisteet toistuvat (x:n arvot rajataan "truncate"-funktiolla välille [0, xsize[), jotta sumennus näyttäisi jatkuvan luonnollisesti myös kuvan reunan yli. */ for(int y=0, offset=0; y<ysize; y++) { for(int x=0; x<xsize; x++) { temp_image[offset] = RGB48(0,0,0); // musta piste, josta ... for(int i=0; i<ksize; i++) temp_image[offset+x] += // ... summaamalla muodostetaan sumennettu piste img->GetPixel(truncate(x-k_center+i, xsize), y) * kernel[i]; } offset+=xsize; } // Muodostetaan lopullinen sumennettu kuva alkuperäiseen kuvaan sumentamalla // väliaikaisen kuvan sarakkeet samalla tavalla. for(int y=0; y<ysize; y++) for(int x=0; x<xsize; x++) { pixel = RGB48(0,0,0); for(int i=0; i<ksize; i++) pixel += temp_image[truncate(y-k_center+i, ysize)*xsize+x]*kernel[i]; img->SetPixel(x,y,pixel); } delete [] f_kernel; delete [] kernel; delete [] temp_image; return true; }
SDLmain.cpp
#include "classes.h" #include <SDL/SDL.h> class SDLImageWrap : public Image { // implementaatio SDL-rajapinnalle private: SDL_Surface *img; public: SDLImageWrap(SDL_Surface *s) { img = s; } bool GetDimensions(int &x, int &y) { if(!img) return 0; x=img->w; y=img->h; return true; } RGB48 GetPixel(int x, int y) { Uint8 *p = (Uint8 *)img->pixels + y*img->pitch + x*3; return RGB48(p[2]<<8,p[1]<<8,p[0]<<8); } void SetPixel(int x, int y, RGB48 color) { Uint8 *p = (Uint8 *)img->pixels + y*img->pitch + x*3; (*p) = color.b>>8; p[1] = color.g>>8; p[2] = color.r>>8; } }; extern bool GaussianBlur(Image *, float); #define BITMAP_FILENAME "kuva.bmp" int main(int argc, char *argv[]) { SDL_Surface *screen, *bitmap; SDL_Event event; int xsize, ysize, mx, my; SDLImageWrap *wrapper; if (SDL_Init(SDL_INIT_VIDEO)<0) { SDL_Quit(); return 1; } bitmap = SDL_LoadBMP(BITMAP_FILENAME); if(bitmap==NULL) { SDL_Quit(); return 1; } xsize = bitmap->w; ysize = bitmap->h; screen = SDL_SetVideoMode(xsize, ysize, 24, 0); if(screen==NULL || SDL_LockSurface(screen)<0) { SDL_Quit(); return 1; } xsize = screen->w; ysize = screen->h; SDL_WM_SetCaption("Gaussian Blur",NULL); while(1) { SDL_UnlockSurface(screen); SDL_BlitSurface(bitmap, NULL, screen, NULL); SDL_LockSurface(screen); SDL_GetMouseState(&mx, &my); GaussianBlur(&(SDLImageWrap(screen)),log(mx+2)*((my+1)/(float)ysize)); SDL_UpdateRect(screen, 0,0,xsize,ysize); while(SDL_PollEvent(&event)) if(event.type==SDL_KEYDOWN || event.type==SDL_QUIT) { SDL_Quit(); return 0; } } return 1; }
Kuinka konetehoa syövä tuo sumennus on? Ajattelin, että siitä voisi saada kivan kentän vaihtumisefektin työn alla olevaan peliini. :)
EDIT: Ainiin, tuossahan oli tuo valmis binääri. Aika hieno efekti, ja kyllähän tuo pelissä toimii kun vain porrastaa pehmennystä jotenkin.
Tupla EDIT: ...ja tuossa pehmennyksessähän varmaan voi käyttää edellistä ruutua kuvana, jolloin ei tarvitse laskea ihan alusta saakka, jolloin tuo varmaan pyörii suht nopeasti.
Jos haluaa lisää nopeutta, kannattaa varmaan karsia aluksi abstraktit luokat pois (Image ---> SDL_Surface).
edit: Ja ajankulutushan on tietysti verrannollinen kuvan mittoihin ja maskin kokoon - eli reaaliaikaisuus saattaa hienommalla resoluutiolla jäädä haaveeksi.
Aina voi käyttää myös laatikkosumennusta (Box blur). Tällöin maskin kaikki arvot olisivat yhtä suuria. Laatikkosumennuksen voi kuitenkin pienellä kikalla toteuttaa hyvin nopeasti ilman maskia näin:
WORD a = 0xFFFF / (2*radius); for(int y=0; y<ysize; y++) { RGB48 sum = RGB24(0,0,0); for(int i=0; i<2*radius; i++) sum += img->GetPixel(truncate(i-radius, xsize), y) * a; for(int i=0; ix<xsize; ix++) { temp_image[iy*xsize+ix] = sum; sum -= img->GetPixel(truncate(x+i-radius, xsize), y) * a; sum += img->GetPixel(truncate(x+i-radius*2, xsize), y) * a; } } // ja sama toiseen suuntaan ...