Série 3 – Images continues génériques

Introduction

Le but de cette série est de continuer le mini-projet images continues commencé la semaine dernière, en utilisant la généricité pour généraliser la notion d'image. Votre point de départ sera donc soit votre propre version de la série 2, soit le code de notre corrigé.

Il peut être judicieux de commencer un nouveau projet Eclipse pour cette série plutôt que de modifier celui de la série précédente, afin de pouvoir au besoin passer de l'un à l'autre et voir les changements.

Exercice 1

Lors de la première partie de ce mini-projet, nous avions défini une image comme étant une fonction qui, étant donné un point dans le plan, retourne la couleur de ce point. En Java, nous avions exprimé cela au moyen de l'interface ImageRGB suivante :

public interface ImageRGB {
    public ColorRGB valueAt(double x, double y);
}

Le but de ce premier exercice est de généraliser ce concept d'image, en définissant une image comme une fonction qui, étant donné un point, retourne non plus une couleur mais une valeur d'un type arbitraire. L'intérêt de cette généralisation deviendra apparent dans les exercices suivants, le seul but de celui-ci étant de faire les modifications qui s'imposent dans le code.

La première chose à faire est de modifier l'interface ImageRGB afin de la généraliser comme expliqué ci-dessus. Pour exprimer le fait qu'une image est une fonction qui, étant donné un point, retourne une valeur d'un type quelconque, on utilise bien entendu la généricité ! La nouvelle version de l'interface ImageRGB, renommée1 au passage Image, se présente donc ainsi :

public interface Image<T> {
    public T valueAt(double x, double y);
}

Une fois cette modification effectuée, il faut bien entendu modifier tout le code existant qui utilisait ImageRGB pour tenir compte du fait que Image est maintenant générique. La manière simple de procéder est de trouver toutes les occurrences de l'interface Image sans paramètre de type et de passer le type ColorRGB en paramètre pour obtenir Image<ColorRGB>. On peut facilement trouver ces occurrences car Eclipse produit un avertissement (mais pas une erreur, pour des raisons qui seront expliquées ultérieurement dans le cours) à chacune d'entre-elles.

En remplaçant ainsi systématiquement les occurrences de ce qui était ImageRGB par Image<ColorRGB>, on obtient un programme totalement équivalent à celui avant la généralisation de l'interface. Même s'il n'est rigoureusement pas faux de faire la modification ainsi, on peut faire mieux dans certains cas.

En effet, en examinant le code de la méthode valueAt d'un transformateur comme ShrinkVerticallyByTwoTransformer, on constate que cette méthode fonctionne indépendamment du type retourné par la méthode valueAt de l'image transformée ! En réfléchissant un peu, on se rend compte que cela est dû au fait que les transformateurs sont purement géométriques, et ne manipulent absolument pas les couleurs. Dès lors, au lieu de leur faire implémenter l'interface Image<ColorRGB>, on peut les modifier pour les rendre eux-mêmes génériques. Ils sont ainsi plus généraux et applicables à d'autres images que les images couleur.

Pour résumer, la généralisation de la notion d'images se fait en trois étapes :

  1. L'interface ImageRGB est renommée en Image puis rendue générique, son paramètre de type représentant le type des valeurs retournées par valueAt.
  2. Tout le code qui utilisait ImageRGB et qui produit et/ou manipule effectivement des images couleur est adapté pour utiliser l'instanciation Image<ColorRGB> en lieu et place de l'ancien ImageRGB.
  3. Les transformateurs, qui sont purement géométriques et ne sont pas spécifiques aux images couleur, sont adaptés pour devenir eux-même génériques.

Les deux premières étapes sont mécaniques et ne devraient pas poser de problème. Si la troisième vous en pose, commencez par modifier les transformateurs exactement comme le reste du code, puis passez à l'exercice suivant. Vous reverrez les transformateurs plus tard.

Une fois les modifications effectuées, assurez-vous d'une part qu'Eclipse ne produit plus aucun avertissement, et d'autre part que le programme est toujours capable d'afficher une image de votre choix.

Exercice 2

Ayant généralisé la notion d'image, il devient possible de définir des images produisant autre chose que des couleurs. Bien entendu, de telles images ne sont pas directement affichables à l'écran, mais elles peuvent être très utiles néanmoins comme nous le verrons à l'exercice suivant.

La première image « non colorée » à définir est une simplification du dégradé linéaire de la série précédente. Cette nouvelle classe, LinearHorizontalGradientMask, est une image de valeurs réelles. C'est-à-dire qu'elle implémente l'interface Image<Double> et sa méthode valueAt retourne simplement la valeur de la proportion du mélange, appelée \(p\) la semaine dernière. Sous forme mathématique, cette « image » se présente ainsi :

\begin{displaymath} p(x, y) = \begin{cases} 0 & \text{si }x \le x_l\\ 1 & \text{si }x \ge x_r\\ (x - x_l)/(x_r - x_l) & \text{sinon} \end{cases} \end{displaymath}

Bien entendu, étant donné que cette image ne produit plus de couleur, le constructeur peut être simplifié et ne prendre que les deux limites du dégradé, \(x_l\) et \(x_r\).

Une telle image, qui associe une valeur réelle comprise entre 0 et 1 à chaque point du plan, est appelée un masque ou encore un canal alpha dans des programmes comme PhotoShop. Ces masques peuvent être utilisés pour mélanger deux images entre-elles, et le masque détermine alors, pour chaque point, la proportion du mélange des images.

Exercice 3

Une fois le masque dégradé linéaire défini, il vous est maintenant possible de définir une classe de composition de deux images au moyen d'un masque. Cette classe, nommée ImageCompositor et qui implémente l'interface Image<ColorRGB>, prend trois arguments : une image d'arrière-plan de type Image<ColorRGB>, une image d'avant-plan de même type et un masque de type Image<Double>. Sa méthode valueAt est définie de manière à ce que la couleur d'un point de l'image finale soit un mélange de la couleur de l'image d'arrière-plan avec la proportion donnée par le masque de la couleur de l'image d'avant-plan.

En termes mathématiques, et en réutilisant la notation du corrigé de la série précédente, cette classe de composition calcule la couleur de \(c\) de l'image finale ainsi, si \(b\) est l'image d'arrière-plan (background), \(f\) celle d'avant-plan (foreground) et \(m\) le masque :

\begin{displaymath} c(x, y) = b(x, y) \oplus_{m(x,y)} f(x,y) \end{displaymath}

Autrement dit, l'image finale est égale à l'arrière-plan partout où le masque vaut 0, égale à l'avant-plan partout où le masque vaut 1, et un mélange des deux lorsque le masque vaut quelque chose entre 0 et 1.

Une fois cette classe définie, on peut l'utiliser pour composer un arrière-plan, formé d'un échiquier de cellules blanches et grises de 0.2 unités de côté, avec un avant-plan, la photo de Dubrovnik fournie, le tout au moyen d'un masque dégradé linéaire allant de -0.3 à 0.3. On obtient le résultat ci-dessous, aux qualités artistiques discutables, mais néanmoins intéressant.

dubrovnik-masked.png

Figure 1 : Dubrovnik masquée

Les transformateurs étant génériques, il est possible de les appliquer aux masques comme aux images couleur. En appliquant le transformateur de rotation au masque pour le faire tourner de 45°, on obtient l'image ci-dessous.

dubrovnik-masked-rotated.png

Figure 2 : Dubrovnik masquée (v2)

Exercice 4

Si vous avez terminé les exercices ci-dessus et qu'il vous reste du temps, laissez libre cours à votre imagination et définissez de nouveaux générateurs, de nouveaux transformateurs, de nouveaux masques ou de nouveaux opérateurs de composition. Ce mini-projet est une excellente manière d'explorer la généricité dans un contexte peut-être plus ludique que celui des collections.

Notes de bas de page

1

A noter que, dans Eclipse, il est très simple de renommer l'interface ImageRGB en Image. Il suffit de placer le curseur sur le nom de cette interface, puis de choisir l'entrée Rename du menu Refactor et d'entrer le nouveau nom, ici Image. Cela fait, Eclipse trouve et renomme toutes les occurrences de ImageRGB en Image dans le projet, et renomme également le fichier de ImageRGB.java en Image.java.