Filtres d’Images Bitmap
Nous retournons aujourd’hui dans le royaume des graphiques Cocoa, où nous allons apprendre comment manipuler des images bitmap à l’échelle du pixel. Cocoa donne une représentation des images bitmap en utilisant la sous-classe de NSImageRep appelée NSBitmapImageRep. Cette sous-classe peut manipuler des formats de données de plusieurs types : TIFF, GIF, JPEG, PNG, BMP et même des données brut sans marque.
Pour démontrer comment nous pouvons effectuer une manipulation de bas niveau—octet par octet—des images bitmap, nous allons implémenter un filtre dans ImageApp (NdT : Voir les articles précédents) dont le but sera de convertir une les couleurs d’une image en dégradés de gris. Notre filtre partira d’une objet NSImage, extraira les données brut bitmap, opèrera sur ces données puis retournera un nouvel objet NSImage modifié.
Préparation de ImageApp à cette tâche
Si vous avez besoin du fichier projet pour commencer, téléchargez le ici. C’est le même que celui que nous avons créé dans le dernier article.
Avant de parler de la complexité de NSBitmapImageRep et de notre filtre à dégradé de gris, il est pertinent de commencer par configurer l’infrastructure de ImageApp de manière à supporter ce nouveau comportement. Cela se fera sous la forme d’un nouveau menu, Filtre, qui contiendra l’unique commande, Convertir en dégradés de gris, qui à son tour invoquera une méthode de MyDocument dont le but sera de faire passer l’image au travers du filtre. Convertir en dégradés de gris communiquera avec MyDocument au travers de First Responder dans MainMenu.nib. (Les propos que nous avons tenus dans le dernier article à propos de File’s Owner pourraient aussi s’appliquer à First Responder. Il n’y a pas d’instance de First Responder contenue dans le nib ; c’est un autre objet autonome. First Responder est de la même nature que File’s Owner).
Pour configurer tout ça, ouvrez MainMenu.nib dans Interface Builder. Double-cliquez sur First Responder pour faire apparaître le panneau d’informations de classe. Dans ce panneau, ajoutez une action à First Responder appelée makeGrayscale:. Cette action est celle à laquelle nous allons connecter notre nouvelle commande de menu.
Pour créer un nouveau menu dans le menu principal, déposez un objet Submenu à partir de la palette des Menus Cocoa et mettez le entre le menu Edition et Fenêtre. Renommez le menu Filtre et changez le nom de la commande de menu par défaut en Convertir en dégradés de gris. Puis connectez la commande de menu à l’action makeGrayscale: de First Responder. Après avoir sauvegardé ceci, nous allons fermer boutique dans Interface Builder et revenir dans Project Builder.
De retour dans Project Builder
Pour préparer ImageApp à cette ajout d’objet filtre, nous devons implémenter l’action makeGrayscale: dans MyDocument. L’objet filtre utilisé pour changer l’image sera une instance de la classe IAGrayscaleFilter. Cette classe définit une seule méthode -filterImage:, qui prend un argument et retourne un NSImage. Le NSImage sera configuré en autodestruction, nous devons donc affirmer la propriété de MyDocument sur l’image retournée en envoyant un message de rétention à l’image retournée. Enfin, l’image retournée sera positionnée comme activeImage du document, et nous mettrons alors à jour l’affichage pour monter la nouvelle image. Pour rafraîchir l’affichage, nous devons ajouter une méthode à IAWindowController qui nous permettra de dire à la vue de l’image quelle image dessiner. Cette méthode sera appelée -setImageToDraw:. Jetons un oeil à -makeGrayscale: pour voir à quoi elle ressemble ; cela nous mène dans MyDocument.m :
- (void)makeGrayscale:(id)sender
{
IAGrayscaleFilter *filter = [[IAGrayscaleFilter alloc] init];
[activeImage autorelease];
activeImage = [[filter filterImage:activeImage] retain];
[windowController setImageToDraw:activeImage];
[filter release];
}
Tout d’abord, nous avons instancié et initialisé IAGrayscaleFilter, et assigné le nouvel objet à la variable filter. Pour que MyDocument reconnaisse IAGrayscaleFilter, nous devons importer son fichier en-tête. Assurez-vous d’avoir fait cela. Ensuite, nous envoyons un message autorelease à l’objet actuellement assigné à activeImage. Nous faisons ceci parce que dans la ligne suivante de code nous réassignons activeImage à un objet NSImage différent. Nous devons le détruire avant de perdre contact avec lui. Ensuite, nous exécutons l’opération de filtrage, puis indiquons à windowController de dessiner la nouvelle image, et enfin nous détruisons filter avant de sortir de la méthode.
Dans IAWindowController nous devons implémenter -setImageToDraw:. Voici ce à quoi cela ressemble :
- (void)setImageToDraw:(NSImage *)image
{
[view setImage:image];
}
Tout ce que nous avons fait ici a été de transférer l’image vers la vue d’image en utilisant setImage:.
Maintenant, nous sommes prêt à commencer de construire notre classe de filtre. Démarrons par créer une nouvelle classe à partir de menu File et nommons la IAGrayscaleFilter. Puis ajoutons la déclaration de méthode à l’en-tête :
- (NSImage *)filterImage:(NSImage *)srcImage;
et changeons l’instruction d’import comme ceci :
#import <AppKit/AppKit.h>
de manière à importer les en-têtes de l’AppKit au lieu de celles de Foundation. Cela est nécessaire parce que nous utilisons des classes de l’AppKit dans cette classe. Maintenant, passons à l’implémentation de notre filtre d’image.
Le Filtre
La méthode opérationelle de toute cette affaire est filterImage:, qui prend une NSImage source comme argument et retourne une nouvelle NSImage en dégradé de gris. Pour accomplir ceci, nous avons besoin de la faculté de pouvoir accéder aux données brut de l’image source, lancer un algorithme sur ces données et stocker les nouvelles valeurs des pixels dans la NSImage retournée. Tout ceci est très simplifié par le fait que nous ne récupérons pas réellement des données des objets NSImage, mais plutôt des instances de NSBitmapImageRep.
Rappelez-vous le premier article sur ImageApp où notre discussion portait sur la relation entre NSImage et NSImageRep. NSImage fournit une couche d’abstraction qui isole les clients des détails du format de donnée de l’image et de son implémentation. Pour maintenir le plus grand niveau de flexivilité, cependant, l’Application Kit fournit la classe NSImageRep, qui représente un pont entre l’abstraction isolante de NSImage et les représentations d’image dépendantes des données définies dans les sous-classes NSImageRep.
La première chose que nous devons faire consiste à accéder à la représentation bitmap de l’image source, remplissons donc cette partie du puzzle, comme indiqué ci-dessous :
- (NSImage *)filterImage:(NSImage *)srcImage
{
NSBitmapImageRep *srcImageRep = srcImageRep = [NSBitmapImageRep imageRepWithData:
[srcImage TIFFRepresentation]];
.
.
.
}
La première chose que nous faisons est de récupérer la représentation bitmap de l’image source. NSBitmapImageRep fournit le constructeur de commodité imageRepWithData:, qui prendra comme paramètre un objet NSData formatté sous forme de données TIFF. Les données TIFF utilisées pour créer l’instance sont obtenues en utilisant la méthode TIFFRepresentation de NSImage ; cela retourne exactement l’objet NSData dont nous avons besoin.
L’étape suivante consiste à configurer le nouvel NSImage qui sera la destination de l’opération de filtrage. Cela suppose non seulement de créer une instance de NSImage à retourner, mais aussi la représentation bitmap de l’image utilisée pour assembler les données de l’image. La combinaison de ces deux pièces est une procédure simple qui sera effectuée avant de retourner le NSImage.
Dans le bout de code suivant, nous ferons seulement ce que nous venons de décrire. La création d’un NSBitmapImageRep en partant de zéro est une procédure fastidieuse, ne soyez donc pas choqué par la taille du code que vous allez voir. La méthode d’initialisation que nous allons utiliser fait plus de 125 caractères ; un des noms de méthodes les plus longs de tout Cocoa ! Nous complétons filterImage: de la manière suivante :
- (NSImage *)filterImage:(NSImage *)srcImage
{
NSBitmapImageRep *srcImageRep = [NSBitmapImageRep
imageRepWithData:[srcImage TIFFRepresentation]];
int w = [srcImageRep pixelsWide];
int h = [srcImageRep pixelsHigh];
int x, y;
NSImage *destImage = [[NSImage alloc] initWithSize:NSMakeSize(w,h)];
NSBitmapImageRep *destImageRep = [[[NSBitmapImageRep alloc]
initWithBitmapDataPlanes:NULL
pixelsWide:w
pixelsHigh:h
bitsPerSample:8
samplesPerPixel:1
hasAlpha:NO
isPlanar:NO
colorSpaceName:NSCalibratedWhiteColorSpace
bytesPerRow:NULL
bitsPerPixel:NULL] autorelease];
.
.
.
[destImage addRepresentation:destImageRep];
return destImage;
}
Maintenant voyons ce que nous avons fait. D’abord, nous avons déclaré quelques variables qui prouveront leur utilité dans la méthode. Nous avons la hauteur et la largeur de l’image source et d’autres variables que nous utiliserons pour spécifier une position au sein de l’image.
Ensuite, nous voyons que nous avons créé un objet NSImage appelé destImage. L’initialisation de cet objet suppose de ne spécifier que la taille de l’image, qui sera la même que celle de l’image source. Les instances de NSImage initialisées de cette manière ne contiennent aucune représentation d’image. Nous allons donc ajouter destImageRep à la liste des représentations de destImage, la manipulation de destImageRep étant équivalente à la manipulation de destImage. Les représentations d’image sont ajoutées à une image en invoquant la méthode addRepresentation: dans l’objet NSImage avec comme argument la représentation que nous souhaitons ajouter.
Le moment est venu d’allouer et d’initialiser un nouvel objet NSBitmapImageRep. Une donnée image est simple, mais la variété de formats et d’organisations nécessite une initialisation flexible. Passons la en revue argument par argument.
Le premier argument, BitmapDataPlanes:, nous permet de fournir de la mémoire à l’objet, qui est configuré pour stocker des données bitmap selon une organisation plane. Une donnée image brut n’est rien d’autre qu’une séquence d’éléments de la taille d’un octet qui stockent la valeur de chaque composante de chaque pixel. Une composante de pixel est une couleur telle que le rouge, le vert, le bleu, l’alpha, le cyan et ainsi de suite. Lorsque nous créons un NSBitmapImageRep nous avons l’option de spécifier un ou deux modèles d’organisation des données : soit des données planes, soit des données interlacées.
Lorsque des données image sont organisées selon le modèle plan, il y a des tableaux séparés en mémoire, ou plans, pour chaque composante couleur. En fait, dans une image RGB, il y a un tableau séparé pour le rouge, le vert et le bleu. Nous pouvons créer ces tableaux par avance et passer un tableau de pointeurs à ce premier argument. Ce tableau comportera autant d’éléments qu’il y aura de composantes couleur et chaque élément sera un pointeur vers le début de chaque tableau de données. Son type sera unsigned char **.
Plutôt que de spécifier ces tableaux de données, nous pouvons passer NULL, ce qui veut dire que les données seront interlacées. Avec ce modèle, il y a un tableau qui contient toutes les données image. Les données sont arrangées de telle façon que les composantes couleur d’un même pixel sont adjacentes en mémoire. Par exemple, les trois premiers éléments du tableau pourraient être les valeurs des composantes rouge, vert et bleu du premier pixel. Les trois octets suivants seraient les mêmes données pour le pixel suivant et ainsi de suite. Nous allons travailler avec des données interlacées.
Les deux arguments suivants, pixelsHigh: et pixelsWide: nous permettent de spécifier la taille de l’image. Ensuite, dans bitsPerSample:, nous spécifions la taille en bits de chaque composante couleur. Nous supposerons ici que chaque composante est codée sur un octet, ou 8 bits, mais il est possible de les coder sur 12 ou 16 bits. Si vous avez des applications hautement spécialisées il n’est pas exclut de choisir des valeurs ésotériques pour bitsPerSample:. Ensuite, nous spécifions le samplesPerPixel:. Pour une image RGB nous passerons 3 pour ce paramètre, si nous créons une image en dégradés de gris, comme ici, nous passons 1.
La combinaison de ces quatre derniers paramètres permet à la classe de déterminer combien de mémoire à allouer à l’ensemble des données brut de la représentation de l’image. Nous pouvons déterminer le nombre total de pixels de l’image en multipliant la hauteur par la largeur. Nous pouvons aussi déterminer l’espace mémoire requis pour chaque pixel en multipliant bitsPerSample par samplesPerPixel.
Ensuite, nous indiquons si oui ou non une des composantes spécifiées dans samplesPerPixel: est une couche alpha. Pour rester simple, nous n’allons pas nous embêter avec une couche alpha, nous passons donc NO. Puis nous spécifiions si oui ou non l’image est plane. Comme nous le savons, elle ne le sera pas, nous passons donc NO encore une fois.
Presque arrivé. Ensuite, nous souhaitons spécifier l’espace couleur de l’image. Un espace couleur indique au système graphique comment interprêter les données de l’image. Un espace couleur familier est RGB (NdT : RVB en français). Dans cet espace colorimétrique, une couleur est représentée par trois composantes de couleur : rouge, vert et bleu. Ce sera l’espace de notre image source (ce que nous supposons pour des questions de simplicité). Un autre espace colorimétrique est l’espace blanc, où une couleur est une nuance de gris. Dans cet espace, chaque pixel n’a qu’une composante couleur. Les espaces RGB peuvent aussi représenter des gris—en ajoutant trois valeurs identiques des trois composantes couleur.
Du fait que nous construisons un filtre qui part d’une image en couleur et qui la convertit en dégradés de gris, nous allons configurer l’espace colorimétrique de notre image sur blanc, désigné dans Cocoa par la constante NSCalibratedWhiteColorSpace.
Enfin, les deux dernières méthodes aux quelles nous passons NULL nous permettent de disposer d’une autre manière de spécifier combien de mémoire allouer à l’image. L’argument bytesPerRow: est équivalent à :
pixelsWide * bitsPerSample * samplesPerPixel / 8
L’autre argument, bitsPerPixel:, est une autre façon de dire :
bitsPerSample * samplesPerPixel
Voilà comment nous faisons un nouvel NSBitmapImageRep, pleinement configuré pour notre usage. Comme toujours, étudiez la documentation de cette classe car elle fournit plein de détails qui ne sont pas couverts ici. Maintenant que nous en avons fini avec cela, passons à la manière de convertir une image couleur en une image grise.
La Manière
J’ai dit à plusieurs reprises que le filtre partirait d’une image couleur et qu’il la convertirait en image grise. Cette opération est simple, et dans sa forme la plus basique, ce n’est pas plus compliqué que de trouver un pixel, de déterminer la valeur des composantes rouge, vert et bleu, de calculer la moyenne de ces valeurs et d’appliquer cette valeur moyenne à la composante blanche du même pixel de la représentation en dégradés de gris de l’image.
Ce petit bout de math doit être effectué pour chaque pixel de l’image. Pour coder ce comportement, nous allons prendre une bonne dose de pointeurs et de tableaux C. Puisque les pointeurs et les tableaux forment une relation étroite en C, je vais me servir de cet article comme opportunité de vous monter quelle relation les unit. Donc, la première chose que nous devons mettre en place est constitué par deux boucles qui vont scruter le buffer complet de données de l’image entière afin d’accéder à chaque pixel. C’est là que nos variables x et y interviennent, en tant que numéros de pixel.
for ( y = 0; y < h; y++ ) {
for ( x = 0; x < w; x++ ) {
// Par ici la magie
}
}
L’action qui s’insère dans la boucle lit la valeur des composantes rouge, vert et bleu du pixel courant, en fait une moyenne puis applique au pixel correspondant de destination cette valeur moyenne. Pour accomplir ceco, nous avons besoin de pointeurs vers le début des buffers de données de l’image source et de destination. Ces pointeurs sont commodément obtenus par des messages bitmapData envoyés vers srcImageRep et vers destImageRep. La méthode bitmapData retourne un pointeur caractère, qui est du type unsigned char *. Cela est très pratique compte tenu que unsigned char est de la même taille que nos composantes couleur, 8-bits. En assemblant ces deux dernières pièces avec notre code existant, cela nous donne :
- (NSImage *)filterImage:(NSImage *)srcImage
{
NSBitmapImageRep *srcImageRep = [NSBitmapImageRep
imageRepWithData:[srcImage TIFFRepresentation]];
NSImage *destImage = [[NSImage alloc] initWithSize:NSMakeSize(w,h)];
int w = [srcImageRep pixelsWide];
int h = [srcImageRep pixelsHigh];
int x, y;
NSBitmapImageRep *destImageRep = [[[NSBitmapImageRep alloc]
initWithBitmapDataPlanes:NULL
pixelsWide:w
pixelsHigh:h
bitsPerSample:8
samplesPerPixel:1
hasAlpha:NO
isPlanar:NO
colorSpaceName:NSCalibratedWhiteColorSpace
bytesPerRow:NULL
bitsPerPixel:NULL] autorelease];
unsigned char *srcData = [srcImageRep bitmapData];
unsigned char *destData = [destImageRep bitmapData];
unsigned char *p1, *p2;
for ( y = 0; y < h; y++ ) {
for ( x = 0; x < w; x++ ) {
}
}
[destImage addRepresentation:destImageRep];
return destImage;
}
En plus de déclarer les pointeurs srcData et destData qui pointent vers le premier élément, ou la tête du buffer de données, nous définissons les pointeurs p1 et p2, qui seront utilisés en tant que pointeurs de travail. Représentez-vous ces pointeurs comme des curseurs positionnés sur l’emplacement, dans le tableau, du pixel que nous sommes en train de manipuler dans la boucle.
Pour comprendre comment nous manipulons les données, nous devons en comprendre plus sur l’organisation de ces données. Dans une image 24 bit, où chaque composante couleur est sur 8 bits, sans composante alpha, les données sont arrangées séquentiellement, avec comme premier octet du buffer de données équivalent à la valeur de la composante bleu du premier pixel. Le second octet du tableau correspond à la composante vert du premier pixel et le troisième octet à la composante rouge. Cette séquence se prolonge pour chaque pixel tout au long de la première ligne de pixel (x incrémental et y constant), puis, comme un retour charriot sur une machine à écrire, nous passons à la ligne suivante (incrémentation de y, réinitialisation de x et examen de la nouvelle ligne). Cela explique pourquoi la boucle sur x est imbriquée dans celle sur y.
Quelques notions d’arithmétique des pointeurs C
En C, un pointeur est une variable qui pointe vers un emplacement de mémoire où une valeur significative est stockée. Dans notre cas, ces valeurs significatives sont les composantes couleur de tous les pixels de l’image. Dans notre code, srcData est un pointeur qui pointe vers la composante bleu du pixel situé en ligne 1, colonne 1. Si nous souhaitons savoir la valeur placée à cet endroit de la mémoire, nous utilisons l’opérateur de dereferencing. Ainsi, *srcData comporte la valeur de la composante du pixel.
Maintenant, comment faire pour connaître la valeur de l’emplacement mémoire adjacent à celui pointé par srcData ? Quelle est la valeur de l’emplacement mémoire en position 2 ou 3 ou 300 positions plus loin que srcData ? C’est simple, nous n’avons qu’à ajouter le nombre de positions au pointeur et nous obtenons un pointeur vers cet emplacement mémoire. Donc, l’emplacement mémoire adjacent à srcData est srcData+1, deux positions plus loin nous avons srcData+2, et ainsi de suite. Comme toujours, pour accéder à la valeur de ces emplacements mémoire, utiliser l’opérateur dereferencing. Donc, *(srcData+1) correspond à la valeur adjacente, ou à la valeur de la composante vert du premier pixel, et ainsi de suite. Remarquez comment nous avons utilisé les parenthèses. Le fait d’écrire *srcData + 1 nous donne la valeur de la composante bleu du premier pixel plus un.
Maintenant, voyons comment nous utilisons ceci dans le code ci-dessous :
unsigned char *srcData = [srcImageRep bitmapData];
unsigned char *destData = [destImageRep bitmapData];
unsigned char *p1, *p2;
int n = [srcImageRep bitsPerPixel] / 8;
for ( y = 0; y < h; y++ ) {
for ( x = 0; x < w; x++ ) {
p1 = srcData + n * (y * w + x);
p2 = destData + y * w + x;
*p2 = (unsigned char)rint((*p1 + *(p1 + 1) + *(p1 + 2)) / 3);
}
}
Prenons le temps de décortiquer ceci. La première ligne de la boucle for indique que p1, un pointeur, va pointer vers l’emplacement mémoire de l’image source qui correspond au pixel x de la ligne y. Nous atteignons ce pixel en déterminant le nombre de pixels contenus dans le buffer de données, ce qui correspond à y fois la largeur de l’image auquel on additionne la valeur de x, et nous multiplions ce résultat par le nombre d’octets de chaque pixel, n. Dans notre code, nous supposons que n équivaut à 3. Le résultat est alors ajouté à l’adresse pointée par srcData.
Ensuite, nous répétons ce calcul pour p2 dans le buffer de données de destination. Cependant, compte tenu que les données de destination sont au format NSCalibratedWhiteColorspace, chaque pixel occupe un seul octet en mémoire (parce que c’est ce qui est requis pour représenter une valeur en dégradés de fris). Donc, le calcul du dessus est le même sauf que n vaut 1.
Dans la troisième ligne, nous accédons à chaque octet rouge, vert et bleu du pixel localisé par p1. L’octet bleu est pointé par p1. Comme les octets vert et rouge interviennent séquentiellement après le bleu, nous pouvons incrémenter l’adresse de p1 de 1 pour atteindre l’emplacement de l’octet vert et, de 2 pour atteindre l’octet rouge. Pour récupérer la valeur réelle stockée dans ces adresses, nous devons déréférencer le pointeur. L’opérateur * ne s’applique qu’aux adresses et va retourner la valeur stockée à cet emplacement qui est pointé par l’adresses. Donc, p1, p1+1 et p1+2 sont des pointeurs qui pointent vers les composantes bleu, vert et rouge, et *p1, *(p1+1) et *(p1+2) sont les valeurs de ces composantes.
Nous additionnons ces trois valeurs, en calculons la moyenne, arrondissons cette valeur au format entier et la renvoyons au format unsigned char (puisque nous sommes limités à 8 bits d’espace de stockage par composant) et appliquons le résultat sur la valeur pointée par p2, qui correspond à *p2.
Vous voilà donc avec un “C pointer arithmetic in a nutshell” (NdT : En référence à la série des ouvrages “in a nutshell“).
Le code précédent aurait pu aussi être écrit en utilisant la syntaxe des tableaux C. L’écriture de notre arithmétique avec une syntaxe pointeur est simple. Par exemple, précédemment, *(p1+n) faisait référence à la valeur de l’emplacement mémoire n positions après p1. Avec une syntaxe tableau cela aurait été écrit p1[n]. En se basant sur ces changements syntaxiques, le code précédent peut être réécrit comme suit :
unsigned char *srcData = [srcImageRep bitmapData];
unsigned char *destData = [destImageRep bitmapData];
unsigned char *p1, *p2;
int n = [srcImageRep bitsPerPixel] / 8;
for ( y = 0; y < height; y++ ) {
for ( x = 0; x < width; x++ ) {
p1 = srcData + n * (y * w + x);
p2 = destData + y * w + x;
p2[0] = (unsigned char)rint((p1[0] + p1[1] + p1[2]) / 3);
}
}
Un autre manière d’écrire les boucles for consiste à remplacer le n*y*w par le nombre d’octets par ligne. Cela simplifierait l’artithmétique des adresses ainsi :
unsigned char *srcData = [srcImageRep bitmapData];
unsigned char *destData = [destImageRep bitmapData];
unsigned char *p1, *p2;
int n = [srcImageRep bitsPerPixel] / 8;
int srcBPR = [srcImageRep bytesPerRow];
int destBPR = [destImageRep bytesPerRow];
for ( y = 0; y < height; y++ ) {
for ( x = 0; x < width; x++ ) {
p1 = srcData + y * srcBPR + n*x;
p2 = destData + y * destBPR + x;
p2[0] = (unsigned char)rint((p1[0] + p1[1] + p1[2]) / 3);
}
}
Ce n’est donc qu’une autre manière de localiser les données. Lorsque nous assemblons toutes les pièces, notre méthode finale -filterImage: ressemble à ceci :
- (NSImage *)filterImage:(NSImage *)srcImage
{
NSBitmapImageRep *srcImageRep = [NSBitmapImageRep
imageRepWithData:[srcImage TIFFRepresentation]];
NSImage *destImage = [[NSImage alloc] initWithSize:NSMakeSize(w,h)];
int w = [srcImageRep pixelsWide];
int h = [srcImageRep pixelsHigh];
int x, y;
NSBitmapImageRep *destImageRep = [[[NSBitmapImageRep alloc]
initWithBitmapDataPlanes:NULL
pixelsWide:w
pixelsHigh:h
bitsPerSample:8
samplesPerPixel:1
hasAlpha:NO
isPlanar:NO
colorSpaceName:NSCalibratedWhiteColorSpace
bytesPerRow:NULL
bitsPerPixel:NULL] autorelease];
unsigned char *srcData = [srcImageRep bitmapData];
unsigned char *destData = [destImageRep bitmapData];
unsigned char *p1, *p2;
int n = [srcImageRep bitsPerPixel] / 8;
for ( y = 0; y < height; y++ ) {
for ( x = 0; x < width; x++ ) {
p1 = srcData + n * (y * w + x);
p2 = destData + y * w + x;
p2[0] = (unsigned char)rint((p1[0] + p1[1] + p1[2]) / 3);
}
}
[destImage addRepresentation:destImageRep];
return destImage;
}
Nous sommes maintenant prêt à compiler le code et à l’essayer.
En résumé
Comme toujours, il y a de nombreuses améliorations que l’on puisse apporter à cette méthode de filtrage. L’implémentation que nous avons créée aujourd’hui est limitée aux images 24 bits. Notre première amélioration pourrait être l’ajout du support des images dotées d’une couche alpha. Dans le projet que vous pouvez télécharger ici, vous trouverez mon implémentation—elle fournit le support alpha.
Vous pourriez aussi utiliser ce simple bout de code pour apporter des améliorations Altivec à la boucle. Je n’ai pas encore eu le temps de me plonger dans les librairies Altivec (mon G4 ne date que de six semaines), mais un filtre d’image qui utilise les librairies Altivec serait pour moi le meileur choix. Altivec a toutes les caractéristiques d’un candidat de premier choix pour la vectorisation, et j’imagine qu’il y aurait des améliorations notables au niveau de la rapidité. Je vous laisse avec ce sujet en guise d’exercice.
Bien, nous avons vu comment réaliser un filtre d’image. Il ya des centaines d’opérations différentes que l’on peut réaliser sur des images au sein du cadre de travail que nous avons établit ici . Ce cadre de travail consiste en cette méthode boite noire, -filterImage, à laquelle nous passons une image en entrée et en récupérons un autre en sortie. La seule qui change tient dans ce qui se passe dans la boucle for. Je ne vais pas prendre le temps de développer des opérations supplémentaires de filtrage, mais dans le prochain article je vous montrerai comment définir et implémenter une interface à une architecture de plug-in pour cette application. Cette architecture de plug-in déplacera la charge de développement de filtres vers les utilisateurs finaux de l’application. A bientôt.

Textes originaux en anglais sur O’Reilly : Filtre d’images bitmap par Mike Beam
© Janvier 2003 Thierry pour Project:Omega
Chargement
Commentaires récents