Traitement d'image avec Java 2D

Le traitement d'image est l'art et la science de la manipulation d'images numériques. Il tient fermement un pied en mathématiques et l'autre en esthétique, et est un composant essentiel des systèmes informatiques graphiques. Si vous avez déjà pris la peine de créer vos propres images pour les pages Web, vous apprécierez sans aucun doute l'importance des capacités de manipulation d'images de Photoshop pour nettoyer les numérisations et nettoyer les images qui ne sont pas optimales.

Si vous avez effectué un travail de traitement d'image dans JDK 1.0 ou 1.1, vous vous souvenez probablement que c'était un peu obtus. L'ancien modèle de producteurs et de consommateurs de données d'image est peu maniable pour le traitement d'images. Avant JDK 1.2, le traitement d'image impliquait des MemoryImageSources, des PixelGrabbers et d'autres arcanes de ce type. Cependant, Java 2D fournit un modèle plus propre et plus facile à utiliser.

Ce mois-ci, nous examinerons les algorithmes derrière plusieurs opérations importantes de traitement d'image ( ops ) et vous montrerons comment ils peuvent être implémentés à l'aide de Java 2D. Nous vous montrerons également comment ces opérations sont utilisées pour affecter l'apparence de l'image.

Le traitement d'image étant une application autonome véritablement utile de Java 2D, nous avons créé l'exemple de ce mois, ImageDicer, pour qu'il soit aussi réutilisable que possible pour vos propres applications. Cet exemple unique montre toutes les techniques de traitement d'image que nous aborderons dans la chronique de ce mois-ci.

Notez que peu de temps avant la publication de cet article, Sun a publié le kit de développement Java 1.2 Beta 4. La bêta 4 semble donner de meilleures performances pour nos exemples d'opérations de traitement d'image, mais elle ajoute également de nouveaux bogues impliquant la vérification des limites de ConvolveOps. Ces problèmes affectent la détection des contours et les exemples de netteté que nous utilisons dans notre discussion.

Nous pensons que ces exemples sont précieux, donc plutôt que de les omettre complètement, nous avons compromis: pour garantir son exécution, l'exemple de code reflète les modifications de la Bêta 4, mais nous avons conservé les chiffres de l'exécution de la 1.2 Bêta 3 afin que vous puissiez voir les opérations fonctionne correctement.

Espérons que Sun corrigera ces bogues avant la version finale de Java 1.2.

Le traitement d'image n'est pas sorcier

Le traitement d'image n'a pas à être difficile. En fait, les concepts fondamentaux sont vraiment assez simples. Une image, après tout, n'est qu'un rectangle de pixels colorés. Le traitement d'une image consiste simplement à calculer une nouvelle couleur pour chaque pixel. La nouvelle couleur de chaque pixel peut être basée sur la couleur de pixel existante, la couleur des pixels environnants, d'autres paramètres ou une combinaison de ces éléments.

L'API 2D introduit un modèle de traitement d'image simple pour aider les développeurs à manipuler ces pixels d'image. Ce modèle est basé sur la java.awt.image.BufferedImageclasse et les opérations de traitement d'image comme la convolution et le seuillage sont représentées par des implémentations de l' java.awt.image.BufferedImageOpinterface.

La mise en œuvre de ces opérations est relativement simple. Supposons, par exemple, que vous ayez déjà l'image source comme BufferedImageappelée source. L'exécution de l'opération illustrée dans la figure ci-dessus ne prendrait que quelques lignes de code:

001 court [] seuil = nouveau court [256]; 002 pour (int i = 0; i <256; i ++) 003 seuil [i] = (i <128)? (court) 0: (court) 255; 004 BufferedImageOp thresholdOp = 005 new LookupOp (new ShortLookupTable (0, threshold), null); 006 BufferedImage destination = thresholdOp.filter (source, null);

C'est vraiment tout ce qu'il y a à faire. Voyons maintenant les étapes plus en détail:

  1. Instanciez l'opération image de votre choix (lignes 004 et 005). Ici, nous avons utilisé a LookupOp, qui est l'une des opérations d'image incluses dans l'implémentation Java 2D. Comme toute autre opération d'image, il implémente l' BufferedImageOpinterface. Nous parlerons plus en détail de cette opération plus tard.

  2. Appelez la filter()méthode de l'opération avec l'image source (ligne 006). La source est traitée et l'image de destination est renvoyée.

Si vous avez déjà créé un BufferedImagequi contiendra l'image de destination, vous pouvez le transmettre comme deuxième paramètre à filter(). Si vous passez null, comme nous l'avons fait dans l'exemple ci-dessus, une nouvelle destination BufferedImageest créée.

L'API 2D comprend une poignée de ces opérations d'image intégrées. Nous en discuterons trois dans cette colonne: la convolution, les tables de recherche et le seuillage. Veuillez vous référer à la documentation Java 2D pour plus d'informations sur les opérations restantes disponibles dans l'API 2D (Ressources).

Convolution

Une opération de convolution vous permet de combiner les couleurs d'un pixel source et de ses voisins pour déterminer la couleur d'un pixel de destination. Cette combinaison est spécifiée à l'aide d'un noyau, un opérateur linéaire qui détermine la proportion de chaque couleur de pixel source utilisée pour calculer la couleur de pixel de destination.

Considérez le noyau comme un modèle superposé sur l'image pour effectuer une convolution sur un pixel à la fois. Au fur et à mesure que chaque pixel est convoluté, le modèle est déplacé vers le pixel suivant dans l'image source et le processus de convolution est répété. Une copie source de l'image est utilisée pour les valeurs d'entrée pour la convolution, et toutes les valeurs de sortie sont enregistrées dans une copie de destination de l'image. Une fois l'opération de convolution terminée, l'image de destination est renvoyée.

Le centre du noyau peut être considéré comme recouvrant le pixel source alambiqué. Par exemple, une opération de convolution qui utilise le noyau suivant n'a aucun effet sur une image: chaque pixel de destination a la même couleur que son pixel source correspondant.

 0,0 0,0 0,0 0,0 1,0 0,0 0,0 0,0 0,0 

La règle cardinale pour la création de noyaux est que les éléments doivent tous s'additionner jusqu'à 1 si vous souhaitez conserver la luminosité de l'image.

Dans l'API 2D, une convolution est représentée par a java.awt.image.ConvolveOp. Vous pouvez construire un en ConvolveOputilisant un noyau, qui est représenté par une instance de java.awt.image.Kernel. Le code suivant construit un en ConvolveOputilisant le noyau présenté ci-dessus.

001 float [] identityKernel = {002 0.0f, 0.0f, 0.0f, 003 0.0f, 1.0f, 0.0f, 004 0.0f, 0.0f, 0.0f 005}; 006 BufferedImageOp identity = 007 new ConvolveOp (new Kernel (3, 3, identityKernel));

L'opération de convolution est utile pour effectuer plusieurs opérations courantes sur des images, que nous détaillerons dans un instant. Différents noyaux produisent des résultats radicalement différents.

Nous sommes maintenant prêts à illustrer quelques noyaux de traitement d'image et leurs effets. Notre image non modifiée est Lady Agnew of Lochnaw, peinte par John Singer Sargent en 1892 et 1893.

Le code suivant crée un ConvolveOpqui combine des quantités égales de chaque pixel source et de ses voisins. Cette technique entraîne un effet de flou.

001 flotteur neuvième = 1.0f / 9.0f; 002 float [] blurKernel = {003 neuvième, neuvième, neuvième, 004 neuvième, neuvième, neuvième, 005 neuvième, neuvième, neuvième 006}; 007 BufferedImageOp blur = new ConvolveOp (new Kernel (3, 3, blurKernel));

Un autre noyau de convolution commun met l'accent sur les bords de l'image. Cette opération est communément appelée détection de bord. Contrairement aux autres noyaux présentés ici, les coefficients de ce noyau ne totalisent pas 1.

001 float [] edgeKernel = {002 0.0f, -1.0f, 0.0f, 003 -1.0f, 4.0f, -1.0f, 004 0.0f, -1.0f, 0.0f 005}; 006 BufferedImageOp edge = new ConvolveOp (new Kernel (3, 3, edgeKernel));

Vous pouvez voir ce que fait ce noyau en regardant les coefficients dans le noyau (lignes 002-004). Réfléchissez un instant à la façon dont le noyau de détection de bord est utilisé pour fonctionner dans une zone entièrement d'une couleur. Chaque pixel se retrouvera sans couleur (noir) car la couleur des pixels environnants annule la couleur du pixel source. Les pixels clairs entourés de pixels sombres resteront clairs.

Notez à quel point l'image traitée est plus sombre par rapport à l'original. Cela se produit parce que les éléments du noyau de détection des bords ne totalisent pas 1.

A simple variation on edge detection is the sharpening kernel. In this case, the source image is added into an edge detection kernel as follows:

 0.0 -1.0 0.0 0.0 0.0 0.0 0.0 -1.0 0.0 -1.0 4.0 -1.0 + 0.0 1.0 0.0 = -1.0 5.0 -1.0 0.0 -1.0 0.0 0.0 0.0 0.0 0.0 -1.0 0.0 

The sharpening kernel is actually only one possible kernel that sharpens images.

The choice of a 3 x 3 kernel is somewhat arbitrary. You can define kernels of any size, and presumably they don't even have to be square. In JDK 1.2 Beta 3 and 4, however, a non-square kernel produced an application crash, and a 5 x 5 kernel chewed up the image data in a most peculiar way. Unless you have a compelling reason to stray from 3 x 3 kernels, we don't recommend it.

You may also be wondering what happens at the edge of the image. As you know, the convolution operation takes a source pixel's neighbors into account, but source pixels at the edges of the image don't have neighbors on one side. The ConvolveOp class includes constants that specify what the behavior should be at the edges. The EDGE_ZERO_FILL constant specifies that the edges of the destination image are set to 0. The EDGE_NO_OP constant specifies that source pixels along the edge of the image are copied to the destination without being modified. If you don't specify an edge behavior when constructing a ConvolveOp, EDGE_ZERO_FILL is used.

The following example shows how you could create a sharpening operator that uses the EDGE_NO_OP rule (NO_OP is passed as a ConvolveOp parameter in line 008):

001 float[] sharpKernel = { 002 0.0f, -1.0f, 0.0f, 003 -1.0f, 5.0f, -1.0f, 004 0.0f, -1.0f, 0.0f 005 }; 006 BufferedImageOp sharpen = new ConvolveOp( 007 new Kernel(3, 3, sharpKernel), 008 ConvolveOp.EDGE_NO_OP, null); 

Lookup tables

Another versatile image operation involves using a lookup table. For this operation, source pixel colors are translated into destination pixels colors through the use of a table. A color, remember, is composed of red, green, and blue components. Each component has a value from 0 to 255. Three tables with 256 entries are sufficient to translate any source color to a destination color.

The java.awt.image.LookupOp and java.awt.image.LookupTable classes encapsulate this operation. You can define separate tables for each color component, or use one table for all three. Let's look at a simple example that inverts the colors of every component. All we need to do is create an array that represents the table (lines 001-003). Then we create a LookupTable from the array and a LookupOp from the LookupTable (lines 004-005).

001 short[] invert = new short[256]; 002 for (int i = 0; i < 256; i++) 003 invert[i] = (short)(255 - i); 004 BufferedImageOp invertOp = new LookupOp( 005 new ShortLookupTable(0, invert), null); 

LookupTable has two subclasses, ByteLookupTable and ShortLookupTable, that encapsulate byte and short arrays. If you create a LookupTable that doesn't have an entry for any input value, an exception will be thrown.

This operation creates an effect that looks like a color negative in conventional film. Also note that applying this operation twice will restore the original image; you're basically taking a negative of the negative.

What if you only wanted to affect one of the color components? Easy. You construct a LookupTable with separate tables for each of the red, green, and blue components. The following example shows how to create a LookupOp that only inverts the blue component of the color. As with the previous inversion operator, applying this operator twice restores the original image.

001 short[] invert = new short[256]; 002 short[] straight = new short[256]; 003 for (int i = 0; i < 256; i++) { 004 invert[i] = (short)(255 - i); 005 straight[i] = (short)i; 006 } 007 short[][] blueInvert = new short[][] { straight, straight, invert }; 008 BufferedImageOp blueInvertOp = 009 new LookupOp(new ShortLookupTable(0, blueInvert), null); 

Posterizing is another nice effect you can apply using a LookupOp. Posterizing involves reducing the number of colors used to display an image.

A LookupOp can achieve this effect by using a table that maps input values to a small set of output values. The following example shows how input values can be mapped to eight specific values.

001 court [] posterize = nouveau court [256]; 002 pour (int i = 0; i <256; i ++) 003 posterize [i] = (short) (i - (i% 32)); 004 BufferedImageOp posterizeOp = 005 nouveau LookupOp (nouveau ShortLookupTable (0, posterize), null);

Limitation

La dernière opération d'image que nous examinerons est le seuillage. Le seuillage rend les changements de couleur à travers une «limite» ou un seuil déterminé par le programmeur, plus évidents (de la même manière que les lignes de contour sur une carte rendent les limites d'altitude plus évidentes). Cette technique utilise une valeur de seuil, une valeur minimale et une valeur maximale spécifiées pour contrôler les valeurs de composante de couleur pour chaque pixel d'une image. Les valeurs de couleur inférieures au seuil reçoivent la valeur minimale. Les valeurs supérieures au seuil reçoivent la valeur maximale.