Traitement d´images en Java avec JAI
Les principaux traitements disponibles
Transformations géométriques
En premier lieu, on peut souhaiter transformer l´image en modifiant son aspect géométrique. On peut recenser différents types de changements géométriques :
- Rotation, par exemple pour mettre en mode portrait une photographie qui a été prise en mode paysage
- Agrandissement d´une image pour par exemple en faire un poster
- Retrécissement d´une image pour en obtenir une miniature
- La technique dite du "Nearest Neighbor" où la valeur du pixel dépendra du pixel le plus proche. En cas d´agrandissement on recopiera la valeur du pixel dans le pixel voisin tandis qu´on fera une moyenne entre le pixel courant et le pixel voisin en cas de réduction.
- L´interpolation bilinéaire consiste à prendre en considération tous les pixels voisins de cette façon :
- L´interpolation bicubique est une extension de la bilinéaire qui utilises les pixels plus lointains en pondérant leur valeur. Cette technique est plus précise mais est plus couteuse en temps de calcul :
Nous allons ici montrer comment effectuer une opération de redimenssionnement en utilisant un filtre fourni avec JAI. Dans tous les exemples qui suivrons nous utiliserons la méthode suivante pour charger une image png à partir du système de fichier :
package fr.umlv.ig2k.ir3.xpose.fchaillo.bonus; import java.io.File; import java.io.IOException; import javax.media.jai.PlanarImage; import javax.media.jai.RenderedOp; import javax.media.jai.operator.PNGDescriptor; import com.sun.media.jai.codec.FileSeekableStream; import com.sun.media.jai.codec.PNGDecodeParam; public class JaiUtil { public static PlanarImage readPNGImage(String imageName) throws IOException{ File f = new File(imageName); RenderedOp op = PNGDescriptor.create(new FileSeekableStream(f), new PNGDecodeParam(), null); return op.getRendering(); } }
Pour modifier la taille de l´image, on utilise le filtre ScaleDescriptor qui permet de redimensionner l´image selon un facteur pour l´axe des x et un autre pour l´axe des y. Dans notre exemple, nous utiliserons le même facteur de 0,5 pour diviser par deux la taille de l´image :
PlanarImage source = JaiUtil.readPNGImage(”source.png”); float scaleX = 0.5f, scaleY = 0.5f; float translationX = 0f, translationY = 0f; RenderingHints hints = new RenderingHints(); RenderedOp scaleOp = ScaleDescriptor.create(source, scaleX, scaleY, translationX, translationY,Interpolation.getInstance( Interpolation.INTERP_BILINEAR), hints); PlanarImage result = scaleOp.getRendering();
Ce filtre permet aussi de translater l´image mais dans notre cas, nous souhaitons juste redimensionner l´image.
On peut remarquer qu´il est nécessaire de préciser le type d´interpolation que JAI effectuera lors du calcul des pixels.
Traitements par pixel
Le deuxième grand type d´opérations que l´on peut effectuer sur une image concerne le modification locale de chaque pixel. Dans ce cas, chaque point voit son contenu altérer sans se préoccuper du contenu alentour.
Cette opération a pour résultat de modifier la couleur des pixels de l´image.
On retrouve plusieurs usages pour cette opérations :
- Le changement d´espace de couleur de l´image, par exemple pour passer d´une image couleur à une image en niveau de gris
- La modification du contraste ou de la luminosité de l´image
Pour implémenter ces deux opérations, nous montrerons deux exemples distincts car ils s´appuient sur deux filtres différents.
Pour montrer la transformation de l´image en niveau de gris, nous supposerons que l´image de départ est en RGBA, c´est à dire que chacun de ses pixels est décrit selon une valeur en Rouge (R), en vert (G), en bleu (B) et en transparence (A pour Alpha).
Nous allons utiliser le filtre BandCombineDescriptor pour ici combiner les quatres bandes de couleur que nous avons et en obtenir au résultat une seule couleur. Pour décrire cette opération, le filtre prend en paramètre un tableau à deux dimension de double. Ce tableau indique comment chaque couleur de la nouvelle image doit être déduite à partir des couleurs de l´image de départ. Ainsi, dans notre cas, notre couleur est obtenue en prenant un tiers de la valeur de chaque couleur et 0 pour la transparence.
Voici l´implémentation :
PlanarImage source = JaiUtil.readPNGImage(”source.png”); double[][] bandMerge = {{0.33, 0.33, 0.33, 0}}; RenderingHints hints = new RenderingHints(); hints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); RenderedOp op = BandCombineDescriptor.create( source, bandMerge, hints); PlanarImage result = op.getRendering();
Pour modifier le contraste ou la luminosité de l´image, nous utilisons une LookupTable dont le principe est similaire à une table de hachage mais pour les valeurs des pixels :
pour chaque valeur de pixel (entre 0 et 255) de chaque couleur (rouge,vert et bleu), nous précisons quelle est la nouvelle valeur après modification. Dans notre cas, nous diminuons la luminosité de l´image en diminuant la valeur de pixel de 50. La méthode clampValue nous permet seulement de nous assurer que la valeur résultante sera toujours contenue entre 0 et 255.
Le code permettant l´opération est le suivant :
PlanarImage source = JaiUtil.readPNGImage(”source.png”); RenderingHints hints = new RenderingHints(); byte[][] datas = new byte[3][256]; for (int i = 0; i < 256; i++) { datas[0][i] = clampValue(i - 50); datas[1][i] = clampValue(i - 50); datas[2][i] = clampValue(i - 50); } LookupTableJAI table = new LookupTableJAI(datas); RenderedOp op = LookupDescriptor.create( source, table, hints); PlanarImage result = op.getRendering();
Traitements par zone
Le traitement par zone a pour but de modifier la valeur de chaque pixel en fonction des valeurs des pixels alentours.
Dans ce but, on utilise une matrice de convolution qui permet de spécifier comment sont considérés les pixels qui sont autour du pixel dont la valeur est calculée.
Une matrice de convolution est carrée et de taille impaire (en général de taille 3x3). Cette matrice contient des coefficients.
Pour calculer la valeur d´un pixel on centre la matrice sur ce pixel, on multiplie la valeur de ce pixel et des pixels alentours par les coefficients de la matrice, puis on additione ces valeurs et on obtient la nouvelle valeur du pixel considéréré.
Voici une image représentant le processus :
Ce type de traitement permet beaucoup d´effets connus qui dépendent des coéfficients que l´on donne à la matrice, les plus connus sont les suivants :
- Effet de flou
- Affinement des détails
- Détection des contours
En contrepartie, ce traitement est lourd à calculer principalement pour deux raisons :
- Il nécessite de travailler sur une copie de l´image puisque pour chaque valeur les valeurs voisines comptent. Ainsi on ne peut pas travailler sur l´image original car chaque changement affecte les changements suivants.
- Pour chaque pixel on doit un nombre importants de calculs (par exemple 10 pour une matrice de convolution de taille 3x3).
Le code pour écrire ce type d´opération est quasiment identique tout le temps. Il est juste nécessaire de modifier la matrice de convolution. Dans l´exemple qui va suivre, je décrirai comment faire un flou à l´aide d´une matrice de convolution de taille 3x3.
Pour effectuer ce traitement, il fait créer un filtre nommé ConvolveDescriptor auquel on spécifie la matrice sous la forme d´un objet KernelJAI qui définit la matrice sous la forme d´un tableau unidimensionnel de coefficients.
Pour créer un flou, il suffit de fournir une matrice de convolution dont tous les coefficients sont égaux. Pour ne pas altérer le contenu de l´image, la somme des coefficients doit faire 1, ainsi dans notre cas, la matrice est rempli avec la valeur 1/9. Voici le code implémentant le filtre :
PlanarImage source = JaiUtil.readPNGImage(”source.png”); RenderingHints hints = new RenderingHints(); float[] matrix = new float[]{ 1/9f, 1/9f, 1/9f, 1/9f, 1/9f, 1/9f, 1/9f, 1/9f, 1/9f }; KernelJAI kernel = new KernelJAI( 3, 3, matrix); RenderedOp op = ConvolveDescriptor.create(source, kernel, hints); PlanarImage result = op.getRendering();