Plugins#
DataLab supports a robust plugin architecture, allowing users to extend the application’s features without modifying its core. Plugins can introduce new processing tools, data import/export formats, or custom GUI elements — all seamlessly integrated into the platform.
What is a plugin?#
A plugin is a Python module that is automatically loaded by DataLab at startup. It can define new features or modify existing ones.
To be recognized as a plugin, the file must:
Be a Python module whose name starts with
cdl_
(e.g.cdl_myplugin.py
),Contain a class that inherits from
cdl.plugins.PluginBase
,Include a class attribute named
PLUGIN_INFO
, which must be an instance ofcdl.plugins.PluginInfo
.
This PLUGIN_INFO object is used by DataLab to retrieve metadata such as the plugin name, type, and menu integration.
Note
Only Python files whose names start with cdl_
will be scanned for plugins.
DataLab supports three categories of plugins, each with its own purpose and registration mechanism:
Processing and visualization plugins Add custom actions for signal or image processing. These may include new computation functions, data visualization tools, or interactive dialogs. Integrated into a dedicated submenu of the “Plugins” menu.
Input/Output plugins Define new file formats (read and/or write) handled transparently by DataLab’s I/O framework. These plugins extend compatibility with custom or third-party data formats.
HDF5 plugins Special plugins that support HDF5 files with domain-specific tree structures. These allow DataLab to interpret signals or images organized in non-standard ways.
Where to put a plugin?#
Plugins are automatically discovered at startup from multiple locations:
The user plugin directory: Typically ~/.DataLab/plugins on Linux/macOS or C:/Users/YourName/.DataLab/plugins on Windows.
A custom plugin directory: Configurable in DataLab’s preferences.
The standalone distribution directory: If using a frozen (standalone) build, the plugins folder located next to the executable is scanned.
The internal cdl/plugins folder (not recommended for user plugins): This location is reserved for built-in or bundled plugins and should not be modified manually.
How to develop a plugin?#
The recommended approach to developing a plugin is to derive from an existing example and adapt it to your needs. You can explore the source code in the cdl/plugins folder or refer to community-contributed examples.
To develop in your usual Python environment (e.g., with an IDE like Spyder), you can:
Install DataLab in your Python environment, using one of the following methods:
Or add the `cdl` package manually to your Python path:
Download the source from the PyPI page,
Unzip the archive,
Add the cdl directory to your PYTHONPATH (e.g., using the PYTHONPATH Manager in Spyder).
Note
Even if you’ve installed cdl in your environment, you cannot run the full DataLab application directly from an IDE. You must launch DataLab via the command line or using the installer-created shortcut to properly test your plugin.
Example: processing plugin#
Here is a minimal example of a plugin that prints a message when activated:
# 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.computation import image as cpima
from cdl.computation import signal as cpsig
from cdl.config import _
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.main):
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.main):
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_height=True, hide_image_type=True
)
if newparam is not None:
obj = test_data.create_noisygauss_image(newparam, add_annotations=False)
self.proxy.add_object(obj)
def create_multigauss_image(self) -> None:
"""Create 2D multi gauss image"""
newparam = self.edit_new_image_parameters(
hide_image_height=True, 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.main):
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",
)
Example: input/output plugin#
Here is a simple example of a plugin that adds a new file formats to 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())
Other examples#
Other examples of plugins can be found in the plugins/examples directory of the DataLab source code (explore here on GitHub).
Public API#
DataLab plugin system#
DataLab plugin system provides a way to extend the application with new functionalities.
Plugins are Python modules that relies on two classes:
PluginInfo
, which stores information about the plugin
PluginBase
, which is the base class for all plugins
Plugins may also extends DataLab I/O features by providing new image or
signal formats. To do so, they must provide a subclass of ImageFormatBase
or SignalFormatBase
, in which format infos are defined using the
FormatInfo
class.
- class cdl.plugins.PluginRegistry(name, bases, attrs)[source]#
Metaclass for registering plugins
- classmethod get_plugin_classes() list[type[PluginBase]] [source]#
Return plugin classes
- classmethod get_plugins() list[PluginBase] [source]#
Return plugin instances
- classmethod get_plugin(name_or_class: str | type[PluginBase]) PluginBase | None [source]#
Return plugin instance
- classmethod register_plugin(plugin: PluginBase)[source]#
Register plugin
- classmethod unregister_plugin(plugin: PluginBase)[source]#
Unregister plugin
- class cdl.plugins.PluginInfo(name: str = None, version: str = '0.0.0', description: str = '', icon: str = None)[source]#
Plugin info
- class cdl.plugins.PluginBaseMeta(name, bases, namespace, /, **kwargs)[source]#
Mixed metaclass to avoid conflicts
- class cdl.plugins.PluginBase[source]#
Plugin base class
- property signalpanel: SignalPanel#
Return signal panel
- property imagepanel: ImagePanel#
Return image panel
- ask_yesno(message: str, title: str | None = None, cancelable: bool = False) bool [source]#
Ask yes/no question
- edit_new_signal_parameters(title: str | None = None, size: int | None = None, hide_signal_type: bool = True) NewSignalParam [source]#
Create and edit new signal parameter dataset
- Parameters:
title – title of the new signal
size – size of the new signal (default: None, get from current signal)
hide_signal_type – hide signal type parameter (default: True)
- Returns:
New signal parameter dataset (or None if canceled)
- edit_new_image_parameters(title: str | None = None, shape: tuple[int, int] | None = None, hide_image_height: bool = False, hide_image_type: bool = True, hide_image_dtype: bool = False) NewImageParam | None [source]#
Create and edit new image parameter dataset
- Parameters:
title – title of the new image
shape – shape of the new image (default: None, get from current image)
hide_image_height – hide image heigth parameter (default: False)
hide_image_type – hide image type parameter (default: True)
hide_image_dtype – hide image data type parameter (default: False)
- Returns:
New image parameter dataset (or None if canceled)
- register(main: main.CDLMainWindow) None [source]#
Register plugin
- cdl.plugins.discover_plugins() list[type[PluginBase]] [source]#
Discover plugins using naming convention
- Returns:
List of discovered plugins (as classes)
- cdl.plugins.get_available_plugins() list[PluginBase] [source]#
Instantiate and get available plugins
- Returns:
List of available plugins (as instances)