Plugins#

DataLab est une application modulaire. Il est possible d’ajouter de nouvelles fonctionnalités à DataLab en écrivant des plugins. Un plugin est un module Python qui est chargé au démarrage par DataLab. Un plugin peut ajouter de nouvelles fonctionnalités à DataLab, ou modifier des fonctionnalités existantes.

Le système de plugins prend actuellement en charge les fonctionnalités suivantes :

  • Fonctionnalités de traitement : ajouter de nouvelles tâches de traitement au système de traitement DataLab, y compris des interfaces graphiques spécifiques.

  • Entrée/sortie : ajouter de nouveaux formats de fichiers au système d’entrée/sortie de DataLab.

  • Fonctionnalités HDF5 : ajouter de nouveaux formats de fichiers HDF5 au système d’entrée/sortie HDF5 de DataLab.

Qu’est-ce qu’un plugin ?#

Un plugin est un module Python qui est chargé au démarrage par DataLab. Un plugin peut ajouter de nouvelles fonctionnalités à DataLab, ou modifier des fonctionnalités existantes.

Un plugin est un module Python qui contient une classe dérivée de la classe cdl.plugins.PluginBase. Le nom de la classe n’est pas important, tant qu’elle est dérivée de cdl.plugins.PluginBase et qu’elle dispose d’un attribut PLUGIN_INFO qui est une instance de la classe cdl.plugins.PluginInfo. L’attribut PLUGIN_INFO est utilisé par DataLab pour récupérer des informations sur le plugin.

Où est positionné un plugin ?#

Etant donné que les plugins sont des modules Python, ils peuvent être placés n’importe où dans le chemin Python de l’installation de DataLab.

Des emplacements supplémentaires spéciaux sont disponibles pour les plugins :

  • Le répertoire plugins dans le dossier de configuration de l’utilisateur (par exemple C:UsersJohnDoe.DataLabplugins sur Windows ou ~/.DataLab/plugins sur Linux).

  • Le répertoire plugins dans le même dossier que l’exécutable DataLab en cas d’installation autonome.

  • Le répertoire plugins dans le package cdl en cas de plugins internes uniquement (c’est-à-dire qu’il n’est pas recommandé d’y placer vos propres plugins).

Exemple : plugin de traitement#

Voici un exemple simple d’un plugin qui ajoute une nouvelle fonctionnalité à DataLab.

# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.

"""
Test Data Plugin for DataLab
----------------------------

This plugin is an example of DataLab plugin. It provides test data samples
and some actions to test DataLab functionalities.
"""

import cdl.obj as dlo
import cdl.tests.data as test_data
from cdl.config import _
from cdl.core.computation import image as cpima
from cdl.core.computation import signal as cpsig
from cdl.plugins import PluginBase, PluginInfo

# ------------------------------------------------------------------------------
# All computation functions must be defined as global functions, otherwise
# they cannot be pickled and sent to the worker process
# ------------------------------------------------------------------------------


def add_noise_to_signal(
    src: dlo.SignalObj, p: test_data.GaussianNoiseParam
) -> dlo.SignalObj:
    """Add gaussian noise to signal"""
    dst = cpsig.dst_11(src, "add_gaussian_noise", f"mu={p.mu},sigma={p.sigma}")
    test_data.add_gaussian_noise_to_signal(dst, p)
    return dst


def add_noise_to_image(src: dlo.ImageObj, p: dlo.NormalRandomParam) -> dlo.ImageObj:
    """Add gaussian noise to image"""
    dst = cpima.dst_11(src, "add_gaussian_noise", f"mu={p.mu},sigma={p.sigma}")
    test_data.add_gaussian_noise_to_image(dst, p)
    return dst


class PluginTestData(PluginBase):
    """DataLab Test Data Plugin"""

    PLUGIN_INFO = PluginInfo(
        name=_("Test data"),
        version="1.0.0",
        description=_("Testing DataLab functionalities"),
    )

    # Signal processing features ------------------------------------------------
    def add_noise_to_signal(self) -> None:
        """Add noise to signal"""
        self.signalpanel.processor.compute_11(
            add_noise_to_signal,
            paramclass=test_data.GaussianNoiseParam,
            title=_("Add noise"),
        )

    def create_paracetamol_signal(self) -> None:
        """Create paracetamol signal"""
        obj = test_data.create_paracetamol_signal()
        self.proxy.add_object(obj)

    def create_noisy_signal(self) -> None:
        """Create noisy signal"""
        obj = self.signalpanel.new_object(add_to_panel=False)
        if obj is not None:
            noiseparam = test_data.GaussianNoiseParam(_("Noise"))
            self.signalpanel.processor.update_param_defaults(noiseparam)
            if noiseparam.edit(self.signalpanel):
                test_data.add_gaussian_noise_to_signal(obj, noiseparam)
                self.proxy.add_object(obj)

    # Image processing features ------------------------------------------------
    def add_noise_to_image(self) -> None:
        """Add noise to image"""
        self.imagepanel.processor.compute_11(
            add_noise_to_image,
            paramclass=dlo.NormalRandomParam,
            title=_("Add noise"),
        )

    def create_peak2d_image(self) -> None:
        """Create 2D peak image"""
        obj = self.imagepanel.new_object(add_to_panel=False)
        if obj is not None:
            param = test_data.PeakDataParam.create(size=max(obj.data.shape))
            self.imagepanel.processor.update_param_defaults(param)
            if param.edit(self.imagepanel):
                obj.data = test_data.get_peak2d_data(param)
                self.proxy.add_object(obj)

    def create_sincos_image(self) -> None:
        """Create 2D sin cos image"""
        newparam = self.edit_new_image_parameters(hide_image_type=True)
        if newparam is not None:
            obj = test_data.create_sincos_image(newparam)
            self.proxy.add_object(obj)

    def create_noisygauss_image(self) -> None:
        """Create 2D noisy gauss image"""
        newparam = self.edit_new_image_parameters(hide_image_type=True)
        if newparam is not None:
            obj = test_data.create_noisygauss_image(newparam)
            self.proxy.add_object(obj)

    def create_multigauss_image(self) -> None:
        """Create 2D multi gauss image"""
        newparam = self.edit_new_image_parameters(hide_image_type=True)
        if newparam is not None:
            obj = test_data.create_multigauss_image(newparam)
            self.proxy.add_object(obj)

    def create_2dstep_image(self) -> None:
        """Create 2D step image"""
        newparam = self.edit_new_image_parameters(hide_image_type=True)
        if newparam is not None:
            obj = test_data.create_2dstep_image(newparam)
            self.proxy.add_object(obj)

    def create_ring_image(self) -> None:
        """Create 2D ring image"""
        param = test_data.RingParam(_("Ring"))
        if param.edit(self.imagepanel):
            obj = test_data.create_ring_image(param)
            self.proxy.add_object(obj)

    def create_annotated_image(self) -> None:
        """Create annotated image"""
        obj = test_data.create_annotated_image()
        self.proxy.add_object(obj)

    # Plugin menu entries ------------------------------------------------------
    def create_actions(self) -> None:
        """Create actions"""
        # Signal Panel ----------------------------------------------------------
        sah = self.signalpanel.acthandler
        with sah.new_menu(_("Test data")):
            sah.new_action(_("Add noise to signal"), triggered=self.add_noise_to_signal)
            sah.new_action(
                _("Load spectrum of paracetamol"),
                triggered=self.create_paracetamol_signal,
                select_condition="always",
                separator=True,
            )
            sah.new_action(
                _("Create noisy signal"),
                triggered=self.create_noisy_signal,
                select_condition="always",
            )
        # Image Panel -----------------------------------------------------------
        iah = self.imagepanel.acthandler
        with iah.new_menu(_("Test data")):
            iah.new_action(_("Add noise to image"), triggered=self.add_noise_to_image)
            # with iah.new_menu(_("Data samples")):
            iah.new_action(
                _("Create image with peaks"),
                triggered=self.create_peak2d_image,
                select_condition="always",
                separator=True,
            )
            iah.new_action(
                _("Create 2D sin cos image"),
                triggered=self.create_sincos_image,
                select_condition="always",
            )
            iah.new_action(
                _("Create 2D noisy gauss image"),
                triggered=self.create_noisygauss_image,
                select_condition="always",
            )
            iah.new_action(
                _("Create 2D multi gauss image"),
                triggered=self.create_multigauss_image,
                select_condition="always",
            )
            iah.new_action(
                _("Create annotated image"),
                triggered=self.create_annotated_image,
                select_condition="always",
            )
            iah.new_action(
                _("Create 2D step image"),
                triggered=self.create_2dstep_image,
                select_condition="always",
            )
            iah.new_action(
                _("Create ring image"),
                triggered=self.create_ring_image,
                select_condition="always",
            )

Exemple : plugin d’entrée/sortie#

Voici un exemple simple d’un plugin qui ajoute de nouveaux formats de fichiers à DataLab.

# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.

"""
Image file formats Plugin for DataLab
-------------------------------------

This plugin is an example of DataLab plugin.
It provides image file formats from cameras, scanners, and other acquisition devices.
"""

import struct

import numpy as np

from cdl.core.io.base import FormatInfo
from cdl.core.io.image.base import ImageFormatBase

# ==============================================================================
# Thales Pixium FXD file format
# ==============================================================================


class FXDFile:
    """Class implementing Thales Pixium FXD Image file reading feature

    Args:
        fname (str): path to FXD file
        debug (bool): debug mode
    """

    HEADER = "<llllllffl"

    def __init__(self, fname: str = None, debug: bool = False) -> None:
        self.__debug = debug
        self.file_format = None  # long
        self.nbcols = None  # long
        self.nbrows = None  # long
        self.nbframes = None  # long
        self.pixeltype = None  # long
        self.quantlevels = None  # long
        self.maxlevel = None  # float
        self.minlevel = None  # float
        self.comment_length = None  # long
        self.fname = None
        self.data = None
        if fname is not None:
            self.load(fname)

    def __repr__(self) -> str:
        """Return a string representation of the object"""
        info = (
            ("Image width", f"{self.nbcols:d}"),
            ("Image Height", f"{self.nbrows:d}"),
            ("Frame number", f"{self.nbframes:d}"),
            ("File format", f"{self.file_format:d}"),
            ("Pixel type", f"{self.pixeltype:d}"),
            ("Quantlevels", f"{self.quantlevels:d}"),
            ("Min. level", f"{self.minlevel:f}"),
            ("Max. level", f"{self.maxlevel:f}"),
            ("Comment length", f"{self.comment_length:d}"),
        )
        desc_len = max(len(d) for d in list(zip(*info))[0]) + 3
        res = ""
        for description, value in info:
            res += ("{:" + str(desc_len) + "}{}\n").format(description + ": ", value)

        res = object.__repr__(self) + "\n" + res
        return res

    def load(self, fname: str) -> None:
        """Load header and image pixel data

        Args:
            fname (str): path to FXD file
        """
        with open(fname, "rb") as data_file:
            header_s = struct.Struct(self.HEADER)
            record = data_file.read(9 * 4)
            unpacked_rec = header_s.unpack(record)
            (
                self.file_format,
                self.nbcols,
                self.nbrows,
                self.nbframes,
                self.pixeltype,
                self.quantlevels,
                self.maxlevel,
                self.minlevel,
                self.comment_length,
            ) = unpacked_rec
            if self.__debug:
                print(unpacked_rec)
                print(self)
            data_file.seek(128 + self.comment_length)
            if self.pixeltype == 0:
                size, dtype = 4, np.float32
            elif self.pixeltype == 1:
                size, dtype = 2, np.uint16
            elif self.pixeltype == 2:
                size, dtype = 1, np.uint8
            else:
                raise NotImplementedError(f"Unsupported pixel type: {self.pixeltype}")
            block = data_file.read(self.nbrows * self.nbcols * size)
        data = np.frombuffer(block, dtype=dtype)
        self.data = data.reshape(self.nbrows, self.nbcols)


class FXDImageFormat(ImageFormatBase):
    """Object representing Thales Pixium (FXD) image file type"""

    FORMAT_INFO = FormatInfo(
        name="Thales Pixium",
        extensions="*.fxd",
        readable=True,
        writeable=False,
    )

    @staticmethod
    def read_data(filename: str) -> np.ndarray:
        """Read data and return it

        Args:
            filename (str): path to FXD file

        Returns:
            np.ndarray: image data
        """
        fxd_file = FXDFile(filename)
        return fxd_file.data


# ==============================================================================
# Dürr NDT XYZ file format
# ==============================================================================


class XYZImageFormat(ImageFormatBase):
    """Object representing Dürr NDT XYZ image file type"""

    FORMAT_INFO = FormatInfo(
        name="Dürr NDT",
        extensions="*.xyz",
        readable=True,
        writeable=True,
    )

    @staticmethod
    def read_data(filename: str) -> np.ndarray:
        """Read data and return it

        Args:
            filename (str): path to XYZ file

        Returns:
            np.ndarray: image data
        """
        with open(filename, "rb") as fdesc:
            cols = int(np.fromfile(fdesc, dtype=np.uint16, count=1)[0])
            rows = int(np.fromfile(fdesc, dtype=np.uint16, count=1)[0])
            arr = np.fromfile(fdesc, dtype=np.uint16, count=cols * rows)
            arr = arr.reshape((rows, cols))
        return np.fliplr(arr)

    @staticmethod
    def write_data(filename: str, data: np.ndarray) -> None:
        """Write data to file

        Args:
            filename: File name
            data: Image array data
        """
        data = np.fliplr(data)
        with open(filename, "wb") as fdesc:
            fdesc.write(np.array(data.shape[1], dtype=np.uint16).tobytes())
            fdesc.write(np.array(data.shape[0], dtype=np.uint16).tobytes())
            fdesc.write(data.tobytes())

Autres exemples#

D’autres exemples de plugins peuvent être trouvés dans le répertoire plugins/examples du code source de DataLab (explorez ici sur GitHub).

API publique#

Système de plugins de DataLab#

Le système de plugins de DataLab fournit un moyen d’étendre l’application avec de nouvelles fonctionnalités.

Les plugins sont des modules Python qui reposent sur deux classes :

  • PluginInfo, qui stocke des informations sur le plugin

  • PluginBase, qui est la classe de base pour tous les plugins

Les plugins peuvent également étendre les fonctionnalités d’entrée/sortie de DataLab en fournissant de nouveaux formats d’image ou de signal. Pour ce faire, ils doivent fournir une sous-classe de ImageFormatBase ou SignalFormatBase, dans laquelle les informations de format sont définies à l’aide de la classe FormatInfo.

class cdl.plugins.PluginRegistry(name, bases, attrs)#

Métaclasse pour l’enregistrement des plugins

classmethod get_plugin_classes() list[type[PluginBase]]#

Retourne les classes de plugins

classmethod get_plugins() list[PluginBase]#

Retourne les instances de plugins

classmethod get_plugin(name_or_class: str | type[PluginBase]) PluginBase | None#

Retourne l’instance de plugin

classmethod register_plugin(plugin: PluginBase)#

Enregistrer le plugin

classmethod unregister_plugin(plugin: PluginBase)#

Désenregistrer le plugin

classmethod get_plugin_infos(html: bool = True) str#

Retourne les informations sur les plugins (noms, versions, descriptions) au format html

Paramètres:

html – retourner du texte formaté en html (par défaut : True)

class cdl.plugins.PluginInfo(name: str = None, version: str = '0.0.0', description: str = '', icon: str = None)#

Informations sur le plugin

class cdl.plugins.PluginBaseMeta(name, bases, namespace, /, **kwargs)#

Métaclasse mixte pour éviter les conflits

class cdl.plugins.PluginBase#

Classe de base du plugin

property signalpanel: SignalPanel#

Retourne le panneau de signal

property imagepanel: ImagePanel#

Retourne le panneau d’image

show_warning(message: str)#

Afficher un message d’avertissement

show_error(message: str)#

Afficher un message d’erreur

show_info(message: str)#

Afficher un message d’information

ask_yesno(message: str, title: str | None = None, cancelable: bool = False) bool#

Poser une question oui/non

edit_new_signal_parameters(title: str | None = None, size: int | None = None, hide_signal_type: bool = True) NewSignalParam#

Créer et éditer un nouveau jeu de paramètres de signal

Paramètres:
  • title – titre du nouveau signal

  • size – taille du nouveau signal (par défaut : None, obtenue à partir du signal actuel)

  • hide_signal_type – masquer le paramètre de type de signal (par défaut : True)

Renvoie:

Nouveau jeu de paramètres de signal (ou None si annulé)

edit_new_image_parameters(title: str | None = None, shape: tuple[int, int] | None = None, hide_image_type: bool = True, hide_image_dtype: bool = False) NewImageParam | None#

Créer et éditer un nouveau jeu de paramètres d’image

Paramètres:
  • title – titre de la nouvelle image

  • shape – dimensions de la nouvelle image (par défaut : None, obtenues à partir de l’image actuelle)

  • hide_image_type – masquer le paramètre de type d’image (par défaut : True)

  • hide_image_dtype – masquer le paramètre de type de données d’image (par défaut : False)

Renvoie:

Nouveau jeu de paramètres d’image (ou None si annulé)

is_registered()#

Retourne True si le plugin est enregistré

register(main: main.CDLMainWindow) None#

Enregistrer le plugin

unregister()#

Désenregistrer le plugin

register_hooks()#

Enregistrer les hooks du plugin

unregister_hooks()#

Désenregistrer les hooks du plugin

abstract create_actions()#

Créer des actions

cdl.plugins.discover_plugins() list[type[PluginBase]]#

Découvrir les plugins en utilisant la convention de nommage

Renvoie:

Liste des plugins découverts (en tant que classes)

cdl.plugins.get_available_plugins() list[PluginBase]#

Instancier et obtenir les plugins disponibles

Renvoie:

Liste des plugins disponibles (en tant qu’instances)