Série 3 – Images continues génériques : corrigé
Introduction
Le code du corrigé est disponible sous la forme d'une archive Zip, qui contient également le code de l'énoncé. Les solutions aux différents exercices sont brièvement discutées ci-dessous.
Exercice 1
Comme dit dans l'énoncé, une fois l'interface ImageRGB
transformée en l'interface générique Image<T>
, le gros du travail consiste à trouver toutes les occurrences de ce qui était le type ImageRGB
et les remplacer par le type Image<ColorRGB>
, totalement équivalent.
Cela dit, il est possible de faire mieux avec les transformateurs. En effet, ceux-ci sont purement géométriques dans le sens où il ne font que transformer les coordonnées du point qu'ils reçoivent pour les passer à la méthode valueAt
de l'image qu'ils transforment. Une fois cela fait, ils retournent directement la valeur retournée par cette méthode valueAt
, sans se soucier de son type. Dès lors, les transformateurs peuvent être rendus génériques, afin de pouvoir être appliqués non seulement des images en couleur mais aussi d'autres types d'images, comme les masques définis à l'exercice suivant.
Par exemple, la classe RotationTransformer
peut être modifiée comme illustré ci-dessous :
public final class RotationTransformer<T> implements Image<T> { private final Image<T> imageToRotate; private final double angle; public RotationTransformer(Image<T> imageToRotate, double angle) { this.imageToRotate = imageToRotate; this.angle = angle; } @Override public T valueAt(double x, double y) { double rX = x * Math.cos(-angle) - y * Math.sin(-angle); double rY = x * Math.sin(-angle) + y * Math.cos(-angle); return imageToRotate.valueAt(rX, rY); } }
Etant donné qu'il est générique, ce transformateur peut non seulement être utilisé pour tourner une image couleur, auquel cas son paramètre de type T
sera instancié avec ColorRGB
, mais aussi pour tourner un masque, auquel cas son paramètre de type T
sera instancié avec Double
.
Exercice 2
La classe LinearHorizontalGradientMask
est une simplification de la classe LinearHorizontalGradientGenerator
, dont la méthode valueAt
produit la proportion du mélange des couleurs au lieu de mélanger effectivement deux couleurs. Le passage de la seconde à la première est donc très simple, il suffit d'enlever les deux couleurs et de retourner la valeur de la proportion, p
dans le code de notre corrigé. On obtient alors :
public final class LinearHorizontalGradientMask implements Image<Double> { private final double leftX, rightX; public LinearHorizontalGradientMask(double leftX, double rightX) { if (! (leftX < rightX)) throw new IllegalArgumentException(/*…*/); this.leftX = leftX; this.rightX = rightX; } @Override public Double valueAt(double x, double y) { double clampedX = Math.max(leftX, Math.min(x, rightX)); return (clampedX - leftX) / (rightX - leftX); } }
A noter que la méthode valueAt
utilise ici une technique légèrement différente que celle utilisée dans le corrigé de la semaine dernière pour calculer p
. Plutôt que d'utiliser un if
pour déterminer si la coordonnée x
se trouve entre leftX
et rightX
, on la ramène dans cet intervalle au moyen des méthode min
et max
.
Exercice 3
La classe de composition n'est pas beaucoup plus difficile à définir qu'une classe transformatrice. La différence est qu'elle ne transforme pas une image, mais bien trois : deux images couleur et un masque.
Etant donné que le résultat d'une composition de deux images couleur au moyen d'un masque est une image couleur, la classe ImageCompositor
doit implémenter l'interface Image<ColorRGB>
. Dès lors, sa méthode valuetAt
retourne une couleur, calculée simplement en mélangeant la couleur de l'image d'arrière-plan avec la proportion de celle d'avant-plan donnée par le masque. Bien entendu, la classe de composition n'applique aucune transformation géométrique ni aux images qu'elle compose ni au masque, et elle passe donc les coordonnées qu'elle reçoit à leur méthode valueAt
sans les transformer.
Le code de cette classe de composition ressemble donc à ceci :
public final class ImageCompositor implements Image<ColorRGB> { private final Image<ColorRGB> background, foreground; private final Image<Double> mask; public ImageCompositor(Image<ColorRGB> background, Image<ColorRGB> foreground, Image<Double> mask) { this.background = background; this.foreground = foreground; this.mask = mask; } @Override public ColorRGB valueAt(double x, double y) { ColorRGB backgroundC = background.valueAt(x, y); ColorRGB foregroundC = foreground.valueAt(x, y); double amount = mask.valueAt(x, y); return backgroundC.mixWith(foregroundC, amount); } }
Une fois cette classe de composition définie, on peut l'utiliser avec la classe de rotation générique de l'exercice précédent pour obtenir la composition qui apparaît dans l'énoncé, de la manière suivante :
// Arrière-plan ColorRGB gray = new ColorRGB(0.5, 0.5, 0.5); Image<ColorRGB> bg = new ChessboardGenerator(ColorRGB.WHITE, gray, 0.2); // Avant-plan URL imageURL = new URL("http://cs108.epfl.ch/images/dubrovnik.jpg"); BufferedImage discreteImage = ImageIO.read(imageURL); Image<ColorRGB> fg = new DiscreteImageAdapter(ColorRGB.BLACK, discreteImage); // Masque tourné de 45° LinearHorizontalGradientMask mask = new LinearHorizontalGradientMask(-0.3, 0.3); Image<Double> rotatedMask = new RotationTransformer<>(mask, Math.toRadians(45)); // Image composite Image<ColorRGB> image = new ImageCompositor(bg, fg, rotatedMask);