Kirjautuminen

Haku

Tehtävät

Koodit: C++: Gauss-sumennus

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

Kommentit

Meitsi [07.01.2006 20:03:27]

#

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.

os [07.01.2006 20:27:20]

#

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 ...

Kirjoita kommentti

Muista lukea kirjoitusohjeet.
Tietoa sivustosta