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);