Correction de Couleurs - John COLIBRI. |
- résumé : outil de modification des couleurs d'une image, en utilisant la décomposition RGB ou HSV
- mots clé : graphique - tBitMap - correction de couleur - modèle RGB et HSV
- logiciel utilisé : Windows XP, Delphi 6.0
- matériel utilisé : Pentium 1.400Mhz, 256 M de mémoire
- champ d'application : Delphi 1 à 2005 sur Windows
- niveau : développeur Delphi
- plan :
1 - Introduction J'ai pris des photos de la maison que nous souhaitions louer dans le golfe du Morbihan. J'ai utilisé pour cela une caméra numérique, en utilisant la carte mémoire pour les photographies. Le résultat des image numériques transférées sur PC était très
décevant: toutes les images étaient trop sombres:
J'avais donc plusieurs choix: - télécharger et apprendre à utiliser un quelconque photoshop
- charger la bitmap en Delphi et la modifier ainsi
Ayant choisi d'effectuer les modifications moi-même, voici un exemple de correction de l'image précédente:
2 - Décomposition couleur 2.1 - La décomposition Rouge / Vert / Bleu Le format des bitmap Windows le plus fréquent de nos jours contient pour chaque
octet de l'image 1 octet par pixel: le rouge le vert et le bleu (RGB). Les fichiers .BMP contiennent un prologue (la taille, le nombre de bits par couleur etc) puis les 3 octets pour chaque point.
Une fois chargé en mémoire, nous pouvons utiliser Canvas.Pixels[x, y] pour récupérer dans un Integer (aussi défini comme un tColorRef) les 3 octets de
couleur (le quatrième octet étant jadis utilisé pour coder les palettes).
2.2 - La ScanLine Pour traiter chaque pixel, nous pouvons utiliser:
with my_bitmap, Canvas do
for l_line_index:= 0 to Width- 1 do
for l_column_index:= 0 to Height- 1 do
... Pixels[l_column_index, l_line_index] ... |
Or l'extraction de chaque point via Canvas.Pixels[x, y] utilise une double indexation qui coûte cher en temps de calcul. De plus, les .BMP sont stockés en
mémoire dans une structure Windows appelée DIB (Device Independent Bitmap) qui stocke les couleurs sous forme de lignes de triplets RGB. Cette structure est accessible par la propriété Bitmap.ScanLine, et il s'avère que
l'utilisation de cette structure accélère les traitements des points de la tBitmap. Il vaut donc mieux utiliser:
type // -- from Wintow
t_RGB_triple= packed record
m_blue, m_green, m_red: Byte;
end;
// -- this represents a line of 3 color bytes
t_RGB_array= ARRAY[0..0] OF t_RGB_triple;
// -- a pointer to this line
t_pt_RGB_array= ^ t_RGB_array;
var l_line_index, l_column_index: Integer;
l_pt_source_row: t_pt_RGB_array;
For l_line_index:= 0 to m_line_count- 1 do
begin
l_pt_source_row:= m_c_bitmap.Scanline[l_line_index];
for l_column_index:= 0 to m_column_count- 1 do
begin ... _pt_source_row[l_column_index].m_red ...
end; // for l_column_index end; // for l_line_index
|
2.3 - La décomposition réversible Pour modifier les couleurs d'une image, il suffit donc de modifier les valeurs des octets RGB. Par exemple, pour "éclaircir" l'image, il suffit d'augmenter
tous les pixels d'un certain montant, par exemple 10. Lorsque la valeur de la couleur se trouve au milieu de la plage 0..255, l'augmentation d'une petite valeur est réversible: 72+ 10 = 82 82- 10 = 72
Mais lorsque la modification dépasse l'intervalle 0..255, la nouvelle valeur est tronquée pour la conserver entre 0 et 255, et la modification inverse n'est plus possible: 250+ 10 = 260 qui est ramené à 255
255- 10 = 245 qui est différent de 250
Si nous souhaitons par conséquent modifier le contenu de l'image de façon
interactive (via des tEdit, tUpDown, tTrackBar, tScrollBox ou autre), nous ne pourrons plus revenir en arrière: il faut recharger l'image de départ et repartir de cette image.
Une solution est de gérer en parallèle une image dont les valeurs RGB sont codées sous forme de Double: les contrôles visuels modifie la valeur des réels, et pour l'affichage, nous recopions, en tronquant au besoin, les valeurs
des réels dans les octets de la tBitmap
2.4 - L'ergonomie Notre premier programme était d'abord parti de logiciels existants. Google fournit plusieurs sources Delphi, qui effectuent la décomposition RGB, puis
permettent des modifications incrémentales des valeurs RGB des octets. Tous ceux que j'ai trouvé travaillent directement sur la tBitmap, donc sur ces octets, et ne permettent donc pas l'annulation des corrections.
Il s'avère que la correction d'image est assez peu intuitive. Il n'est pas évident de décider s'il faut augmenter le contenu rouge, réduire toutes les valeurs, ou de quelle manière.
Notre seconde tentative fut donc d'utiliser le tableau parallèle de Double avec recopie dans la tBitmap. La définition des modifications se faisait en tapant des valeurs dans des tEdit, éventuellement avec des tUpDown.
L'utilisation d'un tTrackBar pour ajuster la valeur de l'incrément fut l'extension naturelle. La modification sur tTrackBar.OnChange s'avéra désagréable: il fallait un peu agiter le curseur pour provoquer le changement
depuis la valeur courante. D'ou la version actuelle, où la position du curseur fournit directement la modification depuis l'image originale. Nous utilisons donc trois curseurs: Rouge, Vert, Bleu, et à chaque mouvement de
curseur, nous recalculons la valeur de chaque point. Puisque nous effectuons les modifications depuis l'original, le tableau de Double devient inutile: nous pourrions directement effectuer les calculs
depuis la bitmap de départ. Nous avons conservé les tableaux de Double pour les calculs en HSV que nous allons présenter maintenant.
2.5 - La décomposition HSV
La décomposition Rouge Vert Bleu est surtout utile si une image est trop biaisée vers l'une de ces composante: trop verdâtre ou rougeâtre. En revanche si l'image est trop grisâtre, trop sombre ou pas assez contrastée,
la représentation RGB n'est pas la plus aisée à utiliser. Plusieurs modèles de couleurs ont été proposés, parmi lesquels le modèle HSV. Chaque point est décomposé en: - Hue qui représente la couleur, comme une valeur entre 0 et 360
- Saturation qui indique la "densité" de la couleur
- Value qui représente l'intensité
La couleur est représentée par un angle, les rouges étant dans la partie 0-60
et 300-360 (environ), les verts dans la partie 60-180, les bleus dans la partie 180, 300. - Voici trois exemples de couleurs:
La différence entre la saturation et l'intensité n'est pas intuitive. Prenons un exemple sur un rouge bordeaux: - en changeant la saturation, la teinte perçue restera la même (rouge
bordeaux) mais sera soit atténuée vers les gris, soit plus "dense"
- en changeant l'intensité, nous balayerons la plage du noir complet à un rouge étincelant
Mathématiquement, les choses sont plus simples. Il s'agit d'un simple changement de coordonnées en 3D: - l'intensité est la plus grande des valeurs Red, Green et Blue:
Max(r, g, b) - la saturation est obtenue par:
(Max(r, g, b)- Min(r, g, b)) / Max(r, g, b) - la couleur est l'angle du point avec l'axe RGB le plus proche du point (si
Rouge est la plus grande valeur, c'est la valeur par rapport à l'axe rouge, etc)
Voici d'ailleurs la fonction de conversion que nous avons utilisée:
function f_rgb_to_hsv(CONST p_k_rgb_float_triple: t_RGB_float_triple): t_hsv_triple;
// -- hue= 0 to 360 (corresponding to 0..360 degrees around hexcone)
// -- saturation= 0 (shade of gray) to 1 (pure color)
// -- intensity= 0 (black) to max {white)
VAR l_intensity_min: t_float;
l_intensity_range: t_float; BEGIN
WITH p_k_rgb_float_triple, Result DO
BEGIN // -- intensity:= Max(red, green, blue)
m_intensity:= f_float_max(m_float_red, f_float_max(m_float_green, m_float_blue));
if m_intensity= 0
then begin
// -- saturation is 0 if r, g and b are all 0
m_saturation:= 0;
// -- achromatic: When saturation = 0, color is undefined
// -- but arbitrarily assigned the value 0
m_color:= 0; end
else begin
l_intensity_min:= f_float_min(m_float_red, f_float_min(m_float_green, m_float_blue));
l_intensity_range:= m_intensity- l_intensity_min;
// -- get the saturation as a percentage range / max
m_saturation:= l_intensity_range / m_intensity;
if m_saturation= 0
then begin
m_color:= 0;
end
else begin
// -- chromatic
// -- find the color with the maximum value
// -- and compute the spread with the other two colors
if m_float_red= m_intensity
then // -- the red is the greatest value
// -- this value becomes the intensity
// -- degrees between yellow and magenta
m_color:= (m_float_green- m_float_blue)* 60.0 / l_intensity_range
else
IF m_float_green= m_intensity
THEN // -- degrees between cyan and yellow
m_color:= 120.0+ (m_float_blue- m_float_red)* 60.0/ l_intensity_range
else
if m_float_blue= m_intensity
THEN // -- degrees between magenta and cyan
m_color:= 240.0+ (m_float_red- m_float_green)* 60.0/ l_intensity_range
else display_bug_halt('no_rgb_max');
end; end;
IF m_color< 0
THEN begin
// -- wrap around
m_color:= m_color+ 360.0;
end; // -- normalize itensity to 1
m_intensity:= m_intensity / 255;
END; // WITH p_k_rgb_float_triple, Result
END; // f_rgb_to_hsv |
Schématiquement, nous pouvons représenter le domaine des couleurs:
- soit comme un cube dont les 3 axes correspondent aux valeurs des octets de rouge, de vert et de bleu:
- soit comme un cône:
- le plan xOy représente les couleurs sur 360 degrés
- la verticale représente l'intensité
- l'intérieur du cône représente la saturation
2.6 - Les transformations
Nous pouvons appliquer n'importe quelle fonction pour remplacer les valeurs des composantes (RGB ou HSV) d'un point par d'autres valeurs: - une translation (ajout d'une valeur constante)
- une homothétie (multiplier la valeur par une constante)
- des transformations non linéaires: le carré, la racine carrée
Dans le projet qui suit, nous avons essentiellement mis en oeuvre: - les translations
- les puissances
En fait, le plus de souplesse est apporté en laissant l'utilisateur dessiner une courbe de réponse qui serait appliquée à chaque composante de chaque point.
Pour cela, nous avons crée un éditeur de courbe, avec interpolation linéaire ou Bézier.
2.7 - L'analyze de l'image Après avoir chargé une image, nous pouvons:
- modifier les composantes RGB (les rouges, les verts, les bleus)
- modifier les composantes HSV
Mais à part un effet visuel immédiat, nous n'avons aucune idée de ce qu'il "faudrait" faire.
Pour cela, nous avons essayé d'ajouter des outils d'analyze: - des histogrammes qui représente le nombre de points RGB, ou HSV. A chaque changement de l'image, les histogrammes sont recalculés, nous donnant une
idée plus quantitative du changement
- une représentation de la gamme des couleurs (la même que dans tColorDialog), avec:
- un affichage de la couleur sous la souris lorsque nous déplaçons la souris sur l'image
- un affichage dans une autre bitmap de tous les autres points ayant une couleur voisine de la couleur sous la souris
3 - Le programme Delphi 3.1 - Le chargement de l'image
L'image est chargée en sélectionnant le fichier dans une tFileListbox. Si nous chargeons une .BMP usuelle, elle sera représentée en mémoire avec le nombre de bit par couleur défini par le fichier. Or pour effectuer nos décompositions,
nous préférons forcer une représentations avec 8 bits pour chaque couleur. Pour cela: - nous chargeons la tBitmap dans une tImage
- nous créons une tBitmap en forçant son nombre de bits de couleurs à 8 bit
pour chaque couleur
- nous copions l'image de la tImage dans notre tBitmap
Voici la mécanique:
procedure load_bitmap; begin
Form1.Image1.Picture.LoadFromFile(g_path+ g_file_name);
with g_c_bitmap do begin
// -- create our bitmap with the 24 bit colors
m_c_bitmap.Free;
m_c_bitmap:= tBitmap.Create;
m_c_bitmap.width:= Form1.Image1.picture.graphic.width;
m_c_bitmap.height:= Form1.Image1.picture.graphic.height;
m_c_bitmap.pixelformat:= pf24bit;
// -- copy the image from the tImage
m_c_bitmap.canvas.draw(0, 0, Form1.Image1.picture.graphic);
end; // with g_c_bitmap end; // load_bitmap |
3.2 - Chargement de JPEG Si notre fichier est un .JPG ou .JPEG, Delphi le convertit automatiquement en .BMP mémoire (DIB: Device Independent Bitmap), du moins pour la version
Delphi 6 que nous utilisons (sinon utilisez Google pour trouver un quelconque logiciel de conversion Delphi, ou même convertissez vous-même en utilisant PaintBrush).
3.3 - Chargement de .PNG
Pour les fichiers .PNG que nous utilisons pour notre site (toutes nos illustrations générées par EdFig sont en .PNG), nous utilisons une librairie d'importation de LibPng.DLL. Le chargement / sauvegarde des .PNG utilisant
cette librairie d'importation ont été décrits en grand détail pour notre lecteur de transparents.
Pour éviter d'inclure cette DLL dans le .ZIP, nous avons mis en commentaire les utilisations de cette unité dans le .ZIP du correcteur de couleur. Si vous souhaitez ajouter cette possibilité, téléchargez le .ZIP du lecteur de
transparents et installez-le comme indiqué dans le paragraphe téléchargement
3.4 - Modifications RGB La forme comporte trois TrackBar permettant d'ajuster chaque couleur
- à chaque déplacement, la correction indiquée par le curseur de chaque couleur est appliquée et la bitmap est ajustée. La transformation est réalisée depuis une copie de l'original, comme expliqué ci-dessus. La valeur
médiane correspond à la valeur de chargement, le déplacement vers la droite ajoute une valeur (à concurrence de 64) et le déplacement vers la gauche soustrait une valeur (à concurrence de -64)
- pour revenir à la situation initiale, cliquez sur le tPanel de la couleur (ou rechargez l'image)
- la checkbox situé à côté de la tTrackBar permet d'utiliser une correction
multiplicative: pour une valeur à droite de la médiane, chaque couleur sera multipliée par un puissance proportionnelle au déplacement (un courbe en cloche qui va donc tasser les points vers les valeur hautes). Pour une
valeur à gauche de la médiane, la puissance est négative, et les points seront donc tassés vers les valeurs faibles.
3.5 - Les histogrammes Les couleurs et leurs transformations dont analysées par 3 histogrammes. Ces
histogrammes répartissent en 255 catégories les points de l'image. Si les valeurs à analyser sont des entiers entre 0 et 255, les bins correspondent exactement aux valeurs à analyser. Sinon des règles de trois sont effectuées.
La classe est définie ainsi:
c_histogram_255= class(c_basic_object)
m_histogram: array[0..255] of Integer;
m_c_canvas: tCanvas;
m_pen_color: Integer;
m_max: Integer;
// -- draw min max limits
m_x_1, m_x_2: Integer;
Constructor create_histogram_255(p_name: String;
p_c_canvas: tCanvas; p_pen_color: Integer);
procedure reset_histogram;
procedure display_histogram;
function f_total_count: Integer;
procedure compute_max;
procedure rescale_histogram(p_y_max: Integer);
procedure draw_histogram(p_y_min, p_y_max: Integer);
procedure draw_histogram_with_border(p_y_offset,
p_fat: Integer);
procedure draw_vertical(p_x, p_y_offset,
p_fat, p_pen_color: Integer);
Destructor Destroy; Override;
end; // c_histogram_255 |
Outre le tableau de statistiques, la classe permet surtout de redimensionner les valeurs, et de dessiner les valeurs dans un intervalle y donné, ce qui nous permet de dessiner les courbes superposées. Voici un exemple pour notre image:
Si nous ajoutons du rouge à toutes les couleurs, nous obtenons:
En cliquant la tCheckBox, nous pouvons comprimer les rouges vers le haut: ou vers le bas:
Passons au mode HSV. Voici la situation de départ:
Voici un exemple de rotation des couleurs:
Pour la saturation, voici une translation de la saturation: Notez que la partie blanche de l'histogramme indique les valeur ayant dépassé le maximum de 1 à cause de la translation.
En comprimant la saturation dans la plage haute, nous obtenons:
A l'inverse, en saturant vers le bas, nous virons au gris:
Pour l'intensité, la compression haute fournit l'image que nous avons retenue (voir en début d'article). Pour tester notre modulation sur mesure, en utilisant les courbes de Bézier, nous avons effectuée deux tests:
Un premier consiste à essayer d'étaler les valeurs de l'histogramme: les petites valeurs sont réduites, les grandes agrandies. En gros une courbe en cloche Et une courbe en creux rassemblerait les points de l'histogramme:
3.6 - La couleur d'un point Lorsque nous déplaçons la souris au dessus de l'image, un cercle indique la valeur de la couleur sur une carte. Par exemple, lorsque nous passons au-dessus
des fleurs violettes situées au milieu à droite, nous obtenons:
Et lorsque nous déplaçons la souris sur cette carte d'image, une tBitmap
affiche quels points ont cette couleur. En nous plaçant au-dessus des rouges, nous obtenons:
Finalement nous pouvons utiliser un rectangle de sélection au-dessus de l'image
pour analyser la composition des points contenus dans cette image. Ici nous avons sélectionné (en tirant / glissant la souris) un rectangle au milieu du divan:
Voici une vue globale du projet:
4 - Télécharger les Sources Vous pouvez télécharger:
- rgb_hsv_color_correction.zip : le correcteur d'image (97 K)
- Librairie .PNG : lecture / écriture de
fichiers .PNG. Pour pouvoir utiliser des .PNG
- enlevez dans le programme principal les commentaires entourant les appels à la librairie .PNG (USES, lecture et écriture)
- téléchargez le .ZIP de lecture de transparents
- dézippez
- placez l'unité u_png_unit et u_png_dll_import dans xxx\colibri_utilities\graphicpng
- placez LibPng.Dll dans xxx\graphic\exe (le répertoire contenant l'EXE
de ce projet. ATTENTION: si la version de LibPng.DLL ne convient pas, ou que vous ne placez pas la .DLL avec l'EXE, vous aurez une erreur d'exécution au lancement du programme.
- vous pouvez aussi télécharger les images .BMP non comprimées de deux grands classiques de la correction d'image:
- parrots (193 K)
- mandrill (890 K)
Les .ZIP qui comprennent: - le .DPR, la forme principale, les formes annexes eventuelles
- les fichiers de paramètres (le schéma et le batch de création)
- dans chaque .ZIP, toutes les librairies nécessaires à chaque projet (chaque .ZIP est autonaume)
Ces .ZIP contiennent des chemins RELATIFS. Par conséquent:
- créez un répertoire n'importe où sur votre machine
- placez le .ZIP dans ce répertoire
- dézippez et les sous-répertoires nécessaires seront créés
- compilez et exécutez
Ces .ZIP ne modifient pas votre PC (pas de changement de la Base de Registre, de DLL ou autre). Pour supprimer le projet, effacez le répertoire.
Comme d'habitude:
- nous vous remercions de nous signaler toute erreur, inexactitude ou problème de téléchargement en envoyant un e-mail à jcolibri@jcolibri.com. Les corrections
qui en résulteront pourront aider les prochains lecteurs
- tous vos commentaires, remarques, questions, critiques, suggestion d'article, ou mentions d'autres sources sur le même sujet seront de même
les bienvenus à jcolibri@jcolibri.com.
- plus simplement, vous pouvez taper (anonymement ou en fournissant votre e-mail pour une réponse) vos commentaires ci-dessus et nous les envoyer en
cliquant "envoyer" :
- et si vous avez apprécié cet article, faites connaître notre site,
ajoutez un lien dans vos listes de liens ou citez-nous dans vos blogs ou réponses sur les messageries. C'est très simple: plus nous aurons de visiteurs et de références Google, plus nous écrirons d'articles.
5 - L'auteur John COLIBRI est passionné par le développement Delphi et les applications de Bases de Données. Il a écrit de nombreux livres et articles, et partage son temps entre le développement de projets (nouveaux projets, maintenance, audit, migration BDE, migration Xe_n, refactoring) pour ses clients, le
conseil (composants, architecture, test) et la
formation. Son site contient des articles
avec code source, ainsi que le programme et le calendrier des stages de formation Delphi, base de données, programmation objet, Services Web, Tcp/Ip et
UML qu'il anime personellement tous les mois, à Paris, en province ou sur site client. |