Sujet proposé par Matthieu Moy pour l'année 2019-2020.
La photo numérique ouvre beaucoup de possibilités en terme de post-traitement d'image. Nous nous intéresserons ici à la gestion du contraste, avec plusieurs familles de traitements :
Le sujet est très libre, et pourra explorer plusieurs (mais pas forcément toutes) des pistes suivantes :
NumPy fournit des structure de données efficaces (tableaux N dimensions), SciPy des algorithmes en tous genre (en particulier des algorithmes travaillant sur les structures de données NumPy), et SciKit-Image est une bibliothèque spécialisée dans le traitement d'images.
Un exemple de quelques traitements est donné dans test_img_contrast.py.
Faisons un essai avec la bibliothèque CImg. Téléchargez la bibliothèque (il suffit d'un fichier CImg.h), et crééz un fichier basic_contrast.cpp dans le même répertoire, contenant :
#include "CImg.h" #include <stdlib.h> using namespace cimg_library; int main(int argc, char **argv) { const char *argv1 = (argc > 1) ? argv[1] : "global_contrast"; CImg<float> original("lena.png"); CImg<float> image = +original; image = image * 2 - 100; (original, image).display(); return 0; }
Compilez et exécutez-le comme ceci (la compilation prend plusieurs secondes, c'est normal)
g++ -pthread -lX11 basic_contrast.cpp -o basic_contrast ./basic_contrast
Une fenêtre apparaît avec le contenu suivant :
Oups, l'original a l'air d'être identique à l'image modifiée ! En fait, CImg a appliqué une normalisation à notre image au moment de l'affichage, et a essentiellement annulé ce que nous venions de faire. Le plus simple pour désactiver cette normalisation est de convertir l'image en entiers 8 bits par pixels (nous utilisons pour l'instant des float). La conversion se passera mal pour les valeurs non comprises entre 0 et 255, mais pas de panique, voici une fonction to_uint8 qui s'occupe de tout :
#include "CImg.h" #include <stdlib.h> using namespace cimg_library; CImg<unsigned char> to_int8(CImg<float> const & img) { CImg<unsigned char> i8 = img; // fix badly clipped pixels for (int i = 0; i < img.width(); i++) { for (int j = 0; j < img.height(); j++) { for (int c = 0; c < 3; c++) { if (img(i, j, c) <= 0.0) { i8(i, j, c) = 0; } else if (img(i, j, c) >= 255.) { i8(i, j, c) = 255; } } } } return i8; } int main(int argc, char **argv) { const char *argv1 = (argc > 1) ? argv[1] : "global_contrast"; CImg<float> original("lena.png"); CImg<float> image = +original; image = image * 2 - 100; (to_int8(original), to_int8(image)).display(); return 0; }
Relancez et regardez le résultat. En cliquant sur une des images, on peut observer la valeur de chaque pixel en plaçant la souris dessus :
Ce point particulier nous permet d'observer un défaut de notre méthode d'augmentation de contraste : les composantes vertes et bleues étaient négatives et ont été tronquées à 0 (« clipped » en anglais). Le résultat est un pixel rouge pur, ce qui n'était pas du tout le cas sur notre image d'origine. Une meilleure option est de changer d'espace de couleur pour faire l'opération. Nous allons utiliser l'espace Lab au lieu de RGB: le canal L représente la luminance du pixel (entre 0 et 100), et les canaux a et b représentent la couleur. CImg nous permet de passer d'un espace à l'autre facilement :
#include "CImg.h" #include <stdlib.h> using namespace cimg_library; CImg<unsigned char> to_int8(CImg<float> const & img) { CImg<unsigned char> i8 = img; // fix badly clipped pixels for (int i = 0; i < img.width(); i++) { for (int j = 0; j < img.height(); j++) { for (int c = 0; c < 3; c++) { if (img(i, j, c) <= 0.0) { i8(i, j, c) = 0; } else if (img(i, j, c) >= 255.) { i8(i, j, c) = 255; } } } } return i8; } int main(int argc, char **argv) { const char *argv1 = (argc > 1) ? argv[1] : "global_contrast"; CImg<float> original("lena.png"); CImg<float> image = +original; image.RGBtoLab(); CImg<float> L = image.get_shared_channel(0); CImg<float> a = image.get_shared_channel(1); CImg<float> b = image.get_shared_channel(2); L = L * 2 - 75; image.LabtoRGB(); (to_int8(original), to_int8(image)).display(); return 0; }
Si on trouve l'image trop terne, on peut multiplier les canaux a et b, par exemple par 1.5 pour un effet un peu brutal.
On peut noter l'utilisation de plusieurs fonctionnalités intéressantes de CImg :
Régler le contraste globale d'une image peut se faire de plusieurs manières différentes (dans les explications, on suppose que la valeur d'un pixel est comprise dans l'intervalle [0,1]) :
Un simple coefficient multiplicateur appliqué à tous les pixels. Ceci correspond à une correction d'exposition pour une photo, mais ne modifie pas réellement le « contraste », cela permet simplement d'éclaircir ou d'assombrir l'ensemble de l'image.
Un ajustement affine du type ax + b, qui avec a>1 et b<0 permet d'assombrir les pixels sombres et d'éclaircir les pixels clairs, donc cette fois-ci d'augmenter le contraste. Mais cette méthode est très brutale, les pixels pouvant sortir de l'intervalle [0, 1].
Une courbe en S, définie par :
On peut par exemple choisir une fonction f de la forme f(x) = ax3 + bx2 + cx + d. Pour chaque valeur de x0, y0, d on peut trouver a, b, c, d par résolution d'un système linéaire. On trouve trivialement d = 0 donc il s'agit d'un système 3x3, paramétré par x0, y0 et d0. On peut résoudre ce système symboliquement une fois pour toutes, ou plus simplement résoudre le système au cas par cas avec un solveur comme numpy.linalg.solve, ce qui peut donner ceci :
La pyramide gaussienne peut être vue comme une généralisation du masque flou : au lieu de séparer l'image en deux couches (contraste local et global), on la décompose en une succession d'images, chacune représentant un niveau de détail de l'image. On peut alors augmenter ou réduire chaque niveau individuellement.