Pagine    Articoli    Prodotti    Forum    Cerca  
Nickname

Password


Non sei registrato?
Registrati a GPI qui!

Puoi anche attivare un vecchio utente GPI e chiedere una nuova password.
I Team

Mappa Team
I nostri utenti

Mappa Utenti
  C++11: Lambda Image Processing
Pubblicato da Dario Oliveri il 2014-01-16 18:22:01

Piccolo articolo su come elaborare immagini usando i  Lambda (E' lungo spiegarne i benefici, ma in pratica usando i Lambda si migliora sia la performance che la mantenibilità).

 

Leggere il formato PPM (Portable Pixel Map)

Il Formato PPM è utile perchè è molto semplice: potete farci vari esperimenti senza dover aggiungere come dipendenza una libreria esterna o (peggio ancora) implementarne una vostra.

(Per visualizzare immagini PPM vi serve GIMP, MSPaint non legge PPM)

 

Andiamo a creare una classe Immagine per semplificarci le cose:

class Image{

    unsigned int SizeX = 0;
    unsigned int SizeY = 0;
    unsigned char * data = nullptr;

public:

};

E iniziamo ad aggiungere i metodi che ci servono..

 

Leggere files PPM:

    bool loadFromFile(const std::string & file_name){
	std::ifstream file(filename.c_str());
        if(!file){ std::cout < < "file notfound" < < std::endl; return false; }
        std::string header;
        file  > > header;
        std::cout < < "header :" < < header < < std::endl;
        file > > SizeX;
        std::cout < < "SizeX :" < < SizeX < < std::endl;
        file > > SizeY;
        std::cout < < "SizeY :" < < SizeY < < std::endl;

        int colors;
        file > > colors;
        if(colors!=255){std::cout < < "wrong color number" < < std::endl; return false; }

        c = new int[SizeX*SizeY*3];
        int index = 0;
        for(int Y = 0; Y < SizeY; Y++)
        for(int X = 0; X < SizeX; X++){
            file > > c[index++];
            file > > c[index++];
            file > > c[index++];

        }
        file.close();
        std::cout < < "Image Loaded (" < < SizeX < < "," < < SizeY < < ")" < < std::endl;
        return true;
    }

 

 Salvare files PPM:

    bool saveToFile(const std::string &filename){
        std::ofstream file(filename.c_str());
        if(!file)
            return false;

        file < < "P3\n";
        file < < SizeX < < " " < < SizeY < < "\n";
        file < < "255\n";

        int idex = 0;
        for(int Y = 0; Y < SizeY; Y++){
            for(int X = 0; X < SizeX; X++){

                file < < data[index++] < < " ";
                file < < data[index++] < < " ";
                file < < data[index++] < < " ";
            }
            file < < "\n";
        }
        return true;
    }

 

 

Un'idea carina che potrebbe venire sarebbe quella sarebbe ora di iniziare a estendere la classe aggiungendo una miriade di metodi per ottenere vari risultati, combinare tra di loro 2 immagini etc (Errore che già feci Piede in boccaqui in image mixer: nothing is truly useless, it always serves as bad example):

 

Malee!!! Finireste con il ripetervi un ciclo "for" per ogni metodo e avreste una marea di metodi da mantenere XD.

 

Attualmente per ottenere il 90% della funzionalità di FreeImageMixer è sufficiente aggiungere solamente 2 metodi (esatto due metodi), e si ottiene un miglioramento della performance di un ordine di grandezza ( Fico ):

 

L'utente crea le funzionalità su uno scheletro pre-esistente

 

Primo metodo: nulla di particolarmente interessante cicliamo l'intera immagine e chiamiamo una lambda per ogni pixel.

 

template < typename Functor >
Image& fill(Functor func){
    unsigned int max_i = sizeY*sizeX*3;
    for(int i=0; i < max_i; i+=3 )
        func(*(data+i),*(data+i+1),*(data+i+2));
    return (*this);
}

 

 

 

Vediamo che anche soltanto invertire i colori di un immagine era abbastanza doloroso prima:

IMAGE_MIXER_START(sizeX,sizeY);

Image I;
I.load("texture.ppm");

Channel R,G,B,Sat; /// BLOAT NON RIUTILIZZABILE

I.getRGB(R,G,B);
Sat = 255;

R = Sat - R;
G = Sat - G;
B = Sat - B;
I.setRGB(R,G,B);
I.save("textureInverted.ppm");

IMAGE_MIXER_END();

 

 

Adesso:

 

void invertiColori(int &R, int &G, int &B){ //SEMPLICE FUNZIONE: RIUTILIZZABILE! 
	R = 255-R;
	G = 255-G;
	B = 255-B;  //DEBUGGARE E' UNA GIOIA COSì
}

int main(){
    Image  I;

    I.load("texture.ppm");
    I.fill ( invertiColori );
    I.save("textureInverted.ppm");

    return 0;
}

 

 

 

Molto meglio no? Ed è quasi 20 volte più veloce;) (e non ho ancora tirato in mezzo multithreading e istruzioni SSE.. sarà per un'altra volta).

 

Le funzionalità le creiamo noi sul momento o le importiamo da qualche altro header. Il motivo di tanta efficienza risiede nel fatto che i dati vengono lavorati "a caldo" sui registri, l'intero array viene caricato una volta sola in memoria:

 

- Leggo dati dall'array

- Lavoro i dati

- Scrivo dati nell'array

 

Il secondo metodo a cui accennavo è quello che permette di utilizzare 2 immagini contemporaneamente (nulla di particolare, stessa idea metodo leggermente diverso):

 

template < typename Functor >
Image & combine2(const Image & other, Functor func){ //per ora ignoro che "other" possa avere una dimensione diversa
    unsigned int max_i = sizeY*sizeX*3;
    for(int i=0; i < max_i; i+=3 )
        func(*(data+i),*(data+i+1),*(data+i+2),
             *(other.data+i), *(other.data+i+1), *(other.data+i+2));
    return (*this);
}

 

 

 

 

esempio di utilizzo:

void interpola(int &R, int &G, int &B, const int & r, const int & g, const int & b){ 
    R = lerp(R,r,0.5); //se avevate già l'interpolazione lineare "lerp" potrete riutilizzarla
    G = lerp(G,g,0.5); //lo stesso vale per le funzioni di libreria tipo "sin" "cos" etc.. ;)
    B = lerp(B,b,0.5);
}

int main(){
    
    Image  I, J;

    I.load("texture.ppm");
    J.load("texture2.ppm");
    I.combine2 (J, interpola );
    I.save("texture.ppm");

    return 0;
}

 

 

 

 Si possono usare anche I Lambda anzichè funzioni in stile C:

 

int main(){
    
    Image  I, J;

    I.load("texture.ppm");
    J.load("texture2.ppm");

    float f = 0.5;

    I.combine2 (J, 
        [=]  //parametri aggiuntivi si possono "catturare per valore"
        (int &R, int &G, int &B, const int & r, const int & g, const int & b){
            R = lerp(R,r,f); 
            G = lerp(G,g,f); 
            B = lerp(B,b,f);

        }
    );
    I.save("texture.ppm");

    return 0;
}

 

 

 

 

bye bye iterators (solo in alcuni casi):

Indirettamente avrete notato che un Container può (in alcuni casi) fare a meno degli iteratori, è sufficiente un metodo che itera da A a B ( o su tutto il container) e su quei dati chiama una lambda (un peccato che nemmeno std::vector permetta ciò!).

 

MEMORY ACCESS PATTERN

La principale cosa a cui stare attenti quando si crea la classe Image, è l'ordine di accesso alle celle di memoria. Per questo articolo ho usato semplicemente un array, ma per altre applicazioni particolari potrebbero essere più efficienti patterns differenti (ad esempio memorizzare le immagini come sezioni quadrate invece che come arrays oppure dividere il lavoro delle lambda su più threads usando una JobsQueue e usare come immagine un array di arrays).

 

La responsabilità di una classe del tipo Image

è quindi fornire un particolare pattern di accesso alla memoria.

 

Implementazioni future:

 

kernels/kernels separabili

 

Ovvero:

-ridimensionamento immagini

-sfocature

-distorsioni (non alle caviglie)

-trasformate varie

 

Note aggiuntive:

Il tipo di problema che ho cercato di risolvere è lo stesso che viene affrontato con gli expression templates, il livello di ottimizzazione fornito è lo stesso. L'unica differenza sta in un codice molto più semplice e pulito (niente templates difficili da capire e mantenere, ci si può concentrare su aspetti più interessanti).

 

(Gli expression templates sono alla base di librerie piuttosto complesse come Eigen o uBLAS)

 

________________

 

Esempio del logo di GPI ma con i colori invertiti (riconvertito in PNG)

 

 

LINK CODICE DI ESEMPIO

 

Campagne crowfunding

Just One Line
Siamo presenti su

     
Copyright ©2016 - Manifesto - Privacy - Termini di Servizio - Community - Collaboratori - Contattaci