La dernière fois j’avais montré comment représenter une image en tant qu’une fonction bornée (voir ce billet). La classe java était intégralement écrite en Clojure. Cette représentation correspond à la représentation mathématique d’une image, mais n’est pas très efficace. Habituellement une image est stockée sous forme d’un tableau à deux dimensions. Cette structure permet de courts temps d’accès aux pixels de l’image.
Ainsi dans la librairie ImageJ sous Java, une image est représentée par une classe abstraite : l’ImageProcessor. Quatre classe en héritent et propose une implémentation concrète : ByteProcessor, ShortProcessor, FloatProcessor et ColorProcessor (voir l’api pour plus de détails) Comme je l’avais déjà souligné, le défaut de ces structures de données est de ne pouvoir représenter des images avec des pixels de coordonnées négatives.
Nous allons donc coder une représentation d’image basée sur un ImageProcessor, mais autorisant l’emploi d’indices négatifs. La classe Image héritera de la classe précédente Imfun.
Initialisation
(ns tip.Image (:gen-class :extends tip.Imfun :state state :init init :methods [[ip [] ij.process.ImageProcessor] [set [Integer Integer Object] void]] :constructors {[clojure.lang.IPersistentCollection ij.process.ImageProcessor] [clojure.lang.IPersistentCollection clojure.lang.IFn]})) (defn -init [start ip] (let [[xs ys] start dom [start [(dec (+ xs (.getWidth ip))) (dec (+ ys (.getHeight ip)))]]] [[dom #(.get ip (- %1 xs) (- %2 ys))] [dom ip]]))
En interne, l’image est stockée sous forme d’un domaine de définition (dom) )et d’un ImageProcessor (ip), stockés tous deux dans state. La méthode init fabrique le domaine de définition à partir des coordonnées de l’origine de l’image et des dimensions de l’image, issues de l’ImageProcessor. La fonction init doit renvoyer un vecteur comportant : les arguments à passer au constructeur de la classe parente l’état de l’instance créée. La classe parente étant Imfun, la première partie du vecteur renvoyé contient donc une fonction d’accès aux pixels de l’ImageProcessor.
Accès à l’ImageProcessor
(defn -ip [this] (second (.state this)))
Rien de spécial sinon que dans la classe parente, le second élément de state contient une fonction alors qu’ici c’est l’ImageProcessor. De cette façon la référence au super constructeur :
:constructors {[clojure.lang.IPersistentCollection ij.process.ImageProcessor]
[clojure.lang.IPersistentCollection clojure.lang.IFn]}
est un peu bidon puisque la fonction interne n’existe plus. De plus, dans la classe parente Imfun, la fonction fun permet de récupérer la fonction interne et donc d’accéder aux valeurs de l’image :
(defn -fun [this] (second (.state this)))
Il est donc indispensable de modifier la fonction fun de façon à exploiter l’ImageProcessor interne.
Accès aux pixels
La fonction fun de Image doit renvoyer la valeur d’un pixel. Pour récupérer la valeur du pixel d’un ImageProcessor, ImageJ propose plusieurs méthodes : get et getPixel qui renvoient un int, getf et getPixelValue qui renvoient un float. Les méthodes get et getf sont les plus rapide car aucune vérification de dépassement n’est réalisée.
Pour ne proposer qu’une seule fonction d’accès fun, une fonction polymorphe est utilisée :
(defmulti -fun (fn [this] (class (.ip this))))
La délégation est basée sur la classe concrète de l’ImageProcessor stocké.
Dans le cas d’un FloatProcessor, la méthode getf est utilisée :
(defmethod -fun ij.process.FloatProcessor [this] (let [[xs ys] (first (.domain this)) fp (.ip this)] #(.getf fp (- %1 xs) (- %2 ys))))
Pour un ByteProcessor, la méthode get renvoie un octet contenu dans un int. Il convient de ne sélectionner que les 8 bits de poids faibles de cet int:
(defmethod -fun ij.process.ByteProcessor [this] (let [[xs ys] (first (.domain this)) bp (.ip this)] #(bit-and (byte (.get bp %1 %2)) 255)))
De même pour le ShortProcessor, seuls les 16 bits de poids faibles sont conservés :
(defmethod -fun ij.process.ShortProcessor [this] (let [[xs ys] (first (.domain this)) sp (.ip this)] #(bit-and (short (.get sp %1 %2)) 65535)))
Par contre, pour un ColorProcessor, l’int contient la couleurs codées sur 3 octets (un pour chaque composante) :
(defmethod -fun ij.process.ColorProcessor [this] (let [[xs ys] (first (.domain this)) cp (.ip this)] #(.get cp %1 %2)))
Modification des pixels
Dans le même esprit que la méthode fun, la méthode set est une fonction polymorphe pour modifier la valeur d’un pixel
(defmulti -set (fn [this x y v] (class (.ip this)))) (defmethod -set ij.process.FloatProcessor [this x y v] (let [[xs ys] (first (.domain this))] (.setf (.ip this) (- x xs) (- y ys) v))) (defmethod -set :default [this x y v] (let [[xs ys] (first (.domain this))] (.set (.ip this) (- x xs) (- y ys) v)))
Discussion
Cette représentation d’une image numérique est complète. Il est possible d’avoir une image en tant que fonction bornée ou en tant que tableau à deux dimensions. Ces deux alternatives permettent l’emploi de coordonnées négatives.
Le défaut de cette représentation est l’utilisation d’une structure de donnée ImageProcessor qui est modifiable. Le canon de la programmation en Clojure est d’inciter à l’utilisation de fonctions pures. C’est à dire de fonctions qui ne s’appuient pas sur un état interne. L’api de Clojure propose ainsi des séquences immuables avec des fonctions pour les manipuler.
Le prochain billet sera donc consacré à l’essai d’une représentation d’image basée sur des structures de données de Clojure.
FMN.
- Une sélection (automatique) de billets similaires :
- Fonction avec méta-données (Clojure). Représentation d'images #3
- Représentation d'images (Clojure et Java), première partie.
- Plugin ImageJ minimal en Clojure: inverser une image
- Image numérique et tableau : est-ce identique ?
- Utiliser ImageJ dans un notebook Sage, un exemple d'appel de Java depuis Python
Tags: clojure, image, imagej, programmation, représentation, traitement d'image