Aller au contenu

Anime Girl Generator et espace colorimétrique

Anime Girl Generator est, à l'origine, un petit soft développé par un (des ?) japonais, servant à concevoir un avatar féminin dans le plus pur style manga.

Anime Girl Generator

Comme je ne pane rien au japonais, il m'est venu l'idée saugrenue de redévelopper le soft de zéro, et de le proposer en français et anglais.

Mon premier essai, que voici, a été réalisé en C# .Net. Je regrette presque de ne pas avoir dès le début choisi une technologie vraiment multiplateforme (et que personne ne vienne me parler de mono !). Je regrette d'autant plus qu'après coup, j'ai découvert que l'auteur du soft original fournissait les sources (oui, je suis un boulet).

Malgré tout, l'expérience était intéressante, et je vais quand même vous fournir ça.

Vous pouvez récupérer ma version sur cette page.

Espace colorimétrique

La partie sur laquelle je me suis le plus éclaté, et sur laquelle je souhaitais m'étendre : la gestion de la teinte.

Petit rappel, dans le monde de l'informatique, il y a plusieurs façons de définir une couleur. On parle d' espaces colorimétriques. Chaque espace colorimétrique définit plusieurs variables qui vont permettre d'influer sur une couleur de différentes manières (sur sa luminosité, sa teinte, son taux de rouge, etc...).

Les principaux espaces colorimétriques (en tout cas ceux que j'ai le plus rencontrés dans ma vie d'informaticien) sont les suivants :

  • RGB (RVB en français) : le plus connu en informatique, car c'est sur ce modèle que se basent les écrans pour afficher des pixels de couleur. Il utilise la synthèse additive, dont les 3 couleurs primaires sont le Rouge, le Vert et le Bleu (d'où RVB).
  • CMYK (CMJN en français) : utilisé à la base en peinture (là où RVB fonctionne sur un mélange de lumières colorées, CMJN fonctionne sur un mélange de peintures colorées). Il s'agit de la synthèse soustractive, dont les couleurs primaires sont le Cyan, le Magenta et Jaune, auxquelles on ajoute le noir pour donner CMJN.
  • HSV/HSL (TSL en français) : pour exprimer les couleurs sur un modèle plus compréhensible pour nos yeux humains, TSL distingue la Teinte (variation entre rouge, bleu, vert, orange, etc...), la Saturation (couleur vive ou pastel) et la Luminosité (couleur claire ou sombre).

En C# .Net, les images sont gérées en tant que Bitmap, sur le modèle RVB. Or, pour modifier les couleurs de façon simple dans AGG, il me fallait permettre à l'utilisateur de faire varier la teinte. En gros, les éléments à ma disposition me permettaient de faire varier :

Espace RVB

Alors que je voulais faire varier la teinte, dans cet esprit :

Espace TSL

Et aussi incroyable que cela puisse paraitre, il n'y a rien dans .Net qui permette d'effectuer cela. Et tous les exemples que j'ai pu trouver sur internet faisaient étrangement beaucoup d'approximations, donnant des résultats douteux (quand on fait varier la teinte, cela influe aussi sur la luminosité et la saturation...).

J'ai donc décidé de prendre les choses en main et de chercher du côté des mathématiciens ce que je ne trouvais pas chez les informaticiens (qui oublient parfois un peu vite leurs origines :-P ).

Un peu de math

Je savais que .Net proposait un système matriciel pour faire varier les couleurs de tous les pixels d'une Bitmap selon nos besoins. En effet, on peut symboliser chaque pixel d'une image sous forme d'un vecteur de 3 nombres (valeurs de rouge, vert, bleu). En réalité, le vecteur comportera 5 valeurs : la 4ème valeur représentant le taux de transparence (la couche alpha), que nous n'utiliserons pas ici, et une 5ème valeur utilisée pour des raisons techniques que je ne détaillerai pas (en gros, l'utilisation d'un seul calcul passant par une matrice 4x4 ne permet que des rotations et des dilatations, alors qu'on peut aussi avoir besoin d'effectuer des translations...).

Ainsi, on a le vecteur (x1, y1, z1) qui représente notre pixel (x pour Rouge, y pour Vert et z pour Bleu). Le but est d'obtenir un nouveau vecteur (x2, y2, z2) représentant une couleur de même luminosité et saturation que la première, mais avec une teinte différente.

Visualisons cela dans l'espace : nous faisons varier nos x, y et z entre 0 et 1. Nous évoluons donc dans un cube de 1 de côté, et dont deux sommets opposés sont (0,0,0) et (1,1,1).

  • Notre x, c'est-à-dire notre éloignement au plan (y,z), exprime la quantité de rouge dans notre image ;
  • Notre y, c'est-à-dire notre éloignement au plan (x,z), exprime la quantité de vert dans notre image ;
  • Notre z, c'est-à-dire notre éloignement au plan (x,y), exprime la quantité de bleu dans notre image ;
  • L'éloignement par rapport à l'axe "identitaire" (je ne suis pas sur de la nomination : je parle de l'axe passant par les points (0,0,0) et (1,1,1)) exprime la saturation. Plus on est proche de l'axe et plus c'est gris, plus on s'en éloigne et plus c'est coloré ;
  • L'éloignement par rapport à l'origine exprime la luminosité. Le point (0,0,0) est blanc, tandis que le (1,1,1) est noir ;
  • Et enfin, l'angle autour de l'axe "identitaire" est ce qui exprime la teinte.

Ainsi, l'objectif va être de tourner autour de cet axe identitaire pour faire varier la teinte, tout en gardant la même saturation (distance à l'axe) et la même luminosité (distance à l'origine). Cette modification s'exprimera donc sous forme d'un angle, en radian ou degré.

Pour cela, on utilise une matrice de rotation, qui sert à calculer la rotation d'un point autour d'un axe. Ladite matrice se présente ainsi :

( x 2 y 2 z 2 ? ? ) = ( x 1 y 1 z 1 ? ? ) ( u 2 + ( v 2 + w 2 ) cos ( θ ) u 2 + v 2 + w 2 u v ( 1 - cos ( θ ) ) - w u 2 + v 2 + w 2 sin ( θ ) u 2 + v 2 + w 2 u w ( 1 - cos ( θ ) ) + v u 2 + v 2 + w 2 sin ( θ ) u 2 + v 2 + w 2 0 0 u v ( 1 - cos ( θ ) ) + w u 2 + v 2 + w 2 sin ( θ ) u 2 + v 2 + w 2 v 2 + ( u 2 + w 2 ) cos ( θ ) u 2 + v 2 + w 2 v w ( 1 - cos ( θ ) ) - u u 2 + v 2 + w 2 sin ( θ ) u 2 + v 2 + w 2 0 0 u w ( 1 - cos ( θ ) ) - v u 2 + v 2 + w 2 sin ( θ ) u 2 + v 2 + w 2 v w ( 1 - cos ( θ ) ) + u u 2 + v 2 + w 2 sin ( θ ) u 2 + v 2 + w 2 w 2 + ( u 2 + v 2 ) cos ( θ ) u 2 + v 2 + w 2 0 0 0 0 0 1 0 0 0 0 0 1 )

Ouch, ça fait mal, hein ?

On notera qu'elle contient les variables suivantes :

  • nos (x, y, z) de départ
  • nos (x, y, z) d'arrivée
  • notre angle θ
  • les variables u, v, w

Cette matrice sert à calculer l'angle autour de n'importe quel axe passant par l'origine. De fait, (u,v,w) définissent un autre point par lequel doit passer cet axe. Ici, nous auront simplement besoin de (1,1,1).

Du coup, la matrice se simplifie si on remplace ces variables par 1 :

( x 2 y 2 z 2 ? ? ) = ( x 1 y 1 z 1 ? ? ) ( 1 + 2 cos ( θ ) 3 ( 1 - cos ( θ ) ) - 3 sin ( θ ) 3 ( 1 - cos ( θ ) ) + 3 sin ( θ ) 3 0 0 ( 1 - cos ( θ ) ) + 3 sin ( θ ) 3 1 + 2 cos ( θ ) 3 ( 1 - cos ( θ ) ) - 3 sin ( θ ) 3 0 0 ( 1 - cos ( θ ) ) - 3 sin ( θ ) 3 ( 1 - cos ( θ ) ) + 3 sin ( θ ) 3 1 + 2 cos ( θ ) 3 0 0 0 0 0 1 0 0 0 0 0 1 )

Et voilà. Il suffit en théorie d'appliquer ce calcul à chaque pixel de notre image.

Application à notre code .Net

En réalité, comme je l'évoquais au début, .Net nous offre la possibilité de lui fournir la matrice toute prête, et il s'occupe de l'appliquer à l'ensemble de notre image :

private void RotateColors(Bitmap image, float degrees)
{
    ImageAttributes imageAttributes = new ImageAttributes();
    int width = image.Width;
    int height = image.Height;
    double r = degrees * System.Math.PI / 180; // degrees to radians
    float cosR = (float)Math.Cos(r);
    float sinR = (float)Math.Sin(r);
    float a = (1 + 2 * cosR) / 3;
    float b = ((1 - cosR) - (float)Math.Sqrt(3) * sinR) / 3;
    float c = ((1 - cosR) + (float)Math.Sqrt(3) * sinR) / 3;

    float[][] colorMatrixElements = { 
        new float[] {a, b, c, 0, 0},
        new float[] {c, a, b, 0, 0},
        new float[] {b, c, a, 0, 0},
        new float[] {0, 0, 0, 1, 0},
        new float[] {0, 0, 0, 0, 1}};

    ColorMatrix colorMatrix = new ColorMatrix(colorMatrixElements);

    imageAttributes.SetColorMatrix(
       colorMatrix,
       ColorMatrixFlag.Default,
       ColorAdjustType.Bitmap);

    using (Graphics g = Graphics.FromImage(image))
    {
        g.DrawImage(
           image,
           new Rectangle(0, 0, width, height),  // destination rectangle 
            0, 0,        // upper-left corner of source rectangle 
            width,       // width of source rectangle
            height,      // height of source rectangle
            GraphicsUnit.Pixel,
           imageAttributes);
    }
}

Je ne suis pas rentré dans les détails de l'utilisation des Bitmap, ColorMatrix et Graphics en .Net car ce n'était pas l'objectif. L'essentiel était d'avoir un aperçu de comment s'utilise cette matrice de rotation dans notre code.

On peut dire que malgré le côté effrayant de cette matrice, son application est toute bête. Il est d'ailleurs difficile d'imaginer qu'il n'existe pas déjà une fonction existante dans le framework .Net pour effectuer cela. L'étape suivante pourrait être de travailler sur les dilatations et translations évoquées plus haut pour pouvoir aussi paramétrer la saturation et la luminosité. Si vous avez quelques bouts de code à proposer, surtout n'hésitez pas à m'en faire part, par mail ou dans les commentaires.

EDIT : j'allais oublier de citer mes sources (non non, je n'ai pas réussi à pondre la première matrice tout seul. Je suis un peu trop rouillé pour ça...) :

EDIT bis : un autre article que je regrette de ne pas avoir trouvé plutôt : Color transformations and the Color Matrix