Aller au contenu principal

Exercices

⭐ Ne garder que le vert

Indice

Mettre le rouge et le bleu à 0.

⭐ Échanger les canaux

Par exemple, échangez le canal rouge et le canal bleu.

Indice

Il existe std::swap(a, b) pour échanger deux valeurs.

⭐ Noir & Blanc

Indice

N'hésitez pas à aller chercher la formule sur internet si besoin !

⭐ Négatif

Indice

Il faut trouver une formule simple, qui transforme le noir en blanc et le blanc en noir (i.e. qui transforme 0 en 1 et 1 en 0).

⭐ Dégradé

info

Vous pouvez créer une image noire avec

sil::Image image{300/*width*/, 200/*height*/};

puis itérer sur les pixels pour les colorer.

Indice

La couleur de chaque pixel doit dépendre de son x uniquement.

Indice 2

Quelle formule permettrait d'avoir 0 (noir) quand x vaut 0 (gauche), et 1 (blanc) quand x vaut image.width() - 1 (droite) ?

⭐⭐ Miroir

Indice

On cherche une formule qui, quand x vaut 0, le transforme en image.width() - 1, quand x vaut 1, le transforme en image.width() - 2, etc.

⭐⭐ Image bruitée

info

Pour obtenir des nombres aléatoires, vous pouvez soit utiliser srand et rand comme vu en TP, soit utiliser les fonctions qu'on vous a fournies dans #include "random.hpp". Elles ont une syntaxe plus simple à utiliser, et utilisent des générateurs aléatoires de meilleure qualité. Vous avez soit random_int(min, max), soit random_float(min, max), soit true_with_probability(probability_of_beeing_true). Si vous voulez obtenir la même suite de nombres aléatoires à chaque fois que vous relancez votre programme, vous pouvez définir une seed avec set_random_seed(0) au début de votre main() (vous pouvez passer n'importe quel nombre autre que 0, ça définira quels seront les nombres générés par les fonctions random).

Indice

Remplacez quelques pixels au hasard par une couleur aléatoire.

⭐⭐ Rotation de 90°

La formule générique pour un angle quelconque est un peu plus compliquée, mais pour 90° il y a une formule plus simple, essayez de la trouver !

Indice

Créez une nouvelle image avec sil::Image new_image{new_width, new_height}; pour stocker le résultat de votre effet, car elle n'aura pas la même taille que l'image originale.

⭐⭐ RGB split

⚠️ Piège à éviter

Créez une nouvelle image et travaillez sur celle-ci. Il ne faut pas modifier l'image originale pendant que vous bouclez pour appliquer l'effet, sinon certains pixels n'utiliseront pas la bonne couleur de l'image originale, mais plutôt la couleur déjà modifiée par un pixel précédent, ce qui pourrait vous donner ce genre de rendu moins intéressant :

Indice

Chaque pixel va prendre comme couleur le rouge d'un pixel un peu à sa droite, son propre vert, et le bleu d'un pixel un peu à sa gauche.

⭐⭐ Luminosité

Image originaleAprès éclaircissementAprès assombrissement
Indice

On voudrait des courbes comme celles-ci, qui diminuent tous les nombres entre 0 et 1 (courbe bleue) (c-à-d assombrisse), ou les augmentent (courbe verte) (c-à-d éclaircisse), tout en gardant 0 à 0 et 1 à 1 (afin de garder la plage dynamique de l'image, pour conserver des noirs purs et des blancs purs).

Indice 2

Les fonctions puissance font exactement ce qu'on veut !
La preuve : https://www.desmos.com/calculator/c3ztk51mng

⭐⭐(⭐) Disque

info

Vous pouvez créer une image noire avec

sil::Image image{500/*width*/, 500/*height*/};

puis itérer sur les pixels pour les colorer.

Indice

Quelle est l'équation d'un disque ? Comment peut on s'en servir pour savoir si un pixel est à l'intérieur ou à l'extérieur du disque ?

⭐ Cercle

En reprenant et modifiant légèrement votre code pour le disque, écrivez le code qui donne un cercle. (Son contour aura une épaisseur donnée thickness).

⭐⭐⭐ Rosace

Maintenant que vous savez dessiner un cercle, dessinez-en plusieurs sur la même image, à des positions bien choisies, de sorte à dessiner une rosace. (PS : il va sûrement falloir faire de la trigo!)

Indice

Comment passer de coordonnées polaires (angle et rayon) à des coordonnées cartésiennes (x et y) ?

⭐⭐ Mosaïque

Indice

Une manière concise de faire ça est d'utiliser un modulo (%) quelque part.

⭐⭐⭐⭐ Mosaïque miroir

Inversez une image sur deux :

⭐⭐⭐ Glitch

info

Pour obtenir des nombres aléatoires, vous pouvez soit utiliser srand et rand comme vu en TP, soit utiliser les fonctions qu'on vous a fournies dans #include "random.hpp". Elles ont une syntaxe plus simple à utiliser, et utilisent des générateurs aléatoires de meilleure qualité. Vous avez soit random_int(min, max), soit random_float(min, max), soit true_with_probability(probability_of_beeing_true). Si vous voulez obtenir la même suite de nombres aléatoires à chaque fois que vous relancez votre programme, vous pouvez définir une seed avec set_random_seed(0) au début de votre main() (vous pouvez passer n'importe quel nombre autre que 0, ça définira quels seront les nombres générés par les fonctions random).

Indice

Prendre un rectangle de pixels et l'intervertir avec un autre rectangle de pixels, ailleurs dans l'image. Faire ça plusieurs fois.

⭐⭐⭐ Tri de pixels

Voici une bonne vidéo expliquant l'algorithme (vous pouvez ignorer le début spécifique à Processing où on voit comment afficher l'image) :


Utilisez image.pixels() pour récupérer le tableau contenant tous les pixels de l'image et le trier.

Ensuite, au lieu de trier tous les pixels de l'image, triez par colonne (ou par ligne) pour un effet plus joli. (Il faudra trier des sous-parties du tableau image.pixels().) Ou encore, triez des sous-parties prises aléatoirement dans l'image.

info

Pour trier un tableau, vous pouvez utiliser std::sort. Vous verrez ça plus en détail au S2, mais voici une brève explication :

std::vector<int> v{8, 5, 3, 1};
std::sort(v.begin(), v.end()); // Trie un tableau du début à la fin
// {1, 3, 5, 8}
std::vector<int> v{8, 5, 3, 1};
std::sort(v.begin(), v.begin() + 2); // Trie les deux premiers éléments du tableau, c'est comme si on lui avait passé le tableau {8, 5}
// {5, 8, 3, 1}
std::vector<int> v{8, 5, 3, 1};
std::sort(v.begin() + 1, v.begin() + 3); // Trie le sous-tableau {5, 3}
// {8, 3, 5, 1}

Et vous pouvez aussi choisir selon quel critère de comparaison les éléments seront triés :

std::vector<glm::vec3> v{/*...*/};
std::sort(v.begin(), v.end(), [](glm::vec3 const& color1, glm::vec3 const& color2)
{
return color1.r < color2.r; // Trie selon la composante rouge
});
std::vector<glm::vec3> v{/*...*/};
std::sort(v.begin(), v.end(), [](glm::vec3 const& color1, glm::vec3 const& color2)
{
return brightness(color1) < brightness(color2); // Trie selon la luminosité des couleurs (NB : c'est à vous de coder la fonction `brightness`)
});

On passe ce qu'on appelle une lambda en 3ème argument : c'est une fonction définie en plein milieu du code. Elle doit prendre en paramètre deux éléments du tableau (deux vec3 en l'occurrence) et retourner un booléen indiquant qui est le plus petit des deux éléments.

info

Pour obtenir des nombres aléatoires, vous pouvez soit utiliser srand et rand comme vu en TP, soit utiliser les fonctions qu'on vous a fournies dans #include "random.hpp". Elles ont une syntaxe plus simple à utiliser, et utilisent des générateurs aléatoires de meilleure qualité. Vous avez soit random_int(min, max), soit random_float(min, max), soit true_with_probability(probability_of_beeing_true). Si vous voulez obtenir la même suite de nombres aléatoires à chaque fois que vous relancez votre programme, vous pouvez définir une seed avec set_random_seed(0) au début de votre main() (vous pouvez passer n'importe quel nombre autre que 0, ça définira quels seront les nombres générés par les fonctions random).

⭐⭐⭐(⭐) Fractale de Mandelbrot

La fractale de Mandelbrot s'obtient ainsi : pour chaque nombre complexe c (correspondant à la position d'un pixel), on initialise un autre nombre complexe z à 0, puis on itère z = z * z + c un certain nombre de fois. Si le nombre z commence à devenir de plus en plus grand, alors c ne fait pas partie de la fractale et on colorie le pixel correspondant en noir. À l'inverse, si z reste de taille modérée peu importe le nombre d'itérations qu'on fait, alors le pixel fait partie de la fractale et on le colorie en blanc.

Plus précisément, on peut prouver que dès que std::abs(z) > 2 alors le nombre z va forcément finir par grandir de plus en plus. On peut donc s'arrêter d'itérer dès que std::abs(z) > 2. Et pour obtenir une fractale plus jolie, plutôt que d'assigner du noir pur on peut assigner un gris plus ou moins sombre en fonction du nombre d'itérations qu'il a fallu faire avant que std::abs(z) > 2.

Conseil : si vous mappez directement le pixel (x, y) au nombre complexe x + i * y, vous allez visualiser les nombres complexes entre 0 et 500, et votre fractale va être beaucoup trop petite. Les nombres intéressants sont plutôt entre -2 et 2. Il va donc falloir appliquer une petite transformation à votre x et y pour les faire rentrer dans cet intervalle.

Conseil : vous pouvez inclure le header <complex> pour utiliser des nombres complexes. Un nombre complexe se définit comme ça :

#include <complex>
int main()
{
std::complex<float> z{3.f, 2.f}; // Définis le nombre z = 3 + 2*i
}

et s'utilise comme un nombre normal : vous pouvez faire des additions, multiplications etc.

info

Vous pouvez créer une image noire avec

sil::Image image{500/*width*/, 500/*height*/};

puis itérer sur les pixels pour les colorer.

⭐⭐⭐(⭐) Tramage

Vous pouvez lire ce super article sur le tramage (a.k.a. dithering en anglais).

Sur l'image ci-dessus j'ai utilisé de l'ordered dithering avec une matrice de Bayer 4x4 (a.k.a. de niveau 1). Tout est expliqué dans l'article ci-dessus ! (Plus précisément, j'ai repris la matrice et le code depuis cet autre article.)

⭐⭐⭐(⭐) Normalisation de l'histogramme

AvantAprès

L'algorithme consiste à trouver le pixel le moins lumineux et le pixel le plus lumineux de l'image, puis à appliquer une transformation à chaque pixel de sorte à ce que le pixel le plus sombre devienne un noir pur (0) et le plus lumineux devienne un blanc pur (1).
(PS : testez avec l'image "images/photo_faible_contraste.jpg", vous verrez bien l'intérêt de l'effet.)

⭐⭐⭐⭐ Vortex

info

Pour appliquer une rotation à un point point, autour d'un autre point center_of_rotation, d'un angle angle (exprimé en radians) vous pouvez utiliser

#include <glm/gtx/matrix_transform_2d.hpp>

glm::vec2 rotated(glm::vec2 point, glm::vec2 center_of_rotation, float angle)
{
return glm::vec2{glm::rotate(glm::mat3{1.f}, angle) * glm::vec3{point - center_of_rotation, 0.f}} + center_of_rotation;
}
info

Pour obtenir la distance entre deux points, vous pouvez utiliser glm::distance(p1, p2);

Details

Indice Chaque pixel subit une rotation, de plus en plus importante au fur et à mesure qu'on s'éloigne du centre.

⭐⭐⭐⭐ Convolutions


Box blur

Les convolutions permettent d'implémenter plein d'effets (flou, détection de contour, augmentation de la netteté, etc.). L'effet va varier en fonction du kernel que vous utilisez lors de votre convolution. Tout est expliqué dans cette excellente vidéo, entre 1m10 et 4m18 :


Conseil : une fois que vous savez que votre algo marche, si vous voulez tester avec des kernels plus gros, ça peut être lent, donc vous avez intérêt à build en release (demandez-moi si vous ne savez pas comment faire).

⭐ Netteté, Contours, etc.

EmbossOutlineSharpen

Une fois que vous avez implémenté l'algo générique de convolution qui prend n'importe quel kernel, vous pourrez trouver sur ce site une liste de kernels pour faire différents effets.

⭐⭐ Filtres séparables

Box blur naïf, 100x100Box blur séparé, 100x100
7.44 secondes0.18 secondes

Quand vous voulez faire un gros flou il faut augmenter la taille du kernel, ce qui peut considérablement ralentir l'algorithme. Heureusement, certains kernels ont une propriété qui nous permet de calculer leur convolution BEAUCOUP plus rapidement. Le box blur et le gaussian blur sont de tels kernels. Voici une vidéo expliquant tout ça :

⭐⭐ Différence de gaussiennes

Voici une vidéo expliquant l'algorithme :

  • NB 1 : Il parle de flou gaussien, mais vous pouvez tout aussi bien réutiliser votre box blur, pas la peine d'implémenter un flou gaussien.
  • NB 2 : Ne faire que l'algo de base, présenté jusqu'à 4m09. Après ça ça devient très compliqué (mais très stylé néanmoins).

⭐⭐⭐⭐⭐ K-means : trouver les couleurs les plus présentes dans une image

Originale2 couleurs3 couleurs16 couleurs

Trouvez les k couleurs les plus représentatives de l'image, puis assignez à chaque pixel la couleur dont il est le plus proche parmi les k.

Voici une bonne vidéo expliquant l'algorithme :


info

Vous pouvez utiliser glm::distance(color1, color2) pour obtenir la distance entre deux couleurs.

info

Pour obtenir des nombres aléatoires, vous pouvez soit utiliser srand et rand comme vu en TP, soit utiliser les fonctions qu'on vous a fournies dans #include "random.hpp". Elles ont une syntaxe plus simple à utiliser, et utilisent des générateurs aléatoires de meilleure qualité. Vous avez soit random_int(min, max), soit random_float(min, max), soit true_with_probability(probability_of_beeing_true). Si vous voulez obtenir la même suite de nombres aléatoires à chaque fois que vous relancez votre programme, vous pouvez définir une seed avec set_random_seed(0) au début de votre main() (vous pouvez passer n'importe quel nombre autre que 0, ça définira quels seront les nombres générés par les fonctions random).

⭐⭐⭐⭐⭐ Filtre de Kuwahara (effet peinture à l'huile)

Voici une vidéo expliquant l'algorithme :
(La version simple de l'algo, qui est expliquée entre 3m11 et 3m30, suffit largement. (Mais si vous voulez aller plus loin, vous êtes les bienvenu.es bien sûr 😉))


info

Vous pouvez utiliser glm::distance(color1, color2) pour obtenir la distance entre deux couleurs.

⭐⭐⭐⭐⭐⭐ Diamond Square

Algorithme de génération de height map, qui peut ensuite être utilisée pour créer des terrains procéduraux dans des jeux vidéos par exemple.

Voici une bonne vidéo expliquant l'algorithme :


Conseil : commencez par travailler sur une image toute petite (e.g. 17x17), afin de bien voir les pixels et ce qu'il se passe.

info

Vous pouvez créer une image noire avec

sil::Image image{17/*width*/, 17/*height*/};

puis itérer sur les pixels pour les colorer.

⭐⭐ Colorer la height map

Appliquez un dégradé de couleur en fonction du niveau de gris de la height map.

info

Avec glm::mix(color1, color2, pourcentage); vous pouvez faire un mélange entre deux couleurs données.

Indice

Essayez par exemple de faire un dégradé d'un bleu sombre à un bleu clair quand le niveau de gris est entre 0 et 0.5, et un autre dégradé entre du vert et du marron quand le niveau de gris est entre 0.5 et 1.

Vous pouvez aussi implémenter vos propres effets !