Frédéric Mélantois
Manipulation d'images en VB.NET et C#
Nous étudierons les différentes manières d'accéder rapidement aux pixels d'une image, aussi bien en C# qu'en VB.NET. Un passage en revue des principaux formats d'images vous sera proposé.
Par Frédéric Mélantois publié le 18/10/2006 à 22:27, lu 14932 fois, 9 pages
 6 | La gestion des images au format 8 bits.
Le format 8 bits correspond aux images possédant au maximum 256 couleurs. Il s'agit des images aux palettes dites indexées. C'est à dire que toutes les couleurs ne sont pas représentées et qu'une sélection a été faîte dans une palette. Une table de couleurs comporte les valeurs de chaque composante. Le format GIF est un « survivant » de l'époque des premières cartes vidéos VGA, où le seul mode disponible en 256 couleurs était le 320 x 200, mode standard des jeux vidéos.Le format 8 bits est aussi associé historiquement aux images en 256 niveaux de gris. L'illustration souvent utilisée est celle des images satellites. Le satellite Spot fournissait un fichier par canal enregistré. Au minimum, les images satellites étaient composées de quatre fichiers (bleu, rouge, vert, infrarouge). Comme à l'époque, les cartes ne pouvaient afficher que 256 couleurs, il était nécessaire de se définir des plages de niveaux de gris pour les trois composantes que l'on avait pu choisir. On ne pouvait choisir que 6 plages par canal affiché, c'est à 6x6x6 = 216 couleurs. Les valeurs possibles étaient donc 0, 51, 102, 153, 204 et 255. Comment redéfinir les octets de nos fichiers de composantes en fonction de ce tableau de valeurs ? Plusieurs techniques étaient possibles :
  1. par segmentation automatique : on modifiait les valeurs des composantes de 0 à 50 vers la valeur 0, les valeurs 51 à 101 vers la valeur 51 et ainsi de suite... Cette approche ne prend pas en compte quantitativement les valeurs des différentes composantes.
  2. par segmentation bornée : on définissait une valeur de fin d'application pour la première borne, par exemple la valeur 30. Toutes les valeurs inférieures à 30 se voyaient modifiées à la valeur 0. On définissait une valeur de début d'application pour la dernière borne, par exemple la valeur 220. Toutes les valeurs supérieures à 220 se trouvaient modifiées en 255. Entre 100 et 220, on répartissait les pixels de manière égalitaire sur les valeurs restantes (51,102,153,204). Afin de définir au mieux la première et dernière borne, on visualisait l'histogramme de l'image (Nombre de pixels par valeur de composante).
  3. Par classification : à l'aide de l'histogramme, on déterminait soit même chaque plage de valeurs devant être réaffectée parmi 6 valeurs (0, 51, 102, 153, 204 et 255).
Grâce à la classification, on peut mettre en évidence des zones particulières d'une image. C'est ainsi que l'on a pu isoler des zones de végétations à partir des images satellites. Des minéraux ont pu être isolés à partir de photos de roches prises au microscope polarisant, ou bien à partir de photos de roches auxquelles on avait appliqué des colorants et soumis à des lampes dont le domaine spectral était bien ciblé. Par la suite, pour avoir de meilleurs résultats, on prit en compte la texture de l'objet à identifier (à isoler). C'est ce qu'on appelle la classification par texture.
Quand on veut transformer une image 32 bits en une image 8 bits (pour par exemple créer une image gif pour le web), nous sommes confrontés aux mêmes problématiques. Pour la démonstration, nous prendrons comme exemple une segmentation automatique. Il est bien évident que nous n'obtiendrons pas les meilleurs résultats visuellement mais l'exemple aura le mérite d'être simple.Nous allons tout d'abord créer une image 8 bits en attribuant une palette de 216 couleurs dont les valeurs des composantes sont réparties de manières égales entre 0 et 255. Voici le code C# :

int width = bitmap.Width;

            int height = bitmap.Height;

            Bitmap bitmap8bits = new Bitmap(width, height, PixelFormat.Format8bppIndexed);

            ColorPalette pal = bitmap8bits.Palette;

 

            byte[] tab = new byte[6] { 0, 51, 102, 153, 204, 255 };

 

            for (int b = 0; b < 6; b++)

                for (int g = 0; g < 6; g++)

                    for (int r = 0; r < 6; r++)

                    {

                        pal.Entries[(36 * r) + (6 * g) + b] =

                        Color.FromArgb(255, tab[r], tab[g], tab[b]);

                    }

            bitmap8bits.Palette = pal;// Reaffectation sinon la palette n'est pas modifie dans l'image

et le code VB.NET :

Dim width As Integer = bitmap.Width

Dim height As Integer = bitmap.Height

 

Dim bitmap8bits As Bitmap = New Bitmap(width, height, PixelFormat.Format8bppIndexed)

Dim pal As ColorPalette = bitmap8bits.Palette

 

Dim tab() As Byte = {0, 51, 102, 153, 204, 255}

Dim b, g, r As Integer

 

For b = 0 To 5

    For g = 0 To 5

        For r = 0 To 5

            pal.Entries((36 * r) + (6 * g) + b) = Color.FromArgb(255, tab(r), tab(g), tab(b))

        Next

    Next

Next

 

bitmap8bits.Palette = pal 'ne pas oublier de reaffecter la palette  l'image

Comme on le voit, les composantes possibles du rouge, vert et bleu correspondront aux valeurs du tableau « tab ». Nous accédons à la palette de l'image grâce à la propriété Palette. Nous obtenons une référence de l'objet « ColorPalette » de l'image (du moins, nous sommes en mesure de le penser). Le calcul de l'index de couleur peut se faire par la formule « (36 * rouge) + (6 * vert) + bleu ». On utilisera cette même formule pour calculer l'index de la couleur à partir des valeurs composantes de l'image 32 ou 24 Bits lors de la conversion en une image 8 bits indexée.Ayant la référence de la palette de l'image, on peut s'attendre à ce que celle-ci soit modifiée pour l'image, ce qui n'est pas le cas. L'utilitaire Reflector de Lutz Roeder nous donne la réponse à ce problème en analysant la méthode « _GetColorPalette() » de l'objet « Image » de « System.Drawing ». Voici le code C# :

private ColorPalette _GetColorPalette()

{

    int num1 = -1;

    int num2 = SafeNativeMethods.Gdip.GdipGetImagePaletteSize(new HandleRef(this, this.nativeImage), out num1);

    if (num2 != 0)

    {

        throw SafeNativeMethods.Gdip.StatusException(num2);

    }

    ColorPalette palette1 = new ColorPalette(num1);

    IntPtr ptr1 = Marshal.AllocHGlobal(num1);

    num2 = SafeNativeMethods.Gdip.GdipGetImagePalette(new HandleRef(this, this.nativeImage), ptr1, num1);

    try

    {

        if (num2 != 0)

        {

            throw SafeNativeMethods.Gdip.StatusException(num2);

        }

        palette1.ConvertFromMemory(ptr1);

    }

    finally

    {

        Marshal.FreeHGlobal(ptr1);

    }

    return palette1;

}

Voici le code VB.NET :

Private Function _GetColorPalette() As ColorPalette

    Dim num1 As Integer = -1

    Dim num2 As Integer = Gdip.GdipGetImagePaletteSize(New HandleRef(Me, Me.nativeImage), num1)

    If (num2 <> 0) Then

        Throw Gdip.StatusException(num2)

    End If

    Dim palette1 As New ColorPalette(num1)

    Dim ptr1 As IntPtr = Marshal.AllocHGlobal(num1)

    num2 = Gdip.GdipGetImagePalette(New HandleRef(Me, Me.nativeImage), ptr1, num1)

    Try

        If (num2 <> 0) Then

            Throw Gdip.StatusException(num2)

        End If

        palette1.ConvertFromMemory(ptr1)

    Finally

        Marshal.FreeHGlobal(ptr1)

    End Try

    Return palette1

End Function

Vous vous apercevez qu'à chaque appel de cette méthode, une nouvelle référence de palette est créée. On n'est donc pas en « liaison » directe avec la palette de l'image. C'est pourquoi nous devons réaffecter la palette extraite et modifiée à la propriété « Palette » de l'image. Il est indispensable de ne pas oublier cette particularité lorsqu'on modifie le contenu de la palette d'une image.
Une table de pré-calcul est indispensable pour déterminer l'index dans le tableau de 6 valeurs (0, 51, 102, 153, 204, 255) à partir de la valeur des différentes composantes d'un pixel de l'image 32 bits. Ensuite, il suffit d'appliquer la formule « (36 * rouge) + (6 * vert) + bleu » pour retrouver l'index dans la palette 216 couleurs. Voici le code C# :

//table de pre-calcul

Byte[] canalTab = new byte[256];

int m;

for (int i = 0; i < 256; i++)

{

    canalTab[i] = (byte)(i / 51);

}

 

unsafe

{

    BitmapData bmpDataOld = bitmap.LockBits(new Rectangle(0, 0,

    width, height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);

    BitmapData bmpDataNew = bitmap8bits.LockBits(new Rectangle(0,

    0, width, height), ImageLockMode.ReadWrite, PixelFormat.Format8bppIndexed);

    byte* oldPixel = (byte*)(void*)bmpDataOld.Scan0;

    byte* newPixel = (byte*)(void*)bmpDataNew.Scan0;

    int offsetNew = bmpDataNew.Stride - (width);

    for (int y = 0; y < height; y++)

    {

        for (int x = 0; x < width; x++)

        {

            newPixel[0] = (byte)((36 * canalTab[oldPixel[2]]) +

            (6 * canalTab[oldPixel[1]]) + canalTab[oldPixel[0]]);

            oldPixel += 4;

            newPixel++;

        }

        newPixel += offsetNew;

    }

    bitmap8bits.UnlockBits(bmpDataNew);

    bitmap.UnlockBits(bmpDataOld);

}

Voici le code VB.NET

'table de pre-calcul

Dim canalTab(256) As Byte

Dim i As Integer

For i = 0 To 255

    canalTab(i) = CByte(i / 51)

Next

 

Dim bmpDataOld As BitmapData = bitmap.LockBits(New Rectangle(0, 0, width, height) _

, System.Drawing.Imaging.ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb)

Dim bmpDataNew As BitmapData = bitmap8bits.LockBits(New Rectangle(0, 0, width, height) _

, ImageLockMode.ReadWrite, PixelFormat.Format8bppIndexed)

 

Dim oldPixel(width * height * 4 - 1) As Byte

Marshal.Copy(bmpDataOld.Scan0, oldPixel, 0, oldPixel.Length)

 

Dim newPixel(width * height - 1) As Byte

'Pas necessaire de recuperer les donnees de l'image 8 bits vierge par un Marshal.Copy

 

Dim loc, x, y As Integer

 

For y = 0 To height - 1

    For x = 0 To width - 1

        loc = width * y + x

        newPixel(loc) = (36 * canalTab(oldPixel(loc * 4 + 2)) _

        + (6 * canalTab(oldPixel(loc * 4 + 1))) _

        + canalTab(oldPixel(loc * 4)))

    Next

Next

 

Marshal.Copy(newPixel, 0, bmpDataNew.Scan0, newPixel.Length)

 

bitmap8bits.UnlockBits(bmpDataNew)

bitmap.UnlockBits(bmpDataOld)

A la vue des différents codes C# et VB.NET, vous pouvez vous apercevoir que l'on peut encore améliorer les performances car la formule « (36 * rouge) + (6 * vert) + bleu » est appliquée sur tous les pixels de l'image. En particulier, on a vu précédemment que la multiplication était une opération très couteuse. Il suffit donc de modifier les codes C# et VB.NET précédents en créant des tables de pré-calculs pour les opérations « 36* » et « 6* ». N'hésitez pas à réaliser ces modifications et comparez les temps de traitement. C'est ainsi que vous vous rendrez compte que ce genre d'optimisation est très importante.
Voici le résultat en image de notre traitement. L'image originale en 32 bits :
 
/content/8d3eb481-8a98-42a6-8033-e851c797aa60/image3.jpg
 
L'image traitée en 216 couleurs :
 
/content/8d3eb481-8a98-42a6-8033-e851c797aa60/image4.jpg
 
Il existe bien évidemment des techniques plus élaborées pour réduire le nombre de couleurs d'une image.
 
» Démarrer une discussion
 
Discussion démarée par vattic le 11/04/2011 à 11:02, 1 commentaire(s).
Discussion démarée par olibara le 18/08/2008 à 00:12, 2 commentaire(s).