février
2010
Après un long moment d’absence, je reviens pour vous parler d’une notion dont l’intérêt m’a été révélé par un article sur le blog de bluestorm. Il s’agit des types fantômes, dont je souhaite vous montrer ici tout le bien. Naturellement, je vais partir d’un cas concret autour de ce que je développe actuellement.
Le contexte
Une application pour microscopistes
Je suis actuellement en train d’écrire les grandes lignes d’un programme destiné à des microscopistes amateurs, dont il existe déjà une version en Visual Basic (à réécrire et faire évoluer). Ce programme doit permettre de mettre en forme des images pour un forum en leur ajoutant un cartouche contenant diverses informations utiles (échelle, type de matériel, coloration éventuelle, etc.). Je vous donne une image ci-dessous pour vous faire une idée. Une caractéristique importante de ce logiciel réside dans sa capacité à dessiner des échelles à partir d’une relation entre un nombre X de pixels et une taille Y en micromètres, déterminée sur une image étalon.
Spores d’helvelle après coloration.
Objectif Zeiss Planapo 100/1.3
Image d’étalon
Une image étalon est une photo dont les éléments ont une taille connue, prise dans les mêmes conditions qu’une image classique. Le plus souvent, il s’agit d’une « règle » dont les graduations sont espacées de 10 µm (la bagatelle de 100 graduations dans un millimètre…). Le programme charge l’image d’étalon, l’analyse à la manière d’un outil de reconnaissance de caractères (en beaucoup plus simple !), et en déduit la taille d’une graduation, exprimée en pixels. De son côté, l’utilisateur indique la taille réelle correspondante, en micromètres cette fois-ci (cf. l’image ci-dessous).
Image d’étalon (1 graduation correspond à 10 µm).
Objectif Zeiss Plan Neofluar 63/1.25
Manipulation d’images
Pour automatiser la reconnaissance des graduations, on souhaite implémenter l’algorithme suivant (plutôt une recette de cuisine) :
- Charger l’image en couleur (24 bits).
- La transformer en niveaux de gris.
- Augmenter la luminosité pour éclaircir le fond.
- Passer en noir et blanc.
- Effacer les poussières pour uniformiser le fond.
- Reboucher les trous dans les traits de graduations.
- Mesurer les traits de graduation et les espaces qu’ils délimitent.
- En déduire la taille d’une graduation (un espace + un trait).
Pour y parvenir, nous allons utiliser LablGTK et CamlImages, deux bibliothèques qui dialoguent plutôt bien. Nous allons nous fixer comme objectif d’écrire un type picture
polymorphe. Nous nous en servirons pour distinguer les images en couleur (24 bits), en niveaux de gris et en noir et blanc. Les fonctions telles que get_width
ou get_pixbuf
s’appliqueront à tout type d’image.
Choix d’une interface
Pour faire court, nous voudrions quelque chose comme ceci :
type 'a picture type fullcolor type grayscale type monochrom val from_file : string -> fullcolor picture (** Charge une image en couleurs. *) (** Fonctions usuelles, valables pour tout type d'image. *) val copy : 'a picture -> 'a picture val get_width : 'a picture -> int val get_height : 'a picture -> int val get_pixbuf : 'a picture -> GdkPixbuf.pixbuf val grayscale : ?filter:(Color.rgb -> int) -> fullcolor picture -> grayscale picture (** Conversion en niveaux de gris d'une image couleur (24 bits). *) val black_and_white : ?threshold:int -> grayscale picture -> monochrom picture (** Conversion en noir et blanc d'une image en niveaux de gris. *) val noise_reduction : ?threshold:int -> monochrom picture -> monochrom picture (** Effacement des poussières sur une image en noir et blanc. *) val fill_gaps : ?threshold:int -> monochrom picture -> monochrom picture (** Rebouchage des trous sur une image en noir et blanc. *)
Pour que cet algorithme ne reste pas nébuleux, voici un PDF d’illustration.
Du côté de l’implémentation
Après l’interface, voyons l’implémentation. A priori, cela peut sembler difficile. Pourtant, grâce aux types fantômes, nous n’avons presque rien à faire ! Définissons d’abord le type picture
:
type 'a picture = {pict: OImages.rgb24_class}
Ne vous étonnez pas de trouver ici un record alors qu’il paraît inutile. Le vrai type picture
, que j’utilise dans mon application, contient d’autres champs (mais aucun n’est de type 'a
, c’est très important). Il a été simplifié ici par souci de pédagogie.
Définissons maintenant trois types fantômes :
type fullcolor type grayscale type monochrom
Un type fantôme est un type utilisé comme paramètre d’un autre type (à la manière du type int
dans int list
), mais qui ne sert pas dans la définition de ce type (comme 'a
dans 'a picture
ci-dessus). Le type fantôme vous permet d’ajouter une information (une contrainte) qui sera propagée correctement par l’algorithme d’inférence de types, sans impact sur l’implémentation. Dans mon cas, il s’agit surtout de s’assurer que l’on passe bien une image en niveaux de gris à la fonction black_and_white
, par exemple.
Nous pouvons maintenant écrire la fonction de création :
let from_file str = {pict = OImages.rgb24 (OImages.load path [])}
puis les fonctions utilitaires :
let get_width t = t.pict#width let get_height t = t.pict#height let get_pixbuf t = Imagegdk.to_pixbuf t.pict#coerce let copy t = {t with pict = Oo.copy t.pict}
Nous pouvons maintenant en venir aux fonctions qui agissent directement sur l’image et, comme vous allez le constater, on ne se doute de rien en lisant cette partie du code :
let grayscale ?(filter = Color.brightness) pic = let res = copy pic in let img = res.pict in for x = 0 to img#width - 1 do for y = 0 to img#height - 1 do let res = filter (img#unsafe_get x y) in img#unsafe_set x y {Color.r = res; g = res; b = res} done done; res let black = {Color.r = 0; g = 0; b = 0} let white = {Color.r = 255; g = 255; b = 255} let black_and_white ?(threshold = 0x90) pic = let res = copy pic in let img = res.pict in for x = 0 to img#width - 1 do for y = 0 to img#height - 1 do let rgb = img#unsafe_get x y in img#unsafe_set x y (if rgb.Color.r > threshold then white else black) done done; res
et ainsi de suite (le détail d’implémentation des fonctions
et fill_gaps
sort du cadre de ce billet; je peux développer sur demande, mais il ne me semble pas pertinent de le faire ici). Avec un appel à ocamlc -i
, toutes ces fonctions accepteraient des images de type 'a picture
. La contrainte que nous avons posée dans l’interface est à la fois valide et transparente du côté de l’implémentation.
Il est intéressant de noter que les types fantômes apportent une information supplémentaire sans impact sur l’implémentation. Cela renforce la sûreté du code sans nuire aux performances (imaginez la lourdeur qui consisterait à s’assurer d’abord que tous les pixels sont bien des teintes de gris !).
En guise de conclusion…
Je parlais d’un aller simple vers le monde des fantômes. Vous savez maintenant pourquoi : quand on y a goûté, on ne peut plus s’en passer. Les types fantômes, c’est bien, mangez-en !
À bientôt,
Cacophrène
Bonjour,
Merci pour ton commentaire.
Merci pour cet article, très intéressant !