Feature Container

navigate includes a feature container that enables reconfigurable acquisition and analysis workflows. The feature container runs a tree of features, where each feature may perform a signal operation, which modifies the state of the microscope’s hardware, or a data operation, where it performs an analysis on acquired image data, or both.

Once a feature is executed, any features dependent on this feature’s execution will execute (for example, move the stage, then snap a picture). Following this, the next set of features in sequence will be executed.

Examples of some existing features include navigate.model.features.common_features.ZStackAcquisition, which acquires a z-stack, and navigate.model.features.autofocus.Autofocus, which finds the ideal plane of focus of a sample using a discrete cosine transform.


Currently Implemented Features


Feature Objects

Each feature is an object that accepts a pointer to navigate.model.model in its __init__() arguments and contains a configuration dictionary that dictates feature behavior in its __init__() function. A complete configuration dictionary is shown below. As few or as many of these options can be specified as needed. Each function is considered a leaf or node of the feature tree.

self.config_table = {'signal': {'init': self.pre_func_signal,
                                'main': self.in_func_signal,
                                'end': self.end_func_signal,
                                'cleanup': self.cleanup_func_signal},
                         'data': {'init': self.pre_func_data,
                                  'main': self.in_func_data,
                                  'end': self.end_func_data,
                                  'cleanup': self.cleanup_func_data},
                         'node': {'node_type': 'multi-step',
                                  'device_related': True,
                                  'need_response': True },
                        }

Both signal and data configuration entries are themselves dictionaries that can contain init, main, end and/or cleanup entries.

  • init entries dictate pre-processing steps that must be run before the main function of the feature starts.

  • main entries dictate the primary operation of the feature, and are run once per acquisition step. They return True if the acquisition should proceed and False if the acquisition should be ended.

  • end entries are run once per main function returning True. They check if the acquisition should end, if we are at any boundary points of the main function (e.g. if we need to change positions in a multi-position z-stack acquisition), and describe any closing operations that must be performed when exiting the feature

  • cleanup entries dictate what happens if the node fails. This is for failsafe controls such as “turn off all lasers”.

The node configuration dictionary contains general properties of feature nodes. node_type can be one-step or multi-step, the latter indicating we have an init, a main and an end. device_related is set to True if we have a multi-step signal container. need_response is set to true if the signal node waits on hardware (e.g. waits for a stage to confirm it has indeed moved) before proceeding.

Each of the functions that are the value entries in self.config_table dictionaries are methods of the feature object.


Creating A Custom Feature Object

Each feature object is defined as a class. Creating a new feature is the same as creating any Python class, but with a few requirements. The first parameter of the __init__ function (after self) must be model, which gives the feature object full access to the navigate model. All the other parameters are keyword arguments and must have default values. The __init__ function should always have a config_table attribute (see above for a description of the config_table).

In the example below, we will create a custom feature that moves to a specified position in navigate’s multi-position table and calculates the sharpness of the image at this position using the Normalized DCT Shannon Entropy metric. An example __init__() function for our FeatureExample class is below.

from navigate.model.analysis.image_contrast import fast_normalized_dct_shannon_entropy

class FeatureExample:

    def __init__(self, model, position_id=0):
        self.model = model
        self.position_id = position_id

        self.config_table = {
            "signal": {
                    "init": self.pre_func_signal,
                    "main": self.in_func_signal,
            },
            "data": {
                    "main": self.in_func_data,
            },
            "node": {
                "device_related": True,
            }
        }
  1. Get multi-position table position from the GUI.

    All the GUI parameters are stored in model.configuration["experiment"] during runtime. Below, we create a function to get all the position stored at position_id from the multi-position table in the GUI when we launch our feature.

    def pre_func_signal(self):
        positions = self.model.configuration["experiment"]["MultiPositions"]
        if self.position_id < len(positions):
            self.target_position = positions[self.position_id]
        else:
            current_position = self.model.get_stage_position()
            self.target_position = dict([(axis[:-4], value) for axis, value in current_position.items()])
    

    More GUI parameters can be found in experiment.yml

  2. Use the stage to move to this position.

    Now, we move stage to the target_position we grabbed from the multi-position table.

    def in_func_signal(self):
        pos = dict([(f"{axis}_abs", value) for axis, value in self.target_position.items()])
        self.model.move_stage(pos, wait_until_done=True)
    
  3. Take a picture and process the resulting image.

In parallel with our signal function call, the camera will acquire an image. The image captured by the camera will be stored in the model data_buffer. The data functions run after an image is acquired. We add code to deal with this image in the "main" data function. Here, we calculate the Shannon entropy of the image.

def in_func_data(self, frame_ids):
    for id in frame_ids:
        image = self.model.data_buffer[id]
        entropy = fast_normalized_dct_shannon_entropy(image,
            psf_support_diameter_xy=3)
        print("entropy of image:", id, entropy)

Now, we’ve create a whole new feature and can use it as we wish.

How to interact with other devices

We interact with all devices through self.model.active_microscope. Here is an example to open shutter:

self.model.active_microscope.shutter.open_shutter()

How to pause and resume data threads in the model

The image data acquired from the camera are handled in an independent thread. As such, the signal and data operations by default run in parallel and do not block each other. Sometimes, we want to be sure a device is ready or has moved. For example, in FeatureExample, we have no guarantee that the stage finished moving before the image was taken. The wait_until_done call only blocks the signal thread from progressing before the stage finishes its move. To ensure the data thread also waits, we need to pause the data thread until the stage is ready.

Here is an example of how we can pause and resume the data thread:

self.model.pause_data_thread()

self.model.move_stage(pos, wait_until_done=True)
# ...

self.model.resume_data_thread()

We can of course replace self.model.move_stage(pos, wait_until_done=True) with whatever task we want to wait for before resuming image acquisition.

Model functions can be found in the API.

Custom Feature Lists

The navigate software allows you to chain feature objects into lists to build acquisition workflows.

Creating a Custom Feature List in Python

To create a customized feature list, follow these steps:

  1. Import the necessary modules:

from navigate.tools.decorators import FeatureList
from navigate.model.features.feature_related_functions import *

FeatureList is a decorator that registers the list of features. feature_related_functions contains convenience imports that allow us to call PrepareNextChannel instead of navigate.model.features.common_features.PrepareNextChannel. These functions make for more readable code.

  1. Create the feature list.

@FeatureList
def feature_example():
    return [
        (
            {"name": PrepareNextChannel},
            {
                "name": LoopByCount,
                "args": ("channels",),
            },
        )
    ]

In this example, the feature list takes one image per selected channel in the GUI. PrepareNextChannel sets up the channel and LoopByCount call this setup once per selected channel.

  1. Now, open navigate.

  2. Go to the Features menu.

    ../_images/step_1.png
  3. Import the customized feature. Select Add Custom Feature List from the Features menu. A dialog box will appear, allowing you to select the Python file containing your customized feature list function.

    ../_images/step_2.png
  4. Choose the Python file containing your customized feature list function. navigate will load the specified feature list, making it available for use in your experiments and analyses. It will appear at the bottom of the Features menu.