Redimensionner une image

Un algorithme de redimensionnement efficace, avec et sans filtrage bilinéaire.

Voir version :

Dépendances (dans l'archive) :
b1.bmp

Télécharger :

#include <sdl/sdl.h>

#ifdef WIN32
#pragma comment(lib,"sdl.lib")
#pragma comment(lib,"sdlmain.lib")
#endif

void UpdateEvents(char* key,SDL_Surface** pscreen)
{
    SDL_Event event;
    while(SDL_PollEvent(&event))
    {
        switch (event.type)
        {
        case SDL_KEYDOWN:
            key[event.key.keysym.sym]=1;
            break;
        case SDL_KEYUP:
            key[event.key.keysym.sym]=0;
            break;
        case SDL_VIDEORESIZE:
            *pscreen = SDL_SetVideoMode(event.resize.w,event.resize.h,32,SDL_SWSURFACE|SDL_DOUBLEBUF|SDL_VIDEORESIZE);  
            break;
        }
    }
}

SDL_Rect Rect(int x,int y,int w,int h)        // pour faire un rectangle plus vite
{
    SDL_Rect r;
    r.x=x;
    r.y=y;
    r.w=w;
    r.h=h;
    return r;
}

SDL_Surface* LoadBMP(char* fichier,int vram)
{
    SDL_Rect R;
    SDL_Surface* r,*f = SDL_LoadBMP(fichier);    // charge l'image dans f en RAM
    if (!f)
    {
        printf("Echec chargement %s\n",fichier);
        SDL_Quit();
        exit(-1);
    }
    r=NULL;
    if (vram)
        r=SDL_CreateRGBSurface(SDL_HWSURFACE, f->w, f->h, 32, 0, 0, 0, 0);// cree une image en VRAM
    if (r==NULL) vram=0;    // Si plus de place en VRAM, alors r= NULL.
    if (!vram)
        r=SDL_CreateRGBSurface(SDL_SWSURFACE, f->w, f->h, 32, 0, 0, 0, 0);  // cree une image en RAM
    R=Rect(0,0,f->w,f->h);
    SDL_BlitSurface(f,NULL,r,&R);    // copie l'image f de la RAM vers firstscreen en VRAM
    SDL_FreeSurface(f);      // supprime la surface f : inutile maintenant --> libere la mémoire
    return r;
}

unsigned char GetPixelComp32(SDL_Surface* surface,int x,int y,int c)
{ // recupere le pixel x,y de la surfece "surface", la composante c (3 composantes RGB)
    unsigned char *p = ((unsigned char*)surface->pixels) + y * surface->pitch + x * 4;
    return p[c];
}

void PutPixelComp32(SDL_Surface* surface,int x,int y,int c,unsigned char val)
{ // ecrit la composante c (3 composantes RGB) sur le pixel x,y de la surface "surface"
    unsigned char *p = ((unsigned char*)surface->pixels) + y * surface->pitch + x * 4;
    p[c] = val;
}

void Stretch_Nearest(SDL_Surface* src,SDL_Surface* dest)
{
    int i,j,k;
    double rx,ry;
    rx = dest->w*1.0/src->w;
    ry = dest->h*1.0/src->h;
    for(i=0;i<dest->w;i++)
        for(j=0;j<dest->h;j++)
            for(k=0;k<3;k++)
            {
                unsigned char pix;
                pix = GetPixelComp32(src,(int)(i/rx),(int)(j/ry),k);
                PutPixelComp32(dest,i,j,k,pix);
            }
}

void Stretch_Linear(SDL_Surface* src,SDL_Surface* dest)
{
    int i,j,k;
    double rx,ry;
    rx = dest->w*1.0/src->w;
    ry = dest->h*1.0/src->h;
    for(i=0;i<dest->w;i++)
        for(j=0;j<dest->h;j++)
        {
            unsigned char pix;
            double valx,valy,fx,fy;
            int minx,miny,maxx,maxy;
            valx = i/rx;
            valy = j/ry;
            minx = (int)valx;
            miny = (int)valy;
            maxx = minx+1;
            if (maxx>=src->w)
                maxx--;
            maxy = miny+1;
            if (maxy>=src->h)
                maxy--;
            fx = valx-minx;  // garde partie flottante
            fy = valy-miny;
            for(k=0;k<3;k++)
            {
                pix = (unsigned char)(GetPixelComp32(src,minx,miny,k)*(1-fx)*(1-fy) + GetPixelComp32(src,maxx,miny,k)*fx*(1-fy)
                    + GetPixelComp32(src,minx,maxy,k)*(1-fx)*fy + GetPixelComp32(src,maxx,maxy,k)*fx*fy);
                PutPixelComp32(dest,i,j,k,pix);
            }
        }
}

void Strechblit(SDL_Surface* src,SDL_Surface* dest)
{
    SDL_Surface* img = SDL_CreateRGBSurface(SDL_SWSURFACE,dest->w,dest->h,32,0,0,0,0);
    Stretch_Nearest(src,img);
    //Stretch_Linear(src,img);
    SDL_BlitSurface(img,NULL,dest,NULL);
    SDL_FreeSurface(img);
}

int main(int argc,char** argv)
{
    SDL_Surface* image,*screen;
    char key[SDLK_LAST]={0};
    SDL_Init(SDL_INIT_VIDEO);
    image = LoadBMP("b1.bmp",0);
    screen=SDL_SetVideoMode(image->w,image->h,32,SDL_SWSURFACE|SDL_DOUBLEBUF|SDL_VIDEORESIZE);  
    SDL_ShowCursor(1);
    while(!key[SDLK_ESCAPE])
    {
        UpdateEvents(key,&screen);
        Strechblit(image,screen);
        SDL_Flip(screen);
    }
    SDL_FreeSurface(image);
    SDL_Quit();
    return 0;
}



Commentaires

	Requis : fichier b1.bmp (a télécharger sur ce site au paragraphe 2), ou bien n'importe quel autre image BMP de votre choix, 
	en adaptant eventuellement le nom.

	Un petit tuto sur le redimentionnement d'image.
	Oui, SDL_GFX sait le faire, mais il est amusant de le refaire sois meme !

	Lancez le programme : vous voyez une petite fenetre apparaitre, avec une image dedans.
	Essayez de redimentionner la fenetre a la souris : vous voyez l'image qui s'adapte, que vous zommiez ou réduisiez.

	Regardons le main.

	char key[SDLK_LAST]={0};

	Un tableau de char : SDLK_LAST est une constante qui défini le nombre maximal de touches. Ainsi, en déclarant cela, je suis
	sur d'avoir un tableau de la bonne taille pouvant garder l'état de toutes touches en mémoire.
	0 en haut, 1 en bas. 

	Ensuite, je charge l'image en mémoire grace a ma fonction LoadBMP.
	Cette fonction, si on va voir, charge l'image, et la recopie dans une autre surface a 32 bits par pixels.
	Ainsi, en sortant de cette fonction, je m'assure que la profondeur de pixels est bien de 32 bits.
	Note : si l'image n'est pas trouvee, un printf me le dit, et le programme quitte.

	Ensuite, je cree le mode vidéo de la taille de l'image : ainsi, j'ai une fenetre qui fera la taille de l'image
	Note : si l'image est trop petite, il se peut que la fenetre soit un peu plus grande
	Note : j'ai passé le parametre SDL_VIDEORESIZE : pour permettre a l'utilisateur de redimentionner lui meme la fenetre

	Ensuite, le while qui sort si j'appuie sur ESC.
	La fonction updateevents enrichie, gere les touches en mettant a jour le tableau key (fondamental pour sortir du while), et
	egalement pour gérer le resize.
	Notez dans la fonction UpdateEvents que si l'event resize est appelé, je rappelle SDL_SetVideoMode, de la nouvelle taille,
	c'est pour cela que je passe un pointeur vers screen a UpdateEvents, pour qu'il puisse egalement mettre a jour screen

	Revenons a la boucle principale.

	Le coeur du programme est la fonction Strechblit

	Cette fonction cree une surface de la taille de l'écran, une fonction Strecth la remplit, puis on la blit sur l'écran.
	Note : je vous accorde que c'est violent : a chaque frame, l'image est recalculée.

	J'ai mis 2 formes de stretch : Stretch_Nearest et Stretch_Linear.
	Par defaut, Stretch_Nearest est activé. Mais quand vous aurez vu le résultat, commentez la ligne, et décommentez Stretch_Linear

Stretch_Nearest
***************

	C'est le zoom/reduce le plus simple : 
	Le but est de remplir chaque pixel de l'image de destination (d'ou le double for i,j) en allant chercher sur l'image source le pixel le plus proche

	Donc on calcule d'abord le ratio entre la coordonnée x de l'image dest, et celle de l'image source (soit rx cette valeur)
	Soit rh son équivalent pour la coordonnée y.

	Pour avoir le pixel correspondant, une simple division suffit (dans la fonction GetPixelComp32)

	Note : je procede composante par composante (Rouge,Vert,Bleu) je ne m'occupe pas de la composante Alpha.
	Voila pourquoi j'ai encore un for sur k.

	Avec ceci, je recalcule un zoom ou un reduce pour chaque pixel.

	Partie délicate :
	-----------------

	Ce qu'il faut bien comprendre, c'est que rx et ry sont des nombres réels :
	En effet, si l'image dest est une fois et demi plus grande, le facteur est de 1.5
	Mais elle peut aussi etre 1.1453 fois plus grande en x, et 2.1554 fois plus grande en y : distortion possible !

	quand, depuis le pixel voulu de l'image dest, on calcule le pixel correspondant sur l'image source, on fait une division.
	Du coup, on va tomber, par exemple, sur le pixel source :

	valx = 5.12
	valy = 9.68

	Ce ne sont pas des nombres entiers !

	En effet, on peut tomber "entre 2 pixels"
	
	Le Stretch_Nearest est un algo violent qui "cast" la valeur trouvée.
	Du coup, elle va tronquer la partie décimale, et dire "ok, dans ce cas je prends le pixel 5 et 9 !

	Ceci a pour conséquences :
	- pour une réduction, si vous avez un mur de briques, vous allez perdre certains traits de jonction des moellons 
	alors que d'autres vont rester : ça va faire tres moche
	- pour un zoom, si vous avez plusieurs pixels de l'image dest cote a cote qui generent par exemple valx = 9.15 ; 9.25 ; 9.34 ; 9.45 ...
	l'algo va tous les tronquer a 9. Donc concretement, vous aurez pour ces pixels la meme valeur de pixel source, donc la meme couleur
	conséquence visuelle -> ça fait des gros carrés de meme couleur !!

	Testez donc !!  :)

Stretch_Linear
**************
	
	Maintenant, commentez la ligne Stretch_Nearest, et décommentez Stretch_Linear
	Lancez le programme : zoomez...
	Fini les gros carrés !
	Si vous avez un mur de brique (plus grand que B1), lancez, et réduisez : fini l'effet d'irrégularité entre les jonctions de briques !

	Alors quel est ce miracle ?
	-> l'interpollation bilinéaire.

	Reprenons l'explication ci dessus.
	Pour un pixel donné, nous obtenons :

	valx = 5.12
	valy = 9.68

	Maintenant, l'astuce :
	Au lieu de faire le violent et de dire "prenons donc 5 et 9"
	Nous allons nous dire : nous sommes au milieu de 4 pixels

	entre (5, 9), (6, 9)
	      (5,10), (6,10)

	Mais nous sommes quand meme plus pret de 5 (en x) que de 6, et plus pret de 10 que de 9 pour y.
	Donc nous allons utiliser une formule d'interpollation bilineaire pour faire un dégradé entre ces 4 pixels, et prendre la valeur qu'il faut.

	Dans le code, pour cet exemple :
	minx = 5 maxx = 6
	miny = 9 maxy = 10  // parties entieres par defaut et par exces

	Note : un if vérifie qu'on ne sort pas de l'image, dans le cas limite ou la valeur minx vaut l'abcisse maximale (pareil pour y)

	fx = 0.12  // partie décimale
	fy = 0.68 

	La forumule d'interpollation bilinéaire, c'est des maths, je vous laisse vous renseigner la dessus. C'est une notion proche de celle de barycentre,
	avec comme contrainte la somme des poids doit valoir 1.
	Cela est le cas puisque (1-fx)*(1-fy) + fx*(1-fy) + (1-fx)*fy + fx*fy   :  ça fait 1 pour tout fx,fy.


Conclusion :
************

	Cet exemple est intéressant pour celui qui veut comprendre comme ça marche.
	L'utilisateur de SDL dira : "SDL_gfx, extention de SDL, contient des fonctions qui font déja ça". Tout a fait.
	
	J'ajoute cependant que les cartes graphiques savent faire cela nativement, et electroniquement (donc bien plus vite)
	Si vous avez touché a OpenGL, les parametres passés a glTextparameter peuvent avoir, pour MAG_FILTER et MIN_FILTER,
	les valeurs GL_NEAREST et GL_LINEAR...

	Tiens tiens... C'est exactement ça :)