Creating Custom components
You may want to adapt one of the components to your needs, here are some hints on how to do this.
Custom Actuators
Actuators are seperated into the main class for basic parameters and control and Acquisition classes
responsible for a started acquisition. The actuator should at least be able to receive a signal from
an interpreter, here normally implemented in the Slot MMActuator.call_action().
Depending on the implementation of image recording, it might also be responsible to hand new
images over to the analyser. This is the case for example in the PycroAcquisition
class. The InjectedPycroAcquisition class used in the examples.main.pyro()
example is a subclass of the more basic actuators.pycromanager.PycroAcquisition with
the additional image injection.
class InjectedPycroAcquisition(PycroAcquisition):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
tif_file = "./180420_120_comp.tif"
self.frame_time = 0.15 # s
self.tif = tifffile.imread(tif_file)
self.start_time = time.perf_counter()
self.timepoint = 0
log.info(f"{tif_file} loaded with shape {self.tif.shape}")
In this example, the function PycroAcquisition.receive_image() of PycroAcquisition is overwritten to inject the
image. Later, the original PycroAcquisition.receive_image() is called by super().receive_image(image, metadata)
with the modified image.
def receive_image(self, image, metadata):
for idx, c in enumerate(self.channels):
if metadata["Channel"] == c["config"]:
channel = idx
if channel == 0:
now = time.perf_counter()
elapsed = now - self.start_time
timepoint = round(elapsed / self.frame_time)
timepoint = np.max([timepoint, self.timepoint + 1])
self.timepoint = np.mod(timepoint, self.tif.shape[0])
image = self.tif[self.timepoint, channel, :, :]
# Send this image to the main receive_image function of PycroAcquisition
return super().receive_image(image, metadata)
Additionally to the actuators that are based on Micro-Manager controlling the microscope, we also
implemented an actuator for our NI DAQ driven microscope actuators.daq.DAQActuator. This can be
a starting point for a very different implementation of an actuator.
An interesting application of a custom actuator could be one that additionally to the framerate
changes another parameter on the microscope. For this, the call_action function would be
overwritten to add functionality.
@Slot(float)
def call_action(self, interval):
# Do something intereseting
self.core.set_position(0)
# Call the original function
super().call_action(interval)
In the PycroAcquisition this behaviour could be even more intricate, as described in
the documentation (pycro-manager documentation)
Analysers
Analysers are seperated into a main QObject (e.g. analysers.image.ImageAnalyser) for the main settings and the interaction with the other
EDA components. They also start a QThreadpool, these threads are used to start asynchronous analysis
of the recorded images in a QRunnable (e.g. analysers.image.ImageAnalyserWorker). The
implementation using a thread pool also ensures, that the analysis keeps up with the acquisition, as
analysis tasks that can’t be handed to a free thread are skipped automatically.
In this role, analysers should be able to receive images and are responsible to send a new result to
the interpreter.
The KerasAnalyser using the model that was reported in the original paper is a subclass of the very basic
ImageAnalyser. It uses workers that are themselves subclasses of ImageAnalyserWorker.
examples.analysers shows two workers that modify the pre and post processing of the images.
To accomodate workers with potentially different inputs and signals, the functions ImageAnalyser._get_worker_args()
and ImageAnalyser.connect_worker_signals() can be overwritten seperately without having to rewrite the whole
worker delivery logic.
See also Custom Neural Networks.
Interpreters
Interpreter implementations are responsible to translate the results of analysers to a specific
action for the actuator. They therefore have to receive the information from the analyser and send
the request for some action to the actuator. A very simple change would be to transform the binary
frame rate interpreter used in the examples to a continous one, that takes the value from the
analyser and just scales it to a delay time before forwarding it to the actuator. This can be
achieved by only overwritting the function that defines the new imaging speed from the value
received from the analyser. The rest of the class stays the same. This is
demonstrated in FrameRateInterpreter.
from eda_plugin.interpreters.frame_rate import BinaryFrameRateInterpreter
from eda_plugin.utility.event_bus import EventBus
class FrameRateInterpreter(BinaryFrameRateInterpreter):
def __init__(self, event_bus: EventBus, gui: bool = True):
super().__init__(event_bus, gui)
def _define_imaging_speed(self, new_value: float):
new_interval = new_value * 10
return new_interval