Category Archives: Enseignement

L'image numérique nobélisée, l'occasion d'expliquer le fonctionnement des CCDs

    Je ne fais que très rarement de commentaires sur l'actualité, mais cette année le comité Nobel à choisi de récompenser les travaux des "maîtres de la lumières" (voir le billet de S. Huet ou celui de A. Gunthert) : Charles K. Kao, Willard S. Boyle, George E. Smith. Les deux derniers pour l'invention de l'imageur semi-conducteur qui a permis à l'image numérique de prendre son essor : le CCD (Charged Coupled Device). Le CCD est le récepteur d'image que l'on trouve dans les appareils photos numériques et les camescopes. Ce récepteur tend à être remplacé par des capteurs CMOS, moins consommateurs d'énergie mais moins précis. Je ne peux donc que me réjouir de cette annonce car c'est la première fois qu'un Nobel récompense des travaux dans un domaine proche du mien.

    Je profite donc de l'occasion pour expliquer comment fonctionne un capteur CCD.

    ccd

    Get the whole story »

    Représentation d'images (Clojure et Java), première partie.

    Dans un billet précédent (Image numérique et tableau : est-ce identique ?), je me plaignais de l'impossibilité chronique de représenter des images avec des indices négatifs. Pour ne pas rester sur une simple constatation, et aussi parce que j'en ai besoin, je vous livre mes premières lignes de code.

    Tout d'abord, quelles sont les contraintes? J'écris mes programmes en Clojure. Pourquoi? Parce que c'est un Lisp et j'aime le Lisp. Ensuite parce c'est un langage rapide, dynamique et tournant sur une machine virtuelle java. Ce dernier point est important car mes programmes doivent être portables. Pour être vraiment portable, ma représentation d'une image devra être complètement transparente pour un développeur Java. Je doit donc écrire une classe. Mais comme je n'aime pas le langage Java, cette classe sera écrite en Clojure. Cela constituera un bon exercice.

    Définitions

    En généralisant à l'extrême, une image n'est rien d'autre qu'une fonction à deux dimensions et bornée. La classe Imfun devra donc hériter de clojure.lang.IFn, l'interface représente les fonctions en Clojure. Ce classe comporte une méthode invoke qui est appelée lorsqu'une instance est utilisée comme une fonction. Il suffit donc d'avoir comme état interne de chaque instance : un domaine de définition et une fonction. Voici le début de la classe Imfun :

    (ns tip.Imfun
      (:gen-class
       :implements [clojure.lang.IFn]
       :state state
       :init init
       :methods [[fun [] clojure.lang.IFn]
                 [domain [] clojure.lang.IPersistentCollection]]
       :constructors {[clojure.lang.IPersistentCollection clojure.lang.IFn] []}))
    Quelques explications :

    • la première ligne indique l'espace de nom utilisé : tip.Imfun,
    • :gen-class permet la génération des fichiers *.class,
    • la classe implémente l'interface clojure.lang.IFn,
    • l'état interne de chaque instance sera stocké dans state,
    • l'initialisation de chaque instance sera réalisée par la fonction init,
    • deux méthodes seront exposées : fun et domain,
    • les arguments du constructeur de la classe seront : une collection (pour le domaine de définition) et une fonction.

    La fonction init doit renvoyer un vecteur comportant : 1/ les arguments à passer au constructeur de la classe parente (rien ici) et 2/ l'état de l'instance crée. Les arguments de init doivent être conformes à ceux définis par :constructors.

    (defn -init [dom f]
      [[] [dom f]])
    Les deux méthodes fun et domain ont pour rôle de renvoyer la fonction sous-jacente et le domain de définition de l'image, tout deux stockés dans state.
    (defn -domain [this]
      (first (.state this)))
    (defn -fun [this]
      (second (.state this)))
    La méthode invoke est utilisée lors de l'application de l'instance en tant que fonction. Lors de cette application, l'appartenance des coordonnées au domaine de définition doit être vérifiées (via la fonction indom?). Si les coordonnées sont en dehors du domaine de définition, l'application renvoie la valeur 0 (cela sera modifié plus tard).
    (defn indom? [dom pt]
      (let [[start end] dom
            [x y] pt]
        (and (>= x (first start))
             (<= x (first end))
             (>= y (second start))
             (<= y (second end)))))

    (defn -invoke ([this x y] (if (indom? (.domain this) [x y]) ((.fun this) x y) 0)) ([this pt] (let [[x y] pt] (this x y))))

    Organisation des fichiers et compilation

    Admettons que mon répertoire de travail soit work. Il faut créer deux sous-répertoire src et classes. Ce dernier contiendra les *.class générés. L'arborescence dans src doît être conforme à l'espace de nom utilsée. Ainsi le code précédent doit être contenu dans le fichier work/src/tip/Imfun.clj. A partir de là, la compilation est réalisée par la ligne suivante :

    fredm:work> java -cp clojure.jar:./src:./classes clojure.main -e "(compile 'tip.Imfun)"
    (cette compilation pourrait être réalisée dans la boucle d'évaluation de Clojure).

    Exemple d'utilisation

    Ouvrons la boucle d'évaluation Clojure :

    fredm:work> java -cp clojure.jar:./src:./classes clojure.main -e
    Clojure 1.1.0-alpha-SNAPSHOT
    user=> (import '(tip Imfun))
    tip.Imfun
    Créons une image, définie de (-1, -1) à (1, 1) et dont la valeur renvoyée vaut la somme des coordonnées :
    fredm:work> java -cp clojure.jar:./src:./classes clojure.main -e
    Clojure 1.1.0-alpha-SNAPSHOT
    user=> (def f #(+ %1 %2))

    'user/f

    user=> (def im (Imfun. [[-1 -1] [1 1]] f))

    'user/im

    im est maintenant une fonction bornée, qui renvoie 0 en dehors de son domaine de définition.

    user=> (.domain im)
    [[-1 -1] [1 1]]
    user=> (im -1 -1)
    -2
    user=> (im 0 1)
    1
    user=> (im 1 2)
    0
    Il est possible de passer comme argument à im, soit deux coordonnées x et y, soit une collection de deux éléments comportant les coordonnées (i.e. la représentation d'un point):
    user=> (im 1 1)
    2
    user=> (im [1 1])
    2
    user=> (im '(1 1))
    2
    Voila, la prochaine fois nous hériterons de cette classe pour définir des images dont les valeurs seront stockées dans un tableau à deux dimensions. En fait nous utliiserons un ImageProcessor de la librairie d'ImageJ.

    FMN.

    Projet Euler avec Clojure (1)

    L'énoncé du premier problème du Projet Euler est :

    If we list all the natural numbers below 10 that are multiples of 3 or 5, we get 3, 5, 6 and 9. The sum of these multiples is 23. Find the sum of all the multiples of 3 or 5 below 1000.

    Il s'agit donc de calculer la somme des multiples de 3 ou 5 inférieurs (strictement) à 1000.

    Une première solution naïve, consiste à parcourir tous les entiers de 0 à 999, ne retenir que les multiples de 3 ou 5, puis de sommer les nombres restants :

    (def sum #(reduce + %))

    (defn euler1-a [] (sum (filter #(or (zero? (rem % 3)) (zero? (rem % 5))) (range 1000))))

    La valeur calculée est :
    user> (euler1a)
    233168
    qui est la bonne réponse.

    Cette solution a l'avantage de la lisibilité : elle est la traduction directe du problème en programme. Cependant elle n'est pas efficace en terme de temps de calcul. Tous les nombres inférieurs à 1000 sont parcouru, y compris ceux qui ne sont multiples ni de 3, ni de 5. Il serait donc plus efficace de ne considérer que les multiples intéressants.

    Une seconde solution vient lorsque l'on voit qu'une séquence des multiples de 3 est obtenue avec :

    (range 0 1000 5)
    Il suffit donc de d'additionner les multiples de 3 et de 5 :
    user> (+ (sum (range 0 1000 3))
             (sum (range 0 1000 5)))
    266333
    Le résultat obtenu est faux. En effet certains nombre apparaissent dans les deux séquences de multiples : tous les nombres ayant 3 et 5 comme multiples communs. Voici les multiples communs inférieurs à 100 :
    user> (intersection (set (range 0 100 3)) (set (range 0 100 5)))

    {0 75 45 15 90 60 30}

    Il ne s'agit en fait que des multiples de 15 (i.e. 3 * 5). Pour obtenir le bon résultat, il s'agit donc de retrancher la somme des multiples de 15 de la somme initiale :

    (defn euler1-b []
      (+ (sum (range 0 1000 3))
         (sum (range 0 1000 5))
         (- (sum (range 0 1000 15)))))
    Cette fois ci le résultat est le bon :
    user> (euler1b)
    233168
    Pour s'amuser un peu, voici une solution itérative qui ne considère que les multiples différents de 3 et 5. L'idée est d'avoir deux compteurs, un pour les multiples de 3 et un second pour les multiples de 5. A chaque itération, un seul des deux compteurs est incrémenté (de 3 pour celui des multiples de 3 et de 5 pour celui des multiples de 5) et la valeur de ce compteur est ajoutée à la somme finale :
    (defn euler1-c []
      (loop [x3 0
             x5 0
             s 0]
        (let [x3+ (+ 3 x3)
              x5+ (+ 5 x5)]
          (if (or (< x3+ 1000)
                  (< x5+ 1000))
            (cond
              (= x3+ x5+) (recur x3+ x5+ (+ x3+ s))
              (< x3+ x5+) (recur x3+ x5 (+ x3+ s))
              :else (recur x3 x5+ (+ x5+ s)))
            s))))
    A chaque itération, le choix du compteur est réalisée en retenant le plus petit des deux. Par exemple si x3 vaut 9 et x5 vaut 10, alors x3+ vaut 12 et x5+ vaut 15. Dans ce cas, c'est x3 qui est incrémenté. Si les deux compteurs sont égaux, les deux sont incrémenté pour ne pas prendre en compte deux fois le même nombre. La valeur calculée est juste :
    user> (euler1c)
    233168
    Voici une petite comparaison des temps de calculs basée sur la répétition de 1000 appels de chaque fonction :
    user> (defmacro timex [n stuff]
            `(time (dotimes [x# ~n]
                     ~stuff)))

    'user/timex

    user> (timex 1000 (euler1-a)) "Elapsed time: 2270.722148 msecs" nil user> (timex 1000 (euler1-b)) "Elapsed time: 1337.681488 msecs" nil user> (timex 1000 (euler1-c)) "Elapsed time: 534.767148 msecs" nil

    La fonction euler1-c qui ne considère que les multiples différents de 3 et de 5 est clairement la plus rapide. Par contre elle est loin d'être la plus claire. Comme souvent, c'est une affaire de compromis.

    FMN.