Workshop créé par Jules Fouchy.
🎓 Étudiants : DE SANTIS Léo & DUPUIS Maxence.
On s'intéresse ici à la création d'effets sur des images avec C++ !
Il s'agit d'une première introduction à la synthèse d'image.
*📁 Cet émoji vous permet d'accéder directement au code source des projets.
On utilise la librairie sil.
- sil nous permet de lire, éditer (via les pixels) et sauvegarder des images.
#include <sil/sil.hpp> //Directive de préprocesseur pour inclure sil
sil::Image image{"mon_image.png"}; //Import d'une image
image.save("resultat.png"); //Sauvegarde et affichage de l'image
Avant | Après |
---|---|
- On souhaite simplement garder la composante Verte active.
- Il suffit de rendre nulles les composantes rouge et bleu de chaque pixel en les parcourant.
int main()
{
sil::Image image{"images/logo.png"};
for (glm::vec3 &color : image.pixels())
{
color.r = 0.f;
color.b = 0.f;
}
image.save("output/pouet.png");
}
Avant | Après |
---|---|
- On souhaite inverser les canaux RGB entre eux. Essayons d'inverser le canal bleu et rouge.
- Méthode 1 : Utiliser une variable temporaire.
- Méthode 2 : Utiliser la fonction swap de la bibliothèque standard pour échanger 2 valeurs.
//Méthode 1
int main()
{
sil::Image image{"images/logo.png"};
for (glm::vec3 &color : image.pixels())
{
float temp{color.b};
color.b = color.r;
color.r = temp;
}
image.save("output/pouet.png");
}
//Méthode 2
int main()
{
sil::Image image{"images/logo.png"};
for (glm::vec3 &color : image.pixels())
std::swap(color.b, color.r);
image.save("output/pouet.png");
}
❗Bien que ce code ne soit pas imposant. La méthode 2 est intéressante pour un code plus lisible et optimisé.
- Écraser une variable.
int main()
{
sil::Image image{"images/logo.png"};
for (glm::vec3 &color : image.pixels())
{
color.b = color.r;
color.r = color.b;
}
image.save("output/pouet.png");
}
Dans le code ci-dessus, on se retrouve avec un canal bleu ayant la même valeur que celle du canal rouge. On perd l'information sur color.b, d'où l'importance d'une variable temporaire temp.
Avant | Après |
---|---|
- On souhaite transformer notre image en noir et blanc.
- Il faut faire la moyenne de la somme des composantes RGB de chaque pixel et attribuer à chaque canal le résultat de ce calcul. Ce résultat se nomme la nuance de gris.
int main()
{
sil::Image image{"images/logo.png"};
for (glm::vec3 &color : image.pixels())
{
float moyenne{(color.r + color.g + color.b) / 3.0f};
color.r = moyenne;
color.g = moyenne;
color.b = moyenne;
}
image.save("output/pouet.png");
}
Avant | Après |
---|---|
- On souhaite inverser le noir et le blanc
- Analysons... On veut que :
0 ➡️ 1, 1 ➡️ 0, 0.8 ➡️ 0.2 ...
- En généralisant, on devine la formule : f(x) = 1 - x
- Il suffit donc d'appliquer cette formule aux composantes RGB de tous nos pixels !
- On souhaite parcourir toute notre largeur en passant progressivement du noir au blanc.
- On remarque que si on fixe un
x
quelconque, lesy
correspondant ne changent pas. On a donc des lignes verticales de même valeur. x
varie de 0 à width - 1 (largeur de l'image).- La variation de teinte doit donc prendre en compte la width (largeur) et la variable
x
. - On doit faire le rapport x / (width - 1) pour chaque pixel. En effet, ce rapport nous donne 1 si on arrive au dernier pixel et 0 au départ. L'incrément nous donnera une valeur de plus en plus blanche. BINGO ! 😜
int main()
{
sil::Image image{300, 200};
for (float x{0}; x < image.width(); x++)
{
for (float y{0}; y < image.height(); y++)
{
image.pixel(x, y).r = x / (image.width() - 1);
image.pixel(x, y).g = x / (image.width() - 1);
image.pixel(x, y).b = x / (image.width() - 1);
}
}
image.save("output/pouet.png");
}
- Remplacer le float par un int.
- Les valeurs prises par les composantes RGB sont des nombres décimaux variants de 0 à 1.
- Diviser un int par un int, ça donne... un int ! Et donc, nos valeurs seraient toutes arrondies à 0, sauf le rapport donnant tout juste 1 ! A savoir le dernier pixel (voir résultat ci-dessous).
❗Les bords gris ont été rajoutés pour bien discerner l'erreur.
Avant | Après |
---|---|
- On souhaite effectuer une rotation verticale de notre image.
- L'idée est de parcourir chaque pixel et d'échanger le pixel concerné par le pixel qui lui est opposé en x.
- Il faut cependant seulement parcourir la moitié de la largeur. En effet, arrivé à la moitié, notre image aura déjà été inversée.
int main()
{
sil::Image image{"images/anakin.jpg"};
for (int x{0}; x < image.width() / 2; x++)
{
for (int y{0}; y < image.height(); y++)
std::swap(image.pixel(x, y), image.pixel(image.width() - (x + 1), y));
}
image.save("output/pouet.png");
}
- Parcourir la totalité de la width. La conséquence, c'est d'avoir une image similaire à celle d'origine. En réalité, elle aura été inversée 2 fois.
Avant | Après |
---|---|
Dans cet exercice, un effet aléatoire de couleur (bruit) a été appliqué à l'image. Chaque pixel de l'image a été modifié en assignant une couleur aléatoire. Pour ce faire, les composantes R (rouge), G (vert) et B (bleu) de chaque pixel ont été remplacées par des valeurs aléatoires comprises entre 0 et 1.
- On utilise la fonction
random_int
pour trouver une position aléatoire sur notre image.random_float
nous permet de générer un float aléatoire entre 0 et 1 qui sera attribué aux différentes composantes RGB du pixel. - On génère
image.pixels().size()-1
pixels aléatoires !
#include "random.hpp"
int main()
{
sil::Image image{"images/logo.png"};
for (size_t i = 0; i < image.pixels().size(); i++)
{
int x_random = random_int(0, image.width());
int y_random = random_int(0, image.height());
image.pixel(x_random, y_random).r = random_float(0, 1.0f);
image.pixel(x_random, y_random).g = random_float(0, 1.0f);
image.pixel(x_random, y_random).b = random_float(0, 1.0f);
}
image.save("output/pouet.png");
}
Avant | Après |
---|---|
Dans cet exercice, une rotation de l'image originale à 90 degrés dans le sens horaire a été effectuée. L'algorithme utilise une approche de manipulation des pixels pour réaliser cette rotation.
- Parcourir chaque pixel de l'image d'origine et le placer dans une nouvelle image avec des coordonnées modifiées pour effectuer la rotation.
- L'implémentation de la rotation se fait en échangeant les coordonnées x et y des pixels entre l'image d'origine et la nouvelle image résultante.
- Le papier et le crayon sont toujours de bons outils !
int main()
{
sil::Image image{"images/logo.png"};
sil::Image voidImage{image.height(), image.width()};
for (int x{0}; x < image.width(); x++)
{
for (int y{0}; y < image.height(); y++)
voidImage.pixel(voidImage.width() - 1 - y, x) = image.pixel(x, y);
}
voidImage.save("output/pouet.png");
}
Avant | Après |
---|---|
Dans cet exercice, un effet de séparation des canaux RGB (RGB split) a été appliqué à l'image. L'algorithme modifie les canaux Rouge (R), Vert (G), et Bleu (B) de l'image pour créer une version où chaque canal est décalé par rapport aux autres.
- Trois boucles distinctes sont utilisées pour traiter séparément les composantes Rouge, Vert et Bleu de chaque pixel de l'image.
- Pour chaque composante de couleur, une boucle spécifique effectue un décalage des pixels à gauche ou à droite en fonction du canal (R, G ou B) tout en conservant les autres canaux.
- Ne pas oublier de décaler les valeurs de pixels.
- Modifier l'image d'origine. Les calculs seront faussés par les précédentes modifications effectués sur les pixels qui ont été réattribués à l'image d'origine.
Après Assombrissement | Avant | Après Eclaircissement |
---|---|---|
Dans cet exercice, un effet d'assombrissement ou d'éclaircissement de l'image a été appliqué en utilisant une variable number
. Cette variable est utilisée pour modifier la puissance des canaux Rouge (R), Vert (G) et Bleu (B) de chaque pixel de l'image.
- Une boucle parcourt chaque pixel de l'image et ajuste la valeur de chaque composante de couleur en fonction de la valeur de
number
. - La fonction
pow
est utilisée pour augmenter ou diminuer la valeur des canaux RVB en fonction de la valeur denumber
, ce qui permet de contrôler l'intensité lumineuse des pixels.
- Multiplier les valeurs sans les fonctions puissances. Cela nous donnerait un résultat trop saturé.
Image |
---|
Dans cet exercice, la formation d'un disque a été appliqué à une image de 500x500. L'algorithme remplit les pixels de l'image pour former un disque centré sur l'image.
- Les pixels situés à l'intérieur du cercle défini par l'équation sont colorés en blanc en vérifiant si sa position correspond à celle à l'intérieur du disque à l'aide d'une équation de cercle.
int main()
{
sil::Image image{500, 500};
int rayon{60};
for (int x{0}; x < image.width(); x++)
{
for (int y{0}; y < image.height(); y++)
{
if (pow(x - image.width() / 2, 2) + pow(y - image.height() / 2, 2) <= pow(rayon, 2))
{
image.pixel(x, y) = {1,
1,
1};
}
}
}
image.save("output/pouet.png");
}
Image |
---|
Dans cet exercice, la formation d'un cercle a été appliqué à une image de 500x500. L'algorithme dessine un cercle avec un rayon et une épaisseur de contours variable.
- Les pixels situés à l'intérieur du cercle sont laissés vides, tandis que ceux se trouvant dans l'épaisseur des contours sont colorés en blanc en déterminant s'ils se trouvent à l'intérieur du cercle ou dans l'épaisseur de ses contours à l'aide d'une équation de cercle modifiée.
int main()
{
sil::Image image{500, 500};
int rayon{60};
int thickness{2};
for (int x{0}; x < image.width(); x++)
{
for (int y{0}; y < image.height(); y++)
{
if (pow(x - image.width() / 2, 2) + pow(y - image.height() / 2, 2) >= pow(rayon, 2) && pow(x - image.width() / 2, 2) + pow(y - image.height() / 2, 2) <= pow(rayon + thickness, 2))
{
image.pixel(x, y) = {1,
1,
1};
}
}
}
image.save("output/pouet.png");
}
Image |
---|
Dans cet exercice, la formation d'une rosace a été appliqué à une image de 500x500. L'algorithme dessine une rosace en superposant plusieurs cercles avec des épaisseurs de contour variables.
- Chaque cercle est défini avec un rayon, une épaisseur de contour et une position de centre différents.
- Le centre défini des cercles est calculé en fonction du cercle trigonométrique par des coordonnées polaires.
- On implémente une fonction
createCircle
car on remarque que la tâche à effectuer est la même que pour le cercle avec des centres différents.
void createCircle(sil::Image &image, int &x, int &y, int ¢er_x, int ¢er_y, int &rayon, int &thickness)
{
if (pow(x - center_x, 2) + pow(y - center_y, 2) >= pow(rayon - thickness, 2) && pow(x - center_x, 2) + pow(y - center_y, 2) <= pow(rayon + thickness, 2))
{
image.pixel(x, y) = {1,
1,
1};
}
}
- L'utilité de la fonction nous permet d'entrer les nouvelles coordonnées des centres après le calcul de ce dernier via les formules de trigonométrie. On remarque 6 cercles positionnés tous les
$i\pi/3$ .$i$ allant donc de 1 à 6.
int main()
{
sil::Image image{500, 500};
int rayon{60};
int thickness{2};
int center_x{image.width() / 2};
int center_y{image.height() / 2};
for (int x{0}; x < image.width(); x++)
{
for (int y{0}; y < image.height(); y++)
{
createCircle(image, x, y, center_x, center_y, rayon, thickness);
for (int i{1}; i <= 6; i++)
{
int newCenter_x{static_cast<int>(center_x + rayon * static_cast<float>(cos(i * 3.14f / 3)))};
int newCenter_y{static_cast<int>(center_y + rayon * static_cast<float>(sin(i * 3.14f / 3)))};
createCircle(image, x, y, newCenter_x, newCenter_y, rayon, thickness);
}
}
}
image.save("output/pouet.png");
}
- Oublier d'ajouter un nouveau centre pour chaque cercle en fonction du centre de base.
- Oublier les passages par référence.
Avant | Après |
---|---|
Dans cet exercice, un effet de mosaïque a été appliqué à l'image en utilisant une version agrandie de l'image originale. L'algorithme divise l'image en une grille de carrés identiques et place des copies de l'image originale dans chaque carré.
- Une fonction
newImacPoster
est utilisée pour placer une copie de l'image originale à une position spécifique dans la nouvelle image. - Sur une grille de carrés est utilisé la fonction
newImacPoster
pour répliquer l'image dans chaque carré de la grille, formant ainsi l'effet de mosaïque. - Les variables
position_x
etposition_y
sont essentielles afin de parcourir correctement la nouvelle image et afficher l'originale.
void newImacPoster(sil::Image &image, sil::Image &newImage, int const &position_x, int const &position_y)
{
for (int x{0}; x < image.width(); x++)
{
for (int y{0}; y < image.height(); y++)
{
newImage.pixel(position_x + x, position_y + y) = image.pixel(x, y);
}
}
}
- Le ratio du nombre de carré est modulable grâce à une variable
ratio
présente au début dumain
.
int main()
{
sil::Image image{"images/arcane.jpg"};
int ratio{5};
sil::Image newImage{ratio * image.width(), ratio * image.height()};
for (int i{0}; i < ratio; i++)
{
for (int j{0}; j < ratio; j++)
newImacPoster(image, newImage, j * image.width(), i * image.height());
}
newImage.save("output/pouet.png");
}
- Oublier de créer une nouvelle image pour y implanter nos autres images.
- Oublier les références (surtout sur
newImage
).
Avant | Après |
---|---|
- Similaire à une mosaïque classique, mais on y ajoute des renversements ciblés sur l'axe x et y.
- On sait comment obtenir la mosaïque (c'est déjà bien).
- Maintenant, on remarque que toutes les images sur les colonnes impaires subissent un miroir par rapport à la verticale (on sait faire ça, on l'a fait sur l'algorithme ⭐⭐ Miroir).
- On remarque aussi que toutes les lignes impaires subissent un miroir par rapport à l'horizontale (en fait c'est le ⭐⭐Miroir adapté pour l'horizontale. Il suffit juste d'inverser y et x).
- L'idée est donc de créer une fonction qui nous permettrait de renverser soit selon la verticale, soit selon l'horizontale. On va utiliser un booléen qui conditionnera nos variables. Allez let's go!
void mirror(sil::Image &image, bool const reverse_y)
{
int divide_x{2};
int divide_y{1};
if (reverse_y)
{
divide_x = 1;
divide_y = 2;
}
for (int x{0}; x < image.width() / divide_x; x++)
{
for (int y{0}; y < image.height() / divide_y; y++)
{
int select_x{image.width() - (x + 1)};
int select_y{y};
if (reverse_y)
{
select_x = x;
select_y = image.height() - (y + 1);
}
std::swap(image.pixel(x, y), image.pixel(select_x, select_y));
}
}
}
- Voilà la fonction mirror ! Si je passe reverse_y à false, on aura notre ⭐⭐ Miroir, si on le set à true, c'est la même chose mais selon les y.
int main()
{
sil::Image const image{"images/arcane.jpg"};
int ratio{6};
bool reverseEffect{true};
sil::Image newImage{ratio * image.width(), ratio * image.height()};
for (int i{0}; i < ratio; i++)
for (int j{0}; j < ratio; j++)
{
sil::Image copy{image};
if (reverseEffect)
{
if (j % 2 != 0)
mirror(copy, false);
if (i % 2 != 0)
mirror(copy, true);
}
printPoster(copy, newImage, j * image.width(), i * image.height());
}
newImage.save("output/pouet.png");
}
- Voilà le main avec un booléen reverseEffect. Si ce dernier est set à false, on retrouvera notre mosaïque classique. Sinon, on applique nos changements et BOOM, ça fait des chocapics !
- Oublier l'& (Référence): Fondamentale pour garder le lien avec la variable d'origine, et donc de pouvoir garder et modifier de l'information dans une fonction. On a alors une portée globale (la modification d'une variable interne à la fonction possède une répercussion sur la variable, partout dans le code). Il ne faut surtout pas l'oublier quand on passe l'image en paramètre de notre fonction.
- Oublier de faire une copy de l'image dans le main à l'intérieur de notre boucle est une erreur. Si on cible l'image définie au début du main directement, le miroir appliqué à notre image ne se réinitialise pas. On travaille avec une même image qui cumule les miroirs, et on est pas au bout de nos surprises.
Avant | Après |
---|---|
- On souhaite sélectionner 2 rectangles de pixels aux hasard dans l'image et les échanger. Les tailles sont gérés aléatoirement mais les 2 rectangles doivent avoir la même taille.
- On va utiliser la librairie
glm
pour manipuler desvec2
nous permettant de stocker une position x et y. Notre code sera alors plus lisible et plus simple à gérer. - L'idée est de générer 2
vec2
. Un 1er avec la position du pixel de départ de notre 1er rectangle. Et un second avec la position de départ du 2ème rectangle.
glm::vec2 inputPositionStart{random_int(0, image.width()),random_in(0, image.height())};
glm::vec2 outputPositionStart{random_int(0, image.width()), random_int(0, image.height())};
- Il faut parcourir une taille commune
rectangleSize
pour pouvoir échanger le même nombre de pixel.
glm::vec2 rectangleSize{random_int(20, 30), random_int(3, 8)};
- Il suffit de boucler en vérifiant que nos pixels sont bien contenu dans l'image, puis d'utiliser la fonction
std::swap
et le tour est joué.
for (int i{0}; i <= rectangleSize.x; i++)
{
for (int j{0}; j <= rectangleSize.y; j++)
if (inputPositionStart.x + i < image.width() &&
inputPositionStart.y + j < image.height() &&
outputPositionStart.x + i < image.width() &&
outputPositionStart.y + j < image.height())
std::swap(image.pixel(inputPositionStart.x + i, inputPositionStart.y + j), image.pixel(outputPositionStart.x + i, outputPositionStart.y + j));
}
- On stock tout ça dans une fonction
ExchangeRectangle
et on boucle !
int main()
{
sil::Image image{"images/fma.jpg"};
int range{300};
for (int i{0}; i < range; i++)
ExchangeRectangle(image);
image.save("output/pouet.png");
}
- Oublier de vérifier si les pixels sont dans l'image.
Image |
---|
Dans cet exercice, un algorithme génère une image représentant la fractale de Mandelbrot. La fractale de Mandelbrot est un ensemble de points complexes dans le plan complexe qui produit une forme fractale lorsqu'elle est visualisée.
- Deux boucles imbriquées parcourent chaque pixel de l'image et effectuent des itérations selon la formule mathématique de la fractale de Mandelbrot.
- Pour chaque pixel de l'image, l'algorithme effectue un certain nombre d'itérations pour déterminer s'il appartient à l'ensemble de Mandelbrot en fonction de sa convergence ou de sa divergence.
- La couleur du pixel est définie en fonction du nombre d'itérations nécessaires avant que la séquence ne diverge.
#include <complex>
int main()
{
sil::Image image{500, 500};
for (float x{0}; x < image.width(); x++)
{
for (float y{0}; y < image.height(); y++)
{
float newX{x / 125 - 2};
float newY{y / 125 - 2};
int count{0};
std::complex<float> c{newX, newY};
std::complex<float> z{0.f, 0.f};
float result{0.f};
while (count < 50)
{
result = static_cast<float>(count) / 50;
z = z * z + c;
if (std::abs(z) > 2)
break;
image.pixel(x, y) = glm::vec3{result};
count++;
}
}
}
image.save("output/pouet.png");
}
- L'utilisation d'un booléen pour la boucle while. L'algorithme ne parviendrai pas à sortir de la boucle.
Avant | Après |
---|---|
Dans cet exercice, un effet de vortex a été appliqué à l'image. L'algorithme effectue une transformation de chaque pixel en utilisant une rotation autour d'un centre donné.
- Une fonction
rotated
est utilisée pour effectuer la rotation des pixels autour d'un centre de rotation. - La transformation de rotation est appliqué en fonction de la distance par rapport au centre de l'image.
- Sortir de l'image en remplaçant les pixels.
- Attribuer les nouvelles coordonnées
newPoint.x, newPoint.y
de la nouvelle imagevoidImage
. -> Notre transformation serait décalé par rapport au centrex,y
de notre image d'origine.
if (newPoint.x < image.width() && newPoint.x >= 0 && newPoint.y < image.height() && newPoint.y >= 0)
voidImage.pixel(x, y) = image.pixel(newPoint.x, newPoint.y);
Avant | Après |
---|---|
Dans cet exercice, un effet de tramage a été appliqué à l'image. L'algorithme transforme l'image en une version trame à l'aide d'une matrice de Bayer prédéfinie pour effectuer un tramage ordonné.
- Une fonction
bwImage
est utilisée pour convertir l'image en noir et blanc en remplaçant chaque composante RGB par la moyenne des valeurs R, G et B de chaque pixel pour obtenir des nuances de gris. - Le tramage est réalisé en itérant sur chaque pixel de l'image et en ajoutant une valeur prédéfinie de la matrice de Bayer à chaque pixel en noir et blanc.
- Selon la valeur résultante après l'ajout, les pixels sont convertis soit en noir (0), soit en blanc (1).
Avant | Après |
---|---|
Dans cet exercice, un effet de normalisation de l'histogramme a été appliqué à l'image. L'algorithme détermine le pixel le plus sombre pour le transformer en noir pur 0
et le pixel le plus clair pour le transformer en blanc pur 1
, normalisant ainsi la plage de valeurs des pixels.
- En utilisant les valeurs identifiées pour le pixel le plus sombre
darkPixel
et le plus clairwhitePixel
, l'algorithme normalise les valeurs RGB de chaque pixel en calculant la moyenne des composantes RGB en fonction du pixel le plus sombre et clair.
- Lors du calcul de normalisation, il ne faut pas oublier de multiplie par l'inverse de la valeur de notre pixel le plus clair pour ne pas se retrouver avec un histogramme trop sombre.
image.pixel(x, y).r = (image.pixel(x, y).r - darkPixel) * 1 / whitePixel;
image.pixel(x, y).g = (image.pixel(x, y).g - darkPixel) * 1 / whitePixel;
image.pixel(x, y).b = (image.pixel(x, y).b - darkPixel) * 1 / whitePixel;
Avant | Outline | Emboss |
---|---|---|
La convolution est le traitement d'une matrice (les pixels de notre image) par une autre petite matrice appelée matrice de convolution ou noyau (kernel). On utilise la convolution pour appliquer des transformations telles que le flou, la netteté, la détection de contours, etc...
- Une fonction
setEffect
est utilisée pour appliquer la convolution à chaque pixel de l'image en utilisant un kernel prédéfini. - Pour effectuer un flou simple, le kernel utilisé est une matrice 3x3 de valeurs prédéfinies. Chaque valeur du kernel multiplie les valeurs des pixels voisins, puis les valeurs résultantes sont utilisées pour former les pixels de la nouvelle image grâce à la moyenne des pixels environnants et du noyau.
- Selon le kernel et les valeurs des pixels environnants, différents effets peuvent être obtenus. Il est modulable avec les kernels proposés en commentaire.
- En fonction du kernel, une division peut être appliqué. Un booléen
divide
est alors mis en place pour être activé comme bon nous semble lorsque cela est nécessaire.
- Ne pas incrémenter la variable
number
comme ceci : Pour éviter que celle-ci ne s'ajoute pas lorsque des pixels dépassent l'image. Le kernel ne fonctionnerait donc pas sur les bords de l'image et serait faussé.
if (x + i >= 0 && x + i < image.width() && y + j >= 0 && y + j < image.height())
{
result += image.pixel(x + i, y + j) * kernel[number];
total += kernel[number];
number++;
}
-
Ne pas ajouter de nouvelle image sinon chaque pixel modifié sera pris en compte par son pixel voisin. Les pixels qui se transforment se base donc sur des pixels déjà transformés. L'effet ne marcherai donc pas.
-
Oublier de changer la valeur du booléen
divide
lorsqu'elle doit être pris en compte ou non (exemple pour l'effet outline, on ne doit pas diviser car on diviserait par 0 !).
Avant | Après |
---|---|
- On souhaite récupérer une portion rectangulaire de pixels. Cette portion doit être trié en fonction de l'intensité. Ainsi le pixel le plus lumineux se trouve au début de la portion et le moins lumineux à la fin. On replace ensuite la portion dans l'image au même endroit.
- On nous donne la fonction suivante, permettant de trier les éléments d'un tableau
table
.
std::sort(table.begin(), table.end(), [](glm::vec3 &color1, glm::vec3 &color2)
{ return brightness(color1) < brightness(color2); });
- Voici la fonction
brightness
qui retourne la somme des composantes RGB d'un pixel.
float brightness(glm::vec3 &color)
{
return (color.r + color.g + color.b);
}
- L'idée est de s'inspirer du glitch en sélectionnant un rectangle de pixel. On trouve aléatoirement un pixel de départ sur l'image et on parcourt une taille générée aléatoirement (pas trop grande non plus) et on fixe pour ce code
y
à 1.
glm::vec2 inputPositionStart{random_int(0, image.width()), random_int(0, image.height())};
glm::vec2 rectangleSize{random_int(20, 30), 1};
- Chaque pixel du rectangle est push dans un tableau.
for (int i{0}; i < rectangleSize.x; i++)
{
for (int j{0}; j < rectangleSize.y; j++)
{
if (inputPositionStart.x + i < image.width() &&
inputPositionStart.y + j < image.height())
table.push_back(image.pixel(inputPositionStart.x + i, inputPositionStart.y + j));
}
}
- On appelle la fonction de tri sur
table
. - On doit alors finalement boucler de la même façon sur notre rectangle en attribuant aux positions, les nouveaux pixels triés du tableau.
- On utilise alors une variable
count
pour parcourir notre tableau.
int count{0};
for (int i{0}; i < rectangleSize.x; i++)
{
for (int j{0}; j < rectangleSize.y; j++)
{
if (inputPositionStart.x + i < image.width() &&
inputPositionStart.y + j < image.height())
{
image.pixel(inputPositionStart.x + i, inputPositionStart.y + j) = table[count];
count++;
}
}
}
- On obtient là un rectangle trié. Il suffit maintenant de boucler!
- Tout le code ci-dessus a été implémenté dans une fonction
getRectangle()
excepté la fonctionbrightness
.
int main()
{
sil::Image image{"images/logo.png"};
for (int i{0}; i < 1000; i++)
getRectangle(image);
image.save("output/pouet.png");
}
- Oublier le
count
. Cette variable est essentielle pour être certain de parcourir tout notre tableau trié et ainsi de placer les pixels au bon endroit. - Ne pas vérifier les bornes. Il faut en effet s'assurer que les pixels que l'on manipule se trouvent dans l'image.
Avant | Après |
---|---|
- Transformer une image en peinture à l'huile, c'est très stylé.
- On prend un pixel, supposons qu'il soit central à un carré de pixel de 5x5. Découpons ce carré en 4 secteurs.
- En travaillant sur chaque secteur, on doit calculer la moyenne des pixels le composant, puis définir un écart type à partir de cette moyenne. On compare les écarts types des 4 secteurs. On retient la moyenne du secteur ayant le plus petit écart type et on applique cette moyenne au pixel central.
- Dans le
main
, définissons nos secteurs.factor
nous permet de savoir de combien de pixel on veut parcourir notre secteur. Plusfactor
est grand, plus l'effet de peinture sera important.
int factor{4};
std::array<std::array<int, 2>, 2> secteur_1{std::array{0, factor}, std::array{0, factor}};
std::array<std::array<int, 2>, 2> secteur_2{std::array{0, factor}, std::array{0, -factor}};
std::array<std::array<int, 2>, 2> secteur_3{std::array{0, -factor}, std::array{0, -factor}};
std::array<std::array<int, 2>, 2> secteur_4{std::array{0, -factor}, std::array{0, factor}};
- Calculons d'abord la moyenne de nos secteurs à partir de la fonction
moyenneSecteur
. La subtilité ici, c'est de définir des variablesincrease_i
etincrease_j
qui prendront une valeur de +1 ou -1 en fonction de la technique de parcours du secteur (ex: si on va de 0 vers -2, on veut décrémenter donc -1 pour chaque itération de boucle).
glm::vec3 moyenneSecteur(sil::Image &image, std::array<std::array<int, 2>, 2> §eur, int &x, int &y)
{
int increase_i{1};
int increase_j{1};
// J'ajoute ou je retire ?
if (secteur[0][1] < 0)
increase_i = -1;
if (secteur[1][1] < 0)
increase_j = -1;
// On détermine la moyenne du secteur
glm::vec3 moyenne_secteur{0.f};
int count{0};
for (int i{secteur[0][0]}; i != secteur[0][1] + increase_i; i += increase_i)
{
for (int j{secteur[1][0]}; j != secteur[1][1] + increase_j; j += increase_j)
{
if (x + i >= 0 && x + i < image.width() && y + j >= 0 && y + j < image.height())
{
moyenne_secteur += image.pixel(x + i, y + j);
count++;
}
}
}
moyenne_secteur /= (float)(count);
return moyenne_secteur;
}
- On calcule ensuite la variance dans une fonction
varianceSecteur
. Même logique de parcours que pour la moyenne sauf qu'on applique la formule de la variance, et on oublie pas de passer la moyenne précédemment calculée en paramètre.
glm::vec3 varianceSecteur(sil::Image &image, std::array<std::array<int, 2>, 2> §eur, int &x, int &y, glm::vec3 moyenne_secteur)
{
int increase_i{1};
int increase_j{1};
// J'ajoute ou je retire ?
if (secteur[0][1] < 0)
increase_i = -1;
if (secteur[1][1] < 0)
increase_j = -1;
// On détermine la variance du secteur
glm::vec3 variance{0.f};
int count{0};
for (int i{secteur[0][0]}; i != secteur[0][1] + increase_i; i += increase_i)
{
for (int j{secteur[1][0]}; j != secteur[1][1] + increase_j; j += increase_j)
{
if (x + i >= 0 && x + i < image.width() && y + j >= 0 && y + j < image.height())
{
variance += (image.pixel(x + i, y + j) - moyenne_secteur) * (image.pixel(x + i, y + j) - moyenne_secteur);
count++;
}
}
}
variance /= (float)(count);
variance = sqrt(variance);
return variance;
}
- On créé une fonction
calculSecteur
qui permet d'envoyer toute les propriétés de notre secteur, tel que lamoyenne
et lavariance
. On push notre secteur dans un tableau qui permettra ensuite de déterminer quelle variance est la plus faible. On fait ça pour tous les secteurs.
void calculSecteur(sil::Image &image, std::vector<std::array<glm::vec3, 2>> &table, std::array<std::array<int, 2>, 2> §or, int &x, int &y)
{
glm::vec3 moyenne{moyenneSecteur(image, sector, x, y)};
table.push_back({moyenne, varianceSecteur(image, sector, x, y, moyenne)});
}
- Dans le
main
, on appelle la fonctioncalculSecteur
pour nos différents secteurs. - On utilise la fonction
sort
qui va nous trier le tableau en question et nous mettre le secteur ayant la plus faible variance en position 0. Ainsi, on récupère à cet indice le secteur. En ciblant l'élément 0 du secteur, nous récupérons la valeur de la moyenne que l'on passe à notre pixel !
for (int x{0}; x < image.width(); x++)
{
for (int y{0}; y < image.height(); y++)
{
std::vector<std::array<glm::vec3, 2>> varianceTable;
calculSecteur(image, varianceTable, secteur_1, x, y);
calculSecteur(image, varianceTable, secteur_2, x, y);
calculSecteur(image, varianceTable, secteur_3, x, y);
calculSecteur(image, varianceTable, secteur_4, x, y);
// On veut la variance la plus faible, ici à l'indice 0
std::sort(
varianceTable.begin(),
varianceTable.end(),
[](std::array<glm::vec3, 2> const &array1, std::array<glm::vec3, 2> const &array2)
{
return glm::length(array1[1]) < glm::length(array2[1]);
});
voidImage.pixel(x, y) = varianceTable[0][0];
}
}
voidImage.save("output/pouet.png");
- Se précipiter, abandonner.