Ce qui suit sera mon premier billet de blog. Rien de vraiment ambitieux, seulement, comme son titre l'indique, des notes de code. Aujourd'hui je compte implémenter un filtre de Sobel en utilisant les instructions SIMD (SSE2 plus précisément) ça prendra surement plus de temps de prévu enfin bon...here we go !
Qu'est ce qu'un filtre de Sobel ?
Un filtre de Sobel c'est un filtre (non ? :o)) qui transforme une image en une autre. Comme vous le savez sans doute, on peut représenter une image de manière abstraite par un tableau de nombres. Le tableau correspond à un écran, chaque élément de ce tableau est un pixel et le nombre représente la couleur associée à ce pixel. Si on a cette représentation en tête, l'application d'un filtre de Sobel sur une image ressemble à peu près à ça :
En fait c'est un peu plus compliqué que ça puisqu'on utilise deux "kernels" (cf. animation ci-dessus) légèrement différents :
L'application de ces deux kernels donne deux images différentes, on additionne ensuite les images pixel par pixel pour obtenir l'image finale.
Choix du format
Dans un premier temps, je vais travailler sur des images en niveau de gris. Reste à choisir un format. Après quelques minutes de recherche j'opte pour PGMB un format extrêmement simple :
- Les données sont en binaire
- Supporte uniquement les niveaux de gris
- 2D
- Pas de compression
- Ne contient qu'une seule image
- Un nombre magique pour identifier le type de fichier : "P5" en ASCII.
- Un espace
- La largeur en ASCII décimal
- Un espace
- La hauteur en ASCII décimal
- Un saut de ligne
- La valeur de gris maximale en ASCII decimal comprise entre 0 et 255
- Un saut de ligne
- Les pixels sont stockés sur un octet entre 0 et la valeur maximale (ex :11111111 en binaire vaut 255 en décimal) et commencent en haut à gauche. La valeur 0 correspond au noir et la valeur maximale au blanc.
Bon maintenant que j'ai choisi le format, il me faudrait une image. Voici une photo du lac de St-Ferréol que j'ai prise le week-end dernier :
Une petite ligne de commande linux :
convert IMG_20130804_093756.jpg lac_st_ferreol.pgm
Et voici le résultat :
Maintenant que j'ai une superbe image au format pgm, je vais pouvoir passer au code ! :P
Lecture et Écriture
La première chose à faire c'est spécifier l'interface que l'on souhaite :
#ifndef __PGM_INTERFACE_H__ #define __PGM_INTERFACE_H__ #include <stdint.h> static const uint16_t PGM_MAGIC = 0x3550; //nombre magique static const uint16_t BUFF_SIZE = 1 << 8; //taille du tampon /*Structure contenant l'image en niveaux de gris*/ struct greyscale_image { uint32_t width, height; //lageur et hauteur uint8_t* pixels; //pointeur vers les pixels }; typedef struct greyscale_image greyscale_image; /*Charge le fichier 'filename' dans l'image 'to_load'*/ void load_from_pgm(char* filename, greyscale_image* to_load); /*Ecrit l'image 'to_write' dans le fichier 'filename'*/ void write_to_pgm(greyscale_image* to_write, char* filename); #endif
L'entête stdint définie les types uint8_t, uint16_t et uint32_t qui correspondent des entiers positifs ou nuls de 8, 16 et 32 bits. Pour obtenir le fameux "nombre magique" du format PGMB en hexadécimal, il suffit d’exécuter la commande :
$ $ head -1 lac_st_ferreol.pgm | hexdump 0000000 3550 000a 0000003
Maintenant que tout est bien spécifié, je passe au .c. Je n'ai pas grand chose à dire de ce côté là mais j'attends vos commentaires et critiques avec impatience. Voici le résultat après quelques erreurs de compilation :
#include <limits.h> #include <stdio.h> #include <string.h> #include <stdlib.h> #include "pgm_interface.h" void load_from_pgm(char* filename, greyscale_image* to_load) { char buff[BUFF_SIZE]; //allocation d'un tampon FILE* file = fopen(filename, "r"); //ouverture du fichier if (file == NULL) { printf("Echec de l'ouverture du fichier en lecture.\n"); return; } if(fgets(buff, sizeof buff, file) == NULL || *(uint16_t*)buff != PGM_MAGIC) { printf("Le fichier n'est pas au format PGMB.\n"); return; } if (fgets(buff, sizeof buff, file) == NULL || sscanf(buff, "%d %d", &(to_load->width), &(to_load->height)) == EOF) { printf("L'entête du fichier est mal formée.\n"); return; } //allocation de l'image to_load->pixels = (uint8_t*)malloc(sizeof(uint8_t) * to_load->width * to_load->height); if (to_load == NULL) { printf("Echec de l'allocation de l'image.\n"); return; } fgets(buff, sizeof buff, file); //passe la ligne '255\n' //copie les pixels en mémoire fread(to_load->pixels, sizeof(uint8_t), to_load->width * to_load->height, file); fclose(file); //fermeture du fichier } void write_to_pgm(greyscale_image* to_write, char* filename) { FILE* file = fopen(filename, "w"); //ouverture du fichier if (file == NULL) { printf("Echec de l'ouverture du fichier en écriture.\n"); return; } //écrit le nombre magique fwrite(&PGM_MAGIC, sizeof(uint8_t), sizeof PGM_MAGIC, file); //puis l'entête fprintf(file, "\n%d %d\n%d\n",to_write->width, to_write->height, 1 << CHAR_BIT - 1); //et les données fwrite(to_write->pixels, sizeof(uint8_t), to_write->width * to_write->height, file); fclose(file); //fermeture du fichier }
Je pense que je vais m'arrêter là pour la première partie de ce billet. Vous pouvez trouver un programme de test et les sources dans le dépôt git associé à ce billet en suivant ce lien :