Utilisation de Java pour du traitement d`image
Transcription
Utilisation de Java pour du traitement d`image
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