Programmation ImageJ
Transcription
Programmation ImageJ
Programmation ImageJ 1.Intérêt de la programmation orientée objet Avant-propos Avant de commencer à programmer dans ImageJ, il faut mieux avant bien comprendre ce qu'est la Programmation Orientée Objet, son vocabulaire et ce qui la différencie de la programmation linéaire (appelée aussi procédurale). Sinon, il vous sera impossible d'écrire un plugin ImageJ, voire un programme Java. 1.1 Principe de la POO : le choix de Java La programmation objet s'appuie sur l'utilisation d'objets (non sans rire ...), c'estàdire que vous créez un objet qui va avoir une fonction précise. L'intérêt de cet objet est qu'il est réutilisable dans n'importe quel programme. Prenons un exemple simple, créons un objet, appelons le connexion, dont le but est de vous connecter à une base de données. Vous codez une fois cet objet et ensuite, dans tous vos programmes, il vous suffit de l'appeler. Plus besoin de recoder cette portion de code. Après le codage de plusieurs objets, on peut dire que la conception d'un programme ressemble plus à un jeu de Lego. Vous y incorporez les objets dont les fonctions vous sont utiles. Je pense que vous voyez l'intérêt ... Vous pouvez récupérer des objets et les réutiliser à volonté donc gain de temps. De plus, Java possède une très grande bibliothèque d'objets prêts à servir. 1.2 POO vs. Programmation linéaire Lors d'une programmation linéaire (ou procédurale), on commence avant tout par chercher les taches à accomplir, puis on code des procédures qui, étape par étape, vont résoudre ces taches. Donc, on travaille avant tout sur les méthodes. En POO, on commence d'abord par isoler les classes (les actions si vous voulez) et seulement après on s'occupe des méthodes. De plus, en POO, chaque méthode est associée à une classe. Que nous apporte cette différence ? D'abord, les classes regroupent les méthodes créant ainsi une bibliothèque de fonctionnalités plus facilement utilisables que des bouts de code. De plus, l'encapsulation évite la corruption par d'autres objets. Donc, si vous avez un bug, cela ne peut venir que de votre classe et non pas d'une autre. Il en découle qu'au lieu d'examiner les 20000 lignes de code, vous n'avez qu'à regarder celles qui sont en rapport avec la classe défaillante. Bon, je vous vois venir, certains vont dire qu'en php quand ils codent, ils font des fonctions qu'ils appellent via des include. En fait, là, vous créez des modules. Mais ces modules ne peuvent avoir qu'une seule instance. Si vous voulez une autre instance, vous devez faire une copie de ce module et faire quelques modifications. Or en POO, une classe peut avoir un nombre illimité d'instances contrairement à vos modules. 1.3.Bases de Java Nous voilà arrivés dans le vif du sujet. Nous allons nous familiariser avec la structure d'un programme Java. En effet, les éléments qui le composent se retrouvent dans toutes les applications. 1.3.1 le code de notre programme public class PremierScript { public static void main(String [ ] args) { System.out.println("Mon premier script, c'est facile finalement"); } } Tout d'abord je tiens à vous rappeler une chose : Java est sensible à la casse. J'explique pour ceux qui ne connaissent pas ce terme. Lorsque vous tapez votre code il faut respecter les majuscules et minuscules. Par exemple si à la place de : public class PremierScript Vous tapez : Public class PremierScript Java ne reconnaîtra pas le mot clé public. Et oui tout cela à cause de la lettre « p » en majuscule. Il vous faut donc être rigoureux làdessus dès le départ. public Ce mot clé est appelé un modificateur d'accès, c'estàdire qu'il détermine quelles autres parties du programme ont accès à cette classe. class Tout ce que l'on programme se situe dans une classe, le terme ouvre donc une classe. PremierScript le terme se trouvant derrière le mot clé class est le nom de la classe. Vous pouvez donner n'importe quel nom à une classe sauf des noms de fonctions. Une règle pourtant, un nom de classe commence toujours par une lettre. Une autre remarque s'impose. Il faut toujours donner le même nom à votre fichier source que celui de la classe publique de votre programme. Ici notre code sera donc sauvegardé sous le nom de PremierScript (respecter là aussi la casse). main Le mot clé main ouvre une méthode. Lorsque vous exécutez un programme, Java va toujours chercher la méthode main. Sans cette méthode l'exécution est impossible. Bien sûr vous pouvez coder d'autres méthodes que la main mais cette dernière doit toujours être présente dans vos scripts. Notez bien les accolades. En java, comme dans d'autres langages, chaque bloc est délimité par une accolade. Donc pour résumer tous vos scripts doivent au moins avoir ça : public class NomdelaClasse { public static void main(String [ ] args) { Les instructions } } Car la classe publique est celle qui est appelée lors de l'exécution du programme et la méthode main est la méthode par défaut donc celle exécutée. En effet si on réfléchit lors de l'exécution de votre script vous tapez : Java NondelaClasse Vous appelez donc la classe Nomdelaclasse, or là vous ne pouvez spécifier quelle méthode vous appelez. Il faut donc que java ait une méthode à lancer par défaut. Devinez laquelle ? Oui, la main. Maintenant regardons ce que contient la méthode main de notre programme. Ici nous avons qu'une seule instruction : System.out.println("Mon premier script, c'est facile finalement"); Première remarque : Comme pour les macros, chaque instruction doit se terminer par un ; donc les retours à la ligne ne délimitent pas la fin d'une instruction. Ce qui d'un autre coté peut être plus que sympathique. Imaginez que vous avez une instruction très complexe et longue. Pour plus de lisibilité vous pourrez étaler son code sur plusieurs lignes. Ici notre instruction affiche une ligne de texte dans la console en l'occurrence : Mon premier script, C'est facile finalement Nous utilisons l'objet System.out et nous appelons sa méthode println. Pour invoquer une méthode d'un objet on utilise donc le . Objet . méthode(paramètre) La méthode println affiche le texte passé en paramètre puis saute une ligne. Histoire que vous le sachiez la différence, si vous aviez appelé la méthode print au lieu de println vous n'auriez pas eu de saut de ligne après l'affichage de votre texte. Je récapitule, nous avons donc vu la structure de base d'un programme java. Sans cela votre script ne peut s'exécuter. Et nous avons aussi abordé comment nous appelions une méthode d'un objet. Voyons maintenant les commentaires. 1.3.2. Les commentaires Les commentaires sont essentiels dans un script. Abusezen, cela ne rendra votre travail que plus facile et comblera de bonheur les personnes qui vous reliront ou qui voudront se baser sur votre plugin pour en écrire un autre. Java ne compile pas les commentaires , ils n'apparaissent donc pas dans votre exécutable. Pour mettre des commentaires on a le choix entre // et /* */ . Le // sert pour une ligne de commentairealors que /* */ englobe un bloc de commentaire. /* Code source de PremierScript Ici on n'est pas dans ImageJ, pas besoin de « _ » dans le nom de la classe */ public class PremierScript { public static void main(String [ ] args) { //Affichage du texte dans la console System.out.println("Mon premier script,c est facile finalement"); } } Vous avez maintenant les bases pour commencer à coder en java. Au cours des quelques chapitres suivants, je vais rappeler un certain nombre de notions générales qu'il faudra assimiler avant d'espérer programmer un plugin ImagJ. Vous pourriez aussi le faire « à l'instinct » en regardant beaucoup de codes sources de divers plugins, c'est d 'ailleurs comme ça que la plupart d'entre nous fonctionnent, mais vous n'aurez pas la pleine compréhension du fonctionnement et surtout des causes de nonfonctionnement d'un plugin (cela arrivera forcément). 1.3.3. Les variables Après avoir vu les notions de programmation orientée objet et la structure d'un programme de base, nous allons maintenant aborder le vaste sujet que représentent les variables. Quels noms leur attribuer, quels types sont à votre disposition, l'affectation, les différentes opérations réalisables ... Cette étape est essentielle dans l'apprentissage de Java. Une variable est un contenant où vous allez pouvoir mettre ce que vous voulez (les données). Lors de l'exécution de votre programme vous pourrez agir sur ces variables pour en modifier leurs valeurs (les données) via de multiples opérations. Voilà pour le concept de variable. Maintenant voyons quelques règles de base concernant la nomination de ces dernières. ● ● ● ● Le nom de votre variable devra toujours commencer par une lettre. Les espaces ne sont pas autorisés mais vous pouvez utilisez le _en remplacement Votre nom peut comporter des lettres et des chiffres Vous ne pouvez pas attribuer comme nom de variable le nom d'une fonction java du style if, inner ou autre. L'un des grands avantages de Java est que l'allocation et la libération de la mémoire (valeur en byte) sont automatiques. Mais cela ne vous dispense pas de devoir déclarer vos variables et surtout le type de la variable. Nous avons 8 types de variables : ● ● ● ● 4 pour travailler sur les nombres entiers (byte, short, int et long) 2 pour travailler sur les nombres réels (double et float) 1 pour travailler sur les chaînes de caractères (char) le type booléen (boolean) Déclarations des variables La déclaration d'une variable est quelque chose de vraiment facile. Rien de bien compliqué, on spécifie d'abord le type de la variable puis son nom. Chaque déclaration étant une instruction on termine par un « ; » . D'habitude, on déclare toutes les variables au début du bloc où on les utilise. Exemple : int nom_variable ; Affectations de valeur Après la déclaration d'un variable, il est temps de la remplir, c'estàdire de lui affecter une valeur. On appelle aussi cette procédure l'initialisation. Pas besoin de longs discours un exemple sera suffisant : int nom_variable ; nom_variable =1 ; Mais bon comme les choses sont quand même bien faites en ce bas monde de temps en temps il est possible de déclarer et d'initialiser une variable en même temps : int nom_variable =1 ; Les constantes Il existe une forme particulière de variable, les constantes. Les constantes sont des variables dont la valeur ne pourra être modifiée dans le programme, des variables constantes quoi ... je sais c'est antinomique ... Pour définir une constante on utilise le motclé final. Final int NOM_VARIABLE =1 ; À partir de ce moment NOM_VARIABLE aura toujours la valeur 1 dans le bloc où elle a été déclarée. Beaucoup de programmeurs écrivent en majuscule le nom de leur variable, histoire d'améliorer la visibilité du code. Bon maintenant si nous voulons une constante accessible par toutes les méthodes d'une classe, il faut définir ce que l'on appelle une constante de classe. On définit alors la constante de classe en dehors de la méthode main permettant ainsi à d'autres méthodes de pouvoir l'appeler. Le motclé employé est ici : static final Un petit exemple : public class Tuto_variable { public static final int NOM_VARIABLE =2; public static void main(String [] arg) { int a =2; int b; b =a *NOM_VARIABLE; System.out.println("le Résultat est :"+b); } } Ce petit programme va tout simplement afficher le chiffre 4. Rien d'extraordinaire je le reconnais mais nous avons bien la preuve que la constante de classe est accessible. Je sais vous allez me dire : Pourquoi avoir mis public static final ? Il y a ici une petite notion en plus. Avec static final la constante de classe est uniquement accessible à l'intérieur de la classe où elle est définie. Pour la rendre accessible par d'autres classes nous plaçons le mot clé public. Pour appeler la constante d'une autre classe nous taperons : Tuto_variable.NOM_VARIABLE et le tour est joué. 1.3.4.Les opérateurs Pour ceux qui ont déjà l'habitude de la programmation, ce chapitre sur les opérateurs ne vous sera pas d'une grande utilité. Mais il faut bien penser à tout ceux qui débutent. Opérateur mathématique Opération Signification + Addition Soustraction * Multiplication / Division % Reste ou modulo Il existe plusieurs façons d'écrire les opérateurs : x=x1 Équivalent : x+=1 Opérateurs d'incrémentation et de décrémentation ● ● [++]incrémentation. Ajoute 1 []décrémentation. Enlève 1 int n =1 ; n++;//n vaut 2 maintenant Opérateurs relationnels et booléens ● [==]Égale ou équivaut Attention, cela ne marche que pour le numérique. Pour les chaînes de caractère la fonction est : chaine1.equals(chaine2) ● ● ● ● ● ● ● [!=]différent [>]supérieur [<]inférieur [>=]supérieur ou égal [<=]inférieur ou égal [&&]et [||]ou 1 == 2 ;//C'est faux 1 != 2 ;//ça c'est vrai Voici donc les différents opérateurs principaux utilisables en java. 1.3.5. Conversion de variables Il vous arrivera bien souvent de devoir convertir une variable dans un autre type pour pouvoir travailler dessus. Exemple : int a =233333 ; float b =a ;//b sera une variable de type float avec la valeur de a Pour convertir une chaîne de caractère en chiffre : String a=«40 » ; Int b =Integer.parseInt(a); 1.3.6. Boucles et Conditions Avec cette section, nous allons enfin posséder les bases de la programmation. Vous allez pouvoir élaborer des algorithmes de programmation utilisant des boucles, des conditions et bien sûr des boucles conditionnelles. Nous allons donc pouvoir par la suite créer de petits programmes simples. Par la suite, il vous faudra acquérir un nouveau type de connaissances, ce que j'appelle les « fonctions utiles de Java » , c'estàdire celles qui vous permettent par exemple d'utiliser la bibliothèque d'ImageJ. Les conditions Il arrive bien souvent que nous ayons à tester une variable ou exécution d'une fonction. Pour ceux qui ont déjà programmé ces notions devraient être acquises. Exemple : nous sommes dans une section membres où l'utilisateur doit s'identifier pour accéder à la section. Si l'utilisateur est identifié, on le redirige vers la page de son compte ; sinon, on affiche un message l'invitant à se retenter son identification. Pour cela, on a utilisé le code : if(la_condition) {bloc de code pour la condition juste} else {bloc de code pour la condition fausse} En gros voila ce que cela donne : import javax.swing.*; public class Tuto_if_else { public static void main(String [] arg) { String a ="fliper"; String demande =JOptionPane.showInputDialog("Votre identifiant :"); if(demande.equals(a)) { System.out.println("Vous etes identifié "); } else { System.out.println("Erreur,vous n\'etes pas identifié "); } System.exit(0); } } Il y a dans cet exemple deux nouvelles notions: ● ● la comparaison entre deux chaînes de caractères une boîte de dialogue pour entrer la valeur de la variable demandée la boîte de dialogue String demande =JOptionPane.showInputDialog("Votre identifiant :"); Cette commande permet d'ouvrir une boîte de dialogue contenant le message mis entre parenthèses. La méthode showInputDialog de la classe JOptionPane renvoit une chaîne de caractères donc si vous demandez un nombre vous devrez convertir la variable de cette manière : int demande =Integer.parseInt(demande); Ensuite notez le : System.exit(0); Chaque fois que vous appelez JOptionPane.showInputDialog vous devez terminez votre programme par System.exit(0);. La raison est purement technique. En fait l'affichage d'une boîte de dialogue démarre un nouveau thread de contrôle. Lors de la sortie de votre méthode main, le nouveau thread ne se termine pas automatiquement. Il faut donc tous les clore grâce à cette commande. Maintenant terminons par : import javax.swing.*; En fait, la classe JOptionPane est définie dans le package jawax.swing et il ne fait pas partir du package de base (java.lang). Il vous faut donc l'importer grâce à la commande import. Comparaison de deux chaînes de caractères L'égalité entre deux nombres a et b se teste comme ceci : a == b . Mais pour deux chaînes de caractères a et b, on fait : a.equals(b). Souvenezvous bien de cette différence, ainsi que du fait que JOptionPane.showInputDialog renvoie toujours une chaîne de caractères. Astuce : Mettez de la tabulation pour chaque bloc de code, les éditeurs de texte bien fait (comme vi ...) identent tout seul le code. Cela vous permettra de le rendre bien plus lisible, et si vous avez un bug vous passerez moins de temps à chercher l'accolade (par exemple) que vous n'avez pas fermée. Il arrive aussi qu'en faisant un algorithme conditionnel (terme barbare qui veut juste dire « si condition alors ») on se retrouve avec plusieurs cas possibles. Exemple : Si j'ai de l'argent, soit je pars en vacances dans le sud pour 100 euros, soit si j'ai plus que cela je pars aux Caraïbes (450 euros) et si j'ai rien ... je reste chez moi. En code ça donne : if(argent==100) {je pars dans le sud} elseif(argent==450) {Finalement j'ai assez pour partir aux Caraïbes} else {Bon je mange des pâtes, je n'ai pas assez d'argent} Donc: if(premier_condition) {Block de code si première condition vraie} if(deuxième condition car première fausse) {Block de code deuxième condition vraie} else {Block de code traitant le cas où les deux conditions sont fausses} Il est bien évident que l'on peut faire autant de elseif que l'on souhaite. Petit exercice : Reprenez le code de l'identification : Tuto_if_else et renommez le Tuto_if_elseif .Vous déclarez une nouvelle variable char et vous lui attribuez un nom d'utilisateur puis vous me rajoutez un elseif permettant de tester l'entrée de l'utilisateur. Le programme va alors vérifier si l'utilisateur est fliper ou l'autre membre, si oui c'est bon, sinon c'est faux ! Malheureusement, vous vous doutez bien que le code va vite devenir illisible si l'on a énormément de possibilité dans l'algorithme conditionnel. Pas de panique, on a pensé à tout. Dans ce cas précis, il existe la fonction switch !!! switch(variable_a_tester) { case 1 : bloc de code break; case 2 : bloc de code break; } default : Bloc de code si aucun case n'est vrai break ; } Dans le switch, on définit la variable qui va se faire tester, puis au niveau des case on met la valeur test. Si la condition est remplie, le bloc de code de ce case est utilisé, sinon, on passe à l'autre case. Si aucun case n'est vrai et que vous avez un default, ce dernier sera exécuté. Remarquez que chaque case se termine par break. Cette commande permet de délimiter le bloc de code du case alors ne l'oubliez pas. Autre remarque très importante la valeur des case doit TOUJOURS être un entier . Conclusion : Nous venons de voir les fonctions qui permettent de faire des tests. Vous vous apercevrez très vite que vous allez les utiliser à longueur de journée donc mémorisez les bien. Les boucles Boucle for Souvenezvous de vos punitions du primaire : copier 100 fois je ne ferai plus l'andouille près du radiateur ... souvenir, souvenir... Et on passait la soirée à écrire 100 fois la même phrase !!! En java, écrire 100 fois cette phrase punitive nous prendrait 5s. Pour cela, nous allons faire une boucle. Je savais bien que j'aurais dû apprendre le java en maternelle ça m'aurait bien servi ! La boucle est une fonction qui va exécuter un nombre de fois défini le code que l'on souhaite. for(init_compteur;valeur_de_fin;incrementation_du_compteur) { bloc de code à répéter } donc pour notre punition cela donne : public class Punition { public static void main(String [] arg) { int a ; for(a=0 ;a<100 ;a++) { System.out.println("je ferai plus l'andouille près du radiateur"); } } } Vous allez voir s'afficher 100 fois la phrase sur votre écran de sortie. Le danger de la boucle infinie Avertissement : faites attention à vos boucles. Une boucle mal faite peut devenir une boucle infinie c'estàdire qui ne s'arrête jamais. Les conséquences peuvent être plus que fâcheuses car vous allez saturez la mémoire de votre machine et donc sûrement la faire planter. Avec le for, il y a peu de chance d'être dans ce cas mais avec la prochaine fonction que nous allons voir, il vaut mieux être prudent !!! Boucle conditionnelles C'est pas le terme exact mais à mon avis il résume bien leurs fonctions. En gros la fonction while veut dire : tant que la condition est vraie alors tu continues à exécuter la boucle . Par exemple, on a une variable int nommée a et on lui donne la valeur de 0 while(a<25) { System.out.println(a); a++; } A chaque passage de la boucle, on a la variable a qui s' incrémente. Lorsqu'elle aura attend la valeur de 25, on sortira de la boucle. Revenons au danger de tout à l'heure, c'estàdire de la boucle infinie. Imaginons que l'on n'incrémente plus la variable a. while(a<25) { System.out.println(a); } Puisque la variable a reste toujours inférieure à 25, la boucle va tourner infiniment. Il existe aussi une autre écriture pour while, cette dernière exécute d'abord le bloc puis effectue le test pour savoir si oui ou non on rentre en boucle. Pour cela on écrit : do { System.out.println(a); a++; } while(a<10) Voila nous avons fait le tour des boucles et des conditions. On peut dire que vous avez à partir de maintenant de bonnes bases pour attaquer réellement la programmation en java. Pour eux qui ont déjà programmé, ces connaissances sont bien assimilées. Pour les débutants, n'hésitez pas à relire et à poser vos questions. Vous trouverez aussi des aides bien faites sur le Web. Bon courage à tous. 2.La programmation ImageJ 2.1. Généralités ImageJ est un logiciel dont les fonctions (dont la plupart sont des plugins ellesmêmes) peuvent être étendues par le biais de plugins. Entendez en français « modules » mais nous utiliserons le terme anglais par la suite. Les plugins sont des classes Java (on commence à connaître ...) placées dans un certain dossier, le dossier plugins d'ImageJ ou un de ses sousdossiers. 2.1.1 « Quick Way » On peut créer un plugin de façon très simple, exactement comme une macro avec l'enregistreur (Plugins > Macros > Record ...). On clique sur « Create » pour obtenir un fichier texte qu'on peut appeler comme on veut. Si dans ce miniéditeur de texte, on va vers « Edit > Convert to plugin », une nouvelle fenêtre s'ouvre qui porte le nom de votre nommage précédent assorti d'un « _ » avec l'extension « .java ». Le nom par défaut est « Macro » dans l'enregisteur donc « Macro_.java » pour notre macro Java. Cette fonctionnalité est très pratique car non seulement elle permet de développer un plugin très rapidement mais en plus elle vous créée le squelette de base de quasiment tous vos futurs plugins. import ij.*; import ij.process.*; import ij.gui.*; import java.awt.*; import ij.plugin.*; public class Youpi_ implements PlugIn { public void run(String arg) { IJ.run("Lena (47K)"); IJ.run("Add Noise"); IJ.run("Smooth"); } Voilà un exemple de code d'un tel plugin. On remarque l'import de différents packages, le fait que la classe principale doit contenir un « _ » dans son nom et qu'elle implémente une certaine classe PlugIn. 2.1.2.Types de plugins Il y a deux types de plugins. Ceux qui n'ont pas besoin d'image en entrée et qui implémentent la classe PlugIn (comme cidessus car il ouvre une imageéchantillon) et ceux qui en ont besoin et qui implémentent donc la classe PlugInFilter. 2.2.Petite présentation1 de l'API2 2.2.1. Les classes La classe principale est ij, de là dérive tout le reste. ij ImageJApplet Classe nécessaire pour faire tourner ImageJ dans un navigateur. L'avantage est qu'une installation n'est pas nécessaire, le désavantage est un accès limité aux ressources de la machine à cause de l'aspect sécurité des applets Java. ImageJ La classe principale d'une application ImageJ. Elle contient la méthode run, principal point d'entrée du programme, et la fenêtre principale d'ImageJ. Executer Une classe pour exécuter les commandes des menus dans des threads séparés (pour ne pas bloquer le reste du programme). IJ Une classe qui contient beaucoup d'utilitaires. ImagePlus La représentation d'une image dans ImageJ, basée sur un ImageProcessor. ImageStack Une pile d'images de taille variable. WindowManager L'ensemble des fenêtres ouvertes. ij.gui ProgressBar Une barre qui informe graphiquement de la progression d'une opération. GenericDialog Une boîte de dialogue modulaire pour interagir avec l'utilisateur. NewImage Pour créer de nouvelles images en partant de zéro. Roi Une classe qui représente une région d'intérêt d'une image. Si le plugin le supporte, on peut traiter la région d'intérêt seule et non pas toute l'image. ImageCanvas 1 La plus grande part de ce qui suit est une traduction de l'excellent « Writing ImageJ Plugins » de Werner Bailer 2 Application Program Interface Un support pour dessiner une image, dérivé de java.awt.Canvas. ImageWindow Une fenêtre pour afficher une image, dérivée de java.awt.Frame. StackWindow Un ImageWindow pour les stacks. HistogramWindow Un ImageWindow pour les histogrammes. PlotWindow Un ImageWindow pour les tracés de courbes. ij.io Un paquet de gestion des entréessorties, à savoir toutes les classes pour lire et écrire les images. ij.measure Les classes relatives aux mesures. ij.plugin La plupart des commandes ImageJ sont des plugins et se retrouvent donc dans cette classe ou ses dérivées. PlugIn Cette interface doit être implémentée par tout plugin qui n'a pas besoin d'image en entrée. Converter Implémentation pour convertir correctement des ImagePlus d'un type vers un autre. ij.plugin.filter PluginFilter Cette interface doit être implémentée par tout plugin qui nécessite une image en entrée (qui effectue donc une opération sur cette image). ij.process ImageConverter Une classe contenant les méthodes pour convertir les images d'un type dans un autre. ImageProcessor Une superclasse abstraite pour certains types d'images. Un imageProcessor contient les méthodes permettant de travailler réellement sur les données contenues dans l'image. StackConverter Le pendant de ImageConverter pour les stacks. StackProcessor Le pendant de ImageProcessor pour les stacks. ij.text Paquet qui contient les classes pour afficher et éditer du texte. 2.2.2 Interfaces Comme déjà évoqué en 2.1.2, ImageJ peut créer deux types de plugins; ceux qui n'ont pas besoin d'une image en entrée et qui implémenteront l'interface PlugIn, et ceux qui en ont besoin et qui implémenteront l'interface PlugInFilter. PlugIn En 2.1.1, nous avons déjà vu un plugin qui implémentait l'interface Plugin. Il avait été généré automatiquement par l'enregistreur de macros et transformé en plugin. Nous ne savions rien de l'API d'ImageJ. Maintenant, nous pouvons développer nos plugins sans utiliser l'enregistreur. À tout seigneur, tout honneur ; le premier programme développé par quelque programmeur que ce soit et dans tous les langages est un programme qui se borne à afficher un simple « Hello, World! ». Nous ne sacrifierons pas à la tradition : import ij.*; import ij.plugin.*; /** * Hello_.java * @author Moi-même */ public class Hello_ implements PlugIn { public void run(String str) { IJ.showMessage("Hello World en Java pour ImageJ !"); } } Commentaire de code : Nous importons d'abord les paquets dont nous aurons besoin ( motclé : import ), suivent quelques lignes de commentaires au format javaDoc3. Nous déclarons ensuite notre classe Hello en n'oubliant pas le nécessaire « _ » dans son nom ; cette classe implémente l'interface PlugIn (d'où l'import de ij.plugin.* qui contient notre interface). Cette interface n'a qu'une méthode publique : run. L'import de ij est nécessaire pour la méthode IJ.showMessage. Note : Nous verrons dans le cadre des travaux pratiques d'autres implémentations de l'interface PlugIn. PlugInFilter import ij.*; //Paquet général import ij.plugin.filter.PlugInFilter; // L'interface PlugInFilter 3 http://java.sun.com/j2se/javadoc/ import ij.process.*; // Les différents ImageProcessor import java.awt.*; public class Inverter_ implements PlugInFilter { public int setup(String arg, ImagePlus imp) { if (arg.equals("about")) { showAbout(); return DONE; } return DOES_8G+DOES_STACKS+SUPPORTS_MASKING; } public void run(ImageProcessor ip) { byte[] pixels = (byte[])ip.getPixels(); //Notez le «cast»4 en byte () int width = ip.getWidth(); Rectangle r = ip.getRoi(); int offset, i; for (int y=r.y; y<(r.y+r.height); y++) { offset = y*width; for (int x=r.x; x<(r.x+r.width); x++) { i = offset + x; pixels[i] = (byte)(255-pixels[i]); } } } void showAbout() { IJ.showMessage("About Inverter_...", "This sample plugin filter inverts 8-bit\n"+ "images. Look at the Inverter_.java source file to see how easy it is in \n"+ "ImageJ to process non-rectangular ROIs, to process all the slices in a stack\n" + "and to display an About box." ); } } 4 On appelle « cast » transtypage en français. Commentaire de code : L'interface PlugInFilter a deux méthodes : set et run. La méthode set permet ici au plugin de savoir sur quel type d'image il peut travailler et d'envoyer une fenêtre d'info sur le plugin. Notez comme on récupère facilement, largeur de l'image, taille de la ROI et un tableau mono dimensionnel des pixels de l'ImageProcessor. On calcule un offset pour ne pas calculer la position de chaque retour à la ligne, on imbrique alors deux boucles pour parcourir l'image. 2.3.Gestion des images En regardant les plugins précédents, on s'aperçoit que les images sont représentées par les objets ImagePlus et ImageProcessor. Quelle est la différence et qu'en estil réellement ? 2.3.1.Types d'images Vous savez maintenant qu' ImageJ est capable de gérer plusieurs types d'images : ● 8 bits niveaux de gris ● 8 bits couleur avec table de couleur ● 16 bits niveaux de gris ● RVB couleur chaque couche étant en 8 bits ● 32 bits niveaux de gris ImagePlus est un objet qui représente une image. Il est basé sur un ImageProcessor, classe qui manipule le tableau de pixels et traite donc réellement l'image. Le type d' ImageProcessor utilisé dépend du type d'image. Ces types d'images sont représentées par des constantes dans ImagePlus : ● COLOR_256 ● COLOR_RGB ● GRAY16 ● GRAY32 ● GRAY8 Je vous laisse deviner à quel type correspond chaque constante ... Évidemment, puisque chaque image est manipulée directement par un ImageProcessor, on peut récupérer ou initialiser le type d' ImageProcessor avec des méthodes d' ImagePlus : ImageProcessor getProcessor() void setProcessor(java.lang.String title, ImageProcessor ip) Lorsqu'on travaille avec des filtres, il est inutile de s'occuper du type de processor de l'image en cours car il est passé en argument à la méthode run. ImageProcessor est une classe abstraite dont dérivent cinq sousclasses : ByteProcessor Utilisé pour les images 8 bits couleurs ou niveaux de gris. Possède une sousclasse nommée BinaryProcessor pour des images en niveau de gris qui ne contiennent que les valeurs 0 et 255. ShortProcessor Pour les images 16 bits (niveau de gris). ColorProcessor Pour les images couleurs 32 bits type entier (RVB 8 bits par canal). FloatProcessor Pour les images 32 bits type flottant niveaux de gris , haute précision. 2.3.2. Récupérer les pixels Pour travailler sur les valeurs de l'image, il faut récupérer la valeur de chaque pixel, on peut utiliser pour cela la méthode java.lang.Object getPixels() de l' ImageProcessor. On fera par exemple : int[] mes_pixels = (int[]) monProcessor.getPixels(); pour une image couleur (le type est integer = entier). Ici, on déclare un tableau d'entiers qui s'appelle mes_pixels et qui prend pour valeurs les valeurs de pixels de monProcessor. Notez que nous effectuons un transtypage pour être certain de ramener des valeurs entières. On récupère donc ici un tableau monodimensionnel. Il faudra récupérer largeur (et hauteur) de l'image pour pouvoir passer d'un tableau monodimensionnel à des coordonnées XY (rappelezvous des deux boucles imbriquées). Attention : Il existe un piège avec la récupération de trois types de processor : ByteProcessor, ShortProcessor et ColorProcessor. Le type de donnée byte en Java est signé, c'estàdire qu'il est compris entre 128 et 127. On attend plutôt d'une image que ses valeurs soient comprises entre 0 et 255 (la lumière est plus ou moins intense ou au pire absente). Quand on fait un transtypage d'un byte, il vaut mieux s'assurer que le signe soit éliminé. Cette opération peut se faire grâce à l'opérateur ET binaire ( & ) : int pix = 0xff & pixels[i]; ... pixels[i] = (byte) pix; Même combat avec le type short 32768 à 32767 en java. Il faut donc faire : int pix = pixels[i] & 0xffff; ... pixels[i] = (short) pix; pour avoir des valeurs entre 0 et 65535. C'est encore un peut plus compliqué avec le type entier (les images couleur) car les trois composantes R, V et B sont empaquetées dans la même valeur entière : int red = (int)(pixels[i] & 0xff0000)>>16; int green = (int)(pixels[i] & 0x00ff00)>>8; int blue = (int)(pixels[i] & 0x0000ff); ... pixels[i] = ((red & 0xff)<<16)+((green & 0xff)<<8) + (blue & 0xff); En plus de la vérification du signe nous utilisons des opérateurs binaires de rotation de bits. 2.3.3.Créer des images ImagePlus createImagePlus(); // ou ImageProcessor createProcessor(int width, int height); Les méthodes cidessus répondent à nos interrogations sur la manière de créer une image. On pourra en fixer le type dans un second temps à l'aide de différents arguments. Explorer l'API pour les connaître ... Vous pourrez créer une copie de l'image courante en créant un image puis recopiant les pixels avec : void copyBits(ImageProcessor ip, int xloc, int yloc, int mode); Le mode est contenu dans l'interface Blitter et correspond à une des valeurs suivantes : ADD destination = destination+source AND destination = destination AND source AVERAGE destination = (destination+source)/2 COPY destination = source COPY_INVERTED destination = 255source COPY_TRANSPARENT Les pixels blancs sont interprétés comme transparents. DIFFERENCE destination = |destinationsource| DIVIDE destination = destination/source MAX destination = maximum(destination,source) MIN destination = minimum(destination,source) MULTIPLY destination = destination x source OR destination = destination OR source SUBTRACT destination = destination – source XOR destination = destination XOR source 2.3.4. Afficher les images ImageJ affiche les images en utilisant une classe nommée ImageWindow. C'est la fenêtre qui contiendra l'image ImagePlus, la classe contient tout ce qui est nécessaire à mettre à jour et afficher les images nouvellement créées. 2.3.5. Quelques méthodes utiles relatives à l'image Je vous livre en vrac quelques méthodes qui vous seront utiles pour écrire vos premiers plugins mais la liste est loin d'être exhaustive. Si vous en voulez plus, consultez l'API, comme d'habitude. ImagePlus void setWindow(ImageWindow win); définit la fenêtre qui affiche l'image. ImageWindow getWindow(); récupère la fenêtre qui affiche l'image. void mouseMoved(int x, int y); affiche la position de la souris et la valeur du pixel dans la « status bar ». LookUpTable createLut(); créée une table de couleur à partir de l'image courante. ij.process.ImageStatistics getStatistics(); équivaut à « measure » dans le menu d'ImageJ. ij.measure.Calibration getCalibration(); retourne la calibration de l'image. ImageProcessor void flipHorizontal() et void flipVertical() retournement horizontal et vertical. void rotate(double angle) rotation d'un certain angle. void scale(double xScale, double yScale) redimensionnement. ImageProcessor crop() découpe l'image selon la ROI courante. Renvoie une nouvelle image. ImageProcessor resize(int dstWidth, int dstHeight) redimensionnement. ImageProcessor rotateLeft() et ImageProcessor rotateRight() rotations horaire et antihoraire. void setInterpolate(boolean interpolate) Si true, crop(), resize() et rotate() utiliseront une interpolation bilinéaire et une simple interpolation en proches voisins si false. void convolve3x3(int[] kernel) convolue l'image avec la matrice dont le noyau est passé en argument. void sharpen() et void smooth() comme dans le menu process d'ImageJ. Filtres 3x3. void dilate(), void erode() et void findEdges() équivaut aux menus process > binary avec des filtres respectivement minimum, maximum (3 x 3) et un opérateur de Sobel. void medianFilter() médian 3x3. void gamma(double value) correction de gamma. void invert() inversion d'image. void add(int value) ajout de constante à chaque pixel. void multiply(double value) multiplie chaque pixel par l'argument. void and(int value) ET binaire avec l'argument. void or(int value) OU binaire avec l'argument void xor(int value) OU exclusif avec l'argument. void log() recalcule les valeurs de pixels sur une échelle logarithmique. void noise(double range) ajoute du bruit aléatoire (dans l'intervalle range) dans l'image void setColor(java.awt.Color color) couleur d'avantplan. void setValue(double value) couleur de remplissage. void setLineWidth(int width) largeur de ligne. void moveTo(int x, int y) position de départ du tracé void lineTo(int x2, int y2) trace une ligne jusqu'aux coordonnées en argument. void drawPixel(int x, int y) colorie le pixel en x,y avec la couleur de tracé en cours. void drawDot(int xcenter, int ycenter) tracé d'un point. void drawDot2(int x, int y) tracé d'un point 2x2. void fill() emplit la ROI rectangulaire de la couleur de tracé en cours. void fill(int[] mask) emplit les pixels de la ROI qui font partie du masque avec couleur en cours. void drawString(java.lang.String s) Dessine la chaîne s à la position courante. int getStringWidth(java.lang.String s) retourne la largeur de la chaîne en pixels. double getMin() et double getMax() retournent les plus petite et grande valeurs affichées. void setMinAndMax(double min, double max) étale les valeurs de pixel dans la largeur du type d'image en cours entre min et max. void resetMinAndMax() pour les images short et float (16 et 32 bits), recalcule le min et max pour un affichage correct (255 niveaux possibles). void autoThreshold() seuil automatique. double getMinThreshold() et double getMaxThreshold() seuils inférieur et supérieur. void setThreshold(double minThreshold, double maxThreshold, int lutUpdate) int[] getHistogram() retourne l'histogramme de l'image. Ce sera un histogramme de luminosité pour les images couleurs et la valeur null pour les 32 bits. int getHistogramSize() La taille est de 256 pour les images 8 bits ou couleur, MinMax+1 pour les 16 bits. void snapshot() sauvegarde l'état du processor en cours. java.lang.Object getPixelsCopy() retourne une référence au snapshot, donc au tableau de pixel avant la dernière modification. void reset() réinitialise l'image à l'état du snapshot. void reset(int[] mask) idem sauf pour les pixels qui font partie du masque. Stacks java.lang.Object[] getImageArray() renvoie le stack comme un tableau d'ImagePlus. boolean isHSB() et boolean isRGB() renvoie true si c'est un stack de 3 images HSB ou RGB. 2.3.6.Quelques autres méthodes utiles Erreurs static void error(java.lang.String msg) affichage d'un message avec « Error ». static void showMessage(java.lang.String msg) affichage d'un message. static void showMessage(java.lang.String title, java.lang.String msg) affichage d'un message dont on spécifie le titre de fenêtre. static boolean showMessageWithCancel(java.lang.String title, java.lang.String msg) affichage d'un message dont on spécifie le titre de fenêtre et qui renvoie false si l'utilisateur clique sur Cancel, true sinon. static void noImage() affichage d'un message d'erreur « no images are open » static void outOfMemory(java.lang.String name) affichage d'un message d'erreur « out of memory » static boolean versionLessThan(java.lang.String version) renvoie un message d'erreur et retourne false si la version d'ImageJ est inférieure à celle spécifiée. Texte static void write(java.lang.String s) écrit s dans la fenêtre de résultat. static void setColumnHeadings(java.lang.String headings) définit les entêtes de la fenêtre de résultat. static java.lang.String d2s(double n) convertit un nombre en chaîne, garde 2 chiffres après la virgule. static java.lang.String d2s(double n, int precision) convertit un nombre en chaîne, garde « precision » chiffres après la virgule. Barre de statut et de progression static void showStatus(java.lang.String s) écrit dans la barre d 'état. static void showTime(ImagePlus imp, long start, java.lang.String str) affichage du temps écoulé depuis start et du taux de pixels traité à la seconde. static void showProgress(double progress) met à jour la valeur de la barre de progression (compris entre 0.0 et 1.0). static double getNumber(java.lang.String prompt, double defaultNumber) pour récupérer un nombre auprès du gentil utilisateur. static java.lang.String getString(java.lang.String prompt, java.lang.String defaultString) pour récupérer une chaîne auprès du gentil utilisateur. Appel de commandes static void doCommand(java.lang.String command) lance une commande dans un thread séparé. static void run(java.lang.String command) lance une commande. Le programme est bloqué jusqu'à la fin de cette commande. static java.lang.Object runPlugIn(java.lang.String className, java.lang.String arg) lance un plugin. static void beep() émission d'un bip. static boolean altKeyDown() et static boolean spaceBarDown() renvoie true si Alt ou Espace sont enfoncées. Mise en application import ij.*; import ij.plugin.PlugIn; public class Message_Test implements PlugIn { public void run(String arg) { IJ.showStatus("Plugin Message Test started."); IJ.showProgress(0.0); IJ.error("I need user input!"); String name = IJ.getString("Please enter your name: ","I.J. User"); IJ.showProgress(0.5); IJ.write("Starting sample plugin Hello ... "); IJ.runPlugIn("Hello_",""); IJ.showProgress(1.0); IJ.showMessage("Finished",name+", thank you for running this plugin"); } } Conclusion En quelques pages, nous avons survolé l'énoooorme API d'ImageJ. Toutes les réponses à vos questions sont dans la documentation de l'API normalement. Attention, ImageJ évolue très vite : Version 0.50, 23Sep97, Version 0.99, 2March99, Version1.01, 25March99, Version 1.10, 8 Nov99,Version 1.20, 20Dec2000, Version 1.30v, 3 July 2003, Version 1.31v, 4Fev2004, Version 1.32j, 12Mai2004. Il sort une révision mineure tous les 8 à 15 jours. Cela ne remet pas en cause vos anciens plugins en général. Vous devrez peutêtre faire quelques modifications mineures pour qu'ils continuent à fonctionner. On imagine qu'ImageJ va maintenant se simplifier, il y aura, à terme, une unification des méthodes d'accès aux images indépendemment de leur type comme avec le package ImageAccess de l'école polytechnique de Lausanne par exemple. Dans tous les cas, l'accès au code source du programme permet d'être sûr d'une chose : ImageJ deviendra ce que NOUS en ferons ...