Utilisation de la propriété TBitMap.ScanLine

| Earl F. Glynn | Bruno Guérangé ( Traduction ) |

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. ♪

- Titre : Utilisation de la propriété TBitMap.ScanLine

- Auteurs : Earl F. Glynn, Bruno Guérangé ( Traduction )

- Parution : 16 novembre 2003

- Licence : Copyright © Earl F. Glynn et copyright © 2003 Bruno Guérangé pour la version Française . Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à 3 ans de prison et jusqu'à 300 000 E de dommages et intérêts.

Sommaire

- 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 sûr 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 API 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é. Une 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'une 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;

// Étrange, 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'à 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 lignes 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, une fois que le Bitmap est créé, il faut absolument affecter Width et Height avant PixelFormat. Dans le cas contraire, 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

// À 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 le 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 vraies (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 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 système, 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'œil 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 pixel sont disposé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'œil 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. À 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); // vérifie 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 où il suggère d'utiliser la méthode d'arrondi Floyd-Steinberg.

Autre exemple : Comment créer un bitmap à partir de données numériques ?

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 autres) 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 utilisant la méthode SaveToFile et lu en utilisant la méthode LoadFromFile. Ces nombres peuvent être « affichés » dans un TImage où 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. 16 : 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=262 144 pixels, mais a 230 427 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 230 427 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.
https://esibert.developpez.com/kylix/migration/image/

UseNet post de Matthijs Laan sur l'utilisation 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 pour é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 considérez où le pixel est dans l'image originelle. En effectuant la rotation inverse, le pixel le plus proche de l'image originale est choisi. À cause de l'utilisation d'entiers dans les calculs, certains artifices peuvent être introduits dans la rotation. Habituellement l'antialiasing n'est pas nécessaire dans la rotation d'images pf24bit de la plupart des objets. L'antialiasing doit être utilisé pour la rotation d'images précises, et surtout dans le cas de textes.

Voir aussi le Flip/Reverse/Rotate Lab Report.

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, suivi 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

Exemple d'échantillonnage avec ScanLine
Exemple de réduction/basculement/inversion d'une image
Exemple de comparaison de deux images

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éo 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'une 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

À 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 le cas typique est sans doute de 5 % (voire 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 DIB 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 DIB, peut-être à cause de l'origine OS2 des BMP.
La technique de soustraire l'adresse de ScanLine[0] à l'adresse de ScanLine[1] résout les deux problèmes très joliment. 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

…

$8374EFFC

1

$8374DC00

…

$8374E5FC

…

…

…

…

479

$83623000

…

$836239FC

Notez la plus grande et la plus petite des adresses. Le début des adresses du bitmap est le pixel 0 de la ligne 479, et la plus grande adresse est celle du pixel 639 de la ligne 0. Donc les adresses augmentent de la gauche vers la droite, mais du bas vers le haut.

Mais ceci n'est pas la seule disposition possible d'un TBitmap. Comme Alex Denissov le souligne dans les NewsGroup, quand la valeur de BitmapInfo.bmiHeader.biHeight est négative, le point (0,0) est le coin haut-gauche. Quand cette valeur est positive, c'est le coin bas-gauche. Normalement un TBitmap au point (0,0) dans le coin haut-gauche.

Un moyen simple de savoir si les adresses de ScanLine[] vont en augmentant ou en diminuant est de calculer :

ScanlineBytes := Integer(Bitmap.Scanline[1]) - Integer(Bitmap.Scanline[0]);

Combien y a-t-il d'octets dans le bitmap ? Calculer ($8374EFFC + 4) - $83623000 = $12C000 = 1,228,800 = 4*640*480, ce qui est bien la valeur attendue.

Pour utiliser « Move », il faut des pointeurs pour les adresses « de » et « vers ». L'adresse « Vers » est l'adresse du pixel 0 de la dernière rangée, comme montré ci-dessous :

Listing 15 : Méthode 'optimisée' pour copier un tableau d'entier dans ScanLine[]

procedure TForm1.Button1Click(Sender: TObject);
VAR
Bitmap:  TBitmap;
x     :  ARRAY OF Integer;
i,j   :  INTEGER;
p     :  ^Integer;
Start :  DWORD;
begin
SetLength(x, 640*480);
FOR j := 0 TO 479 DO
FOR i := 0 TO 639 DO
x[640*j + i] := i*j;   // Quelque chose d'intéressant
Start := GetTickCount;
Bitmap := TBitmap.Create;
TRY
Bitmap.PixelFormat := pf32bit;
Bitmap.Width  := 640;
Bitmap.Height := 480;
p := Bitmap.Scanline[Bitmap.Height-1];  // Adresse de départ de la dernière rangée
Move(x[0], p^, Bitmap.Width*Bitmap.Height*SizeOf(Integer));
Image1.Picture.Graphic := Bitmap;
ShowMessage( IntToStr(GetTickCount-Start) + ' ms')
FINALLY
Bitmap.Free;
END
end;

GetTickCount a été utilisé dans les deux cas pour mesurer l'efficacité de la méthode. Je n'ai pas pu détecter de différence significative entre les deux méthodes sur un Pentium III 650MHz.

Deux remarques pour terminer :
1 - les deux méthodes présentées ci-dessus ne sont pas identiques. Les deux bitmaps obtenus sont retournés de haut en bas. Ce qui a été ignoré ici ;
2 - les données brutes (i*j des coordonnées des points) utilisées pour le calcul du bitmap affichent un bitmap intéressant.

VI - Problèmes inattendus avec l'utilisation de ScanLine (Delphi 3 et 4)

VI-A - Corruption de l'image originelle suite à un Assign

Utiliser ScanLine juste après un TBitmap.Assign va modifier le bitmap originel (problème avec Delphi 3 et 4, corrigé avec Delphi 5). TBimap.Assign doit être une copie complète (toutes les données copiées et non pas une copie partielle ( imple copie du pointeur). Actuellement, à cause du compteur de référencement, l'assignation n'est qu'une copie partielle tant qu'aucune opération « significative » n'est effectuée provoquant cette fois-ci la création d'une copie indépendante du bitmap. Ceci peut être parfois très subtil à déterminer.

Ci-dessous, l'assignation d'un seul pixel change la copie partielle en copie complète. La nécessité de ceci est soit un bug soit un problème de conception.

BitmapAlign := TBitmap.Create;
TRY
// Assign ne fait pas une copie indépendante directement
// Ce problème ne concerne que Delphi 3 et Delphi 4
BitmapAlign.Assign(Bitmaps[2]);

// On force BitmapAlign à être totalement indépendant de Bitmaps[2]
// Ceci est une rustine nécessaire si ScanLine est la seule méthode
// d'accès aux pixels. Sans ceci le bitmap originel et le nouveau
// bitmap travaillent sur les mêmes données de pixels !!
BitmapAlign.Canvas.Pixels[0,0] := Bitmaps[2].Canvas.Pixels[0,0];

<Code n'accédant aux pixels que par ScanLine>

FINALLY
BitmapAlign.Free
END

Voici un exemple montrant ce problème : ScreenBitmapAssignEnigma.PAS.

La solution de Finn Tolderlund (après avoir parcouru la VCL…) est d'utiliser la commande :

BitmapAlign.Assign(Bitmaps[2]);
BitmapAlign.FreeImage;
<Code n'accédant aux pixels que par ScanLine>

Le FreeImage a pour effet de convertir la copie partielle en copie complète du bitmap. Merci à Finn pour cette astuce.

VI-B - L'erreur d'assignation d'un pixel pf24bit (rare bug de l'optimisation de Delphi)

Le problème apparait dans Delphi 3 et Delphi 4, mais semble réparé dans la mise à jour 2 de Delphi 4 (Build 5.104). Merci à Marko Peric de m'avoir informé que cette mise à jour corrige le problème.

Le code suivant sur la copie d'un TRGBTriple d'une ligne de ScanLine à une autre provoque une violation d'accès si les optimisations de Delphi sont activées.

VAR
BitmapA  : TBitmap;
BitmapB  : TBitmap;
i        : INTEGER;
j        : INTEGER;
RGBTriple: TRGBTriple;
rowInB   : pRGBTripleArray;
rowInA   : pRGBTripleArray;

// Définition d'un Bitmap pf24bit, BitmapA

BitmapB := TBitmap.Create;
TRY
BitmapB.Width := BitmapA.Width;
BitmapB.Height := BitmapA.Height;
BitmapB.PixelFormat := pf24bit;

FOR j := 0 TO BitmapA.Height-1 DO
BEGIN
RowInA := BitmapA.ScanLine[j];
RowInB := BitmapB.Scanline[j];
FOR i := 0 TO BitmapA.Width-1 DO
BEGIN
RowInB[i] := RowInA[i];  // Violation d'accès
END
END;

La méthode suivante est le « meilleur » contournement que j'ai trouvé (sans désactiver les optimisations qui font trop de différences dans les temps d'exécution).

VAR
RGBTriple: TRGBTriple;

// Coutournement du problème avec les meilleures performances
RGBTriple := RowInA[i];
RowInB[i] := RGBTriple

Voir les détails additionnels sur ce problème. Étrangement, Borland pense que ce n'est pas un bug. Ils m'ont dit : « Nous avons déterminé que votre rapport d'erreur n'est pas un bug, mais peut-être une erreur de compréhension sur le fonctionnement du produit. »

VI-C - Erreur interne C1127 ( Delphi4 ) et C1141 ( Delphi 5 )

Fragment de code provoquant une Erreur interne C1127 ( Delphi4 ) et C1141 ( Delphi 5 ).

Voir Borland Bug Case 430492.

VI-D - Présence simultanée des sections DDB et DIB sur le même bitmap

C'est un problème avec une implémentation complexe autorisant les sections DDB et DIB à exister simultanément sur un seul Bitmap, comme décrit dans le UseNet post de Takuo Nakamura.

Conclusion

Cette note technique couvre bon nombre d'exemples sur l'utilisation des propriétés ScanLine et PixelFormat pour manipuler les pixels de différentes façons. Beaucoup de traitements d'images et de fonctions graphiques peuvent être écrits en Delphi avec des procédures très rapides utilisant ScanLine. J'espère que d'autres vont découvrir combien sont utiles les possibilités de développement RAD de Delphi dans le traitement d'images et l'imagerie sur PC.

Merci à Danny Thorpe, ingénieur confirmé de Delphi R&D pour ses remarques constructives.

Merci à Gus Elliot de m'avoir signalé une omission dans le listing 13. (25 jan 2001)

Merci à Alacazam pour la correction orthographique de la version traduite.

Références

Using Device-Independent Bitmaps and Palettes
Sample: 16 and 32 Bits-Per-Pel Bitmap Formats