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
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 := (Sender AS TButton).Tag;
Bitmap := TBitmap.Create;
TRY
WITH Bitmap DO
BEGIN
Width := 32;
Height := 32;
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;
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 :
procedure TForm1.ButtonPf4bitClick(Sender: TObject);
VAR
Bitmap: TBitmap;
i,j : INTEGER;
m : INTEGER;
row : pByteArray;
begin
Bitmap := TBitmap.Create;
TRY
Bitmap.Width := Image1.Width;
Bitmap.Height := Image1.Height;
Bitmap.PixelFormat := pf4bit;
FOR j := 0 TO Bitmap.Height-1 DO
BEGIN
m := j DIV 16;
row := Bitmap.Scanline[j];
FOR i := 0 TO Bitmap.Width DIV 2 - 1 DO
BEGIN
row[i] := m +
(m SHL 4);
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>;
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
VAR
Bitmap4: TBitmap;
Bitmap8: TBitmap;
i : INTEGER;
j : INTEGER;
Row4 : pByteArray;
Row8 : pByteArray;
...
Bitmap8 := TBitmap.Create;
TRY
Bitmap8.Width := Bitmap4.Width;
Bitmap8.Height := 2 * Bitmap4.Height;
Bitmap8.PixelFormat := pf8Bit;
FOR j := 0 TO Bitmap4.Height-1 DO
BEGIN
Row4 := Bitmap4.Scanline[j];
Row8 := Bitmap8.Scanline[j];
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
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
public
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');
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];
WITH Row24[i] DO
BEGIN
rgbtRed := LogicalPalette.palPalEntry[index].peRed;
rgbtGreen := LogicalPalette.palPalEntry[index].peGreen;
rgbtBlue := LogicalPalette.palPalEntry[index].peBlue
END
END
END;
Bitmap24.SaveToFile('Deer24.BMP');
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.
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;
G : 0..31;
B : 0..31;
RGB : WORD;
Row : pWordArray;
...
Bitmap := TBitmap.Create;
TRY
Bitmap.Width := Image1.Width;
Bitmap.Height := Image1.Height;
Bitmap.PixelFormat := pf15bit;
R := 31;
G := 31;
B := 0;
RGB := (R SHL 10) OR (G SHL 5) OR B;
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;
G : 0..63;
B : 0..31;
RGB : WORD;
Row : pWordArray;
begin
Bitmap := TBitmap.Create;
TRY
Bitmap.Width := Image1.Width;
Bitmap.Height := Image1.Height;
Bitmap.PixelFormat := pf16bit;
R := 31;
G := 63;
B := 0;
RGB := (R SHL 11) OR (G SHL 5) OR B;
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;
rgbtGreen := 255;
rgbtBlue := 0;
END
END
END;
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;
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 ;
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');
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;
ASSERT(Bitmap.PixelFormat = pf24bit);
Bitmap.PixelFormat := pf15bit;
Bitmap.SaveToFile('Tulip3-15B.BMP')
FINALLY
Bitmap.Free
END
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.
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
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.
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 ;
III-E. Autres exemples
IV. Utilisation inappropriée de ScanLine
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;
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;
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
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 :
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 | |