Accueil
Rechercher:
sur developpez.com sur les forums
Forums | Tutoriels | F.A.Q's | Participez | Hébergement | Contacts
Club Emploi Blogs   TV   Dév. Web PHP XML Python Autres 2D-3D-Jeux Sécurité Windows Linux PC Mac
Accueil Conception Java DotNET Visual Basic  C  C++ Delphi MS-Office SQL & SGBD Oracle  4D  Business Intelligence
FORUMS DELPHI F.A.Q DELPHI TUTORIELS DELPHI LIVRES COMPOSANTS SOURCES DEFI TELECHARGEZ DELPHI TV
Nono40.developpez.com
Le petit coin du web de Nono40
SOURCES ARTICLES NONOVISU ACCUEIL NOUVELLES

Utilisation de la propriété TBitMap.ScanLine

Date de publication : 16/11/2003 , Date de mise a jour : 16/11/2003

Par Earl F. Glynn (efg's computer lag)
 Nono40 ( Traduction ) (nono40.developpez.com)
 

Ce document est la traduction de la version anglaise de l'auteur disponible ici :
http://www.efg2.com/Lab/ImageProcessing/Scanline.htm

La propriété ScanLine a été introduite avec Delphi 3, elle permet un accès rapide à chaque pixel. Mais vous devez connaître le format de pixel utilisé (TBitMap.PixelFormat ) pour accéder correctement aux pixels. Cet article va montrer comment utiliser la propriété ScanLine pour traiter des pixels dans différentes applications graphiques ou autres. En premier lieu, chaque format de pixel va être décrit, accompagné d'un exemple. Après cette présentation théorique, plusieurs applications seront décrites, la plupart utilisant le format pf24bit. En dernier, quelques problèmes particuliers sur l'utilisation de ScanLine seront présentés.


Introduction
I. ScanLine et PixelFormat
I-A. Format pfCustom
I-B. Format pfDevice
I-C. Format pf1bit
I-D. Format pf4bit
I-E. Format pf8bit
I-F. Format pf15bit et pf16bit
I-G. Format pf24bit
I-H. Format pf32bits
I-I. Conversion de format
II. Autres notes sur ScanLine
III. Exemples utilisant ScanLine
III-A. Le programme Daisy
III-B. Le programme Split
III-C. Rotation d'une image
III-D. Permutation de couleurs dans un bitmap pf24bit
III-E. Autres exemples
IV. Utilisation inappropriée de ScanLine
V. Optimisation
V-A. Minimiser le nombre d'accès à ScanLines
V-B. Accéder à ScanLine en utilisant l'assembleur
V-C. Copie d'une zone de données vers ScanLines
VI. Problèmes inattendus avec l'utilisation de ScanLine ( Delphi 3 et 4)
VI-A. Corruption de l'image originelle suite à un Assign
VI-B. L'erreur d'assignation d'un pixel pf24bit ( rare bug de l'optimisation de Delphi )
VI-C. Erreur interne C1127 ( Delphi4 ) et C1141 ( Delphi 5 )
VI-D. Présence simultanée des sections DDB et DIB sur le même bitmap
Conclusion
Références


Introduction

Delphi 1 et 2 fournissent une propriété Pixels pour accéder à chaque pixel d'un Canvas. Mais cette méthode d'accès est très lente. Par exemple, le code donné ci-dessous montre comment effectuer une rotation de 90° sur un bitmap en utilisant Pixels. Ceci fonctionne bien sur de petits bitmaps (RotatePixels Lab Report) , mais est beaucoup trop long pour des bitmaps plus larges. Par contre, la propriété Pixels permet d'effectuer des traitements sur toutes les tailles de pixels avec le même code.

Listing 1 : Rotation d'un bitmap en utilisant la propriété Canvas.Pixels
WITH ImageFrom.Canvas.ClipRect DO BEGIN FOR i := Left TO Right DO FOR j := Top TO Bottom DO ImageTo.Canvas.Pixels[j,Right-i-1] := ImageFrom.Canvas.Pixels[i,j] END;
Avec Delphi 1 et 2, l'alternative à l'utilisation de Pixels est l'appel de fonctions APIs pour accéder aux données des pixels directement ( comme la fonction GetDIBits ). Mais accéder aux données d'un DIB ( Device Independent Bitmap ) est plus compliqué. Un fois que les données de DIB sont créées, il faut souvent les convertir de nouveau en Bitmap pour pouvoir les afficher dans un TImage. Les propriétés ScanLine et PixelFormat de Delphi 3 offrent une alternative intéressante.


I. ScanLine et PixelFormat

ScanLine contient les données des pixels ; mais, avant d'accéder aux pixels, vous devez connaître le format de ScanLine en mémoire en utilisant la propriété PixelFormat. Les valeurs possibles de PixelFormat sont définies dans l'unité GRAPHICS.PAS et sont pfCustom, pfDevice, pf1bit, pf4bit, pf8bit, pf15bit, pf16bit, pf24bit, and pf32bit.

Note pour Kylix : Kylix (K1 - K3) supporte seulement les formats suivants : pf1bit, pf8bit, pf16bit, pf32bit, pfCustom (définies dans QGraphics.pas). Dans Kylix ScanLine est défini sur un QImage et non un QPixMaps. La première fois que vous appelez ScanLine, le Pixmap est converti en QImage, ce qui est une perte de temps.
Voir UseNote Post by Mattias Thoma à propos de cette conversion.

Quel que soit le format de pixel, chaque ligne de données est alignée sur une frontière de double-mot.


I-A. Format pfCustom

Quand Delphi ne peut pas déterminer le PixelFormat d'un BitMap il affecte le format à pfCustom. Si vous essayez d'affecter pfCustom à PixelFormat une exception EInvalidGraphic est générée.
( Voir Colin Wilson's UseNet Post about cases involving pfCustom. )


I-B. Format pfDevice

Si vous créez un TBitmap et que vous n'affectez ni PixelFormt et ni HandleType, un DDB ( Device Dependent Bitmap ) est créé avec un PixelFormat de pfDevice. Le HandleType sera bmDDB. Si vous fixez HandleType à bmDIB, ou si vous fixez PixelFormat ( autre que pfDevice ), vous obtenez un DIB ( Device Independent Bitmap ).


I-C. Format pf1bit

Vous pouvez économiser une grande quantité de mémoire si vous n'avez besoin que d'un seul bit par pixel ( comme les différents masques ), mais vous devez vous faire aux manipulations de bits pour accéder aux pixels. Avec seulement un seul bit d'information pour chaque pixel, les couleurs "normales" d'un bitmap pf1bit sont le noir et le blanc. Une palette de deux couleurs peut tout de même être définie pour afficher deux couleurs quelconques.

Utilisez un TByteArray, défini dans l'unité SysUtils.PAS, pour accéder aux données :

pByteArray = ^TByteArray; TByteArray = ARRAY[0..32767] OF BYTE;
La taille en octets d'un ligne de ScanLine pf1bit est BitMap.Width Div 8 si la taille est multiple de 8. Si la taille n'est pas multiple de 8, utilisez la formule suivante pour calculer le nombre d'octets : 1 + (Bitmap.Width - 1) DIV 8.

L'utilisation d'un TByteArray de SysUtils limite la taille d'un Bitmap pf1bit à 32768x32768 pixels, ce qui n'est pas vraiment une restriction, étant donné que ceci demande plus de mémoire que celle disponible dans les ordinateurs actuels. ( Actuellement, les très grands Bitmaps posent un problème sous Windows 95/98. Les limitations sont moindres avec Windows NT/2000. Voir Very Large Bitmap Lab Report for details. ) Je préfère cette construction à celle de Borland qui est un ARRAY[0..0] nécessitant de désactiver le contrôle d'étendue pour toutes les variables. ( Voir Borland FAQ 890D pour un exemple utilisant cette méthode que je considère comme une utilisation maladroite des variables de compilation conditionnelle. )

Le pf1bit Lab Report montre comment créer et travailler avec un Bitmap pf1bit, y compris comment ajouter une palette de deux couleurs. Dans cet essai, les propriétés Tag des boutons noir, blanc et rayé sont respectivement $00, $FF et $55 ( 01010101 en binaire ). Quand un bouton est appuyé, chaque octet de chaque ligne de ScanLine est modifié avec cette valeur. L'affichage est effectué en assignant le bitmap à ImageBits.Picture.Graphic. Voir "listing 2"

Listing 2. Remplir un bitmap pf1bit avec un masque fixe
procedure TFormPf1bit.ButtonTagFillClick(Sender: TObject); VAR Bitmap: TBitmap; i : INTEGER; j : INTEGER; Row : pByteArray; Value : BYTE; begin // Value = $00 = 00000000 en binaire pour le noir // Value = $FF = 11111111 en binaire pour le blanc // Value = $55 = 01010101 en binaire pour les rayures Value := (Sender AS TButton).Tag; Bitmap := TBitmap.Create; TRY WITH Bitmap DO BEGIN Width := 32; Height := 32; // Etrange, pourquoi cette ligne doit suivre Width/Height pour // que le code fonctionne ? Dans le cas contraire, le bitmap // est toujours noir. PixelFormat := pf1bit; IF CheckBoxPalette.Checked THEN Bitmap.Palette := GetTwoColorPalette END; FOR j := 0 TO Bitmap.Height-1 DO BEGIN Row := pByteArray(Bitmap.Scanline[j]); FOR i := 0 TO (Bitmap.Width DIV BitsPerPixel)-1 DO BEGIN Row[i] := Value END END; ImageBits.Picture.Graphic := Bitmap FINALLY Bitmap.Free END end;
Voici la réponse de Danny Thorpe's (Borland R&D) concernant le point "étrange" dans le commentaire du listing 2 :
Une réponse partielle est que la séquence change quand le Handle du bitmap est créé. Dès que le Bitmap a une largeur ou une hauteur non nulle, le Handle est créé. Affecter le PixelFormat après la taille provoque la création d'un nouveau handle avec le format de pixel demandé. Affecter PixelFormat avant la taille, enregistre l'information jusqu'a ce que le handle soit créé.
Ceci serait bien s'il n'y avait pas dans le code de création d'un DIB monochrome une référence à un champ non initialisé de la structure DIB... SrcDIB.dsbm.bmBits = nil n'est pas le cas quand la routine interne CopyBitMap est appelée pour créer un nouveau bitmap ( Source Handle = 0, SrcDIB non initialisé ). Ceci sera corrigé à partir de Delphi 6.

Pour créer le "g" affiché dans pf1bit Lab Report ou pour créer une flèche, les octets d'un tableau de constantes sont copiés dans ScanLine. Les valeurs suivantes sont celles utilisées pour les deux premières ligne du bitmap "g" :

$00, $FC, $0F, $C0 $07, $FF, $1F, $E0 00000000 11111100 00001111 11000000 00000111 11111111 00011111 11100000
Voir le code du bouton "ButtonG" pour plus de détails.

Le bouton "invert" effectue une inversion bit à bit du bitmap. La boucle qui inverse chaque ligne de ScanLine fonctionne comme suit :

Inverser un bitmap pf1bit
FOR j := 0 TO Bitmap.Height-1 DO BEGIN RowOut := pByteArray(Bitmap.Scanline[j]); RowIn := pByteArray(ImageBits.Picture.Bitmap.Scanline[j]); FOR i := 0 TO (Bitmap.Width DIV BitsPerPixel)-1 DO BEGIN RowOut[i] := NOT RowIn[i] END END;
Le bitmap pf1bit possède une palette ! Voir pf1bit Lab Report

Pour un bitmap pf1bit, un fois que le Bitmap est créé il faut absolument affecter Width et Height avant PixelFormat. Dans la cas contaire le bitmap pf1bit sera seulement affiché en noir. ( Cette énigme du bitmap pf1bit est décrite dans un post UseNet )


I-D. Format pf4bit

Travailler avec des bitmaps pf4bit est compliqué par le fait que chaque pixel n'est qu'une partie d'un octet. Comme pour les bitmaps pf1bit, il faudra travailler avec les bits pour les bitmaps pf4bit. Avec 4 bits par pixel, il est possible d'utiliser 16 couleurs. Le plus souvent elles correspondent au standard VGA 16 couleurs. Les palettes ne sont qu'une complication supplémentaire en travaillant avec des bitmaps pf4bit.

Voici un exemple simple d'utilisation d'un bitmap pf4bit :

// Affichage de barre de 16 couleurs dans un bitmap pf4bit // efg, 6 October 1998 procedure TForm1.ButtonPf4bitClick(Sender: TObject); VAR Bitmap: TBitmap; i,j : INTEGER; m : INTEGER; row : pByteArray; // Chaque pixel est un morceau (1/2 octet ) begin Bitmap := TBitmap.Create; TRY Bitmap.Width := Image1.Width; Bitmap.Height := Image1.Height; // Doit être un multiple de 16 Bitmap.PixelFormat := pf4bit; // 16 couleurs FOR j := 0 TO Bitmap.Height-1 DO BEGIN m := j DIV 16; // 16 bands : 0 .. 15 row := Bitmap.Scanline[j]; FOR i := 0 TO Bitmap.Width DIV 2 - 1 DO // 2 pixels par octet BEGIN row[i] := m + {pixel du morceau bas} (m SHL 4); {pixel du morceau haut} END END; Image1.Picture.Graphic := Bitmap FINALLY Bitmap.Free END end;
Pour voir comment un bitmap, défini par un tableau de constantes, peut être utilisé comme un pinceau ( TBrush.Bitmap ) regardez the Brush Bitmaps Lab Report.

Pour un autre exemple, regardez Combine pf4bit Bitmaps Lab Report montrant comment créer un pf8bit ou pf24bit à partir de deux bitmaps pf4bits. Une partie de cet exemple est décrit dans le chapitre suivant sur les bitmaps pf8bit.


I-E. Format pf8bit

Travailler avec un bitmap pf8bit est très facile, chaque pixel occupant un octet ils peuvent être utilisés via un TByteArray. Le listing 4 montre les boucles For imbriquées pour affecter tous les pixels d'un bitmap pf8bit. ( ce code est une partie du code CycleColors Lab Report, qui sera présenté plus loin.)

Listing 4 : Affectation des pixels d'un pf8bit en utilisant TByteArray
VAR i : INTEGER; j : INTEGER; Row: pByteArray . . . FOR j := 0 TO BitmapBase.Height-1 DO BEGIN Row := BitmapBase.ScanLine[j]; FOR i := 0 TO BitmapBase.Width-1 DO BEGIN Row[i] := <pixel value 0..255>; // Affecter un index de palette END END; ImageShow.Picture.Graphic := BitmapBase;
Travailler avec des bitmaps pf8bit est facile, la valeur octet affectée à un point d'une ligne représente la couleur du point mais indirectement. Cette valeur est un index dans une palette de couleur. La palette contient les composantes R, V et B de chaque couleur.

J'ai présenté la méthode d'accès des bitmap pf8bit simplement pour information. Habituellement j'utilise des bitmaps pf24bits permettant de contrôler la couleur d'un point plus facilement qu'avec la complexité des palettes dans Windows. ( voir l'exemple ci-dessous )

Le listing 5 montre comment copier les valeurs d'un bitmap pf4bit vers un bitmap pf8bit. Voir le Combine pf4bit Bitmaps Lab Report pour les détails concernant la méthode pour regrouper les deux palettes des bitmap pf4bit dans la palette du bitmap pf8bit.

Listing 5 : copier les données d'un bitmap pf4bit dans un bitmap pf8bit
// A partir d'un bitmap pf4bit nommé Bitmap4, transférer les données // vers un bitmap pf8bit nommé Bitmap8. VAR Bitmap4: TBitmap; // pf4bit Bitmap Bitmap8: TBitmap; // pf8bit Bitmap i : INTEGER; j : INTEGER; Row4 : pByteArray; // pf4bit Scanline Row8 : pByteArray; // pf8bit Scanline ... Bitmap8 := TBitmap.Create; TRY Bitmap8.Width := Bitmap4.Width; Bitmap8.Height := 2 * Bitmap4.Height; Bitmap8.PixelFormat := pf8Bit; // Copie des Scalines pf4bit vers les Scanlines pf8bit FOR j := 0 TO Bitmap4.Height-1 DO BEGIN Row4 := Bitmap4.Scanline[j]; // Scanline origine Row8 := Bitmap8.Scanline[j]; // Scanline destination // Width[Bytes] = Width[Pixels] / 2 pour un Bitmap pf4bit // On suppose que la largeur est paire. FOR i := 0 TO (Bitmap4.Width DIV 2)-1 DO BEGIN Row8[2*i ] := Row4[i] DIV 16; Row8[2*i+1] := Row4[i] MOD 16 END END; Image8.Picture.Graphic := Bitmap8; FINALLY Bitmap8.Free END
Voici un autre exemple : comment utiliser GetPaletteEntries et convertir un Bitmap pf8bit vers un Bitmap pf24bit. Cet exemple est présenté seulement à titre de démonstration, car pour convertir un bitmap 8 bits en bit map 24 bits, il suffit d'affecter une nouvelle valeur à la propriété PixelFormat.

Listing 5 : copier les données d'un bitmap pf4bit dans un bitmap pf8bit
// efg, 24 August 1998 unit ScreenGetPaletteEntries; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ExtCtrls; type TForm1 = class(TForm) Image1: TImage; ButtonReadpf8bit: TButton; Memo1: TMemo; Image2: TImage; procedure ButtonReadpf8bitClick(Sender: TObject); private { Private declarations } public { Public declarations } end; var Form1: TForm1; implementation {$R *.DFM} CONST MaxPixelCount = 65536; TYPE TRGBArray = ARRAY[0..MaxPixelCount-1] OF TRGBTriple; pRGBArray = ^TRGBArray; procedure TForm1.ButtonReadpf8bitClick(Sender: TObject); VAR i : CARDINAL; index : BYTE; j : CARDINAL; LogicalPalette : TMaxLogPalette; PaletteEntryCount: CARDINAL; Bitmap8 : TBitmap; Bitmap24 : TBitmap; Row8 : pByteArray; Row24 : pRGBArray; begin Bitmap8 := TBitmap.Create; TRY Bitmap8.LoadFromFile('Deer.BMP'); // Affichage du Bitmap pf8bit Image1.Picture.Graphic := Bitmap8; IF Bitmap8.PixelFormat <> pf8bit THEN ShowMessage('Format attendu : 8-bits/pixel BMP'); IF Bitmap8.Palette = 0 THEN ShowMessage ('Pas de palette avec le BMP') ELSE BEGIN PaletteEntryCount := GetPaletteEntries(Bitmap8.Palette, 0, 255, LogicalPalette.palPalEntry); FOR i := 0 TO PaletteEntryCount-1 DO BEGIN Memo1.Lines.Add(Format('%3.3d %3.3d %3.3d %3.3d %d', [i, LogicalPalette.palPalEntry[i].peRed, LogicalPalette.palPalEntry[i].peGreen, LogicalPalette.palPalEntry[i].peBlue, LogicalPalette.palPalEntry[i].peFlags])) END; Bitmap24 := TBitmap.Create; TRY Bitmap24.Width := Bitmap8.Width; Bitmap24.Height := Bitmap8.Height; Bitmap24.PixelFormat := pf24bit; FOR j := 0 TO Bitmap8.Height-1 DO BEGIN Row8 := Bitmap8.Scanline[j]; Row24 := Bitmap24.Scanline[j]; FOR i := 0 TO Bitmap8.Width-1 DO BEGIN index := Row8[i]; // index dans la palette WITH Row24[i] DO BEGIN rgbtRed := LogicalPalette.palPalEntry[index].peRed; rgbtGreen := LogicalPalette.palPalEntry[index].peGreen; rgbtBlue := LogicalPalette.palPalEntry[index].peBlue END END END; // Sauvegarde dans la nouveau BMP Bitmap24.SaveToFile('Deer24.BMP'); // Affichage du BMP 24-bit (Sans palette maintenant) Image2.Picture.Graphic := Bitmap24; FINALLY Bitmap24.Free END END FINALLY Bitmap8.Free END end; end.
Bitmaps pf8bit en mémoire.
Vous pouvez créer un bitmap pf8bit en mémoire, avec sa palette, mais l'apparence de ce bitmap peut dépendre du mode vidéo actif. Si vous utilisez un mode couleurs 15/16 bits ou couleurs vrais (24/32 bits ) il est possible de voir toutes les couleurs de la palette 256 couleurs d'un bitmap pf8bit. ( voir image suivante )

Cependant, l'affichage du même bitmap dans un mode 256 couleurs, va perdre 20 des 256 couleurs de la palette comme le montre l'image suivante.

Windows prend 20 des 256 couleurs pour l'affichage des composants en mode 256 couleurs. Le reste des 236 couleurs peut être utilisé par l'application.

Cet exemple de palette 8bits peut être téléchargé ici pour vos propres essais ( Delphi 3, 4 et 5 ). Faites attention, lorsque vous créez des bitmaps pf8bit avec leur palette, que leur affichage sera dans un mode vidéo "couleurs vraies".

Voir UseNet Post sur la méthode de création d'un bitmap avec un dégradé de 256 niveaux de gris.

Voir le mail de Dean Verhoeven sur l'utilisation de SetDIBColorTable pour modifier la palette d'un bitmap pf8bit.

Pour avoir des informations complémentaires sur les palettes, voir Color, Section B de la page Delphi Graphics Algorithms.


I-F. Format pf15bit et pf16bit

Tous les formats de pixel ne sont sont pas forcément disponibles sur tous les PC. Par exemple pf15bit n'est pas disponible sur certaines machines à cause de limitations de la carte vidéo ou de son driver. Voici une méthode pour vérifier que le format pf15bit est supporté :

BEGIN bitmap := TBitmap.Create; TRY bitmap.Width := 32; bitmap.Height := 32; bitmap.PixelFormat := pf15Bit; IF bitmap.PixelFormat <> pf15bit THEN ShowMessage('pf15bit non supporté !'); ...
En cas de doute il faut vérifier que le format de pixel créé est correct. Apparemment PixelFormat prend la valeur pfCustom si le format demandé n'est pas disponible. Voir le UseNet Post de Robert Rossmair sur la possibilité de contourner le problème. Et aussi le UseNet Post de Colin Wilson sur les cas de pf15bit/pf16bit aboutissant à pfCustom.

Utilisez le type pWordArray défini dans SysUtils.pas pour accéder aux pixels d'un bitmap pf15bit ou pf16bit. L'utilisation de ce type limite la taille de l'image à 16384x16384 mais ce n'est pas vraiment une limitation, car la création d'un tel bitmap va prendre toutes les ressources sytèmes, particulièrement sous Windows 95/98.

La disposition des bits dans un mot, pour chaque pixel, est le suivant :

pf15bit: 0 rrrrr vvvvv bbbbb pf16bit: rrrrr vvvvvv bbbbb
Un pixel pf15bit a 5 bits pour chaque couleur R, V et B. Par contre, un pixel pf16bit possède un bit de plus pour la couleur verte. Le vert a un bit de plus car l'oeil est plus sensible au vert qu'au bleu et rouge.

Le listing 6 montre comment créer un bitmap pf15bit rempli avec des pixels jaunes. Notez qu'un pixel avec rouge=31, vert=31 et bleu=0 est jaune.

Listing 6 : Création d'un bitmap pf15bit jaune
VAR Bitmap: TBitmap; i : INTEGER; j : INTEGER; R : 0..31; // chaque composante RVB a 5 bits G : 0..31; B : 0..31; RGB : WORD; Row : pWordArray; // de SysUtils ... Bitmap := TBitmap.Create; TRY Bitmap.Width := Image1.Width; Bitmap.Height := Image1.Height; Bitmap.PixelFormat := pf15bit; R := 31; // Max Red G := 31; // Max Green B := 0; // No Blue // "FillRect" utilisant ScanLine // Les bits dans chaque pixels sont diposés selon : // 0rrrrrvvvvvbbbbb. Le bit de poids fort est ignoré RGB := (R SHL 10) OR (G SHL 5) OR B; // "Jaune" FOR j := 0 TO Bitmap.Height-1 DO BEGIN Row := Bitmap.Scanline[j]; FOR i := 0 TO Bitmap.Width-1 DO Row[i] := RGB END; Image1.Picture.Graphic := Bitmap FINALLY Bitmap.Free END
Voici un exemple similaire pour un bitmap pf16bit mais avec R=31 et V=63.

procedure TForm1.ButtonFillYellowPf16bitClick(Sender: TObject); VAR Bitmap: TBitmap; i : INTEGER; j : INTEGER; R : 0..31; // 5 bits G : 0..63; // 6 bits (l'oeil est plus sensible au vert) B : 0..31; // 5 bits RGB : WORD; Row : pWordArray; // de SysUtils begin Bitmap := TBitmap.Create; TRY Bitmap.Width := Image1.Width; Bitmap.Height := Image1.Height; Bitmap.PixelFormat := pf16bit; R := 31; // Max rouge G := 63; // Max vert B := 0; // Pas de bleu // "FillRect" en utilisant ScanLine RGB := (R SHL 11) OR (G SHL 5) OR B; // "Jaune" FOR j := 0 TO Bitmap.Height-1 DO BEGIN Row := Bitmap.Scanline[j]; FOR i := 0 TO Bitmap.Width-1 DO Row[i] := RGB END; Image1.Picture.Graphic := Bitmap FINALLY Bitmap.Free END end;
Lors de la conversion d'un bitmap pt15bit ( 5 bits par couleur ) vers un bitmap pf24bit ( 8 bits par couleur ) que se passe-t-il avec les trois bits supplémentaires ? Voir le UseNet Post de efg à ce sujet. La conversion n'est pas déterminable. Le résultat dépend de la carte vidéo.

Suivant le UseNet Post de Ian Martin, Photoshop ne lit pas correctement les bitmaps pf16bits générés par Delphi. La solution de Ian est d'utiliser plutôt des bitmaps pf24bits.

Voir aussi le UseNet Post de Paul Nicholls sur l'utilisation de BASM pour modifier un bitmap pf16bit.


I-G. Format pf24bit

Pour les bitmaps pf24Bit je définis ( j'espère que Borland le ferait aussi ) le type suivant dans le listing 7. Il est similaire au type TByteArray.

Listing 7 : Définition de TRGBTripleArray
CONST PixelCountMax = 32768; TYPE pRGBTripleArray = ^TRGBTripleArray; TRGBTripleArray = ARRAY[0..PixelCountMax-1] OF TRGBTriple;
L'utilisation d'une valeur importante pour PixelCountMax a deux avantages. Aucun bitmap ne peut être créé de cette taille pour le moment et ceci permet de conserver le contrôle d'étendue sur la propriété ScanLine.

Comme la définition du listing 7 met des limites pour l'étendue de l'index ( vous pouvez réduire cette étendue ), la définition suivante est sans doute plus simple :

TYPE TRGBTripleArray = ARRAY[WORD] OF TRGBTriple;
Borland définit un TRBGTripleArray dans l'unité Graphics.pas mais c'est un ARRAY[BYTE] of TRGBTriple.

Commentaire de Danny Thorpe (Borland R&D) :
Ceci est fait pour l'utilisation dans le traitement des palettes de couleurs. Ce n'est pas fait pour accéder aux pixels. Vous remarquerez que dans les évolutions de D1 à D4, j'ai éliminé beaucoup de réservations de mémoire tampon dans les routines des bitmaps. L'un des plus grands gains de temps était de ne pas allouer de la mémoire temporaire pour les palettes de couleurs. A la place juste une variable locale sur la pile de la taille maximum possible dont j'avais besoin : 256 RGBTriples. Ceci prend entre 700 et 1000 octets dans la pile mais c'est négligeable sous Win32, et l'espace est récupéré en quelques nanosecondes.
Ceci ne fonctionnerait pas avec un TRGBTriple sur une étendue WORD. Même si cela tient dans la pile, ceci resterait trop consommateur.

L'utilisation du type TRGBTripleArray de Borland poserait des problèmes même pour un bitmap d'une taille "normale", car l'étendue doit être entre 0 et 255. La définition que je propose n'a pas cette limitation.

La définition de Borland dans Windows.pas de TRGBTriple est la suivante :

pRGBTriple = ^TRGBTriple; TRGBTriple = PACKED RECORD rgbtBlue : BYTE; rgbtGreen: BYTE; rgbtRed : BYTE; END;
Le listing 8 montre comment créer un bitmap pf24bit dont les pixels sont jaunes. Notez que les pixels avec rouge=255, vert=255 et bleu=0 sont jaunes.

Listing 8A : Création d'un bitmap pf24bit jaune
VAR i : INTEGER; j : INTEGER; Row: pRGBTripleArray; ... Bitmap := TBitmap.Create; TRY Bitmap.PixelFormat := pf24bit; Bitmap.Width := ImageRGB.Width; Bitmap.Height := ImageRGB.Height; FOR j := 0 TO Bitmap.Height-1 DO BEGIN Row := Bitmap.Scanline[j]; FOR i := 0 TO Bitmap.Width-1 DO BEGIN WITH Row[i] DO BEGIN rgbtRed := 255; // pixels jaunes rgbtGreen := 255; rgbtBlue := 0; END END END; // Affichage à l'écran ImageRGB.Picture.Graphic := Bitmap; FINALLY Bitmap.Free END
à partir de Delphi 4.02 ou supérieur, on a une approche simplifiée en utilisant une constante TRGBTriple :

Linsting 8B : avec l'utilisation de CONST TRGBTriple
CONST Yellow: TRGBTriple = (rgbtBlue: 0; rgbtGreen: 255; rgbtRed: 255); VAR i : INTEGER; j : INTEGER; Row: pRGBTripleArray; ... Bitmap := TBitmap.Create; TRY Bitmap.PixelFormat := pf24bit; Bitmap.Width := ImageRGB.Width; Bitmap.Height := ImageRGB.Height; FOR j := 0 TO Bitmap.Height-1 DO BEGIN Row := Bitmap.Scanline[j]; FOR i := 0 TO Bitmap.Width-1 DO BEGIN Row[i] := Yellow END END; // Affichage à l'écran ImageRGB.Picture.Graphic := Bitmap; FINALLY Bitmap.Free END
Mais comment un TColor peut-il être converti en TRGBTriple ?

FUNCTION ColorToRGBTriple(CONST Color: TColor): TRGBTriple; BEGIN WITH RESULT DO BEGIN rgbtRed := GetRValue(Color); rgbtGreen := GetGValue(Color); rgbtBlue := GetBValue(Color) END END {ColorToRGBTriple};
Faites attention que TColor peut avoir différents formats internes. Le code ci-dessus suppose que le format est $00BBVVRR qui peut être créé en utilisant les fonctions RGB.

Comment un bitmap pf24bit peut-il être converti en un bitmap pf15bit ?
Considérons la méthode "brute" avec ScanLine et la méthode "simple" avec PixelFormat. Voir le listing 9.

Listing 9 : Convertir un bitmap pf24bit en bitmap pf15bit
VAR Bitmap15: TBitmap; Bitmap : TBitmap; i : INTEGER; j : INTEGER; Row15 : pWordArray; Row24 : pRGBTripleArray; ... Bitmap := TBitmap.Create; TRY Bitmap.LoadFromFile('N:\Images\Flowers\Tulip3.BMP'); // Conversion en utilisant la méthode "brute" Bitmap15 := TBitmap.Create; TRY Bitmap15.Width := Bitmap.Width; Bitmap15.Height := Bitmap.Height; Bitmap15.PixelFormat := pf15bit; FOR j := 0 TO Bitmap.Height-1 DO BEGIN Row15 := Bitmap15.Scanline[j]; Row24 := Bitmap.Scanline[j]; FOR i := 0 TO Bitmap.Width-1 DO BEGIN WITH Row24[i] DO Row15[i] := (rgbtRed SHR 3) SHL 10 OR (rgbtGreen SHR 3) SHL 5 OR (rgbtBlue SHR 3) END END; Bitmap15.SaveToFile('Tulip3-15A.BMP'); FINALLY Bitmap15.Free END; // Conversion en utilisant la méthode "simple" ASSERT(Bitmap.PixelFormat = pf24bit); // verifie pf24bit Bitmap.PixelFormat := pf15bit; // L'affection effectue la conversion Bitmap.SaveToFile('Tulip3-15B.BMP') FINALLY Bitmap.Free END // Tulip3-15A.BMP et Tulip3-15B.BMP doivent être identiques.
Dans l'exemple ci-dessus, les deux bitmaps sont identiques en comparant pixel à pixel. Par contre, ils peuvent avoir un CRC32 différent, les fichiers ne seront donc pas identiques. Sans doute à cause d'un en-tête différent entre les deux fichiers.

Une meilleure approche dans la réduction de 24-bits par pixel à 15/16 bits par pixel est donnée par Phil McRevis dans un UseNet post ou il suggère d'utiliser la méthode d'arrondi Floyd-Steinberg.


Voir aussi le UseNet post d'efg sur la méthode pour retourner un bitmap en écrivant les lignes dans un TMemoryStream puis en lisant les lignes dans un second bitmap dans l'ordre inverse.


I-H. Format pf32bits

Analogue au TRBGTriple, voici la définition d'un TRGBQuadArray pour travailler avec les bitmaps pf32bit. Voir listing 10.

Définition de TRGBQuadArray
CONST PixelCountMax = 32768; TYPE pRGBQuadArray = ^TRGBQuadArray; TRGBQuadArray = ARRAY[0..PixelCountMax-1] OF TRGBQuad;
Une définition simplifiée, sans utiliser PixelCountMax, serait :

TYPE TRGBQuadArray = ARRAY[WORD] OF TRGBQuad;
La définition de Borland, dans Windows.pas du TRGBQuad est la suivante :

pRGBQuad = ^TRGBQuad; TRGBQuad = PACKED RECORD rgbBlue : BYTE; rgbGreen: BYTE; rgbRed : BYTE; rgbReserved: BYTE END;
Tant que l'on parle des informations de couleur, notez que TRGBQuad est équivalent à TRGBTriple. Les deux types possèdent 24 bits pour le codage de la couleur : 8 pour le rouge, 8 pour le vert et 8 pour le bleu. Le TRGBQuad possède un octet réservé supplémentaire qui est parfois appelé octet "alpha" ( surtout dans le monde du Mac ). Cet octet est utilisé par certaines applications pour stocker un masque de niveaux de gris. Mais il n'y a aucune information fiable sur le contenu de cet octet "alpha". Ce contenu n'est pas prévisible. Voir le UseNet post de Stan ou "Vous ne pouvez jamais connaître le contenu de l'octet alpha".

Commentaire de Danny Thorpe (Borland R&D) :
NT supporte à peu près n'importe quelle disposition des bits RGB. La seule limitation est que les bits de chaque couleur doivent être contigus. Il peut être facile de lire un fichier RVB de type big-indian ( comme les fichiers RAW de Sun ou autre ) simplement en créant le DIBsection avec les masques BVR. Juste pour rire, j'ai créé une fois un DIBSection de 32bpp avec une information de couleur sur 24 bits, puis j'ai créé un autre DIBSection en utilisant le même tampon pour les pixels, mais en plaçant les données dans l'octet "alpha" réservé pour les données des points 32bpp.

Une utilisation de ScanLine d'un bitmap pf32bit est de créer un tableau dynamique de valeur Single issues d'un calcul numérique. Ce genre de tableau peut être sauvé sur le disque en utlisant la méthode SaveToFile et lu en utilisant la méthode LoadFromFile. Ces nombres peuvent être "affichés" dans un TImage ou chaque pixel représente une fraction. L'octet réservé du TRGBQuad correspond à la partie exposant du type Single et n'influera pas sur l'image. Les images ainsi créées, à partir de données scientifiques, peuvent montrer des dessins très intéressants. Voir l'exemple d'utilisation d'un bitmap pf32bit pour contenir et afficher des valeurs Single IEEE issues de Lyapunov Exponents Lab Report le Fractals Show 2 Lab Report montre comment utiliser un bitmap pf32bit comme une matrice d'entiers de quatre octets.

Dans certains tests l'utilisation d'un bitmap pf32bit est environ 5% plus rapide qu'un bitmap pf24bit. Sans doute à cause de l'alignement des points sur une limite de 32 bits. Donc l'utilisation de 24 bits par pixel augmente légèrement le temps de calcul, mais pas suffisamment en comparaison de l'espace récupéré entre le format pf32bit et pf24bit.

Voir aussi :
-l'exemple de Ken Florentino sur l'utilisation de bitmaps pf32bit en assembleur.
-Le UseNet post d'efg sur "Enigme pf32bit : 'alpha byte' indéterminé dans un nouveau bitmap ?".
-Kylix Nota : Le UseNet post pour un exemple de pf32bit avec Kylix.
-Le UseNet post d'efg sur "Comment transtyper un TColor décalé en TRGBQuad ?"

VAR Color : TColor; yellow: TRGBQuad; .. Color := clYellow; yellow := TRGBQuad(Color SHL 8);
En partant d'un TColor avec un format interne de $00BBVVRR, si vous le décalez de 8 bits vers la gauche, vous obtenez $BBVVRR00, ce qui correspond au format interne d'un TRGBQuad. Un transtypage permet ensuite l'affectation du décalage à un TRGBQuad.


I-I. Conversion de format

Affecter une nouvelle valeur à PixelFormat pour effectuer une conversion d'un format de pixel à un autre. Ceci fonctionne bien pour la conversion d'un format de pixel bas vers un format de pixel plus important ou pour la conversion de PixelFormat n'ayant pas à traiter des palettes ( ex : pf15bit vers/depuis pf24bit ). Mais affecter un nouveau PixelFormat ne garantit pas que la palette obtenue soit correcte si elle est utile.

Un bitmap pf24bit peut avoir des milliers de couleurs. Par exemple le Mandrill monkey bitmap est un tableau 512x512=262144 pixels, mais a 230427 couleurs différentes ! ( L'utilitaire Show Image donne le nombre de couleurs différentes d'une image dans le coin en bas à gauche de l'écran ). En mode 256 couleurs ( pf8bit ), Windows réserve normalement 20 couleurs pour l'affichage des boutons, panneaux, icônes, etc... , laissant seulement 236 couleurs pour l'application. Pour afficher l'image "Mandrill monkey" un algorithme est nécessaire pour déterminer les 236 meilleures couleurs des 230427 de l'image.

Regardez la démo Show Demo One Lab Report pour voir un algorithme qui crée une palette pour afficher une image de 24 bits par pixel dans un mode 256 couleurs. Cet exemple prend chaque TRGBTriple dans chaque ScanLine et recherche la couleur de la palette la plus approchante, il n'effectue pas une conversion d'un bitmap pf24bit en bitmap pf8bit.

Vous êtes très chanceux si vos images pf24bit s'affichent correctement en mode 256 couleurs.


II. Autres notes sur ScanLine

Question( UseNet post de Paul Nicholls ) : "Si je veux sauver des pointeurs issus de ScanLine d'un bitmap dans des variables, quand dois-je mettre à jour ces variables pour que les pointeurs soient de nouveau valides ?"
Réponse( UseNet post de Steve Schafer ) : "Je ne pense pas qu'il soit conseillé de garder une copie des pointeurs ScanLine. Même si ça peut être fait en sécurité dans certaines circonstances et sur une version de Delphi, il peut être dangereux de le faire dans les mêmes circonstances dans le futur."
"Obtenez un pointeur ScanLine, utilisez-le et jetez-le !"

UseNet post de Peter Haas sur l'utilisation de Bitmap.Dormant après l'affectation de PixelFormat pour corriger un problème dans Delphi 1 à 4 avec le TBitmapInfoHeader.

UseNet post de Finn Tolderlund sur ScanLine et FreeImage.

Exemples de Eric Sibert sur les bitmaps pf24bit et pf32bit lors de la migration vers Kylix.
http://esibert.developpez.com/kylix/migration/image/image.htm

UseNet post de Matthijs Laan sur l'utlilisation de MMX pour effectuer un XOR sur un bitmap.

UseNet post de Andrew Rybenkov sur l'utilisation de GetDIBits/SetDIBits comme alternative à ScanLine.


III. Exemples utilisant ScanLine


III-A. Le programme Daisy

Le programme Daisy utilise la même technique que celle du listing 8 pour créer une image pf24bit. Le détail de la méthode DrawDaisy montre comment cette image est créée. Brièvement, les plans rouge et vert de cette image contiennent toutes les nuances de ces couleurs. Le plan bleu contient le "Daisy" avec seulement la plus lumineuse des valeurs bleues. Pour cette application, vous devez disposer au minimum d'un écran 800x600 et d'un mode vidéo de 15 bits par couleur ou plus.

Afficher une image 24 bits par couleur est facile avec un mode vidéo de 15 bits par couleur ou supérieur car windows n'utilise de palettes que pour les modes 256 couleurs ou inférieurs. Si vous essayez d'afficher un bitmap pf24bit dans un mode de seulement 256 couleurs, vous êtes à la merci de la palette courante de Windows. Voir le Show Demo One Lab Report pour une alternative à ce problème.


III-B. Le programme Split

Le programme Split est un simple traitement d'image pout étudier les plans de couleurs d'une image. Le programme SPLIT lit le fichier DAISY.BMP créé par le programme DAISY ( ou un autre fichier BMP de 24 bits par couleur ). L'appui sur les boutons à gauche permet d'afficher les plans rouge, vert et bleu correspondants.

Le programme SPLIT sauvegarde l'image originale dans BitmapRGB en tant que base pour la création des autres bitmaps.

Quand la case à cocher "Monochrome" située près du bouton "RGB Composite" est cochée, chaque composante rouge, vert et bleu est affectée à la même valeur. Cette valeur est définie par (T + V + B) / 3. Le bitmap obtenu ainsi, BitmapGray, est assigné à Image.Picture.Graphic pour affichage. Voir listing 11.

MakeShadesofGrayImage à partir d'une image RVB
PROCEDURE TFormSplit.MakeShadesOfGrayImage; VAR Gray : INTEGER; i : INTEGER; j : INTEGER; rowRGB : pRGBTripleArray; rowGray: pRGBTripleArray; BEGIN Screen.Cursor := crHourGlass; TRY FOR j := BitmapRGB.Height-1 DOWNTO 0 DO BEGIN rowRGB := BitmapRGB.Scanline[j]; rowGray := BitmapGray.Scanline[j]; FOR i := BitmapRGB.Width-1 DOWNTO 0 DO BEGIN // Intensité = (R + G + B) DIV 3 WITH rowRGB[i] DO Gray := (rgbtRed + rgbtGreen + rgbtBlue) DIV 3; WITH rowGray[i] DO BEGIN rgbtRed := Gray; rgbtGreen := Gray; rgbtBlue := Gray END END END; FINALLY Screen.Cursor := crDefault END END;
Une autre "meilleure" méthode pour créer des niveaux de gris peut être utilisée. Voir le Spectra lab report pour deux autres méthodes basées sur "Y", le niveau de gris utilisé pour la conversion des informations de couleur ( YUV/YIQ ) dans l'affichage sur une télévision noir et blanc.

Pour afficher le plan rouge, les valeurs rouges des pixels ( les valeurs rgbtRed ) sont assignées à un autre bitmap, nommé BitmapR. Les valeurs bleues et vertes des pixels sont quant à elles mises à zéro. Quand la case "Monochrome" est cochée, le niveau de rouge est affecté à rgbtRed, rgbtBlue et rgbtGreen ; en résulte une image en niveaux de gris.

L'explication de la conversion de RVB ( Rouge-Vert-Bleu ) en HSV ( Hue-Saturation-Valeur ) est décrite dans le HSV Lab Report. Un excellent livre sur les conversions de couleurs ( et tout ce qui concerne l'imagerie sur PC ) : Computer Graphics Principles and Practice de Foley, et al, Addison-Wesley, 1996. Ou bien regardez dans l'article général Color Information dans la Reference Library par efg. Pour les conventions de couleur dans Delphi regardez Color, Section B de la page Delphi Graphics Algorithms.


III-C. Rotation d'une image

Comme présentée précédemment, la rotation d'un bitmap est trop lente avec la propriété Pixels. L'utilisation de ScanLine pour la rotation de n'importe quel angle est plus rapide. Le Rotate Scanline Lab Report montre que la rotation d'un bitmap pf24bit de 640x480 par degré dans le sens des aiguilles d'une montre prend quand même plus d'une seconde sur un Pentium 166 MHz.

Chaque pixel n'est pas tourné dans sa nouvelle position. Vous partez de l'image tournée et considerez où le pixel est dans l'image originelle. En effectuant la rotation inverse, le pixel le plus proche de l'image originale est choisi. A cause de l'utilisation d'entiers dans les calculs, certains artifices peuvent être introduits dans la rotation. Habituellement l'anti-aliasing n'est pas nécessaire dans la rotation d'images pf24bit de la plupart des objets. L'anti-aliasing doit être utilisé pour la rotation d'images précises, et surtout dans le cas de textes.



III-D. Permutation de couleurs dans un bitmap pf24bit

L'ancienne limitation de la palette VGA ne peut fonctionner avec des images 24 bits. Souvenez-vous : Windows utilise des palettes seulement avec les modes 256 couleurs ou inférieurs. Les palettes ne sont pas utilisées dans les modes couleurs ( 15 ou 16 bits ) et couleurs vraies ( 24 bits ou plus ).

Quand Windows utilise des palettes, vous avez seulement 236 couleurs disponibles, car les 10 premières et 10 dernières couleurs de la palette sont définies par Windows. Même avec cette technique, la permutation de couleurs d'un bitmap pf24bit est un peu lente sans accélération matérielle. Le Color Cycle Lab Report utilise une table de correspondance de 1280 couleurs ( 5*256 ), qui est un nombre bien plus grand qu'une palette windows normale.

La méthode FormCreate définit les entrées dans le ARRAY of TRGBTriples : ColorCycle. Les 256 premières couleurs sont un dégradé de rouge, suvi par 256 niveaux de vert et 256 niveaux de bleu. Le quatrième ensemble est très similaire à la palette "Tempête de feu" utilisée dans le programme "FractInt fractal". Le cinquième ensemble définit des niveaux de gris.

Après avoir lancé le programme CycleColor, l'image fractale est créée en un temps de 90 secondes sur un pentium 166 MHz. ( Les maths utilisées pour la création de cette image sortent du cadre de cet article. ) Une fois que l'image est terminée, la case "Cycle Colors" est activée. L'image fractale est un bitmap pf8bit ( créé suivant une méthode approchant celle du listing 4 ).

Quand la case "Cycle Colors" est cochée, l'événement OnIdle de l'application prend le bitmap pf8bit nommé BitmapBase et définit tous les pixels RVB de BitmapRGB en utilisant le tableau ColorCycle. Voir le listing 12 ci-dessous. Le fait de décocher la case stoppe la permutation des couleurs. Légèrement lente, la permutation des couleurs effectuée complètement par programme est quand même impressionnante.

Permutation de couleurs en utilisant la tâche Idle
PROCEDURE TFormColorCycle.IdleAction(Sender: TObject; VAR Done: BOOLEAN); VAR i : INTEGER; index : INTEGER; j : INTEGER; RowIn : pByteArray; RowRGB: pRGBTripleArray; BEGIN IF NOT CheckBoxCycle.Checked THEN Done := TRUE ELSE BEGIN INC (CycleStart); IF CycleStart >= ColorList.Count THEN CycleStart := 0; LabelCycle.Caption := IntToStr(CycleStart); FOR j := 0 TO BitmapBase.Height-1 DO BEGIN RowIn := BitmapBase.ScanLine[j]; RowRGB := BitmapRGB.ScanLine[j]; FOR i := 0 TO BitmapBase.Width-1 DO BEGIN index := CycleStart + RowIn[i]; IF index >= ColorList.Count THEN index := index - ColorList.Count; RowRGB[i] := pRGBTriple(ColorList.Items[index])^ END END; ImageShow.Picture.Graphic := BitmapRGB; Done := FALSE; END END {IdleAction};

III-E. Autres exemples



IV. Utilisation inappropriée de ScanLine

Par Danny Thorpe(Borland R&D)

Beaucoup de gens se précipitent pour utiliser ScanLine pour des opérations qui pourraient être effectuées plus rapidement par le GDI. L'avantage de ScanLines[] est de donner un accès direct aux pixels avec un minimum de temps de traitement. Ceci ne signifie pas que ScanLines[] est le moyen le plus rapide pour manipuler le contenu d'un bitmap. Beaucoup de cartes vidéo aujourd'hui contiennent des moteurs dédiés intégrés très sophistiqués, qui peuvent effectuer des opérations sur les données d'un bitmap beaucoup plus rapidement que ne le fait le CPU. J'ai vu des cartes vidéos remettre à zéro un bitmap entier avant même que le CPU n'ai fini la première ligne de ScanLines. Le CPU ne peut faire qu'un seul point à la fois, et chaque accès est un accès à la mémoire pouvant mettre le pipeline CPU en attente de réponse du cache. Si la partie DIBSection réside en mémoire vidéo le CPU doit passer à travers le bus PCI ou AGP pour la lire. La carte vidéo n'a aucune de ces restrictions, elle a un accès immédiat et direct sans temps de latence à la mémoire vidéo. Et beaucoup de moteurs intégrés sont conçus pour traiter plusieurs pixels en une seule fois. Remplir la mémoire vidéo avec des zéros ou une couleur ( ou un pinceau : Brush ) est aussi une opération prioritaire pour le hardware de la carte vidéo. J'ai entendu dire que le secret d'un carte 3D de haut niveau pour remplir de zéro extrêmement vite, n'est pas de mettre des zéros dans la RAM, mais de couper l'alimentation d'une partie de la mémoire !

Un exemple : le listing 3 pour inverser un bitmap en balayant les pixels et en effectuant un NOT sur chacun d'entre eux. Je doute sérieusement que cela ait les mêmes performances que PatBlt(Bitmap.Canvas.Handle, 0, 0, Bitmap.Width, Bitmap.Height, DSTINVERT), même sur un vieux tampon VGA tramé. Le listing 3 est bien sûr donné à titre démonstratif, comme le fait que la documentation de Delphi mentionne Canvas.Pixels[], les gens vont adopter le code exemple comme la vérité, sans se soucier à quel point c'est inapproprié pour le travail productif.

Même observation pour le listing 6 : la fonction est décrite comme équivalente à FillRect avec un pinceau jaune. Il n'est pas précisé que FillRect sera sans doute beaucoup plus rapide. Cet exemple n'est donc que démonstratif et non productif.

Pareillement, mixer des palettes de bitmap peut être effectué sans manipuler les pixels. L'astuce est de fusionner les palettes en premier, d'assigner la nouvelle palette au bitmap de destination et enfin d'effectuer un blit du bitmap source sur celui de destination. Le GDI et le hardware de la carte vidéo vont faire la correspondance de couleurs pour vous.

Aussi, depuis Delphi 4, assigner un Handle de palette à un bitmap va recalculer les pixels du bitmap pour obtenir la couleur la plus approchante dans la nouvelle palette. Delphi 3 et précédents ne le font pas.


V. Optimisation


V-A. Minimiser le nombre d'accès à ScanLines

Plusieurs fois dans les newsgroups Delphi quelqu'un suggère que le nombre d'accès à ScanLines peut être minimisé par une technique sans le temps de traitement qui doit exister à chaque appel. Par exemple, plutôt que d'accéder à ScanLine une fois par rangée, ( comme beaucoup d'exemples ici ) avec un peu de code supplémentaire ScanLine peut n'être accédé que seulement deux fois au début de la boucle de traitement des lignes. Un pointeur peut être incrémenté en utilisation les opérations arithmétiques entières plutôt que d'appeler ScanLine sur chaque rangée. Voir le Listing 13 pour un exemple.

Listing 13 : Minimiser les accès à ScanLines[]
procedure TForm1.ButtonOptimizedClick(Sender: TObject); VAR Bitmap : TBitmap; Delta : INTEGER; i : INTEGER; j : INTEGER; k : INTEGER; LoopCount : INTEGER; row : pRGBTripleArray; ScanlineBytes: INTEGER; StartTime : DWORD; // DWORD pour que D3 et D4 soient contents begin LabelOptimized.Caption := ''; LoopCount := SpinEditTimes.Value; StartTime := GetTickCount; FOR k := 1 TO LoopCount DO BEGIN Bitmap := TBitmap.Create; TRY Bitmap.PixelFormat := pf24bit; Bitmap.Width := 640; Bitmap.Height := 480; row := Bitmap.Scanline[0]; ScanlineBytes := Integer(Bitmap.Scanline[1]) - Integer(row); FOR j := 0 TO Bitmap.Height-1 DO BEGIN FOR i := 0 TO Bitmap.Width-1 DO BEGIN WITH row[i] DO BEGIN rgbtRed := k; rgbtGreen := i MOD 256; rgbtBlue := j MOD 256; END END; INC(Integer(Row), ScanlineBytes); END; ImageOptimized.Picture.Graphic := Bitmap FINALLY Bitmap.Free END END; Delta := GetTickCount - StartTime; // ms LabelOptimized.Caption := IntToStr(Delta) + ' Total ms; ' + Format('%.1f', [Delta / LoopCount]) + ' ms/bitmap'; end;
Mais quelle est l'efficacité de cette technique ? Pour le déterminer un petit programme ScanlineTiming D3/D4 a été écrit pour comparer la technique "brute" accédant à ScanLine pour chaque rangée à la technique expliquée ci-dessus, dans le listing 13. Les résultats sont affichés dans le tableau suivant :

Temps ( ± écart type ) pour un bitmap de 640x480 basé sur 5 essais de 100 bitmaps chacun.

Vitesse CPU MHz "Brute" ms/bitmap "optimisée" ms/bitmap gain ms/bitmap
120 88.30 ± 0.60 187.90 ± 0.90 0.40
166 70.90 ± 0.07 69.80 ± 0.04 1.10
400 16.27 ± 0.02 15.40 ± 0.00 0.87
450 17.69 ± 0.35 16.78 ± 0.32 0.91
A mon avis, cette technique est certainement la dernière chose à penser quand vous travaillez avec un bitmap. Tous les autres algorithmes doivent être affinés avant. Rarement sauver une milliseconde par bitmap vaut l'optimisation, peut-être n'est-ce significatif qu'avec des applications temps-réel. Sur les PC les plus rapides, cela ne représente qu'un gain de 5%.

Commentaire par mail de Robert Lee à propos de cette optimisation :
... L'efficacité de cette technique dépend surtout de ce que vous faites avec le bitmap. Par exemple, la moitié du temps dans votre exemple est passé à créer et détruire le bitmap lui-même. Si le bitmap en question est déjà créé, alors l'effet relatif de cette technique va augmenter de 10%. Aussi, si les dimensions sont changées en quelque chose de long et étroit, ou si l'opération porte sur deux bitmaps ou plus ( mélange... ) alors l'effet sera progressivement plus significatif. Juste avec un petit essai, j'ai doublé la différence seulement en changeant la taille du bitmap.
Ma stratégie préférée pour mesurer l'impact d'une technique d'optimisation est de faire état du "meilleur" cas et du cas "typique". Alors la cas typique est sans doute de 5% ( voire même moins ), mais le "meilleur" effet peut être de 2 à 4x. [ merci, Robert, pour l'éclaircissement ]

Commentaire de Danny Thorpe (Borland R&D)
Au sujet de l'impact sur les performances d'un appel répété à ScanLines : oui, il y a un gain potentiel. Le tampon de la DIBSection en mémoire n'est pas garanti comme étant cohérent ( à cause d'opérations GDI récentes ) tant que GDIFlush n'est pas appelé avant d'accéder au pointeur. La propriété ScanLine[] doit appeler GDIFlush pour être sûre que le tampon est synchronisé. Si le bitmap a été modifié par des appels GDI, alors l'appel de GDIFlush peut bloquer en attendant que la file du GDI soit vide. Si aucune modification n'est en cours, GDIFlush devrait finir immédiatement, mais il y a toujours un surplus de temps dû à un appel supplémentaire.
De plus, TBitmap.GetScanLines() appelle TBitMap.Changing, qui peut avoir à créer un bitmap complètement nouveau avec une copie de l'image si celui-ci est partagé. Ceci va complètement effondrer les performances dès le premier appel à ScanLines, et perdre un peu même si celui ci n'est pas partagé.
La technique montrée dans le listing 13 pour minimiser le temps perdu par ScanLines[] a été développée par moi lors de l'écriture de la classe TJPEGImage. Il y a plus de travail que ce que l'on peut voir et qui devrait être mentionné avec le listing. Le moyen le plus facile pour éliminer les appels à ScanLines[] est de faire son propre pointeur et utiliser les opérations arithmétiques pour le faire avancer d'un début de ligne à la suivante. Il y a deux risques à le faire :
1) Tenir compte de l'alignement DWord de chaque ligne
2) La disposition physique des lignes dans le tampon mémoire : les DIBs peuvent être orientés de "haut en bas", où la première ligne de pixels réside dans les premiers octets de la zone tampon ; ou de "bas en haut", où la première ligne de pixels réside dans les derniers octets de la zone tampon. Par expérience, de "bas en haut" est l'orientation la plus courante pour les DIBs, peut-être à cause de l'origine OS2 des BMPs.
La technique de soustraire l'adresse de ScanLine[0] à l'adresse de ScanLine[1] résout les deux problèmes très joliement. Cela donne la distance entre deux lignes ScanLines y compris l'ajustement pour l'alignement DWord si besoin. Et le signe de l'écart indique implicitement l'orientation du DIB en mémoire. Ceci élimine le besoin d'ajouter de coûteux tests conditionnels pour avancer dans la boucle. Il suffit d'incrémenter le pointeur de l'écart et il va se placer sur la ligne suivante, qu'elle soit avant ou après l'adresse en cours.

Envoyez-moi une réponse SVP si vous pensez que j'ai oublié quelque chose dans les tests ou si vous obtenez des différences significatives.


V-B. Accéder à ScanLine en utilisant l'assembleur

Voir le UseNet post de Paul Nicholls sur l'accès à un bitmap pf16bit en utilisant BASM.

Voir le UseNet post de Robert Rossmair sur le calcul de la couleur moyenne d'un bitmap.

Voir l'exemple de Ken Florentino sur l'utilisation d'un bitmap pf32bit en assembleur.


V-C. Copie d'une zone de données vers ScanLines

Dans un message du groupe borland.public.delphi, Mikael Stalvik veut créer un tableau d'entiers ( 4 octets ) en mémoire et le copier dans un bitmap pf32bit. J'ai créé deux exemples pour Mikael, un "non-optimisé" et un autre "optimisé". Mais les performances entre les deux sont équivalentes.

La méthode "non-optimisée" est de remplir les lignes dans l'ordre croissant ( de haut en bas ).

Listing 14 : Méthode 'non-optimisée' pour copier un tableau d'entiers dans ScanLine[]
procedure TForm1.Button1Click(Sender: TObject); TYPE TRGBQuadArray = ARRAY[WORD] OF INTEGER; pRGBQuadArray = ^TRGBQuadArray; VAR i,j : INTEGER; Bitmap: TBitmap; row : pRGBQuadArray; Start : DWORD; begin Start := GetTickCount; Bitmap := TBitmap.Create; TRY Bitmap.PixelFormat := pf32bit; Bitmap.Width := 640; Bitmap.Height := 480; FOR j := 0 TO Bitmap.Height-1 DO BEGIN row := Bitmap.Scanline[j]; FOR i := 0 TO Bitmap.Width-1 DO BEGIN row[i] := i*j // Quelque chose pour faire une image "intéressante" END END; Image1.Picture.Graphic := Bitmap; ShowMessage( IntToStr(GetTickCount-Start) + ' ms') FINALLY Bitmap.Free; END end;
La méthode optimisée utilise une simple opération "Move" pour copier toutes les données dans ScanLine. ( N.B. : il y a des conditions d'alignement sur ScanLine, chaque ligne de ScanLine doit être un multiple de 8 octets. Ces conditions sont ignorées ici. On suppose que le bitmap a la bonne largeur pour qu'aucun alignement ne soit ajouté à la fin de chaque ligne ).

Mais quelle est l'adresse de chaque pixel pf32bit dans un TBitMap ? Insérons le test IF suivant dans le code au-dessus pour les trouver :

// Affiche l'adresse des "coins" du bitmap IF ((i=0) OR (i=Bitmap.Width-1)) AND ((j=0) OR (j=1) OR (j=Bitmap.Height-1)) THEN Memo1.Lines.Add('(' + IntToStr(i) + ', ' + IntToStr(j) + ') = ' + InttoHex( Integer(@row[i]),8 ));
Voici les résultats pour quelques points particuliers d'un bitmap pf32bit :

Ligne\Colonne 0 ... ... ... 639
0 $8374E600 ... $8374EF