Plugins

Lenscraft functionality can be extended with plugins. Plugins are primarily intended to add custom nodes but there are a few more extension points and more will be added over time.

This is the basic anatomy of a plugin

from lenscraft.plugin import LenscraftPlugin

plugin = LenscraftPlugin()

plugin.register_node(...
plugin.register_image_loader(...
plugin.register_camera(...

Manual Plugin Registration

To run Lenscraft with your plugin use following command:

lenscraft --plugin example_plugin.plugin

Lenscraft will dynamically import the module and look for a Plugin instance called plugin.

If you want to use a different name you can use

lenscraft --plugin example_plugin.plugin:my_plugin

Plugin Discovery

Lenscraft will automatically try to import plugins from any module that starts with lenscraft_

So, if you create a directory called lenscraft_plugin in the root of your project and add a __init__.py with the following code, Lenscraft will automatically load that plugin.

from lenscraft.node import Node, NodeInput, NodeOutput, NumberValue
from lenscraft.plugin import LenscraftPlugin


plugin = LenscraftPlugin()

Extension Points

Custom Nodes

This is how you can implement a basic node.


@plugin.node()
class AddTwoNumbers(Node):
    def setup(self):
        self.add_input("A", IntSlot())
        self.add_input("B", IntSlot())

        self.add_output("Result", Number)

    def compute(self):
        a = self.get_input_value("A")
        b = self.get_input_value("B")

        self.set_output_value("Result", a + b)

The first argument to add_input is the input name. Used in the UI and in get_input_value below.

The second argument to add_input is a Slot instance. Slots are part of the type system in Lenscraft and is how we specify what type of value the node expects and can enforce constraints on those values. More about Slots in the type system section.

Here is a list of the most used slot types:

  • ImageSlot
  • NumberInRange
  • EnumSlot
  • BoolSlot
  • IntSlot
  • OddIntSlot
  • FloatSlot
  • StringSlot
  • FilePath
  • FolderPath

Custom Image Loader

By default Lenscraft will use PIL.Image.open to load images. This will cover most standard image formats. If you need to work with more obscure file formats, it is possible to add a custom ImageLoader.

This example shows how you could load image data from a .dm4 file, commonly used for electron microscopy. The ncempy library is doing the heavy lifting for us. The plugin just associates the custom loader with the dm4 file extension. Now Lenscraft will call this loader for all file paths that have the .dm4 extension.

Note that is it possible to override ImageLoaders. Lenscraft will use the latest one it found for each file extension. If you add a .png ImageLoader, that will replace the default PIL implementation.

import ncempy.io as nio
from lenscraft.core.io import ImageLoader, Image
from lenscraft.plugin import LenscraftPlugin

plugin = LenscraftPlugin()

class DM4Loader(ImageLoader):
    def load_image(self, path):

        # Load the DM4 file
        dmData = nio.read(path)

        # Extract the image data
        image = dmData['data']

        return Image(path, image)

plugin.register_image_loader(DM4Loader(), ".dm4")

Custom Camera

Lenscraft can show you a live preview from a USB or built-in webcam, but you can also add a custom camera implementation. In this context, a camera is just a class that can produce a sequence of images.

For example, lets say we want to load a sequence of frames from a directory.

class FolderCamera(Camera):
    def __init__(self, folder: str, loop: bool = True):
        self.folder = Path(folder)
        self.loop = loop

    def start(self):
        self.index = 0
        self.started = True
        self.images = self._load_images(self.folder)

    def read(self):
        if self.index >= len(self.images):
            if self.loop:
                self.index = 0
            else:
                return None

        path = self.images[self.index]
        frame = cv2.imread(path)
        self.index += 1
        return frame

    def release(self):
        self.started = False

    @classmethod
    def setup_form(cls) -> Optional[Form]:
        return (
            Form(
                title="Folder Camera",
                description="Loads a sequence of image files from a folder"
            )
            .add_field("folder", FolderPath(), label="Image Folder Path")
            .add_field("loop", BoolSlot(True), label="Loop When Finished")
        )

start will alway be called before reading frames from the camera, thats where you can setup any resources you need. read will be called each frame to get the lates image data. It should return a numpy array release will be called at the end when the camera is closed. Thats where you should cleanup resources.

Implementing setup_form is optional but thats what allows you customize the camera configuration through the UI at runtime. If a form instance is returned, the user will be promoted with a dialog where parameters can be set.

The for field system is basically the same as the node input system. Slots are used to specify what type of value we expect. More about Slots in the type system section.

Note that the first argument in add_field matches the keyword arguments in the class __init__ method.