Skip to content

smallboyc/imac-cpp-workshop

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

36 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🐲 Workshop C++ S1 - IMAC

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.

🚩 Les bases

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


⭐ Ne garder que le vert

Avant Après
alt text alt text

📁 Code source

Description :

  • On souhaite simplement garder la composante Verte active.

Spécificités :

  • 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");
}


⭐ Échanger les canaux

Avant Après
alt text alt text

📁 Code source

Description :

  • On souhaite inverser les canaux RGB entre eux. Essayons d'inverser le canal bleu et rouge.

Spécificités :

  • 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é.

Pièges potentiels à éviter :

  • É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.



⭐ Noir & Blanc

Avant Après
alt text alt text

📁 Code source

Description :

  • On souhaite transformer notre image en noir et blanc.

Spécificités :

  • 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");
}


⭐ Négatif

Avant Après
alt text alt text

📁 Code source

Description :

  • On souhaite inverser le noir et le blanc

Spécificités :

  • 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 !


⭐ Dégradé

drawing

📁 Code source

Description :

  • On souhaite parcourir toute notre largeur en passant progressivement du noir au blanc.

Spécificités :

  • On remarque que si on fixe un x quelconque, les y 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");
}

Pièges potentiels à éviter :

  • 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).

Grapefruit slice atop a pile of other slices

Les bords gris ont été rajoutés pour bien discerner l'erreur.



⭐⭐ Miroir

Avant Après
alt text alt text

📁 Code source

Description :

  • On souhaite effectuer une rotation verticale de notre image.

Spécificités :

  • 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");
}

Pièges potentiels à éviter :

  • 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.


⭐⭐ Image Bruitée

Avant Après
Image d'origine Image modifiée

📁 Code source

Description

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.

Spécificités

  • 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");
}


⭐⭐ Rotation de 90°

Avant Après
Image d'origine Image modifiée

📁 Code source

Description

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.

Spécificités

  • 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. image
  • 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");
}


⭐⭐ RGB Split

Avant Après
Image d'origine Image modifiée

📁 Code source

Description

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.

Spécificités

  • 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.

Pièges potentiels à éviter :

  • 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.


⭐⭐ Luminosité

Après Assombrissement Avant Après Eclaircissement
Image modifiée sombre Image d'origine Image modifiée claire

📁 Code source

Description

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.

Spécificités

  • 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 de number, ce qui permet de contrôler l'intensité lumineuse des pixels.

Pièges potentiels à éviter :

  • Multiplier les valeurs sans les fonctions puissances. Cela nous donnerait un résultat trop saturé.


⭐⭐ Disque

Image
Image d'origine

📁 Code source

Description

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.

Spécificités

  • 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");
}


⭐ Cercle

Image
Image d'origine

📁 Code source

Description

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.

Spécificités

  • 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");
}


⭐⭐⭐ Rosace

Image
Image d'origine

📁 Code source

Description

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.

Spécificités

  • 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 &center_x, int &center_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");
}

Pièges potentiels à éviter :

  • Oublier d'ajouter un nouveau centre pour chaque cercle en fonction du centre de base.
  • Oublier les passages par référence.


⭐⭐ Mosaïque

Avant Après
Image d'origine Image modifiée

📁 Code source

Description

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é.

Spécificités

  • 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 et position_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 du main.
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");
}

Pièges potentiels à éviter :

  • Oublier de créer une nouvelle image pour y implanter nos autres images.
  • Oublier les références (surtout sur newImage).


⭐⭐⭐ Mosaïque miroir

Avant Après
alt text drawing

📁 Code source

Description :

  • Similaire à une mosaïque classique, mais on y ajoute des renversements ciblés sur l'axe x et y.

Spécificités :

  • 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 !

Pièges potentiels à éviter :

  • 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.


⭐⭐⭐ Glitch

Avant Après
alt text alt text

📁 Code source

Description :

  • 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.

Spécificités :

  • On va utiliser la librairie glm pour manipuler des vec2 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");
}

Pièges potentiels à éviter :

  • Oublier de vérifier si les pixels sont dans l'image.


⭐⭐⭐ Fractale de Mandelbrot

Image
Image

📁 Code source

Description

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.

Spécificités

  • 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");
}

Pièges potentiels à éviter :

  • L'utilisation d'un booléen pour la boucle while. L'algorithme ne parviendrai pas à sortir de la boucle.


⭐⭐⭐(⭐) Vortex

Avant Après
Image d'origine Image modifiée

📁 Code source

Description

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é.

Spécificités

  • 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.

Pièges potentiels à éviter :

  • Sortir de l'image en remplaçant les pixels.
  • Attribuer les nouvelles coordonnées newPoint.x, newPoint.y de la nouvelle image voidImage. -> Notre transformation serait décalé par rapport au centre x,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);


⭐⭐⭐(⭐) Tramage

Avant Après
Image d'origine Image modifiée

📁 Code source

Description

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é.

Spécificités

  • 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).


⭐⭐⭐(⭐) Normalisation de l'histogramme

Avant Après
Image d'origine Image modifiée

📁 Code source

Description

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.

Spécificités

  • En utilisant les valeurs identifiées pour le pixel le plus sombre darkPixel et le plus clair whitePixel, 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.

Pièges potentiels à éviter :

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


⭐⭐⭐⭐ Convolutions

Avant Outline Emboss
Image d'origine Image modifiée claire Image modifiée sombre

📁 Code source

Description

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...

Spécificités

  • 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.

Pièges potentiels à éviter :

  • 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 !).



⭐⭐⭐⭐ Tri de pixels

Avant Après
alt text alt text

📁 Code source

Description :

  • 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.

Spécificités :

  • 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 fonction brightness.
int main()
{
    sil::Image image{"images/logo.png"};
    for (int i{0}; i < 1000; i++)
        getRectangle(image);
    image.save("output/pouet.png");
}

Pièges potentiels à éviter :

  • 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.


⭐⭐⭐⭐⭐ Filtre de Kuwahara

Avant Après
alt text alt text

📁 Code source

Description :

  • 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.

Spécificités :

  • Dans le main, définissons nos secteurs. factor nous permet de savoir de combien de pixel on veut parcourir notre secteur. Plus factor 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 variables increase_i et increase_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> &secteur, 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> &secteur, 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 la moyenne et la variance. 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> &sector, 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 fonction calculSecteur 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");

Pièges potentiels à éviter :

  • Se précipiter, abandonner.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published