Dans cette étude, nous allons nous intéresser plus particulièrement au traitements des images. Cette étude fait suite au graphismes 2D traités dans le cours précédent. En effet, afin de tirer le meilleur parti et bien exploiter les résultats voulus, nous devons bien maîtriser les graphisme 2D. Je vous invite donc à revoir cette étude dans le cas où vous ne connaîtriez bien pas cette technique.
Dans l'étude sur le graphisme 2D, vous avez vu comment créer des images simples en traçant des lignes et des formes. Des images complexes, telles que des photographies, sont habituellement générées en externe, par exemple avec un scanner ou un logiciel dédié à la manipulation d'images. Il est également possible de produire une image pixel par pixel, et de stocker le résultat dans un tableau. Nous allons aborder tout ces aspects ici.
Afin de bien maîtriser le sujet, nous allons aborder successivement :
Depuis la JDK 1.4, nous disposons du paquetage javax.imageio qui contient la prise en charge immédiate (on dit aussi synchrone) de la lecture et de l'écriture de plusieurs format de fichiers communs, ainsi que le cadre autorisant des tierces parties à ajouter des outils de lecture et d'écriture pour d'autres formats. Plus spécifiquement, le JDK contient des outils de lecture pour les formats GIF, JPEG et PNG et des outils d'écriture pour les formats JPEG et PNG. Ce paquetage sera plutôt utilisé dans le cas où nous construisons une application fenêtrée.
Pour les applets, la démarche est différente puisque l'image doit être téléchargée depuis un serveur Web, ce qui peut prendre, nous nous en doutons, un peu plus de temps. Nous sommes donc obligés de prévoir plutôt une lecture asynchrone. L'applet dispose déjà de méthodes adaptées à cette situation.
Les bases de la bibliothèque sont extrêmement simples. Pour charger une image en mémoire depuis un dispositif externe, comme le disque dur par exemple, vous utiliserez la méthode statique read() de la classe ImageIO.
File fichier = new File("fichier image.jpg");
BufferedImage image = ImageIO.read(fichier); // ou Image image = ImageIO.read(fichier);
...
surface.drawImage(image, 50, 150, null));
Cette image est ensuite représentée en mémoire au travers d'un objet de la classe BufferedImage. C'est d'ailleurs le type d'objet que renvoie la méthode read(). Cette classe BufferedImage hérite elle-même de la classe abstraite Image qui dispose des méthodes minimales correspondant au canevas nécessaire à l'affichage de l'image. La classe BufferedImage dispose de beaucoup plus de méthodes et attributs que la classe Image afin de permettre de nombreux traitements sur les images.
Attention : La lecture du fichier est ici synchrone, c'est-à-dire que la méthode read() attend que l'image soit entièrement chargée avant de retourner l'objet résultant. Cette méthode est donc blocante.
La classe ImageIO choisit un lecteur approprié, en fonction du type de fichier. Dans ce but, elle peut consulter l'extension de fichier et le "numéro magique" situé au début du fichier. Lorsqu'un lecteur adapté ne peut être trouvé ou si le lecteur ne parvient pas à décoder le contenu du fichier, la méthode read() renvoie null.
Par ailleurs, la méthode read() de la classe ImageIO peut également récupérer une image à partir d'une URL :
Image image = ImageIO.read(new URL("http://site/répertoire/fichier image.gif"));
surface.drawImage(image, 50, 150, null));
L'écriture d'une image est tout aussi simple :
File fichier = new File("fichier image.jpg");
String format ="JPEG";
BufferedImage image;
...
ImageIO.write(image, format, fichier);
Ici, la chaîne format identifie le format de l'image, notamment "JPEG" ou "PNG". La classe ImageIO choisit l'outil approprié et enregistre le fichier.
Cette fois-ci, la technique est plus délicate et donc plus complexe. Quand vous écrivez une image, vous pouvez configurer différents paramètres metadata décriture, tel que le taux de compression. Cependant, vous ne pouvez le faire directement avec la méthode write() de ImageIO. A la place, vous devez utiliser d'autres classes (et paquetages) d'ImageIO.
Il est possible d'avoir plus d'un fournisseur de lecture et d'écriture pour un format spécifique. De ce fait, les méthodes telles que getImageWritersByFormatName() de la classe ImageIO renvoit un Iterator. Pour personnaliser les taux de compression en sortie, vous pouvez examiner tous les fournisseurs intallés et trouvez le niveau maximum de compression supporté. Ou vous pouvez simplement utiliser le premier. Voyons ici l'approche la plus simple :
Iterator iter = ImageIO.getImageWriterByFormatName("JPEG"); if (iter.hasNext()) {
ImageWriter = (ImageWriter) iter.next(); ... }
Vous pouvez récupérer les paramètres d'écritures par défaut pour un ImageWriter spécifique à travers sa méthode getDefaultWriteParam(). La méthode retourne un objet de type ImageWriteParam. Pour les JPEG, c'est une instance de javax.imageio.jpeg.JPEGImageWriteParam (bien que vous n'ayez pas besoin de le savoir). Pour chnager le niveau de compression, vous devez appeler l'objet ImageWriteParam dont vous voulez spécifier explicitement le taux de compression :
iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
Ensuite, vous spécifiez la qualité de compression avec :
iwp.setCompressionQuality(float);
Au lieu de sélectionner une valeur au hasard, vous pouvez demander au writer quelles valeurs de qualité de compression il supporte (ou comment il comprimera l'image) :
float[] valeurs = iwp.getCompressionQualityValues();
Voici une méthode qui synthétise toute la démarche que nous venons de découvrir
void écrireFichier(BufferedImage image) { try { ImageWriter writer = ImageIO.getImageWritersByFormatName("JPEG").next(); ImageWriteParam iwp = writer.getDefaultWriteParam(); iwp.setCompressionMode(iwp.MODE_EXPLICIT); float[] valeurs = iwp.getCompressionQualityValues(); iwp.setCompressionQuality(valeurs[valeurs.length-1]); FileImageOutputStream fichier = new FileImageOutputStream(new File("nom du fichier")); writer.setOutput(fichier); IIOImage imageFlux = new IIOImage(image, null, null); writer.write(null, imageFlux, iwp); } catch (IOException ex) { état.setText("Problème d'enregistrement"); } }
Ici, le taux de compression est le plus fort. Notez que la méthode écrit directement avec l'objet ImageWriter et non avec l'objet ImageIO. Comme les paramètres de sortie sont personnalisés, vous devez utiliser un objet de type IIOImage (IIO signifiant ImageIO) représentant l'image à écrire. Vous ne pouvez utiliser un BufferedImage complet. Le BufferedImage peut être directement converti en IIOImage comme étape supplémentaire.
Cette fois-ci, l'image doit se trouver dans l'ordinateur où se trouve le serveur Web. En effet, une applet n'a pas le droit de dialoguer et de récupérer des ressources avec un autre ordinateur que ce dernier. Pour des raisons de sécurité évidente, surtout d'ailleurs à cause d'Internet, l'applet n'a aucune autorisation particulière pour récupérer des ressources sur le poste client dans lequel elle a été téléchargée. Généralement, l'image doit être récupérée depuis l'application Web où se trouve l'applet. Nous imaginons bien que du coup, le temps de chargement d'une image dans une applet sera beaucoup plus long que lors d'une application fenêtrée.
La classe ImageIO ne correspond pas à ce cas de figure. En effet, cette classe propose des méthodes read() qui sont synchrones, c'est-à-dire que l'exécution du programme est interrompu tant que la totalité de l'image n'est pas entièrement récupérée. Le temps de téléchargement de l'image pouvant être très long, l'applet serait inutilement bloquée, et donc inutilisable durant tout le temps du transfert de l'image.
Heureusement, l'applet dispose déjà de la méthode intégrée getImage(). Le comportement de cette méthode est opposée à celle de la méthode read(). En effet, getImage() propose un fonctionnement asynchrone.
Voici le format de la méthode getImage() :
getImage(URL, "fichier image.jpg");
Voici un exemple :
Image image = this.getImage(this.getDocumentBase(), "chouette.jpg");
... surface.drawImage(image, 0, 0, this);
...
// this indique que l'applet elle-même sert de spectateur. L'image s'affiche dès qu'elle est entièrement chargée.
La méthode getDocumentBase() permet de renvoyer l'adresse URL complète de l'application Web où se situe la page Web contenant l'applet. Dans cet exemple, je suppose que l'image "chouette.jpg" se trouve dans le même répertoire que la page Web.
Les images sont traitées un peu différemment des formes. En particulier, nous ne faisons pas appel au Paint courant pour dessiner une image car celle-ci contient ses propres informations de couleurs.
Comme pour le texte, la classe Graphics2D dispose de la méthode spécifique drawImage() qui permet de placer l'image à l'emplacement indiqué. Cette méthode attend toutefois en premier argument l'objet de type Image (ou ses decendant comme BufferedImage) qui représente l'image réelle récupérée à partir du disque dur. Nous avons déjà découvert la classe ImageIO qui permet de récupérer un tel fichier.
Image image = ImageIO.read(new File("fichier image.gif"));
surface.drawImage(image, 50, 150, null));
La variable image contient alors une référence à un objet qui encapsule les données images. Vous pouvez afficher l'image grâce à la méthode drawImage() de la classe Graphics.
surface.drawImage(image, positionX, positionY, spectateur));
La classe java.awt.Image représente une vue d'une image. La vue est créée à partir d'une image source fournissant des données sous la forme de pixels. Les images peuvent provenir d'une source statique, comme des fichiers GIF, JPEG, PNG ou dynamique comme un stream d'animation ou un moteur graphique. La classe Image de Java 2 gère également l'animation GIF89a (gifs animés), de sorte qu'il est aussi facile de travailler avec des animations simples qu'avec des images statiques.
Voici un exemple où nous affichons une image à partir du coin supérieur gauche :
package dessin; import java.awt.*; import java.awt.image.*; import java.io.*; import javax.imageio.*; import javax.swing.*; public class Fenêtre extends JFrame { public Fenêtre() throws IOException { this.setDefaultCloseOperation(this.EXIT_ON_CLOSE); this.setSize(358, 260); this.setTitle("Voir image"); this.getContentPane().setBackground(Color.ORANGE); this.getContentPane().add(new Zone()); } public static void main(String[] args) throws IOException { new Fenêtre().setVisible(true); } } class Zone extends JComponent { private BufferedImage image; public Zone() throws IOException { image = ImageIO.read(new File("chouette.jpg")); } public Zone(BufferedImage image) { this.image = image; } protected void paintComponent(Graphics surface) { surface.drawImage(image, 0, 0, null); } }
Une autre version de la méthode drawImage() permet de changer l'échelle d'une image. Cette version utilise deux arguments supplémentaires pour désigner la largeur et la hauteur voulues quelle que soit les dimensions originales de l'image. Cette méthode permet donc de réaliser un reéchantillonage :
surface.drawImage(image, positionX, positionY, largeur, hauteur, spectateur));
Voici un exemple qui adapte la taille de l'image à la dimension de la fenêtre (218x160) tout en respectant le ratio de l'image :
class Zone extends JComponent { private BufferedImage image; private double ratio; public Zone() throws IOException { image = ImageIO.read(new File("chouette.jpg")); ratio = (double)image.getWidth()/image.getHeight(); } public Zone(BufferedImage image) { this.image = image; ratio = (double)image.getWidth()/image.getHeight(); } protected void paintComponent(Graphics surface) { surface.drawImage(image, 0, 0, this.getWidth(), (int)(this.getWidth()/ratio), null); } }
En réalité, il existe 6 versions de la méthode drawImage() :
Jusqu'à présent, dans le tracé, nous n'avons pas utilisé de spectateur puisque les images étaient entièrement récupérée avant d'être affichée. Ce type de traitement est appelé, nous l'avons déjà vu, traitement synchrone. Dans ce cas là, nous plaçons la valeur null dans le dernier paramètre de la méthode drawImage().
Avec les applets toutefois, les images sont traitées de façon asynchrone, ce qui signifie que Java effectue les opérations telles que les chargement et le dimensionnement des images de son côté (permettant au code principal de continuer). Dans une application cliente classique ceci n'est pas très important ; les images sont en général petites, pour les boutons par exemple, et le plus souvent assemblées avec l'application pour un affichage instantané. Toutefois, comme vous le constaterez dans les API de manipulation de données d'images, Java a été conçu pour manipuler des images à la fois localement et sur le Web.
En fait, s'il s'agit d'une nouvelle image, Java n'essaiera même pas de la transférer tant qu'elle ne sera pas nécessaire. L'avantage de cette technique, c'est que Java peut effectuer le traitement des images dans un environnement puissant et multi-thread. Toutefois, cela pose certains problèmes.
Lorsque Java récupère une image, comment savoir si elle est entièrement chargée ? Que se passe-t-il si nous avons besoin de connaître les propriétés de l'image (comme ses dimensions) avant de pouvoir commencer à travailler avec ? Que se passe-t-il si une erreur se produit lors du chargement de l'image ?
Ces problèmes sont traités par les spectateurs, des objets qui implémentent l'interface ImageObserver. Toutes les opérations qui dessinent ou traitent des objets Image reviennent immédiatement, mais prennent un objet spectateur en paramètre. Les objets ImageObserver surveillent l'état de l'image et mettent ces informations à la disposition du reste de l'application. Lorsque les données de l'image sont chargées, un spectateur est informé de la progression. Il est averti de la disponibilté de nouveaux pixels, de l'achèvement d'une zone de l'image et de la survenue d'une erreur en cours de chargement. Le spectateur reçoit dès que possible certains attributs sur l'image tels que la dimensions et d'autres propriétés.
La méthode drawImage(), comme d'autres opérations sur les images (getWidth() et getHeight() de Image notamment), prend une référence sur un objet ImageObserver en paramètre. Cette méthode renvoie une valeur booléenne qui indique si l'image a été (ou pas) affichée entièrement. Si l'image n'a pas encore été téléchargée ou si elle n'est qu'en partie disponible, la méthode drawImage() ne dessine qu'une fraction de l'image et se termine.
En arrière-plan, le système graphique commence (ou continue) à charger les données de l'image. L'objet spectateur est enregistré comme étant intéressé par les informations concernant cette image. Il est donc appelé de façon répétée (périodiquement) lorsque les données supplémentaires sont reçues et lorsque l'image entière est arrivée.
Le spectateur peut faire ce qu'il veut de ces informations. Le plus souvent, il appelle la méthode repaint() pour ordonner au composant graphique contenant l'image de dessiner la zone de l'image fraîchement arrivée.
N'oubliez pas que la méthode repaint() entraîne un appel à la méthode paintComponent(), là où nous plaçons notre code spécifique correspondant à notre affichage personnalisé. De cette manière, le composant graphique peut redessiner l'image à mesure qu'elle arrive ou attendre son chargement complet.
Il existe des spectateurs préfabriqués. Il se trouve précisément dans tous les composants graphiques, comme JComponent. En effet, JComponent implémente l'interface ImageObserver et fournit une fonction de rafraîchissement simple. Cela signifie que tout composant graphique peut être utilisé comme son propre spectateur : nous ne faisons, dans ce cas là, que faire passer une référence à notre propre objet graphique à l'aide de this.
Voici ce que nous pouvons écrire dans le cas d'une applet :
index.html |
---|
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <title></title> </head> <body bgcolor="black" text="yellow"> <h2 align="center">C'est chouette...</h2><hr /> <p align="center"> <applet code="dessin.AppletImage.class" width="356" height="240"/> <P> </body> </html> |
AppletImage.java |
---|
package dessin; import java.awt.*; import javax.swing.*; public class AppletImage extends javax.swing.JApplet { public void init() { ZoneImage zone = new ZoneImage(this.getImage(this.getDocumentBase(), "chouette.jpg")); this.getContentPane().add(zone); } } class ZoneImage extends JComponent { private Image image; ZoneImage(Image image) { this.image = image; } protected void paintComponent(Graphics surface) { surface.drawImage(image, 0, 0, this); } } |
Dans ce cas là, notre composant ZoneImage est utilisé comme spectateur et appele la méthode repaint() pour redessiner l'image si nécessaire. Si l'image arrive lentement, le composant ZoneImage est averti régulièrement à l'arrivée de nouvelles données. L'image est donc affichée progressivement.
Les propriétés awt.image.incrementaldraw et awt.image.redrawrate contrôle ce comportement. redrawate limite le nombre d'appels à la méthode repaint() : la valeur par défaut est une fois toutes les 100 millisecondes. incrementaldraw est la valeur par défaut. Elle est initialisée à true. Il suffit de la positionner à false pour différer le tracé de l'image.
Plutôt que de travailler avec l'image originale et de l'afficher directement sur le panneau graphique, il peut être judicieux de retailler l'image initiale afin qu'elle corresponde parfaitement à la taille de la fenêtre, surtout si cette image est très grande. Il suffit pour cela de faire une copie de l'image originale au travers de la méthode getScaledInstance() de la classe abstraite Image. Comme cette méthode fait partie de la classe Image, par héritage, la classe BufferedImage en bénéficie.
Image image = ImageIO.read(new File("fichier image.gif"));
Image imageRetaillée = image.getScaledInstance(300, 250, Image.SCALE_AREA_AVERAGING);
...
surface.drawImage(imageRetaillée, 0, 0, null));
Cette méthode adapte l'image d'origine à la taille indiquée ; ici 300 x 250 pixels. Elle renvoie un nouvel objet Image que nous pouvons dessiner comme tout autre image. SCALE_AREA_AVERAGING est une constante de Image indiquant à getScaledInstance() l'algorithme de mise à l'échelle à utiliser. L'algorithme utilisé ici essaie d'obtenir un bon résultat, au détriment de la vitesse. Les autres solutions plus rapides sont SCALE_REPLICATE, qui procède en dupliquant de lignes et des colonnes de balayage (ce qui est plus rapide). Nous pouvons également préciser SCALE_FAST ou SCALE_SMOOTH et laisser l'implémentation choisir un algorithme approprié optimisant la durée ou la qualité.
Mettre une image à l'échelle avant même d'appeler drawImage() peut vraiment améliorer les performances car, dans ce cas, le chargement et le dimensionnement ne sont fait qu'une seule fois. Sinon, les appels répétés à drawImage() provoquent à chaque fois un redimensionnement de l'image, consommateur de ressources.
A titre d'exemple, nous allons reprendre l'exercice qui adapte la taille de l'image à la dimension de la fenêtre (218x160) tout en respectant le ratio de l'image. Mais cette fois-ci, l'image est retaillée :
class Zone extends JComponent { private Image image; private final int largeur = 218-8; public Zone() throws IOException { BufferedImage image = ImageIO.read(new File("chouette.jpg")); double ratio = (double)image.getWidth()/image.getHeight(); this.image = image.getScaledInstance(largeur, (int)(largeur/ratio), Image.SCALE_AREA_AVERAGING); } protected void paintComponent(Graphics surface) { surface.drawImage(image, 0, 0, null); } }
La couleur est un phénomène que fait à la fois intervenir la physique de la matière, notamment les ondes électromagnétiques avec le smatériaux physiques, et l'interprétation de ce phénomène physique par le système visuel constitué notamment de l'oeil et du cerveau.
Nous connaissons le spectre de la lumière blanche mis en évidence par Isaac Newton en 1666 lors de la diffraction de la lumière blanche par un prisme :
Spectre des couleurs.
Ce sont également les couleurs présentes dans l'arc-en-ciel, phénomène résultant de la diffraction de la lumière du soleil dans les gouttelette d'eau.
Une lumière contient une part de lumière achromatique et une part de lumière chromatique. Une lumière est dite achromatique lorsqu'elle contient toutes les longueurs d'onde de façon approximativement égales, ce qui donne du coup le blanc, qui dans ce cas là peut être considéré comme une absence de couleur.
La teinte est le nom de la couleur (violet, rouge, indigo... ), c'est-à-dire de la longueur d'onde dominante. C'est une grandeur qui est repérable : nous pouvons déterminer aisément la longueur d'onde dominante et donner un nom en fonction du spectre vu ci-dessus. Par contre, cette grandeur est non mesurable et non additive : en effet, nous ne pouvons pas déterminer la couleur résultant d'une addition de deux autres couleurs.
La saturation ou indice de pureté représente l'inverse du degré de dilution de la couleur dans la lumière blanche.
La luminosité est l'intensité de la lumière achromatique (sans couleur). Elle est mesurable et additive.
Les lois de Grassman expriment la décomposition d'une lumière colorée en une lumière saturée (chromatique pure) et une lumière blanche (achromatique pure).
La relation fondamentale exprime la décomposition de la lumèire L en une composante chromatique Lc et une composante achromatique Lw (w pour White, blanc en anglais) et s'exprime par :
L = Lc + Lw
Si la composante est Lw nulle, la lumière est complètement saturée. Si la composante Lc est nulle, la lumière est achromatique (blanche). Sinon, dans les autres cas de figure, la lumière est diluée dans le blanc.
Nous définissons par couleur complémentaire une couleur dont l'ajout à une autre couleur donne une lumière blanche :
L(vert) + L(pourpre) = Lw.
Une représentation de la couleur nécessite forcément une discrétisation. Il existe plusieurs systèmes de représentation dans un espace à trois dimensions, soit en fonction de grandeurs physiques telles que la teinte, la saturation et la luminosité, soit en fonction de couleurs (RGB, CMY, etc.).
L'espace des couleurs primaires RGB (Red Green Blue), également appelé RVB en français (Rouge Vert Bleu), est calqué sur notre perception visuelle. Il utilise trois couleurs de base : le rouge (d=700nm), le vert (d=546nm) et le bleu (d=435,8nm).
L'espace des couleurs secondaires CMY (Cyan Magenta Yellow) est basé sur trois couleurs : le jaune, le cyan et le magenta. Ces trois couleurs sont les complémentaires des couleurs primaires.
Les couelurs du système additif sont les couelurs primaires : rouge, vert et bleu. Nous obtenons les autres couelurs par addition de ces couleurs. C'est ce qui se produit par exemple lors de la projection : trois faisceaux rouge, vert et bleu en proportions identiques conduisent à une lumière blanche.
Si l'on projette un faisceau rouge et un faisceau bleu, nous obtenons une lumière magenta ; un faisceau rouge et vert, une lumière jaune ; un faisceau vert et bleu, une lumière cyan.
Nous obtenons ainsi les couleurs secondaires par ajout des couleurs primaires, deux à deux. Vu autrement, l'ajout de vert et de magenta donne du blanc : ce sont donc des couleurs complémentaires, de même que le bleu et le jaune ainsi que le rouge et le cyan. Les couleurs primaires et secondaires sont donc complémentaires.
La synthèse soustractive se produit en imprimerie. C'est pourquoi les imprimeurs utilisent les composantes CMY. Si on soustrait la lumière magenta (par exemple avec un filtre), on obtient de la lumière verte. Si on soustrait la lumière cyan, on obtient de la lumière rouge et si on soustrait la lumière jaune, on obtient de la lumière bleue.
Si on soustrait à la fois de la lumière magenta, cyan et jaune (par exemple en superposant trois filtres), on n'obtient plus de lumière, donc du noir.
En imprimerie, on utilise les composantes CMY auxquelles est ajoutée une composante noire notée généralement K (blacK) donnant lieu au système CMYK. En effet, dans de nombreux documents, il y a beaucoup de noir : texte, traits, etc. Si on veut générer du texte noir, il faut superposer une lettre jaune, une lettre cyan et une lettre magenta. D'une part, on va utiliser trois encres différentes, et ce, très fréquemment. D'autre part, on demande une grande précision aux lettres (détails fins) et un léger décalage des impressions des trois lettres serait très visible.
Ainsi, à la fois pour des raisons d'économie d'encre et de précision, il a été choisi d'ajouter une quatrième composante.
Les composantes chromatiques (r, g, b) d'une lumière (ou valeurs de chromaticité) sont les proportions dans lesquelles les couleurs primaires sont mélangées afin d'obtenir cette lumière, selon la synthèse additive. Une couleur C s'exprime selon la formule :
C = rR + gG + bB.
La courbe ci-dessous nous permet de déterminer les composantes chromatiques nécessaires à l'obtention d'une couleur de longueur d'onde choisie dans le domaine du visible.
Certaines longueurs d'onde vont poser problème car certaines composantes chromatiques vont être négatives : comment enlever une quantité non présente en peinture ? Un compromis consiste à ajouter suffisamment de blanc (donc un mélange de rouge, de vert et de bleu) afin de rendre toutes les composantes chromatiques positives. L'inconvéniant est que la couleur obtenue sera diluée : il est donc impossible d'exprimer toutes les couleurs saturées en composantes RGB.
En 1935, la CIE (Commission internationale de l'éclairage) a défini un nouveau triplet de couleurs permettant de représenter l'ensemble des couleurs avec des composantes positives. Ce sont les composantes X, Y et Z qui sont les couleurs théoriques répondant aux propriétés suivantes :
Ce système de couleurs est noté XYZ.
Le diagramme de chromaticité, c'est le plan d'équation x+y+z=1 de l'espace XYZ.
Il permet, pour une couleur C donnée, de déterminer sa longueur d'onde dominante (sa teinte) ainsi que son degré de dilution. Dans un diagramme de chromaticité, le blanc est situé au centre. Sur la périphérie se trouvent les couleurs saturées. Pour déterminer la teinte d'un point dans ce diagramme, il suffit de tracer la droite reliant le blanc à ce point. Si on prolonge la droite du côté du point blanc, on obtient la couleur complémentaire. Le degré de dilution est obtenu enfaisant le rapport de la distance entre le point et sa couleur saturée par la distance entre le blanc et la couleur saturée.
Conversion du système RGB vers XYZ et réciproquement - on utilise pour cela les équations suivantes :
x = 0.489989 r + 0.310008 g + 0.2 b
y = 0.26533 r + 0.81249 g + 0.01 b
z = 0.0 r + 0.01 g + 0.99 b
r = 2.3647 x - 0.89658 y - 0.468083 z
g = -0.515155x + 1.426409y - 0.088746z
b = -0.005203 x - 0.014407 y + 1.0092 z
La dénomination de cette représentation vient de la traduction anglaise de teinte, saturation, valeur (ou luminance) : Hue, Saturation,
Value.
Ce modéle est également appelé "modèle du peintre" en référence aux méthodes de peinture : le peintre commence par choisir une couleur (H) puis ajoute du blanc pour désaturer la couleur (S) et ajoute enfin du noir pour dévaleur la couleur (V).
Supposons que vous ayez une image et que vous souhaitiez améliorer son apparence. Vous devez alors avoir accès à chacun de ses pixels et les remplacer par d'autres pixels. Vous pouvez aussi choisir de calculer chaque pixel, par exemple pour afficher le résultat d'une mesure physique ou d'un calcul mathématique.
Jusqu'à présent, nous avons surtout étudié les objets de la classe java.awt.Image ainsi que la façon de les charger et les dessiner. Comment examiner l'intérieur de l'image et actualiser ses données ? En fait, Image ne permet pas d'accéder à ses données. Un objet de la classe Image n'est pas modifiable. D'ailleurs, je le rappelle, la classe Image est abstraite. Nous devons faire appel à une classe beaucoup plus sophistiquée, java.awt.image.BufferedImage.
Ces classes sont étroitement apparentées puisque BufferedImage est en réalité une classe fille de Image. Mais BufferedImage propose de nombreux contrôles sur les données de l'image. Puisqu'il s'agit d'une classe fille de Image, et grâce au polymorphisme, il est possible de passer un BufferedImage à l'une des méthodes de Graphics2D qui acceptent une image.
En réaliter, comme son l'indique, BufferedImage est une image tampon qui représente un tableau rectangulaire de pixels. Une image n'est donc qu'un rectangle de pixels de couleurs, concept assez simple. Toutefois, une grande complexité se cache dans la classe BufferedImage car il existe de nombreuses manières de représenter les couleurs des pixels.
Dans une image de données RGB, les valeurs rouge, vert ou bleu de chaque pixels peuvent être stockées dans des tableaux d'octets, ou chaque pixel peut être représenté par un entier contenant les valeurs rouge, vert ou bleu. Telle image en 16 niveaux de gris stockera 8 pixels dans chaque élément d'un tableau d'entiers. Il existe de nombreuses manières de stocker des données image, et BufferedImage est conçus pour les supporter toutes.
Une image tampon est donc un tableau rectangulaire de pixels qui correspond au canevas suivant :
Un BufferedImage est constituée de deux parties :
C'est ainsi qu'une image est affichée à l'écran. Le système graphique récupère les données de chaque pixel de l'image à partir du Raster. Puis le ColorModel indique ce que doit être la couleur de chaque pixel, et le système graphique est capable de la définir.
Le Raster (la trame) est lui-même composé de deux parties :
L'API 2D comporte diverses variantes de ColorModel, SampleModel et DataBuffer. Elles servent de parpaings de construction commodes pour la plupart des formats de stockage d'image courants.
Il existe plusieurs façon de représenter une couleur. Plusieurs codages des couleurs existent : les valeurs rouge, vert, bleu, (RGB) ; teinte, saturation, valeur (HSV, hue, saturation, value) ; teinte, luminosité, saturation (HLS) ; et d'autres encore. En outre, il est possible de fournir les informations de couleur entière pour chaque pixel, ou un simple indice dans une table de couleurs (palette) pour chaque pixel. La façon de représenter une couleur s'appelle un modèle de couleur. L'API 2D propose des outils de gestion pour tous les modèles de couleur imaginables. Nous n'étudierons ici que deux grandes familles de modèles de couleur : direct et indexé.
Pour générer les données d'un pixel, il est nécessaire de préciser un modèle de couleur ; la classe abstraite java.awt.image.ColorModel représente un modèle de couleur. Par défaut, Java 2D utilise un modèle de couleur directe baptisée ARGB. Le A signifie "alpha", nom traditionnellement utilisé pour transparence. RGB fait référence aux comopsantes rouge, vert et bleu, combinées pour produire une couleur composite unique.
Dans le modèle ARGB par défaut, chaque pixel est représenté par un entier sur 32 bits, décomposé en quatre champs de 8 bits : dans l'ordre, la transparence (Alpha) puis les composantes rouge, vert et bleu :
Pour créer une instance du modèle ARGB par défaut, appelez la méthode statique getRGBDefault() de ColorModel. Elle renvoie un objet DirectColorModel, classe fille de ColorModel.
Dans un modèle de couleur indexé, chaque pixel est représenté par le plus petit élément d'information : un index dans une table de valeurs de couleur réelles. Pour certaines applications, ce modèle peut se révéler plus commode. En précense d'un affichage 8 bits ou moins (comme pour le Web), l'utilisation d'un modèle indexé sera plus efficace, dans la mesure où le matériel utilise une certaine forme de modèle de couleur indexé.
Après voir pris connaissance des différentes classes mises en jeu, notamment la classe BufferedImage, et de la théorie relative à la notion de couleur, nous allons maintenant mettre en pratique ces différents concepts.
La plupart des images que vous manipulez sont simplement lues à partir d'un fichier image. Elles ont pu être soit produites par un appareil photo numériques ou par un scanner, soit produite par un programme de dessin.
Toutefois, il est possible de créer une image de toute pièce, notamment lorsque vous désirez tracer des courbes pixel par pixel. La création de l'image se fait par l'intermédiaire du constructeur de BufferedImage.
BufferedImage(int largeur, int hauteur, int typeImage);
Pour créer une image, construisez un objet BufferedImage en fournissant la taille de l'image et son type. Le type le plus répandu est BufferedImage.TYPE_INT_ARGB, dans lequel chaque pixel est spécifié par un entier décrivant les valeurs de rouge, de vert, de bleu et de transparence (ou alpha).
Le fait de travailler avec la transparence permet de conserver la couleur de fond choisie. Sans transparence le fond est noir. Dans ce contexte, et quelque soit le cas de figure, l'image proposée est une image vide.
BufferedImage image = new BufferedImage(largeur, hauteur, BufferedImage.TYPE_INT_ARGB);
Les types d'images indiquent comment les couleurs des pixels sont codés :
Types d'images | |
---|---|
TYPE_3BYTE_BGR | bleu, vert, rouge, sur 8 bits chacun |
TYPE_4BYTE_ABGR | alpha, bleu, vert, rouge, sur 8 bits chacun |
TYPE_4BYTE_ABGR_PRE | alpha, bleu, vert, rouge, sur 8 bits chacun, les couleurs pondérées |
TYPE_BYTE_BINARY | 1 bit par pixel, groupés en octets |
TYPE_BYTE_INDEXED | 1 octet par pixel, indice dans une table de couleurs |
TYPE_BYTE_GRAY | 1 octet par pixel, niveau de gris |
TYPE_USHORT_555_RGB | rouge, vert, bleu, sur 5 bits, codés dans un short |
TYPE_USHORT_565_RGB | rouge sur 5, vert sur 6, bleu sur 5 bits |
TYPE_USHORT_GRAY | niveau de gris sur 16 bits |
TYPE_INT_RGB | rouge, vert, bleu sur 8 bits chacun, dans un int |
TYPE_INT_BGR | bleu, vert, rouge (Solaris) |
TYPE_INT_ARGB | alpha, rouge, vert, bleu sur 8 bits, dans un int |
TYPE_INT_ARGB_PRE | les couleurs déjà pondérées par alpha |
Avant de travailler sur les pixels de l'image, soit par consultation, soit en proposant une nouvelle valeur de couleur pour un pixel particulier, souvenez-vous que la classe BufferedImage est composée de deux objets :
Ainsi, appelez la méthode getRaster() pour obtenir un objet de type WritableRaster. Cet objet qui correspond à la trame de l'image est utile pour accéder aux pixels de l'image et pour les modifier.
WritableRaster trame = image.getRaster();
Cet objet de type WritableRaster possède un certain nombre de méthodes, notamment la méthode setPixel(). La méthode setPixel() vous permet de fixer un seul pixel. Cette méthode est surchargée pour chaque type d'image.
Ainsi, si votre image est créée à partir du type TYPE_INT_ARGB, la méthode est :
setPixel(int x, int y, int[] données);
Le tableau de données doit alors contenir quatre entiers dont les valeurs sont comprises entre 0 et 255, respectivement pour les composantes rouge, vert, bleu et alpha. (quelque soit le type d'image, le dernier paramètre est toujours un tableau d'entier).
int noir = {0, 0, 0, 255};
trame.setPixel(x, y, noir);
Voici un exemple où la composition de l'image consiste à tracer une sinusoïde en triple épaisseur. La période de la sinusoïde doit toujours correspondre à la largeur de la fenêtre, même si un redimensionnement est proposé.
La couleur de fond de la fenêtre doit être conservé en orange. Le tracé s'effectuera en jaune. Pour conserver la couleur de fond, il est alors judicieux d'utiliser la transparence. Du coup, le meilleur type d'image est alors TYPE_INT_ARGB.
Pour connaître exactement les dimensions de l'image à mettre en place. Il est judicieux de redéfinir la méthode setBounds(). En effet, cette dernière est systématiquement sollicité lorsqu'on change la dimension de la fenêtre ou alors pour l'affichage de la fenêtre la toute première fois.
public class Fenêtre extends JFrame { public Fenêtre() throws IOException { this.setDefaultCloseOperation(this.EXIT_ON_CLOSE); this.setSize(300, 250); this.setTitle("Tracé de dessin"); this.getContentPane().setBackground(Color.ORANGE); this.getContentPane().add(new Zone()); } public static void main(String[] args) throws IOException { new Fenêtre().setVisible(true); } } class Zone extends JComponent { private BufferedImage image; protected void paintComponent(Graphics surface) { surface.drawImage(image, 0, 0, null); } public void setBounds(int x, int y, int largeur, int hauteur) { super.setBounds(x, y, largeur, hauteur); init(); } private void init() { int largeur = this.getWidth(); int hauteur = this.getHeight(); image = new BufferedImage(largeur, hauteur, BufferedImage.TYPE_INT_ARGB); WritableRaster trame = image.getRaster(); int[] jaune = {255, 255, 0, 255}; for (int x=0; x<largeur; x++) { double angle = (double)x/largeur; int y = (int)(hauteur/2*(1+0.9*Math.sin(angle*2*Math.PI))); trame.setPixel(x, y, jaune); trame.setPixel(x,y-1, jaune); trame.setPixel(x, y+1, jaune); } } }
La difficulté dans l'utilisation de cette méthode setPixel() réside dans le fait qu'il ne suffit pas de fournir une valeur Color, mais qu'il faut connaître le mode de couleurs utilisé par l'image en mémoire. Cela dépend du type de l'image. Nous venons de le voir, si votre image est du type TYPE_INT_ARGB, chaque pixel est décrit impérativement par quatre valeurs, une composante de rouge, une de vert, une de bleu et une pour la couche alpha, chacune variant de 0 à 255. Vous devez le fournir dans un tableau de quatre entiers. Si, par exemple, pour ce type d'image, vous proposez un tableau de trois valeurs au lieu de quatre (occultant ainsi la valeur alpha), vous allez avoir une exception qui va être levée et votre programme ne fonctionnera pas correctement.
Si vous désirez, malgré tout, régler votre couleur à l'aide d'un tableau de trois valeurs correspondant uniquement aux couleurs fondamentales sans utiliser la couche alpha, il est alors préférable de choisir un autre type d'image, par exemple le type TYPE_INT_RGB.
class Zone extends JComponent { private BufferedImage image; protected void paintComponent(Graphics surface) { surface.drawImage(image, 0, 0, null); } public void setBounds(int x, int y, int largeur, int hauteur) { super.setBounds(x, y, largeur, hauteur); init(); } private void init() { int largeur = this.getWidth(); int hauteur = this.getHeight(); image = new BufferedImage(largeur, hauteur, BufferedImage.TYPE_INT_RGB); WritableRaster trame = image.getRaster(); int[] jaune = {255, 255, 0}; for (int x=0; x<largeur; x++) { double angle = (double)x/largeur; int y = (int)(hauteur/2*(1+0.9*Math.sin(angle*2*Math.PI))); trame.setPixel(x, y, jaune); trame.setPixel(x,y-1, jaune); trame.setPixel(x, y+1, jaune); } } }
Vous remarquez, vu que nous n'utilisons plus la couche alpha, que le fond de l'image par défaut est noir. Nous pouvons utiliser la méthode setPixel() aussi bien sur des images vides que sur des fichiers images.
De même qu'il est possible de modifier la valeur d'un pixel à l'aide de la méthode setPixel() issue de la classe WritableRaster, nous pouvons faire l'inverse, c'est-à-dire lire la couleur d'un pixel particulier. Pour cela, toujours par l'intermédiaire de la classe WritableRaster (par héritage) il suffit d'utiliser la méthode getPixel(). En réalité, cette méthode est implémentée dans la classe mère Raster. Par ailleurs, tout comme la méthode setPixel(), la méthode getPixel() est surchargée pour correspondre au type d'image choisi.
Si, encore une fois, je prend le type d'image TYPE_INT_ARGB, vous devez au préalable fournir un tableau vierge de quatre entiers, destiné à contenir les valeurs d'échantillonnage.
int[] c = new int[4]; trame.getPixel(x, y, c); Color couleur = new Color(c[0], c[1], c[2], c[3]);
A titre d'exemple, nous allons utiliser la méthode getPixel() afin de récupérer l'histogramme des composantes rouge, vert et bleu d'une photo stockée dans un fichier. Vous remarquerez au passage que cette photo est plutôt surexposée puisque les courbes se situent largement vers la droite. Je rappelle qu'un histogramme consiste à comptabiliser la valeur de chaque intensité lumineuse (de 0 à 255) pour les trois couleurs fondamentales. Le tracé de l'histogramme se fait en surimpression sur la photo. Pour cela, nous utiliserons largement les techniques de tracé de Java 2D.
class Zone extends JComponent { private BufferedImage image, histogramme; private final int largeur = 256; private final int hauteur = 200; private Graphics2D dessin; private final int[] rouge = new int[256]; private final int[] vert = new int[256]; private final int[] bleu = new int[256]; public Zone() throws IOException { image = ImageIO.read(new File("chouette.jpg")); récupérerRVB(); tracerHistogrammes(); } protected void paintComponent(Graphics surface) { surface.drawImage(image, 0, 0, null); surface.drawImage(histogramme, (this.getWidth()-largeur)/2, (this.getHeight()-hauteur)/2, null); } private void récupérerRVB() { Raster trame = image.getRaster(); int[] rgb = new int[3]; int maximum = 0; for (int y=0; y<image.getHeight(); y++) for (int x=0; x<image.getWidth(); x++) { trame.getPixel(x, y, rgb); rouge[rgb[0]]++; vert[rgb[1]]++; bleu[rgb[2]]++; } } private void tracerHistogrammes() { histogramme = new BufferedImage(largeur, hauteur, BufferedImage.TYPE_INT_ARGB); dessin = histogramme.createGraphics(); Rectangle2D rectangle = new Rectangle2D.Double(0, 0, largeur-1, hauteur-1); dessin.draw(rectangle); dessin.setPaint(new Color(1F, 1F, 1F, 0.2F)); dessin.fill(rectangle); changerAxes(); dessin.setPaint(new Color(1F, 0F, 0F, 0.4F)); tracerHistogramme(rouge); dessin.setPaint(new Color(0F, 1F, 0F, 0.4F)); tracerHistogramme(vert); dessin.setPaint(new Color(0F, 0F, 1F, 0.4F)); tracerHistogramme(bleu); } private void changerAxes() { dessin.translate(0, hauteur); double surfaceImage = image.getWidth()*image.getHeight(); double surfaceHistogramme = histogramme.getWidth()*histogramme.getHeight(); dessin.scale(1, -surfaceHistogramme/surfaceImage/3.7); } private void tracerHistogramme(int[] couleur) { for (int i=0; i<255; i++) dessin.drawLine(i, 0, i, couleur[i]); } }
Si votre image est d'un autre type que TYPE_INT_ARGB, et que vous connaissiez le format utilisé pour les pixels, vous pouvez toujours passer par les méthodes getPixel() et setPixel(). Cependant, vous devez également connaître le format d'encodage de chaque composante pour ce type d'image.
L'enjeu ici est de pouvoir récupérer les valeurs des composantes rouge, vert et bleu, quel que soit le modèle de couleur utilisé afin d'effectuer les traitements sur les pixels de façon généralisé indépendamment du type d'image. Effectivement, chaque type d'image possède un modèle de couleurs qui permet de passer les valeurs d'échantillonnage en tableau au modèle de couleurs standard RVB.
Souvenez-vous que le type de l'image est encapsulé dans le modèle de couleur représenté par la classe ColorModel. Au préalable, avant d'effectuer les traitements au niveau du pixel, il faut d'abord récupérer le modèle de couleur au moyen de la méthode getColorModel() de la classe BufferedImage :
ColorModel modèle = image.getColorModel();
Pour déterminer les composantes de couleur d'un pixel, appelez ensuite la méthode getDataElements() de la classe Raster. Cette méthode est l'équivalent de la méthode getPixel(), avec l'avantage sur cette dernière qu'il n'est pas indispensable de connaître le modèle de couleur utilisé.
Object données = trame.getDataElements(x, y, null);
L'objet renvoyé par la méthode getDataElements() est en fait un tableau de valeurs d'échantillonnage. Vous n'avez pas besoin de le savoir pour le traiter, mais cela explique pourquoi cette méthode est appelée getDataElements().
Maintenant que nous avons récupérées les données relatives au pixel choisi, le modèle de couleur peut, dès lors, transformer ces données en valeurs standard ARGB et ainsi effectuer la conversion d'un modèle particulier vers le modèle RVB. Ainsi, la méthode getRGB() de la classe ColorModel renvoie une valeur int comprenant les composantes rouge, vert, bleu et alpha sur quatre blocs de 8 bits chacun. Ensuite, vous pouvez reconstituer une valeur Color à partir de cet entier grâce au constructeur :
Color(int argb, boolean alpha)
Voici donc l'enchênement à réaliser :
ColorModel modèle = image.getColorModel();
Object données = trame.getDataElements(x, y, null);
int argb = modèle.getRGB(données);
Color couleur = new Color(argb, true);
A noter qu'il est aussi possible de récupérer la valeur de chacune des composantes en particulier à l'aide des méthodes respectives getRed(), getGreen(), getBlue() et getAlpha() de la classe ColorModel.
ColorModel modèle = image.getColorModel();
Object données = trame.getDataElements(x, y, null);
int rouge = modèle.getRed(données);
A titre d'exercice, reprenons l'exemple traité sur la recherche des histogrammes d'une image stockée sur le disque dur. Seule la méthode récupérerRVB() subit la modification. En effet, c'était à l'intérieur de cette méthode que nous faisions appel à la méthode getPixel(). Remarquez au passage que grâce à cette méthode getDataElements(), le codage est plus réduit et surtout plus logique et facile à lire.
private void récupérerRVB() { Raster trame = image.getRaster(); ColorModel modèle = image.getColorModel(); int maximum = 0; for (int y=0; y<image.getHeight(); y++) for (int x=0; x<image.getWidth(); x++) { Object données = trame.getDataElements(x, y, null); rouge[modèle.getRed(données)]++; vert[modèle.getGreen(données)]++; bleu[modèle.getBlue(données)]++; } }
A l'aide cette approche, nous voyons plus facilement une plus grande connivence entre le modèle de couleur et la trame elle-même.
.
Lorsque vous souhaitez choisir une couleur particulière pour un pixel, il faut suivre ces étapes à l'envers. La méthode getRGB() de la classe Color fournit une valeur int contenant les composantes alpha, rouge, vert et bleu. Passez cette valeur à la méthode getDataElements() de la classe ColorModel. La valeur de retour est un Object contenant une description de la couleur spécifique à un modèle de couleurs particulier. Renvoyer alors cet objet à la méthode setDataElements() de la classe WritableRaster. Encore une fois, cette méthode est l'équivalent de la méthode setPixel(), avec l'avantage sur cette dernière qu'il n'est pas indispensable de connaître le modèle de couleur utilisé.
ColorModel modèle = image.getColorModel();
int argb = couleur.getRGB();
Object données = modèle.getDataElements(argb, null);
trame.setDataElements(x, y, données);
Il est également possible de proposer une valeur entière argb à l'aide d'une couleur prédéfinie :
int argb = Color.red.getRGB();
A titre d'exemple, nous allons reprendre le tracé de la sinusoïde en triple épaisseur. Ici, nous remplaçons la méthode setPixel() par la méthode setDataElements(), et ceci à l'intérieur de la méthode init().
class Zone extends JComponent { private BufferedImage image; protected void paintComponent(Graphics surface) { surface.drawImage(image, 0, 0, null); } public void setBounds(int x, int y, int largeur, int hauteur) { super.setBounds(x, y, largeur, hauteur); init(); } private void init() { int largeur = this.getWidth(); int hauteur = this.getHeight(); image = new BufferedImage(largeur, hauteur, BufferedImage.TYPE_INT_ARGB); WritableRaster trame = image.getRaster(); ColorModel modèle = image.getColorModel(); int argb = Color.yellow.getRGB(); Object données = modèle.getDataElements(argb, null); for (int x=0; x<largeur; x++) { double angle = (double)x/largeur; int y = (int)(hauteur/2*(1+0.9*Math.sin(angle*2*Math.PI))); trame.setDataElements(x, y, données); trame.setDataElements(x, y-1, données); trame.setDataElements(x, y+1, données); } } }
Lorsque vous récupérez une image à partir d'un fichier, le type de l'image est fixé. Il est toutefois possible de changer sont type d'image pour avoir, par exemple, une photo en noir et blanc alors que celle-ci a été prise en couleur. Le plus simple consiste à copier l'image originale dans une autre créée avec le bon type comme, par exemple, le type niveau de gris.
Pour copier une image dans une autre, le plus facile est de passer par un Graphics, et de proposer ensuite le tracé de l'image au moyen de la méthode drawImage() surchargée pour ce type de traitement :
class Zone extends JComponent { private BufferedImage image; public Zone() throws IOException { BufferedImage source = ImageIO.read(new File("chouette.jpg")); image = new BufferedImage(source.getWidth(), source.getHeight(), BufferedImage.TYPE_USHORT_GRAY); Graphics2D dessin = image.createGraphics(); dessin.drawImage(source, null, null); } protected void paintComponent(Graphics surface) { surface.drawImage(image, 0, 0, null); } }
Nous remarquons que la plupart des classes propres aux traitements d'image sont conçues pour être manipulées avec le modèle standard ARGB. Dans certains cas, toutefois, il peut être intéressant de travailler plutôt avec le modèle représentant la teinte, la saturation et la luminosité HSB (Hue : teinte, Saturation, Brightness : luminosité). La classe Color propose des méthodes statiques qui permettent de passer de l'un à l'autre. Il existe en effet les méthodes :
A titre d'exemple, nous allons réhausser les couleurs de la chouette en proposant donc une saturation des couleurs 50% plus fortes.
Sans saturation ---------------------------------------- Avec saturation (légèrement plus de couleur)
class Zone extends JComponent { private BufferedImage image; public Zone() throws IOException { image = ImageIO.read(new File("chouette.jpg")); WritableRaster trame = image.getRaster(); ColorModel modèle = image.getColorModel(); for (int y=0; y<image.getHeight(); y++) for (int x=0; x<image.getWidth(); x++) { Object données = trame.getDataElements(x, y, null); float[] hsb = new float[3]; Color.RGBtoHSB(modèle.getRed(données), modèle.getGreen(données), modèle.getBlue(données), hsb); Object changement = modèle.getDataElements(Color.HSBtoRGB(hsb[0], hsb[1]*1.5F, hsb[2]), null); trame.setDataElements(x, y, changement); } } protected void paintComponent(Graphics surface) { surface.drawImage(image, 0, 0, null); } }
Lorsque nous effectuons un traitement d'image non pas sur des photos mais plutôt à partir d'une image vierge qui sera ensuite utilisée afin d'effectuer des tracés, il peut s'avérer intéressant de créer son propre modèle de couleur avec juste le nombre de couleur requis. En effet, le mode par défaut souvent utilisé TYPE_INT_ARGB prend systématiquement 32 bits pour chaque pixel. Si votre tracé n'utlise que trois couleurs, nous obtenons beaucoup de déchets.
Nous avons alors la possibilité de définir son propre modèle de couleur qui est un modèle de couleur indexé. Dans ce cas là, il suffit de spécifier exactement les couleurs à utiliser.
Jusqu'à présent, la démarche consistait à créer d'abord un BufferedImage et ensuite de récupérer à la fois la trame et le modèle de couleur à partir de cette création. Dans cette procédure, nous sommes obligé d'imposer un modèle de couleur prédéfini. Ici, c'est justement ce que nous ne désirons pas faire. La démarche sera donc inversée.
Nous devons donc suivre l'orde de création suivant :
A titre d'exemple, nous allons reprendre le tracé de la sinusoïde avec en plus le tracé de l'axe des abscisses. Nous imposons ici, qu'il n'existera que trois couleurs possibles par pixel, le fond en rouge, l'axe en jaune, et la courbe en vert. Le nombre de bits par pixel est alors de 2 (22 possibilités).
class Zone extends JComponent { private BufferedImage image; protected void paintComponent(Graphics surface) { surface.drawImage(image, 0, 0, null); } public void setBounds(int x, int y, int largeur, int hauteur) { super.setBounds(x, y, largeur, hauteur); init(); } private void init() { int largeur = this.getWidth(); int hauteur = this.getHeight(); int longueur = ((largeur+7)*hauteur)/4; byte[] données = new byte[longueur]; DataBuffer buffeur = new DataBufferByte(données, longueur); WritableRaster trame = Raster.createPackedRaster(buffeur, largeur, hauteur, 2, null); ColorModel modèle = new IndexColorModel(2, 3, new byte[] {(byte)Color.red.getRed(), (byte)Color.yellow.getRed(), (byte)Color.green.getRed()}, new byte[] {(byte)Color.red.getGreen(), (byte)Color.yellow.getGreen(), (byte)Color.green.getGreen()}, new byte[] {(byte)Color.red.getBlue(), (byte)Color.yellow.getBlue(), (byte)Color.green.getBlue()}); image = new BufferedImage(modèle, trame, false, null); Graphics2D dessin = image.createGraphics(); dessin.setPaint(Color.yellow); dessin.draw(new Line2D.Double(0, hauteur/2, largeur, hauteur/2)); int argb = Color.green.getRGB(); Object couleur = modèle.getDataElements(argb, null); for (int x=0; x<largeur; x++) { double angle = (double)x/largeur; int y = (int)(hauteur/2*(1+0.9*Math.sin(angle*2*Math.PI))); trame.setDataElements(x, y, couleur); trame.setDataElements(x, y-1, couleur); trame.setDataElements(x, y+1, couleur); } } }
Les données de l'image sont stockées sous la forme d'un tableau d'octets (byte), dont chaque élément comprend quatre pixels (4x2=8). La longueur du tableau est donc calculée en multipliant la largeur et la hauteur de l'image en divisant le résultat par quatre. Nous devons également tenir compte du fait que chaque rangée de l'image démarre sur une frontière d'octets (cas le plus défavorable 7). Par exemple, une image de 13 pixels de large utilisera réellement 2 octets (16 bits) pour chaque rangée :
int longueur = ((largeur+7)*hauteur)/4;
Le tableau d'octet est ensuite créé. La classe Raster contiendra une référence à ce tableau au travers du DataBuffer. Par la suite, et de façon classique, nous changerons dynamiquement les données de l'image. Une fois en possession du tableau de données d'image, nous pouvons facilement créer une DataBuffer :
byte[] données = new byte[longueur];
DataBuffer buffeur = new DataBufferByte(données, longueur);
DataBuffer possède donc plusieurs classes filles, par exemple DataBufferByte, destinées à faciliter la création d'un tampon de données correspondant parfaitement aux tableaux brutes de même type.
L'étape suivante consiste normalement à créer un SampleModel. Nous pourrons alors créer un Raster à partir du SampleModel et du DataBuffer. Par chance, la classe Raster contient quelques méthodes statiques utilisables pour créer des types courants de Raster. L'une de ces méthodes createPackedRaster() crée un Raster à partir de données contenant plusieurs pixels empaquetés en éléments de tableau. Nous l'utilisons en fournissant le tampon de données, la largeur et la hauteur, et en précisant que chaque pixel occupe 2 bits :
WritableRaster trame = Raster.createPackedRaster(buffeur, largeur, hauteur, 2, null);
Le dernier argument est un java.awt.Point indiquant l'emplacement du coin supérieur gauche du Raster. La valeur null indique que nous utilisons <0, 0> (emplacement par défaut).
Le dernier élément du puzzle est le ColorModel. Chaque pixel vaut 0 ou 1 ou 2, mais comment interpréter cela en tant que couleur ? Ici, nous utilisons un IndexColorModel avec une très petite palette ne comportant que trois entrées, une pour le rouge, une pour le jaune et une pour le vert :
ColorModel modèle = new IndexColorModel(2, 3,
new byte[] {(byte)Color.red.getRed(), (byte)Color.yellow.getRed(), (byte)Color.green.getRed()},
new byte[] {(byte)Color.red.getGreen(), (byte)Color.yellow.getGreen(), (byte)Color.green.getGreen()},
new byte[] {(byte)Color.red.getBlue(), (byte)Color.yellow.getBlue(), (byte)Color.green.getBlue()});
Le constructeur IndexColorModel utilisé ici accepte le nombre de bits par pixel (2), le nombre d'entrées de la palette (3), et trois tableaux d'octets qui sont les composantes rouge, vert et bleu des couleurs de la palette. Notre palette est constituée de trois couleurs défnies plus haut. Si vous les connaisssez, vous pouvez plutôt écrire directement avec les valeurs décimales, par exemple pour le rouge (255, 0, 0).
Avec tous les éléments en main, nous créons un BufferedImage. Pour créer le BufferedImage, nous passons le modèle de couleur et la trame inscriptible que nous venons de créer :
image = new BufferedImage(modèle, trame, false, null);
Il existe une méthode intéressante dans la classe BufferedImage qui permet de prendre qu'une partie de l'image. Il s'agit de la méthode getSubimage() qui retourne alors un autre BufferedImage et dont la zone à récupérer est spécifiée par les paramètres de la méthode :
BufferedImage copie = image.getSubimage(x, y, largeur, hauteur);
Voici un exemple qui permet de limiter la zone d'affichage à la tête de la chouette :
class Zone extends JComponent { private BufferedImage image; public Zone() throws IOException { image = ImageIO.read(new File("chouette.jpg")).getSubimage(90, 20, 160, 140); } public Zone(BufferedImage image) { this.image = image; } protected void paintComponent(Graphics surface) { surface.drawImage(image, 0, 0, null); } }
Dans le chapitre précédent, nous avons vu comment réaliser des traitements sur une image au niveau de chaque pixel. Dans ce chapitre, nous allons effectuer d'autres types de traitement qui cette fois-ci influencerons systématiquement la totalité de l'image. Dans ce contexte, ce type de traitement utilise la technique des filtres.
Dans le chapitre précédent, nous avons surtout vu comment construire une image à partir de rien. Cependant, il arrive souvent que vous possédiez déjà une image et vous souhaitez l'améliorer d'une manière ou d'une autre. Naturellement, vous pouvez passer par les méthodes getPixel()/getDataElements() de la section précédente pour lire les données de l'image, les manipuler et les écrire dans la nouvelle image. Mais heureusement la tecnologie Java 2 fournit déjà un ensemble de filtres qui prennent en charge les opérations les plus courantes de traitement d'image.
Nous connaissons déjà largement la classe BufferedImage qui permet de contrôler les pixels d'une image. Il existe maintenant des classes qui fournissent l'ensemble des traitements possibles par la technique de filtrage. Toutes ces nouvelles classes implémentent l'interface BufferedImageOp. Il existe en réalité, cinq classes qui opèrent des traitements bien spécifiques :
Pour réaliser une opération de filtrage, il suffit de faire appel à la méthode filter() d'une des classes de filtrage qui implémente l'interface BufferedImageOp sur l'image à traiter, représentée donc par la classe BufferedImage :
filter(BufferedImage source, BufferedImage destination)
Certaines opérations peuvent transformer une image directement, sans passer par une autre image, mais la plupart n'en sont pas capables.
.
En gros, voici, la procédure à suivre pour réaliser un filtre sur une image :
BufferedImage imageOriginale = ImageIO.read(new File("image.jpg"));
BufferedImageOp opération = new AffineTransformOp(...);
BufferedImage imagefiltrée = new BufferedImage(imageOriginale.getWidth(), imageOriginale.getHeight(), imageOriginale.getType());
opération.filter(imageOriginale, imagefiltrée);
...
Nous allons commencer par la première opération qui s'occupe de réaliser des transformations affines. Les transformations affines sont des transformations en deux dimensions qui préservent le parallélisme des lignes ; cela comprend des opérations telles que le redimmensionnement et les rotations, mais également les translations et les cisaillements.
L'opérateur d'image java.awt.image.AffineTransformOp effectue une transformation géométrique d'une image source pour produire une image de destination. Pour créer un objet AffineTransformOp, précisez la transformation affine souhaitée, sous la forme d'un java.awt.geom.AffineTransform.
Toutefois, le constructeur AffineTransformOp nécessite, certe une transformation affine, mais également une stratégie d'interpolation. Une interpolation est en effet nécessaire pour calculer les pixels de l'image cible si ceux-ci ne correspondent pas exactement aux pixels de l'image source. Il existe trois algorithmes correspondant stratégies d'interpolation :
En réalité, nous retrouvons dans la classe AffineTransform à peu près les mêmes types de méthodes de transformations que celles que nous avons étudiées dans l'étude sur le graphisme 2D, savoir :
A titre d'exemple, nous allons afficher une photo qui a été réalisée avec un capteur de 6.5 MPx. Nous devons donc récupérer cette photo au format JPEG et effectuer deux transformations. D'une part, nous devons réduire la taille de cette image de telle sorte que sa surface soit 100 fois plus petite. Par ailleurs, la photo doit subir une rotation de 90° dans le sens horaire. Il faut que la qualité finale de cette image subisse le moins de déperdition de qualité possible.
===>
class Zone extends JComponent { private BufferedImage image; public Zone() throws IOException { BufferedImage source = ImageIO.read(new File("couché de soleil.jpg")); BufferedImage imageRetaillée = new BufferedImage(source.getWidth()/10, source.getHeight()/10, source.getType()); AffineTransform retailler = AffineTransform.getScaleInstance(0.1, 0.1); int interpolation = AffineTransformOp.TYPE_BICUBIC; AffineTransformOp retaillerImage = new AffineTransformOp(retailler, interpolation); retaillerImage.filter(source, imageRetaillée);
image = new BufferedImage(source.getHeight()/10, source.getWidth()/10, source.getType()); double centreDeRotation = imageRetaillée.getHeight()/2; AffineTransform pivoter = AffineTransform.getRotateInstance(Math.toRadians(90), centreDeRotation, centreDeRotation); AffineTransformOp pivoterImage = new AffineTransformOp(pivoter, interpolation); pivoterImage.filter(imageRetaillée, image); } protected void paintComponent(Graphics surface) { surface.drawImage(image, 0, 0, null); } }
La remise à l'échelle (rescale) consiste à mulitplier les valeurs des pixels d'une image par une constante. La taille de l'image n'est pas affectée (pour le cas où vous pensiez que remise à l'échelle signifiait mise à l'échelle), mais les couleurs de ses pixels le sont. Par exemple, dans une image RGB, chaque valeur rouge, vert et bleu de chaque pixel est multiplié par le multiplicateur d'échelle. Nous pouvons également ajuster le résultat par un déplacement.
Dans l'API 2D, la remise à l'échelle est effectuée par la classe java.awt.image.RescaleOp. Pour créer un tel opérateur, indiquez le multiplicateur, le déplacement, et certaines indications de contrôle de qualité de conversion (les mêmes que pour la classe Graphics2D).
opération = new RescaleOp(multiplicateur, déplacement, qualité);
Ainsi, les opérations de “rescaling” modifient les intensités des composantes RGB par deux paramètres :
La nouvelle intensité est :
i’ = i * m + d
Ainsi,
Exemples :
op = new RescaleOp(.5f, 0, null) plus sombre
op = new RescaleOp(.5f, 64, null) plus sombre avec éclairement
op = new RescaleOp(1.2f, 0, null) plus brillant
op = new RescaleOp(1.5f, 0, null) encore plus brillant
Les valeurs d'échantillonnage qui sont trop grandes ou trop petites après le changement d'échelle prennent la valeur de la plus grande ou de la plus petite valeur autorisée. Si l'image est au format ARGB (le cas le plus fréquent pour les photos), le changement d'échelle est traité séparément pour le rouge, le vert et le bleu, mais pas pour la couche alpha. Du coup, il est préférable de choisir le type d'image BufferedImage.TYPE_INT_RGB.
Voici un exemple qui permet d'assombrir l'image de la chouette :
===>
class Zone extends JComponent { private BufferedImage image; public Zone() throws IOException { image = assombrir(ImageIO.read(new File("chouette.jpg"))); } private BufferedImage assombrir(BufferedImage source) { BufferedImage image = new BufferedImage(source.getWidth(), source.getHeight(), BufferedImage.TYPE_INT_RGB); RescaleOp assombrir = new RescaleOp(0.8f, 0, null); assombrir.filter(source, image); return image; } protected void paintComponent(Graphics surface) { surface.drawImage(image, 0, 0, null); } }
Pour éclaircir ou assombrir une image, nous pouvons employer un RescaleOp pour augmenter ou diminuer linéairement l'intensité de chaque valeur de couleur. Des effets d'éclaircissement ou d'assombrissement plus réaliste demandent cependant une transformtion non linéaire. Il est largement préférable d'utiliser dans ce cas là l'opérateur LookupOp traité ci-dessous. Par exemple, on peut utiliser LookupOp pour gérer l'éclaicissement en se basant sur la fonction de racine carrée, qui augmente les couleurs intermédiaire que celles qui sont claires ou foncées.
Toutefois, RescaleOp peut s'avérer utile pour des traitements auxquels nous ne pensons pas à priori. Par exemple, si vous récupérez une photo à partir d'un négatif, il est alors nécessaire d'inverser toutes les couleurs de l'image. Ce type d'opération est linéaire, donc cette classe est tout-à-fait adaptée à la situation :
BufferedImage imageOriginale = ImageIO.read(new File("négatif.jpg"));
RescaleOp opération = new RescaleOp(-1.0F, 255F, null);
BufferedImage imagefiltrée = opération.filter(imageOriginale, imagefiltrée);
L'opération LookupOp permet de spécifier une correspondance arbitraire des valeurs d'échantillonnage. Vous devez fournir un tableau précisant comment chaque valeur doit être transformée.
Le constructeur LookupOp() nécessite un objet de type LookupTable et éventuellement une carte de conseils d'affichage. En réalité, la classe LookupTable est abstraite. Il existe deux sous-classes concrètes : ByteLookupTable et ShortLookupTable. Comme les valeurs de couleur RVB sont des octets, nous utilisons généralement ByteLookupTable. Ce type de tableau peut être construit à partir d'un tableau d'octets et d'un indice entier.
Dans les constructeurs de type LookupTable (en réalité ByteLookupTable() et ShortLookupTable()) :
ByteLookupTable nouvelletable = new ByteLookupTable(décalage, tableauOctets);
opération = new LookupOp(nouvelletable, qualité);
Attention : La recherche est appliquée séparément à chaque valeur de couleur, mais pas à la couche alpha. Pour le traitement , il est alors nécessaire de prendre le type d'image BufferedImage.TYPE_INT_RGB. Vous ne pouvez pas appliquer une LookupOp sur une image à couleurs indexées. Dans ces images, chaque valeur d'échantillonnage est en fait un indice dans une palette de couleurs.
Voici un exemple qui permet d'inverser la valeur de toutes les couleurs. Cela peut être bien utile dans le cas où nous devons scanner des négatifs :
class Zone extends JComponent { private BufferedImage image; public Zone() throws IOException { image = inverser(ImageIO.read(new File("chouette.jpg"))); } private BufferedImage inverser(BufferedImage source) { BufferedImage image = new BufferedImage(source.getWidth(), source.getHeight(), BufferedImage.TYPE_INT_RGB); byte[] inverser = new byte[256]; for (int i=0; i<256; i++) inverser[i] = (byte) (255-i); ByteLookupTable table = new ByteLookupTable(0, inverser); LookupOp inversion = new LookupOp(table, null); inversion.filter(source, image); return image; } protected void paintComponent(Graphics surface) { surface.drawImage(image, 0, 0, null); } }
Voici un autre exemple qui permet de récupérer une seule composante de couleur, comme ici le rouge :
class Zone extends JComponent { private BufferedImage image; public Zone() throws IOException { image = composanteRouge(ImageIO.read(new File("chouette.jpg"))); } private BufferedImage composanteRouge(BufferedImage source) { BufferedImage image = new BufferedImage(source.getWidth(), source.getHeight(), source.getType()); byte[] normal = new byte[256]; byte[] zéro = new byte[256]; for (int i=0; i<256; i++) { normal[i] = (byte)i; zéro[i] = 0; } byte[][] rouge = { normal, zéro, zéro}; ByteLookupTable table = new ByteLookupTable(0, rouge); LookupOp inversion = new LookupOp(table, null); inversion.filter(source, image); return image; } protected void paintComponent(Graphics surface) { surface.drawImage(image, 0, 0, null); } }
Voici ci-dessous un ensemble de méthodes, à l'intérieur d'une classe dénommée LookupFabrique, qui peuvent s'avérer très utiles dans vos différents traitements d'image :
class LookupFabrique { static short[] clair = new short[256]; static short[] meilleurClair = new short[256]; static short[] simplifie = new short[256]; static short[] inverser = new short[256]; static short[] ident = new short[256]; static short[] zero = new short[256]; static { for (int i = 0; i < 256; i++) { clair[i] = (short)(128 + i / 2); meilleurClair[i] = (short)(Math.sqrt((double)i / 255.0) * 255.0); simplifie[i] = (short)(i - (i % 32)); inverser[i] = (short)(255 - i); ident[i] = (short)i; zero[i] = (short)0; } } static LookupOp createClair() { return new LookupOp(new ShortLookupTable(0, clair), null); } static LookupOp createMeilleurClair() { return new LookupOp( new ShortLookupTable(0, meilleurClair), null); } static LookupOp createSimplifie() { return new LookupOp( new ShortLookupTable(0, simplifie), null); } static LookupOp createInverser() { return new LookupOp(new ShortLookupTable(0, inverser), null); } static short[][] rougeSeul = { inverser, ident, ident }; static short[][] vertSeul = { ident, inverser, ident }; static short[][] bleuSeul = { ident, ident, inverser }; static LookupOp createInverserRouge() { return new LookupOp( new ShortLookupTable(0, rougeSeul), null); } static short[][] sansRouge= { zero, ident, ident }; static short[][] sansVert = { ident, zero, ident }; static short[][] sansBleu = { ident, ident, zero }; static LookupOp createSansRouge() { return new LookupOp( new ShortLookupTable(0, sansRouge), null); } }
A l'aide de la classe ColorConvertOp, il est possible de changer d'espace de couleurs. L'une des grosses utilisations, est le passage en noir et blanc d'une photo couleur. Le constructeur de cette classe réclame un objet de la classe ColorSpace qui définie le type de conversion à réaliser suivi, éventuellement, de la carte de conseils d'affichage.
class Zone extends JComponent { private BufferedImage image; public Zone() throws IOException { image = ImageIO.read(new File("chouette.jpg")); ColorConvertOp gris = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_GRAY), null); gris.filter(image, image); } protected void paintComponent(Graphics surface) { surface.drawImage(image, 0, 0, null); } }
La plus puissante des transformations est ConvolveOp, qui effectue une convolution mathématique. Nous préférons éviter d'entrer trop dans les détails mathématiques des convolutions, mais l'idée principale à retenir est assez simple.
Considérons, par exemple, l'élaboration d'un filtre de flou. Cet effet est obtenu en remplaçant chaque pixel par la valeur moyenne des huits pixels voisins et du point considéré. Intuitivement, nous pouvons comprendre pourquoi cette opération rend l'image plus floue. Mathématiquement, cette moyenne peut être exprimée sous la forme d'une convolution utilisant le noyau suivant :
| 1/9 1/9 1/9 |
| 1/9 1/9 1/9 |
| 1/9 1/9 1/9 |
Le noyau d'une convolution est une matrice qui indique le coefficient multiplicateur appliqué à chaque voisin du pixel considéré.
.
Pour construire une opération de convolution, il faut commencer par définir un tableau contenant les valeurs du noyau, puis construire un objet Kernel (noyau). Ensuite, construisez une objet ConvolveOp à partir du noyau, et servez-vous-en pour filtrer l'image. En réalité, il existe deux constructeurs ConvolveOp() :
ConvolveOp(Kernel noyau)
ConvolveOp(Kernel noyau, int conditionAuBord, RenderingHints qualité)
Les conditions au bord sont :
Voici donc quelques exemples où vous pourrez remarquer, qu'avec très peu d'écriture, vous avez des résultats spectaculaires :
class Zone extends JComponent { private BufferedImage image; public Zone() throws IOException { image = flou(ImageIO.read(new File("chouette.jpg"))); } private BufferedImage flou(BufferedImage source) { BufferedImage image = new BufferedImage(source.getWidth(), source.getHeight(), source.getType()); float[] flou = new float[] { 1f/9f, 1f/9f, 1f/9f, 1f/9f, 1f/9f, 1f/9f, 1f/9f, 1f/9f, 1f/9f }; Kernel noyau = new Kernel(3, 3, flou); ConvolveOp opération = new ConvolveOp(noyau); opération.filter(source, image); return image; } protected void paintComponent(Graphics surface) { surface.drawImage(image, 0, 0, null); } }
class Zone extends JComponent { private BufferedImage image; public Zone() throws IOException { image = contour(ImageIO.read(new File("chouette.jpg"))); } private BufferedImage contour(BufferedImage source) { BufferedImage image = new BufferedImage(source.getWidth(), source.getHeight(), source.getType()); float[] contour = new float[] { 0f, -1f, 0f, -1f, 4f, -1f, 0f, -1f, 0f }; Kernel noyau = new Kernel(3, 3, contour); ConvolveOp opération = new ConvolveOp(noyau); opération.filter(source, image); return image; } protected void paintComponent(Graphics surface) { surface.drawImage(image, 0, 0, null); } }
class Zone extends JComponent { private BufferedImage image; public Zone() throws IOException { image = accentuation(ImageIO.read(new File("chouette.jpg"))); } private BufferedImage accentuation(BufferedImage source) { BufferedImage image = new BufferedImage(source.getWidth(), source.getHeight(), source.getType()); float[] accentuation = new float[] { 0f, -1f, 0f, -1f, 5f, -1f, 0f, -1f, 0f }; Kernel noyau = new Kernel(3, 3, accentuation); ConvolveOp opération = new ConvolveOp(noyau); opération.filter(source, image); return image; } protected void paintComponent(Graphics surface) { surface.drawImage(image, 0, 0, null); } }
class Zone extends JComponent { private BufferedImage image; public Zone() throws IOException { image = filtre(ImageIO.read(new File("chouette.jpg"))); } private BufferedImage filtre(BufferedImage source) { BufferedImage image = new BufferedImage(source.getWidth(), source.getHeight(), source.getType()); float[] filtre = new float[] { 1f/4f, 0f, -1f/4f, 2f/4f, 0f, -2f/4f, 1f/4f, 0f, -1f/4f }; Kernel noyau = new Kernel(3, 3, filtre); ConvolveOp opération = new ConvolveOp(noyau); opération.filter(source, image); return image; } protected void paintComponent(Graphics surface) { surface.drawImage(image, 0, 0, null); } }
Pour l'axe des y, il faut proposer le noyau suivant :
float[] filtre = new float[] { -1f/4f, -2f/4f, -1f/4f, 0f, 0f, 0f, 1f/4f, 2f/4f, 1f/4f };
Pour terminer toute cette étude, je vous propose maintenant de pouvoir récupérer des informations de prises de vue issues d'un appareil photo et dont les données sont codées à l'intérieur même d'une image JPEG. Ce codage respecte un standard, et ce type de données est stockée suivant le format EXIF.
En réalité, il n'existe pas dans le JDK actuel de classes qui permettent de récupérer ces données EXIF, vous devez, soit réaliser un ensemble de classes permettant de réaliser ces opérations ou alors passer par une bibliothèque tierce qui traite de ce genre de problème. Cette bibliothèque existe en effet au site : http://www.drewnoakes.com/code/exif/.
J'ai personnellement utilisée cette archive et vous avez ci-dessous un exemple d'utilisation. Si vous de désirez pas allez sur le site en question, vous pouvez toujours télécharger l'archive que j'ai moi-même récupérée : metadata-extractor-2.3.1.jar
Attention, dans vos projets, vous devez impérativement intégrer cette archive pour que le fonctionnement soit correct.
.
import com.drew.imaging.jpeg.*; import com.drew.metadata.*; import com.drew.metadata.exif.*; import java.io.*; import java.util.*; public class Principal { public static void main(String[] args) throws JpegProcessingException, MetadataException { Metadata metadata = new Metadata(); new ExifReader(new File("image.jpg")).extract(metadata); Directory exif = metadata.getDirectory(ExifDirectory.class); System.out.println("Vitesse : "+exif.getDescription(ExifDirectory.TAG_EXPOSURE_TIME)); System.out.println("Ouverture : "+exif.getDescription(ExifDirectory.TAG_FNUMBER)); System.out.println("largeur : "+exif.getInt(ExifDirectory.TAG_EXIF_IMAGE_WIDTH)); System.out.println("hauteur : "+exif.getInt(ExifDirectory.TAG_EXIF_IMAGE_HEIGHT)); System.out.println("ASA : "+exif.getDescription(ExifDirectory.TAG_ISO_EQUIVALENT)); System.out.println("Date : "+exif.getDate(ExifDirectory.TAG_DATETIME)); System.out.println("Fabriquant : "+exif.getDescription(ExifDirectory.TAG_MAKE)); System.out.println("Modèle d'appareil : "+exif.getDescription(ExifDirectory.TAG_MODEL)); System.out.println("Décalage d'exposition : "+exif.getDescription(ExifDirectory.TAG_EXPOSURE_BIAS)); System.out.println("Focale : "+exif.getDescription(ExifDirectory.TAG_FOCAL_LENGTH)); System.out.println("Horizontal / Vertical : "+exif.getDescription(ExifDirectory.TAG_ORIENTATION)); System.out.println("Type de priorité : "+exif.getDescription(ExifDirectory.TAG_EXPOSURE_PROGRAM)); System.out.println("Mesure d'exposition : "+exif.getDescription(ExifDirectory.TAG_EXPOSURE_MODE)); System.out.println("Mode d'exposition : "+exif.getDescription(ExifDirectory.TAG_METERING_MODE)); System.out.println("Flash : "+exif.getDescription(ExifDirectory.TAG_FLASH)); System.out.println("Balance des blancs : "+exif.getDescription(ExifDirectory.TAG_WHITE_BALANCE_MODE)); /* Iterator directories = metadata.getDirectoryIterator(); while (directories.hasNext()) { Directory directory = (Directory)directories.next(); // iterate through tags and print to System.out Iterator tags = directory.getTagIterator(); while (tags.hasNext()) { Tag tag = (Tag)tags.next(); // use Tag.toString() System.out.println(tag); } } */ } }
Voici le résultat :
Vitesse : 1/160 sec
Ouverture : F16
largeur : 3504
hauteur : 2336
ASA : 400
Date : Mon Jun 05 17:41:24 CEST 2006
Fabriquant : Canon
Modèle d'appareil : Canon EOS 20D
Décalage d'exposition : 0 EV
Focale : 105,0 mm
Horizontal / Vertical : Top, left side (Horizontal / normal)
Type de priorité : Aperture priority
Mesure d'exposition : Auto exposure
Mode d'exposition : Multi-segment
Flash : Flash did not fire, auto
Balance des blancs : Manual white balance
L'objet exif de la classe Directory dispose d'un ensemble de méthodes qui permettent de récupérer les informations souhaitées. Vous devez alors préciser, en paramètre de ces méthodes, l'information requise en choisissant la constante adaptée de la classe ExifDirectory.
Voici l'ensemble de ces méthodes :
Et pour conclure, voici un tout petit logiciel de traitement d'images, qui n'a bien entendu aucune prétention, mais qui toutefois, met en oeuvre les différentes classes que nous venons de découvrir tout au long de cette étude :
Pour découvrir l'ensemble du projet défini au moyen de Netbeans : Traitement images.zip