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
 8 | Gestion des images 1 bit
Les images en noir et blanc (au format "Format1bppIndexed") sont codées suivant le principe que la valeur d'un pixel équivaut à un bit. Un« byte » peut donc contenir alors les valeurs de 8 pixels. Lorsqu'on doit afficher une photo couleur dans une page noir et blanc d'un journal, il est nécessaire de procéder à une conversion d'une image 32 bits en une image 1 bit. Il est nécessaire d'avoir une équivalence en "niveau de gris" des couleurs de l'image à traiter afin de pouvoir décider quelle valeur de gris sera convertie en noir ou en blanc. Il existe une formule empirique permettant de convertir une image couleur en niveau de gris :NivGris = 0.3 * Bleu + 0.59 * vert + 0.11 * Rouge
Il faut bien noter que pour effectuer cette opération, il faut réaliser de très nombreuses multiplications. Nous avons vu précédemment que l'on pouvait gagner du temps processeur en précalculant ces opérations. C'est le rôle des tables de pré-calculs. Nous pouvons voir que le résultat de ces opérations contiendra des nombres à virgule. On préférera l'emploi d'un « Single » ou d'un « float » à un « Double » qui lui coûtera beaucoup plus. L'idéal étant de traiter avec des « int » ou « Interger », la formule ci-dessus peut être modifiée comme suit : NivGris = (76 * Bleu + 151 * vert + 28 * Rouge) >> 8
Nous n'avons fait que multiplier les valeurs décimales par 256, nous n'avons gardé que la partie entière car l'approximation est suffisante. Il suffit de diviser le tout par 256 pour réaliser l'équivalence empirique plus haut. Ainsi, les tables de précalculs des multiplications sont constituées d'« int » ou d'« Integer » La division par 256 est assurée par un décalage de 8 bits, peu coûteux en temps processeur. Nous aurions pu multiplier par 100 au lieu de 256, mais il aurait fallu diviser par 100, ce qui représente un coup supplémentaire par rapport à un décalage de 8.
Une fois la valeur de gris calculée, il nous faut décider quelle plage de valeurs de gris correspondra au noir et au blanc. On peut par exemple dire que les valeurs supérieures à 128 correspondront au blanc et les autres au noir. Mais cette démarche ne donne pas de bons résultats visuellement. Cette technique ne tient pas compte du voisinage du pixel. Dans les années 80, ont été mises au point les premières techniques de dithering. Nous allons appliquer l'algorithme de dithering de Bayer, historiquement un des plus connus, c'est à dire qu'on applique un motif à l'image afin de n'avoir que des pixels ayant pour valeur 0 ou 1 pour la nouvelle image 1 bit. A l'origine, l'algorithme fonctionnait pour 64 niveaux de gris, nous allons l'appliquer à nos 256 niveaux. Voici le code C# commenté :

            int width = bitmap.Width;

            int height = bitmap.Height;

            Bitmap bitmap1Bit = new Bitmap(width, height, PixelFormat.Format1bppIndexed);

 

            //le pattern a appliquer sur des niveaux de gris de 64

            byte[][] pattern = new byte[][]{ new byte[]{0,32,8,40,34,2,10,42},

new byte[]{48,16,56,24,50,18,58,26},

new byte[]{12,44,4,36,14,46,6,38},

new byte[]{60,28,52,20,62,30,54,22},

new byte[]{3,35,11,43,1,33,9,41},

new byte[]{51,19,59,27,49,17,57,25},

new byte[]{15,47,7,39,13,45,5,37},

new byte[]{63,31,55,23,61,29,53,21}};

 

            //tables de precalculs des multiplications pour le calcul du niveau de gris

            int[] multiBleu = new int[256];

            int[] multiVert = new int[256];

            int[] multiRouge = new int[256];

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

            {

                multiBleu[i] = 76 * i;

                multiVert[i] = 151 * i;

                multiRouge[i] = 28 * i;

            }

 

            unsafe

            {

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

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

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

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

 

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

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

 

                //l'offset de l'image 1 bit

                int offset = bmpDataNew.Stride - (width / 8);

 

                byte pixel;//Niveau de gris

                int newPix = 0;//ensemble de 8 pixels

 

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

                {

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

                    {

                        //A partir des composantes du pixel

                        //de l'image 32 bits, on determine

                        //le niveau de gris

 

                        pixel = (byte)((multiBleu[oldPixel[0]] + multiVert[oldPixel[1]]

                            + multiRouge[oldPixel[2]]) >> 8);

 

                        //on compare le pixel (reduit  64 niveaux)

                        //au pattern (en fonction de sa position

                        //par rapport  l'image)

                        if ((pixel >> 2) > pattern[x & 7][y & 7])

                            //on allume le pixel (Bit=1)

                            //dans le byte

                            newPix = newPix | (1 << (7 - (x % 8)));

 

                        if (x % 8 == 0)

                        {

                            //quand on a rempli 8 bits

                            //on ecrit le byte dans l'image

                            newPixel[0] = (byte)newPix;

 

                            //on avance le pointeur

                            newPixel++;

 

                            //on teint tous les pixels

                            //pour le nouveau cycle de 8 bits

                            newPix = 0;

                        }

                        oldPixel += 4;

                    }

                    newPixel += offset;

                }

                bitmap.UnlockBits(bmpDataOld);

                bitmap1Bit.UnlockBits(bmpDataNew);

            }

Sachez que le temps d'exécution du code précédent peut-être encore être amélioré, de l'ordre de 25% ! Je vous donnerai la solution dans le prochain article. En attendant, je vous laisse chercher. Un petit indice, cela concerne les tableaux ;-)Voici le code VB.NET :

Dim width As Integer = bitmap.Width

Dim height As Integer = bitmap.Height

 

'le pattern à appliquer (il était à l'origine destiné à des niveaux de gris de 64)

Dim pattern() As Byte = {0, 32, 8, 40, 34, 2, 10, 42 _

                          , 0, 32, 8, 40, 34, 2, 10, 42 _

                          , 48, 16, 56, 24, 50, 18, 58, 26 _

                          , 12, 44, 4, 36, 14, 46, 6, 38 _

                          , 60, 28, 52, 20, 62, 30, 54, 22 _

                          , 3, 35, 11, 43, 1, 33, 9, 41 _

                          , 51, 19, 59, 27, 49, 17, 57, 25 _

                          , 15, 47, 7, 39, 13, 45, 5, 37 _

                          , 63, 31, 55, 23, 61, 29, 53, 21}

 

Dim bitmap1Bit As Bitmap = New Bitmap(width, height, PixelFormat.Format1bppIndexed)

 

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

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

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

, ImageLockMode.ReadWrite, PixelFormat.Format1bppIndexed)

 

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

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

 

Dim newPixel(bmpDataNew.Stride * height - 1) As Byte

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

 

Dim pix, locOld, locNew, x, y As Integer

 

Dim newpix As Byte

 

 

'tables de precalcul des multiplications pour le calcul de localisation des pixels

'de l'ancienne image et de la nouvelle.

Dim multiOld(height) As Integer

Dim multiNew(height) As Integer

 

For y = 0 To height - 1

    multiOld(y) = width * y

    multiNew(y) = bmpDataNew.Stride * y

Next

 

 

'tables de precalcul des multiplications pour le calcul du niveau de gris

Dim tabBleu(256) As Integer

Dim tabVert(256) As Integer

Dim tabRouge(256) As Integer

 

'Initialisation

For y = 0 To 255

    tabBleu(y) = 76 * y

    tabVert(y) = 151 * y

    tabRouge(y) = 28 * y

Next

 

For y = 0 To height - 1

 

    For x = 0 To width - 1

        'on récupère le niveau de gris sur 255

        locNew = multiNew(y) + (x / 8)

        locOld = (multiOld(y) + x) * 4

        'Formule ci-dessous en commentaires, à éviter car coûteuse

        'pix = CByte(0.3 * oldPixel(locOld) + 0.59 * oldPixel(locOld + 1) + 0.11 * oldPixel(locOld + 2))

        pix = (tabBleu(oldPixel(locOld)) + tabVert(oldPixel(locOld + 1)) + tabRouge(oldPixel(locOld + 2))) >> 8

 

        If ((pix >> 2) > pattern(((x And 7) << 3) + (y And 7))) Then

            'On allume le pixel

            newpix = newpix Or (1 << (7 - (x Mod 8)))

        End If

 

        If (x Mod 8 = 0) Then

            'quand on a rempli 8 bits

            'on écrit le byte dans l'image

            newPixel(locNew) = newpix

            'on éteint tous les pixels

            'pour le nouveau cycle de 8 bits

            newpix = 0

        End If

    Next

 

Next

 

'on recopie notre nouveau tableau dans la nouvelle image

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

 

 

bitmap1Bit.UnlockBits(bmpDataNew)

bitmap.UnlockBits(bmpDataOld)

Voici une photo pour notre journal :
 
/content/8d3eb481-8a98-42a6-8033-e851c797aa60/arctriomphe.jpg
 
 
» 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).