Traitement d'image en noir et blanc

Contenu du snippet

Bonjours à tous

Voilà je doit travaillé sur des image et les convertir en noir & blanc (C'est un projet de génération de fax), il s'agit aussi de mon premier projet C# (le C/C++ est assez loin derriere moi mais je suis content de revenir a ce type de syntax aprés une longue période VB).

cette classe fais partie d'un projet bien plus vaste mais elle pourra aidé des paersonne qui auront a travailler sur des image en noir et blanc (sujet peu traiter sur les sites codes source a ce que j'ai vue), il y'a aussi Frédéric Mélantois qui traite du sujet du traitement d'image sur son blog et dans des articles (dernier : http://www.techheadbrothers.com/Articles.aspx?Id=8d3eb481-8a98-42a6-8033-e851c797aa60&p=1).
donc ca pourras peut-être soulever des questions/sugestions sur le sujet.

Enfin bref j'ai essayer d'optimiser mais y'a sans doute d'autres optimisation a faire.

C'est la première fois que je traite des image et dans l'ensemble je ne suis pas mécontant du résultat , mon application est fluide toutefois je ne suis pas satisfais de la gestion de la luminositer, j'ai essayer de trouver un moyen de ne pas "bruler" les parties claires mais on as trés vite une perte de constraste, si quelqu'un peut me donnée des méthodes pour réaliser ca de maniére plus propre je suis preneur(j'y mais juste la condition que l'algo s'applique sur le niveau de gris.

La classe supporte trois algo pour la transphormation niveau de gris -> Noir et blanc :
RANDOM (on compare le niveau de gris a une valeur aléatoire),
HALFTONE (on compare le niveau de gris a une valeur fixé),
FLOYD Steinberg (algo par difusion d'erreur) sans doute le plus intéréssant mais aussi le plus gourmand

il est a noté que l'algo Floyd a tendonce a créer des motif répétitif pour les surfaces de couleur uniforme et dégradé on pourat donc l'amélioré en ajoutant une petite dose d'aléatoire...

le traitement noir et blanc est volontairement séparer de la partie qui gére le passage en noir et blanc, en effet cette partie n'est recalculer que lorsque l'on modifie la taille de l'image ce qui est assez rare pour des pages de fax.

Source / Exemple :


//Structure de manipulation de tableaux RGB
    public struct RGBPix
    {
        public byte R;
        public byte G;
        public byte B;
    }

    //Classe de manipulation et conversion d'image en noir et blanc
    public class FaxImage
    {

        //Petite enumération des différent algo gérer par la classe
        public enum BWAlgorithme
        {
            Floyd,
            Haltone,
            Random
        }
        //Immage d'origine
        private Bitmap _OriginalImg = null;
        //Pointer ver un tableau de byte contenant les données en niveaux de gris
        private IntPtr _GreyDat = IntPtr.Zero;
        //Image finale
        private Bitmap _BWImg = null;

        //variable de configuration internes
        private BWAlgorithme _DitherAlgo = BWAlgorithme.Floyd;
        private sbyte _Luminosity = 0;
        private int _Height = 0;
        private int _Width = 0;

        //Constructeur On lui passe l'image d'origine, et la taille finale souhaiter
        public FaxImage(System.Drawing.Bitmap Img,int Width,int Height)
        {
            this.Width = Width;
            this.Height = Height;
            this._OriginalImg = Img;
        }

        //Métode pour changer d'image d'origine
        public void SetImage(Bitmap Image)
        {
            this._OriginalImg = Image;
            if (this._GreyDat != IntPtr.Zero)
            {
                System.Runtime.InteropServices.Marshal.FreeHGlobal(this._GreyDat);
                this._GreyDat = IntPtr.Zero;
            }
        }

        //Propriété pour récupérer l'image en noir et blanc
        public System.Drawing.Bitmap BWImage
        {
            get
            {
                //On recalcul l'image en niveau de gris si besoin.
                if (this._GreyDat == IntPtr.Zero)
                    Make_GrayDat();
                //On recalcul l'image noir et blanc si besoin
                if (_BWImg==null)
                    switch (this._DitherAlgo)
                    {
                        case BWAlgorithme.Floyd:
                            Make_BWDat_Floyd();
                            break;
                        case BWAlgorithme.Haltone:
                            Make_BWDat_HalfTone();
                            break;
                        case BWAlgorithme.Random:
                            Make_BWDat_Random();
                            break;
                    }
                //On retourne l'image
                return this._BWImg;
            }            
        }

        Propriéter de modification de la luminositer de l'image final
        public sbyte Luminosity
        {
            get
            {
                return this._Luminosity;
            }
            set
            {
                if (this._Luminosity != value)
                {
                    this._Luminosity = value;
                    if (this._BWImg != null)
                    {
                        this._BWImg.Dispose();
                        this._BWImg = null;
                    }
                }
            }
        }

        //Propriéter pour indiquer l'algo a utiliser lors de la conversion
        public BWAlgorithme DitherAlgo
        {
            get
            {
                return this._DitherAlgo;
            }
            set
            {
                if (this._DitherAlgo != value)
                {
                    this._DitherAlgo = value;
                    if (this._BWImg != null)
                    {
                        this._BWImg.Dispose();
                        this._BWImg = null;
                    }
                }
            }
        }

        //Propriéter de la hauteur final de l'image
        public int Height
        {
            get
            {
                return this._Height;
            }
            set
            {
                if (value < 1) value = 1;
                if (this._Height != value)
                {
                    this._Height = value;
                    if (this._GreyDat != IntPtr.Zero)
                    {
                        System.Runtime.InteropServices.Marshal.FreeHGlobal(this._GreyDat);
                        this._GreyDat = IntPtr.Zero;
                    }

                }
            }
        }

        //Propriéter de la largeur final de l'image
        public int Width
        {
            get
            {
                return this._Width;
            }
            set
            {   
                if (value < 1) value = 1;
                if (this._Width != value)
                {
                    this._Width = value;
                    if (this._GreyDat != IntPtr.Zero)
                    {
                        System.Runtime.InteropServices.Marshal.FreeHGlobal(this._GreyDat);
                        this._GreyDat = IntPtr.Zero;
                    }

                }
            }
        }

        private void Make_BWDat_Floyd()
        {
            //Initialisation de l'image
            if (this._BWImg != null)
                this._BWImg.Dispose();
            this._BWImg=new Bitmap(this._Width, this._Height, PixelFormat.Format1bppIndexed);
            BitmapData BWDat = this._BWImg.LockBits(new Rectangle(0, 0, this._Width, this._Height), ImageLockMode.WriteOnly, PixelFormat.Format1bppIndexed);

            //Allocation de la mémoire pour les erreurs
            IntPtr Erreurs = System.Runtime.InteropServices.Marshal.AllocHGlobal(((this._Height + 1) * this._Width + 1) * sizeof(int));

            //Calcule des donnée noir et blanc
            unsafe
            {
                byte CurBitMsk;
                int PointVal;
                int ErrToDispatch;
                byte* CurGrey = (byte*)this._GreyDat.ToPointer();
                int* CurErreur = (int*)Erreurs.ToPointer();
                int* CurErreur7 = &(CurErreur[1]);
                int* CurErreur1 = &(CurErreur[this._Width - 1]);
                int* CurErreur5 = &(CurErreur[this._Width]);
                int* CurErreur3 = &(CurErreur[this._Width + 1]);

                //initialisation des premiéres erreurs (les autres seront initialiser à la volée).
                for (int* LastErreur = &(((int*)Erreurs.ToPointer())[this._Width]); CurErreur <= LastErreur; CurErreur++)

  • CurErreur = 0;
CurErreur = (int*)Erreurs.ToPointer(); Random Rnd = new Random(); float LuminosityRatio = (float)((float)255/(float)(255 - Math.Abs(this._Luminosity))); byte LuminosityOffset = (this._Luminosity < 0) ? (byte)0 : (byte)this._Luminosity; //Table de précacule de la luminosité byte* LuminosityTab = stackalloc byte [255]; for (int grayValue = 0; grayValue < 255; grayValue++) LuminosityTab[grayValue] = (byte)(grayValue / LuminosityRatio + LuminosityOffset); for (int y = 0; y < this._Height; y++) { CurBitMsk = 128;// 1 << 7; for ( byte* CurBWs = &(((byte*)BWDat.Scan0.ToPointer())[BWDat.Stride * y]), //On ointe vers le premier octe de la ligne en cours. afterLastGrey = &(((byte*)this._GreyDat.ToPointer())[(y+1)*this.Width]); //Borne de la ligne des niveaux de gris //On parcourt la ligne courante CurGrey < afterLastGrey; //incrémentation des pointeurs CurGrey++, CurErreur++, CurErreur7++, CurErreur1++, CurErreur5++, CurErreur3++ ) { //Calcul de la valeur du point PointVal = LuminosityTab[*CurGrey] + *CurErreur + Rnd.Next(-25, 25); if(PointVal > 128) { //Enregistrement du point
  • CurBWs |= CurBitMsk;
//Calcul de l'erreur ErrToDispatch = PointVal - 255; } else { //Enregistrement du point
  • CurBWs &= (byte)(255 ^ CurBitMsk);
//Calcul de l'erreur ErrToDispatch = PointVal; } //Diffusion de l'erreur
  • CurErreur7 += ((ErrToDispatch * 7) >> 4);
  • CurErreur1 += (ErrToDispatch >> 4);
  • CurErreur5 += ((ErrToDispatch * 5) >> 4);
  • CurErreur3 = ((ErrToDispatch * 3) >> 4); // = et pas += pour initialisé à la volée
//Initialisation des pointeurs et du masque de bit courant pour le point suivant CurBitMsk >>= 1; if (CurBitMsk == 0) { CurBitMsk = 128;//1 << 7; CurBWs++; } } } } //Libération du tableau des erreurs System.Runtime.InteropServices.Marshal.FreeHGlobal(Erreurs); //Sauveguarde de l'image. this._BWImg.UnlockBits(BWDat); } private void Make_BWDat_Random() { //Initialisation de l'image if (this._BWImg != null) this._BWImg.Dispose(); this._BWImg = new Bitmap(this._Width, this._Height, PixelFormat.Format1bppIndexed); BitmapData BWDat = this._BWImg.LockBits(new Rectangle(0, 0, this._Width, this._Height), ImageLockMode.WriteOnly, PixelFormat.Format1bppIndexed); //initialisation d'un Tableau de byte aléatoire. Byte[] rnds = new Byte[this._Width * this._Height]; new Random().NextBytes(rnds); //Calcule des donnée noir et blanc unsafe { byte* CurBWs = (byte*)BWDat.Scan0.ToPointer(); byte* CurGrey = (byte*)this._GreyDat.ToPointer(); byte CurBitMsk; double LuminosityRatio = (255 - Math.Abs(this._Luminosity)) / 255.0; for (int y = 0; y < this._Height; y++) { CurBitMsk = 128;// 1 << 7; CurBWs = &(((byte*)BWDat.Scan0.ToPointer())[BWDat.Stride * y]); for (int x = 0; x < this._Width; x++) { //Enregistrement du point
  • CurBWs = (byte)((((*CurGrey * LuminosityRatio) + ((this._Luminosity < 0) ? (sbyte)0 : this._Luminosity)) > rnds[x + y * this._Width]) ? (*CurBWs | CurBitMsk) : (*CurBWs & (255 ^ CurBitMsk)));
//Initialisation des pointeurs et du masque de bit courant pour le point suivant CurBitMsk >>= 1; if (CurBitMsk == 0) { CurBitMsk = 128; // 1 << 7; CurBWs++; } CurGrey++; } } } //Sauveguarde de l'image. this._BWImg.UnlockBits(BWDat); } private void Make_BWDat_HalfTone() { //Initialisation de l'image if (this._BWImg != null) this._BWImg.Dispose(); this._BWImg = new Bitmap(this._Width, this._Height, PixelFormat.Format1bppIndexed); BitmapData BWDat = this._BWImg.LockBits(new Rectangle(0, 0, this._Width, this._Height), ImageLockMode.WriteOnly, PixelFormat.Format1bppIndexed); //Calcul des donnée noir et blanc unsafe { byte* CurBWs = (byte*)BWDat.Scan0.ToPointer(); byte* CurGrey = (byte*)this._GreyDat.ToPointer(); byte CurBitMsk; double LuminosityRatio = (255 - Math.Abs(this._Luminosity)) / 255.0; for (int y = 0; y < this._Height; y++) { CurBitMsk = 1 << 7; CurBWs = &(((byte*)BWDat.Scan0.ToPointer())[BWDat.Stride * y]); for (int x = 0; x < this._Width; x++) { //Enregistrement du point
  • CurBWs = (byte)((((*CurGrey * LuminosityRatio) + ((this._Luminosity < 0) ? (sbyte)0 : this._Luminosity)) > 128) ? (*CurBWs | CurBitMsk) : (*CurBWs & (255 ^ CurBitMsk)));
//Initialisation des pointeurs et du masque de bit courant pour le point suivant CurBitMsk >>= 1; if (CurBitMsk == 0) { CurBitMsk = 1 << 7; CurBWs++; } CurGrey++; } } } //Enregistrement des modification. this._BWImg.UnlockBits(BWDat); } public void Make_GrayDat() { //On redimensionne l'image d'origine. Bitmap SizedImg = new Bitmap (this._OriginalImg ,this._Width,this._Height); //OnRécupére les données de l'image. BitmapData RGBDat = SizedImg.LockBits(new Rectangle(0, 0, this._Width, this._Height), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb); //On initialise le tableau de donnée. if (this._GreyDat != IntPtr.Zero) { System.Runtime.InteropServices.Marshal.ReAllocHGlobal(this._GreyDat,new IntPtr(this._Width * this._Height)); } else { this._GreyDat = System.Runtime.InteropServices.Marshal.AllocHGlobal(this._Width * this._Height); } //On Recalcule les données de niveau de gris. unsafe { //tables de precalculs des multiplications pour le calcul du niveau de gris //Les tableaus sont créer dans le tas (+ rapide por les petits tableau) //ils sont libéré à la fin de la fonction. int* multiBleu = stackalloc int[256]; int* multiVert = stackalloc int[256]; int* multiRouge = stackalloc int[256]; for (int i = 0; i < 256; i++) { multiBleu[i] = 76 * i; multiVert[i] = 151 * i; multiRouge[i] = 28 * i; } byte* CurGrey = (byte*)this._GreyDat.ToPointer(); for (int y = 0; y < this._Height; y++) { //On pointe vers le début de la ligne RGBPix* CurRGBDat = (RGBPix*)&(((byte*)RGBDat.Scan0.ToPointer())[y * RGBDat.Stride]); //Balayage de la ligne for ( // Calcul de la borne supérieur de la ligne byte* afterLastGrey = &(((byte*)this._GreyDat.ToPointer())[(y+1)*this.Width]); //Borne de la ligne des niveaux de gris //On Balaye toute la ligne CurGrey < afterLastGrey; //On pointe vers les données suivantes CurRGBDat++, CurGrey++ )
  • CurGrey = (byte)((multiBleu[CurRGBDat->B] + multiVert[CurRGBDat->G] + multiRouge[CurRGBDat->R]) >> 8);
} } //On supprime les donnée en noir et blans pour forcer leur recalcule if (_BWImg != null) { this._BWImg.Dispose(); this._BWImg=null; } //On libére les ressoure SizedImg.UnlockBits(RGBDat); SizedImg.Dispose(); } }

Conclusion :


=============================================
Quelques explications
=============================================

les compteurs:
---------------------

On remarque que les for correspondant au balayage des lignes n'utilise pas de compteur classique.
mais travaile directement sur les pointeurs.

En effet lorsque je balaye une ligne j'utilise un pointeur pour acceder au pixel courrant,
et j'incremente directement le pointeur pour acceder au pixel suivant, en effet
  • PointVar est plus rapide que PointVar[x]

donc x n'a plus d'autre utilité que de compter le nombre de fois ou je vais répéter les opératon,

la démarche est de se demander comment supprimer x tous bonnement.

et le principe est simple :

Avant d'entrer dans la boucle ou au début de celle ci j'initialize PointVar
pour pointer sur le premier pixel de la ligne,
Dans le même temps je créer un second pointer BornePointVar qui pointe lui sur le dernier pixel de la ligne + 1 (ce n'est pas grave si il est hors de l'image ou même de la mémoire aloué car il ne serat jamais fais accé a la donnée sous jacente).

ca donne ca :
for(
type* PointVar = AddressePremierPixelLigne,
type* BornePointVar = AddresseDernierPixelLigne + 1;

PointVar < BornePointVar;

PointVar++
)
{
............
}

les masques de pixel :
---------------------

lors de laccés aux donnée d'une image noir et blanc les pixels sont sur chaque bit, il faut donc utilisé un masque de bit pour travailler au niveau de chaqu'un des pixel

la méthode la plus rapide que j'ai trouver est :

a la fin de la boucle for sur chaque pixels de la ligne :

avec CurBWs pointeur byte* sur les octets de la ligne,
et CurBitMsk byte

CurBitMsk >>= 1;
if (CurBitMsk == 0)
{
CurBitMsk = 128;//1 << 7;
CurBWs++;
}

bien entendu il faut avant de lire le premier pixel de la ligne initialiser le masque avec :
CurBitMsk = 128;//1 << 7;

ATTENTION la méthode de lecture de la ligne avec des bornes pointeur
n'est pas directement utilisable avec les images en noire et blanc pusique les donnée de pixel ne sont pas caller sur des octets.

mais dans le cas présent ce n'est pas un problème puisque les donnée sont lu depuis un tableau de byte des couleurs en niveaux de gris

======================================================

Je mettrait a jour avec les conseil que j'aurrais retenue ainsi qu'avec d'autre partie du programme qui traite des images.

A bientôt...

Vous n'êtes pas encore membre ?

inscrivez-vous, c'est gratuit et ça prend moins d'une minute !

Les membres obtiennent plus de réponses que les utilisateurs anonymes.

Le fait d'être membre vous permet d'avoir un suivi détaillé de vos demandes et codes sources.

Le fait d'être membre vous permet d'avoir des options supplémentaires.