Utiliser ImageJ dans un notebook Sage, un exemple d'appel de Java depuis Python

(archive du 2009.11.10)

Sage est un logiciel formidable : un grand ensemble de langages et de librairies de mathématiques, avec Python pour faire la glue. Tout cela est assez remarquable, mais j’utilise beaucoup Java pour développer mes applications de traitement et d’analyse d’images. En particulier j’exploite la plateforme ImageJ qui permet de construire des plugins assez facilement distribuables. Il s’agit donc de pouvoir utiliser (facilement) une librairie Java avec Python, donc Sage. La méthode complète est décrite dans ce billet, permettant d’exploiter ImageJ dans Sage.

Comme Sage s’appuie sur Python, il suffit de trouver une méthode pour appeler du code Java depuis Python. Plusieurs solutions existent (si j’exclue les trucs bricolables avec du RMI):

  • Javaclass : pas réussi à le faire fonctionner. Le passage de valeurs me semblant de toute façon nécessiter du travail.
  • JCC : un peu lourd à mettre en place, en particulier le nécessité d’ajouter des options longues comme le bras au lancement de python.
  • JPype : le plus simple, se présentant comme un module python et d’une utilisation très aisée.

Installation de JPype

L’installation de JPype est assez classique :

unzip JPype-0.5.4.1.zip
cd JPype-0.5.4.1
sage -python setup.py build

Ici cela peut coincer car JPype utilise le module sets qui n’est plus inclu dans les versions récentes de python. Il n’y a pas beaucoup de modifications à apporter. Vous pouvez télécharger la version modifiée. Attention il faut tout de même suivre les instructions données dans le README.TXT pour donner le chemin vers votre installation JDK. Si la compilation se passe bien (sinon m’envoyer un mail), il suffit d’installer le module :

sage -python setup.py install

Maintenant nous allons travailler dans un notebook Sage (téléchargeable ici).

Utilisation de JPype

C’est assez direct :

  1. importer le module,
  2. lancer la machine virtuelle,
  3. importer un ou plusieurs packages java.

Comments

fmn: Après quelques discussions avec des collègues, ce genre de transfert est bien le point faible de JNI. Si j’ai le temps, je regarderais les performances de JNA qui indique : Java array and NIO Buffer arguments (primitive types and pointers) as pointer-to-buffer. A voir.

Laurent: Je n’ai pas trouvé d’autres solutions. Finalement, je m’en remets à l’échange d’images par lecture/écriture : les problèmes de synchronisation et de gestion d’erreurs entre les deux machines sont gérés par jpype, et les performances sont correctes, sauf pour l’écriture par imagej qui semble prendre environ 0.5 sec sur un pc avec peu de ram (1 Go) alors que ce chiffre tombe à 0.05 sur un autre pc avec 2 Go de ram, ou lors du premier chargement de la machine java. C’est un comportement étrange qui semble lié à la mise en cache de la machine. Merci pour tout. Laurent

Laurent: Hum, exact. Bien vu pour le coup du 2D python vers 1D java. J’étais passé à côté. Grand merci.

fmn: Il semble que la conversion dans les deux cas ne porte pas sur le même nombre d’éléments :

im = np.zeros((1024,1024), dtype = np.uint8)
len(JArray(JByte, 1)(im)

renvoit la valeur 1024. Mais le tableau renvoyé ne contient que des uint8. Comme en Java, un tableau 2D est un tableau de tableau, seuls les premiers éléments soient retenus. Par contre:

im = np.zeros((1024,1024), dtype = np.uint8)
len(JArray(JByte, 1)(im.flatten())

donne la bonne valeur 1048576, mais pour une durée ~0.67s. Sinon mes essais actuels pour affecter le tableau ainsi créer à un ByteProcessor echouent également. Je continue…

Laurent: J’ai fait un nouveau test, en utilisant les wrappers de jpype. Et je me suis aperçu de la chose suivante : (image est de type numpy.array de 2 dimensions, 1024*1024, dtype=uint8) JArray(JInt,2)(image) : 4 sec. Permet de transférer vers un objet ByteProcessor de imagej avec la méthode setIntArray de ImageProcessor JArray(JInt,1)(image) : 0.003 sec !!! Mais je n’ai pas trouvé de possibilité, dans l’api imagej, de transférer vers un objet ImageProcessor. JArray(JByte,1)(image) : 0.0012 sec. Je pensais pouvoir transférer les données vers un objet ByteProcessor grâce à la méthode setPixels. Mais jpype me retourne une erreur d’exception alors que le code de cette méthode devrait marcher (pour ce que je connais de java). Conclusion : je ne sais pas ce qui entraine ce facteur 1000 entre les deux types de conversion (1D ou 2D). Le code python ou le code java ? En outre, je ne trouve pas de point d’entrer dans le code imagej pour convertir JArray(JByte,1) vers ByteProcessor. C’est vraiment frustrant, car là je sens que j’approche d’une solution. Une solution serait de créer un plugin imagej permettant ce transfert. Mais là je touche à mes limites : je ne suis pas codeur java. Je vous tiens au courant si je progresse.

fmn: Il semble que jni permette d’obtenir depuis C un pointeur sur un tableau (voir http://java.sun.com/docs/books/jni/html/objtypes.html#27346). Mais je n’ai jamais testé cela. En fait pour moi l’intérêt de JPype est d’accéder à java, sans mettre les mains dans JNI. Mais il semble qu’il y ait un certain prix à ce confort. Peut-être que les autres méthodes que je mentionne en début de billet (JCC et JavaClass) sont plus performantes? En tout cas, merci d’avoir sur ce problème et je suis preneur d’infos sur ce point. FMN.

Laurent: J’ai aussi fait un test sur une machine plus musclée d’un collègue. Le temps passe de 5 à 3 secondes. Donc pas de miracle. Je voulais éviter le mécanisme d’échanges de données sur disque, car, pour assurer une fiabilité correcte, il faut prévoir et coder un “vrai" protocole d’échange (synchronisation des deux logiciels, gestion de message d’erreur, adaptation des chemins si distribution du logiciel, “handshake" etc…) entre les deux programmes. Et ce, des deux côtés ce qui implique de coder un plugins imagej dédié. Effectivement, l’auteur indique bien des problèmes de performance mais je n’aurais jamais imaginé que cela pouvait tomber si bas (1024x1024 octects ce n’est jamais qu’un objet “petit" objet de 1 Mo). J’ai regardé s’il y avait moyen de partager entre imagej et python l’espace mémoire directement sans copie préalable (transmission simple de pointeurs). Mais, de ce point de vue, java et l’api imagej ne semblent pas offrir cette possibilité. Enfin, il y a encore une possibilité : je regarde s’il est possible de transférer non pas l’image brute mais déjà compressée en mémoire. Mais cela semble aussi complexe. Je suis donc sur le point de renoncer et de m’en remettre à la conversion de plugins java en C au sein d’une dll. En tout cas merci d’avoir pris le temps de jeter un oeil à ces problèmes. Si jamais je trouve une astuce, je ne manquerai pas de la partager sur votre blog. Laurent

fmn: Laurent, effectivement avec java il faut inclure dans le classpath, le chemin de toutes les librairies. Sinon pour le problème de transfert. Mes temps de conversions sont du même ordre. Par exemple

ar = im.getProcessor().getIntArray()
ar_python = np.array(ar)

donne un temps d’env. 5s (pour une image 1024 par 1024). Par contre en ajoutant une indication du type du tableau crée:

ar = im.getProcessor().getIntArray()
ar_python = np.array(ar, dtype=np.uint8)

le temps de conversion “tombe" à ~2s. Ce qui reste assez inexploitable si des transferts fréquents sont nécessaire. L’auteur de JPype (voir Performances dans la doc) est conscient de cette inéfficacité, due à jni semble-t-il. La comparaison avec le transfert en utilisant ctypes est forcément en faveur de ctypes, puisque l’on reste dans le même “monde". Ici il y a un transfert forcé de/vers la jvm. Dans ton cas, je procéderai autrement. Des tests rapides montrent qu’en passant par une écriture/lecture disque, on peut transférer une image beaucoup plus rapidement. Par exemple, avec:

ij.IJ.save(im, 'tmp.gif')
ar_python = pylab.imread('tmp.gif')

le temps de transfert est de 0,08s.

fmn: Laurent, je suis ravi d’apprendre que ce tutoriel est profitable. Concernant l’appel de plugins, quelle est exactement la difficulté : appeler des plugins depuis python ou appeler des plugins tout court ?

Laurent: Bonjour et merci de la réponse. La difficulté était d’appeler des plugins ImageJ depuis Python. Pour une raison simple (voir après) mais mal indiquée par le module, je n’arrivais pas à accéder aux classes de ces plugins. En fait, pour un plugin imagej donné, il faut fournir non seulement le chemin de son .jar mais aussi ceux de toutes les classes dont il dépend dans le path java. Le message d’erreur de jpype ne m’a pas beaucoup aidé, puisqu’il indiquait toujours qu’il ne trouvait pas la classe principale que je tentais de lier, alors qu’en fait c’était les dépendances qui n’étaient pas trouver. Ce problème est donc résolu. Par contre, au final, je suis très déçu : les échanges de donnés entre python et imagej sont très très lents. Sur mon pc, une image de 1024x1024 octets met un temps > 5 sec pour transiter d’un objet numpy.array vers un objet ByteProcessor, et vice versa (voir votre exemple avec une image 1024x1024). Je pensais accéder à toute la puissance des traitements disponibles sur imagej depuis le projet que je développe en Python. C’est le cas, mais cette lenteur est, pour mon projet, inacceptable : je vise un temps de traitement global inférieur à la seconde (ce qui est déjà énorme sur les ordinateurs d’aujourd’hui). Et je voulais éviter de devoir coder les plugins imagej qui m’intéressaient de java vers C et créer une dll appelable depuis Python. Savez-vous si cette lenteur de transfert est normal et s’il n’y a pas moyen d’accélérer les choses ? Pouvez-vous faire l’essai avec votre exemple et une image de 1024x1024 pixels d’un octet ? A titre de comparaison, le transfert en python et une dll à l’aide de ctypes est quasi-instantané. Merci encore une fois de partager votre expérience.


Laurent: Merci beaucoup pour ce tutoriel qui est une excellente mise en jambe et ouvre à Python, par ce biais un champ immense de possibilités en traitement d’image. Votre exemple marche parfaitement sous Windows aussi. Par contre, je me casse les dents sur l’appel de classes de plugins installés sur ImageJ. Si vous avez surmonté cette difficulté, un tuyau serait le bienvenu. Dans tous les cas, merci beaucoup d’avoir partagé votre précieuse expérience.