Binarisation d'images

5/5 (20 avis)

Vue 17 769 fois - Téléchargée 1 815 fois

Description

IMPORTANT: Avant d'ouvrir les fichiers qui se terminent par ...FrameUnit.pas, BIEN LIRE LA REMARQUE FINALE!

C'est suite à une suggestion de Caribensila (voir http://www.delphifr.com/codes/RECHERCHE-SUPPRESSION-IMAGES-DOUBLE-BASEE-SUR-COMPARAISON-INTELLIGENTE_38711.aspx) que j'ai programmé ça. C'est une application simple de traitement d'images. Elle permet de faire subir à une image originale une série de transformations successives et d'afficher l'image résultante. Le but à l'origine était de trouver une méthode pour extraire un grain de beauté d'une photographie pour étudier son aspect au cours du temps.

Pour utiliser le programme, il faut choisir une image initiale, puis ajoutter des filtres, et enfin cliquer sur le bouton "Lancer le calcul". Il y a une fonction de zoom, et on peut faire s'afficher les images initiale et finale en surimpression avec une transparence paramètrable. Avec le menu principal, on peut importer et exporter des chaines de filtres (fichiers *.filter , il y en a 3 fournis).

Pour l'instant il y a 8 filtres disponibles:
+ Filtre médian: un filtre statistique qui remplace chaque pixel par sa valeur médiane à l'intérieur d'une boîte carrée de taille paramètrable (c'est à dire la valeur du milieu lorsqu'on classe les intensités des pixels voisins dans la boite par ordre croissant)
+ Extracteur de canaux: ce filtre extrait des canaux d'une image. Par exemple, on peut extraire le canal vert d'une image en RGB
+ RGB -> HSV: transformation RGB vers HSV (Hue, Saturation, Value) c'est à dire en français la teinte, la saturation et l'intensité lumineuse. C'est un autre mode de représentation des couleurs, très utile en traitement d'images.
+ HSV -> RGB: la transformation réciproque
+ RGB -> Luminance: extraction de la luminance
+ Binarisation: une fonction qui détermine, par une méthode mathématique basée sur l'étude d'ensembles aléatoires, un niveau statistiquement représentatif qui détermine 2 zones dans l'image: l'intérieur et l'extérieur. L'intérieur est transformé en blanc, l'extérieur en noir. Cette fonction est très lente, car elle met en jeu des tas de calculs.
+ Norme du gradient: calcule la norme du gradient de l'image. Utile par exemple pour la détection de bords.
+ Seuillage: met en blanc les pixels dont la valeur est plus grande qu'un certain seuil, en noir sinon.

Sauf dans le cas de RGB->HSV, HSV->RGB et "Extraction de canaux", les filtres travaillent séparément sur tous les canaux de l'image. Je n'ai pas mis de vérification du nombre de canaux, par exemple si vous essayez de transformer une image en HSV qlors qu'elle n'a qu'un seul canal, il se produira une erreur pas très explicite (access violation vraisemblablement), donc il ne faut pas faire n'importe quoi!

Les filtres sont "enchainés", c'est à dire qu'ils s'appliquent l'un après l'autre. Par exemple "RGB -> Luminance" suivi de "Norme du gradient" extraira la norme du gradient de la luminance.

Il y a des exemples de chaines de filtres fournis dans le dossier Exemples/, ainsi que des images pour tester. Dans la plupart des cas, en utilisant le filtre Luminance.filter on arrive à extraire le grain de beauté de l'image (c'est à dire que l'image est bien séparée en 2 parties, l'une étant le grain de beauté). Mais ça ne marche pas à tous les coups... Je suis en train de travailler sur une méthode plus précise, mais ça risque de prendre des temps de calcul prohibitifs...

Il y a une unité de traitement d'images fournie avec le projet: ImgUtils.pas. Elle définit une classe TBitmapData utile pour gérer les images et faire des calculs dessus. J'ai conscience que ça manque de commentaires, je les mettrai si quelqu'un les demande.

Source / Exemple :


unit MainFormUnit;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, ImgUtils, ExtDlgs, JPEG, ExtCtrls, Buttons, StdCtrls, ComCtrls,
  MedianBoxFilterFrameUnit, ChannelExtractFrameUnit, RGB2HSVFrameUnit,
  HSV2RGBFrameUnit, RGB2LuminanceFrameUnit, BinarizeFrameUnit,
  GradientNormFrameUnit, TresholdFrameUnit, FilterFrameUnit,
  Menus, ToolWin, ImgList;

type
  TMainForm = class(TForm)
    OpenPictureDialog1: TOpenPictureDialog;
    Panel1: TPanel;
    Edit1: TEdit;
    SpeedButton1: TSpeedButton;
    GroupBox1: TGroupBox;
    ListBox1: TListBox;
    PopupMenu1: TPopupMenu;
    ToolBar1: TToolBar;
    ToolButton1: TToolButton;
    ToolButton2: TToolButton;
    Panel2: TPanel;
    GroupBox2: TGroupBox;
    ToolButton3: TToolButton;
    ToolButton4: TToolButton;
    ImageList1: TImageList;
    ToolButton5: TToolButton;
    Panel3: TPanel;
    PageControl1: TPageControl;
    TabSheet1: TTabSheet;
    ScrollBox1: TScrollBox;
    PaintBox1: TPaintBox;
    TabSheet2: TTabSheet;
    ScrollBox2: TScrollBox;
    PaintBox2: TPaintBox;
    TabSheet3: TTabSheet;
    ScrollBox3: TScrollBox;
    PaintBox3: TPaintBox;
    GroupBox3: TGroupBox;
    TrackBar1: TTrackBar;
    GroupBox4: TGroupBox;
    TrackBar2: TTrackBar;
    MainMenu1: TMainMenu;
    Fichier1: TMenuItem;
    Exporterlachanedefiltres1: TMenuItem;
    Importerunechanedefiltres1: TMenuItem;
    Lancerlachane1: TMenuItem;
    OpenDialog1: TOpenDialog;
    SaveDialog1: TSaveDialog;
    procedure FormCreate(Sender: TObject);
    procedure SpeedButton1Click(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure ListBox1Click(Sender: TObject);
    procedure ToolButton2Click(Sender: TObject);
    procedure ToolButton4Click(Sender: TObject);
    procedure ToolButton5Click(Sender: TObject);
    procedure FormShow(Sender: TObject);
    procedure TrackBar1Change(Sender: TObject);
    procedure PaintBox1Paint(Sender: TObject);
    procedure PaintBox2Paint(Sender: TObject);
    procedure PaintBox3Paint(Sender: TObject);
    procedure Lancerlachane1Click(Sender: TObject);
    procedure TrackBar2Change(Sender: TObject);
    procedure Exporterlachanedefiltres1Click(Sender: TObject);
    procedure Importerunechanedefiltres1Click(Sender: TObject);
  private
    FSrcBitmap,FDstBitmap,FPreviewBitmap:TBitmap;
  public
    procedure RegisterFilter(FilterClass:TFilterFrameClass);
    procedure FilterMenuItemClick(Sender:TObject);

    procedure UpdateGUI;
    procedure UpdatePreview;

    function GetZoomRect:TRect;

    procedure ResetProcess;
    procedure Process;
  end;

var
  MainForm: TMainForm;

implementation

{$R *.dfm}

procedure TMainForm.FormCreate(Sender: TObject);
begin
  SpeedButton1.Align:=alRight;
  Edit1.Align:=alRight;
  FSrcBitmap:=TBitmap.Create;
  FDstBitmap:=TBitmap.Create;
  FPreviewBitmap:=TBitmap.Create;
  RegisterFilter(TMedianBoxFilterFrame);
  RegisterFilter(TChannelExtractFrame);
  RegisterFilter(TRGB2HSVFrame);
  RegisterFilter(THSV2RGBFrame);
  RegisterFilter(TRGB2LuminanceFrame);
  RegisterFilter(TBinarizeFrame);
  RegisterFilter(TGradientNormFrame);
  RegisterFilter(TTresholdFrame);
end;

procedure TMainForm.SpeedButton1Click(Sender: TObject);
var
  p:TBitmap;
begin
  if OpenPictureDialog1.Execute then begin
    Edit1.Text:=OpenPictureDialog1.FileName;
    p:=LoadBitmapFromFile(OpenPictureDialog1.FileName);
    FSrcBitmap.Destroy;
    FSrcBitmap:=p;
    UpdatePreview;
    ResetProcess;
    Lancerlachane1.Enabled:=(FSrcBitmap.Width>0) and (FSrcBitmap.Height>0);
  end;
end;

procedure TMainForm.FormDestroy(Sender: TObject);
begin
  FSrcBitmap.Destroy;
  FDstBitmap.Destroy;
  FPreviewBitmap.Destroy;
end;

procedure TMainForm.RegisterFilter(FilterClass: TFilterFrameClass);
var
  m:TMenuItem;
begin
  m:=TMenuItem.Create(Self);
  m.Caption:=FilterClass.Caption;
  m.Tag:=Integer(FilterClass);
  m.OnClick:=FilterMenuItemClick;
  PopupMenu1.Items.Add(m);
  RegisterClass(FilterClass);
end;

procedure TMainForm.UpdateGUI;
begin
  if ListBox1.ItemIndex>-1 then begin
    while GroupBox2.ControlCount>0 do
      GroupBox2.Controls[0].Parent:=nil;
    with TFilterFrame(ListBox1.Items.Objects[ListBox1.ItemIndex]) do begin
      Align:=alClient;
      Parent:=GroupBox2;
    end;
    ToolButton2.Enabled:=True;
  end else
    ToolButton2.Enabled:=False;
  ToolButton4.Enabled:=ListBox1.ItemIndex>0;
  ToolButton5.Enabled:=(ListBox1.ItemIndex>-1) and (ListBox1.ItemIndex<ListBox1.Count-1);
  UpdatePreview;
end;

procedure TMainForm.FilterMenuItemClick(Sender: TObject);
var
  f:TFrame;
begin
  with TFilterFrameClass(TMenuItem(Sender).Tag) do begin
    f:=Create(ListBox1);
    f.Name:='';
    ListBox1.ItemIndex:=ListBox1.Items.AddObject(Caption,f);
  end;
  UpdateGUI;
end;

procedure TMainForm.ListBox1Click(Sender: TObject);
begin
  UpdateGUI;
end;

procedure TMainForm.ToolButton2Click(Sender: TObject);
var
  a:Integer;
begin
  a:=ListBox1.ItemIndex;
  ListBox1.Items.Objects[a].Destroy;
  ListBox1.Items.Delete(a);
  if a>=ListBox1.Items.Count then
    Dec(a);
  ListBox1.ItemIndex:=a;
  UpdateGUI;
end;

procedure TMainForm.ToolButton4Click(Sender: TObject);
begin
  ListBox1.Items.Exchange(ListBox1.ItemIndex,ListBox1.ItemIndex-1);
  UpdateGUI;
end;

procedure TMainForm.ToolButton5Click(Sender: TObject);
begin
  ListBox1.Items.Exchange(ListBox1.ItemIndex,ListBox1.ItemIndex+1);
  UpdateGUI;
end;

procedure TMainForm.FormShow(Sender: TObject);
begin
  TrackBar1.SetTick(0);
  Lancerlachane1.ImageIndex:=4;
end;

procedure TMainForm.TrackBar1Change(Sender: TObject);
begin
  UpdatePreview;
end;

procedure TMainForm.UpdatePreview;
var
  r:TRect;
begin
  r:=GetZoomRect;
  PaintBox1.BoundsRect:=r;
  PaintBox1.Invalidate;
  PaintBox2.BoundsRect:=r;
  PaintBox2.Invalidate;
  PaintBox3.BoundsRect:=r;
  PaintBox3.Invalidate;
end;

function TMainForm.GetZoomRect: TRect;
const
  T:array[-5..5] of Single=(0.1,0.2,0.3,0.5,0.75,1,1.5,2,3,5,10);
begin
  Result.TopLeft:=Point(0,0);
  Result.Right:=Round(FSrcBitmap.Width*T[TrackBar1.Position]);
  Result.Bottom:=Round(FSrcBitmap.Height*T[TrackBar1.Position]);
end;

procedure TMainForm.PaintBox1Paint(Sender: TObject);
begin
  PaintBox1.Canvas.StretchDraw(GetZoomRect,FSrcBitmap);
end;

procedure TMainForm.PaintBox2Paint(Sender: TObject);
begin
  PaintBox2.Canvas.StretchDraw(GetZoomRect,FDstBitmap);
end;

procedure TMainForm.PaintBox3Paint(Sender: TObject);
var
  BF:TBlendFunction;
begin
  FPreviewBitmap.Width:=FSrcBitmap.Width;
  FPreviewBitmap.Height:=FSrcBitmap.Height;
  FPreviewBitmap.Canvas.Draw(0,0,FSrcBitmap);
  with BF do begin
    BlendOp:=AC_SRC_OVER;
    BlendFlags:=0;
    SourceConstantAlpha:=TrackBar2.Position;
    AlphaFormat:=0;
  end;
  with FDstBitmap do
    Windows.AlphaBlend(FPreviewBitmap.Canvas.Handle,0,0,FPreviewBitmap.Width,FPreviewBitmap.Height,Canvas.Handle,0,0,Width,Height,BF);
  PaintBox3.Canvas.StretchDraw(GetZoomRect,FPreviewBitmap);
end;

procedure TMainForm.Process;
var
  p:TBitmapData;
  a:Integer;
begin
  p:=TBitmapData.CreateAsRGB(FSrcBitmap);
  try
    for a:=0 to ListBox1.Items.Count-1 do
      with TFilterFrame(ListBox1.Items.Objects[a]) do
        try
          Filter(p);
        except
          on e:Exception do begin
            Messagebox(0,PChar('Le filtre "'+Caption+'" n°'+IntToStr(a)+' a provoqué une erreur: '#13+e.Message),PChar('Exception '+e.ClassName),MB_ICONERROR);
            raise;
          end;
        else
          Messagebox(0,PChar('Le filtre "'+Caption+'" n°'+IntToStr(a)+' a provoqué une erreur non spécifiée'),PChar('Exception inconnue'),MB_ICONERROR);
          raise;
        end;
    p.WriteToBitmap(FDstBitmap);
  finally
    p.Destroy;
    UpdatePreview;
  end;
end;

procedure TMainForm.ResetProcess;
begin
  FDstBitmap.Width:=0;
  FDstBitmap.Height:=0;
  UpdatePreview;
end;

procedure TMainForm.Lancerlachane1Click(Sender: TObject);
begin
  Process;
end;

procedure TMainForm.TrackBar2Change(Sender: TObject);
begin
  PaintBox3Paint(nil);
end;

procedure TMainForm.Exporterlachanedefiltres1Click(Sender: TObject);
var
  f:TFileStream;
  a,b:Integer;
  s:string;
begin
  if SaveDialog1.Execute then begin
    f:=TFileStream.Create(SaveDialog1.FileName,fmOpenWrite or fmCreate);
    try
      f.Seek(0,soFromBeginning);
      for a:=0 to ListBox1.Items.Count-1 do begin
        s:=ListBox1.Items.Objects[a].ClassName;
        b:=Length(s);
        f.Write(b,SizeOf(b));
        f.Write(s[1],b);
        f.WriteComponent(ListBox1.Items.Objects[a] as TComponent);
      end;
    finally
      f.Destroy;
    end;
  end;
end;

procedure TMainForm.Importerunechanedefiltres1Click(Sender: TObject);
var
  f:TFileStream;
  g:TFilterFrame;
  a:Integer;
  s:string;
begin
  if OpenDialog1.Execute then begin
    f:=TFileStream.Create(OpenDialog1.FileName,fmOpenRead);
    try
      for a:=0 to ListBox1.Items.Count-1 do
        ListBox1.Items.Objects[a].Destroy;
      ListBox1.Items.Clear;
      ListBox1.ItemIndex:=-1;
      f.Seek(0,soFromBeginning);
      while f.Position<f.Size do begin
        f.Read(a,SizeOf(a));
        SetLength(s,a);
        f.Read(s[1],a);
        g:=TFilterFrameClass(GetClass(s)).Create(nil);
        g.DestroyComponents;
        g.Name:='';
        f.ReadComponent(g);
        ListBox1.Items.AddObject(g.Caption,g);
      end;
    finally
      f.Destroy;
      UpdateGUI;
    end;
  end;
end;

end.

Conclusion :


Remarque finale: les frames qui sont dans les fichiers se terminant par ...FrameUnit.pas sont des descendants de TFilterFrame. Pour cette raison, lorsqu'on les ouvre dans Delphi certaines propriétés qui ne devraient pas être là sont rajouttées dans le fichier .dfm correspondant par Delphi, car l'IDE ne connait pas la classe TFilterFrame. Ces 3 propriétés incorrectes sont les suivantes:

OldCreateOrder = True
PixelsPerInch = 96
TextHeight = 13

Si vous ouvrez l'un de ces fichiers avec Delphi, il faudra éditer manuellement le fichier *.dfm correspondant (par exemple en faisant Alt+F12) et supprimer ces 3 propriétés, enregistrer et fermer le fichier avant de compiler. Sinon, lors de la création du filtre correspondant, il se produira une erreur "Error while reading property OldCreateOrder: property does not exist". Il existe un moyen de contourner le problème, mais il implique l'installation d'un package, et je ne voulais pas alourdir le code.

Codes Sources

A voir également

Ajouter un commentaire Commentaires
Caribensila
Messages postés
2527
Date d'inscription
jeudi 15 janvier 2004
Statut
Membre
Dernière intervention
16 octobre 2019
18
28 juil. 2008 à 09:04
@NetHacker
En résumé :
1ère étape: Convertir ton image en niveaux de gris.
2étape : Déterminer un seuil de gris sous lequel les pixels deviendront blancs, et au dessus duquel les pixels deviendront noirs.

@Tous
Mais il y a une autre façon de transformer une image en niveaux de gris, autre que celle proposée par CPTPINGU. C'est :

Gris = R*0.2125 + V*0.7154 + B*0.0721

Cette méthode est plus proche de la sensibilité réelle de notre oeuil aux couleurs.

La question que je me pose, perso, c'est de savoir laquelle de ces deux méthodes est la mieux adaptée à un traitement tel que celui que nous propose Forman ici.
Et, question subsidiaire : Est-ce que les gris des anciennes épreuves dites "en noir et blanc" des photos argentiques tiennent compte de notre sensibilité aux couleurs, ou est-ce qu'ils ne sont qu'un simple effet photochimique sans correctif?
cs_Forman
Messages postés
600
Date d'inscription
samedi 8 juin 2002
Statut
Membre
Dernière intervention
6 avril 2010
1
28 juil. 2008 à 00:12
Pour faire la méthode de CptPingu avec mon source, tu peux utiliser la chaine de filtres suivante:
-RGB -> Luminance
-Seuillage (avec une valeur paramétrable)

Autre solution (mais beaucoup plus lente!):
-RGB -> Luminance
-Binarization (avec distance euclidienne et norme L infini)
cs_Forman
Messages postés
600
Date d'inscription
samedi 8 juin 2002
Statut
Membre
Dernière intervention
6 avril 2010
1
28 juil. 2008 à 00:04
Ben il y a la fonction Binarize dans ImgUtils.pas qui fait ça aussi, c'est même le principal intérêt de ce code...

Tout dépend dans quel contexte tu veux faire ça. Si tu veux une méthode simple (et rapide!) celle de CptPingu est très bien. Peut-être d'ailleurs faudrait-il plutôt faire son test avec la couleur verte, qui en général est mieux liée à l'intensité lumineuse.

Ma méthode est plus complexe, et sert à déterminer un seuil (la valeur 127 mentionnée par CptPingu) qui soit adapté à la statistique de l'image, par exemple pour extraire un objet sur un fond uni. Ca prend pas mal de temps. Une fois le seuil calculé, c'est exactement le même algo en remplaçant 127 par la valeur qu'on a trouvée.
CptPingu
Messages postés
3834
Date d'inscription
dimanche 12 décembre 2004
Statut
Modérateur
Dernière intervention
20 octobre 2021
124
27 juil. 2008 à 23:51
Cette question aurait plus sa place sur le forum qu'ici.
Toutefois, pour réaliser cela, tu dois convertir ton image en noir et blanc
pixel.rouge = (pixel.rouge + pixel.vert + pixel.bleu) / 3
pixel.vert = (pixel.rouge + pixel.vert + pixel.bleu) / 3
pixel.bleu = (pixel.rouge + pixel.vert + pixel.bleu) /3

Puis la binariser:
pixel.rouge = si pixel.rouge > 127 alors blanc(0XFF) sinon noir (0X00)
pixel.vert = pixel.rouge
pixel.bleu = pixel.rouge
nethacker
Messages postés
288
Date d'inscription
mardi 2 mai 2006
Statut
Membre
Dernière intervention
12 octobre 2011

27 juil. 2008 à 23:19
Comment binariser une image en rendant tout ce qui est dans une couleur 0 et tout ce qui ne l'est pas 1 ? pour avoir enfin une image blanc et noir !
Afficher les 20 commentaires

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.