Images en SWING

Généralités

La classe abstraite Image d'AWT est la super-classe de tous les objets qui représentent des images graphiques. Des classes spécifiques à chaque plate-forme, (sun.awt.motif.X11Image sous X), dérivent de cette classe. Les méthodes principales de cette classe sont les suivantes.

int getWidth(ImageObserver observer)
retourne la largeur en pixels de l'image.
int getHeight(ImageObserver observer)
retourne la hauteur en pixels de l'image.
ImageProducer getSource()
retourne le producteur d'image associé.

Il existe aussi une classe ImageIcon pour les icônes. L'utilisation de cette classe peut être détournée afin de charger facilement des images.

Lecture d'une image

Lecture asynchrone

La lecture d'une image se fait par les méthodes getImage du toolkit. Ces méthodes prennent en paramètre une chaîne de caractères pour un fichier local ou une URL pour un fichier distant. Les formats classiques d'images GIG, JPEG et PNG sont supportés.

  Image image = Toolkit.getDefaultToolkit().getImage("image.jpg");
  URL url = new URL("http://www.somewhere.fr/image.jpg");
  Image image = Toolkit.getDefaultToolkit().getImage(url);

L'appel à une méthode getImage ne provoque par le chargement immédiat de l'image. Celui-ci est différé jusqu'à l'affichage proprement dit de l'image par un appel à une méthode drawImage. Le chargement de l'image est effectué par un thread de manière asynchrone. De cette manière, le fonctionnement de l'application se poursuit pendant le chargement de l'image. Ce mécanisme permet une meilleure réactivité des applications.

Ce chargement asynchrone des images provoque parfois des surprises. Le morceau de code ci-dessous affiche la valeur -1 pour la hauteur (height) et la largeur (width) de l'image. Au moment des appels aux méthodes getHeight et getWidth, l'image n'est pas encore chargée et ses dimensions sont pas conséquent inconnues.

  Image image = Toolkit.getDefaultToolkit().getImage("image.jpg");
  System.out.println("Height = " + image.getHeight(null));
  System.out.println("Width = " + image.getWidth(null));

Lecture synchrone

Il est aussi possible d'attendre la fin du chargement d'une image. Ceci pénalise l'application qui reste bloquée pendant le chargement mais ceci simplifie la programmation. L'attente d'une image est tout à fait acceptable lorsque le fichier est local et de taille raisonnable.

L'attente du chargement d'une image se fait par l'intermédiaire d'un objet de classe MediaTracker. L'image est d'abord enregistrée auprès du MediaTracker par la méthode addImage(Image, int id). L'identificateur id associé à une image est en faite une priorité. Plusieurs images peuvent être associées à la même valeur. Les images associées à une valeur supérieure sont chargées en priorité. L'attente du chargement de l'image se fait par les méthodes waitForID(int) pour attendre toutes les images d'une priorité donnée ou waitForAll pour les attendre toutes. Un exemple typique de l'utilisation d'un MediaTracker est donné ci-dessous. Les méthodes waitForID et waitForAll lèvent une exception InterruptedException lorsque le chargement n'aboutit pas. Il faut donc encapsuler les appels à ces méthodes dans une structure try catch.

  image = Toolkit.getDefaultToolkit().getImage("image.jpg");
  // Création du MediaTracker et enregistrement de l'image
  MediaTracker tracker = new MediaTracker(this);
  tracker.addImage(image, 0);
  // Attente proprement dite
  try { tracker.waitForID(0); }
  catch(InterruptedException e) {}

Utilisation de ImageIcon

La classe ImageIcon est une implémentation de l'interface Icon par des images. Les constructeurs de cette classe utilisent un MediaTracker pour attendre que l'image soit chargée (car les icônes sont a priori de petites images). L'image peut être récupérée par la méthode getImage(). Une façon très simple d'attendre le chargement d'une image est la suivante.

  ImageIcon imageIcon = new ImageIcon("image.jpg");
  Image image = imageIcon.getImage();

Affichage d'une image

De manière générale, l'affichage d'une image se fait par une des variantes de la méthode drawImage des classes Graphics et Graphics2D des contextes graphiques.

L'appel à la méthode drawImage se fait généralement dans la redéfinition de la méthode paintComponent(Graphics g) en utilisant le contexte graphique reçu en paramètre comme dans le code ci-dessous.

  public void paintComponent(Graphics g) {
      super.paintComponent(g);
      g.drawImage(image, 0, 0, this);
  }

Les variantes de la méthode drawImage prennent toujours en paramètre l'image, les coordonnées de positionnement du coin haut et gauche de l'image ainsi qu'un objet implémentant l'interface ImageObserver. Certaines variantes prennent aussi une couleur de fond qui remplace les pixels transparents de l'image, ou des nouvelles dimensions pour l'image.

Suivi du chargement d'une image

Dans le cas où les images sont chargées de manière asynchrone, il est possible de suivre le chargement au fur et à mesure qu'il s'effectue. Les méthodes drawImage prennent en paramètre un objet implémentant l'interface ImageObserver. Cette interface déclare une unique méthode imageUpdate. L'objet ImageObserver en paramètre de drawImage voit sa méthode imageUpdate appelée à chaque progression du chargement de l'image.

Dans la pratique, on peut passer null en paramètre ou plutôt this. L'interface ImageObserver est en effet implémentée par la classe Component et par conséquent tous les composants graphiques ont une méthode imageUpdate. Par défaut, celle-ci force le dessin du composant lorsque l'image progresse.

Il est possible de redéfinir la méthode imageUpdate pour suivre le chargement de l'image. Cette-ci reçoit plusieurs paramètres dont le premier est l'image et le second infoflags est un entier qui indique (par certains bits) quelles informations sont maintenant disponibles. La signification des autres paramètres dépend de la valeur de infoflags.

  public boolean imageUpdate(Image image, int flags, int x, int y,
			     int width, int height) {
      System.out.println("imageUpdate() : x = " + x + ", y = " + y +
			 ", width = " + width + ", height = " + height);
      // Affichage de l'image lorsque l'image est totalement chargée
      if ((flags & ALLBITS) != 0)
	  repaint();
      return true;
  }

Images modifiables

Les objets de classe Image ne sont pas modifiables. Pour avoir une image modifiable, il faut utiliser un objet de classe BufferedImage qui étend la classe Image. La situation est semblable à celle des classes String et StringBuffer à la différence près que StringBuffer n'étend pas la classe String.

Le constructeur le plus simple de BufferedImage prend en paramètre les dimensions de l'image (hauteur et largeur) ainsi que le type de l'image. Le type de l'image détermine comment est codé chaque pixel de l'image. Les types utilisables sont définis par des constantes de la classe BufferedImage. Ils sont les suivants.

TYPE_INT_RGB
Chaque pixel est codé par un entier avec 8 bits pour chacune des composantes RGB (8 bits ne sont pas utilisés).
TYPE_INT_ARGB
Chaque pixel est codé par un entier avec 8 bits pour chacune des composantes αRGB.
TYPE_INT_ARGB_PRE
Chaque pixel est codé par un entier avec 8 bits pour chacune des composantes αRGB. Les composantes RGB sont déjà pré-multipliées par α.
TYPE_INT_BGR
Chaque pixel est codé par un entier avec 8 bits pour chacune des composantes RGB dans l'ordre B, G et R (modèle standard Windows et Solaris).
TYPE_3BYTE_BGR
Chaque pixel est codé par trois octets avec un octet pour chaque composante RGB.
TYPE_4BYTE_ABGR
Chaque pixel est codé par quatre octets avec un octet pour chaque composante αRGB
TYPE_4BYTE_ABGR_PRE
Chaque pixel est codé par quatre octets avec un octet pour chaque composante αRGB. Les composantes RGB sont déjà pré-multipliées par α.
TYPE_BYTE_GRAY
Chaque pixel est codé par un octet avec une seule composante.
TYPE_USHORT_GRAY
Chaque pixel est codé par un entier court avec une seule composante.
TYPE_BYTE_BINARY
Chaque pixel est codé par 1, 2 ou 4 bits. Ceci dépend du modèle de couleur (qui doit être de type IndexedColorModel) utilisé.
TYPE_BYTE_INDEXED
Chaque pixel est codé par un octet qui donne une entrée dans une table de couleur (avec un modèle de couleur de classe IndexedColorModel
TYPE_USHORT_565_RGB
Chaque pixel est codé par un entier court avec respectivement 5, 6 et 5 bits pour les composantes RGB (pas de composante α).
TYPE_USHORT_555_RGB
Chaque pixel est codé par un entier court avec 5 bits pour chaque composante RGB (pas de composante α).

Une image comprend automatiquement un modèle de couleur et un raster. Le modèle de couleur détermine le codage le la couleur de chaque pixel. Il fait référence à un espace de couleur qui détermine une façon de représenter les couleurs. Le raster encapsule le tableau de pixels de l'image. C'est par son intermédiaire qu'on accède en lecture et/ou en écriture à la couleur d'un pixel donné.

Modèles et espaces de couleurs

Un modèle de couleur détermine le codage de la couleur de chaque pixel. Ceci comprend l'espace mémoire alloué à chaque pixel et la répartition entre les différentes composantes. Cette notion est semblable à celle de Visual de X-Window. Certains modèles de couleur utilisent une table (comme X-Window) alors que d'autres stockent directement les composantes. Un modèle de couleur fait automatiquement référence à un espace de couleur qui est un système de représentation des couleurs.

Espaces de couleur

Un espace de couleur se matérialise en SWING par un objet de classe ColorSpace. Il spécifie un système de coordonnées appelées composantes pour représenter les couleurs. L'espace de couleur détermine le nombre de composantes et leurs significations. L'espace de couleur par défaut de SWING est le système sRGB qui est une variante du système RGB. Dans cet espace de couleur, chaque couleur est représentée par ses trois composantes RGB. Il existe d'autres systèmes classiques pour représenter les couleurs comme HSV (Hue, Saturation, Value), HLS, CMY (Cyan, Magenta, Yellow), CMYK (Cyan, Magenta, Yellow, blacK) et le système absolu CIExyz.

La notion de composante α pour la transparence est étrangère à un système de couleur. Elle a uniquement un sens d'un point de vue informatique. C'est le modèle de couleur qui détermine la présence d'une composante α.

La classe ColorSpace a des méthodes fromRGB, toRGB, fromCIEXYZ et toCIEXYZ permettant de convertir une couleur de l'espace courant dans les espaces sRGB standard de SWING et à l'espace absolu CIExyz.

Modèles de couleur

Un modèle de couleur se matérialise en SWING par un objet de classe ColorModel. Cette classe est abstraite et les modèles de couleurs se répartissent en trois catégories correspondant aux classes suivantes qui dérivent de ColorModel.

ComponentColorModel
Ce modèle est le seul à permettre d'utiliser un espace de couleur de type autre que RGB. Dans ce modèle, les composantes des couleurs sont stockées de manière indépendante. Le nombre de composantes dépend de l'espace de couleurs.
IndexColorModel
Dans ce modèle, chaque couleur est représentée par une entrée dans une table de couleurs représentées en sRGB. Ce modèle correspond au visual PseudoColor (non décomposé) de X-Window.
PackedColorMode
Dans ce modèle et la classe dérivée DirectColorModel, les composantes αRGB sont stockées dans un unique entier éventuellement court. Les bits utilisés par chaque composante sont déterminés par des masques.

La méthode getDataElements de la classe ColorModel permet la conversion d'une couleur du système standard sRGB dans le modèle courant. Cette méthode retourne un objet qui peut ensuite être transmis au raster pour spécifier la couleur d'un pixel. La méthode getRGB effectue la conversion inverse. Elle retourne une couleur dans le modèle standard sRGB.

Raster

Un objet de type Raster encapsule le tableau de pixels d'une image. Il fournit des méthodes pour accéder à la couleur d'un pixel. Par défaut un raster de classe Raster permet uniquement de lire mais pas de changer la couleur d'un pixel. Les raster permettant aussi de changer la couleur sont de classe WritableRaster. Seules les images BufferedImage ont un raster de ce type.

Un raster est composé d'un objet de classe DataBuffer qui contient dans un tableau les données brutes et d'un modèle d'échantillonnage de classe SampleModel qui interprète les données brutes.

Les méthodes permettant de lire ou changer la couleur d'un pixel sont les méthodes getPixel, setPixel, getDataElements et setDataElements. Les méthodes getPixel et setPixel utilisent des tableaux d'entiers ou flottants pour spécifier les composantes de la couleur. Le nombre de composantes et leurs significations dépendent du modèle de couleur. L'utilisation de ces méthode est uniquement possible si on connaît précisément le modèle de couleur de l'image manipulée. Sinon, il faut utiliser les méthodes getDataElements et setDataElements qui utilisent des objets convertis par le modèle de couleur. Le morceau de code suivant illustre l'utilisation de ces méthodes.

  // Image 200x100 de type ARGB
  Image image = new BufferedImage(200, 100, BufferedImage.TYPE_INT_ARGB);
  WritableRaster raster = image.getRaster();
  ColorModel model = image.getColorModel();
  // Couleur 
  Color color = new Color(…);
  // Coordonnées sRGB de cette couleur
  int argb = color.getRGB();
  // Conversion de cette couleur dans le modèle de couleur
  Object colorData = model.getDataElements(argb, null);
  // Pixel (x,y) de couleur color
  raster.setDataElements(x, y, colorData);

Transformations d'images

Swing autorise la manipulation d'images de manière assez aisée. Le mécanisme de traitement d'image est basé sur un principe de producteur/consommateur d'images.

Producteur et consommateur d'images

L'idée est que le producteur produit les pixels de l'image et les délivre au consommateur qui les utilise. Le consommateur commence par s'enregistrer auprès du producteur puis lui envoie une demande d'images. Le producteur répond en envoyant l'image par l'intermédiaire d'une méthode setPixels du consommateur. Le mécanisme est semblable à celui des événements avec les générateurs (producteurs) d'événements et les écouteurs (consommateurs d'événements).

Les producteurs d'images implémentent l'interface ImageProducer et les consommateurs implémentent l'interface ImageConsumer. La méthode getSource() de la classe Image retourne un objet de classe ImageProducer qui va pouvoir fournir cette image à des consommateurs. Les méthodes principales de ImageProducer sont les suivantes.

addConsumer(ImageConsumer)
pour enregistrer un consommateur
startProduction(ImageConsumer)
pour enregistrer un consommateur et demander l'envoi immédiat de l'image
requestTopDownLeftRightResend(ImageConsumer)
pour demander l'envoi de l'image

Les méthodes principales de ImageConsumer sont les suivantes.

imageComplete(int status)
méthode appelée par le producteur pour avertir que l'envoi de l'image est terminé
setColorModel(ColorModel)
donne le modèle de couleur de l'image
setDimensions(int width, int height)
donne les dimensions de l'image
setPixels
le producteur envoie les pixels en appelant cette méthode du consommateur

Filtres

En SWING, le traitement d'une image se fait par l'intermédiaire d'un filtre. Celui-ci se place entre le producteur et le consommateur et modifie l'image lors de son transfert. Un filtre se comporte simultanément comme un consommateur et un producteur. Le mécanisme est semblable à celui des flux InputStream et OutputStream des entrées/sorties Java. Les filtres sont des objets de classe ImageFilter. Cette classe implémente l'interface ImageConsumer. Le producteur est construit en encapsulant le filtre dans un objet de classe FilteredImageSource. Le morceau de code ci-dessous illustre l'utilisation typique de ces classes.

  // Image originale
  Image original = getImage("image.gif");
  // Filtre 
  ImageFilter filter = …
  // Producteur de l'image filtrée
  ImageProducer source = new FilteredImageSource(original.getSource(), filter);
  // Image résultat
  Image result = createImage(source);

Opérations sur les images modifiables

Pour manipuler des images modifiables de classe BufferedImage, on peut aussi appliquer des transformations. Celles-ci sont des objets de type BufferedImageOp qui possède une méthode filter. Cette méthode prend en paramètre une image source et retourne une image modifiée par l'opération.

La classe BufferedImageOp est en fait une interface qui est implémentée par des classes pour chacune des transformations classiques sur les images. Ces classes sont les suivantes.

AffineTransformOp
transformation affine (translation, symétrie, rotation, …). Cette classe utilise on objet de type AffineTransform. Cette classe possède des méthodes statiques getTranslateInstance, getScaleInstance, getRotateInstance et getShearInstance pour créer des transformations de base. Ces transformations peuvent ensuite être composées par la méthode concatenate.
ColorConvertOp
transformation des couleurs
ConvolveOp
convolution (application d'une moyenne pondérée des pixels adjacents)
LookupOp
manipulation des tables des couleurs
RescaleOp
intensités des couleurs

Un filtre peut être obtenu à partir d'une transformation en utilisant la classe BufferedImageFilter qui étend la classe ImageFilter. Le constructeur de cette classe prend en paramètre un objet de type BufferedImageOp à encapsuler.

Fabrication des filtres

La classe ImageFilter et en fait une classe abstraite qui doit être étendue pour créer des filtres. Il existe déjà plusieurs classes qui permettent d'implémenter les filtres classiques.

BufferedImageFilter
filtre à partir d'une transformation d'images
CropImageFilter
extraction d'une partie
ReplicateScaleFilter
copie et redimensionnement
RGBImageFilter
transformation des couleurs