Prototyping a custom processing pipeline#
This example shows how to prototype a custom image processing pipeline using DataLab:
Define a custom processing function
Create a macro-command to apply the function to an image
Use the same code from an external IDE (e.g. Spyder) or a Jupyter notebook
Create a plugin to integrate the function in the DataLab GUI
Define a custom processing function#
For illustrating the extensibility of DataLab, we will use a simple image processing function that is not available in the standard DataLab distribution, and that represents a typical use case for prototyping a custom processing pipeline.
The function that we will work on is a denoising filter that combines the ideas of averaging and edge detection. This filter will average the pixel values in the neighborhood, but with a twist: it will give less weight to pixels that are significantly different from the central pixel, assuming they might be part of an edge or noise.
Here is the code of the weighted_average_denoise
function:
def weighted_average_denoise(data: np.ndarray) -> np.ndarray:
"""Apply a custom denoising filter to an image.
This filter averages the pixels in a 5x5 neighborhood, but gives less weight
to pixels that significantly differ from the central pixel.
"""
def filter_func(values: np.ndarray) -> float:
"""Filter function"""
central_pixel = values[len(values) // 2]
differences = np.abs(values - central_pixel)
weights = np.exp(-differences / np.mean(differences))
return np.average(values, weights=weights)
return spi.generic_filter(data, filter_func, size=5)
For testing our processing function, we will use a generated image from a DataLab plugin example (plugins/examples/cdl_example_imageproc.py). Before starting, make sure that the plugin is installed in DataLab (see the first steps of the tutorial Detecting blobs on an image).
Create a macro-command#
Letâs get back to our custom function. We can create a new macro-command that will apply the function to the current image. To do so, we open the âMacro Panelâ and click on the âNew macroâ button.
DataLab creates a new macro-command which is not empty: it contains a sample code that shows how to create a new image and add it to the âImage Panelâ. We can remove this code and replace it with our own code:
# Import the necessary modules
import numpy as np
import scipy.ndimage as spi
from cdl.proxy import RemoteProxy
# Define our custom processing function
def weighted_average_denoise(values: np.ndarray) -> float:
"""Apply a custom denoising filter to an image.
This filter averages the pixels in a 5x5 neighborhood, but gives less weight
to pixels that significantly differ from the central pixel.
"""
central_pixel = values[len(values) // 2]
differences = np.abs(values - central_pixel)
weights = np.exp(-differences / np.mean(differences))
return np.average(values, weights=weights)
# Initialize the proxy to DataLab
proxy = RemoteProxy()
# Switch to the "Image Panel" and get the current image
proxy.set_current_panel("image")
image = proxy.get_object()
if image is None:
# We raise an explicit error if there is no image to process
raise RuntimeError("No image to process!")
# Get a copy of the image data, and apply the function to it
data = np.array(image.data, copy=True)
data = spi.generic_filter(data, weighted_average_denoise, size=5)
# Add new image to the panel
proxy.add_image("My custom filtered data", data)
In DataLab, macro-commands are simply Python scripts:
Macros are part of DataLabâs workspace, which means that they are saved and restored when exporting and importing to/from an HDF5 file.
Macros are executed in a separate process, so we need to import the necessary modules and initialize the proxy to DataLab. The proxy is a special object that allows to communicate with DataLab.
As a consequence, when defining a plugin or when controlling DataLab from an external IDE, we can use exactly the same code as in the macro-command. This is a very important point, because it means that we can prototype our processing pipeline in DataLab, and then use the same code in a plugin or in an external IDE to develop it further.
Note
The macro-command is executed in DataLabâs Python environment, so we can use the modules that are available in DataLab. However, we can also use our own modules, as long as they are installed in DataLabâs Python environment or in a Python distribution that is compatible with DataLabâs Python environment.
If your custom modules are not installed in DataLabâs Python environment, and
if they are compatible with DataLabâs Python version, you can prepend the
sys.path
with the path to the Python distribution that contains your
modules:
import sys
sys.path.insert(0, "/path/to/my/python/distribution")
This will allow you to import your modules in the macro-command and mix them with the modules that are available in DataLab.
Warning
If you use this method, make sure that your modules are compatible with DataLabâs Python version. Otherwise, you will get errors when importing them.
Now, letâs execute the macro-command by clicking on the âRun macroâ button:
The macro-command is executed in a separate process, so we can continue to work in DataLab while the macro-command is running. And, if the macro-command takes too long to execute, we can stop it by clicking on the âStop macroâ button.
During the execution of the macro-command, we can see the progress in the âMacro Panelâ window: the process standard output is displayed in the âConsoleâ below the macro editor. We can see the following messages:
---[...]---[# ==> Running 'Untitled 01' macro...]
: the macro-command startsConnecting to DataLab XML-RPC server...OK [...]
: the proxy is connected to DataLab---[...]---[# <== 'Untitled 01' macro has finished]
: the macro-command ends
Prototyping with an external IDE#
Now that we have a working prototype of our processing pipeline, we can use the same code in an external IDE to develop it further.
For example, we can use the Spyder IDE to debug our code. To do so, we need to install Spyder but not necessarily in DataLabâs Python environment (in the case of the stand-alone version of DataLab, it wouldnât be possible anyway).
The only requirement is to install a DataLab client in Spyderâs Python environment:
If you use the stand-alone version of DataLab or if you want or need to keep DataLab and Spyder in separate Python environments, you can install the DataLab Simple Client (
cdl-client
) using thepip
package manager:pip install cdl-client
Or you may also install the DataLab Python package (
cdl
) which includes the client (but also other modules, so we donât recommend this method if you donât need all DataLabâs features in this Python environment):pip install cdl
If you use the DataLab Python package, you may run Spyder in the same Python environment as DataLab, so you donât need to install the client: it is already available in the main DataLab package (the
cdl
package).
Once the client is installed, we can start Spyder and create a new Python script:
1# -*- coding: utf-8 -*-
2"""
3Example of remote control of DataLab current session,
4from a Python script running outside DataLab (e.g. in Spyder)
5
6Created on Fri May 12 12:28:56 2023
7
8@author: p.raybaut
9"""
10
11# %% Importing necessary modules
12
13import numpy as np
14import scipy.ndimage as spi
15from cdlclient import SimpleRemoteProxy
16
17# %% Connecting to DataLab current session
18
19proxy = SimpleRemoteProxy()
20proxy.connect()
21
22# %% Executing commands in DataLab (...)
23
24
25# Define our custom processing function
26def weighted_average_denoise(data: np.ndarray) -> np.ndarray:
27 """Apply a custom denoising filter to an image.
28
29 This filter averages the pixels in a 5x5 neighborhood, but gives less weight
30 to pixels that significantly differ from the central pixel.
31 """
32
33 def filter_func(values: np.ndarray) -> float:
34 """Filter function"""
35 central_pixel = values[len(values) // 2]
36 differences = np.abs(values - central_pixel)
37 weights = np.exp(-differences / np.mean(differences))
38 return np.average(values, weights=weights)
39
40 return spi.generic_filter(data, filter_func, size=5)
41
42
43# Switch to the "Image Panel" and get the current image
44proxy.set_current_panel("image")
45image = proxy.get_object()
46if image is None:
47 # We raise an explicit error if there is no image to process
48 raise RuntimeError("No image to process!")
49
50# Get a copy of the image data, and apply the function to it
51data = np.array(image.data, copy=True)
52data = weighted_average_denoise(data)
53
54# Add new image to the panel
55proxy.add_image("Filtered using Spyder", data)
Prototyping with a Jupyter notebook#
We can also use a Jupyter notebook to prototype our processing pipeline. To do so, we need to install Jupyter but not necessarily in DataLabâs Python environment (in the case of the stand-alone version of DataLab, it wouldnât be possible anyway).
The only requirement is to install a DataLab client in Jupyterâs Python environment (see the previous section for more details: that is exactly the same procedure as for Spyder or any other IDE like Visual Studio Code, for example).
Creating a plugin#
Now that we have a working prototype of our processing pipeline, we can create a plugin to integrate it in DataLabâs GUI. To do so, we need to create a new Python module that will contain the plugin code. We can use the same code as in the macro-command, but we need to make some changes.
See also
The plugin system is described in the Plugins section.
Apart from integrating the feature to DataLabâs GUI which is more convenient for the user, the advantage of creating a plugin is that we can take benefit of the DataLab infrastructure, if we encapsulate our processing function in a certain way (see below):
Our function will be executed in a separate process, so we can interrupt it if it takes too long to execute.
Warnings and errors will be handled by DataLab, so we donât need to handle them ourselves.
The most significant change is that we need to define a function that will be
operating on DataLabâs native image objects (cdl.obj.ImageObj
), instead of
operating on NumPy arrays. So we need to find a way to call our custom function
weighted_average_denoise
with a cdl.obj.ImageObj
as input and output.
To avoid writing a lot of boilerplate code, we can use the function wrapper provided
by DataLab: cdl.computation.image.Wrap11Func
.
Besides we need to define a class that describes our plugin, which must inherit
from cdl.plugins.PluginBase
and name the Python script that contains the
plugin code with a name that starts with cdl_
(e.g. cdl_custom_func.py
), so
that DataLab can discover it at startup.
Moreover, inside the plugin code, we want to add an entry in the âPluginsâ menu, so that the user can access our plugin from the GUI.
Here is the plugin code:
1# -*- coding: utf-8 -*-
2
3"""
4Custom denoising filter plugin
5==============================
6
7This is a simple example of a DataLab image processing plugin.
8
9It is part of the DataLab custom function tutorial.
10
11.. note::
12
13 This plugin is not installed by default. To install it, copy this file to
14 your DataLab plugins directory (see `DataLab documentation
15 <https://datalab-platform.com/en/features/general/plugins.html>`_).
16"""
17
18import numpy as np
19import scipy.ndimage as spi
20
21import cdl.computation.image as cpi
22import cdl.obj
23import cdl.param
24import cdl.plugins
25
26
27def weighted_average_denoise(data: np.ndarray) -> np.ndarray:
28 """Apply a custom denoising filter to an image.
29
30 This filter averages the pixels in a 5x5 neighborhood, but gives less weight
31 to pixels that significantly differ from the central pixel.
32 """
33
34 def filter_func(values: np.ndarray) -> float:
35 """Filter function"""
36 central_pixel = values[len(values) // 2]
37 differences = np.abs(values - central_pixel)
38 weights = np.exp(-differences / np.mean(differences))
39 return np.average(values, weights=weights)
40
41 return spi.generic_filter(data, filter_func, size=5)
42
43
44class CustomFilters(cdl.plugins.PluginBase):
45 """DataLab Custom Filters Plugin"""
46
47 PLUGIN_INFO = cdl.plugins.PluginInfo(
48 name="My custom filters",
49 version="1.0.0",
50 description="This is an example plugin",
51 )
52
53 def create_actions(self) -> None:
54 """Create actions"""
55 acth = self.imagepanel.acthandler
56 proc = self.imagepanel.processor
57 with acth.new_menu(self.PLUGIN_INFO.name):
58 for name, func in (("Weighted average denoise", weighted_average_denoise),):
59 # Wrap function to handle ``ImageObj`` objects instead of NumPy arrays
60 wrapped_func = cpi.Wrap11Func(func)
61 acth.new_action(
62 name, triggered=lambda: proc.compute_11(wrapped_func, title=name)
63 )
To test it, we have to add the plugin script to one of the plugin directories that are discovered by DataLab at startup (see the Plugins section for more details, or the Detecting blobs on an image for an example).