Utilisation de Java pour du traitement d`image

publicité
Groupe Vision, CUI, Université de Genève
http://cui.unige.ch/DI/cours/1811/
2001-2005, Julien Kronegg
Utilisation de Java pour du
traitement d’image - bases
Ce document se propose de montrer comment fonctionne la partie graphique bitmap de la
machine virtuelle Java, en particulier pour le traitement d’images numériques. Il ne s’agit pas
là d’un cours complet mais plutôt d’un exposé de la matière nécessaire pour bien réussir les
TPs.
Les méthodes basées ici utilisent exclusivement les classes standards des packages java.awt
et java.awt.image. Ces classes ne soient pas conçues spécifiquement pour le traitement
d’image, elles sont relativement inefficaces au niveau du temps CPU utilisé. D’autres
librairies, par exemple JIGL proposent de meilleurs outils pour le traitement d’image mais ne
seront pas exposées dans ce document.
Note : il est conseillé de ne pas imprimer ce document parce qu’il sera complété au fur et à
mesure du déroulement du cours et des TPs (en plus, ça économise du papier).
1
Introduction
Une image est représentée en Java par un objet de la classe Image. Un tel objet contient des
données telles que la taille de l'image, les couleurs des pixels de l'image ou le modèle de
couleur utilisé.
D'autres classes Java sont associées aux images et servent à leur traitement :
• ImageProducer;
• ImageConsumer;
• ImageObserver;
L’ImageProducer agit comme un producteur selon le modèle producteur-consommateur.
C’est lui qui effectue par exemple du chargement de l’image.
L’ImageConsumer agit comme un consommateur selon le modèle producteur-consomateur.
C’est lui qui effectue le stockage des informations dans l’Image.
L’ImageObserver est le dispositif qui permet de contrôler le déroulement du processus
producteur-consomateur qui se déroule de manière asynchrone, comme nous le verrons plus
tard.
2
Chargement d'une image depuis un fichier
Java permet de charger des fichiers GIF, JPEG ou PNG depuis une unité de stockage (disque
ou réseau). Le fichier est chargé par la classe Applet ou Toolkit de java.awt par les
méthodes getImage ou createImage. Par exemple :
Toolkit tk = Toolkit.getDefaultToolkit();
Image img = tk.getImage("images/test.jpeg");
Les images sont chargées en fonction de la demande. Les choses se déroulent de la manière
suivante :
1
1. la méthode getImage (ou createImage) crée une instance d'Image avec son
ImageProducer associé et se termine immédiatement (aucune vérification si l'image
existe vraiment);
2. lorsque l'image est nécessaire (par exemple pour un traitement ou pour l'affichage),
l'Image demande à l'ImageConsumer la représentation en pixels de l’image ;
3. l’ImageConsumer demande alors à l’ImageProducer de charger l'image depuis le fichier
(le chemin du fichier est stocké par l’ImageProducer);
ImageProducer
ImageConsumer
Image
fichier
Cette manière de procéder permet à un programme Java de démarrer sans que le chargement
des images ralentisse l'exécution (ce qui est normal puisque les images ne sont pas chargées).
L'inconvénient majeur est que l'image n'étant pas chargée, il n'est pas possible de connaître sa
taille.
Lorsque l’image est vraiment nécessaire, elle est chargée par une thread séparée du
programme principal, ce qui permet de ne pas bloquer l’application (p.ex. si il y a beaucoup
d'images à charger depuis une connexion lente). Il s’agit là d’un chargement asynchrone qui
pose le problème de savoir quand l’image est effectivement chargée. Pour cela, chaque
méthode utilisant une image prend en paramètre un ImageObserver qui est averti lorsque
l’image est chargée (ou en partie chargée) via sa méthode imageUpdate. Toute classe qui
hérite de Component implémente ImageObserver. Le comportement est alors de réafficher
le composant lorsque l’image est chargée (méthode repaint).
Ce mode de chargement asynchrone des images explique que toutes les méthodes utilisant des
images demandent un ImageObserver en paramètre.
Il est possible de forcer le chargement des images pour éviter le chargement sur demande.
Pour cela, on utilise le MediaTracker qui permet d’attendre jusqu’à ce qu’une ou plusieurs
images appartenant à un Component soient chargées :
MediaTracker mt = new MediaTracker(unComponent); // p.ex. this
mt.addImage(img,0);
try {
mt.waitForID(0);
} catch (InterruptedException e) {
}
Une
autre
méthode
existe pour charger une image.
javax.swing.ImageIcon qui utilise le MediaTracker :
Il
s'agit
de
la
classe
Image img = (new javax.swing.ImageIcon("nom_du_fichier.ext")).getImage();
3
Affichage d’une image
L’affichage d’une image est une chose relativement simple à comprendre lorsque l’on sait
comment les images sont chargées.
Tout Component peut afficher une image au moyen d’un objet Graphics, donné en
paramètre de la méthode paint du composant. La méthode à utiliser est drawImage (x0 et y0
sont les coordonnées d'origine de l'image sur le Graphics) :
2
public void paint(Graphics g) {
g.drawImage(uneImage, x0, y0, unImageObserver);
}
Afin de réafficher le composant une fois que l’image a été chargée, il est nécessaire de choisir
l’ImageObserver à fournir en paramètre, le plus simple étant de donner le Component luimême (this).
Bien que tout composant puisse afficher une image, il est conseillé pour commencer de
choisir une Applet ou une Frame.
4
Accès aux pixels d’une image (Image -> int[])
L’accès aux valeurs des pixels d’une image consiste est réalisé par un ImageConsumer
particulier : le PixelGrabber.
Les pixels de l’image sont extraits dans un tableau d’entiers à une dimension et contenant le
autant de cases que de pixels dans l’image (stockage par ligne) :
int[] pixels = new int[width * height];
Note: un int[] est un objet (au sens de Java). Par conséquent, l’affectation "int[] a = b;" ne
copie pas le tableau b dans le tableau a (deep copy), mais effectue une copie de référence
(shallow copy) : les deux tableaux pointent donc sur les même données. Donc, lorsqu’on
modifie l’un, ça modifie l’autre puisqu’il s’agit du même tableau. Pour effectuer une deep
copy, utiliser System.arraycopy().
Chaque pixel est code sur un int de 32 bits dont la représentation est la suivante1 :
31............. 24 23 ............ 16 15 .............. 8 7 .................0
alpha
red
green
blue
La valeur alpha indique le niveau de transparence du pixel (0=transparent, 255=opaque). Les
valeurs red, green et blue indiquent le niveau des couleurs de base (0=0% de couleur de base,
255=100% de couleur de base). Les pixels sont acquis de la manière suivante :
PixelGrabber pg = new PixelGrabber(img, 0, 0, width, height, pixels, 0, width);
try {
pg.grabPixels();
} catch (InterruptedException e) {
System.err.println("interrupted waiting for pixels!");
}
Il est possible d'utiliser les opération de manipulation de bits sur chaque pixel (<< = shift à
gauche, >> = shift à droite, & = and, | = or).
5
Création d’une image (int[] -> Image)
Java ne permet pas d'afficher directement une image sous forme de tableau d'entiers mais
uniquement les objets de la classe Image. Pour cela, il faut convertir le tableau d'entier en
1
Dans le cours d’imagerie, cela correspond à la description du § I.3.4.4 avec N=24 et K=32. Dans Windows XP,
c’est en général aussi comme cela que les couleurs sont codées (Propriétés d’affichage/Paramètres/Qualité
couleur : optimale (32 bits) ).
3
Image puis afficher cette Image. La création d’une image se fait sur le même principe que le
chargement de l’image depuis le disque puisqu’il s’agit aussi de créer une image. La
différence est que la source de donnée est un int[] au lieu d’un fichier :
int[]
ImageProducer
ImageConsumer
Image
L’ImageProducer à utiliser est java.awt.image.MemoryImageSource et l’ImageConsumer
est par exemple le Toolkit utilisé pour charger l’image précédemment. Le code est donc le
suivant (MemoryImageSource est dans java.awt.image) :
Toolkit tk = Toolkit.getDefaultToolkit();
Image img = tk.createImage(new MemoryImageSource(width,height,pixels,0,width));
Note: le MemoryImageSource n’a besoin d’être créé que lorsque la taille de l’image change,
pas lorsque le contenu des pixels change. C’est normal puisque pixels est un int[], donc un
objet, il est passé par référence et non par valeur lors de la création du MemoryImageSource.
6
Comment sauver une image au format JPEG ou PNG
Pour sauver une Image, vous devez la convertir en BufferedImage, puis la sauver avec
ImageIO. Pour simplifier, vous pouvez utiliser la méthode suivante :
import
import
import
import
javax.imageio.*;
java.io.*;
javax.swing.*;
java.awt.image.*;
/**
Enregistre l'image sur le disque. Le format est défini par
l'extension du fichier. Un message d'erreur est affiché si
le format est inconnu.
@param image_name nom de fichier à écrire. Doit se terminer par .JPEG ou .PNG
@param img l’image à sauvegarder
*/
public void save(String image_name, Image img) {
BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = bi.createGraphics();
g.drawImage(img, 0, 0, width, height, null);
String file_format = image_name.substring(image_name.lastIndexOf('.')+1);
try {
boolean success = ImageIO.write(bi, file_format, new File(image_name));
if (!success) {
JOptionPane.showMessageDialog(new JFrame(), "Ecriture impossible:"+file_format);
} catch (Exception e) {
e.printStackTrace();
}//end try
}//end save
7
Paint & Repaint
Deux méthodes importantes de Component sont paint(Graphics g) et repaint().
Elles sont utilisées pour réafficher le contenu d’un Component. La méthode repaint() efface le
Graphics du composant et demande ensuite à la machine virtuelle Java de réafficher le
composant via la méthode paint(Graphics g): il s’agit donc d’un comportement asynchrone.
Pour forcer le réaffichage immédiat, on peut utiliser comp.paint(comp.getGraphics()), qui
évite l’effacement du Graphics et le réaffiche sans délai. Cette technique n’est normalement
nécessaire que lorsque vous avez un algorithme très gourmand en temps de calcul et que la
machine virtuelle n’a plus le temps de faire le réaffichage.
4
Téléchargement