CHAPITRE 2 - DOSSIER DE PROGRAMMATION


1. Les différents plugins développés
1.1. Les plugins Type
1.2. Les plugins Action
1.2.1. Architecture
1.2.2. Description

1.3. Les plugins Visualisation
1.3.1. Les besoins pour l'architecture
1.3.2. Architecture

1.4. Les plugins Interface
1.4.1. Les besoins pour l'architecture
1.4.2. L'interface principale
1.4.3. Les afficheurs
1.4.3.1. L'architecture
1.4.3.2. L'interface 2Dimage
1.4.3.3. L'interface MRI
1.4.3.4. L'interface FIT


2. Modification du fonctionnement de l'architecture
2.1. Le WindowManager
2.2. La classe plugin
2.3. Particularités des "invokes"
2.4. Mode interactif

3. Communication entre les plugins
3.1. Lancement d'une application depuis l'interface graphique
3.2. Explications

4. Extensions
4.1. Embarcation du langage de script Python
4.2. Gestion d'un historique
4.3. Construction d'un graphe de scène

5. Manuel de maintenance
5.1. Comment ajouter un nouveau plugin
5.2. Modification du Makefile






Initialement, aucune interface n’avait été implantée (ni graphique, ni textuelle). Le logiciel proposait uniquement l’extraction d’une coupe dans une image IRM 3d, cette fonctionnalité ayant été codée « en dur ».

Notre principal travail a été, non seulement d’implanter les fonctionnalités demandées, mais aussi et surtout d’étoffer la communication entre les différents plugins.



1. Les différents plugins développés
1.1. Les plugins Type

Nous allons à présent décrire les différents composants ajoutés dans l’architecture existante. Il est à noter que les diagrammes qui suivent ne contiennent les principaux attributs et fonctions pertinentes.



Figure 12 - Le type Fit ajouté

Le projet qui nous avait été fourni intégrait différents plugin types permettant de manipuler des images. Toutefois pour permettre l’intégration du plugin action fit, il était nécessaire de créer un nouveau type de données adapté à nos besoins.

Ce nouveau plugin est le TypeFit, il hérite de la classe TypeArray. Les données présentes dans ce type sont:

  • le tableau des valeurs sur lesquelles on désire travailler.
  • la taille de ce tableau.
  • 3 valeurs permettant de définir la courbe résultat. Pour le cas linéaire, il s’agit du coefficient directeur de la droite, de son ordonnée à l’origine et du coefficient de corrélation. Pour le cas exponentiel, il s’agit de A, b et r dans l’équation y = A ebx + r


    Ce nouveau type est utilisé à deux reprises dans le logiciel. La première utilisation est à l’ouverture d’un fichier de valeurs dont on veut évaluer la fonction fit. Les données brutes sont envoyées au plugin, en précisant que les coefficients de la courbe sont à –1. La deuxième utilisation se fait lors de l’appel de l’action fit. Cette action prend en entrée et en sortie un typeFit. Elle calcule les 3 coefficients de la courbe et met à jour leurs valeurs dans le plugin.

    Il est à noter que le fit linéaire et exponentiel utilisent le même type de données et que si l’on désire implanter une autre méthode de calcul des coefficients (notamment pour l’exponentiel) ou une nouvelle forme de courbe résultat (par exemple la fonction puissance), le plugin sera réutilisable.

  • 1.2. Les plugins Action

    1.2.1. Architecture





    Figure 13 - Architecture des plugins d'Actions ajoutés

    Le projet implantait initialement les actions ActionMRI3DclearSlice et ActionBETSegmentation en mode non interactif.

    1.2.2. Description

    Nous avons rajouté deux actions, il s’agit du fit et de la baguette magique en deux et trois dimensions. L’implantation de ces outils reprend les algorithmes précédemment évoqués dans la partie 3.

    Ces plugins récupèrent en entrée les données à traiter à partir d’un plugin type. On effectue l’action. Soit la donnée en entrée est modifiée, soit nous créons une nouvelle donnée.

    1.3. Les plugins Visualisation

    1.3.1. Les besoins pour l'architecture

    Nous avons besoin de ce type de plugin pour pouvoir décharger le travail de conversion de données aux plugin IO ou Interface. Ces derniers pourront donc servir à afficher ou bien sauver des données d’une famille sans ce soucier de leur type réel. En effet, le travail de conversion se sera effectué lors du passage dans le plugin. Les données renvoyées par les plugins visualisations sont brutes : par exemple, pour une image, un simple tableau de pixel ainsi que sa taille.

    1.3.2. Architecture



    Figure 14 - Architecture des plugins Visu ajoutés

  • Dans le cas d’un affichage, VisuFit convertit les données au format integer, qui est utilisé par les toolkit graphique. Le plugin met à l’échelle les données pour occuper au mieux l’espace d’affichage, effectue un changement de repère pour ces données (l’axe étant de haut en bas dans l’interface, et de bas en haut dans le tableau de valeurs), transcrit les valeurs de l’équation et les informations textuelles sur les coefficients, devant agrémenter cet affichage.

  • Visu2Dimage convertit les données du type générique au format unsigned char pour qu’il soit exploitable par l’affichage ou la sauvegarde. Les données transmises comportent également les dimensions de l’image.

  • extrait une coupe à partir d’une donnée IRM suivant son axe et son index dans la donnée. Il opère ensuite la même conversion vers des unsigned char pour les raisons évoquées précédemment.

  • 1.4. Les plugins Interface

    Les plugins d’interface sont de trois types :

  • L’interface principale, instanciée une fois au démarrage du logiciel
  • Les afficheurs qui permettent de visualiser les données chargées
  • Les boîtes de dialogue diverses servant principalement à récupérer les données nécessaires pour paramétrer principalement les plugins Action.

    1.4.1. Les besoins pour l'architecture

    A un instant précis, plusieurs données peuvent avoir été chargées et être visualisées. On peut donc avoir à l’écran plusieurs afficheurs ouverts, chacun affichant des données qui peuvent être de type différents. De plus, on doit pouvoir appliquer une action quelconque à chacune de ces données.

    1èresolution :
    La 1ère solution envisagée a été de proposer dans l’interface principale toutes les différentes actions applicables aux données. Une fois le choix de l’action effectué, l’utilisateur devait choisir la donnée sur laquelle appliquer l’action (par exemple en sélectionnant l’afficheur dans lequel la donnée est visualisée).
    Cette 1ère méthode nous a amenés aux problèmes suivants :

  • La donnée sur laquelle est appliquée l’action étant choisie après l’action elle-même, rien n’empêcherait l’utilisateur de sélectionner une donnée dont le type serait incompatible avec l’action. Cela autoriserait toutes les fantaisies, comme par exemple appliquer l’algorithme de la baguette magique 3D sur une image 2D !
  • L’action étant choisie dans l’interface principale et la donnée dans un afficheur quelconque, il paraît difficile de déclencher le processus. Les deux plug-ins mis en cause (l’Interface principale et l’afficheur) devront communiquer.

    Il faudra que :
  • soit l’afficheur informe l’interface principale de la donnée sur laquelle appliquer l’action
  • soit l’interface principale informe l’afficheur de l’action à appliquer sur la donnée.
  • Rappelons que les deux plug-ins étant déjà lancés, il ne leur est plus possible de communiquer par la voix traditionnelle des paramètres passés lors de leur création.

    2èmesolution :
    Pour palier aux problèmes que nous venons de décrire, l’idée serait de choisir à la fois l’action et la donnée sur laquelle l’appliquer dans le même plug-in. Or, l’interface principale ne connaît les données chargées que par l’intermédiaire du DataManager (ce qui correspond à des indices dans un tableau). Il a donc fallu proposer les différentes actions disponibles dans chaque afficheur. De ce fait, l’utilisateur ne peut sélectionner que les actions applicables sur le type de données visualisé.

    De plus, comme l’action est choisie à partir du plug-in permettant de visualiser la donnée, il n’y a plus de problème de communication, car tout se fait dans l’afficheur.

    Cette méthode présente néanmoins un inconvénient : lors de l’ajout d’un nouvel afficheur, il est nécessaire de réimplanter la procédure de lancement d’une action, celle-ci étant à présent déclenchée par l’afficheur.

    L’idéal serait de proposer à l’utilisateur le choix de l’action à effectuer dans l’afficheur, mais d’exécuter la procédure de lancement de cette action dans l’interface principale.

    C’est dans ce but que nous avons introduit un nouvel élément : le WindowManager (que nous décrirons un peu plus loin, dans la partie 2.1 de ce chapitre).

  • 1.4.2. L'interface principale



    Figure 15 - Architecture du plugin MainWindow

    L’interface principale est lancée au démarrage de l’application, et contient un menu, une console Python, et un graphe de scène (non encore implanté).

    Elle permet d’ouvrir un fichier de type Rec/Par.

    Ci-après une capture de notre interface principale :



    Figure 16 - L'interface principale

    1.4.3. Les afficheurs

    1.4.3.1. L'architecture



    Figure 17 - Architecture des afficheurs

    Comme nous en avons parlé précédemment, nous disposons de fenêtres graphiques dites « afficheurs » (appelées Viewer) pour visualiser les données que l’on manipule. Ces plug-ins sont dérivés de InterfacePlugin.

    1.4.3.2. L'interface 2Dimage

    L’interface 2Dimage est constitué du composant CanvasImageViewer qui sera réutilisé dans les futurs plug-ins Interface d’affichage. Il intègre l’affichage d’une image et la création du menu dynamique des actions applicables au type de données affiché. Il intègre également un menu permettant de fermer et sauver une image. Enfin il possède une fonction de zoom pour le confort de l’utilisateur.

    1.4.3.3. L'interface MRI

    Le Viewer3D en plus des propriétés du CanvasImageViewer, possède une série de boutons radio pour choisir l’axe d’exploration et un pour se déplacer sur cet axe. L’afficheur sait donc à tout moment l’axe courant et l’index dans la donnée IRM.

    Ci-après des captures d’écran de notre afficheur de coupes MRI (selon trois coupes).



    Figure 18 - Coupe représentant une vue axiale du cerveau



    Figure 19 - Coupe représentant une vue sagittale du cerveau



    Figure 20 - Coupe représentant une vue coronale du cerveau

    1.4.3.4. L'interface FIT

    Cette interface est appelée :

  • Pour afficher le tableau de points original, en indiquant les valeurs maximales sur les deux axes
  • Pour ajouter à cet affichage la courbe obtenue, son équation et, pour le Fit linéaire, son coefficient de corrélation.



    Figure 21 - Capture de l'interface FIT

  • 2. Modification du fonctionnement de l'architecture
    2.1. Le WindowManager

    De même que le DataManager recense les données chargées et le PluginManager les plug-ins disponibles, le WindowManager maintient une liste de toutes les fenêtres créées et contient, pour chaque fenêtre, une référence vers celle-ci.



    Figure 22 - Le WindowManager

    De cette façon, l’interface principale, référencée par l’indice 0 du WindowManager, est connue de chaque afficheur. Il suffit alors à l’afficheur de transmettre à l’interface principale :

  • à la fois l’action (c’est-à-dire une entrée dans le PluginManager)
  • et la donnée (c’est-à-dire une entrée dans le DataManager).
  • C’est ensuite l’interface principale qui lancera la procédure.

    Ainsi, les seules communications entre les objets de l’interface se font de l’afficheur vers l’interface principale.

    On peut alors se demander l’utilité de stocker des références vers les afficheurs dans le WindowManager ? Lorsqu’un plug-in Interface est lancé, il crée une fenêtre d’affichage puis se termine. Si nous ne conservons aucune référence sur cette fenêtre, il ne sera plus possible d’en modifier le contenu. Toute modification des données devra donc être visualisée dans une nouvelle fenêtre. C’est pourquoi, nous conservons les références dans le WindowManager.

    2.2. La classe plugin

    Pour construire dynamiquement au sein des différents afficheurs la liste des plug-ins applicables sur la donnée affichée, il a fallu insérer un nouveau champ dans le de chaque plug-in : le type de la donnée sur lequel le plug-in s’applique. Il reste donc à rechercher lorsque l’on fabrique le menu action et le menu sauvegarde, les plug-ins compatibles avec la donnée affichée. Il s’agit d’une solution partielle car elle n’autorise qu’un seul type de données manipulable par un plug-in, alors que cette restriction n’est pas obligatoire. A long terme, il pourrait être plus judicieux de disposer d’un arbre des types implémentés. Ainsi, un plug-in dédié à un type de donnée pourrait s’appliquer également sur tous les types de données parents de celui-ci.

    2.3. Particularités des "invokes"

    Pour permettre une bonne communication entre les différents types de plug-in, nous avons déterminé une liste de paramètres obligatoires avec un ordre imposé. Il s’agit des 6 premiers paramètres des plug-ins qui comptent dans l’ordre :

  • Le mode d'exécution : runMode
  • Le manager de plugin : pluginManager
  • Le manager de données : dataManager
  • L'indice de la donnée manipulée : dataId
  • Le manager de fenêtre : windowManager
  • L'indice de la fenêtre pour le résultat : winId (-1 si on souhaite une nouvelle fenêtre)
  • 2.4. Mode interactif

    Pour pouvoir utiliser les actions en mode interactif, nous avons créé de nouveaux plug-ins de type Interface : les GetParameter pour pouvoir récupérer les paramètres nécessaires au plug-in action.



    Figure 23 - Architecture des GetParameters

    Ce nouveau composant est nécessaire pour tout plug-in Action que l’on souhaite paramétrer interactivement. De plus, certaines actions comme la baguette magique peuvent avoir recours à un clic de souris sur l’image affichée. Nous avons donc mis au point un système permettant cette interaction par l’intermédiaire d’un flag. Une fois la boîte de dialogue validée, si le clic est nécessaire, l’action n’est pas invoquée tant que l’utilisateur n’a pas cliqué dans la fenêtre d’affichage. Une fois cliquée, l’information de position est stockée dans les paramètres envoyés au plug-in.

    Les plug-in GetParameter suivent des règles d’implantation précises :

  • Une boîte de dialogue pour entrer les paramètres nécessaires à l’exécution de l’action.
  • Les 5 valeurs de retour obligatoires sont dans l’ordre :
  • La valeur de sortie du plugin
  • Le nombre de paramètre que contient le vecteur paramètre
  • Le vecteur paramètre qui sera passer au plugin Action
  • Un flag pour savoir si on doit cliquer sur l'image
  • Un indice où insérer dans le vecteur de paramètres les informations de positions relatives au clic de souris si besoin est.

  • La possibilité de choisir si l’on souhaite sauvegarder ou afficher le résultat, et choisir le plug-in adéquat à appeler après l’action.

    Voici une capture de la boîte de dialogue :



    Figure 24 - Boîte de dialogue pour paramétrer l'action "baguette magique"

  • 3. Communication entre les plugins

    Une des difficultés de ce projet fût d’élaborer un protocole de communication viable pour valider une architecture noyau/plugin où la communication s’effectue exclusivement par l’intermédiaire des plug-ins.

    L’hypothèse avancée au départ consistant à utiliser le système des exceptions pour faire communiquer entre eux les plug-ins a dû être abandonnée. En effet, le mécanisme du chargement de plug-in (dans le cas des plug-ins Interface) implique que la fonction se termine, bien que l’afficheur reste valide. Il paraît compliqué de faire cohabiter ces deux comportements.

    Maintenant, pour mieux comprendre les mécanismes de communication que nous avons mis en œuvre autour des cinq types de plug-ins, nous allons les illustrer par un schéma.

    3.1. Lancement d'une application depuis l'interface graphique

    Pour illustrer les interactions des paramètres au sein de l’architecture, nous vous proposons de suivre le cheminement des informations au travers d’un cas d’utilisation : l’appel d’une action depuis un afficheur.



    Figure 24 - Boîte de dialogue pour paramétrer l'action "baguette magique"

    3.2. Explications

    Le schéma ci-dessus décrit la communication entre les différents plug-ins, particulièrement les échanges de paramètres ainsi décrits dans les boites.

    Il est à noter que chaque plug-in contient au moins les 6 paramètres suivants :

  • 0 - runMode
  • 1 - le PluginManager
  • 2 - le DataManager
  • 3 - un DaraId
  • 4 - le WindowManager
  • 5 - le WinId
  • (1) : l’utilisateur a sélectionné un plug-in Action dans le menu de l’interface Viewer1. Ce plug-in est identifié par son identifiant PlugId. On appelle alors une méthode du plug-in InterfaceMain : callActionPlugin.

    (2) : le plug-in InterfaceMain invoque le plug-in GetParameters associé au plug-in Action d’identifiant PlugId pour récupérer les paramètres nécessaires au plugin Action correspondant.

    (3) : Le plugin GetParameters contient plusieurs valeurs de retour

  • un vecteur de paramètres params qui dépend de l’action.

  • un flag pour savoir si un clic est attendu (NeedClic)

  • si on attend un clic, un flag qui nous dit où l’insérer dans le vecteur de paramètres (IndexClic).
  • Attention, le vecteur de paramètres params contient toujours les informations suivantes (mises en bleu dans le schéma ci-dessus) :

  • un flag SaveOrDisplay

  • un flag CreateOrModify (optionnel suivant l'action)

  • un champ NextPluginToInvoke pour savoir quel plug-in doit prendre le relais, un IO pour une sauvegarde ou un Interface pour un affichage.
  • Par exemple, dans le cas d’une Action de baguette magique, on ajoutera les paramètres suivants :

  • la tolérance

  • les coordonnées x et y de la sélection
  • Ce qui fait 12 paramètres pour ActionMagicWand !

    (4) : le plug-in InterfaceMain invoque le plug-in Action, et lui envoie les paramètres params dont il a besoin.

    (5) : Arrivé là, le plug-in Action crée une nouvelle donnée, qui doit être typée pour être reconnue par le DataManager. C’est pourquoi, il va invoquer le plug-in Type. La nouvelle donnée aura donc un nouvel identifiant dans le DataManager.

    (6) : Le plug-in Action invoque le plug-in Visu, qui va préparer les données en vue d’un affichage ou d’une sauvegarde. Visu génère tout le temps une nouvelle Image ou un fichier compatible avec le plug-in qu’il doit appeler par la suite.

    (7a) : Si on souhaite faire une sauvegarde de la donnée, le plug-in Visu invoke Input/Output, en lui transmettant le fichier à sauver.

    (7b) : Si on souhaite faire un affichage de la donnée, le plug-in Visu invoque InterfaceViewer2, en lui transmettant l’Image calculée ainsi que sa taille.

    (8a) : Le plug-in Interface crée un nouveau Viewer(Viewer2) si WinId = -1

    (8b) : Sinon, il transmet l’Image au Viewer associé au WinId. Le Viewer1 mettra ainsi à jour l’image.

    4. Extensions
    4.1. Embarcation du langage de script Python

    Dans cette partie sur l’extension, nous aborderons plus en détail, la partie concernant l’embarcation d’un langage de script. En effet, cette fonctionnalité était initialement prévue lors de l’élaboration du cahier des charges avec le client. Mais, suite aux nombreux problèmes qui se sont posés dans la partie concernant l’expertise génie logiciel du nouveau système noyau / plug-in, cette intégration n’a pu se faire. Cependant, nous avons effectué des recherches sur cette fonctionnalité. C’est pourquoi nous allons vous exposer les principales directions à suivre pour l’implémentation.

    L’intégration d’un langage de script, et particulièrement Python passe par deux phases :

  • l’extension du langage de script : création de nouvelles fonctions appelables depuis une console ou des scripts.

  • l’embarquement d’un interpréteur au sein même du logiciel : pour pouvoir interpréter les lignes de commandes saisies au sein du logiciel.
  • Pour effectuer ces deux étapes, nous proposons d'utiliser une API implantée en C pour rendre accessible depuis python des fonctions définies en C. Il est également possible de lancer une interprétation de script, de ligne de commande ou même d’une session python depuis une fonction C.

    Pour notre projet, la principale chose à faire est de créer, pour chaque plug-in, une fonction chargée de récupérer les paramètres qui lui sont spécifiques. Ensuite, c’est cette fonction qui positionne les paramètres, qui instancie et qui lance le plugin.

    l’interaction avec python se situe à deux niveaux. Elle est constamment présente en mode ligne de commande (non-interactif). C’est-à-dire que les lignes de commande tapées sont directement des lignes de commande python. L’autre niveau se situe en mode graphique (interactif), car le logiciel pourrait proposer également une console graphique pouvant recevoir des commandes python.

    Il faut donc distinguer ces deux cas d’utilisation par deux fonctions différentes qui seront autorisées l’une ou l’autre suivant le cas.

    Pour une ligne de commande en mode non-interactif, le processus doit comporter : ouverture du fichier / action / sauvegarde.

    Tandis que dans l’autre cas, il faut : ouvrir un fichier ou récupérer une donnée / appliquer une action / afficher ou sauvegarder le résultat.

    Le principe semble simple, mais nous pensons qu’il reste encore à élaborer un protocole valide de communication pour cette fonctionnalité. De plus, les conversions de types entre C et python et vice et versa semblent ne pas être si simples au vu des articles que nous avons pu consulter au cours de nos recherches.

    4.2. Gestion d'un historique

    Pour poursuivre sur les extensions que nous proposons, il paraît nécessaire de disposer d’un outil de « undo » au sein du logiciel. Nous pensons que cet outil, en l’état actuel de l’architecture, semble pouvoir s’intégrer facilement. En effet, il suffirait de complexifier un peu la structure de donnée stockée dans le . En plus de disposer d’un pointeur vers un afficheur ( grâce à son identifiant ), on pourrait également disposer d’une liste des objets qui ont déjà été pointés par cet afficheur, et de l’indice dans cette liste de l’objet actuellement pointé. Il serait donc possible de remonter dans les données pour accéder aux données antérieures. Nous exposons ici une manière simple d’intégrer un système sommaire de gestion d’historique. Pour qu’il fonctionne, cela implique que les données ne soient jamais écrasées, ce qui peut apparaître comme assez coûteux.

    4.3. Construction d'un graphe de scène

    Pour terminer ce chapitre sur les extensions possibles, nous exposerons l’utilité d’implanter un nouveau composant à l’interface du logiciel. Il s’agirait de proposer une vision hiérarchisée des traitements appliqués à une donnée sous la forme de graphe de scène. Ainsi, il serait possible de représenter graphiquement une suite d’actions sur une donnée et même de pouvoir modifier une de ces actions. Alors, en parcourant le graphe, nous pourrions être capable de générer la nouvelle donnée sans repasser par toute les étapes manuellement. Il s’agit ici d’un composant haut niveau qui pourrait engendrer un remaniement de l’architecture, mais il correspond à un réel besoin, et devrait par la suite trouver sa place au sein du logiciel.

    5. Manuel de maintenance
    5.1. Comment ajouter un nouveau plugin

    Les différents points suivants sont à respecter :

  • Définir une classe qui hérite du type de plugin qu’on souhaite instancier.

  • Définir les deux fonctions de prototype :
  • extern "C" PluginInfo* query(void) : qui renvoie une description de la liste des paramètres à passer au plug-in (en mettant en tête de cette liste les 6 paramètres obligatoires).

  • extern "C" Plugin* create(void) : qui renvoie une instance du plugin créé.

  • Définir le constructeur de la classe

  • Définir la méthode getHandle() pour accéder au handle du plug-in

  • Définir la méthode invoke qui implémente l’algorithme à mettre en oeuvre.

  • Pour permettre un ajout rapide de plug-in, l’architecture comporte des canevas des types de plug-in suivant : IO, Action, Visu. Ces fichiers sont : IOExample, ActionExample, VisuExample. (Cf. Annexe 1)
  • 5.2. Modification du Makefile

    Il faut créer une nouvelle entrée dans le Makefile pour compiler le plugin.

    Ajouter le nom de sortie du plug-in dans la liste des plug-ins à construire.

    Pour ce qui est des plug-ins Interface qui utilisent QT, il faut créer des fichier moc pour les classes dérivant de celles du toolkit. Pour cela, définir la classe dans le fichier en-tête. En début de description de la classe (juste après { ), il faut ajouter la commande . Ensuite, ajouter moc_filename.cpp dans la liste des fichiers moc à générer et dans celle des fichiers nécessaires à la compilation du plug-in. Le fichier sera correctement créé si le fichier source est de la forme : filename.hpp.



    retour au sommaire            chapitre 1: Analyse et Conception du projet