8 Cours ENPC, printemps 1999
Java, objets, interfaces et Internet

Images

Le paquet java.awt.image définit des interfaces et des classes relatives aux images. La classe abstraite Image est cependant définie dans le paquet java.awt comme classe directement dérivée de Object.

Une image peut être obtenue par une des méthodes getImage(URL) pour une URL absolue, ou getImage(URL, String) pour une URL relative, de la classe Applet ; la classe URL figure dans le paquet java.net.Une invocation typique de cette méthode est :

	Image
	  image1 = getImage(getCodeBase(), "toto.gif");
	  image2 = getImage(getDocumentBase(), "toto.gif");
	

Les méthodes getCodeBase() et getDocumentBase() retournent respectivement l'URL de la classe de l'applette et l'URL de la page HTML contenant l'applette.

Une invocation de la méthode getImage termine immédiatement et ne provoque pas le chargement de l'image ; l'objet de type Image qu'elle retourne ne contient donc que des informations permettant d'obtenir effectivement l'image et non l'image elle-même. Le chargement de celle-ci est provoqué par d'autres méthodes (notamment, les demandes d'affichage), et réalisé dans un thread distinct. La surveillance du chargement est déléguée à un objet de classe MediaTracker.

Les objets de la classe MediaTracker servent à surveiller le chargement des images (et éventuellement d'autres catégories de données multimédias). Le constructeur de cette classe prend en argument la composante sur laquelle les images seront affichées (ce sera souvent this). Les images à charger sont confiées à un MediaTracker grâce à sa méthode addImage(Image, int) dont le deuxième argument permet de spécifier une priorité de chargement pour un groupe d'images. Le chargement est provoqué ici par la méthode waitForID(int) qui attend le chargement du groupe d'images de priorité donnée. La méthode checkID(int) est un test qui vérifie si les images d'un groupe de priorité ont été toutes chargées.

Enfin, l'affichage est provoqué par la méthode drawImage, de la classe Graphics (paramètre de la méthode paint de la composante), dont les trois premiers arguments sont l'image et les deux coordonnées du coin supérieur gauche de l'image dans la composante ; le quatrième argument peut être null.

public class ImageDemo extends Applet {
  Image image;
  MediaTracker tracker;

  public void init() {
    tracker = new MediaTracker(this);
    image = getImage(getCodeBase(), "toto.gif");
    tracker.addImage(image, 0);
    try {
      tracker.waitForID(0);
    }
    catch (InterruptedException e) {}
  }
  
  public void paint(Graphics g) {
    if (tracker.checkID(0)) {
      g.drawImage(image, 0, 0, this);
    }
  }
}

Ceci est un exemple typique de délégation d'une opération à un autre objet qui s'y consacre. D'autres exemples de ce mécanisme : un Container délègue à un LayoutManager le placement de ses composantes, une composante délègue à un récepteur d'événements le traitement de ces événements. Ici, une composante délègue à un MediaTracker le contrôle du chargement des images.

Double tampon

Cette technique, utile quand l'image est l'objet d'un calcul, est permise par la classe Image. Sans double tampon, si le calcul de l'image est fait dans la méthode paint(Graphics), quand le navigateur a besoin de réafficher sa page, il appelle Applet.repaint(), qui se résoud en Component.repaint(), qui appelle à son tour la méthode paint(Graphics) redéfinie dans l'applette, et le calcul est recommencé.

La bonne solution est de créer un objet i de type Image de la même dimension que l'applette qui garde en mémoire le résultat du calcul de l'image. Ce calcul est fait par une méthode calculer(Graphics) qui est appelée par init() avec en argument le contexte graphique de l'image i, obtenu par getGraphics(), et non avec celui de l'applette.

  public void init() {
    i = createImage(getSize().width, getSize().height);
    calculer(i.getGraphics());
  }

  void calculer(Graphics g) {
    ...
    g.drawLine( ... );
    ...
  }
Il ne reste plus à paint(Graphics) qu'à copier le contenu du contexte graphique de i sur le contexte graphique de l'applette :
  public void paint(Graphics g) {
    if( i != null ) g.drawImage(i, 0, 0, this);
  }
Si l'applette utilise un thread, qui est créé et démarré par init(), la méthode calculer(Graphics) doit être appelée à partir de run() :
  public void run() {
    ...
    calculer(i.getGraphics());
    repaint();
  }

Animations

Il est facile de réaliser une image animée en affichant une suite d'images. Le contrôle de cet affichage est confié à un thread temporisé.

public class Animation extends Applet 
  implements Runnable {
  Image[] images;
  MediaTracker tracker;
  Thread t;
  final int N = 5;
  int c;

  public void init() {
    images = new Image[N];
    tracker = new MediaTracker(this);

    for (int i = 0; i<N; i++) {
      images[i] = getImage(getCodeBase(), "images/" + i + ".gif");
      tracker.addImage(images[i], 0);
    }
    try {
      tracker.waitForID(0);
    }
    catch (InterruptedException e) {}
    t = new Thread(this);
    t.start();
  }

  public void paint(Graphics g) {
    g.drawImage(images[c], 0, 0, null);
  }

  public void run() {
    while (true) {
      try {
        c++;
        if (c == N) c = 0;
	repaint();
	Thread.sleep(300);
      }
      catch (InterruptedException e) {}
    }
  }
}

Un défaut du programme précédent est que le thread d'animation est démarré dans la méthode init(), au chargement de l'applette et qu'il reste vivant si la page web qui contient l'applette n'est plus affichée (il passe de l'état actif à l'état suspendu). On peut y remédier en créant le nouveau thread d'animation dans start() au lieu de init(), et en le terminant dans stop() :

  public void start() {
    t = new Thread(this);
    t.start();
  }

  public void stop() {
    t.stop();
  }

Cependant, l'invocation t.stop() est exécutée par le thread t et non par le thread de l'applette, lequel peut invoquer ensuite new Thread(this) sans que t ait été effectivement terminé. On peut donc avoir plusieurs threads d'animation simultanément actifs, tous opérant sur la même surface graphique. Une solution consiste à ce que la méthode stop() de l'applette attende la terminaison du thread d'animation avant de retourner :

  public void stop() {
    t.stop();
    try {
      t.join();
    }
    catch (InterruptedException e) {}
  }

Son

L'interface AudioClip est définie dans le package java.applet. Une classe implémentant cette interface doit définir les trois méthodes play(), loop() et stop(). Un clip audio est obtenu par une des méthodes getAudioClip(URL) pour une URL absolue, ou getAudioClip(URL, String) pour une URL relative, de la classe Applet. Il est toujours joué par un thread distinct.
import java.applet.AudioClip;
import java.net.URL;

public class A extends Applet
{
  AudioClip music;
  
  public void init() {
      music = getAudioClip(getCodeBase(), "bruit.au");
  }
  
  public void start() {
      music.loop();
  }
  
  public void stop() {
      music.stop();
  }
}

Chargement d'image dans une application

La façon de charger des images expliquée ci-dessus n'est valable que dans une applette. Il faut recourir à d'autres méthodes dans le cas d'une application :

  Image i = Toolkit.getDefaultToolkit().getImage("toto.gif");

Filtrage d'image

Le traitement d'images est réalisé en termes de production et de consommation d'images. La méthode getSource() de la classe Image retourne un objet de type ImageProducer. La production d'une image n'est pas réalisée par l'objet de type Image, mais est déléguée à un objet de type ImageProducer, qui est l'interface des objets capables de produire une image ; ces producteurs d'images sont nécessaires quand on veut transformer une image, à l'aide d'un filtre, c'est-à-dire d'un objet d'une sous-classe de ImageFilter.

La sous-classe CropImageFilter de ImageFilter permet d'extraire une zone rectangulaire d'une image (cropping). Voici un filtre associé au rectangle d'origine (10, 20), de largeur 100 et de hauteur 150 :

  ImageFilter filtre = new CropImageFilter(10, 20, 100, 150);

La sous-classe abstraite RGBImageFilter de ImageFilter doit être étendue en une classe définissant une méthode qui transforme la valeur de couleur rgb du pixel de coordonnées (x, y) :

  public int filterRGB(int x, int y, int rgb) { ... }

À partir d'un producteur d'image et d'un filtre, on obtient un nouveau producteur d'image ; l'image filtrée est ensuite obtenue par la méthode createImage() de la composante sur laquelle l'image sera affichée :

  Image image = ... ;
  Imageproducer source = image.getSource();
  ImageProducer ip = new FilteredImageSource(source, filtre);
  Image imageFiltree = createImage(ip);
    ...
  g.drawImage(imageFiltree, 5, 10, null);

Les objets de classe FilteredImageSource sont à la fois des producteurs et des consommateurs d'image.

Le modèle de couleur

Une couleur est représentée en Java par un objet de classe Color, selon le modèle RGB (Red, Green, Blue). Une couleur est déterminée par ses trois composantes "primaires", chacune étant quantifiée par un entier compris entre 0 et 255 (intensité maximale). Certaines couleurs usuelles sont des constantes de la classe Color par exemple Color.blue ou Color.orange, les autres peuvent être obtenues ainsi :
  Color
    presqueNoir = new Color(2, 0, 5),
    presqueBlanc = new Color(251, 252, 255);

Les couleurs sont implémentées par des entiers (32 bits), les bits de 31 à 24 étant nuls, ceux de 23 à 16 codant le rouge, de 15 à 8 le vert, et de 7 à 0 le bleu. On peut construire directement l'entier codant une couleur à l'aide de shifts et du ou logique : le presqueBlanc serait codé par

      251<<16 | 252<<8 | 255

Dans le cas d'une image, chaque pixel a une couleur, et les 8 bits de poids fort de la couleur, au lieu d'être nuls, servent à coder la transparence, ou alpha, du pixel (0 = transparence, 255 = opacité) : c'est le modèle alphaRGB. Ces quatre composantes peuvent être obtenues ainsi (par masquages) à partir d'un entier pixel codant la couleur, à l'aide du et logique :

int
  alpha = (pixel & 0xff000000) >> 24,
  red  = (pixel & 0xff0000) >> 16,
  green  = (pixel & 0xff00) >> 8,
  blue  = pixel & 0xff;
Pour transformer une couleur en niveau de gris, on affectera la moyenne des trois composantes de couleur à chacune de ces composantes (l'alpha n'est pas modifiée) :
int blackAndWhite(int alpha, int red, int green, int blue) {
  int average = (red + green + blue) / 3;

  red = green = blue = average;
  return
    alpha << 24 | red << 16 | green << 8 | blue;
}
Pour transformer une couleur en sa couleur négative, on la retranche de 255 (l'alpha n'est pas modifiée) :
int negative(int alpha, int red, int green, int blue) {
  
  red = 255 - red;
  green = 255 - green;
  blue = 255 - blue;
  return
    alpha << 24 | red << 16 | green << 8 | blue;
}
Voici enfin la classe NegativeFilter, qui utilise la méthode précédente :
class NegativeFilter extends RGBImageFilter {

  public NegativeFilter() {
    canFilterIndexColorModel = true;
  }

  public int filterRGB(int x, int y, int pixel) {
    int
      alpha = pixel & 0xff000000,
      red  = pixel & 0xff0000,
      green  = pixel & 0xff00,
      blue  = pixel & 0xff;
    return negative(alpha, red, green, blue);
  }

  int negative(int alpha, int red, int green, int blue) { ... }

L'affectation canFilterIndexColorModel = true permettra au filtre d'opérer sur chacune des couleurs au lieu d'opérer sur chacun des pixels, ce qui peut être une optimisation considérable.

Ce filtre s'utilise ainsi :

  Image image = ... ;
  ImageProducer ip =
    new FilteredImageSource(image.getSource(), new NegativeFilter());
  Image imageNegative = createImage(ip);
    ...
  g.drawImage(imageNegative, 5, 10, null);

Acquérir une zone d'image

La classe PixelGrabber permet de représenter une zone rectangulaire d'une image par un tableau d'entiers sur lequel des opérations plus générales de traitement d'image seront possibles :

  int[] pixels = new int[w * h];
  PixelGrabber pg = new PixelGrabber(img, x, y, w, h, pixels, 0, w);
  try {
    pg.grabPixels();
  }
  catch (InterruptedException e) {}

La zone rectangulaire d'origine (x, y), de largeur w et de hauteur h de l'image img sera stockée dans le tableau unidimensionnel pixels de taille w*h. Le pixel de coordonnées (i, j) (par rapport à l'origine du rectangle) sera l'entier pixels[j * w + i].

Inversement, la classe MemoryImageSource permet de transformer un tableau de pixels en image :

ImageProducer ip = new MemoryImageSource(w, h, pixels, 0, w);
Image i = createImage(ip);


[Cours Java] [Notes 7] [Notes 9]

Mise à jour :